fix(metadata): resolve visible schema for query DDL and loads

This commit is contained in:
lxk955
2026-10-10 00:13:37 +08:00
committed by GitHub
parent 26a579ba7c
commit 54b302683a
9 changed files with 270 additions and 11 deletions
@@ -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)
+8
View File
@@ -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) {
@@ -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)
@@ -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),
@@ -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<string | null>(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<DatabaseType>("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<DatabaseType>("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<DatabaseType>("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(),
);
});
});
@@ -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 {
@@ -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<T
async () => {
// 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<T
resultCount: metadata.columns.length,
force: request.force === true,
});
return { columns: metadata.columns, tableType: metadata.tableType, cacheStatus: request.force ? "refresh" : "miss", ageMs: 0, cachedAt: metadata.cachedAt };
return { schema: metadata.schema, columns: metadata.columns, tableType: metadata.tableType, cacheStatus: request.force ? "refresh" : "miss", ageMs: 0, cachedAt: metadata.cachedAt };
}
export function tableMetadataToDataTabMeta(metadata: TableMetadata, overrides?: { schema?: string }): NonNullable<QueryTab["tableMeta"]> {
+3 -1
View File
@@ -6283,13 +6283,15 @@ export const useQueryStore = defineStore("query", () => {
}
function loadedEditableSourceFromColumns(target: EditableSourceMetadataTarget, loadedColumns: Awaited<ReturnType<typeof loadTableColumns>>): 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,
+13 -1
View File
@@ -84,7 +84,7 @@ test("uses agent-reported visible schemas for Vastbase and Kingbase index lookup
const listIndexes = vi.fn(async (): Promise<IndexInfo[]> => []);
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);
});