From a66590868ec8291c1511ff4de80c29593008a817 Mon Sep 17 00:00:00 2001 From: Zzz <59008530+ordinary-s@users.noreply.github.com> Date: Tue, 25 Aug 2026 21:26:00 +0800 Subject: [PATCH] fix(kingbase): resolve schema for unqualified grid edits --- .../drivers/kingbase-go/kingbase_metadata.go | 61 ++++++++---- agents/drivers/kingbase-go/main_test.go | 75 ++++++++++++++- .../src/lib/metadata/tableMetadataCache.ts | 10 +- apps/desktop/src/stores/queryStore.ts | 14 +-- packages/app-tests/queryStore.test.ts | 95 +++++++++++++++++++ packages/app-tests/tableMetadataCache.test.ts | 62 +++++++----- 6 files changed, 260 insertions(+), 57 deletions(-) diff --git a/agents/drivers/kingbase-go/kingbase_metadata.go b/agents/drivers/kingbase-go/kingbase_metadata.go index b107e1881a..e64c327aa8 100644 --- a/agents/drivers/kingbase-go/kingbase_metadata.go +++ b/agents/drivers/kingbase-go/kingbase_metadata.go @@ -88,6 +88,7 @@ type metadataListConstraints struct { type columnInfo struct { Name string `json:"name"` DataType string `json:"data_type"` + ResolvedSchema *string `json:"resolved_schema,omitempty"` FullDataType string `json:"-"` IsNullable bool `json:"is_nullable"` ColumnDefault *string `json:"column_default"` @@ -1279,43 +1280,66 @@ func completionNameMatches(name string, request completionAssistantRequest) bool } func (s *server) getColumns(schema, table string) ([]columnInfo, error) { - effective, err := s.effectiveSchema(schema) - if err != nil { - return nil, err - } - primary, _ := s.primaryKeys(effective, table) if s.mode.mysqlCompat { + effective, err := s.effectiveSchema(schema) + if err != nil { + return nil, err + } + primary, _ := s.primaryKeys(effective, table) return s.informationSchemaColumns(effective, table, primary) } catalog, prefix := "sys_catalog", "sys" if s.mode.postgresCatalog { catalog, prefix = "pg_catalog", "pg" - return s.queryCatalogColumns(effective, table, primary, catalog, prefix, "pg_get_expr") + result, err := s.queryCatalogColumns(schema, table, catalog, prefix, "pg_get_expr") + return s.finishCatalogColumns(schema, table, result, err) } expression := "sys_get_expr" if s.usePgDefaultExpression { expression = "pg_get_expr" } - result, err := s.queryCatalogColumns(effective, table, primary, catalog, prefix, expression) + result, err := s.queryCatalogColumns(schema, table, catalog, prefix, expression) if err != nil && expression == "sys_get_expr" && isUndefinedFunction(err, expression) { // Some V8R6 PostgreSQL-mode databases keep sys_catalog while adbin is // pg_node_tree. Cache the compatible function after the exact failure. s.usePgDefaultExpression = true - return s.queryCatalogColumns(effective, table, primary, catalog, prefix, "pg_get_expr") + result, err = s.queryCatalogColumns(schema, table, catalog, prefix, "pg_get_expr") } - return result, err + return s.finishCatalogColumns(schema, table, result, err) +} + +func (s *server) finishCatalogColumns(schema, table string, result []columnInfo, err error) ([]columnInfo, error) { + if err != nil || len(result) == 0 { + return result, err + } + resolvedSchema := strings.TrimSpace(schema) + if result[0].ResolvedSchema != nil { + resolvedSchema = *result[0].ResolvedSchema + } + primary, _ := s.primaryKeys(resolvedSchema, table) + for index := range result { + result[index].IsPrimaryKey = primary[strings.ToLower(result[index].Name)] + } + if s.mode.sqlServerIdentity { + s.applyIdentityMetadata(resolvedSchema, table, result) + } + return result, nil } func (s *server) queryCatalogColumns( schema, table string, - primary map[string]bool, catalog, prefix, expression string, ) ([]columnInfo, error) { identityExpression := "a.attidentity" if s.catalogIdentityUnsupported { identityExpression = "CAST(NULL AS varchar(1)) AS attidentity" } - query := fmt.Sprintf(`SELECT a.attname, format_type(a.atttypid, a.atttypmod), NOT a.attnotnull, + relationPredicate := fmt.Sprintf("n.nspname = %s AND c.relname = %s", quoteLiteral(schema), quoteLiteral(table)) + if strings.TrimSpace(schema) == "" { + visibilityFunction := kingbaseCatalogFunction(catalog, "sys_table_is_visible", "pg_table_is_visible") + relationPredicate = fmt.Sprintf("c.relname = %s AND %s(c.oid)", quoteLiteral(table), visibilityFunction) + } + query := fmt.Sprintf(`SELECT n.nspname, a.attname, format_type(a.atttypid, a.atttypmod), NOT a.attnotnull, %s(ad.adbin, ad.adrelid), col_description(a.attrelid, a.attnum), CASE WHEN t.typname = 'numeric' AND a.atttypmod > 0 THEN ((a.atttypmod - 4) >> 16) & 65535 END, CASE WHEN t.typname = 'numeric' AND a.atttypmod > 0 THEN (a.atttypmod - 4) & 65535 END, @@ -1324,11 +1348,11 @@ func (s *server) queryCatalogColumns( FROM %s.%s_attribute a JOIN %s.%s_type t ON t.oid = a.atttypid JOIN %s.%s_class c ON c.oid = a.attrelid JOIN %s.%s_namespace n ON n.oid = c.relnamespace LEFT JOIN %s.%s_attrdef ad ON ad.adrelid = a.attrelid AND ad.adnum = a.attnum -WHERE n.nspname = %s AND c.relname = %s AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum`, expression, identityExpression, catalog, prefix, catalog, prefix, catalog, prefix, catalog, prefix, catalog, prefix, quoteLiteral(schema), quoteLiteral(table)) +WHERE %s AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum`, expression, identityExpression, catalog, prefix, catalog, prefix, catalog, prefix, catalog, prefix, catalog, prefix, relationPredicate) rows, err := s.metadataQuery(query) if err != nil && !s.catalogIdentityUnsupported && isUndefinedColumn(err, "attidentity") { s.catalogIdentityUnsupported = true - return s.queryCatalogColumns(schema, table, primary, catalog, prefix, expression) + return s.queryCatalogColumns(schema, table, catalog, prefix, expression) } if err != nil { return nil, err @@ -1336,21 +1360,18 @@ WHERE n.nspname = %s AND c.relname = %s AND a.attnum > 0 AND NOT a.attisdropped defer rows.Close() result := []columnInfo{} for rows.Next() { - var name, dataType string + var resolvedSchema, name, dataType string var nullable bool var defaultValue, comment, identity sql.NullString var precision, scale, length sql.NullInt64 - if err := rows.Scan(&name, &dataType, &nullable, &defaultValue, &comment, &precision, &scale, &length, &identity); err != nil { + if err := rows.Scan(&resolvedSchema, &name, &dataType, &nullable, &defaultValue, &comment, &precision, &scale, &length, &identity); err != nil { return nil, err } - result = append(result, columnInfo{Name: name, DataType: dataType, IsNullable: nullable, ColumnDefault: nullStringPtr(defaultValue), IsPrimaryKey: primary[strings.ToLower(name)], Extra: kingbaseIdentityClause(identity.String), Comment: nullStringPtr(comment), NumericPrecision: nullIntPtr(precision), NumericScale: nullIntPtr(scale), CharacterMaximumLength: nullIntPtr(length)}) + result = append(result, columnInfo{Name: name, DataType: dataType, ResolvedSchema: stringPtr(resolvedSchema), IsNullable: nullable, ColumnDefault: nullStringPtr(defaultValue), Extra: kingbaseIdentityClause(identity.String), Comment: nullStringPtr(comment), NumericPrecision: nullIntPtr(precision), NumericScale: nullIntPtr(scale), CharacterMaximumLength: nullIntPtr(length)}) } if err := rows.Err(); err != nil { return nil, err } - if s.mode.sqlServerIdentity { - s.applyIdentityMetadata(schema, table, result) - } return result, nil } @@ -1435,7 +1456,7 @@ func (s *server) queryInformationSchemaColumns(schema, table string, primary map if parsed := boundedVarcharLength(dataType); parsed != nil && !length.Valid { length = sql.NullInt64{Int64: int64(*parsed), Valid: true} } - result = append(result, columnInfo{Name: name, DataType: resolvedInformationSchemaDataType(dataType, fullDataType.String), FullDataType: fullDataType.String, IsNullable: strings.EqualFold(nullable, "YES"), ColumnDefault: nullStringPtr(defaultValue), IsPrimaryKey: primary[strings.ToLower(name)], Comment: nullStringPtr(comment), NumericPrecision: nullIntPtr(precision), NumericScale: nullIntPtr(scale), CharacterMaximumLength: nullIntPtr(length)}) + result = append(result, columnInfo{Name: name, DataType: resolvedInformationSchemaDataType(dataType, fullDataType.String), ResolvedSchema: stringPtr(schema), FullDataType: fullDataType.String, IsNullable: strings.EqualFold(nullable, "YES"), ColumnDefault: nullStringPtr(defaultValue), IsPrimaryKey: primary[strings.ToLower(name)], Comment: nullStringPtr(comment), NumericPrecision: nullIntPtr(precision), NumericScale: nullIntPtr(scale), CharacterMaximumLength: nullIntPtr(length)}) } return result, rows.Err() } diff --git a/agents/drivers/kingbase-go/main_test.go b/agents/drivers/kingbase-go/main_test.go index 653ece2d4e..e41efd7623 100644 --- a/agents/drivers/kingbase-go/main_test.go +++ b/agents/drivers/kingbase-go/main_test.go @@ -228,8 +228,8 @@ func (connection *fallbackConn) QueryContext(_ context.Context, query string, _ identity = nil } return &valueRows{ - columns: []string{"column_name", "data_type", "is_nullable", "column_default", "column_comment", "numeric_precision", "numeric_scale", "character_maximum_length", "attidentity"}, - rows: [][]driver.Value{{"id", "integer", false, nil, nil, int64(32), int64(0), nil, identity}}, + columns: []string{"resolved_schema", "column_name", "data_type", "is_nullable", "column_default", "column_comment", "numeric_precision", "numeric_scale", "character_maximum_length", "attidentity"}, + rows: [][]driver.Value{{"public", "id", "integer", false, nil, nil, int64(32), int64(0), nil, identity}}, }, nil } return nil, errors.New("unexpected query: " + query) @@ -1078,6 +1078,74 @@ func TestListIndexesFallsBackWhenWithOrdinalityIsUnsupported(t *testing.T) { } } +func TestGetColumnsUsesResolvedSchemaAcrossCatalogMetadata(t *testing.T) { + tests := []struct { + name string + postgresCatalog bool + requestedSchema string + resolvedSchema string + visibilityFunction string + sqlServerIdentity bool + }{ + {name: "sys catalog search path", resolvedSchema: "tenant_visible", visibilityFunction: "sys_catalog.sys_table_is_visible(c.oid)", sqlServerIdentity: true}, + {name: "postgres catalog search path", postgresCatalog: true, resolvedSchema: "tenant_pg", visibilityFunction: "pg_catalog.pg_table_is_visible(c.oid)"}, + {name: "explicit schema", requestedSchema: "tenant_explicit", resolvedSchema: "tenant_explicit"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + catalog := "sys_catalog" + prefix := "sys" + if test.postgresCatalog { + catalog = "pg_catalog" + prefix = "pg" + } + state := &metadataDriverState{query: func(query string) (driver.Rows, error) { + switch { + case strings.Contains(query, "FROM "+catalog+"."+prefix+"_attribute a"): + if test.visibilityFunction != "" { + if !strings.Contains(query, test.visibilityFunction) { + return nil, fmt.Errorf("unqualified columns query did not use the catalog visibility function: %s", query) + } + } else if strings.Contains(query, "table_is_visible") || !strings.Contains(query, "n.nspname = '"+test.requestedSchema+"'") { + return nil, fmt.Errorf("explicit-schema columns query changed resolution behavior: %s", query) + } + return &valueRows{ + columns: []string{"nspname", "attname", "format_type", "nullable", "default", "comment", "precision", "scale", "length", "identity"}, + rows: [][]driver.Value{{test.resolvedSchema, "feearea", "character varying", false, nil, nil, nil, nil, nil, nil}}, + }, nil + case strings.Contains(query, "FROM information_schema.table_constraints"): + if !strings.Contains(query, "tc.table_schema='"+test.resolvedSchema+"'") { + return nil, fmt.Errorf("primary-key lookup did not use resolved schema: %s", query) + } + return &valueRows{columns: []string{"column_name"}, rows: [][]driver.Value{{"feearea"}}}, nil + case strings.Contains(query, "FROM sys.identity_columns"): + if !strings.Contains(query, "n.nspname='"+test.resolvedSchema+"'") { + return nil, fmt.Errorf("identity lookup did not use resolved schema: %s", query) + } + return &valueRows{columns: []string{"attname", "seed_value", "increment_value"}, rows: [][]driver.Value{{"feearea", "1", "1"}}}, nil + default: + return nil, fmt.Errorf("unexpected query: %s", query) + } + }} + server := newServer() + server.db = openMetadataDB(t, state) + server.mode.postgresCatalog = test.postgresCatalog + server.mode.sqlServerIdentity = test.sqlServerIdentity + + columns, err := server.getColumns(test.requestedSchema, "m_workflow") + if err != nil { + t.Fatal(err) + } + if len(columns) != 1 || columns[0].ResolvedSchema == nil || *columns[0].ResolvedSchema != test.resolvedSchema || !columns[0].IsPrimaryKey { + t.Fatalf("resolved relation metadata was lost: %#v", columns) + } + if test.sqlServerIdentity && (columns[0].Extra == nil || *columns[0].Extra != "IDENTITY(1,1)") { + t.Fatalf("identity metadata did not use the resolved relation: %#v", columns) + } + }) + } +} + func TestListIndexesDoesNotFallbackForUnrelatedErrors(t *testing.T) { expectedErr := errors.New("metadata connection reset") state := &metadataDriverState{query: func(string) (driver.Rows, error) { return nil, expectedErr }} @@ -3286,6 +3354,9 @@ func TestMySQLCompatColumnsUsePostgresColumnComments(t *testing.T) { if columns[0].Extra != nil { t.Fatalf("MySQL-compatible metadata must not infer PostgreSQL identity: %#v", columns[0].Extra) } + if columns[0].ResolvedSchema == nil || *columns[0].ResolvedSchema != "public" { + t.Fatalf("MySQL-compatible metadata must keep its effective schema: %#v", columns[0]) + } state.mu.Lock() defer state.mu.Unlock() diff --git a/apps/desktop/src/lib/metadata/tableMetadataCache.ts b/apps/desktop/src/lib/metadata/tableMetadataCache.ts index 888abb0616..d78edb4962 100644 --- a/apps/desktop/src/lib/metadata/tableMetadataCache.ts +++ b/apps/desktop/src/lib/metadata/tableMetadataCache.ts @@ -328,13 +328,13 @@ export async function loadTableMetadata(request: TableMetadataRequest): Promise< scope, async () => { // Column discovery can be especially slow on Oracle. Start row-identity - // discovery independently unless an unqualified Vastbase relation must - // first report its visible schema for the index lookup. - const resolveVastbaseSchema = request.databaseType === "vastbase" && !request.schema; - const indexesPromise = resolveVastbaseSchema ? undefined : loadTableIndexes(request).catch((): IndexInfo[] => []); + // discovery independently unless an agent-backed PostgreSQL-family + // relation must first report its visible schema for the index lookup. + const resolveReportedSchema = (request.databaseType === "vastbase" || request.databaseType === "kingbase") && !request.schema; + const indexesPromise = resolveReportedSchema ? undefined : loadTableIndexes(request).catch((): IndexInfo[] => []); const columnsResult = await columnsPromise; const columns = columnsResult.columns; - const resolvedSchema = resolveVastbaseSchema ? columns.find((column) => column.resolved_schema)?.resolved_schema : request.schema; + const resolvedSchema = resolveReportedSchema ? columns.find((column) => column.resolved_schema)?.resolved_schema : request.schema; const indexes = columns.length > 0 ? await (indexesPromise ?? loadTableIndexes({ ...request, schema: resolvedSchema }).catch((): IndexInfo[] => [])) : []; const primaryKeys = editableRowIdentifierColumns(request.databaseType as DatabaseType, columns, indexes, request.tableType); return { diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 05e71e782b..d50601b5ab 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -3823,15 +3823,16 @@ export const useQueryStore = defineStore("query", () => { // Oracle-family connection databases are service names, not schemas. When // the query does not qualify a schema, let the driver resolve the current // login user's schema instead of looking up metadata under the service name. - // An unqualified Vastbase query runs in the connection's current - // search_path. Do not reinterpret the selected database as a schema; the - // agent resolves the visible relation's actual namespace with the columns. - const useCurrentVastbaseSchema = dbType === "vastbase" && !source.schema && !tab.schema; + // Unqualified agent-backed PostgreSQL-family queries run in the + // connection's current search_path. Do not reinterpret the selected + // database as a schema; the agent reports the visible relation's actual + // namespace with the columns. + const resolveAgentSearchPathSchema = (dbType === "vastbase" || dbType === "kingbase") && !source.schema && !tab.schema; // PostgreSQL-compatible unqualified names also resolve through the // connection's search_path. Keep the metadata request unqualified when no // schema was selected instead of assuming public (or the database name). const useCurrentPostgresSchema = (dbType === "postgres" || dbType === "kwdb") && !source.schema && !tab.schema; - const resolvedSchema = (dbType === "sqlserver" && !source.schema) || (ORACLE_LIKE_METADATA_TYPES.has(dbType) && !schema) || useCurrentVastbaseSchema || useCurrentPostgresSchema ? "" : metadataSchemaForConnection(conn, metadataDatabase, schema || undefined); + const resolvedSchema = (dbType === "sqlserver" && !source.schema) || (ORACLE_LIKE_METADATA_TYPES.has(dbType) && !schema) || resolveAgentSearchPathSchema || useCurrentPostgresSchema ? "" : metadataSchemaForConnection(conn, metadataDatabase, schema || undefined); const metadataSchema = normalizeUppercaseFoldedMetadataIdentifier(dbType, resolvedSchema || undefined, source.schema ? source.schemaQuoted : false) || ""; const metadataTableName = normalizeUppercaseFoldedMetadataIdentifier(dbType, source.tableName, source.tableNameQuoted)!; const metadataCatalog = normalizeUppercaseFoldedMetadataIdentifier(dbType, source.catalog, source.catalogQuoted); @@ -3863,7 +3864,8 @@ export const useQueryStore = defineStore("query", () => { } function loadedEditableSourceFromMetadata(target: EditableSourceMetadataTarget, metadata: Awaited>["metadata"]): LoadedEditableSource { - const writeSchema = target.request.databaseType === "vastbase" && !target.writeSchema ? metadata.schema : target.writeSchema; + const usesReportedSchema = target.request.databaseType === "vastbase" || target.request.databaseType === "kingbase"; + const writeSchema = usesReportedSchema && !target.writeSchema ? metadata.schema : target.writeSchema; return { source: target.source, analysis: target.analysis, diff --git a/packages/app-tests/queryStore.test.ts b/packages/app-tests/queryStore.test.ts index 6dc0a99b41..3cfbda143b 100644 --- a/packages/app-tests/queryStore.test.ts +++ b/packages/app-tests/queryStore.test.ts @@ -3003,6 +3003,101 @@ test("uses the visible Vastbase relation schema for unqualified query writes", a } }); +test("uses the visible Kingbase relation schema for an unqualified aliased query target", async () => { + const restoreStorage = installMemoryStorage(); + setActivePinia(createPinia()); + const connectionStore = useConnectionStore(); + const store = useQueryStore(); + const originalFetch = globalThis.fetch; + const columnRequests: Array<{ database: string | null; schema: string | null; table: string | null }> = []; + + connectionStore.addEphemeralConnection(kingbaseConn("kingbase-1")); + + globalThis.fetch = withConnectionHealthMock(async (input, init) => { + const url = String(input); + if (url === "/api/query/execute-multi") { + return new Response( + JSON.stringify([ + { + columns: ["feearea", "month", "status"], + rows: [["1010", 202507, "3"]], + affected_rows: 0, + execution_time_ms: 1, + }, + ]), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); + } + if (url === "/api/query/analyze-editability") { + const body = JSON.parse(String(init?.body ?? "{}")); + const explicitSchema = body.sql.includes("actual_schema") ? "actual_schema" : undefined; + return new Response( + JSON.stringify({ + editable: true, + analysis: { + schema: explicitSchema, + schemaQuoted: false, + tableName: "m_workflow", + tableNameQuoted: false, + tableAlias: "mw", + selectStar: true, + sources: [{ key: "mw:0", schema: explicitSchema, tableName: "m_workflow", tableNameQuoted: false, schemaQuoted: false, alias: "mw" }], + columns: [], + }, + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); + } + if (url.startsWith("/api/schema/columns?")) { + const params = new URL(url, "http://localhost").searchParams; + columnRequests.push({ database: params.get("database"), schema: params.get("schema"), table: params.get("table") }); + const resolvedSchema = params.get("schema") || "workflow_schema"; + return new Response( + JSON.stringify([ + { name: "feearea", data_type: "varchar", resolved_schema: resolvedSchema, is_nullable: false, column_default: null, is_primary_key: false, extra: null, comment: null }, + { name: "month", data_type: "integer", resolved_schema: resolvedSchema, is_nullable: false, column_default: null, is_primary_key: false, extra: null, comment: null }, + { name: "status", data_type: "varchar", resolved_schema: resolvedSchema, is_nullable: false, column_default: null, is_primary_key: false, extra: null, comment: null }, + ]), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); + } + if (url === "/api/query/prepare-pagination-plan") { + const body = JSON.parse(String(init?.body ?? "{}")); + return new Response(JSON.stringify({ sqlToExecute: body.options.sql, useAgentResultSession: false }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } + return new Response("unexpected request", { status: 500 }); + }); + + try { + const tabId = store.createTab("kingbase-1", "lx_dev_db", "Unqualified", "query"); + await store.executeTabSql(tabId, "select * from m_workflow AS mw where feearea = '1010' and month = 202507"); + + const tab = store.tabs.find((item) => item.id === tabId); + await waitFor(() => columnRequests.length > 0 && tab?.tableMeta?.tableName === "m_workflow"); + assert.deepEqual(columnRequests, [{ database: "lx_dev_db", schema: "", table: "m_workflow" }]); + assert.equal(tab?.tableMeta?.database, "lx_dev_db"); + assert.equal(tab?.tableMeta?.schema, "workflow_schema"); + assert.deepEqual(tab?.tableMeta?.primaryKeys, []); + assert.equal(tab?.queryAnalysis?.tableAlias, "mw"); + assert.equal(tab?.queryEditabilityReason, undefined); + + const explicitTabId = store.createTab("kingbase-1", "lx_dev_db", "Explicit", "query"); + await store.executeTabSql(explicitTabId, "select * from actual_schema.m_workflow AS mw where feearea = '1010'"); + + const explicitTab = store.tabs.find((item) => item.id === explicitTabId); + await waitFor(() => columnRequests.length > 1 && explicitTab?.tableMeta?.tableName === "m_workflow"); + assert.deepEqual(columnRequests[1], { database: "lx_dev_db", schema: "actual_schema", table: "m_workflow" }); + assert.equal(explicitTab?.tableMeta?.schema, "actual_schema"); + assert.equal(explicitTab?.queryAnalysis?.tableAlias, "mw"); + } finally { + globalThis.fetch = originalFetch; + restoreStorage(); + } +}); + test("keeps PostgreSQL quoted primary keys distinct from case-only result columns", async () => { const restoreStorage = installMemoryStorage(); setActivePinia(createPinia()); diff --git a/packages/app-tests/tableMetadataCache.test.ts b/packages/app-tests/tableMetadataCache.test.ts index 46deab9ae1..5a7da706df 100644 --- a/packages/app-tests/tableMetadataCache.test.ts +++ b/packages/app-tests/tableMetadataCache.test.ts @@ -61,37 +61,51 @@ test("table metadata loader deduplicates equivalent in-flight requests and cache assert.equal(getColumns.mock.calls.length, 1); }); -test("uses the visible Vastbase relation schema for the index lookup", async () => { - const getColumns = vi.fn(async (): Promise => [ - { - name: "id", - data_type: "bigint", - resolved_schema: "tenant_b", - is_nullable: false, - column_default: null, - is_primary_key: true, - extra: null, - }, - ]); +test("uses agent-reported visible schemas for Vastbase and Kingbase index lookups", async () => { + const resolvedSchemas: Record = { + "vastbase-1": "tenant_b", + "kingbase-1": "workflow_schema", + }; + const getColumns = vi.fn(async (connectionId: string): Promise => { + const resolvedSchema = resolvedSchemas[connectionId]; + assert.ok(resolvedSchema); + return [ + { + name: "id", + data_type: "bigint", + resolved_schema: resolvedSchema, + is_nullable: false, + column_default: null, + is_primary_key: true, + extra: null, + }, + ]; + }); const listIndexes = vi.fn(async (): Promise => []); vi.doMock("@/lib/backend/api", () => ({ getColumns, listIndexes })); const { clearTableMetadataCache, loadTableMetadata } = await import("../../apps/desktop/src/lib/metadata/tableMetadataCache.ts"); clearTableMetadataCache(); - const result = await loadTableMetadata({ - connectionId: "conn", - database: "app", - schema: "", - tableName: "users", - tableType: "TABLE", - databaseType: "vastbase", - driverProfile: "vastbase", - }); + const testCases = [ + { connectionId: "vastbase-1", databaseType: "vastbase", resolvedSchema: "tenant_b" }, + { connectionId: "kingbase-1", databaseType: "kingbase", resolvedSchema: "workflow_schema" }, + ] as const; + for (const [index, testCase] of testCases.entries()) { + const result = await loadTableMetadata({ + connectionId: testCase.connectionId, + database: "app", + schema: "", + tableName: "users", + tableType: "TABLE", + databaseType: testCase.databaseType, + driverProfile: testCase.databaseType, + }); - assert.equal(result.metadata.schema, "tenant_b"); - assert.equal(listIndexes.mock.calls.length, 1); - assert.equal(listIndexes.mock.calls[0]?.[2], "tenant_b"); + assert.equal(result.metadata.schema, testCase.resolvedSchema); + assert.equal(listIndexes.mock.calls[index]?.[2], testCase.resolvedSchema); + } + assert.equal(listIndexes.mock.calls.length, 2); }); test("table metadata invalidation keeps unrelated schemas cached", async () => {