mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-09 16:23:27 +08:00
🐛 fix(dameng): 修复索引元数据查询与错误提示
修正 ALL_IND_COLUMNS 与 ALL_INDEXES 的索引归属关联;保留复合索引顺序、唯一性与索引类型元数据;索引加载失败时显示错误,并补齐回归测试和多语言文案。 Fixes #735
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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[],
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "インデックスの変更は検出されませんでした",
|
||||
|
||||
@@ -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": "Изменения индекса не обнаружены",
|
||||
|
||||
@@ -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": "没有检测到索引变更",
|
||||
|
||||
@@ -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": "未偵測到索引變更",
|
||||
|
||||
Reference in New Issue
Block a user