diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 3fe5f9472d..033e1ddb70 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -1032,6 +1032,8 @@ const allFilterModeOptions: Array<{ value: FilterMode; labelKey: string }> = [ { value: "not-equals", labelKey: "grid.filterBuilderNotEquals" }, { value: "like", labelKey: "grid.filterBuilderContains" }, { value: "not-like", labelKey: "grid.filterBuilderNotContains" }, + { value: "begins-with", labelKey: "grid.filterBuilderBeginsWith" }, + { value: "ends-with", labelKey: "grid.filterBuilderEndsWith" }, { value: "greater-than", labelKey: "grid.filterBuilderGreaterThan" }, { value: "greater-than-or-equal", diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 00fafd6921..f8d299a4fe 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -1711,6 +1711,8 @@ export default { filterBuilderNotEquals: "Does not equal", filterBuilderContains: "Contains", filterBuilderNotContains: "Does not contain", + filterBuilderBeginsWith: "Starts with", + filterBuilderEndsWith: "Ends with", filterBuilderGreaterThan: "Greater than", filterBuilderGreaterThanOrEqual: "Greater than or equal to", filterBuilderLessThan: "Less than", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index b9ae2e9f70..c68f98dbf8 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -1594,6 +1594,8 @@ export default withEnglishFallback({ filterBuilderNotEquals: "Distinto de", filterBuilderContains: "Contiene", filterBuilderNotContains: "No contiene", + filterBuilderBeginsWith: "Empieza por", + filterBuilderEndsWith: "Termina por", filterBuilderGreaterThan: "Mayor que", filterBuilderGreaterThanOrEqual: "Mayor o igual que", filterBuilderLessThan: "Menor que", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index 9849a53b8c..9d9bddfc75 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -1592,6 +1592,8 @@ export default withEnglishFallback({ filterBuilderNotEquals: "Diverso da", filterBuilderContains: "Contiene", filterBuilderNotContains: "Non contiene", + filterBuilderBeginsWith: "Inizia con", + filterBuilderEndsWith: "Termina con", filterBuilderGreaterThan: "Maggiore di", filterBuilderGreaterThanOrEqual: "Maggiore o uguale a", filterBuilderLessThan: "Minore di", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 5022a698f1..3f99df7caa 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -1610,6 +1610,8 @@ export default withEnglishFallback({ filterBuilderNotEquals: "等しくない", filterBuilderContains: "含む", filterBuilderNotContains: "含まない", + filterBuilderBeginsWith: "で始まる", + filterBuilderEndsWith: "で終わる", filterBuilderGreaterThan: "より大きい", filterBuilderGreaterThanOrEqual: "以上", filterBuilderLessThan: "より小さい", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 3b65592839..ffb41270f2 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -1587,6 +1587,8 @@ export default withEnglishFallback({ filterBuilderNotEquals: "같지 않음", filterBuilderContains: "포함", filterBuilderNotContains: "포함하지 않음", + filterBuilderBeginsWith: "…으로 시작", + filterBuilderEndsWith: "…으로 끝", filterBuilderGreaterThan: "보다 큼", filterBuilderGreaterThanOrEqual: "크거나 같음", filterBuilderLessThan: "보다 작음", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index ceff64fff0..f40d0c5c16 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -1594,6 +1594,8 @@ export default withEnglishFallback({ filterBuilderNotEquals: "Diferente de", filterBuilderContains: "Contém", filterBuilderNotContains: "Não contém", + filterBuilderBeginsWith: "Começa com", + filterBuilderEndsWith: "Termina com", filterBuilderGreaterThan: "Maior que", filterBuilderGreaterThanOrEqual: "Maior ou igual a", filterBuilderLessThan: "Menor que", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index b9746b9a8f..5bffae02fc 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -1636,6 +1636,8 @@ export default withEnglishFallback({ filterBuilderNotEquals: "不等于", filterBuilderContains: "包含", filterBuilderNotContains: "不包含", + filterBuilderBeginsWith: "以…开始", + filterBuilderEndsWith: "以…结束", filterBuilderGreaterThan: "大于", filterBuilderGreaterThanOrEqual: "大于等于", filterBuilderLessThan: "小于", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 12a6c815b3..590b531fb1 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -1593,6 +1593,8 @@ export default withEnglishFallback({ filterBuilderNotEquals: "不等於", filterBuilderContains: "包含", filterBuilderNotContains: "不包含", + filterBuilderBeginsWith: "以…開始", + filterBuilderEndsWith: "以…結束", filterBuilderGreaterThan: "大於", filterBuilderGreaterThanOrEqual: "大於等於", filterBuilderLessThan: "小於", diff --git a/apps/desktop/src/lib/__tests__/dataGrid/dataGridColumnFilter.victoriametrics.spec.ts b/apps/desktop/src/lib/__tests__/dataGrid/dataGridColumnFilter.victoriametrics.spec.ts index 66bf4d38f3..2e91487987 100644 --- a/apps/desktop/src/lib/__tests__/dataGrid/dataGridColumnFilter.victoriametrics.spec.ts +++ b/apps/desktop/src/lib/__tests__/dataGrid/dataGridColumnFilter.victoriametrics.spec.ts @@ -4,7 +4,7 @@ import type { DataGridContextFilterMode } from "@/lib/dataGrid/dataGridSql"; describe("VictoriaMetrics data grid filters", () => { it("does not expose SQL filter modes for MetricsQL results", () => { - const modes: DataGridContextFilterMode[] = ["equals", "not-equals", "is-null", "is-not-null", "is-blank", "is-not-blank", "like", "not-like", "less-than", "greater-than", "in", "not-in", "between", "not-between"]; + const modes: DataGridContextFilterMode[] = ["equals", "not-equals", "is-null", "is-not-null", "is-blank", "is-not-blank", "like", "not-like", "begins-with", "ends-with", "less-than", "greater-than", "in", "not-in", "between", "not-between"]; expect(modes.every((mode) => !filterModeIsSupportedForDatabase(mode, "victoriametrics"))).toBe(true); expect(filterModeIsSupportedForDatabase("equals", "postgres")).toBe(true); diff --git a/apps/desktop/src/lib/dataGrid/columnFormatter.ts b/apps/desktop/src/lib/dataGrid/columnFormatter.ts index b30098e7c9..a8aa7330af 100644 --- a/apps/desktop/src/lib/dataGrid/columnFormatter.ts +++ b/apps/desktop/src/lib/dataGrid/columnFormatter.ts @@ -190,7 +190,26 @@ export function normalizeColumnFormatter(value: unknown): ColumnFormatterConfig return undefined; } -const FOREIGN_KEY_DISPLAY_FILTER_MODES = new Set(["equals", "not-equals", "is-null", "is-not-null", "is-blank", "is-not-blank", "like", "not-like", "less-than", "less-than-or-equal", "greater-than", "greater-than-or-equal", "in", "not-in", "between", "not-between"]); +const FOREIGN_KEY_DISPLAY_FILTER_MODES = new Set([ + "equals", + "not-equals", + "is-null", + "is-not-null", + "is-blank", + "is-not-blank", + "like", + "not-like", + "begins-with", + "ends-with", + "less-than", + "less-than-or-equal", + "greater-than", + "greater-than-or-equal", + "in", + "not-in", + "between", + "not-between", +]); function normalizeForeignKeyDisplayFilter(value: unknown): ForeignKeyDisplayFilterConfig | undefined { if (!value || typeof value !== "object") return undefined; diff --git a/apps/desktop/src/lib/dataGrid/dataGridSql.ts b/apps/desktop/src/lib/dataGrid/dataGridSql.ts index 198dabbae3..a53203e397 100644 --- a/apps/desktop/src/lib/dataGrid/dataGridSql.ts +++ b/apps/desktop/src/lib/dataGrid/dataGridSql.ts @@ -58,7 +58,7 @@ export interface DataGridCopyInsertStatementOptions { insertMode?: DataGridCopyInsertMode; } -export type DataGridContextFilterMode = "equals" | "not-equals" | "is-null" | "is-not-null" | "is-blank" | "is-not-blank" | "like" | "not-like" | "less-than" | "less-than-or-equal" | "greater-than" | "greater-than-or-equal" | "in" | "not-in" | "between" | "not-between"; +export type DataGridContextFilterMode = "equals" | "not-equals" | "is-null" | "is-not-null" | "is-blank" | "is-not-blank" | "like" | "not-like" | "begins-with" | "ends-with" | "less-than" | "less-than-or-equal" | "greater-than" | "greater-than-or-equal" | "in" | "not-in" | "between" | "not-between"; export interface DataGridContextFilterConditionOptions { databaseType?: DatabaseType; diff --git a/crates/dbx-core/src/data_grid_sql.rs b/crates/dbx-core/src/data_grid_sql.rs index c3895e9c47..904eca7e69 100644 --- a/crates/dbx-core/src/data_grid_sql.rs +++ b/crates/dbx-core/src/data_grid_sql.rs @@ -153,6 +153,8 @@ pub enum DataGridContextFilterMode { IsNotBlank, Like, NotLike, + BeginsWith, + EndsWith, LessThan, LessThanOrEqual, GreaterThan, @@ -578,6 +580,22 @@ pub fn build_data_grid_context_filter_condition(options: DataGridContextFilterCo None ) )), + DataGridContextFilterMode::BeginsWith => Some(format!( + "{like_column} LIKE {}", + format_grid_sql_literal( + &Value::String(format!("{}%", value_to_filter_text(value))), + options.database_type, + None + ) + )), + DataGridContextFilterMode::EndsWith => Some(format!( + "{like_column} LIKE {}", + format_grid_sql_literal( + &Value::String(format!("%{}", value_to_filter_text(value))), + options.database_type, + None + ) + )), DataGridContextFilterMode::LessThan => Some(format!( "{column} < {}", format_data_grid_context_filter_literal( @@ -4153,6 +4171,48 @@ mod tests { .as_deref(), Some("\"created_at\"::text NOT LIKE '%2026%'") ); + assert_eq!( + build_data_grid_context_filter_condition(DataGridContextFilterConditionOptions { + database_type: Some(DatabaseType::Mysql), + identifier_quote: None, + column_name: "file_name".to_string(), + mode: DataGridContextFilterMode::BeginsWith, + value: json!("FN"), + values: Vec::new(), + end_value: None, + column_info: Some(column("file_name", "varchar", false, None)), + }) + .as_deref(), + Some("`file_name` LIKE 'FN%'") + ); + assert_eq!( + build_data_grid_context_filter_condition(DataGridContextFilterConditionOptions { + database_type: Some(DatabaseType::Mysql), + identifier_quote: None, + column_name: "file_name".to_string(), + mode: DataGridContextFilterMode::EndsWith, + value: json!(".sql"), + values: Vec::new(), + end_value: None, + column_info: Some(column("file_name", "varchar", false, None)), + }) + .as_deref(), + Some("`file_name` LIKE '%.sql'") + ); + assert_eq!( + build_data_grid_context_filter_condition(DataGridContextFilterConditionOptions { + database_type: Some(DatabaseType::Postgres), + identifier_quote: None, + column_name: "update_date".to_string(), + mode: DataGridContextFilterMode::BeginsWith, + value: json!("128"), + values: Vec::new(), + end_value: None, + column_info: Some(column("update_date", "bigint", false, None)), + }) + .as_deref(), + Some("\"update_date\"::text LIKE '128%'") + ); assert_eq!( build_data_grid_column_value_filter_condition(DataGridColumnValueFilterConditionOptions { database_type: Some(DatabaseType::SqlServer), diff --git a/packages/app-tests/dataGridColumnFilter.test.ts b/packages/app-tests/dataGridColumnFilter.test.ts index f5af73886f..805a3e66ef 100644 --- a/packages/app-tests/dataGridColumnFilter.test.ts +++ b/packages/app-tests/dataGridColumnFilter.test.ts @@ -168,6 +168,30 @@ test("passes value-less blank modes through the shared context filter API", asyn } }); +test("passes begins-with and ends-with modes through the shared context filter API", async () => { + installFilterFetchMock(); + for (const mode of ["begins-with", "ends-with"] as const) { + await buildDataGridContextFilterCondition({ + databaseType: "mysql", + columnName: "file_name", + mode, + value: "FN", + }); + assert.deepEqual(lastContextFilterOptions, { + databaseType: "mysql", + columnName: "file_name", + mode, + value: "FN", + }); + } + assert.equal(filterModeIsSupportedForDatabase("begins-with", "mysql"), true); + assert.equal(filterModeIsSupportedForDatabase("ends-with", "postgres"), true); + assert.equal(filterModeIsSupportedForDatabase("begins-with", "cassandra"), true); + assert.equal(filterModeHasCompleteValue("begins-with", "FN"), true); + assert.equal(filterModeHasCompleteValue("begins-with", " "), false); + assert.equal(filterModeHasCompleteValue("ends-with", ".sql"), true); +}); + test("passes list and range values through the shared context filter API", async () => { installFilterFetchMock(); await buildDataGridContextFilterCondition({