🐛 fix(dameng): 修复索引元数据查询与错误提示

修正 ALL_IND_COLUMNS 与 ALL_INDEXES 的索引归属关联;保留复合索引顺序、唯一性与索引类型元数据;索引加载失败时显示错误,并补齐回归测试和多语言文案。

Fixes #735
This commit is contained in:
Syngnat
2026-07-27 22:57:06 +08:00
parent 8ed3fb9413
commit c192b6cc3a
13 changed files with 237 additions and 35 deletions

View File

@@ -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<IndexDefinition>(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);

View File

@@ -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);

View File

@@ -19,6 +19,33 @@ export interface SchemaExecutionSnapshot {
failedStatementIndex?: number;
}
export interface IndexMetadataResponse {
success: boolean;
data?: unknown;
message?: unknown;
}
export interface IndexMetadataResolution<T> {
indexes: T[];
errorDetail: string | null;
}
export const resolveIndexMetadataResponse = <T = unknown>(
response: IndexMetadataResponse,
): IndexMetadataResolution<T> => {
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[],

View File

@@ -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)
}
}

View File

@@ -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) {

View File

@@ -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))

View File

@@ -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])
}
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": "インデックスの変更は検出されませんでした",

View File

@@ -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": "Изменения индекса не обнаружены",

View File

@@ -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": "没有检测到索引变更",

View File

@@ -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": "未偵測到索引變更",