fix(spanner): restore column completion for default schema

Closes #9367
This commit is contained in:
t8y2
2026-09-27 07:16:29 +00:00
parent 71027f1412
commit ba37b2c15f
2 changed files with 115 additions and 2 deletions
@@ -26,6 +26,19 @@ function postgresConnection(): ConnectionConfig {
} as ConnectionConfig;
}
function spannerConnection(): ConnectionConfig {
return {
...postgresConnection(),
id: "spanner-1",
name: "Cloud Spanner",
db_type: "spanner",
host: "spanner.googleapis.com",
port: 443,
username: "",
database: "projects/test-project/instances/test-instance/databases/app",
} as ConnectionConfig;
}
function mysqlConnection(): ConnectionConfig {
return {
...postgresConnection(),
@@ -789,6 +802,104 @@ describe("connectionStore completion assistant", () => {
expect(columns).toEqual([expect.objectContaining({ name: "REPORT_ID", table: "ORDERS", schema: undefined, dataType: "NUMBER" })]);
});
it("loads Cloud Spanner columns from the empty GoogleSQL default schema", async () => {
const completionAssistantSearch = vi.fn().mockResolvedValue({
candidates: [{ name: "SingerId", kind: "column", schema: "", parent_schema: "", parent_name: "Singers", data_type: "INT64" }],
incomplete: false,
fallback_used: false,
});
const getColumns = vi.fn();
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
completionAssistantSearch,
getColumns,
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
store.connections = [spannerConnection()];
store.connectedIds.add("spanner-1");
const columns = await store.listCompletionColumns("spanner-1", spannerConnection().database, "Singers", "");
expect(completionAssistantSearch).toHaveBeenCalledWith(expect.objectContaining({ schema: "", parent_schema: "", parent_name: "Singers" }));
expect(getColumns).not.toHaveBeenCalled();
expect(columns).toEqual([expect.objectContaining({ name: "SingerId", table: "Singers", schema: "", dataType: "INT64" })]);
});
it.each(["sales", "public"])("keeps Cloud Spanner column completion scoped to the named %s schema", async (schema) => {
const completionAssistantSearch = vi.fn().mockResolvedValue({
candidates: [{ name: "OrderId", kind: "column", schema, parent_schema: schema, parent_name: "Orders", data_type: schema === "public" ? "bigint" : "INT64" }],
incomplete: false,
fallback_used: false,
});
const getColumns = vi.fn();
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
completionAssistantSearch,
getColumns,
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
store.connections = [spannerConnection()];
store.connectedIds.add("spanner-1");
const columns = await store.listCompletionColumns("spanner-1", spannerConnection().database, "Orders", schema);
expect(completionAssistantSearch).toHaveBeenCalledWith(expect.objectContaining({ schema, parent_schema: schema, parent_name: "Orders" }));
expect(getColumns).not.toHaveBeenCalled();
expect(columns).toEqual([expect.objectContaining({ name: "OrderId", table: "Orders", schema })]);
});
it("still skips column metadata for another schema-aware database without a selected schema", async () => {
const completionAssistantSearch = vi.fn();
const getColumns = vi.fn();
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
completionAssistantSearch,
getColumns,
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
store.connections = [postgresConnection()];
store.connectedIds.add("pg-1");
await expect(store.listCompletionColumns("pg-1", "app", "orders", undefined)).resolves.toEqual([]);
expect(completionAssistantSearch).not.toHaveBeenCalled();
expect(getColumns).not.toHaveBeenCalled();
});
it.each(["empty", "error"] as const)("falls back to canonical Cloud Spanner metadata after an %s assistant result", async (assistantResult) => {
const completionAssistantSearch = assistantResult === "empty" ? vi.fn().mockResolvedValue({ candidates: [], incomplete: false, fallback_used: false }) : vi.fn().mockRejectedValue(new Error("assistant unavailable"));
const getColumns = vi.fn().mockResolvedValue([{ name: "SingerId", data_type: "INT64", is_nullable: false, column_default: null, is_primary_key: true, extra: null, comment: null }]);
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
completionAssistantSearch,
getColumns,
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
store.connections = [spannerConnection()];
store.connectedIds.add("spanner-1");
const columns = await store.listCompletionColumns("spanner-1", spannerConnection().database, "Singers", "");
expect(completionAssistantSearch).toHaveBeenCalledOnce();
expect(getColumns).toHaveBeenCalledWith("spanner-1", spannerConnection().database, "", "Singers", undefined, undefined);
expect(columns).toEqual([expect.objectContaining({ name: "SingerId", table: "Singers", schema: "", dataType: "INT64" })]);
});
it("uses the Dameng login schema for unqualified column completion", async () => {
const completionAssistantSearch = vi.fn().mockRejectedValue(new Error("assistant unavailable"));
const getColumns = vi.fn().mockResolvedValue([{ name: "ID", data_type: "BIGINT", is_nullable: false, column_default: null, is_primary_key: true, extra: null, comment: null }]);
+4 -2
View File
@@ -9059,10 +9059,12 @@ export const useConnectionStore = defineStore("connection", () => {
const oracleIdentifier = effectiveDbType === "oracle" || effectiveDbType === "oceanbase-oracle";
const uppercaseUnquotedIdentifier = oracleIdentifier || effectiveDbType === "saphana";
const completionTable = uppercaseUnquotedIdentifier && context?.tableQuoted === false ? table.toUpperCase() : table;
const rawCompletionSchema = schema?.trim() || (effectiveDbType === "dameng" ? config?.username?.trim() || undefined : undefined);
const normalizedSchema = schema?.trim();
const rawCompletionSchema = effectiveDbType === "spanner" ? normalizedSchema : normalizedSchema || (effectiveDbType === "dameng" ? config?.username?.trim() || undefined : undefined);
const completionSchema = uppercaseUnquotedIdentifier && rawCompletionSchema && context?.schemaQuoted === false ? rawCompletionSchema.toUpperCase() : rawCompletionSchema;
const usesCurrentSchema = usesOracleCurrentSchemaCompletion(effectiveDbType, completionSchema);
if (isSchemaAwareDatabase(connectionId) && !connectionUsesDatabaseObjectTreeMode(config) && !completionSchema && !usesCurrentSchema) {
const hasCompletionSchema = completionSchema != null && (completionSchema !== "" || effectiveDbType === "spanner");
if (isSchemaAwareDatabase(connectionId) && !connectionUsesDatabaseObjectTreeMode(config) && !hasCompletionSchema && !usesCurrentSchema) {
return [];
}
const sessionCacheScope = usesCurrentSchema && context?.clientSessionId ? `:${context.clientSessionId}:${context.version ?? 0}` : "";