From 6686d1628e5fcd0554618b5ecaf224d339afd7d4 Mon Sep 17 00:00:00 2001 From: zipg Date: Wed, 26 Aug 2026 15:07:37 +0800 Subject: [PATCH] fix(completion): resolve ob-oracle schema for alias column completion --- .../OceanBaseOracleAgentTest.java | 61 +++++++++++++++++++ .../sql/oracleCompletionSession.spec.ts | 3 + .../src/lib/sql/oracleCompletionSession.ts | 6 +- .../connectionStore.completion.spec.ts | 35 +++++++++++ apps/desktop/src/stores/connectionStore.ts | 15 ++--- 5 files changed, 112 insertions(+), 8 deletions(-) diff --git a/agents/drivers/oceanbase-oracle/src/test/java/com/dbx/agent/oceanbaseoracle/OceanBaseOracleAgentTest.java b/agents/drivers/oceanbase-oracle/src/test/java/com/dbx/agent/oceanbaseoracle/OceanBaseOracleAgentTest.java index 7e5709cdd4..48f2066b5a 100644 --- a/agents/drivers/oceanbase-oracle/src/test/java/com/dbx/agent/oceanbaseoracle/OceanBaseOracleAgentTest.java +++ b/agents/drivers/oceanbase-oracle/src/test/java/com/dbx/agent/oceanbaseoracle/OceanBaseOracleAgentTest.java @@ -398,6 +398,28 @@ class OceanBaseOracleAgentTest { Assertions.assertTrue(sql.get(0).contains("c.DATA_DEFAULT"), sql.get(0)); } + @Test + void getColumnsResolvesTheCurrentSchemaWhenSchemaIsEmpty() { + List sql = new ArrayList<>(); + List params = new ArrayList<>(); + OceanBaseOracleAgent agent = new OceanBaseOracleAgent(); + TestSupport.setPrivateConnection(agent, currentSchemaColumnConnection( + sql, + params, + resultSet(new String[]{"CURRENT_SCHEMA"}, new Object[][]{{"APP"}}), + columnResultSet(new Object[][]{ + {"PARAM_VALUE", "VARCHAR2", "Y", null, null, 100, 100, null, null, 0} + }) + )); + + List columns = agent.getColumns("", "TBPARAM"); + + Assertions.assertEquals(List.of("PARAM_VALUE"), columns.stream().map(ColumnInfo::getName).toList()); + Assertions.assertEquals(List.of("APP", "TBPARAM", "APP", "TBPARAM"), params); + Assertions.assertTrue(sql.get(0).contains("SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')"), sql.get(0)); + Assertions.assertTrue(sql.get(1).contains("FROM ALL_TAB_COLUMNS"), sql.get(1)); + } + @Test void tableDdlIncludesDefaultsAndOnlyNonBlankColumnComments() { OceanBaseOracleAgent agent = new OceanBaseOracleAgent(); @@ -496,6 +518,45 @@ class OceanBaseOracleAgentTest { }); } + private static Connection currentSchemaColumnConnection(List sql, List params, ResultSet currentSchema, ResultSet columns) { + Statement schemaStatement = proxy(Statement.class, (method, args) -> { + if ("executeQuery".equals(method.getName())) { + sql.add(String.valueOf(args[0])); + return currentSchema; + } + if ("close".equals(method.getName())) { + return null; + } + return defaultValue(method.getReturnType()); + }); + PreparedStatement columnStatement = proxy(PreparedStatement.class, (method, args) -> { + if ("executeQuery".equals(method.getName())) { + return columns; + } + if ("setString".equals(method.getName())) { + params.add(String.valueOf(args[1])); + return null; + } + if ("close".equals(method.getName())) { + return null; + } + return defaultValue(method.getReturnType()); + }); + return proxy(Connection.class, (method, args) -> { + if ("createStatement".equals(method.getName())) { + return schemaStatement; + } + if ("prepareStatement".equals(method.getName())) { + sql.add(String.valueOf(args[0])); + return columnStatement; + } + if ("isClosed".equals(method.getName())) { + return false; + } + return defaultValue(method.getReturnType()); + }); + } + private static Connection objectSourceConnection(List sql, List params, ResultSet resultSet) { PreparedStatement statement = objectSourceStatement(params, resultSet, false); return proxy(Connection.class, (method, args) -> { diff --git a/apps/desktop/src/lib/__tests__/sql/oracleCompletionSession.spec.ts b/apps/desktop/src/lib/__tests__/sql/oracleCompletionSession.spec.ts index bf8a82a5e5..73c6508799 100644 --- a/apps/desktop/src/lib/__tests__/sql/oracleCompletionSession.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/oracleCompletionSession.spec.ts @@ -8,8 +8,11 @@ const contentAreaSource = readFileSync(new URL("../../../components/layout/Conte describe("Oracle session-scoped completion", () => { it("uses the tab session only for unqualified references without a selected schema", () => { expect(usesOracleSessionCompletionColumns({ databaseType: "oracle", clientSessionId: "tab-a" })).toBe(true); + expect(usesOracleSessionCompletionColumns({ databaseType: "oceanbase-oracle", clientSessionId: "tab-a" })).toBe(true); expect(usesOracleSessionCompletionColumns({ databaseType: "oracle", clientSessionId: "tab-a", referenceSchema: "REPORTING" })).toBe(false); + expect(usesOracleSessionCompletionColumns({ databaseType: "oceanbase-oracle", clientSessionId: "tab-a", referenceSchema: "REPORTING" })).toBe(false); expect(usesOracleSessionCompletionColumns({ databaseType: "oracle", clientSessionId: "tab-a", selectedSchema: "REPORTING" })).toBe(false); + expect(usesOracleSessionCompletionColumns({ databaseType: "oceanbase-oracle", clientSessionId: "tab-a", selectedSchema: "REPORTING" })).toBe(false); expect(usesOracleSessionCompletionColumns({ databaseType: "postgres", clientSessionId: "tab-a" })).toBe(false); expect(usesOracleSessionCompletionColumns({ databaseType: "oracle" })).toBe(false); }); diff --git a/apps/desktop/src/lib/sql/oracleCompletionSession.ts b/apps/desktop/src/lib/sql/oracleCompletionSession.ts index 9477fbfb82..c4cd84284a 100644 --- a/apps/desktop/src/lib/sql/oracleCompletionSession.ts +++ b/apps/desktop/src/lib/sql/oracleCompletionSession.ts @@ -1,5 +1,9 @@ import type { DatabaseType } from "@/types/database"; +export function usesOracleCurrentSchemaCompletion(databaseType?: DatabaseType, schema?: string | null): boolean { + return (databaseType === "oracle" || databaseType === "oceanbase-oracle") && !schema; +} + export function usesOracleSessionCompletionColumns(options: { databaseType?: DatabaseType; selectedSchema?: string; referenceSchema?: string | null; clientSessionId?: string }): boolean { - return options.databaseType === "oracle" && !options.selectedSchema && !options.referenceSchema && !!options.clientSessionId; + return usesOracleCurrentSchemaCompletion(options.databaseType, options.referenceSchema || options.selectedSchema) && !!options.clientSessionId; } diff --git a/apps/desktop/src/stores/__tests__/connectionStore.completion.spec.ts b/apps/desktop/src/stores/__tests__/connectionStore.completion.spec.ts index d22549559a..4d93200934 100644 --- a/apps/desktop/src/stores/__tests__/connectionStore.completion.spec.ts +++ b/apps/desktop/src/stores/__tests__/connectionStore.completion.spec.ts @@ -1,5 +1,6 @@ import { createPinia, setActivePinia } from "pinia"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { getSqlCompletionContext } from "@/lib/sql/sqlCompletion"; import type { ConnectionConfig } from "@/types/database"; function installLocalStorage() { @@ -715,6 +716,40 @@ describe("connectionStore completion assistant", () => { expect(upper).toEqual(lower); }); + it("uses the OceanBase Oracle session schema for an unqualified aliased table", async () => { + const completionAssistantSearch = vi.fn(); + const getColumns = vi.fn().mockResolvedValue([{ name: "PARAM_VALUE", data_type: "VARCHAR2(100)", is_nullable: true, column_default: null, is_primary_key: false, extra: null, comment: null }]); + + vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false })); + vi.doMock("@/lib/backend/api", () => ({ + checkConnectionHealth: vi.fn().mockResolvedValue(undefined), + completionAssistantSearch, + getColumns, + })); + + const sql = "select a. from tbparam a"; + const completion = getSqlCompletionContext(sql, sql.indexOf("a.") + 2, { databaseType: "oceanbase-oracle" }); + const reference = completion.referencedTables[0]; + expect(reference).toEqual(expect.objectContaining({ name: "tbparam", alias: "a", schema: undefined, nameQuoted: false })); + + const { useConnectionStore } = await import("@/stores/connectionStore"); + const store = useConnectionStore(); + store.connections = [oceanBaseOracleConnection()]; + store.connectedIds.add("oceanbase-oracle-1"); + + const columns = await store.listCompletionColumns("oceanbase-oracle-1", "OBORCL", reference!.name, reference!.schema, { + clientSessionId: "tab-a", + version: 0, + tableQuoted: reference!.nameQuoted, + schemaQuoted: reference!.schemaQuoted, + }); + + expect(completionAssistantSearch).not.toHaveBeenCalled(); + expect(getColumns).toHaveBeenCalledWith("oceanbase-oracle-1", "OBORCL", "", "TBPARAM", undefined, "tab-a"); + expect(columns).toEqual([expect.objectContaining({ name: "PARAM_VALUE", table: "TBPARAM", schema: undefined, dataType: "VARCHAR2(100)" })]); + expect(store.lookupLocalCompletionColumns("oceanbase-oracle-1", "OBORCL", "TBPARAM")).toEqual([]); + }); + it("keeps quoted OceanBase Oracle identifiers exact and isolated from unquoted cache entries", async () => { const completionAssistantSearch = vi.fn(async (request: { parent_name?: string | null; parent_schema?: string | null }) => ({ candidates: [ diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 7f965cba7b..4cb7e5ae3a 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -74,6 +74,7 @@ import { type ConnectionExportProtection, } from "@/lib/connection/connectionConfigTransfer"; import type { SqlCompletionColumn, SqlCompletionForeignKey, SqlCompletionObject, SqlCompletionTable } from "@/lib/sql/sqlCompletion"; +import { usesOracleCurrentSchemaCompletion } from "@/lib/sql/oracleCompletionSession"; import { mergeSqlObjectNavigationType, sqlObjectNavigationTypeFromTableType } from "@/lib/sql/sqlNavigation"; import * as api from "@/lib/backend/api"; import { isTauriRuntime } from "@/lib/backend/tauriRuntime"; @@ -7807,11 +7808,11 @@ export const useConnectionStore = defineStore("connection", () => { const completionTable = uppercaseUnquotedIdentifier && context?.tableQuoted === false ? table.toUpperCase() : table; const rawCompletionSchema = schema?.trim() || (config?.db_type === "dameng" ? config.username?.trim() || undefined : undefined); const completionSchema = uppercaseUnquotedIdentifier && rawCompletionSchema && context?.schemaQuoted === false ? rawCompletionSchema.toUpperCase() : rawCompletionSchema; - const usesOracleCurrentSchema = config?.db_type === "oracle" && !completionSchema; - if (isSchemaAwareDatabase(connectionId) && !connectionUsesDatabaseObjectTreeMode(config) && !completionSchema && !usesOracleCurrentSchema) { + const usesCurrentSchema = usesOracleCurrentSchemaCompletion(config?.db_type, completionSchema); + if (isSchemaAwareDatabase(connectionId) && !connectionUsesDatabaseObjectTreeMode(config) && !completionSchema && !usesCurrentSchema) { return []; } - const sessionCacheScope = usesOracleCurrentSchema && context?.clientSessionId ? `:${context.clientSessionId}:${context.version ?? 0}` : ""; + const sessionCacheScope = usesCurrentSchema && context?.clientSessionId ? `:${context.clientSessionId}:${context.version ?? 0}` : ""; const cacheKey = `${completionColumnsKey(connectionId, database, completionTable, completionSchema, catalog, context)}${sessionCacheScope}`; if (!completionColumnsCache.value[cacheKey]) { const requestRevision = completionCacheRevision(connectionId, database); @@ -7820,7 +7821,7 @@ export const useConnectionStore = defineStore("connection", () => { async () => { await ensureConnected(connectionId); // Use assistant metadata opportunistically, then fall back to canonical metadata. - if (!usesOracleCurrentSchema && !catalog) { + if (!usesCurrentSchema && !catalog) { try { const assistantColumns = await listCompletionAssistantColumns(connectionId, database, completionTable, completionSchema, context, requestRevision); if (assistantColumns.length > 0) { @@ -7845,8 +7846,8 @@ export const useConnectionStore = defineStore("connection", () => { // Fall back to the existing metadata path below. } } - const querySchema = usesOracleCurrentSchema ? "" : metadataQuerySchema(connectionId, database, completionSchema); - const columns = await api.getColumns(connectionId, database, querySchema, completionTable, catalog, usesOracleCurrentSchema ? context?.clientSessionId : undefined); + const querySchema = usesCurrentSchema ? "" : metadataQuerySchema(connectionId, database, completionSchema); + const columns = await api.getColumns(connectionId, database, querySchema, completionTable, catalog, usesCurrentSchema ? context?.clientSessionId : undefined); if (requestRevision !== completionCacheRevision(connectionId, database)) return; completionColumnsCache.value[cacheKey] = columns; evictOldestCacheEntries(completionColumnsCache.value, COMPLETION_CACHE_MAX); @@ -7867,7 +7868,7 @@ export const useConnectionStore = defineStore("connection", () => { isNullable: column.is_nullable, comment: column.comment, })); - if (!usesOracleCurrentSchema) indexCompletionColumns(connectionId, database, completionTable, completionSchema, columns, catalog, context); + if (!usesCurrentSchema) indexCompletionColumns(connectionId, database, completionTable, completionSchema, columns, catalog, context); return columns; }