diff --git a/src-tauri/src/commands/schema.rs b/src-tauri/src/commands/schema.rs index 0f280389e..be4920a65 100644 --- a/src-tauri/src/commands/schema.rs +++ b/src-tauri/src/commands/schema.rs @@ -444,7 +444,13 @@ async fn pg_ddl(pool: &sqlx::postgres::PgPool, schema: &str, table: &str) -> Res if idx.is_primary { continue; } let unique = if idx.is_unique { "UNIQUE " } else { "" }; let cols = idx.columns.iter().map(|c| format!("\"{c}\"")).collect::>().join(", "); - ddl.push_str(&format!("\nCREATE {unique}INDEX \"{}\" ON \"{schema}\".\"{table}\" ({cols});", idx.name)); + let using = idx.index_type.as_deref().map(|t| format!(" USING {t}")).unwrap_or_default(); + let include = idx.included_columns.as_deref().filter(|c| !c.is_empty()).map(|cols| format!(" INCLUDE ({})", cols.iter().map(|c| format!("\"{c}\"")).collect::>().join(", "))).unwrap_or_default(); + let filter = idx.filter.as_deref().map(|f| format!(" WHERE {f}")).unwrap_or_default(); + ddl.push_str(&format!("\nCREATE {unique}INDEX \"{}\" ON \"{schema}\".\"{table}\"{using} ({cols}){include}{filter};", idx.name)); + if let Some(ref c) = idx.comment { + ddl.push_str(&format!("\nCOMMENT ON INDEX \"{schema}\".\"{}\" IS '{}';", idx.name, c.replace('\'', "''"))); + } } Ok(ddl) } @@ -475,8 +481,11 @@ async fn build_sqlserver_ddl(client: &mut db::sqlserver::SqlServerClient, schema for idx in &indexes { if idx.is_primary { continue; } let unique = if idx.is_unique { "UNIQUE " } else { "" }; + let idx_type = idx.index_type.as_deref().map(|t| format!("{t} ")).unwrap_or_default(); let cols = idx.columns.iter().map(|c| format!("[{c}]")).collect::>().join(", "); - ddl.push_str(&format!("\nCREATE {unique}INDEX [{}] ON [{schema}].[{table}] ({cols});", idx.name)); + let include = idx.included_columns.as_deref().filter(|c| !c.is_empty()).map(|cols| format!(" INCLUDE ({})", cols.iter().map(|c| format!("[{c}]")).collect::>().join(", "))).unwrap_or_default(); + let filter = idx.filter.as_deref().map(|f| format!(" WHERE {f}")).unwrap_or_default(); + ddl.push_str(&format!("\nCREATE {unique}{idx_type}INDEX [{}] ON [{schema}].[{table}] ({cols}){include}{filter};", idx.name)); } Ok(ddl) } diff --git a/src-tauri/src/db/mod.rs b/src-tauri/src/db/mod.rs index 754bab761..c43741926 100644 --- a/src-tauri/src/db/mod.rs +++ b/src-tauri/src/db/mod.rs @@ -52,6 +52,10 @@ pub struct IndexInfo { pub columns: Vec, pub is_unique: bool, pub is_primary: bool, + pub filter: Option, + pub index_type: Option, + pub included_columns: Option>, + pub comment: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src-tauri/src/db/mysql.rs b/src-tauri/src/db/mysql.rs index f444a4743..412c22a8d 100644 --- a/src-tauri/src/db/mysql.rs +++ b/src-tauri/src/db/mysql.rs @@ -332,10 +332,11 @@ pub async fn execute_query(pool: &MySqlPool, sql: &str, bare: bool) -> Result Result, String> { let sql = format!( "SELECT INDEX_NAME, GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS columns, \ - MIN(NON_UNIQUE) = 0 AS is_unique, INDEX_NAME = 'PRIMARY' AS is_primary \ + MIN(NON_UNIQUE) = 0 AS is_unique, INDEX_NAME = 'PRIMARY' AS is_primary, \ + INDEX_TYPE \ FROM information_schema.STATISTICS \ WHERE TABLE_SCHEMA = {} AND TABLE_NAME = {} \ - GROUP BY INDEX_NAME \ + GROUP BY INDEX_NAME, INDEX_TYPE \ ORDER BY INDEX_NAME", quote_value(database), quote_value(table), @@ -354,6 +355,10 @@ pub async fn list_indexes(pool: &MySqlPool, database: &str, table: &str) -> Resu columns: cols_str.split(',').map(|s| s.to_string()).collect(), is_unique: row.get::("is_unique"), is_primary: row.get::("is_primary"), + filter: None, + index_type: Some(get_str_by_name(row, "INDEX_TYPE")), + included_columns: None, + comment: None, } }) .collect()) diff --git a/src-tauri/src/db/oracle_driver.rs b/src-tauri/src/db/oracle_driver.rs index d09f2bd6a..e6d1eccf8 100644 --- a/src-tauri/src/db/oracle_driver.rs +++ b/src-tauri/src/db/oracle_driver.rs @@ -134,13 +134,14 @@ pub async fn list_indexes(conn: &OracleClient, schema: &str, table: &str) -> Res "SELECT i.INDEX_NAME, \ LISTAGG(ic.COLUMN_NAME, ',') WITHIN GROUP (ORDER BY ic.COLUMN_POSITION) AS columns, \ i.UNIQUENESS, \ - CASE WHEN c.CONSTRAINT_TYPE = 'P' THEN 1 ELSE 0 END AS IS_PK \ + CASE WHEN c.CONSTRAINT_TYPE = 'P' THEN 1 ELSE 0 END AS IS_PK, \ + i.INDEX_TYPE \ FROM ALL_INDEXES i \ JOIN ALL_IND_COLUMNS ic ON i.INDEX_NAME = ic.INDEX_NAME AND i.OWNER = ic.INDEX_OWNER AND i.TABLE_OWNER = ic.TABLE_OWNER \ LEFT JOIN ALL_CONSTRAINTS c ON i.INDEX_NAME = c.INDEX_NAME AND i.TABLE_OWNER = c.OWNER \ AND c.CONSTRAINT_TYPE = 'P' \ WHERE i.TABLE_OWNER = '{s}' AND i.TABLE_NAME = '{t}' \ - GROUP BY i.INDEX_NAME, i.UNIQUENESS, c.CONSTRAINT_TYPE \ + GROUP BY i.INDEX_NAME, i.UNIQUENESS, c.CONSTRAINT_TYPE, i.INDEX_TYPE \ ORDER BY i.INDEX_NAME", s = schema.replace('\'', "''"), t = table.replace('\'', "''") ); @@ -152,6 +153,10 @@ pub async fn list_indexes(conn: &OracleClient, schema: &str, table: &str) -> Res columns: cols_str.split(',').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, + index_type: row.get_string(4).map(|s| s.to_string()), + included_columns: None, + comment: None, } }).collect()) } diff --git a/src-tauri/src/db/postgres.rs b/src-tauri/src/db/postgres.rs index 5f11c70e1..4ca785427 100644 --- a/src-tauri/src/db/postgres.rs +++ b/src-tauri/src/db/postgres.rs @@ -275,17 +275,23 @@ 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(a.attname ORDER BY k.n) AS columns, \ + array_agg(COALESCE(a.attname, pg_get_indexdef(ix.indexrelid, k.n, true)) ORDER BY k.n) AS columns, \ ix.indisunique AS is_unique, \ - ix.indisprimary AS is_primary \ + ix.indisprimary AS is_primary, \ + pg_get_expr(ix.indpred, ix.indrelid) AS filter_expr, \ + am.amname AS index_type, \ + ix.indnkeyatts AS nkeyatts, \ + ix.indkey AS indkey, \ + obj_description(i.oid, 'pg_class') AS index_comment \ FROM pg_index ix \ JOIN pg_class t ON t.oid = ix.indrelid \ JOIN pg_class i ON i.oid = ix.indexrelid \ JOIN pg_namespace n ON n.oid = t.relnamespace \ + JOIN pg_am am ON am.oid = i.relam \ JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, n) ON true \ - JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum \ + LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum AND k.attnum > 0 \ WHERE n.nspname = $1 AND t.relname = $2 \ - GROUP BY i.relname, ix.indisunique, ix.indisprimary \ + GROUP BY i.relname, i.oid, ix.indisunique, ix.indisprimary, ix.indpred, ix.indrelid, am.amname, ix.indnkeyatts, ix.indkey \ ORDER BY i.relname", ) .bind(schema) @@ -296,11 +302,20 @@ pub async fn list_indexes(pool: &PgPool, schema: &str, table: &str) -> Result("index_name"), - columns: row.get::, _>("columns"), - is_unique: row.get::("is_unique"), - is_primary: row.get::("is_primary"), + .map(|row| { + let all_cols: Vec = row.get::, _>("columns"); + let nkeyatts = row.get::, _>("nkeyatts").unwrap_or(all_cols.len() as i16) as usize; + let included = if nkeyatts < all_cols.len() { all_cols[nkeyatts..].to_vec() } else { vec![] }; + IndexInfo { + name: row.get::("index_name"), + columns: all_cols, + is_unique: row.get::("is_unique"), + is_primary: row.get::("is_primary"), + filter: row.get::, _>("filter_expr"), + index_type: row.get::, _>("index_type"), + included_columns: if included.is_empty() { None } else { Some(included) }, + comment: row.get::, _>("index_comment"), + } }) .collect()) } diff --git a/src-tauri/src/db/sqlite.rs b/src-tauri/src/db/sqlite.rs index aa4056b0a..31d94cc1a 100644 --- a/src-tauri/src/db/sqlite.rs +++ b/src-tauri/src/db/sqlite.rs @@ -87,6 +87,10 @@ pub async fn list_indexes(pool: &SqlitePool, _schema: &str, table: &str) -> Resu columns, is_unique, is_primary, + filter: None, + index_type: None, + included_columns: None, + comment: None, }); } Ok(indexes) diff --git a/src-tauri/src/db/sqlserver.rs b/src-tauri/src/db/sqlserver.rs index 7b6f19b41..327c30eec 100644 --- a/src-tauri/src/db/sqlserver.rs +++ b/src-tauri/src/db/sqlserver.rs @@ -155,13 +155,16 @@ pub async fn get_columns(client: &mut SqlServerClient, schema: &str, table: &str pub async fn list_indexes(client: &mut SqlServerClient, schema: &str, table: &str) -> Result, String> { let sql = format!( - "SELECT i.name, STRING_AGG(c.name, ',') WITHIN GROUP (ORDER BY ic.key_ordinal) AS columns, \ - i.is_unique, i.is_primary_key \ + "SELECT i.name, \ + STRING_AGG(CASE WHEN ic.is_included_column = 0 THEN c.name END, ',') WITHIN GROUP (ORDER BY ic.key_ordinal) AS columns, \ + i.is_unique, i.is_primary_key, i.type_desc, \ + STRING_AGG(CASE WHEN ic.is_included_column = 1 THEN c.name END, ',') AS included_cols, \ + i.filter_definition \ FROM sys.indexes i \ JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id \ JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id \ WHERE i.object_id = OBJECT_ID('{s}.{t}') AND i.name IS NOT NULL \ - GROUP BY i.name, i.is_unique, i.is_primary_key \ + GROUP BY i.name, i.is_unique, i.is_primary_key, i.type_desc, i.filter_definition \ ORDER BY i.name", s = schema.replace('\'', "''"), t = table.replace('\'', "''") ); @@ -169,11 +172,16 @@ pub async fn list_indexes(client: &mut SqlServerClient, schema: &str, table: &st let rows = stream.into_first_result().await.map_err(|e| e.to_string())?; Ok(rows.iter().map(|row| { let cols_str = row.get::<&str, _>(1).unwrap_or(""); + 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(), 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()), + index_type: row.get::<&str, _>(4).map(|s| s.to_string()), + included_columns: if inc_str.is_empty() { None } else { Some(inc_str.split(',').map(|s| s.to_string()).collect()) }, + comment: None, } }).collect()) } diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 5abfa0ebd..92da4f70a 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -335,7 +335,12 @@ export default { actions: "Actions", indexName: "Index", indexColumns: "Columns", + indexColumnsPlaceholder: "Select columns...", unique: "Unique", + indexType: "Type", + includedColumns: "Included", + includedColumnsPlaceholder: "Select columns...", + filter: "Filter", primary: "Primary", drop: "Drop", restore: "Restore", diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 0a1f5d545..81a5a20dd 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -335,7 +335,12 @@ export default { actions: "操作", indexName: "索引名", indexColumns: "字段列表", + indexColumnsPlaceholder: "选择列...", unique: "唯一", + indexType: "类型", + includedColumns: "包含列", + includedColumnsPlaceholder: "选择列...", + filter: "过滤条件", primary: "主键", drop: "删除", restore: "恢复", diff --git a/src/lib/schemaDiff.ts b/src/lib/schemaDiff.ts index 44ec2761d..d8f913731 100644 --- a/src/lib/schemaDiff.ts +++ b/src/lib/schemaDiff.ts @@ -180,7 +180,13 @@ export function generateSyncSql( if (idx.type === "added" && idx.source) { const cols = idx.source.columns.map((c) => quoteId(c, dbType)).join(", "); const unique = idx.source.is_unique ? "UNIQUE " : ""; - lines.push(`CREATE ${unique}INDEX ${quoteId(idx.name, dbType)} ON ${qt} (${cols});`); + const idxType = idx.source.index_type ?? ""; + const usingClause = idxType && dbType === "postgres" ? ` USING ${idxType}` : ""; + const typePrefix = idxType && dbType === "sqlserver" ? `${idxType} ` : ""; + const incCols = idx.source.included_columns ?? []; + const includeClause = incCols.length > 0 ? ` INCLUDE (${incCols.map((c) => quoteId(c, dbType)).join(", ")})` : ""; + const filter = idx.source.filter ? ` 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) { lines.push(`DROP INDEX ${quoteId(idx.name, dbType)} ON ${qt};`); diff --git a/src/lib/tableStructureEditorSql.ts b/src/lib/tableStructureEditorSql.ts index 9a7963716..4009e39f3 100644 --- a/src/lib/tableStructureEditorSql.ts +++ b/src/lib/tableStructureEditorSql.ts @@ -18,6 +18,10 @@ export interface EditableStructureIndex { columns: string[]; isUnique: boolean; isPrimary: boolean; + filter: string; + indexType: string; + includedColumns: string[]; + comment: string; original?: IndexInfo; markedForDrop: boolean; } @@ -245,7 +249,18 @@ function buildIndexSql(options: BuildTableStructureChangeSqlOptions, warnings: s if (!name || columns.length === 0) continue; const unique = index.isUnique ? "UNIQUE " : ""; const cols = columns.map((column) => quoteIdent(databaseType, column)).join(", "); - statements.push(`CREATE ${unique}INDEX ${quoteIdent(databaseType, name)} ON ${table} (${cols});`); + const idxType = clean(index.indexType); + 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 filter = clean(index.filter); + const whereClause = filter ? ` 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") { + statements.push(`COMMENT ON INDEX ${quoteIdent(databaseType, name)} IS ${quoteString(comment)};`); + } } return statements; diff --git a/src/lib/tableStructureEditorState.ts b/src/lib/tableStructureEditorState.ts index a5f73f4ed..8419abaa3 100644 --- a/src/lib/tableStructureEditorState.ts +++ b/src/lib/tableStructureEditorState.ts @@ -22,18 +22,15 @@ export function createIndexDrafts(indexes: IndexInfo[]): EditableStructureIndex[ columns: [...index.columns], isUnique: index.is_unique, isPrimary: index.is_primary, + filter: index.filter ?? "", + indexType: index.index_type ?? "", + includedColumns: index.included_columns ? [...index.included_columns] : [], + comment: index.comment ?? "", original: index, markedForDrop: false, })); } -export function splitIndexColumns(value: string): string[] { - return value - .split(/[,\s]+/g) - .map((part) => part.trim()) - .filter(Boolean); -} - export function toColumnNames(columns: string[]): string { return columns.join(", "); } diff --git a/src/types/database.ts b/src/types/database.ts index 0de1b3c7e..f9142dd1f 100644 --- a/src/types/database.ts +++ b/src/types/database.ts @@ -44,6 +44,7 @@ export interface ColumnInfo { comment?: string | null; numeric_precision?: number | null; numeric_scale?: number | null; + character_maximum_length?: number | null; } export interface IndexInfo { @@ -51,6 +52,10 @@ export interface IndexInfo { columns: string[]; is_unique: boolean; is_primary: boolean; + filter?: string | null; + index_type?: string | null; + included_columns?: string[] | null; + comment?: string | null; } export interface ForeignKeyInfo { diff --git a/tests/tableStructureEditorSql.test.ts b/tests/tableStructureEditorSql.test.ts index ee8ae087b..9c125eb59 100644 --- a/tests/tableStructureEditorSql.test.ts +++ b/tests/tableStructureEditorSql.test.ts @@ -27,6 +27,10 @@ function index(overrides: Partial): EditableStructureInd columns: overrides.columns ?? ["name"], isUnique: overrides.isUnique ?? false, isPrimary: overrides.isPrimary ?? false, + filter: overrides.filter ?? "", + indexType: overrides.indexType ?? "", + includedColumns: overrides.includedColumns ?? [], + comment: overrides.comment ?? "", original: overrides.original, markedForDrop: overrides.markedForDrop ?? false, }; diff --git a/tests/tableStructureEditorState.test.ts b/tests/tableStructureEditorState.test.ts index 2b4ec35b3..e5f740895 100644 --- a/tests/tableStructureEditorState.test.ts +++ b/tests/tableStructureEditorState.test.ts @@ -3,7 +3,6 @@ import test from "node:test"; import { createColumnDrafts, createIndexDrafts, - splitIndexColumns, toColumnNames, } from "../src/lib/tableStructureEditorState.ts"; import type { ColumnInfo, IndexInfo } from "../src/types/database.ts"; @@ -101,6 +100,5 @@ test("creates editable index drafts and splits pasted column lists", () => { originalName: "idx_name", }, ]); - assert.deepEqual(splitIndexColumns("id, name email"), ["id", "name", "email"]); assert.equal(toColumnNames(["id", "name"]), "id, name"); });