From a59494e4c4b9c0bab420e56290193c04d2f40713 Mon Sep 17 00:00:00 2001 From: yavon007 Date: Sun, 3 May 2026 20:33:09 +0800 Subject: [PATCH] fix: guard index column splits, restrict INCLUDE/WHERE by db, unify index type dropdowns - Filter empty strings from STRING_AGG/LISTAGG splits in SQL Server, Oracle, MySQL - Emit INCLUDE clause only for PostgreSQL and SQL Server (not MySQL/SQLite) - Emit WHERE clause only for PostgreSQL, SQL Server, SQLite (not MySQL) - Replace index type free-text input with dropdown for all 5 database types - Fix hardcoded column count in resize handle (use indexColLabels.length - 1) - Fix pg_get_indexdef type mismatch: cast k.n from bigint to integer --- src-tauri/src/db/mysql.rs | 2 +- src-tauri/src/db/oracle_driver.rs | 2 +- src-tauri/src/db/postgres.rs | 2 +- src-tauri/src/db/sqlserver.rs | 2 +- .../structure/TableStructureEditorDialog.vue | 49 ++++++++++++++----- src/lib/schemaDiff.ts | 5 +- src/lib/tableStructureEditorSql.ts | 5 +- tests/tableStructureEditorSql.test.ts | 4 +- 8 files changed, 50 insertions(+), 21 deletions(-) diff --git a/src-tauri/src/db/mysql.rs b/src-tauri/src/db/mysql.rs index 412c22a8d2..c7d8c8a366 100644 --- a/src-tauri/src/db/mysql.rs +++ b/src-tauri/src/db/mysql.rs @@ -352,7 +352,7 @@ pub async fn list_indexes(pool: &MySqlPool, database: &str, table: &str) -> Resu let cols_str = get_str_by_name(row, "columns"); IndexInfo { name: get_str_by_name(row, "INDEX_NAME"), - columns: cols_str.split(',').map(|s| s.to_string()).collect(), + columns: cols_str.split(',').filter(|s| !s.is_empty()).map(|s| s.to_string()).collect(), is_unique: row.get::("is_unique"), is_primary: row.get::("is_primary"), filter: None, diff --git a/src-tauri/src/db/oracle_driver.rs b/src-tauri/src/db/oracle_driver.rs index e6d1eccf8d..2ae7d87d23 100644 --- a/src-tauri/src/db/oracle_driver.rs +++ b/src-tauri/src/db/oracle_driver.rs @@ -150,7 +150,7 @@ pub async fn list_indexes(conn: &OracleClient, schema: &str, table: &str) -> Res let cols_str = row.get_string(1).unwrap_or(""); IndexInfo { name: row.get_string(0).unwrap_or("").to_string(), - columns: cols_str.split(',').map(|s| s.to_string()).collect(), + columns: cols_str.split(',').filter(|s| !s.is_empty()).map(|s| s.to_string()).collect(), is_unique: row.get_string(2).unwrap_or("") == "UNIQUE", is_primary: row.get_i64(3).unwrap_or(0) == 1, filter: None, diff --git a/src-tauri/src/db/postgres.rs b/src-tauri/src/db/postgres.rs index 3ba3f84704..6dcef76cd6 100644 --- a/src-tauri/src/db/postgres.rs +++ b/src-tauri/src/db/postgres.rs @@ -275,7 +275,7 @@ pub async fn execute_query(pool: &PgPool, sql: &str) -> Result Result, String> { let rows: Vec = sqlx::query( "SELECT i.relname AS index_name, \ - array_agg(COALESCE(a.attname, pg_get_indexdef(ix.indexrelid, k.n, true)) ORDER BY k.n) AS columns, \ + array_agg(COALESCE(a.attname, pg_get_indexdef(ix.indexrelid, k.n::int, true)) ORDER BY k.n) AS columns, \ ix.indisunique AS is_unique, \ ix.indisprimary AS is_primary, \ pg_get_expr(ix.indpred, ix.indrelid) AS filter_expr, \ diff --git a/src-tauri/src/db/sqlserver.rs b/src-tauri/src/db/sqlserver.rs index 327c30eece..47d54c0bfe 100644 --- a/src-tauri/src/db/sqlserver.rs +++ b/src-tauri/src/db/sqlserver.rs @@ -175,7 +175,7 @@ pub async fn list_indexes(client: &mut SqlServerClient, schema: &str, table: &st let inc_str = row.get::<&str, _>(5).unwrap_or(""); IndexInfo { name: row.get::<&str, _>(0).unwrap_or("").to_string(), - columns: cols_str.split(',').map(|s| s.to_string()).collect(), + columns: cols_str.split(',').filter(|s| !s.is_empty()).map(|s| s.to_string()).collect(), is_unique: row.get::(2).unwrap_or(false), is_primary: row.get::(3).unwrap_or(false), filter: row.get::<&str, _>(6).map(|s| s.to_string()), diff --git a/src/components/structure/TableStructureEditorDialog.vue b/src/components/structure/TableStructureEditorDialog.vue index ff1e5ff8f4..57ff3be89d 100644 --- a/src/components/structure/TableStructureEditorDialog.vue +++ b/src/components/structure/TableStructureEditorDialog.vue @@ -16,6 +16,9 @@ import { import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { + Select, SelectContent, SelectItem, SelectTrigger, SelectValue, +} from "@/components/ui/select"; import { useConnectionStore } from "@/stores/connectionStore"; import { useToast } from "@/composables/useToast"; import { buildTableStructureChangeSql, type EditableStructureColumn, type EditableStructureIndex } from "@/lib/tableStructureEditorSql"; @@ -72,6 +75,26 @@ const connection = computed(() => props.prefillConnectionId ? store.getConfig(props.prefillConnectionId) : undefined ); const databaseType = computed(() => connection.value?.db_type); + +const indexTypesByDb: Record = { + postgres: ["BTREE", "HASH", "GIST", "SPGIST", "GIN", "BRIN"], + mysql: ["BTREE", "HASH", "FULLTEXT", "SPATIAL", "RTREE"], + sqlserver: ["CLUSTERED", "NONCLUSTERED", "COLUMNSTORE", "NONCLUSTERED COLUMNSTORE", "XML", "SPATIAL"], + oracle: ["NORMAL", "BITMAP", "FUNCTION-BASED NORMAL", "FUNCTION-BASED DOMAIN", "DOMAIN", "CLUSTER"], + sqlite: ["BTREE"], +}; +const indexTypeOptions = computed(() => indexTypesByDb[databaseType.value ?? ""] ?? []); + +const indexColLabels = computed(() => [ + t('structureEditor.indexName'), + t('structureEditor.indexColumns'), + t('structureEditor.unique'), + t('structureEditor.indexType'), + t('structureEditor.includedColumns'), + t('structureEditor.filter'), + t('structureEditor.comment'), + t('structureEditor.actions'), +]); const targetSchema = computed(() => props.prefillSchema || props.prefillDatabase || ""); const targetLabel = computed(() => [ connection.value?.name, @@ -341,23 +364,14 @@ watch(open, (value) => { {{ label }}
{ {{ index.indexType || 'BTREE' }} + 0 ? ` INCLUDE (${incCols.map((c) => quoteId(c, dbType)).join(", ")})` : ""; - const filter = idx.source.filter ? ` WHERE ${idx.source.filter}` : ""; + const includeClause = incCols.length > 0 && (dbType === "postgres" || dbType === "sqlserver") ? ` INCLUDE (${incCols.map((c) => quoteId(c, dbType)).join(", ")})` : ""; + const supportsWhere = dbType === "postgres" || dbType === "sqlserver" || dbType === "sqlite"; + const filter = idx.source.filter && supportsWhere ? ` WHERE ${idx.source.filter}` : ""; lines.push(`CREATE ${unique}${typePrefix}INDEX ${quoteId(idx.name, dbType)} ON ${qt}${usingClause} (${cols})${includeClause}${filter};`); } else if (idx.type === "removed") { if (isMySQL) { diff --git a/src/lib/tableStructureEditorSql.ts b/src/lib/tableStructureEditorSql.ts index 4009e39f34..92923720e4 100644 --- a/src/lib/tableStructureEditorSql.ts +++ b/src/lib/tableStructureEditorSql.ts @@ -253,9 +253,10 @@ function buildIndexSql(options: BuildTableStructureChangeSqlOptions, warnings: s const usingClause = idxType && databaseType === "postgres" ? ` USING ${idxType}` : ""; const typePrefix = idxType && databaseType === "sqlserver" ? `${idxType} ` : ""; const incCols = index.includedColumns.map(clean).filter(Boolean); - const includeClause = incCols.length > 0 ? ` INCLUDE (${incCols.map((c) => quoteIdent(databaseType, c)).join(", ")})` : ""; + const includeClause = incCols.length > 0 && (databaseType === "postgres" || databaseType === "sqlserver") ? ` INCLUDE (${incCols.map((c) => quoteIdent(databaseType, c)).join(", ")})` : ""; const filter = clean(index.filter); - const whereClause = filter ? ` WHERE ${filter}` : ""; + const supportsWhere = databaseType === "postgres" || databaseType === "sqlserver" || databaseType === "sqlite"; + const whereClause = filter && supportsWhere ? ` WHERE ${filter}` : ""; statements.push(`CREATE ${unique}${typePrefix}INDEX ${quoteIdent(databaseType, name)} ON ${table}${usingClause} (${cols})${includeClause}${whereClause};`); const comment = clean(index.comment); if (comment && databaseType === "postgres") { diff --git a/tests/tableStructureEditorSql.test.ts b/tests/tableStructureEditorSql.test.ts index a7c73ef686..7788e6f5f2 100644 --- a/tests/tableStructureEditorSql.test.ts +++ b/tests/tableStructureEditorSql.test.ts @@ -392,7 +392,7 @@ test("SQL Server index with type + include combined", () => { ]); }); -test("MySQL index does not emit USING or type prefix, but INCLUDE and WHERE are passed through", () => { +test("MySQL index omits USING, type prefix, INCLUDE, and WHERE (unsupported)", () => { const result = buildTableStructureChangeSql({ databaseType: "mysql", tableName: "orders", @@ -412,7 +412,7 @@ test("MySQL index does not emit USING or type prefix, but INCLUDE and WHERE are assert.deepEqual(result.warnings, []); assert.deepEqual(result.statements, [ - "CREATE INDEX `idx_orders_status` ON `orders` (`status`) INCLUDE (`total`) WHERE deleted = 0;", + "CREATE INDEX `idx_orders_status` ON `orders` (`status`);", ]); });