feat: add index type, included columns, filter, and comment support
- Add filter, index_type, included_columns, and comment fields to IndexInfo - PostgreSQL: query pg_am for index type, use indnkeyatts to split key/included columns, retrieve index comments via obj_description() - SQL Server: add type_desc, filter_definition, included columns via is_included_column - MySQL: add INDEX_TYPE from information_schema.STATISTICS - Oracle: add INDEX_TYPE from ALL_INDEXES - DDL generation: add USING (PostgreSQL), type prefix + INCLUDE + WHERE (SQL Server), COMMENT ON INDEX (PostgreSQL) - Frontend: update types, state mapping, SQL generation, and i18n translations
This commit is contained in:
parent
b88e1b8ccb
commit
4e1b35186b
|
|
@ -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::<Vec<_>>().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::<Vec<_>>().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::<Vec<_>>().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::<Vec<_>>().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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,10 @@ pub struct IndexInfo {
|
|||
pub columns: Vec<String>,
|
||||
pub is_unique: bool,
|
||||
pub is_primary: bool,
|
||||
pub filter: Option<String>,
|
||||
pub index_type: Option<String>,
|
||||
pub included_columns: Option<Vec<String>>,
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -332,10 +332,11 @@ pub async fn execute_query(pool: &MySqlPool, sql: &str, bare: bool) -> Result<Qu
|
|||
pub async fn list_indexes(pool: &MySqlPool, database: &str, table: &str) -> Result<Vec<IndexInfo>, 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::<bool, _>("is_unique"),
|
||||
is_primary: row.get::<bool, _>("is_primary"),
|
||||
filter: None,
|
||||
index_type: Some(get_str_by_name(row, "INDEX_TYPE")),
|
||||
included_columns: None,
|
||||
comment: None,
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -275,17 +275,23 @@ pub async fn execute_query(pool: &PgPool, sql: &str) -> Result<QueryResult, Stri
|
|||
pub async fn list_indexes(pool: &PgPool, schema: &str, table: &str) -> Result<Vec<IndexInfo>, String> {
|
||||
let rows: Vec<PgRow> = 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<Ve
|
|||
|
||||
Ok(rows
|
||||
.iter()
|
||||
.map(|row| IndexInfo {
|
||||
name: row.get::<String, _>("index_name"),
|
||||
columns: row.get::<Vec<String>, _>("columns"),
|
||||
is_unique: row.get::<bool, _>("is_unique"),
|
||||
is_primary: row.get::<bool, _>("is_primary"),
|
||||
.map(|row| {
|
||||
let all_cols: Vec<String> = row.get::<Vec<String>, _>("columns");
|
||||
let nkeyatts = row.get::<Option<i16>, _>("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::<String, _>("index_name"),
|
||||
columns: all_cols,
|
||||
is_unique: row.get::<bool, _>("is_unique"),
|
||||
is_primary: row.get::<bool, _>("is_primary"),
|
||||
filter: row.get::<Option<String>, _>("filter_expr"),
|
||||
index_type: row.get::<Option<String>, _>("index_type"),
|
||||
included_columns: if included.is_empty() { None } else { Some(included) },
|
||||
comment: row.get::<Option<String>, _>("index_comment"),
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<Vec<IndexInfo>, 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::<bool, _>(2).unwrap_or(false),
|
||||
is_primary: row.get::<bool, _>(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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -335,7 +335,12 @@ export default {
|
|||
actions: "操作",
|
||||
indexName: "索引名",
|
||||
indexColumns: "字段列表",
|
||||
indexColumnsPlaceholder: "选择列...",
|
||||
unique: "唯一",
|
||||
indexType: "类型",
|
||||
includedColumns: "包含列",
|
||||
includedColumnsPlaceholder: "选择列...",
|
||||
filter: "过滤条件",
|
||||
primary: "主键",
|
||||
drop: "删除",
|
||||
restore: "恢复",
|
||||
|
|
|
|||
|
|
@ -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};`);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(", ");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ function index(overrides: Partial<EditableStructureIndex>): 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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue