From b43424783830c34fa3239fb0feee9b100d21ca55 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Mon, 29 Jun 2026 20:09:01 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(ddl):=20=E8=A1=A5=E9=BD=90?= =?UTF-8?q?=20kingbase=20=E8=A1=A8DDL=E7=B4=A2=E5=BC=95=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 SHOW CREATE TABLE 不可用时,fallback 额外拉取索引元数据并生成 CREATE INDEX 语句\n- 跳过与主键重复的索引,避免在 fallback DDL 中重复输出主键索引\n- 补充 kingbase DDL fallback 的索引回归测试\n\nFixes #601 --- internal/app/methods_db.go | 119 +++++++++++++++++- .../app/methods_db_create_statement_test.go | 53 +++++++- 2 files changed, 168 insertions(+), 4 deletions(-) diff --git a/internal/app/methods_db.go b/internal/app/methods_db.go index 595f63f1..e777a8f8 100644 --- a/internal/app/methods_db.go +++ b/internal/app/methods_db.go @@ -3,6 +3,7 @@ package app import ( "context" "fmt" + "sort" "strconv" "strings" "time" @@ -1796,7 +1797,12 @@ func resolveCreateStatementWithFallbackWithText(dbInst db.Database, config conne return "", colErr } - fallbackDDL, buildErr := buildFallbackCreateStatementWithText(dbType, ddlSchemaName, ddlTableName, columns, text) + var indexes []connection.IndexDefinition + if indexRows, idxErr := dbInst.GetIndexes(metadataSchemaName, metadataTableName); idxErr == nil { + indexes = indexRows + } + + fallbackDDL, buildErr := buildFallbackCreateStatementWithText(dbType, ddlSchemaName, ddlTableName, columns, indexes, text) if buildErr != nil { if sourceErr != nil { return "", sourceErr @@ -1901,10 +1907,10 @@ func hasCreateTableOrViewHead(sqlText string) bool { } func buildFallbackCreateStatement(dbType string, schemaName string, tableName string, columns []connection.ColumnDefinition) (string, error) { - return buildFallbackCreateStatementWithText(dbType, schemaName, tableName, columns, defaultDBBackendText) + return buildFallbackCreateStatementWithText(dbType, schemaName, tableName, columns, nil, defaultDBBackendText) } -func buildFallbackCreateStatementWithText(dbType string, schemaName string, tableName string, columns []connection.ColumnDefinition, text func(string, map[string]any) string) (string, error) { +func buildFallbackCreateStatementWithText(dbType string, schemaName string, tableName string, columns []connection.ColumnDefinition, indexes []connection.IndexDefinition, text func(string, map[string]any) string) (string, error) { if text == nil { text = defaultDBBackendText } @@ -1962,6 +1968,7 @@ func buildFallbackCreateStatementWithText(dbType string, schemaName string, tabl if len(primaryKeys) > 0 { columnLines = append(columnLines, " PRIMARY KEY ("+strings.Join(primaryKeys, ", ")+")") } + indexStatements := buildFallbackIndexStatements(dbType, qualifiedTable, primaryKeys, indexes) ddl := strings.Builder{} ddl.WriteString("CREATE TABLE ") @@ -1969,6 +1976,10 @@ func buildFallbackCreateStatementWithText(dbType string, schemaName string, tabl ddl.WriteString(" (\n") ddl.WriteString(strings.Join(columnLines, ",\n")) ddl.WriteString("\n);") + if len(indexStatements) > 0 { + ddl.WriteString("\n") + ddl.WriteString(strings.Join(indexStatements, "\n")) + } if len(columnCommentLines) > 0 { ddl.WriteString("\n") ddl.WriteString(strings.Join(columnCommentLines, "\n")) @@ -1976,6 +1987,108 @@ func buildFallbackCreateStatementWithText(dbType string, schemaName string, tabl return ddl.String(), nil } +type fallbackIndexGroup struct { + Name string + Unique bool + Columns []string +} + +func buildFallbackIndexStatements(dbType string, qualifiedTable string, primaryKeys []string, indexes []connection.IndexDefinition) []string { + grouped := groupFallbackIndexDefinitions(indexes) + if len(grouped) == 0 { + return nil + } + + statements := make([]string, 0, len(grouped)) + for _, idx := range grouped { + if strings.TrimSpace(idx.Name) == "" || len(idx.Columns) == 0 { + continue + } + if sameFallbackColumnNameList(idx.Columns, primaryKeys) { + continue + } + + quotedColumns := make([]string, 0, len(idx.Columns)) + for _, columnName := range idx.Columns { + columnName = strings.TrimSpace(columnName) + if columnName == "" { + continue + } + quotedColumns = append(quotedColumns, quoteIdentByType(dbType, columnName)) + } + if len(quotedColumns) == 0 { + continue + } + + prefix := "CREATE INDEX" + if idx.Unique { + prefix = "CREATE UNIQUE INDEX" + } + statements = append(statements, fmt.Sprintf( + "%s %s ON %s (%s);", + prefix, + quoteIdentByType(dbType, idx.Name), + qualifiedTable, + strings.Join(quotedColumns, ", "), + )) + } + + return statements +} + +func groupFallbackIndexDefinitions(indexes []connection.IndexDefinition) []fallbackIndexGroup { + if len(indexes) == 0 { + return nil + } + + groupMap := make(map[string][]connection.IndexDefinition) + order := make([]string, 0) + for _, idx := range indexes { + name := strings.TrimSpace(idx.Name) + if name == "" { + continue + } + if _, ok := groupMap[name]; !ok { + order = append(order, name) + } + groupMap[name] = append(groupMap[name], idx) + } + + grouped := make([]fallbackIndexGroup, 0, len(order)) + for _, name := range order { + rows := groupMap[name] + sort.SliceStable(rows, func(i, j int) bool { + return rows[i].SeqInIndex < rows[j].SeqInIndex + }) + + group := fallbackIndexGroup{Name: name, Unique: true} + for _, row := range rows { + if row.NonUnique != 0 { + group.Unique = false + } + columnName := strings.TrimSpace(row.ColumnName) + if columnName != "" { + group.Columns = append(group.Columns, columnName) + } + } + grouped = append(grouped, group) + } + + return grouped +} + +func sameFallbackColumnNameList(a []string, b []string) bool { + if len(a) == 0 || len(a) != len(b) { + return false + } + for i := range a { + if !strings.EqualFold(strings.TrimSpace(a[i]), strings.TrimSpace(b[i])) { + return false + } + } + return true +} + func buildFallbackColumnCommentStatement(dbType string, schemaName string, tableName string, qualifiedTable string, columnName string, comment string) string { colName := strings.TrimSpace(columnName) commentText := strings.TrimSpace(comment) diff --git a/internal/app/methods_db_create_statement_test.go b/internal/app/methods_db_create_statement_test.go index 9323ff28..953d68a2 100644 --- a/internal/app/methods_db_create_statement_test.go +++ b/internal/app/methods_db_create_statement_test.go @@ -13,6 +13,8 @@ type fakeCreateStatementDB struct { createErr error columns []connection.ColumnDefinition columnsErr error + indexes []connection.IndexDefinition + indexesErr error queryRows []map[string]interface{} queryErr error @@ -20,7 +22,10 @@ type fakeCreateStatementDB struct { createTable string colsSchema string colsTable string + indexSchema string + indexTable string columnsCalls int + indexCalls int queries []string } @@ -49,7 +54,10 @@ func (f *fakeCreateStatementDB) GetAllColumns(dbName string) ([]connection.Colum return nil, nil } func (f *fakeCreateStatementDB) GetIndexes(dbName, tableName string) ([]connection.IndexDefinition, error) { - return nil, nil + f.indexCalls++ + f.indexSchema = dbName + f.indexTable = tableName + return f.indexes, f.indexesErr } func (f *fakeCreateStatementDB) GetForeignKeys(dbName, tableName string) ([]connection.ForeignKeyDefinition, error) { return nil, nil @@ -278,6 +286,49 @@ func TestResolveCreateStatementWithFallback_KingbaseIncludesColumnComments(t *te } } +func TestResolveCreateStatementWithFallback_KingbaseIncludesSecondaryIndexes(t *testing.T) { + t.Parallel() + + dbInst := &fakeCreateStatementDB{ + createSQL: "SHOW CREATE TABLE not directly supported in Kingbase/Postgres via SQL", + columns: []connection.ColumnDefinition{ + {Name: "id", Type: "bigint", Nullable: "NO", Key: "PRI"}, + {Name: "tenant_id", Type: "bigint", Nullable: "NO"}, + {Name: "api_name", Type: "character varying(100 char)", Nullable: "YES"}, + }, + indexes: []connection.IndexDefinition{ + {Name: "mes_third_sys_log_pkey", ColumnName: "id", NonUnique: 0, SeqInIndex: 1, IndexType: "BTREE"}, + {Name: "idx_mes_third_sys_log_tenant_api", ColumnName: "tenant_id", NonUnique: 1, SeqInIndex: 1, IndexType: "BTREE"}, + {Name: "idx_mes_third_sys_log_tenant_api", ColumnName: "api_name", NonUnique: 1, SeqInIndex: 2, IndexType: "BTREE"}, + {Name: "uk_mes_third_sys_log_api", ColumnName: "api_name", NonUnique: 0, SeqInIndex: 1, IndexType: "BTREE"}, + }, + } + + ddl, err := resolveCreateStatementWithFallback(dbInst, connection.ConnectionConfig{ + Type: "kingbase", + }, "demo_db", "ldf_server.mes_third_sys_log") + if err != nil { + t.Fatalf("resolveCreateStatementWithFallback() unexpected error: %v", err) + } + if dbInst.indexCalls != 1 { + t.Fatalf("expected one GetIndexes call, got %d", dbInst.indexCalls) + } + if dbInst.indexSchema != "ldf_server" || dbInst.indexTable != "mes_third_sys_log" { + t.Fatalf("expected index target ldf_server.mes_third_sys_log, got %q.%q", dbInst.indexSchema, dbInst.indexTable) + } + for _, want := range []string{ + `CREATE INDEX idx_mes_third_sys_log_tenant_api ON ldf_server.mes_third_sys_log (tenant_id, api_name);`, + `CREATE UNIQUE INDEX uk_mes_third_sys_log_api ON ldf_server.mes_third_sys_log (api_name);`, + } { + if !strings.Contains(ddl, want) { + t.Fatalf("expected fallback DDL to contain %q, got: %s", want, ddl) + } + } + if strings.Contains(ddl, `CREATE UNIQUE INDEX mes_third_sys_log_pkey`) { + t.Fatalf("primary key index should not be emitted separately, got: %s", ddl) + } +} + func TestResolveCreateStatementWithFallback_KeepQualifiedSchema(t *testing.T) { t.Parallel()