mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-01 18:01:25 +08:00
🐛 fix(query-results): 修复金仓带模式表主键识别
- PG 类数据库保留 schema.table 作为查询结果表名和元数据表名 - 元数据连接继续使用当前数据库,避免把 schema 误当 database - 补充金仓列、索引和前端可编辑定位回归测试
This commit is contained in:
@@ -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(<QueryEditor tab={createTab({
|
||||
dbName: 'ldf_server_dbs_dev',
|
||||
query: 'SELECT * FROM ldf_server.mes_work_order',
|
||||
})} />);
|
||||
});
|
||||
|
||||
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';
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user