From 54b302683ab97f596e2fcb0bfa8a7d67fa5ab257 Mon Sep 17 00:00:00 2001 From: lxk955 <164191365+lxk955@users.noreply.github.com> Date: Sat, 10 Oct 2026 00:13:37 +0800 Subject: [PATCH] fix(metadata): resolve visible schema for query DDL and loads --- .../drivers/kingbase-go/kingbase_metadata.go | 8 + agents/drivers/kingbase-go/main_test.go | 8 + .../drivers/vastbase-go/vastbase_metadata.go | 8 + apps/desktop/src/components/grid/DataGrid.vue | 6 +- .../useDataGridTableMetadataLoaders.spec.ts | 211 ++++++++++++++++++ .../useDataGridTableMetadataLoaders.ts | 16 +- .../src/lib/metadata/tableMetadataCache.ts | 6 +- apps/desktop/src/stores/queryStore.ts | 4 +- packages/app-tests/tableMetadataCache.test.ts | 14 +- 9 files changed, 270 insertions(+), 11 deletions(-) create mode 100644 apps/desktop/src/composables/__tests__/useDataGridTableMetadataLoaders.spec.ts diff --git a/agents/drivers/kingbase-go/kingbase_metadata.go b/agents/drivers/kingbase-go/kingbase_metadata.go index 95ac5deeec..ac4d39e386 100644 --- a/agents/drivers/kingbase-go/kingbase_metadata.go +++ b/agents/drivers/kingbase-go/kingbase_metadata.go @@ -2738,6 +2738,14 @@ func (s *server) getTableDDL(schema, table string) (string, error) { if err != nil { return "", err } + if len(columns) == 0 { + if visibleColumns, fallbackErr := s.getColumns("", table); fallbackErr == nil && len(visibleColumns) > 0 { + columns = visibleColumns + if columns[0].ResolvedSchema != nil && *columns[0].ResolvedSchema != "" { + effective = *columns[0].ResolvedSchema + } + } + } tableComment, _ := s.getTableComment(effective, table) ddl := s.renderTableDDL(effective, table, columns, tableComment) ddl, err = s.appendTableIndexDDL(effective, table, ddl) diff --git a/agents/drivers/kingbase-go/main_test.go b/agents/drivers/kingbase-go/main_test.go index 1c3a16dbc4..fab47f6227 100644 --- a/agents/drivers/kingbase-go/main_test.go +++ b/agents/drivers/kingbase-go/main_test.go @@ -3917,6 +3917,14 @@ func TestTableDDLIncludesIdentityIndexesTriggersAndComments(t *testing.T) { t.Fatalf("table DDL missing %q:\n%s", expected, ddl) } } + + ddlUnqualified, err := server.getTableDDL("", "orders") + if err != nil { + t.Fatal(err) + } + if !strings.Contains(ddlUnqualified, `"id" integer GENERATED BY DEFAULT AS IDENTITY NOT NULL`) { + t.Fatalf("identity clause missing from unqualified table DDL: %s", ddlUnqualified) + } } func TestRenderTableDDLIncludesEscapedComments(t *testing.T) { diff --git a/agents/drivers/vastbase-go/vastbase_metadata.go b/agents/drivers/vastbase-go/vastbase_metadata.go index 15a7943252..7f8aec7964 100644 --- a/agents/drivers/vastbase-go/vastbase_metadata.go +++ b/agents/drivers/vastbase-go/vastbase_metadata.go @@ -1868,6 +1868,14 @@ func (s *server) getTableDDL(schema, table string) (string, error) { if err != nil { return "", err } + if len(columns) == 0 { + if visibleColumns, fallbackErr := s.getColumns("", table); fallbackErr == nil && len(visibleColumns) > 0 { + columns = visibleColumns + if columns[0].ResolvedSchema != nil && *columns[0].ResolvedSchema != "" { + effective = *columns[0].ResolvedSchema + } + } + } tableComment, _ := s.getTableComment(effective, table) ddl := renderTableDDL(effective, table, columns, tableComment) ddl, err = s.appendTableIndexDDL(effective, table, ddl) diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 234f8ab256..79265d0dc3 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -397,7 +397,7 @@ import { filterObjectBrowserTableColumns } from "@/lib/table/objectBrowserTableI import { gaussdbMTypeDisplayName } from "@/lib/table/postgresDataTypeHelp"; import { reserveDataGridHeaderLine } from "@/lib/dataGrid/dataGridHeaderLayout"; import { buildColumnIndexMap, columnIndexColorClass, columnIndexNameKey, type ColumnIndexKind } from "@/lib/dataGrid/dataGridColumnIndexIcon"; -import { supportsTableStructureEditing } from "@/lib/database/databaseCapabilities"; +import { isSchemaAware, supportsTableStructureEditing } from "@/lib/database/databaseCapabilities"; import { rememberDataGridConditionHistory } from "@/lib/dataGrid/dataGridConditionHistory"; import { restoreDataGridLocalColumnFilters, type DataGridLocalFilterOption } from "@/lib/dataGrid/dataGridLocalColumnFilterState"; import { effectiveDatabaseTypeForConnection, gaussdbCountQueryDopHint } from "@/lib/database/jdbcDialect"; @@ -12016,7 +12016,7 @@ const canShowTableIndexes = computed(() => tableMetadataCapabilities.value.index async function probeTablePartitionStatus() { const connectionId = props.connectionId; const database = props.database; - const schema = props.tableMeta?.schema || props.database || ""; + const schema = props.tableMeta?.schema || props.schema || (isSchemaAware(resolvedDatabaseType.value) ? "" : props.database) || ""; const tableName = props.tableMeta?.tableName; const identity = currentIndexTableIdentity.value; if (!tableMetadataCapabilities.value.partitions || !connectionId || !database || !tableName || !identity) { @@ -12366,7 +12366,7 @@ async function loadForeignKeyDisplayLabels() { configuredColumns.map(async (columnIndex) => { const config = savedForeignKeyDisplayConfig(columnIndex); if (!config) return; - const schema = config.refSchema || props.tableMeta?.schema || props.schema || props.database || ""; + const schema = config.refSchema || props.tableMeta?.schema || props.schema || (isSchemaAware(resolvedDatabaseType.value) ? "" : props.database) || ""; try { const columns = await foreignKeyDisplayRequests.request(requestGeneration, JSON.stringify(["columns", props.connectionId, props.database || "", props.tableMeta?.catalog || "", schema, config.refTable]), () => api.getColumns(props.connectionId!, props.database || "", schema, config.refTable, props.tableMeta?.catalog), diff --git a/apps/desktop/src/composables/__tests__/useDataGridTableMetadataLoaders.spec.ts b/apps/desktop/src/composables/__tests__/useDataGridTableMetadataLoaders.spec.ts new file mode 100644 index 0000000000..0cfae8c65e --- /dev/null +++ b/apps/desktop/src/composables/__tests__/useDataGridTableMetadataLoaders.spec.ts @@ -0,0 +1,211 @@ +// @vitest-environment happy-dom +import { computed, ref } from "vue"; +import { describe, expect, it, vi, beforeEach } from "vitest"; +import { useDataGridTableMetadataLoaders } from "@/composables/useDataGridTableMetadataLoaders"; +import type { DatabaseType } from "@/types/database"; + +const mocks = vi.hoisted(() => ({ + loadObjectDdl: vi.fn(), + getColumns: vi.fn(), + listIndexes: vi.fn(), +})); + +vi.mock("@/lib/metadata/objectDdlCache", () => ({ + loadObjectDdl: mocks.loadObjectDdl, +})); + +vi.mock("@/lib/backend/api", () => ({ + getColumns: mocks.getColumns, + listIndexes: mocks.listIndexes, + getTableOwner: vi.fn(), + listForeignKeys: vi.fn(), + listTriggers: vi.fn(), + listConstraints: vi.fn(), + listPartitions: vi.fn(), +})); + +function createTestState() { + return { + ddlContent: ref(""), + ddlLoading: ref(false), + tableInfoColumns: ref([]), + tableInfoColumnsLoading: ref(false), + tableOwner: ref(null), + tableOwnerLoading: ref(false), + tableOwnerError: ref(""), + indexes: ref([]), + indexesLoaded: ref(false), + indexesLoading: ref(false), + indexesError: ref(""), + foreignKeys: ref([]), + foreignKeysLoaded: ref(false), + foreignKeysLoading: ref(false), + foreignKeysError: ref(""), + triggers: ref([]), + triggersLoaded: ref(false), + triggersLoading: ref(false), + triggersError: ref(""), + constraints: ref([]), + constraintsLoaded: ref(false), + constraintsLoading: ref(false), + constraintsError: ref(""), + partitioning: ref(null), + partitioningLoaded: ref(false), + partitioningLoading: ref(false), + partitioningError: ref(""), + partitioningRequestGeneration: ref(0), + tableInfoColumnsRequestGeneration: ref(0), + tableOwnerRequestGeneration: ref(0), + indexesRequestGeneration: ref(0), + foreignKeysRequestGeneration: ref(0), + constraintsRequestGeneration: ref(0), + }; +} + +describe("useDataGridTableMetadataLoaders", () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.loadObjectDdl.mockResolvedValue({ ddl: "CREATE TABLE test (id int);" }); + mocks.getColumns.mockResolvedValue([{ name: "id", data_type: "int" }]); + mocks.listIndexes.mockResolvedValue([]); + }); + + it("does not fall back to database as schema for schema-aware database Kingbase (#11347)", async () => { + const state = createTestState(); + const resolvedDatabaseType = ref("kingbase"); + const showTableInfo = ref(false); + + const loaders = useDataGridTableMetadataLoaders({ + props: { + connectionId: "conn-1", + database: "TESTDB", + schema: undefined, + tableMeta: { + tableName: "CTRC_REG_PRO_FLOW", + columns: [], + primaryKeys: [], + }, + }, + state, + settingsStore: { + editorSettings: { + refreshDdlOnOpen: false, + generateSqlIncludeDatabaseName: false, + generateSqlQuoteIdentifiers: true, + }, + } as any, + connectionStore: {} as any, + resolvedDatabaseType: computed(() => resolvedDatabaseType.value), + canShowTableIndexes: computed(() => true), + showTableInfo, + toast: vi.fn(), + formatBackendError: (e) => String(e), + toastMongoIndexRefreshError: vi.fn(), + }); + + await loaders.fetchDdl(); + + expect(mocks.loadObjectDdl).toHaveBeenCalledWith( + expect.objectContaining({ + connectionId: "conn-1", + database: "TESTDB", + schema: "", + tableName: "CTRC_REG_PRO_FLOW", + }), + expect.anything(), + ); + }); + + it("preserves explicit schema for schema-aware database Kingbase", async () => { + const state = createTestState(); + const resolvedDatabaseType = ref("kingbase"); + const showTableInfo = ref(false); + + const loaders = useDataGridTableMetadataLoaders({ + props: { + connectionId: "conn-1", + database: "TESTDB", + schema: "public", + tableMeta: { + tableName: "CTRC_REG_PRO_FLOW", + schema: "public", + columns: [], + primaryKeys: [], + }, + }, + state, + settingsStore: { + editorSettings: { + refreshDdlOnOpen: false, + generateSqlIncludeDatabaseName: false, + generateSqlQuoteIdentifiers: true, + }, + } as any, + connectionStore: {} as any, + resolvedDatabaseType: computed(() => resolvedDatabaseType.value), + canShowTableIndexes: computed(() => true), + showTableInfo, + toast: vi.fn(), + formatBackendError: (e) => String(e), + toastMongoIndexRefreshError: vi.fn(), + }); + + await loaders.fetchDdl(); + + expect(mocks.loadObjectDdl).toHaveBeenCalledWith( + expect.objectContaining({ + connectionId: "conn-1", + database: "TESTDB", + schema: "public", + tableName: "CTRC_REG_PRO_FLOW", + }), + expect.anything(), + ); + }); + + it("falls back to database as schema for non-schema-aware database MySQL", async () => { + const state = createTestState(); + const resolvedDatabaseType = ref("mysql"); + const showTableInfo = ref(false); + + const loaders = useDataGridTableMetadataLoaders({ + props: { + connectionId: "conn-mysql", + database: "shop_db", + schema: undefined, + tableMeta: { + tableName: "orders", + columns: [], + primaryKeys: [], + }, + }, + state, + settingsStore: { + editorSettings: { + refreshDdlOnOpen: false, + generateSqlIncludeDatabaseName: false, + generateSqlQuoteIdentifiers: true, + }, + } as any, + connectionStore: {} as any, + resolvedDatabaseType: computed(() => resolvedDatabaseType.value), + canShowTableIndexes: computed(() => true), + showTableInfo, + toast: vi.fn(), + formatBackendError: (e) => String(e), + toastMongoIndexRefreshError: vi.fn(), + }); + + await loaders.fetchDdl(); + + expect(mocks.loadObjectDdl).toHaveBeenCalledWith( + expect.objectContaining({ + connectionId: "conn-mysql", + database: "shop_db", + schema: "shop_db", + tableName: "orders", + }), + expect.anything(), + ); + }); +}); diff --git a/apps/desktop/src/composables/useDataGridTableMetadataLoaders.ts b/apps/desktop/src/composables/useDataGridTableMetadataLoaders.ts index ab938525fb..b23d0bb8a6 100644 --- a/apps/desktop/src/composables/useDataGridTableMetadataLoaders.ts +++ b/apps/desktop/src/composables/useDataGridTableMetadataLoaders.ts @@ -1,5 +1,6 @@ import { computed, type ComputedRef, type Ref } from "vue"; import * as api from "@/lib/backend/api"; +import { isSchemaAware } from "@/lib/database/databaseCapabilities"; import { applyDdlDatabaseQualifier, omitDdlIdentifierQuotes } from "@/lib/sql/ddlDisplay"; import { sqlFormatDialectForDbType } from "@/lib/sql/sqlFormatter"; import { loadObjectDdl } from "@/lib/metadata/objectDdlCache"; @@ -84,20 +85,27 @@ export interface DataGridTableMetadataLoaderOptions { export function useDataGridTableMetadataLoaders(options: DataGridTableMetadataLoaderOptions) { const { props, state } = options; + const defaultSchema = () => { + if (isSchemaAware(options.resolvedDatabaseType.value)) { + return props.tableMeta?.schema || props.schema || ""; + } + return props.tableMeta?.schema || props.schema || props.database || ""; + }; + const currentTableIdentity = (kind: "columns" | "owner" | "indexes" | "foreignKeys" | "constraints") => kind === "foreignKeys" ? foreignKeyTableIdentity({ connectionId: props.connectionId, database: props.database, catalog: props.tableMeta?.catalog, - schema: props.tableMeta?.schema, + schema: defaultSchema(), tableName: props.tableMeta?.tableName, }) : columnIndexTableIdentity({ connectionId: props.connectionId, database: props.database, catalog: props.tableMeta?.catalog, - schema: props.tableMeta?.schema, + schema: defaultSchema(), tableName: props.tableMeta?.tableName, }); @@ -106,7 +114,7 @@ export function useDataGridTableMetadataLoaders(options: DataGridTableMetadataLo return { connectionId: props.connectionId, database: props.database || "", - schema: props.tableMeta.schema || props.database || "", + schema: defaultSchema(), tableName: props.tableMeta.tableName, catalog: props.tableMeta.catalog, objectType: tableObjectSourceKind(props.tableMeta.tableType), @@ -123,7 +131,7 @@ export function useDataGridTableMetadataLoaders(options: DataGridTableMetadataLo // Query execution and source-table metadata can belong to different // databases (for example SQL Server three-part table references). request.database = props.tableMeta?.database || request.database; - request.schema = props.tableMeta?.schema || props.schema || props.database || ""; + request.schema = defaultSchema(); options.showTableInfo.value = true; state.ddlLoading.value = true; try { diff --git a/apps/desktop/src/lib/metadata/tableMetadataCache.ts b/apps/desktop/src/lib/metadata/tableMetadataCache.ts index 642dd8853c..11d5a8a7b6 100644 --- a/apps/desktop/src/lib/metadata/tableMetadataCache.ts +++ b/apps/desktop/src/lib/metadata/tableMetadataCache.ts @@ -60,6 +60,7 @@ export interface TableColumnsMetadata { } export interface TableColumnsLoadResult { + schema?: string; columns: ColumnInfo[]; tableType?: string; cacheStatus: MetadataLoadCacheStatus; @@ -248,8 +249,9 @@ export async function loadTableColumns(request: TableMetadataRequest): Promise { // Display-only loader: columns only, never index discovery. const columns = await api.getColumns(request.connectionId, request.database, request.schema ?? "", request.tableName, request.catalog); + const resolvedSchema = (request.databaseType === "vastbase" || request.databaseType === "kingbase") && !request.schema ? columns.find((column) => column.resolved_schema)?.resolved_schema : request.schema; return { - schema: request.schema || undefined, + schema: resolvedSchema || undefined, tableName: request.tableName, tableType: request.tableType, catalog: request.catalog, @@ -272,7 +274,7 @@ export async function loadTableColumns(request: TableMetadataRequest): Promise { diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index b898b5dfb2..dc386c3f19 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -6283,13 +6283,15 @@ export const useQueryStore = defineStore("query", () => { } function loadedEditableSourceFromColumns(target: EditableSourceMetadataTarget, loadedColumns: Awaited>): LoadedEditableSource { + const usesReportedSchema = target.request.databaseType === "vastbase" || target.request.databaseType === "kingbase"; + const writeSchema = usesReportedSchema && !target.writeSchema ? loadedColumns.schema : target.writeSchema; return { source: target.source, analysis: target.analysis, tableMeta: { catalog: target.request.catalog, database: target.request.database, - schema: target.writeSchema, + schema: writeSchema, tableName: target.request.tableName, tableType: loadedColumns.tableType, columns: loadedColumns.columns, diff --git a/packages/app-tests/tableMetadataCache.test.ts b/packages/app-tests/tableMetadataCache.test.ts index 5a7da706df..81db42c01e 100644 --- a/packages/app-tests/tableMetadataCache.test.ts +++ b/packages/app-tests/tableMetadataCache.test.ts @@ -84,7 +84,7 @@ test("uses agent-reported visible schemas for Vastbase and Kingbase index lookup 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"); + const { clearTableMetadataCache, loadTableColumns, loadTableMetadata } = await import("../../apps/desktop/src/lib/metadata/tableMetadataCache.ts"); clearTableMetadataCache(); const testCases = [ @@ -104,6 +104,18 @@ test("uses agent-reported visible schemas for Vastbase and Kingbase index lookup assert.equal(result.metadata.schema, testCase.resolvedSchema); assert.equal(listIndexes.mock.calls[index]?.[2], testCase.resolvedSchema); + + const columnsResult = await loadTableColumns({ + connectionId: testCase.connectionId, + database: "app", + schema: "", + tableName: "users", + tableType: "TABLE", + databaseType: testCase.databaseType, + driverProfile: testCase.databaseType, + force: true, + }); + assert.equal(columnsResult.schema, testCase.resolvedSchema); } assert.equal(listIndexes.mock.calls.length, 2); });