From 04bbab3d7e08dc92a05ddb58b924c74b1f7c8bfd Mon Sep 17 00:00:00 2001 From: Syngnat Date: Mon, 29 Jun 2026 15:18:13 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(query-results):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E9=87=91=E4=BB=93=E5=B8=A6=E6=A8=A1=E5=BC=8F=E8=A1=A8?= =?UTF-8?q?=E4=B8=BB=E9=94=AE=E8=AF=86=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PG 类数据库保留 schema.table 作为查询结果表名和元数据表名 - 元数据连接继续使用当前数据库,避免把 schema 误当 database - 补充金仓列、索引和前端可编辑定位回归测试 --- .../QueryEditor.external-sql-save.test.tsx | 49 ++++++++ frontend/src/utils/queryResultTable.test.ts | 16 +++ frontend/src/utils/queryResultTable.ts | 32 ++++- .../app/methods_db_metadata_retry_test.go | 118 +++++++++++++++--- 4 files changed, 193 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/QueryEditor.external-sql-save.test.tsx b/frontend/src/components/QueryEditor.external-sql-save.test.tsx index 6ffb973..075258a 100644 --- a/frontend/src/components/QueryEditor.external-sql-save.test.tsx +++ b/frontend/src/components/QueryEditor.external-sql-save.test.tsx @@ -6192,6 +6192,55 @@ describe('QueryEditor external SQL save', () => { expect(messageApi.warning).not.toHaveBeenCalled(); }); + it('keeps Kingbase schema-qualified query results writable without treating the schema as the database', async () => { + storeState.connections[0].config.type = 'kingbase'; + storeState.connections[0].config.database = 'ldf_server_dbs_dev'; + backendApp.DBQueryMulti.mockResolvedValueOnce({ + success: true, + data: [{ + columns: ['id', 'work_order_no'], + rows: [{ id: 1001, work_order_no: 'MO-1001' }], + }], + }); + backendApp.DBGetColumns.mockResolvedValueOnce({ + success: true, + data: [{ name: 'id', key: 'PRI' }, { name: 'work_order_no', key: '' }], + }); + backendApp.DBGetIndexes.mockResolvedValueOnce({ + success: true, + data: [{ name: 'mes_work_order_pkey', columnName: 'id', nonUnique: 0, seqInIndex: 1 }], + }); + + 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(backendApp.DBGetColumns).toHaveBeenCalledWith(expect.anything(), 'ldf_server_dbs_dev', 'ldf_server.mes_work_order'); + expect(backendApp.DBGetIndexes).toHaveBeenCalledWith(expect.anything(), 'ldf_server_dbs_dev', 'ldf_server.mes_work_order'); + expect(dataGridState.latestProps?.tableName).toBe('ldf_server.mes_work_order'); + 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(); + }); + 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/utils/queryResultTable.test.ts b/frontend/src/utils/queryResultTable.test.ts index 20da643..6453256 100644 --- a/frontend/src/utils/queryResultTable.test.ts +++ b/frontend/src/utils/queryResultTable.test.ts @@ -57,6 +57,22 @@ describe('extractQueryResultTableRef', () => { }); }); + it('keeps PostgreSQL-like schema-qualified table names while using the current database for metadata lookups', () => { + expect(extractQueryResultTableRef('SELECT * FROM ldf_server.mes_work_order', 'kingbase', 'ldf_server_dbs_dev')) + .toEqual({ + tableName: 'ldf_server.mes_work_order', + metadataDbName: 'ldf_server_dbs_dev', + metadataTableName: 'ldf_server.mes_work_order', + }); + + expect(extractQueryResultTableRef('SELECT * FROM ops.jobs LIMIT 20', 'postgres', 'app_db')) + .toEqual({ + tableName: 'ops.jobs', + metadataDbName: 'app_db', + metadataTableName: 'ops.jobs', + }); + }); + it('keeps DuckDB schema-qualified table names for metadata lookups', () => { expect(extractQueryResultTableRef('SELECT * FROM main.events LIMIT 500', 'duckdb', 'main')) .toEqual({ diff --git a/frontend/src/utils/queryResultTable.ts b/frontend/src/utils/queryResultTable.ts index 753f56d..2c3d37a 100644 --- a/frontend/src/utils/queryResultTable.ts +++ b/frontend/src/utils/queryResultTable.ts @@ -23,7 +23,26 @@ const isOracleLikeDialect = (dialect: string): boolean => { const keepsQualifiedTableNameForMetadata = (dialect: string): boolean => { const normalized = String(dialect || '').trim().toLowerCase(); - return normalized === 'duckdb'; + return normalized === 'duckdb' || isPostgresLikeDialect(normalized); +}; + +const isPostgresLikeDialect = (dialect: string): boolean => { + const normalized = String(dialect || '').trim().toLowerCase(); + return normalized === 'postgres' + || normalized === 'postgresql' + || normalized === 'pg' + || normalized === 'kingbase' + || normalized === 'kingbase8' + || normalized === 'kingbasees' + || normalized === 'kingbasev8' + || normalized === 'highgo' + || normalized === 'vastbase' + || normalized === 'opengauss' + || normalized === 'open_gauss' + || normalized === 'open-gauss' + || normalized === 'gaussdb' + || normalized === 'gauss_db' + || normalized === 'gauss-db'; }; const isQuotedIdentifier = (part: string): boolean => { @@ -85,16 +104,21 @@ export const extractQueryResultTableRef = ( ? defaultOracleSchema || normalizeCurrentDbName(currentDb, dialect) : normalizeCurrentDbName(currentDb, dialect); const metadataDbName = owner || fallbackSchema; - const tableName = isOracleLikeDialect(dialect) && owner + const qualifiedTableName = owner ? `${owner}.${metadataTableName}` : metadataTableName; + const pgLikeQualifiedMetadata = isPostgresLikeDialect(dialect) && owner; + const resolvedMetadataDbName = pgLikeQualifiedMetadata + ? normalizeCurrentDbName(currentDb, dialect) + : metadataDbName; + const tableName = (isOracleLikeDialect(dialect) && owner) || pgLikeQualifiedMetadata ? `${owner}.${metadataTableName}` : (keepsQualifiedTableNameForMetadata(dialect) && owner ? `${owner}.${metadataTableName}` : metadataTableName); const resolvedMetadataTableName = keepsQualifiedTableNameForMetadata(dialect) && owner - ? `${owner}.${metadataTableName}` + ? qualifiedTableName : metadataTableName; return { tableName, - metadataDbName, + metadataDbName: resolvedMetadataDbName || metadataDbName, metadataTableName: resolvedMetadataTableName, }; }; diff --git a/internal/app/methods_db_metadata_retry_test.go b/internal/app/methods_db_metadata_retry_test.go index 17dcca8..13c26f6 100644 --- a/internal/app/methods_db_metadata_retry_test.go +++ b/internal/app/methods_db_metadata_retry_test.go @@ -24,21 +24,23 @@ func requireDuckDBOptionalDriverRuntime(t *testing.T) { } type fakeMetadataRetryDB struct { - columns []connection.ColumnDefinition - indexes []connection.IndexDefinition - columnsErr error - indexesErr error - queryResults []fakeMetadataQueryResult - queryRows []map[string]interface{} - queryFields []string - queryErr error - queries []string - columnCalls int - indexCalls int - columnSchema string - columnTable string - indexSchema string - indexTable string + columns []connection.ColumnDefinition + indexes []connection.IndexDefinition + columnsErr error + indexesErr error + queryResults []fakeMetadataQueryResult + queryRows []map[string]interface{} + queryFields []string + queryErr error + queries []string + columnCalls int + indexCalls int + columnSchema string + columnTable string + indexSchema string + indexTable string + connectCalls int + connectConfig connection.ConnectionConfig } type fakeMetadataQueryResult struct { @@ -48,9 +50,13 @@ type fakeMetadataQueryResult struct { err error } -func (f *fakeMetadataRetryDB) Connect(config connection.ConnectionConfig) error { return nil } -func (f *fakeMetadataRetryDB) Close() error { return nil } -func (f *fakeMetadataRetryDB) Ping() error { return nil } +func (f *fakeMetadataRetryDB) Connect(config connection.ConnectionConfig) error { + f.connectCalls++ + f.connectConfig = config + return nil +} +func (f *fakeMetadataRetryDB) Close() error { return nil } +func (f *fakeMetadataRetryDB) Ping() error { return nil } func (f *fakeMetadataRetryDB) Query(query string) ([]map[string]interface{}, []string, error) { f.queries = append(f.queries, query) for _, result := range f.queryResults { @@ -225,6 +231,82 @@ func TestDBGetIndexesUsesSearchPathForPostgresPureTableMetadata(t *testing.T) { } } +func TestDBGetColumnsKeepsCurrentDatabaseForKingbaseQualifiedTableMetadata(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: "kingbase", + Host: "127.0.0.1", + Port: 54321, + User: "system", + Database: "ldf_server_dbs_dev", + }, "ldf_server_dbs_dev", "ldf_server.mes_work_order") + + if !result.Success { + t.Fatalf("expected DBGetColumns success, got failure: %s", result.Message) + } + if dbInst.connectConfig.Database != "ldf_server_dbs_dev" { + t.Fatalf("expected kingbase metadata connection to keep current database, got %q", dbInst.connectConfig.Database) + } + if dbInst.columnSchema != "ldf_server" || dbInst.columnTable != "mes_work_order" { + t.Fatalf("expected kingbase qualified column metadata to pass ldf_server/mes_work_order, got %q.%q", dbInst.columnSchema, dbInst.columnTable) + } +} + +func TestDBGetIndexesKeepsCurrentDatabaseForKingbaseQualifiedTableMetadata(t *testing.T) { + originalNewDatabaseFunc := newDatabaseFunc + originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc + t.Cleanup(func() { + newDatabaseFunc = originalNewDatabaseFunc + resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc + }) + + dbInst := &fakeMetadataRetryDB{ + indexes: []connection.IndexDefinition{{Name: "mes_work_order_pkey", ColumnName: "id", 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: "kingbase", + Host: "127.0.0.1", + Port: 54321, + User: "system", + Database: "ldf_server_dbs_dev", + }, "ldf_server_dbs_dev", "ldf_server.mes_work_order") + + if !result.Success { + t.Fatalf("expected DBGetIndexes success, got failure: %s", result.Message) + } + if dbInst.connectConfig.Database != "ldf_server_dbs_dev" { + t.Fatalf("expected kingbase metadata connection to keep current database, got %q", dbInst.connectConfig.Database) + } + if dbInst.indexSchema != "ldf_server" || dbInst.indexTable != "mes_work_order" { + t.Fatalf("expected kingbase qualified index metadata to pass ldf_server/mes_work_order, got %q.%q", dbInst.indexSchema, dbInst.indexTable) + } +} + func TestDBGetColumnsKeepsDatabaseForMySQLMetadata(t *testing.T) { originalNewDatabaseFunc := newDatabaseFunc originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc