diff --git a/frontend/package.json.md5 b/frontend/package.json.md5
index 7396e24..bed8925 100755
--- a/frontend/package.json.md5
+++ b/frontend/package.json.md5
@@ -1 +1 @@
-d0464f9da25e9356e61652e638c99ffe
\ No newline at end of file
+0295a42fd931778d85157816d79d29e5
\ No newline at end of file
diff --git a/frontend/src/components/DataViewer.primary-key.test.tsx b/frontend/src/components/DataViewer.primary-key.test.tsx
index a9fb464..c32ca05 100644
--- a/frontend/src/components/DataViewer.primary-key.test.tsx
+++ b/frontend/src/components/DataViewer.primary-key.test.tsx
@@ -137,6 +137,26 @@ describe('DataViewer safe editing locator', () => {
renderer.unmount();
});
+ it('enables table preview editing when primary key metadata uses boolean aliases', async () => {
+ backendApp.DBGetColumns.mockResolvedValue({
+ success: true,
+ data: [{ column_name: 'ID', isPrimary: true }, { column_name: 'NAME' }],
+ });
+
+ const renderer = await renderAndReload();
+
+ expect(dataGridState.latestProps?.pkColumns).toEqual(['ID']);
+ expect(dataGridState.latestProps?.editLocator).toMatchObject({
+ strategy: 'primary-key',
+ columns: ['ID'],
+ valueColumns: ['ID'],
+ readOnly: false,
+ });
+ expect(dataGridState.latestProps?.readOnly).toBe(false);
+ expect(messageApi.warning).not.toHaveBeenCalled();
+ renderer.unmount();
+ });
+
it('uses a unique index when the table has no primary key', async () => {
backendApp.DBGetColumns.mockResolvedValue({
success: true,
diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx
index 66d4412..14f02b9 100644
--- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx
+++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx
@@ -1723,6 +1723,46 @@ describe('QueryEditor external SQL save', () => {
expect(messageApi.warning).not.toHaveBeenCalled();
});
+ it('uses snake_case unique index metadata for query result row locators', async () => {
+ storeState.connections[0].config.type = 'kingbase';
+ storeState.connections[0].config.database = 'KINGBASE';
+ backendApp.DBQueryMulti.mockResolvedValueOnce({
+ success: true,
+ data: [{ columns: ['NAME', '__gonavi_locator_1_EMAIL'], rows: [{ NAME: 'old-name', __gonavi_locator_1_EMAIL: 'a@example.com' }] }],
+ });
+ backendApp.DBGetColumns.mockResolvedValueOnce({
+ success: true,
+ data: [{ column_name: 'EMAIL' }, { column_name: 'NAME' }],
+ });
+ backendApp.DBGetIndexes.mockResolvedValueOnce({
+ success: true,
+ data: [{ index_name: 'users_email_key', column_name: 'EMAIL', is_unique: 't', seq_in_index: '1', index_type: 'btree' }],
+ });
+
+ let renderer: ReactTestRenderer;
+ await act(async () => {
+ renderer = create();
+ });
+
+ await act(async () => {
+ await findButton(renderer!, '运行').props.onClick();
+ });
+ await act(async () => {
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ expect(dataGridState.latestProps?.editLocator).toMatchObject({
+ strategy: 'unique-key',
+ columns: ['EMAIL'],
+ valueColumns: ['__gonavi_locator_1_EMAIL'],
+ hiddenColumns: ['__gonavi_locator_1_EMAIL'],
+ readOnly: false,
+ });
+ expect(dataGridState.latestProps?.readOnly).toBe(false);
+ expect(messageApi.warning).not.toHaveBeenCalled();
+ });
+
it('uses hidden Oracle ROWID for query results without primary or unique keys', async () => {
storeState.connections[0].config.type = 'oracle';
storeState.connections[0].config.database = 'ORCLPDB1';
diff --git a/frontend/src/components/dataGridCopyInsert.test.ts b/frontend/src/components/dataGridCopyInsert.test.ts
index 534b8dc..40a0b91 100644
--- a/frontend/src/components/dataGridCopyInsert.test.ts
+++ b/frontend/src/components/dataGridCopyInsert.test.ts
@@ -108,6 +108,20 @@ describe('buildCopyInsertSQL', () => {
]);
});
+ it('accepts non-MySQL index metadata aliases when resolving safe unique locators', () => {
+ expect(resolveUniqueKeyGroupsFromIndexes([
+ { index_name: 'events_slug_key', column_name: 'slug', is_unique: 't', seq_in_index: '1', index_type: 'BTREE' } as any,
+ { INDEX_NAME: 'events_tenant_code_key', COLUMN_NAME: 'code', UNIQUE: true, COLUMN_POSITION: 2 } as any,
+ { INDEX_NAME: 'events_tenant_code_key', COLUMN_NAME: 'tenant_id', UNIQUE: true, COLUMN_POSITION: 1 } as any,
+ { indexName: 'events_ext_key', columnName: 'external_id', indexType: 'UNIQUE' } as any,
+ { index_name: 'idx_note', column_name: 'note', non_unique: '1' } as any,
+ ])).toEqual([
+ ['slug'],
+ ['tenant_id', 'code'],
+ ['external_id'],
+ ]);
+ });
+
it('builds UPDATE SQL with a primary-key WHERE clause and keeps literal formatting aligned with INSERT', () => {
const result = buildCopyUpdateSQL({
dbType: 'mysql',
diff --git a/frontend/src/components/dataGridCopyInsert.ts b/frontend/src/components/dataGridCopyInsert.ts
index 2287b45..2b363d1 100644
--- a/frontend/src/components/dataGridCopyInsert.ts
+++ b/frontend/src/components/dataGridCopyInsert.ts
@@ -407,13 +407,59 @@ export const resolveUniqueKeyGroupsFromIndexes = (indexes: IndexDefinition[] | u
columns: Array<{ columnName: string; seqInIndex: number; order: number }>;
};
+ const readIndexProp = (index: unknown, keys: string[]): unknown => {
+ const source = index as Record | null | undefined;
+ if (!source || typeof source !== 'object') return undefined;
+ for (const key of keys) {
+ if (source[key] !== undefined && source[key] !== null) return source[key];
+ }
+ for (const [sourceKey, raw] of Object.entries(source)) {
+ if (keys.some((key) => sourceKey.toLowerCase() === key.toLowerCase())) {
+ return raw;
+ }
+ }
+ return undefined;
+ };
+
+ const readIndexText = (index: unknown, keys: string[]): string => {
+ const raw = readIndexProp(index, keys);
+ return raw === undefined || raw === null ? '' : String(raw).trim();
+ };
+
+ const readIndexBool = (index: unknown, keys: string[]): boolean | undefined => {
+ const raw = readIndexProp(index, keys);
+ if (raw === undefined || raw === null) return undefined;
+ if (typeof raw === 'boolean') return raw;
+ if (typeof raw === 'number') return raw !== 0;
+ const text = String(raw).trim().toLowerCase();
+ if (['1', 't', 'true', 'y', 'yes', 'unique'].includes(text)) return true;
+ if (['0', 'f', 'false', 'n', 'no', 'nonunique', 'non-unique'].includes(text)) return false;
+ return undefined;
+ };
+
+ const isUniqueIndex = (index: unknown): boolean => {
+ const nonUniqueRaw = readIndexProp(index, ['nonUnique', 'NonUnique', 'non_unique', 'NON_UNIQUE', 'Non_unique']);
+ if (nonUniqueRaw !== undefined && nonUniqueRaw !== null) {
+ if (typeof nonUniqueRaw === 'number') return nonUniqueRaw === 0;
+ const text = String(nonUniqueRaw).trim().toLowerCase();
+ if (['0', 'false', 'f', 'no', 'n'].includes(text)) return true;
+ if (['1', 'true', 't', 'yes', 'y'].includes(text)) return false;
+ }
+ const unique = readIndexBool(index, ['isUnique', 'is_unique', 'IS_UNIQUE', 'unique', 'UNIQUE']);
+ if (unique !== undefined) return unique;
+ const uniqueness = readIndexText(index, ['uniqueness', 'UNIQUENESS']);
+ if (uniqueness.toLowerCase() === 'unique') return true;
+ const indexType = readIndexText(index, ['indexType', 'IndexType', 'index_type', 'INDEX_TYPE']);
+ return indexType.toLowerCase() === 'unique';
+ };
+
const buckets = new Map();
(indexes || []).forEach((index, order) => {
- if (index?.nonUnique !== 0) {
+ if (!isUniqueIndex(index)) {
return;
}
- const name = String(index?.name || '').trim();
- const columnName = String(index?.columnName || '').trim();
+ const name = readIndexText(index, ['name', 'Name', 'indexName', 'index_name', 'INDEX_NAME']);
+ const columnName = readIndexText(index, ['columnName', 'ColumnName', 'column_name', 'COLUMN_NAME']);
if (!name || !columnName) {
return;
}
@@ -426,7 +472,9 @@ export const resolveUniqueKeyGroupsFromIndexes = (indexes: IndexDefinition[] | u
}
bucket.columns.push({
columnName,
- seqInIndex: Number.isFinite(Number(index?.seqInIndex)) ? Number(index.seqInIndex) : 0,
+ seqInIndex: Number.isFinite(Number(readIndexProp(index, ['seqInIndex', 'SeqInIndex', 'seq_in_index', 'SEQ_IN_INDEX', 'columnPosition', 'column_position'])))
+ ? Number(readIndexProp(index, ['seqInIndex', 'SeqInIndex', 'seq_in_index', 'SEQ_IN_INDEX', 'columnPosition', 'column_position']))
+ : 0,
order,
});
});
diff --git a/frontend/src/utils/columnDefinition.test.ts b/frontend/src/utils/columnDefinition.test.ts
index 41122f5..cf8d6d6 100644
--- a/frontend/src/utils/columnDefinition.test.ts
+++ b/frontend/src/utils/columnDefinition.test.ts
@@ -28,4 +28,11 @@ describe('columnDefinition metadata normalization', () => {
comment: '更新时间',
});
});
+
+ it('maps boolean primary and unique metadata aliases to GoNavi keys', () => {
+ expect(getColumnDefinitionKey({ column_name: 'id', isPrimary: true })).toBe('PRI');
+ expect(getColumnDefinitionKey({ column_name: 'id', primary_key: 't' })).toBe('PRI');
+ expect(getColumnDefinitionKey({ column_name: 'email', is_unique: 'yes' })).toBe('UNI');
+ expect(getColumnDefinitionKey({ column_name: 'id', column_key: 'primary key' })).toBe('PRI');
+ });
});
diff --git a/frontend/src/utils/columnDefinition.ts b/frontend/src/utils/columnDefinition.ts
index c9e85cb..ca03235 100644
--- a/frontend/src/utils/columnDefinition.ts
+++ b/frontend/src/utils/columnDefinition.ts
@@ -20,6 +20,34 @@ const readStringProperty = (value: unknown, keys: string[]): string => {
return '';
};
+const readProperty = (value: unknown, keys: string[]): unknown => {
+ const source = value as Record | null | undefined;
+ if (!source || typeof source !== 'object') return undefined;
+
+ for (const key of keys) {
+ if (source[key] !== undefined && source[key] !== null) {
+ return source[key];
+ }
+ }
+
+ for (const [sourceKey, raw] of Object.entries(source)) {
+ if (keys.some((key) => sourceKey.toLowerCase() === key.toLowerCase())) {
+ return raw;
+ }
+ }
+
+ return undefined;
+};
+
+const readBooleanProperty = (value: unknown, keys: string[]): boolean => {
+ const raw = readProperty(value, keys);
+ if (raw === undefined || raw === null) return false;
+ if (typeof raw === 'boolean') return raw;
+ if (typeof raw === 'number') return raw !== 0;
+ const text = String(raw).trim().toLowerCase();
+ return text === '1' || text === 't' || text === 'true' || text === 'y' || text === 'yes' || text === 'pri' || text === 'primary';
+};
+
export const getColumnDefinitionName = (column: unknown): string => (
readStringProperty(column, ['name', 'Name', 'COLUMN_NAME', 'column_name', 'field', 'Field'])
);
@@ -28,9 +56,24 @@ export const getColumnDefinitionType = (column: unknown): string => (
readStringProperty(column, ['type', 'Type', 'DATA_TYPE', 'data_type'])
);
-export const getColumnDefinitionKey = (column: unknown): string => (
- readStringProperty(column, ['key', 'Key', 'COLUMN_KEY', 'column_key'])
-);
+export const getColumnDefinitionKey = (column: unknown): string => {
+ const key = readStringProperty(column, ['key', 'Key', 'COLUMN_KEY', 'column_key']);
+ if (key) {
+ const normalized = key.trim();
+ const lowered = normalized.toLowerCase();
+ if (lowered === 'pri' || lowered === 'primary' || lowered === 'primary key') return 'PRI';
+ if (lowered === 'uni' || lowered === 'unique') return 'UNI';
+ if (lowered === 'mul' || lowered === 'multiple') return 'MUL';
+ return normalized;
+ }
+ if (readBooleanProperty(column, ['primaryKey', 'primary_key', 'isPrimary', 'is_primary', 'IS_PRIMARY', 'pk', 'PK'])) {
+ return 'PRI';
+ }
+ if (readBooleanProperty(column, ['unique', 'isUnique', 'is_unique', 'UNIQUE', 'IS_UNIQUE'])) {
+ return 'UNI';
+ }
+ return '';
+};
export const getColumnDefinitionExtra = (column: unknown): string => (
readStringProperty(column, ['extra', 'Extra'])
diff --git a/internal/app/db_context.go b/internal/app/db_context.go
index a279bf7..cc0424e 100644
--- a/internal/app/db_context.go
+++ b/internal/app/db_context.go
@@ -75,7 +75,7 @@ func normalizeSchemaAndTable(config connection.ConnectionConfig, dbName string,
return schema, table
}
if table != "" {
- return "public", table
+ return "", table
}
}
@@ -99,10 +99,34 @@ func normalizeSchemaAndTable(config connection.ConnectionConfig, dbName string,
switch dbType {
case "postgres", "kingbase", "highgo", "vastbase", "opengauss":
- // PG/金仓/瀚高/海量:dbName 在 UI 里是"数据库",schema 需从 tableName 或使用默认 public。
+ // PG/金仓/瀚高/海量:dbName 在 UI 里是"数据库",未限定 schema 的普通导出/DDL 路径沿用 public。
return "public", rawTable
default:
// MySQL:dbName 表示数据库;Oracle/达梦:dbName 表示 schema/owner。
return rawDB, rawTable
}
}
+
+func normalizeMetadataSchemaAndTable(config connection.ConnectionConfig, dbName string, tableName string) (string, string) {
+ schema, table := normalizeSchemaAndTable(config, dbName, tableName)
+ switch resolveDDLDBType(config) {
+ case "postgres", "kingbase", "highgo", "vastbase", "opengauss":
+ rawTable := strings.TrimSpace(tableName)
+ if rawTable == "" {
+ return schema, table
+ }
+ parsedSchema, parsedTable := db.SplitSQLQualifiedName(rawTable)
+ if parsedTable != "" {
+ if parsedSchema != "" {
+ return parsedSchema, parsedTable
+ }
+ return "", parsedTable
+ }
+ if strings.Contains(rawTable, ".") {
+ return schema, table
+ }
+ return "", table
+ default:
+ return schema, table
+ }
+}
diff --git a/internal/app/db_context_test.go b/internal/app/db_context_test.go
index 48a6d19..bcc6463 100644
--- a/internal/app/db_context_test.go
+++ b/internal/app/db_context_test.go
@@ -62,6 +62,132 @@ func TestNormalizeSchemaAndTable_KingbaseNormalizesEscapedQualifiedName(t *testi
}
}
+func TestNormalizeSchemaAndTable_KingbasePureTableUsesCurrentSearchPath(t *testing.T) {
+ t.Parallel()
+
+ schema, table := normalizeSchemaAndTable(connection.ConnectionConfig{
+ Type: "kingbase",
+ }, "demo_db", "users")
+
+ if schema != "" || table != "users" {
+ t.Fatalf("expected kingbase pure table to use current search_path, got %q.%q", schema, table)
+ }
+}
+
+func TestNormalizeSchemaAndTable_PGLikePureTableKeepsPublicFallback(t *testing.T) {
+ t.Parallel()
+
+ for _, dbType := range []string{"postgres", "highgo", "vastbase", "opengauss"} {
+ t.Run(dbType, func(t *testing.T) {
+ t.Parallel()
+
+ schema, table := normalizeSchemaAndTable(connection.ConnectionConfig{
+ Type: dbType,
+ }, "demo_db", "users")
+
+ if schema != "public" || table != "users" {
+ t.Fatalf("expected %s pure table to keep public fallback, got %q.%q", dbType, schema, table)
+ }
+ })
+ }
+}
+
+func TestNormalizeMetadataSchemaAndTable_PGLikePureTableUsesSearchPath(t *testing.T) {
+ t.Parallel()
+
+ for _, dbType := range []string{"postgres", "highgo", "vastbase", "opengauss", "kingbase"} {
+ t.Run(dbType, func(t *testing.T) {
+ t.Parallel()
+
+ schema, table := normalizeMetadataSchemaAndTable(connection.ConnectionConfig{
+ Type: dbType,
+ }, "demo_db", "users")
+
+ if schema != "" || table != "users" {
+ t.Fatalf("expected %s metadata pure table to use search_path, got %q.%q", dbType, schema, table)
+ }
+ })
+ }
+}
+
+func TestNormalizeMetadataSchemaAndTable_PGLikeQualifiedTableKeepsSchema(t *testing.T) {
+ t.Parallel()
+
+ schema, table := normalizeMetadataSchemaAndTable(connection.ConnectionConfig{
+ Type: "postgres",
+ }, "demo_db", `"audit.schema"."order.items"`)
+
+ if schema != "audit.schema" || table != "order.items" {
+ t.Fatalf("expected metadata qualified table to keep schema/table, got %q.%q", schema, table)
+ }
+}
+
+func TestNormalizeMetadataSchemaAndTable_PGLikeDottedUnquotedTableKeepsFallback(t *testing.T) {
+ t.Parallel()
+
+ schema, table := normalizeMetadataSchemaAndTable(connection.ConnectionConfig{
+ Type: "postgres",
+ }, "demo_db", "audit.users")
+
+ if schema != "audit" || table != "users" {
+ t.Fatalf("expected metadata dotted table to keep explicit schema, got %q.%q", schema, table)
+ }
+}
+
+func TestNormalizeMetadataSchemaAndTable_PGLikeQuotedDottedTableUsesSearchPath(t *testing.T) {
+ t.Parallel()
+
+ schema, table := normalizeMetadataSchemaAndTable(connection.ConnectionConfig{
+ Type: "postgres",
+ }, "demo_db", `"order.items"`)
+
+ if schema != "" || table != "order.items" {
+ t.Fatalf("expected quoted dotted metadata table to use search_path, got %q.%q", schema, table)
+ }
+}
+
+func TestNormalizeMetadataSchemaAndTable_NonPGLikeKeepsNormalBehavior(t *testing.T) {
+ t.Parallel()
+
+ schema, table := normalizeMetadataSchemaAndTable(connection.ConnectionConfig{
+ Type: "mysql",
+ }, "demo_db", "users")
+
+ if schema != "demo_db" || table != "users" {
+ t.Fatalf("expected mysql metadata to keep db/table behavior, got %q.%q", schema, table)
+ }
+}
+
+func TestNormalizeSchemaAndTable_PGLikePureTableStillSplitsKingbaseSearchPathOnlyInMetadata(t *testing.T) {
+ t.Parallel()
+
+ schema, table := normalizeSchemaAndTable(connection.ConnectionConfig{
+ Type: "kingbase",
+ }, "demo_db", "users")
+
+ if schema != "" || table != "users" {
+ t.Fatalf("expected kingbase normal path to keep existing search_path behavior, got %q.%q", schema, table)
+ }
+}
+
+func TestNormalizeMetadataSchemaAndTable_PGLikePreservesNormalFallbackForQuotedQualifiedTable(t *testing.T) {
+ t.Parallel()
+
+ for _, dbType := range []string{"highgo", "vastbase", "opengauss"} {
+ t.Run(dbType, func(t *testing.T) {
+ t.Parallel()
+
+ schema, table := normalizeMetadataSchemaAndTable(connection.ConnectionConfig{
+ Type: dbType,
+ }, "demo_db", `"audit"."users"`)
+
+ if schema != "audit" || table != "users" {
+ t.Fatalf("expected %s metadata qualified table to keep schema, got %q.%q", dbType, schema, table)
+ }
+ })
+ }
+}
+
func TestNormalizeRunConfig_OceanBaseOracleKeepsServiceName(t *testing.T) {
t.Parallel()
diff --git a/internal/app/methods_db.go b/internal/app/methods_db.go
index 08be907..9a15d3a 100644
--- a/internal/app/methods_db.go
+++ b/internal/app/methods_db.go
@@ -1444,7 +1444,7 @@ func (a *App) DBGetColumns(config connection.ConnectionConfig, dbName string, ta
return connection.QueryResult{Success: false, Message: err.Error()}
}
- schemaName, pureTableName := normalizeSchemaAndTable(config, dbName, tableName)
+ schemaName, pureTableName := normalizeMetadataSchemaAndTable(config, dbName, tableName)
columns, err := dbInst.GetColumns(schemaName, pureTableName)
if err != nil && shouldRefreshCachedConnection(err) {
if a.invalidateCachedDatabase(runConfig, err) {
@@ -1473,7 +1473,7 @@ func (a *App) DBGetIndexes(config connection.ConnectionConfig, dbName string, ta
return connection.QueryResult{Success: false, Message: err.Error()}
}
- schemaName, pureTableName := normalizeSchemaAndTable(config, dbName, tableName)
+ schemaName, pureTableName := normalizeMetadataSchemaAndTable(config, dbName, tableName)
indexes, err := dbInst.GetIndexes(schemaName, pureTableName)
if err != nil && shouldRefreshCachedConnection(err) {
if a.invalidateCachedDatabase(runConfig, err) {
diff --git a/internal/app/methods_db_metadata_retry_test.go b/internal/app/methods_db_metadata_retry_test.go
index 19b1183..fefbf3b 100644
--- a/internal/app/methods_db_metadata_retry_test.go
+++ b/internal/app/methods_db_metadata_retry_test.go
@@ -10,12 +10,16 @@ import (
)
type fakeMetadataRetryDB struct {
- columns []connection.ColumnDefinition
- indexes []connection.IndexDefinition
- columnsErr error
- indexesErr error
- columnCalls int
- indexCalls int
+ columns []connection.ColumnDefinition
+ indexes []connection.IndexDefinition
+ columnsErr error
+ indexesErr error
+ columnCalls int
+ indexCalls int
+ columnSchema string
+ columnTable string
+ indexSchema string
+ indexTable string
}
func (f *fakeMetadataRetryDB) Connect(config connection.ConnectionConfig) error { return nil }
@@ -34,6 +38,8 @@ func (f *fakeMetadataRetryDB) GetCreateStatement(dbName, tableName string) (stri
}
func (f *fakeMetadataRetryDB) GetColumns(dbName, tableName string) ([]connection.ColumnDefinition, error) {
f.columnCalls++
+ f.columnSchema = dbName
+ f.columnTable = tableName
if f.columnsErr != nil {
return nil, f.columnsErr
}
@@ -44,6 +50,8 @@ func (f *fakeMetadataRetryDB) GetAllColumns(dbName string) ([]connection.ColumnD
}
func (f *fakeMetadataRetryDB) GetIndexes(dbName, tableName string) ([]connection.IndexDefinition, error) {
f.indexCalls++
+ f.indexSchema = dbName
+ f.indexTable = tableName
if f.indexesErr != nil {
return nil, f.indexesErr
}
@@ -112,6 +120,110 @@ func TestDBGetColumnsRetriesAfterCachedConnectionRefresh(t *testing.T) {
}
}
+func TestDBGetColumnsUsesSearchPathForPostgresPureTableMetadata(t *testing.T) {
+ originalNewDatabaseFunc := newDatabaseFunc
+ originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc
+ t.Cleanup(func() {
+ newDatabaseFunc = originalNewDatabaseFunc
+ resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc
+ })
+
+ dbInst := &fakeMetadataRetryDB{
+ columns: []connection.ColumnDefinition{{Name: "id", Key: "PRI"}},
+ }
+ newDatabaseFunc = func(dbType string) (db.Database, error) {
+ return dbInst, nil
+ }
+ resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) {
+ return raw, nil
+ }
+
+ app := NewAppWithSecretStore(secretstore.NewUnavailableStore("test"))
+ result := app.DBGetColumns(connection.ConnectionConfig{
+ Type: "postgres",
+ Host: "127.0.0.1",
+ Port: 5432,
+ User: "postgres",
+ Database: "demo_db",
+ }, "demo_db", "users")
+
+ if !result.Success {
+ t.Fatalf("expected DBGetColumns success, got failure: %s", result.Message)
+ }
+ if dbInst.columnSchema != "" || dbInst.columnTable != "users" {
+ t.Fatalf("expected postgres pure table metadata to pass empty schema/users, got %q.%q", dbInst.columnSchema, dbInst.columnTable)
+ }
+}
+
+func TestDBGetIndexesUsesSearchPathForPostgresPureTableMetadata(t *testing.T) {
+ originalNewDatabaseFunc := newDatabaseFunc
+ originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc
+ t.Cleanup(func() {
+ newDatabaseFunc = originalNewDatabaseFunc
+ resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc
+ })
+
+ dbInst := &fakeMetadataRetryDB{
+ indexes: []connection.IndexDefinition{{Name: "users_email_key", ColumnName: "email", NonUnique: 0}},
+ }
+ newDatabaseFunc = func(dbType string) (db.Database, error) {
+ return dbInst, nil
+ }
+ resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) {
+ return raw, nil
+ }
+
+ app := NewAppWithSecretStore(secretstore.NewUnavailableStore("test"))
+ result := app.DBGetIndexes(connection.ConnectionConfig{
+ Type: "postgres",
+ Host: "127.0.0.1",
+ Port: 5432,
+ User: "postgres",
+ Database: "demo_db",
+ }, "demo_db", "users")
+
+ if !result.Success {
+ t.Fatalf("expected DBGetIndexes success, got failure: %s", result.Message)
+ }
+ if dbInst.indexSchema != "" || dbInst.indexTable != "users" {
+ t.Fatalf("expected postgres pure table index metadata to pass empty schema/users, got %q.%q", dbInst.indexSchema, dbInst.indexTable)
+ }
+}
+
+func TestDBGetColumnsKeepsDatabaseForMySQLMetadata(t *testing.T) {
+ originalNewDatabaseFunc := newDatabaseFunc
+ originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc
+ t.Cleanup(func() {
+ newDatabaseFunc = originalNewDatabaseFunc
+ resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc
+ })
+
+ dbInst := &fakeMetadataRetryDB{
+ columns: []connection.ColumnDefinition{{Name: "id", Key: "PRI"}},
+ }
+ newDatabaseFunc = func(dbType string) (db.Database, error) {
+ return dbInst, nil
+ }
+ resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) {
+ return raw, nil
+ }
+
+ app := NewAppWithSecretStore(secretstore.NewUnavailableStore("test"))
+ result := app.DBGetColumns(connection.ConnectionConfig{
+ Type: "mysql",
+ Host: "127.0.0.1",
+ Port: 3306,
+ User: "root",
+ }, "demo_db", "users")
+
+ if !result.Success {
+ t.Fatalf("expected DBGetColumns success, got failure: %s", result.Message)
+ }
+ if dbInst.columnSchema != "demo_db" || dbInst.columnTable != "users" {
+ t.Fatalf("expected mysql metadata to pass database/table, got %q.%q", dbInst.columnSchema, dbInst.columnTable)
+ }
+}
+
func TestDBGetIndexesRetriesAfterCachedConnectionRefresh(t *testing.T) {
originalNewDatabaseFunc := newDatabaseFunc
originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc
diff --git a/internal/db/duckdb_metadata.go b/internal/db/duckdb_metadata.go
index e24790b..c7edca5 100644
--- a/internal/db/duckdb_metadata.go
+++ b/internal/db/duckdb_metadata.go
@@ -200,6 +200,12 @@ func normalizeDuckDBObjectPath(dbName string, tableName string) duckDBObjectPath
case 2:
if rawDB != "" {
dbParts := splitDuckDBQualifiedName(rawDB)
+ if len(dbParts) == 1 && (strings.EqualFold(dbParts[0], "main") || strings.EqualFold(dbParts[0], "memory")) {
+ return duckDBObjectPath{
+ Schema: normalizeDuckDBIdentifier(parts[0]),
+ Object: normalizeDuckDBIdentifier(parts[1]),
+ }
+ }
if len(dbParts) == 1 {
return duckDBObjectPath{
Catalog: normalizeDuckDBIdentifier(dbParts[0]),
diff --git a/internal/db/duckdb_metadata_test.go b/internal/db/duckdb_metadata_test.go
index 0b2b7c3..9d3e4f4 100644
--- a/internal/db/duckdb_metadata_test.go
+++ b/internal/db/duckdb_metadata_test.go
@@ -226,6 +226,20 @@ func TestNormalizeDuckDBObjectPath_PreservesCatalogSchemaAndQuotedDots(t *testin
}
}
+func TestNormalizeDuckDBObjectPath_DoesNotTreatMainDatabaseAsExternalCatalog(t *testing.T) {
+ t.Parallel()
+
+ path := normalizeDuckDBObjectPath("main", "main.events")
+ if path.Catalog != "" || path.Schema != "main" || path.Object != "events" {
+ t.Fatalf("unexpected duckdb main path: %+v", path)
+ }
+
+ memoryPath := normalizeDuckDBObjectPath("memory", "main.events")
+ if memoryPath.Catalog != "" || memoryPath.Schema != "main" || memoryPath.Object != "events" {
+ t.Fatalf("unexpected duckdb memory path: %+v", memoryPath)
+ }
+}
+
func TestParseDuckDBExpressionList_KeepsQuotedExpressionsIntact(t *testing.T) {
t.Parallel()
diff --git a/internal/db/highgo_impl.go b/internal/db/highgo_impl.go
index 492d389..bfe68b9 100644
--- a/internal/db/highgo_impl.go
+++ b/internal/db/highgo_impl.go
@@ -250,178 +250,31 @@ func (h *HighGoDB) GetCreateStatement(dbName, tableName string) (string, error)
}
func (h *HighGoDB) GetColumns(dbName, tableName string) ([]connection.ColumnDefinition, error) {
- schema := strings.TrimSpace(dbName)
- if schema == "" {
- schema = "public"
- }
- table := strings.TrimSpace(tableName)
+ schema, table := normalizePGLikeMetadataTable(dbName, tableName)
if table == "" {
return nil, fmt.Errorf("表名不能为空")
}
- esc := func(s string) string { return strings.ReplaceAll(s, "'", "''") }
-
- query := fmt.Sprintf(`
-SELECT
- a.attname AS column_name,
- pg_catalog.format_type(a.atttypid, a.atttypmod) AS data_type,
- CASE WHEN a.attnotnull THEN 'NO' ELSE 'YES' END AS is_nullable,
- pg_get_expr(ad.adbin, ad.adrelid) AS column_default,
- col_description(a.attrelid, a.attnum) AS comment,
- CASE WHEN pk.attname IS NOT NULL THEN 'PRI' ELSE '' END AS column_key
-FROM pg_class c
-JOIN pg_namespace n ON n.oid = c.relnamespace
-JOIN pg_attribute a ON a.attrelid = c.oid
-LEFT JOIN pg_attrdef ad ON ad.adrelid = c.oid AND ad.adnum = a.attnum
-LEFT JOIN (
- SELECT i.indrelid, a3.attname
- FROM pg_index i
- JOIN pg_attribute a3 ON a3.attrelid = i.indrelid AND a3.attnum = ANY(i.indkey)
- WHERE i.indisprimary
-) pk ON pk.indrelid = c.oid AND pk.attname = a.attname
-WHERE c.relkind IN ('r', 'p')
- AND n.nspname = '%s'
- AND c.relname = '%s'
- AND a.attnum > 0
- AND NOT a.attisdropped
-ORDER BY a.attnum`, esc(schema), esc(table))
-
- data, _, err := h.Query(query)
+ data, _, err := h.Query(buildPGLikeColumnsMetadataQuery(schema, table))
if err != nil {
return nil, err
}
- var columns []connection.ColumnDefinition
- for _, row := range data {
- col := connection.ColumnDefinition{
- Name: fmt.Sprintf("%v", row["column_name"]),
- Type: fmt.Sprintf("%v", row["data_type"]),
- Nullable: fmt.Sprintf("%v", row["is_nullable"]),
- Key: fmt.Sprintf("%v", row["column_key"]),
- Extra: "",
- Comment: "",
- }
-
- if v, ok := row["comment"]; ok && v != nil {
- col.Comment = fmt.Sprintf("%v", v)
- }
-
- if v, ok := row["column_default"]; ok && v != nil {
- def := fmt.Sprintf("%v", v)
- col.Default = &def
- if strings.HasPrefix(strings.ToLower(strings.TrimSpace(def)), "nextval(") {
- col.Extra = "auto_increment"
- }
- }
-
- columns = append(columns, col)
- }
- return columns, nil
+ return buildPGLikeColumnDefinitions(data), nil
}
func (h *HighGoDB) GetIndexes(dbName, tableName string) ([]connection.IndexDefinition, error) {
- schema := strings.TrimSpace(dbName)
- if schema == "" {
- schema = "public"
- }
- table := strings.TrimSpace(tableName)
+ schema, table := normalizePGLikeMetadataTable(dbName, tableName)
if table == "" {
return nil, fmt.Errorf("表名不能为空")
}
- esc := func(s string) string { return strings.ReplaceAll(s, "'", "''") }
-
- query := fmt.Sprintf(`
-SELECT
- i.relname AS index_name,
- a.attname AS column_name,
- ix.indisunique AS is_unique,
- x.ordinality AS seq_in_index,
- am.amname AS index_type
-FROM pg_class t
-JOIN pg_namespace n ON n.oid = t.relnamespace
-JOIN pg_index ix ON t.oid = ix.indrelid
-JOIN pg_class i ON i.oid = ix.indexrelid
-JOIN pg_am am ON i.relam = am.oid
-JOIN unnest(ix.indkey) WITH ORDINALITY AS x(attnum, ordinality) ON TRUE
-JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = x.attnum
-WHERE t.relkind IN ('r', 'p')
- AND t.relname = '%s'
- AND n.nspname = '%s'
-ORDER BY i.relname, x.ordinality`, esc(table), esc(schema))
-
- data, _, err := h.Query(query)
+ data, _, err := h.Query(buildPGLikeIndexesMetadataQuery(schema, table))
if err != nil {
return nil, err
}
- parseBool := func(v interface{}) bool {
- switch val := v.(type) {
- case bool:
- return val
- case string:
- s := strings.ToLower(strings.TrimSpace(val))
- return s == "t" || s == "true" || s == "1" || s == "y" || s == "yes"
- default:
- s := strings.ToLower(strings.TrimSpace(fmt.Sprintf("%v", v)))
- return s == "t" || s == "true" || s == "1" || s == "y" || s == "yes"
- }
- }
-
- parseInt := func(v interface{}) int {
- switch val := v.(type) {
- case int:
- return val
- case int64:
- return int(val)
- case float64:
- return int(val)
- case string:
- var n int
- _, _ = fmt.Sscanf(strings.TrimSpace(val), "%d", &n)
- return n
- default:
- var n int
- _, _ = fmt.Sscanf(strings.TrimSpace(fmt.Sprintf("%v", v)), "%d", &n)
- return n
- }
- }
-
- var indexes []connection.IndexDefinition
- for _, row := range data {
- isUnique := false
- if v, ok := row["is_unique"]; ok && v != nil {
- isUnique = parseBool(v)
- }
-
- nonUnique := 1
- if isUnique {
- nonUnique = 0
- }
-
- seq := 0
- if v, ok := row["seq_in_index"]; ok && v != nil {
- seq = parseInt(v)
- }
-
- indexType := ""
- if v, ok := row["index_type"]; ok && v != nil {
- indexType = strings.ToUpper(fmt.Sprintf("%v", v))
- }
- if indexType == "" {
- indexType = "BTREE"
- }
-
- idx := connection.IndexDefinition{
- Name: fmt.Sprintf("%v", row["index_name"]),
- ColumnName: fmt.Sprintf("%v", row["column_name"]),
- NonUnique: nonUnique,
- SeqInIndex: seq,
- IndexType: indexType,
- }
- indexes = append(indexes, idx)
- }
- return indexes, nil
+ return buildPGLikeIndexDefinitions(data), nil
}
func (h *HighGoDB) GetForeignKeys(dbName, tableName string) ([]connection.ForeignKeyDefinition, error) {
diff --git a/internal/db/kingbase_impl.go b/internal/db/kingbase_impl.go
index 6f508ad..161a222 100644
--- a/internal/db/kingbase_impl.go
+++ b/internal/db/kingbase_impl.go
@@ -443,264 +443,31 @@ func (k *KingbaseDB) GetCreateStatement(dbName, tableName string) (string, error
}
func (k *KingbaseDB) GetColumns(dbName, tableName string) ([]connection.ColumnDefinition, error) {
- // 解析 schema.table 格式
- schema := strings.TrimSpace(dbName)
- table := strings.TrimSpace(tableName)
-
- // 如果 tableName 包含 schema (格式: schema.table)
- if parts := strings.SplitN(table, ".", 2); len(parts) == 2 {
- parsedSchema := strings.TrimSpace(parts[0])
- parsedTable := strings.TrimSpace(parts[1])
- if parsedSchema != "" && parsedTable != "" {
- schema = parsedSchema
- table = parsedTable
- }
- }
-
- // 如果仍然没有 schema,使用 current_schema()
- // 这样可以自动匹配当前连接的 search_path
- if schema == "" {
- return k.getColumnsWithCurrentSchema(table)
- }
-
+ schema, table := normalizePGLikeMetadataTable(dbName, tableName)
if table == "" {
return nil, fmt.Errorf("表名不能为空")
}
- // 转义函数:处理单引号,移除双引号
- esc := func(s string) string {
- // 移除前后的双引号(如果存在)
- s = strings.Trim(s, "\"")
- // 转义单引号
- return strings.ReplaceAll(s, "'", "''")
- }
-
- query := fmt.Sprintf(`
-SELECT
- a.attname AS column_name,
- pg_catalog.format_type(a.atttypid, a.atttypmod) AS data_type,
- CASE WHEN a.attnotnull THEN 'NO' ELSE 'YES' END AS is_nullable,
- pg_get_expr(ad.adbin, ad.adrelid) AS column_default,
- col_description(a.attrelid, a.attnum) AS comment,
- CASE WHEN pk.attname IS NOT NULL THEN 'PRI' ELSE '' END AS column_key
-FROM pg_class c
-JOIN pg_namespace n ON n.oid = c.relnamespace
-JOIN pg_attribute a ON a.attrelid = c.oid
-LEFT JOIN pg_attrdef ad ON ad.adrelid = c.oid AND ad.adnum = a.attnum
-LEFT JOIN (
- SELECT i.indrelid, a3.attname
- FROM pg_index i
- JOIN pg_attribute a3 ON a3.attrelid = i.indrelid AND a3.attnum = ANY(i.indkey)
- WHERE i.indisprimary
-) pk ON pk.indrelid = c.oid AND pk.attname = a.attname
-WHERE c.relkind IN ('r', 'p')
- AND n.nspname = '%s'
- AND c.relname = '%s'
- AND a.attnum > 0
- AND NOT a.attisdropped
-ORDER BY a.attnum`, esc(schema), esc(table))
-
- data, _, err := k.Query(query)
+ data, _, err := k.Query(buildPGLikeColumnsMetadataQuery(schema, table))
if err != nil {
return nil, err
}
- var columns []connection.ColumnDefinition
- for _, row := range data {
- col := connection.ColumnDefinition{
- Name: fmt.Sprintf("%v", row["column_name"]),
- Type: fmt.Sprintf("%v", row["data_type"]),
- Nullable: fmt.Sprintf("%v", row["is_nullable"]),
- Key: fmt.Sprintf("%v", row["column_key"]),
- Extra: "",
- Comment: "",
- }
-
- if row["column_default"] != nil {
- def := fmt.Sprintf("%v", row["column_default"])
- col.Default = &def
- if strings.HasPrefix(strings.ToLower(strings.TrimSpace(def)), "nextval(") {
- col.Extra = "auto_increment"
- }
- }
-
- if v, ok := row["comment"]; ok && v != nil {
- col.Comment = fmt.Sprintf("%v", v)
- }
-
- columns = append(columns, col)
- }
- return columns, nil
-}
-
-// getColumnsWithCurrentSchema 使用 current_schema() 查询当前schema的表
-func (k *KingbaseDB) getColumnsWithCurrentSchema(tableName string) ([]connection.ColumnDefinition, error) {
- table := strings.TrimSpace(tableName)
- if table == "" {
- return nil, fmt.Errorf("表名不能为空")
- }
-
- // 转义函数
- esc := func(s string) string {
- s = strings.Trim(s, "\"")
- return strings.ReplaceAll(s, "'", "''")
- }
-
- // 使用 current_schema() 获取当前schema
- query := fmt.Sprintf(`
-SELECT
- a.attname AS column_name,
- pg_catalog.format_type(a.atttypid, a.atttypmod) AS data_type,
- CASE WHEN a.attnotnull THEN 'NO' ELSE 'YES' END AS is_nullable,
- pg_get_expr(ad.adbin, ad.adrelid) AS column_default,
- col_description(a.attrelid, a.attnum) AS comment,
- CASE WHEN pk.attname IS NOT NULL THEN 'PRI' ELSE '' END AS column_key
-FROM pg_class c
-JOIN pg_namespace n ON n.oid = c.relnamespace
-JOIN pg_attribute a ON a.attrelid = c.oid
-LEFT JOIN pg_attrdef ad ON ad.adrelid = c.oid AND ad.adnum = a.attnum
-LEFT JOIN (
- SELECT i.indrelid, a3.attname
- FROM pg_index i
- JOIN pg_attribute a3 ON a3.attrelid = i.indrelid AND a3.attnum = ANY(i.indkey)
- WHERE i.indisprimary
-) pk ON pk.indrelid = c.oid AND pk.attname = a.attname
-WHERE c.relkind IN ('r', 'p')
- AND n.nspname = current_schema()
- AND c.relname = '%s'
- AND a.attnum > 0
- AND NOT a.attisdropped
-ORDER BY a.attnum`, esc(table))
-
- data, _, err := k.Query(query)
- if err != nil {
- return nil, err
- }
-
- var columns []connection.ColumnDefinition
- for _, row := range data {
- col := connection.ColumnDefinition{
- Name: fmt.Sprintf("%v", row["column_name"]),
- Type: fmt.Sprintf("%v", row["data_type"]),
- Nullable: fmt.Sprintf("%v", row["is_nullable"]),
- Key: fmt.Sprintf("%v", row["column_key"]),
- Extra: "",
- Comment: "",
- }
-
- if row["column_default"] != nil {
- def := fmt.Sprintf("%v", row["column_default"])
- col.Default = &def
- if strings.HasPrefix(strings.ToLower(strings.TrimSpace(def)), "nextval(") {
- col.Extra = "auto_increment"
- }
- }
-
- if v, ok := row["comment"]; ok && v != nil {
- col.Comment = fmt.Sprintf("%v", v)
- }
-
- columns = append(columns, col)
- }
- return columns, nil
+ return buildPGLikeColumnDefinitions(data), nil
}
func (k *KingbaseDB) GetIndexes(dbName, tableName string) ([]connection.IndexDefinition, error) {
- // 解析 schema.table 格式
- schema := strings.TrimSpace(dbName)
- table := strings.TrimSpace(tableName)
-
- // 如果 tableName 包含 schema (格式: schema.table)
- if parts := strings.SplitN(table, ".", 2); len(parts) == 2 {
- parsedSchema := strings.TrimSpace(parts[0])
- parsedTable := strings.TrimSpace(parts[1])
- if parsedSchema != "" && parsedTable != "" {
- schema = parsedSchema
- table = parsedTable
- }
- }
-
+ schema, table := normalizePGLikeMetadataTable(dbName, tableName)
if table == "" {
return nil, fmt.Errorf("表名不能为空")
}
- // 转义函数:处理单引号,移除双引号
- esc := func(s string) string {
- s = strings.Trim(s, "\"")
- return strings.ReplaceAll(s, "'", "''")
- }
-
- // 构建查询:如果没有指定schema,使用current_schema()
- var query string
- if schema != "" {
- query = fmt.Sprintf(`
- SELECT
- i.relname as index_name,
- a.attname as column_name,
- ix.indisunique as is_unique
- FROM
- pg_class t,
- pg_class i,
- pg_index ix,
- pg_attribute a,
- pg_namespace n
- WHERE
- t.oid = ix.indrelid
- AND i.oid = ix.indexrelid
- AND a.attrelid = t.oid
- AND a.attnum = ANY(ix.indkey)
- AND t.relkind = 'r'
- AND t.relname = '%s'
- AND n.oid = t.relnamespace
- AND n.nspname = '%s'
- `, esc(table), esc(schema))
- } else {
- query = fmt.Sprintf(`
- SELECT
- i.relname as index_name,
- a.attname as column_name,
- ix.indisunique as is_unique
- FROM
- pg_class t,
- pg_class i,
- pg_index ix,
- pg_attribute a,
- pg_namespace n
- WHERE
- t.oid = ix.indrelid
- AND i.oid = ix.indexrelid
- AND a.attrelid = t.oid
- AND a.attnum = ANY(ix.indkey)
- AND t.relkind = 'r'
- AND t.relname = '%s'
- AND n.oid = t.relnamespace
- AND n.nspname = current_schema()
- `, esc(table))
- }
-
- data, _, err := k.Query(query)
+ data, _, err := k.Query(buildPGLikeIndexesMetadataQuery(schema, table))
if err != nil {
return nil, err
}
- var indexes []connection.IndexDefinition
- for _, row := range data {
- nonUnique := 1
- if val, ok := row["is_unique"]; ok {
- if b, ok := val.(bool); ok && b {
- nonUnique = 0
- }
- }
-
- idx := connection.IndexDefinition{
- Name: fmt.Sprintf("%v", row["index_name"]),
- ColumnName: fmt.Sprintf("%v", row["column_name"]),
- NonUnique: nonUnique,
- IndexType: "BTREE", // Default
- }
- indexes = append(indexes, idx)
- }
- return indexes, nil
+ return buildPGLikeIndexDefinitions(data), nil
}
func (k *KingbaseDB) GetForeignKeys(dbName, tableName string) ([]connection.ForeignKeyDefinition, error) {
diff --git a/internal/db/kingbase_impl_test.go b/internal/db/kingbase_impl_test.go
index 8a171fd..c754d43 100644
--- a/internal/db/kingbase_impl_test.go
+++ b/internal/db/kingbase_impl_test.go
@@ -199,6 +199,50 @@ func TestKingbaseGetDatabasesFallsBackToCurrentDatabase(t *testing.T) {
}
}
+func TestKingbaseGetIndexesParsesStringUniqueAndVisibleRelation(t *testing.T) {
+ registerFakeKingbaseDriverOnce.Do(func() {
+ sql.Register(fakeKingbaseDriverName, fakeKingbaseDriver{})
+ })
+
+ db, err := sql.Open(fakeKingbaseDriverName, "")
+ if err != nil {
+ t.Fatalf("open fake kingbase db failed: %v", err)
+ }
+ defer db.Close()
+
+ fakeKingbaseStateMu.Lock()
+ fakeKingbaseState.queryErr = nil
+ fakeKingbaseState.queryResults = map[string]fakeKingbaseQueryResult{}
+ fakeKingbaseState.lastQuery = ""
+ fakeKingbaseState.queries = nil
+ fakeKingbaseStateMu.Unlock()
+
+ client := &KingbaseDB{conn: db}
+ indexes, err := client.GetIndexes("", "users")
+ if err != nil {
+ t.Fatalf("GetIndexes returned error: %v", err)
+ }
+ if len(indexes) != 2 {
+ t.Fatalf("expected two index rows, got %+v", indexes)
+ }
+ if indexes[0].Name != "users_email_key" || indexes[0].ColumnName != "tenant_id" || indexes[0].NonUnique != 0 || indexes[0].SeqInIndex != 1 {
+ t.Fatalf("unexpected first index row: %+v", indexes[0])
+ }
+ if indexes[1].Name != "users_email_key" || indexes[1].ColumnName != "email" || indexes[1].NonUnique != 0 || indexes[1].SeqInIndex != 2 {
+ t.Fatalf("unexpected second index row: %+v", indexes[1])
+ }
+
+ fakeKingbaseStateMu.Lock()
+ lastQuery := fakeKingbaseState.lastQuery
+ fakeKingbaseStateMu.Unlock()
+ if !strings.Contains(lastQuery, "pg_catalog.pg_table_is_visible(t.oid)") {
+ t.Fatalf("expected search_path visible relation metadata query, got %s", lastQuery)
+ }
+ if strings.Contains(lastQuery, "current_schema()") || strings.Contains(lastQuery, "n.nspname = 'public'") {
+ t.Fatalf("metadata query should not force current_schema/public, got %s", lastQuery)
+ }
+}
+
type fakeKingbaseDriver struct{}
func (fakeKingbaseDriver) Open(name string) (driver.Conn, error) {
@@ -230,6 +274,15 @@ func (fakeKingbaseConn) QueryContext(ctx context.Context, query string, args []d
}
return &fakeKingbaseRows{columns: result.columns, rows: result.rows}, nil
}
+ if strings.Contains(query, "FROM pg_class t") && strings.Contains(query, "JOIN pg_index ix") && strings.Contains(query, "t.relname = 'users'") {
+ return &fakeKingbaseRows{
+ columns: []string{"index_name", "column_name", "is_unique", "seq_in_index", "index_type"},
+ rows: [][]driver.Value{
+ {"users_email_key", "tenant_id", "t", "1", "btree"},
+ {"users_email_key", "email", "t", "2", "btree"},
+ },
+ }, nil
+ }
if fakeKingbaseState.queryErr != nil {
return nil, fakeKingbaseState.queryErr
}
diff --git a/internal/db/metadata_value.go b/internal/db/metadata_value.go
new file mode 100644
index 0000000..e9f8b81
--- /dev/null
+++ b/internal/db/metadata_value.go
@@ -0,0 +1,44 @@
+package db
+
+import (
+ "fmt"
+ "strings"
+)
+
+func parseMetadataBool(value interface{}) bool {
+ switch val := value.(type) {
+ case bool:
+ return val
+ case int:
+ return val != 0
+ case int64:
+ return val != 0
+ case float64:
+ return val != 0
+ case string:
+ text := strings.ToLower(strings.TrimSpace(val))
+ return text == "t" || text == "true" || text == "1" || text == "y" || text == "yes" || text == "unique"
+ default:
+ text := strings.ToLower(strings.TrimSpace(fmt.Sprintf("%v", value)))
+ return text == "t" || text == "true" || text == "1" || text == "y" || text == "yes" || text == "unique"
+ }
+}
+
+func parseMetadataInt(value interface{}) int {
+ switch val := value.(type) {
+ case int:
+ return val
+ case int64:
+ return int(val)
+ case float64:
+ return int(val)
+ case string:
+ var n int
+ _, _ = fmt.Sscanf(strings.TrimSpace(val), "%d", &n)
+ return n
+ default:
+ var n int
+ _, _ = fmt.Sscanf(strings.TrimSpace(fmt.Sprintf("%v", value)), "%d", &n)
+ return n
+ }
+}
diff --git a/internal/db/pg_metadata.go b/internal/db/pg_metadata.go
new file mode 100644
index 0000000..5d952f7
--- /dev/null
+++ b/internal/db/pg_metadata.go
@@ -0,0 +1,159 @@
+package db
+
+import (
+ "fmt"
+ "strings"
+
+ "GoNavi-Wails/internal/connection"
+)
+
+func normalizePGLikeMetadataTable(schemaName, tableName string) (string, string) {
+ schema := strings.TrimSpace(schemaName)
+ table := strings.TrimSpace(tableName)
+ if parsedSchema, parsedTable := SplitSQLQualifiedName(table); parsedSchema != "" && parsedTable != "" {
+ schema = parsedSchema
+ table = parsedTable
+ }
+ schema = strings.TrimSpace(normalizeSQLIdentifierEscapes(schema))
+ table = strings.TrimSpace(normalizeSQLIdentifierEscapes(table))
+ schema = strings.Trim(schema, `"`)
+ table = strings.Trim(table, `"`)
+ return schema, table
+}
+
+func escapePGLikeMetadataLiteral(raw string) string {
+ text := strings.TrimSpace(raw)
+ text = strings.Trim(text, `"`)
+ return strings.ReplaceAll(text, "'", "''")
+}
+
+func buildPGLikeVisibleRelationPredicate(alias string, schemaName string) string {
+ relAlias := strings.TrimSpace(alias)
+ if relAlias == "" {
+ relAlias = "c"
+ }
+ if strings.TrimSpace(schemaName) == "" {
+ return fmt.Sprintf("pg_catalog.pg_table_is_visible(%s.oid)", relAlias)
+ }
+ return fmt.Sprintf("n.nspname = '%s'", escapePGLikeMetadataLiteral(schemaName))
+}
+
+func buildPGLikeColumnsMetadataQuery(schemaName, tableName string) string {
+ return fmt.Sprintf(`
+SELECT
+ a.attname AS column_name,
+ pg_catalog.format_type(a.atttypid, a.atttypmod) AS data_type,
+ CASE WHEN a.attnotnull THEN 'NO' ELSE 'YES' END AS is_nullable,
+ pg_get_expr(ad.adbin, ad.adrelid) AS column_default,
+ col_description(a.attrelid, a.attnum) AS comment,
+ CASE WHEN pk.attname IS NOT NULL THEN 'PRI' ELSE '' END AS column_key
+FROM pg_class c
+JOIN pg_namespace n ON n.oid = c.relnamespace
+JOIN pg_attribute a ON a.attrelid = c.oid
+LEFT JOIN pg_attrdef ad ON ad.adrelid = c.oid AND ad.adnum = a.attnum
+LEFT JOIN (
+ SELECT i.indrelid, a3.attname
+ FROM pg_index i
+ JOIN pg_attribute a3 ON a3.attrelid = i.indrelid AND a3.attnum = ANY(i.indkey)
+ WHERE i.indisprimary
+) pk ON pk.indrelid = c.oid AND pk.attname = a.attname
+WHERE c.relkind IN ('r', 'p')
+ AND %s
+ AND c.relname = '%s'
+ AND a.attnum > 0
+ AND NOT a.attisdropped
+ORDER BY a.attnum`, buildPGLikeVisibleRelationPredicate("c", schemaName), escapePGLikeMetadataLiteral(tableName))
+}
+
+func buildPGLikeIndexesMetadataQuery(schemaName, tableName string) string {
+ return fmt.Sprintf(`
+SELECT
+ i.relname AS index_name,
+ a.attname AS column_name,
+ ix.indisunique AS is_unique,
+ x.ordinality AS seq_in_index,
+ am.amname AS index_type
+FROM pg_class t
+JOIN pg_namespace n ON n.oid = t.relnamespace
+JOIN pg_index ix ON t.oid = ix.indrelid
+JOIN pg_class i ON i.oid = ix.indexrelid
+JOIN pg_am am ON i.relam = am.oid
+JOIN unnest(ix.indkey) WITH ORDINALITY AS x(attnum, ordinality) ON TRUE
+JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = x.attnum
+WHERE t.relkind IN ('r', 'p')
+ AND t.relname = '%s'
+ AND %s
+ AND ix.indisvalid
+ AND ix.indpred IS NULL
+ AND x.ordinality <= ix.indnkeyatts
+ AND NOT EXISTS (
+ SELECT 1 FROM unnest(ix.indkey) AS expr_key(attnum) WHERE expr_key.attnum <= 0
+ )
+ORDER BY i.relname, x.ordinality`, escapePGLikeMetadataLiteral(tableName), buildPGLikeVisibleRelationPredicate("t", schemaName))
+}
+
+func buildPGLikeColumnDefinitions(data []map[string]interface{}) []connection.ColumnDefinition {
+ columns := make([]connection.ColumnDefinition, 0, len(data))
+ for _, row := range data {
+ col := connection.ColumnDefinition{
+ Name: fmt.Sprintf("%v", row["column_name"]),
+ Type: fmt.Sprintf("%v", row["data_type"]),
+ Nullable: fmt.Sprintf("%v", row["is_nullable"]),
+ Key: fmt.Sprintf("%v", row["column_key"]),
+ Extra: "",
+ Comment: "",
+ }
+
+ if v, ok := row["comment"]; ok && v != nil {
+ col.Comment = fmt.Sprintf("%v", v)
+ }
+
+ if v, ok := row["column_default"]; ok && v != nil {
+ def := fmt.Sprintf("%v", v)
+ col.Default = &def
+ if strings.HasPrefix(strings.ToLower(strings.TrimSpace(def)), "nextval(") {
+ col.Extra = "auto_increment"
+ }
+ }
+
+ columns = append(columns, col)
+ }
+ return columns
+}
+
+func buildPGLikeIndexDefinitions(data []map[string]interface{}) []connection.IndexDefinition {
+ indexes := make([]connection.IndexDefinition, 0, len(data))
+ for _, row := range data {
+ isUnique := false
+ if v, ok := row["is_unique"]; ok && v != nil {
+ isUnique = parseMetadataBool(v)
+ }
+
+ nonUnique := 1
+ if isUnique {
+ nonUnique = 0
+ }
+
+ seq := 0
+ if v, ok := row["seq_in_index"]; ok && v != nil {
+ seq = parseMetadataInt(v)
+ }
+
+ indexType := ""
+ if v, ok := row["index_type"]; ok && v != nil {
+ indexType = strings.ToUpper(fmt.Sprintf("%v", v))
+ }
+ if indexType == "" {
+ indexType = "BTREE"
+ }
+
+ indexes = append(indexes, connection.IndexDefinition{
+ Name: fmt.Sprintf("%v", row["index_name"]),
+ ColumnName: fmt.Sprintf("%v", row["column_name"]),
+ NonUnique: nonUnique,
+ SeqInIndex: seq,
+ IndexType: indexType,
+ })
+ }
+ return indexes
+}
diff --git a/internal/db/pg_metadata_test.go b/internal/db/pg_metadata_test.go
new file mode 100644
index 0000000..8b59ea2
--- /dev/null
+++ b/internal/db/pg_metadata_test.go
@@ -0,0 +1,80 @@
+package db
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestBuildPGLikeMetadataQueriesUseVisibleRelationForPureTable(t *testing.T) {
+ t.Parallel()
+
+ columnQuery := buildPGLikeColumnsMetadataQuery("", "users")
+ if !strings.Contains(columnQuery, "pg_catalog.pg_table_is_visible(c.oid)") {
+ t.Fatalf("expected visible relation predicate for column metadata, got %s", columnQuery)
+ }
+ if strings.Contains(columnQuery, "n.nspname = 'public'") || strings.Contains(columnQuery, "current_schema()") {
+ t.Fatalf("pure table column metadata should not force public/current_schema, got %s", columnQuery)
+ }
+
+ indexQuery := buildPGLikeIndexesMetadataQuery("", "users")
+ if !strings.Contains(indexQuery, "pg_catalog.pg_table_is_visible(t.oid)") {
+ t.Fatalf("expected visible relation predicate for index metadata, got %s", indexQuery)
+ }
+ if strings.Contains(indexQuery, "n.nspname = 'public'") || strings.Contains(indexQuery, "current_schema()") {
+ t.Fatalf("pure table index metadata should not force public/current_schema, got %s", indexQuery)
+ }
+}
+
+func TestBuildPGLikeMetadataQueriesKeepExplicitSchema(t *testing.T) {
+ t.Parallel()
+
+ columnQuery := buildPGLikeColumnsMetadataQuery("audit", "users")
+ if !strings.Contains(columnQuery, "n.nspname = 'audit'") {
+ t.Fatalf("expected explicit schema predicate, got %s", columnQuery)
+ }
+ if strings.Contains(columnQuery, "pg_catalog.pg_table_is_visible") {
+ t.Fatalf("explicit schema metadata should not use visibility predicate, got %s", columnQuery)
+ }
+}
+
+func TestBuildPGLikeColumnDefinitionsMarksPrimaryKey(t *testing.T) {
+ t.Parallel()
+
+ columns := buildPGLikeColumnDefinitions([]map[string]interface{}{
+ {
+ "column_name": "id",
+ "data_type": "bigint",
+ "is_nullable": "NO",
+ "column_default": "nextval('users_id_seq'::regclass)",
+ "column_key": "PRI",
+ },
+ })
+
+ if len(columns) != 1 {
+ t.Fatalf("unexpected column count: %d", len(columns))
+ }
+ if columns[0].Name != "id" || columns[0].Key != "PRI" || columns[0].Extra != "auto_increment" {
+ t.Fatalf("unexpected primary key column: %+v", columns[0])
+ }
+}
+
+func TestBuildPGLikeIndexDefinitionsParsesStringUnique(t *testing.T) {
+ t.Parallel()
+
+ indexes := buildPGLikeIndexDefinitions([]map[string]interface{}{
+ {
+ "index_name": "users_email_key",
+ "column_name": "email",
+ "is_unique": "t",
+ "seq_in_index": "1",
+ "index_type": "btree",
+ },
+ })
+
+ if len(indexes) != 1 {
+ t.Fatalf("unexpected index count: %d", len(indexes))
+ }
+ if indexes[0].Name != "users_email_key" || indexes[0].ColumnName != "email" || indexes[0].NonUnique != 0 || indexes[0].SeqInIndex != 1 {
+ t.Fatalf("unexpected unique index metadata: %+v", indexes[0])
+ }
+}
diff --git a/internal/db/postgres_impl.go b/internal/db/postgres_impl.go
index fefce41..7753203 100644
--- a/internal/db/postgres_impl.go
+++ b/internal/db/postgres_impl.go
@@ -355,185 +355,31 @@ func (p *PostgresDB) GetCreateStatement(dbName, tableName string) (string, error
}
func (p *PostgresDB) GetColumns(dbName, tableName string) ([]connection.ColumnDefinition, error) {
- schema := strings.TrimSpace(dbName)
- if schema == "" {
- schema = "public"
- }
- table := strings.TrimSpace(tableName)
+ schema, table := normalizePGLikeMetadataTable(dbName, tableName)
if table == "" {
return nil, fmt.Errorf("表名不能为空")
}
- esc := func(s string) string { return strings.ReplaceAll(s, "'", "''") }
-
- query := fmt.Sprintf(`
-SELECT
- a.attname AS column_name,
- pg_catalog.format_type(a.atttypid, a.atttypmod) AS data_type,
- CASE WHEN a.attnotnull THEN 'NO' ELSE 'YES' END AS is_nullable,
- pg_get_expr(ad.adbin, ad.adrelid) AS column_default,
- col_description(a.attrelid, a.attnum) AS comment,
- CASE WHEN pk.attname IS NOT NULL THEN 'PRI' ELSE '' END AS column_key
-FROM pg_class c
-JOIN pg_namespace n ON n.oid = c.relnamespace
-JOIN pg_attribute a ON a.attrelid = c.oid
-LEFT JOIN pg_attrdef ad ON ad.adrelid = c.oid AND ad.adnum = a.attnum
-LEFT JOIN (
- SELECT i.indrelid, a3.attname
- FROM pg_index i
- JOIN pg_attribute a3 ON a3.attrelid = i.indrelid AND a3.attnum = ANY(i.indkey)
- WHERE i.indisprimary
-) pk ON pk.indrelid = c.oid AND pk.attname = a.attname
-WHERE c.relkind IN ('r', 'p')
- AND n.nspname = '%s'
- AND c.relname = '%s'
- AND a.attnum > 0
- AND NOT a.attisdropped
-ORDER BY a.attnum`, esc(schema), esc(table))
-
- data, _, err := p.Query(query)
+ data, _, err := p.Query(buildPGLikeColumnsMetadataQuery(schema, table))
if err != nil {
return nil, err
}
- var columns []connection.ColumnDefinition
- for _, row := range data {
- col := connection.ColumnDefinition{
- Name: fmt.Sprintf("%v", row["column_name"]),
- Type: fmt.Sprintf("%v", row["data_type"]),
- Nullable: fmt.Sprintf("%v", row["is_nullable"]),
- Key: fmt.Sprintf("%v", row["column_key"]),
- Extra: "",
- Comment: "",
- }
-
- if v, ok := row["comment"]; ok && v != nil {
- col.Comment = fmt.Sprintf("%v", v)
- }
-
- if v, ok := row["column_default"]; ok && v != nil {
- def := fmt.Sprintf("%v", v)
- col.Default = &def
- if strings.HasPrefix(strings.ToLower(strings.TrimSpace(def)), "nextval(") {
- col.Extra = "auto_increment"
- }
- }
-
- columns = append(columns, col)
- }
- return columns, nil
+ return buildPGLikeColumnDefinitions(data), nil
}
func (p *PostgresDB) GetIndexes(dbName, tableName string) ([]connection.IndexDefinition, error) {
- schema := strings.TrimSpace(dbName)
- if schema == "" {
- schema = "public"
- }
- table := strings.TrimSpace(tableName)
+ schema, table := normalizePGLikeMetadataTable(dbName, tableName)
if table == "" {
return nil, fmt.Errorf("表名不能为空")
}
- esc := func(s string) string { return strings.ReplaceAll(s, "'", "''") }
-
- query := fmt.Sprintf(`
-SELECT
- i.relname AS index_name,
- a.attname AS column_name,
- ix.indisunique AS is_unique,
- x.ordinality AS seq_in_index,
- am.amname AS index_type
-FROM pg_class t
-JOIN pg_namespace n ON n.oid = t.relnamespace
-JOIN pg_index ix ON t.oid = ix.indrelid
-JOIN pg_class i ON i.oid = ix.indexrelid
-JOIN pg_am am ON i.relam = am.oid
-JOIN unnest(ix.indkey) WITH ORDINALITY AS x(attnum, ordinality) ON TRUE
-JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = x.attnum
-WHERE t.relkind IN ('r', 'p')
- AND t.relname = '%s'
- AND n.nspname = '%s'
- AND ix.indisvalid
- AND ix.indpred IS NULL
- AND x.ordinality <= ix.indnkeyatts
- AND NOT EXISTS (
- SELECT 1 FROM unnest(ix.indkey) AS expr_key(attnum) WHERE expr_key.attnum <= 0
- )
-ORDER BY i.relname, x.ordinality`, esc(table), esc(schema))
-
- data, _, err := p.Query(query)
+ data, _, err := p.Query(buildPGLikeIndexesMetadataQuery(schema, table))
if err != nil {
return nil, err
}
- parseBool := func(v interface{}) bool {
- switch val := v.(type) {
- case bool:
- return val
- case string:
- s := strings.ToLower(strings.TrimSpace(val))
- return s == "t" || s == "true" || s == "1" || s == "y" || s == "yes"
- default:
- s := strings.ToLower(strings.TrimSpace(fmt.Sprintf("%v", v)))
- return s == "t" || s == "true" || s == "1" || s == "y" || s == "yes"
- }
- }
-
- parseInt := func(v interface{}) int {
- switch val := v.(type) {
- case int:
- return val
- case int64:
- return int(val)
- case float64:
- return int(val)
- case string:
- // best effort
- var n int
- _, _ = fmt.Sscanf(strings.TrimSpace(val), "%d", &n)
- return n
- default:
- var n int
- _, _ = fmt.Sscanf(strings.TrimSpace(fmt.Sprintf("%v", v)), "%d", &n)
- return n
- }
- }
-
- var indexes []connection.IndexDefinition
- for _, row := range data {
- isUnique := false
- if v, ok := row["is_unique"]; ok && v != nil {
- isUnique = parseBool(v)
- }
-
- nonUnique := 1
- if isUnique {
- nonUnique = 0
- }
-
- seq := 0
- if v, ok := row["seq_in_index"]; ok && v != nil {
- seq = parseInt(v)
- }
-
- indexType := ""
- if v, ok := row["index_type"]; ok && v != nil {
- indexType = strings.ToUpper(fmt.Sprintf("%v", v))
- }
- if indexType == "" {
- indexType = "BTREE"
- }
-
- idx := connection.IndexDefinition{
- Name: fmt.Sprintf("%v", row["index_name"]),
- ColumnName: fmt.Sprintf("%v", row["column_name"]),
- NonUnique: nonUnique,
- SeqInIndex: seq,
- IndexType: indexType,
- }
- indexes = append(indexes, idx)
- }
- return indexes, nil
+ return buildPGLikeIndexDefinitions(data), nil
}
func (p *PostgresDB) GetForeignKeys(dbName, tableName string) ([]connection.ForeignKeyDefinition, error) {
diff --git a/internal/db/vastbase_impl.go b/internal/db/vastbase_impl.go
index 7bb5521..a1ece0a 100644
--- a/internal/db/vastbase_impl.go
+++ b/internal/db/vastbase_impl.go
@@ -249,178 +249,31 @@ func (v *VastbaseDB) GetCreateStatement(dbName, tableName string) (string, error
}
func (v *VastbaseDB) GetColumns(dbName, tableName string) ([]connection.ColumnDefinition, error) {
- schema := strings.TrimSpace(dbName)
- if schema == "" {
- schema = "public"
- }
- table := strings.TrimSpace(tableName)
+ schema, table := normalizePGLikeMetadataTable(dbName, tableName)
if table == "" {
return nil, fmt.Errorf("表名不能为空")
}
- esc := func(s string) string { return strings.ReplaceAll(s, "'", "''") }
-
- query := fmt.Sprintf(`
-SELECT
- a.attname AS column_name,
- pg_catalog.format_type(a.atttypid, a.atttypmod) AS data_type,
- CASE WHEN a.attnotnull THEN 'NO' ELSE 'YES' END AS is_nullable,
- pg_get_expr(ad.adbin, ad.adrelid) AS column_default,
- col_description(a.attrelid, a.attnum) AS comment,
- CASE WHEN pk.attname IS NOT NULL THEN 'PRI' ELSE '' END AS column_key
-FROM pg_class c
-JOIN pg_namespace n ON n.oid = c.relnamespace
-JOIN pg_attribute a ON a.attrelid = c.oid
-LEFT JOIN pg_attrdef ad ON ad.adrelid = c.oid AND ad.adnum = a.attnum
-LEFT JOIN (
- SELECT i.indrelid, a3.attname
- FROM pg_index i
- JOIN pg_attribute a3 ON a3.attrelid = i.indrelid AND a3.attnum = ANY(i.indkey)
- WHERE i.indisprimary
-) pk ON pk.indrelid = c.oid AND pk.attname = a.attname
-WHERE c.relkind IN ('r', 'p')
- AND n.nspname = '%s'
- AND c.relname = '%s'
- AND a.attnum > 0
- AND NOT a.attisdropped
-ORDER BY a.attnum`, esc(schema), esc(table))
-
- data, _, err := v.Query(query)
+ data, _, err := v.Query(buildPGLikeColumnsMetadataQuery(schema, table))
if err != nil {
return nil, err
}
- var columns []connection.ColumnDefinition
- for _, row := range data {
- col := connection.ColumnDefinition{
- Name: fmt.Sprintf("%v", row["column_name"]),
- Type: fmt.Sprintf("%v", row["data_type"]),
- Nullable: fmt.Sprintf("%v", row["is_nullable"]),
- Key: fmt.Sprintf("%v", row["column_key"]),
- Extra: "",
- Comment: "",
- }
-
- if val, ok := row["comment"]; ok && val != nil {
- col.Comment = fmt.Sprintf("%v", val)
- }
-
- if val, ok := row["column_default"]; ok && val != nil {
- def := fmt.Sprintf("%v", val)
- col.Default = &def
- if strings.HasPrefix(strings.ToLower(strings.TrimSpace(def)), "nextval(") {
- col.Extra = "auto_increment"
- }
- }
-
- columns = append(columns, col)
- }
- return columns, nil
+ return buildPGLikeColumnDefinitions(data), nil
}
func (v *VastbaseDB) GetIndexes(dbName, tableName string) ([]connection.IndexDefinition, error) {
- schema := strings.TrimSpace(dbName)
- if schema == "" {
- schema = "public"
- }
- table := strings.TrimSpace(tableName)
+ schema, table := normalizePGLikeMetadataTable(dbName, tableName)
if table == "" {
return nil, fmt.Errorf("表名不能为空")
}
- esc := func(s string) string { return strings.ReplaceAll(s, "'", "''") }
-
- query := fmt.Sprintf(`
-SELECT
- i.relname AS index_name,
- a.attname AS column_name,
- ix.indisunique AS is_unique,
- x.ordinality AS seq_in_index,
- am.amname AS index_type
-FROM pg_class t
-JOIN pg_namespace n ON n.oid = t.relnamespace
-JOIN pg_index ix ON t.oid = ix.indrelid
-JOIN pg_class i ON i.oid = ix.indexrelid
-JOIN pg_am am ON i.relam = am.oid
-JOIN unnest(ix.indkey) WITH ORDINALITY AS x(attnum, ordinality) ON TRUE
-JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = x.attnum
-WHERE t.relkind IN ('r', 'p')
- AND t.relname = '%s'
- AND n.nspname = '%s'
-ORDER BY i.relname, x.ordinality`, esc(table), esc(schema))
-
- data, _, err := v.Query(query)
+ data, _, err := v.Query(buildPGLikeIndexesMetadataQuery(schema, table))
if err != nil {
return nil, err
}
- parseBool := func(val interface{}) bool {
- switch v := val.(type) {
- case bool:
- return v
- case string:
- s := strings.ToLower(strings.TrimSpace(v))
- return s == "t" || s == "true" || s == "1" || s == "y" || s == "yes"
- default:
- s := strings.ToLower(strings.TrimSpace(fmt.Sprintf("%v", val)))
- return s == "t" || s == "true" || s == "1" || s == "y" || s == "yes"
- }
- }
-
- parseInt := func(val interface{}) int {
- switch v := val.(type) {
- case int:
- return v
- case int64:
- return int(v)
- case float64:
- return int(v)
- case string:
- var n int
- _, _ = fmt.Sscanf(strings.TrimSpace(v), "%d", &n)
- return n
- default:
- var n int
- _, _ = fmt.Sscanf(strings.TrimSpace(fmt.Sprintf("%v", val)), "%d", &n)
- return n
- }
- }
-
- var indexes []connection.IndexDefinition
- for _, row := range data {
- isUnique := false
- if val, ok := row["is_unique"]; ok && val != nil {
- isUnique = parseBool(val)
- }
-
- nonUnique := 1
- if isUnique {
- nonUnique = 0
- }
-
- seq := 0
- if val, ok := row["seq_in_index"]; ok && val != nil {
- seq = parseInt(val)
- }
-
- indexType := ""
- if val, ok := row["index_type"]; ok && val != nil {
- indexType = strings.ToUpper(fmt.Sprintf("%v", val))
- }
- if indexType == "" {
- indexType = "BTREE"
- }
-
- idx := connection.IndexDefinition{
- Name: fmt.Sprintf("%v", row["index_name"]),
- ColumnName: fmt.Sprintf("%v", row["column_name"]),
- NonUnique: nonUnique,
- SeqInIndex: seq,
- IndexType: indexType,
- }
- indexes = append(indexes, idx)
- }
- return indexes, nil
+ return buildPGLikeIndexDefinitions(data), nil
}
func (v *VastbaseDB) GetForeignKeys(dbName, tableName string) ([]connection.ForeignKeyDefinition, error) {