From c192b6cc3ae133a12e0463da663a9e42daab899a Mon Sep 17 00:00:00 2001 From: Syngnat Date: Mon, 27 Jul 2026 22:57:06 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(dameng):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E7=B4=A2=E5=BC=95=E5=85=83=E6=95=B0=E6=8D=AE=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2=E4=B8=8E=E9=94=99=E8=AF=AF=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修正 ALL_IND_COLUMNS 与 ALL_INDEXES 的索引归属关联;保留复合索引顺序、唯一性与索引类型元数据;索引加载失败时显示错误,并补齐回归测试和多语言文案。 Fixes #735 --- frontend/src/components/TableDesigner.tsx | 18 ++++- .../tableDesignerIndexUtils.test.ts | 21 +++++ .../src/components/tableDesignerIndexUtils.ts | 27 +++++++ internal/db/dameng_columns_runtime_test.go | 37 +++++++++ internal/db/dameng_impl.go | 33 +------- internal/db/dameng_metadata.go | 53 +++++++++++++ internal/db/dameng_metadata_test.go | 77 +++++++++++++++++++ shared/i18n/de-DE.json | 1 + shared/i18n/en-US.json | 1 + shared/i18n/ja-JP.json | 1 + shared/i18n/ru-RU.json | 1 + shared/i18n/zh-CN.json | 1 + shared/i18n/zh-TW.json | 1 + 13 files changed, 237 insertions(+), 35 deletions(-) diff --git a/frontend/src/components/TableDesigner.tsx b/frontend/src/components/TableDesigner.tsx index 7ce85866..3ea5c604 100644 --- a/frontend/src/components/TableDesigner.tsx +++ b/frontend/src/components/TableDesigner.tsx @@ -9,7 +9,7 @@ import Editor from './MonacoEditor'; import { TabData, ColumnDefinition, IndexDefinition, ForeignKeyDefinition, TriggerDefinition } from '../types'; import { useStore } from '../store'; import { DBGetColumns, DBGetIndexes, DBQueryAudited, DBGetForeignKeys, DBGetTriggers, DBShowCreateTable } from '../../wailsjs/go/app/App'; -import { hasIndexFormChanged, normalizeIndexFormFromRow, shouldRestoreOriginalIndex, toggleIndexSelection as getNextIndexSelection, type IndexDisplaySnapshot } from './tableDesignerIndexUtils'; +import { hasIndexFormChanged, normalizeIndexFormFromRow, resolveIndexMetadataResponse, shouldRestoreOriginalIndex, toggleIndexSelection as getNextIndexSelection, type IndexDisplaySnapshot } from './tableDesignerIndexUtils'; import { buildIndexCreateSqlPreview } from './tableDesignerIndexSql'; import { buildAlterTablePreviewSql, buildCreateTablePreviewSql, hasAlterTableDraftChanges, type StarRocksCreateTableOptions, type StarRocksDistributionType, type StarRocksKeyModel, type StarRocksTableKind } from './tableDesignerSchemaSql'; import { summarizeDuckDbPrimaryKeyChange } from './tableDesignerDuckDbPrimaryKey'; @@ -1026,10 +1026,20 @@ const TableDesigner: React.FC<{ tab: TabData; embedded?: boolean }> = ({ tab, em const loadIndexes = DBGetIndexes(rpcConfig, dbName, tableName) .then((idxRes) => { if (!isCurrentRequest()) return; - setIndexes(idxRes.success && Array.isArray(idxRes.data) ? idxRes.data : []); + const result = resolveIndexMetadataResponse(idxRes); + setIndexes(result.indexes); + if (result.errorDetail !== null) { + message.error(t('table_designer.message.load_indexes_failed', { + detail: result.errorDetail || t('table_designer.fallback.unknown_error', undefined, i18nLanguage), + }, i18nLanguage)); + } }) - .catch(() => { - if (isCurrentRequest()) setIndexes([]); + .catch((error: unknown) => { + if (!isCurrentRequest()) return; + setIndexes([]); + message.error(t('table_designer.message.load_indexes_failed', { + detail: formatLoadError(error) || t('table_designer.fallback.unknown_error', undefined, i18nLanguage), + }, i18nLanguage)); }) .finally(() => { if (isCurrentRequest()) setIndexesLoading(false); diff --git a/frontend/src/components/tableDesignerIndexUtils.test.ts b/frontend/src/components/tableDesignerIndexUtils.test.ts index 413915a9..57ffd4f8 100644 --- a/frontend/src/components/tableDesignerIndexUtils.test.ts +++ b/frontend/src/components/tableDesignerIndexUtils.test.ts @@ -4,6 +4,7 @@ import { buildIndexCreateSqlPreview } from './tableDesignerIndexSql'; import { hasIndexFormChanged, normalizeIndexFormFromRow, + resolveIndexMetadataResponse, shouldRestoreOriginalIndex, toggleIndexSelection, type IndexDisplaySnapshot, @@ -115,6 +116,26 @@ describe('tableDesignerIndexUtils', () => { expect(selected).toEqual([]); }); + it('keeps index metadata failures distinguishable from a successful empty list', () => { + expect(resolveIndexMetadataResponse({ + success: false, + data: null, + message: 'invalid column name OWNER', + })).toEqual({ + indexes: [], + errorDetail: 'invalid column name OWNER', + }); + + expect(resolveIndexMetadataResponse({ + success: true, + data: [], + message: '', + })).toEqual({ + indexes: [], + errorDetail: null, + }); + }); + it('only restores original index when create step fails after drop step', () => { expect(shouldRestoreOriginalIndex({ failedStatementIndex: 1 })).toBe(true); expect(shouldRestoreOriginalIndex({ failedStatementIndex: 0 })).toBe(false); diff --git a/frontend/src/components/tableDesignerIndexUtils.ts b/frontend/src/components/tableDesignerIndexUtils.ts index 11c0018e..9b49e593 100644 --- a/frontend/src/components/tableDesignerIndexUtils.ts +++ b/frontend/src/components/tableDesignerIndexUtils.ts @@ -19,6 +19,33 @@ export interface SchemaExecutionSnapshot { failedStatementIndex?: number; } +export interface IndexMetadataResponse { + success: boolean; + data?: unknown; + message?: unknown; +} + +export interface IndexMetadataResolution { + indexes: T[]; + errorDetail: string | null; +} + +export const resolveIndexMetadataResponse = ( + response: IndexMetadataResponse, +): IndexMetadataResolution => { + if (!response.success) { + return { + indexes: [], + errorDetail: String(response.message || '').trim(), + }; + } + + return { + indexes: Array.isArray(response.data) ? response.data as T[] : [], + errorDetail: null, + }; +}; + export const normalizeIndexFormFromRow = ( row: IndexDisplaySnapshot, supportedKinds: IndexKind[], diff --git a/internal/db/dameng_columns_runtime_test.go b/internal/db/dameng_columns_runtime_test.go index 09fff513..d5b0b78b 100644 --- a/internal/db/dameng_columns_runtime_test.go +++ b/internal/db/dameng_columns_runtime_test.go @@ -61,6 +61,19 @@ func (damengColumnsMetadataConn) QueryContext(_ context.Context, query string, _ }, nil } + if strings.Contains(query, "all_ind_columns") { + if !strings.Contains(query, "i.owner = c.index_owner AND i.index_name = c.index_name") { + return nil, errors.New("ALL_IND_COLUMNS must join ALL_INDEXES through INDEX_OWNER") + } + return &damengColumnsMetadataRows{ + columns: []string{"INDEX_NAME", "COLUMN_NAME", "UNIQUENESS", "COLUMN_POSITION", "INDEX_TYPE"}, + values: [][]driver.Value{ + {"IDX_ORDERS_TENANT_CREATED", "TENANT_ID", "NONUNIQUE", int64(1), "NORMAL"}, + {"IDX_ORDERS_TENANT_CREATED", "CREATED_AT", "NONUNIQUE", int64(2), "NORMAL"}, + }, + }, nil + } + return &damengColumnsMetadataRows{ columns: []string{ "COLUMN_NAME", "DATA_TYPE", "DATA_LENGTH", "CHAR_LENGTH", "DATA_PRECISION", @@ -160,3 +173,27 @@ func TestDamengGetColumnsKeepsBaseMetadataWhenAutoIncrementQueryFails(t *testing t.Fatalf("unexpected fallback columns: %+v", columns) } } + +func TestDamengGetIndexesUsesIndexOwnerJoinAndMapsColumnOrder(t *testing.T) { + resetDamengColumnsMetadataQueryState(t, false) + + damengDB := &DamengDB{conn: openDamengColumnsMetadataDB(t)} + indexes, err := damengDB.GetIndexes("biz", "orders") + if err != nil { + t.Fatalf("GetIndexes returned error: %v", err) + } + if len(indexes) != 2 { + t.Fatalf("unexpected index column count: %d", len(indexes)) + } + if indexes[0].Name != "IDX_ORDERS_TENANT_CREATED" || indexes[0].ColumnName != "TENANT_ID" || indexes[0].SeqInIndex != 1 || indexes[0].IndexType != "NORMAL" { + t.Fatalf("unexpected first index column: %+v", indexes[0]) + } + if indexes[1].ColumnName != "CREATED_AT" || indexes[1].SeqInIndex != 2 { + t.Fatalf("unexpected second index column: %+v", indexes[1]) + } + + queries := damengColumnsMetadataQueries() + if len(queries) != 1 || !strings.Contains(queries[0], "c.table_owner = 'BIZ'") || !strings.Contains(queries[0], "c.table_name = 'ORDERS'") { + t.Fatalf("expected one normalized schema index query, got=%v", queries) + } +} diff --git a/internal/db/dameng_impl.go b/internal/db/dameng_impl.go index 1828a6c0..6852c64a 100644 --- a/internal/db/dameng_impl.go +++ b/internal/db/dameng_impl.go @@ -332,40 +332,11 @@ func (d *DamengDB) GetColumns(dbName, tableName string) ([]connection.ColumnDefi } func (d *DamengDB) GetIndexes(dbName, tableName string) ([]connection.IndexDefinition, error) { - query := fmt.Sprintf(`SELECT index_name, column_name, uniqueness - FROM all_ind_columns - JOIN all_indexes USING (index_name, owner) - WHERE table_owner = '%s' AND table_name = '%s'`, - strings.ToUpper(dbName), strings.ToUpper(tableName)) - - if dbName == "" { - query = fmt.Sprintf(`SELECT index_name, column_name, uniqueness - FROM user_ind_columns - JOIN user_indexes USING (index_name) - WHERE table_name = '%s'`, strings.ToUpper(tableName)) - } - - data, _, err := d.Query(query) + data, _, err := d.Query(buildDamengIndexesQuery(dbName, tableName)) if err != nil { return nil, err } - - var indexes []connection.IndexDefinition - for _, row := range data { - unique := 1 - if val, ok := row["UNIQUENESS"]; ok && val == "UNIQUE" { - unique = 0 - } - - idx := connection.IndexDefinition{ - Name: fmt.Sprintf("%v", row["INDEX_NAME"]), - ColumnName: fmt.Sprintf("%v", row["COLUMN_NAME"]), - NonUnique: unique, - IndexType: "BTREE", - } - indexes = append(indexes, idx) - } - return indexes, nil + return buildDamengIndexDefinitions(data), nil } func (d *DamengDB) GetForeignKeys(dbName, tableName string) ([]connection.ForeignKeyDefinition, error) { diff --git a/internal/db/dameng_metadata.go b/internal/db/dameng_metadata.go index f72b3e43..0b9a95f5 100644 --- a/internal/db/dameng_metadata.go +++ b/internal/db/dameng_metadata.go @@ -188,6 +188,59 @@ func applyDamengAutoIncrementColumns(columns []connection.ColumnDefinition, data return columns } +func buildDamengIndexesQuery(dbName, tableName string) string { + upperDBName := strings.ReplaceAll(strings.ToUpper(strings.TrimSpace(dbName)), "'", "''") + upperTableName := strings.ReplaceAll(strings.ToUpper(strings.TrimSpace(tableName)), "'", "''") + + if upperDBName == "" { + return fmt.Sprintf(`SELECT c.index_name, c.column_name, i.uniqueness, c.column_position, i.index_type + FROM user_ind_columns c + JOIN user_indexes i ON i.index_name = c.index_name + WHERE c.table_name = '%s' + AND c.column_name IS NOT NULL + ORDER BY c.index_name, c.column_position`, upperTableName) + } + + return fmt.Sprintf(`SELECT c.index_name, c.column_name, i.uniqueness, c.column_position, i.index_type + FROM all_ind_columns c + JOIN all_indexes i ON i.owner = c.index_owner AND i.index_name = c.index_name + WHERE c.table_owner = '%s' + AND c.table_name = '%s' + AND c.column_name IS NOT NULL + ORDER BY c.index_name, c.column_position`, upperDBName, upperTableName) +} + +func buildDamengIndexDefinitions(data []map[string]interface{}) []connection.IndexDefinition { + indexes := make([]connection.IndexDefinition, 0, len(data)) + for _, row := range data { + name := getDamengRowString(row, "INDEX_NAME") + columnName := getDamengRowString(row, "COLUMN_NAME") + if name == "" || columnName == "" { + continue + } + + nonUnique := 1 + if strings.EqualFold(getDamengRowString(row, "UNIQUENESS"), "UNIQUE") { + nonUnique = 0 + } + + seqInIndex, _ := getDamengRowInt(row, "COLUMN_POSITION") + indexType := getDamengRowString(row, "INDEX_TYPE") + if indexType == "" { + indexType = "BTREE" + } + + indexes = append(indexes, connection.IndexDefinition{ + Name: name, + ColumnName: columnName, + NonUnique: nonUnique, + SeqInIndex: seqInIndex, + IndexType: indexType, + }) + } + return indexes +} + func buildDamengForeignKeysQuery(dbName, tableName string) string { upperDBName := strings.ToUpper(strings.TrimSpace(dbName)) upperTableName := strings.ToUpper(strings.TrimSpace(tableName)) diff --git a/internal/db/dameng_metadata_test.go b/internal/db/dameng_metadata_test.go index 8da4e45e..fd910e8a 100644 --- a/internal/db/dameng_metadata_test.go +++ b/internal/db/dameng_metadata_test.go @@ -166,3 +166,80 @@ func TestBuildDamengColumnDefinitions_MapsComment(t *testing.T) { t.Fatalf("expected comment to be mapped, got=%q", columns[0].Comment) } } + +func TestBuildDamengIndexesQuery_JoinsAllViewsByIndexOwner(t *testing.T) { + t.Parallel() + + query := buildDamengIndexesQuery("app", "orders") + + if !strings.Contains(query, "JOIN all_indexes i ON i.owner = c.index_owner AND i.index_name = c.index_name") { + t.Fatalf("expected schema query to join ALL_INDEXES through INDEX_OWNER, got: %s", query) + } + if !strings.Contains(query, "i.index_type") { + t.Fatalf("expected index type metadata to be selected, got: %s", query) + } + if strings.Contains(strings.ToLower(query), "using (index_name, owner)") { + t.Fatalf("ALL_IND_COLUMNS exposes INDEX_OWNER rather than OWNER, got: %s", query) + } + if !strings.Contains(query, "c.table_owner = 'APP'") || !strings.Contains(query, "c.table_name = 'ORDERS'") { + t.Fatalf("expected normalized schema and table predicates, got: %s", query) + } + if !strings.Contains(query, "ORDER BY c.index_name, c.column_position") { + t.Fatalf("expected stable multi-column index ordering, got: %s", query) + } +} + +func TestBuildDamengIndexesQuery_UsesUserViewsWithoutSchema(t *testing.T) { + t.Parallel() + + query := buildDamengIndexesQuery("", "orders") + + if !strings.Contains(query, "FROM user_ind_columns c") { + t.Fatalf("expected current-schema index columns view, got: %s", query) + } + if !strings.Contains(query, "JOIN user_indexes i ON i.index_name = c.index_name") { + t.Fatalf("expected current-schema index metadata join, got: %s", query) + } +} + +func TestBuildDamengIndexDefinitions_MapsCaseInsensitiveMultiColumnMetadata(t *testing.T) { + t.Parallel() + + indexes := buildDamengIndexDefinitions([]map[string]interface{}{ + { + "index_name": "IDX_ORDERS_TENANT_CREATED", + "column_name": "TENANT_ID", + "uniqueness": "NONUNIQUE", + "column_position": 1, + "index_type": "NORMAL", + }, + { + "INDEX_NAME": "IDX_ORDERS_TENANT_CREATED", + "COLUMN_NAME": "CREATED_AT", + "UNIQUENESS": "NONUNIQUE", + "COLUMN_POSITION": "2", + }, + { + "INDEX_NAME": "UK_ORDERS_NUMBER", + "COLUMN_NAME": "ORDER_NUMBER", + "UNIQUENESS": "UNIQUE", + "COLUMN_POSITION": 1, + }, + }) + + if len(indexes) != 3 { + t.Fatalf("expected three index columns, got=%d", len(indexes)) + } + if indexes[0].Name != "IDX_ORDERS_TENANT_CREATED" || indexes[0].ColumnName != "TENANT_ID" || indexes[0].NonUnique != 1 || indexes[0].SeqInIndex != 1 { + t.Fatalf("unexpected first index column: %#v", indexes[0]) + } + if indexes[0].IndexType != "NORMAL" { + t.Fatalf("expected index type metadata to be preserved, got: %#v", indexes[0]) + } + if indexes[1].SeqInIndex != 2 { + t.Fatalf("expected second column position, got: %#v", indexes[1]) + } + if indexes[2].NonUnique != 0 { + t.Fatalf("expected UNIQUE metadata to map to NonUnique=0, got: %#v", indexes[2]) + } +} diff --git a/shared/i18n/de-DE.json b/shared/i18n/de-DE.json index 7d1b73ce..99263f60 100644 --- a/shared/i18n/de-DE.json +++ b/shared/i18n/de-DE.json @@ -8116,6 +8116,7 @@ "table_designer.message.index_updated": "Index aktualisiert", "table_designer.message.indexes_deleted": "{{count}} Indizes gelöscht", "table_designer.message.load_columns_failed": "Spalten konnten nicht geladen werden: {{detail}}", + "table_designer.message.load_indexes_failed": "Indizes konnten nicht geladen werden: {{detail}}", "table_designer.message.no_changes_detected": "Keine Änderungen erkannt", "table_designer.message.no_copyable_columns": "Keine kopierbaren Spalten ausgewählt", "table_designer.message.no_index_changes": "Keine Indexänderungen erkannt", diff --git a/shared/i18n/en-US.json b/shared/i18n/en-US.json index 3923d4e7..281162b3 100644 --- a/shared/i18n/en-US.json +++ b/shared/i18n/en-US.json @@ -8116,6 +8116,7 @@ "table_designer.message.index_updated": "Index updated", "table_designer.message.indexes_deleted": "{{count}} indexes deleted", "table_designer.message.load_columns_failed": "Failed to load columns: {{detail}}", + "table_designer.message.load_indexes_failed": "Failed to load indexes: {{detail}}", "table_designer.message.no_changes_detected": "No changes detected", "table_designer.message.no_copyable_columns": "No copyable columns selected", "table_designer.message.no_index_changes": "No index changes detected", diff --git a/shared/i18n/ja-JP.json b/shared/i18n/ja-JP.json index 6757de25..d99014d5 100644 --- a/shared/i18n/ja-JP.json +++ b/shared/i18n/ja-JP.json @@ -8116,6 +8116,7 @@ "table_designer.message.index_updated": "インデックスを更新しました", "table_designer.message.indexes_deleted": "{{count}} 件のインデックスを削除しました", "table_designer.message.load_columns_failed": "列の読み込みに失敗しました: {{detail}}", + "table_designer.message.load_indexes_failed": "インデックスの読み込みに失敗しました: {{detail}}", "table_designer.message.no_changes_detected": "変更は検出されませんでした", "table_designer.message.no_copyable_columns": "コピーできる列が選択されていません", "table_designer.message.no_index_changes": "インデックスの変更は検出されませんでした", diff --git a/shared/i18n/ru-RU.json b/shared/i18n/ru-RU.json index 6c5b64e0..dfc1f890 100644 --- a/shared/i18n/ru-RU.json +++ b/shared/i18n/ru-RU.json @@ -8116,6 +8116,7 @@ "table_designer.message.index_updated": "Индекс обновлен", "table_designer.message.indexes_deleted": "{{count}} индексов удалено", "table_designer.message.load_columns_failed": "Не удалось загрузить столбцы: {{detail}}", + "table_designer.message.load_indexes_failed": "Не удалось загрузить индексы: {{detail}}", "table_designer.message.no_changes_detected": "Изменения не обнаружены", "table_designer.message.no_copyable_columns": "Не выбраны столбцы для копирования", "table_designer.message.no_index_changes": "Изменения индекса не обнаружены", diff --git a/shared/i18n/zh-CN.json b/shared/i18n/zh-CN.json index 085fc476..a7cc44ac 100644 --- a/shared/i18n/zh-CN.json +++ b/shared/i18n/zh-CN.json @@ -8116,6 +8116,7 @@ "table_designer.message.index_updated": "索引修改成功", "table_designer.message.indexes_deleted": "{{count}} 个索引删除成功", "table_designer.message.load_columns_failed": "加载字段失败:{{detail}}", + "table_designer.message.load_indexes_failed": "加载索引失败:{{detail}}", "table_designer.message.no_changes_detected": "没有检测到变更", "table_designer.message.no_copyable_columns": "未选择可复制字段", "table_designer.message.no_index_changes": "没有检测到索引变更", diff --git a/shared/i18n/zh-TW.json b/shared/i18n/zh-TW.json index 932cec31..4cb1d23c 100644 --- a/shared/i18n/zh-TW.json +++ b/shared/i18n/zh-TW.json @@ -8116,6 +8116,7 @@ "table_designer.message.index_updated": "索引修改成功", "table_designer.message.indexes_deleted": "{{count}} 個索引刪除成功", "table_designer.message.load_columns_failed": "載入欄位失敗:{{detail}}", + "table_designer.message.load_indexes_failed": "載入索引失敗:{{detail}}", "table_designer.message.no_changes_detected": "未偵測到變更", "table_designer.message.no_copyable_columns": "未選取可複製欄位", "table_designer.message.no_index_changes": "未偵測到索引變更",