fix(oceanbase-oracle): complete tables across schemas
This commit is contained in:
1 parent
53184faf87
commit
a73bc4df69
10 files changed
+574
-22
No files matched your search
@@ -42,6 +42,7 @@ import { completionLabelPresentation } from "@/lib/editor/sqlCompletionPresentat
|
||||
import { supportsDatabaseNameCompletion } from "@/lib/database/databaseFeatureSupport";
|
||||
import { sqlSnippetDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { oracleDatabaseLinkCompletionContext, oracleDatabaseLinkCompletionItems } from "@/lib/sql/oracleDatabaseLinkCompletion";
|
||||
import { isOracleCompletionDatabase } from "@/lib/sql/oracleCompletionSession";
|
||||
import { buildRedisCompletionItemsFromContext, getRedisCompletionContext, getRedisCompletionResultValidFor, shouldAutoOpenRedisCompletion, takesKeyArgument } from "@/lib/redis/redisCompletion";
|
||||
import type { SqlCompletionColumn, SqlCompletionContext, SqlCompletionForeignKey, SqlCompletionItem, SqlCompletionObject } from "@/lib/sql/sqlCompletion";
|
||||
import type { CompletionAssistantObjectKind, SqlServerCompletionContext } from "@/types/database";
|
||||
@@ -995,7 +996,7 @@ export function useQueryEditorCompletion(options: QueryEditorCompletionOptions)
|
||||
completionContext,
|
||||
knownDatabases: databaseNames,
|
||||
});
|
||||
const globalOracleTableSearch = props.databaseType === "oracle" && completionContext.suggestTables && !completionContext.qualifier;
|
||||
const globalOracleTableSearch = isOracleCompletionDatabase(props.databaseType) && completionContext.suggestTables && !completionContext.qualifier;
|
||||
const tables = schemaLookupDatabase
|
||||
? []
|
||||
: shouldLoadTables
|
||||
@@ -1157,7 +1158,7 @@ export function useQueryEditorCompletion(options: QueryEditorCompletionOptions)
|
||||
knownDatabases: databaseNames,
|
||||
});
|
||||
if (!localOnlyMetadata && !schemaLookupDatabase && (completionContext.suggestTables || (!!completionContext.qualifier && !isReferencedTableQualifier(completionContext)))) {
|
||||
const globalOracleTableSearch = props.databaseType === "oracle" && completionContext.suggestTables && !completionContext.qualifier;
|
||||
const globalOracleTableSearch = isOracleCompletionDatabase(props.databaseType) && completionContext.suggestTables && !completionContext.qualifier;
|
||||
const refreshEpoch = completionEpoch;
|
||||
queueTableCompletionRefresh(async () => {
|
||||
if (refreshEpoch !== completionEpoch) return;
|
||||
@@ -1270,7 +1271,7 @@ export function useQueryEditorCompletion(options: QueryEditorCompletionOptions)
|
||||
if (parts.length === 0) {
|
||||
// Oracle resolves unqualified routines across all schemas (owner semantics).
|
||||
// openGauss keeps the PG search_path scope for the unqualified case.
|
||||
if (props.databaseType === "oracle") return [{ schema: props.schema, globalSearch: true }];
|
||||
if (isOracleCompletionDatabase(props.databaseType)) return [{ schema: props.schema, globalSearch: true }];
|
||||
return [{ schema: props.schema }];
|
||||
}
|
||||
if (parts.length === 1) {
|
||||
@@ -1366,7 +1367,7 @@ export function useQueryEditorCompletion(options: QueryEditorCompletionOptions)
|
||||
completionContext,
|
||||
knownDatabases: databaseNames,
|
||||
});
|
||||
const globalOracleTableSearch = props.databaseType === "oracle" && completionContext.suggestTables && !completionContext.qualifier;
|
||||
const globalOracleTableSearch = isOracleCompletionDatabase(props.databaseType) && completionContext.suggestTables && !completionContext.qualifier;
|
||||
let tables = schemaLookupDatabase
|
||||
? []
|
||||
: shouldLoadTables
|
||||
@@ -1385,7 +1386,7 @@ export function useQueryEditorCompletion(options: QueryEditorCompletionOptions)
|
||||
let completionObjects = shouldLoadObjects ? (localOnlyMetadata ? lookupLocalCompletionObjectsForContext(completionContext, scope) : await listCompletionObjectsForContext(completionContext, scope)) : scopedCachedCompletionObjects;
|
||||
if (epoch !== completionEpoch) return null;
|
||||
|
||||
if (!props.catalog && props.databaseType !== "oracle" && !localOnlyMetadata && completionContext.qualifier && completionObjects.length === 0) {
|
||||
if (!props.catalog && !isOracleCompletionDatabase(props.databaseType) && !localOnlyMetadata && completionContext.qualifier && completionObjects.length === 0) {
|
||||
const target = routineCompletionTargetForContext(completionContext, scope);
|
||||
const schemaObjects = await connectionStore.listCompletionObjects(props.connectionId!, target.database, target.mask, MAX_COMPLETION_TABLES, target.schema, undefined, false, scope.schema);
|
||||
if (schemaObjects.length > 0) {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
|
||||
/** Native Oracle and OceanBase Oracle share multi-schema completion semantics. */
|
||||
export function isOracleCompletionDatabase(databaseType?: DatabaseType): boolean {
|
||||
return databaseType === "oracle" || databaseType === "oceanbase-oracle";
|
||||
}
|
||||
|
||||
export function usesOracleCurrentSchemaCompletion(databaseType?: DatabaseType, schema?: string | null): boolean {
|
||||
return (databaseType === "oracle" || databaseType === "oceanbase-oracle") && !schema;
|
||||
return isOracleCompletionDatabase(databaseType) && !schema;
|
||||
}
|
||||
|
||||
export function usesOracleSessionCompletionColumns(options: { databaseType?: DatabaseType; selectedSchema?: string; referenceSchema?: string | null; clientSessionId?: string }): boolean {
|
||||
|
||||
@@ -20,6 +20,7 @@ import { quoteTableIdentifier, quoteTableIdentifierIfNeeded } from "@/lib/table/
|
||||
import { driverProfileCompletionObjects, driverProfileCompletionTableMetadata, driverProfileCompletionTables, driverProfileRoutineSignatures } from "@/lib/database/driverProfileExtensions";
|
||||
import { DORIS_FUNCTION_DOCS, DORIS_FUNCTION_SIGNATURES } from "@/lib/sql/doris/functions";
|
||||
import { rejectsAliasReferenceInHaving } from "@/lib/database/databaseFeatureSupport";
|
||||
import { isOracleCompletionDatabase } from "@/lib/sql/oracleCompletionSession";
|
||||
|
||||
export { DEFAULT_SQL_SNIPPETS, resolveSqlSnippetBodyForDatabase } from "@/lib/sql/sqlSnippetTemplates";
|
||||
|
||||
@@ -3907,10 +3908,10 @@ function resolveTableSchemaQualification(
|
||||
currentSchema: string | undefined,
|
||||
schemasByTableName: Map<string, Set<string>>,
|
||||
): { ambiguousTableName: boolean; schemaQualification: boolean; defaultApplyName: string } {
|
||||
const oracleSchemaQualification = databaseType === "oracle" && table.schema && table.schema.toUpperCase() !== "PUBLIC" && (!currentSchema || normalizeIdentifierPart(table.schema) !== normalizeIdentifierPart(currentSchema));
|
||||
const oracleSchemaQualification = isOracleCompletionDatabase(databaseType) && table.schema && table.schema.toUpperCase() !== "PUBLIC" && (!currentSchema || normalizeIdentifierPart(table.schema) !== normalizeIdentifierPart(currentSchema));
|
||||
// A bare table name is ambiguous when metadata contains the same name in multiple schemas.
|
||||
// Keep Oracle's current-schema behavior, but qualify the generic/PostgreSQL/SQL Server paths.
|
||||
const ambiguousTableName = databaseType !== "oracle" && (schemasByTableName.get(normalizeIdentifierPart(table.name))?.size ?? 0) > 1;
|
||||
const ambiguousTableName = !isOracleCompletionDatabase(databaseType) && (schemasByTableName.get(normalizeIdentifierPart(table.name))?.size ?? 0) > 1;
|
||||
const schemaQualification = !!table.schema && (oracleSchemaQualification || ambiguousTableName);
|
||||
const defaultApplyName = schemaQualification ? `${quoteCompletionApplyIdentifier(table.schema!, dialect)}.${quoteCompletionApplyIdentifier(table.name, dialect)}` : quoteCompletionApplyIdentifier(table.name, dialect);
|
||||
return { ambiguousTableName, schemaQualification, defaultApplyName };
|
||||
@@ -3948,7 +3949,7 @@ function buildTableItems(
|
||||
detail,
|
||||
apply: formatTableAliasApply(applyName, alias),
|
||||
boost: computeBoost(table.name, prefix) + 1000 + (table.boost ?? 0),
|
||||
dedupeKey: table.applyName || ambiguousTableName || (databaseType === "oracle" && table.schema) ? applyName : undefined,
|
||||
dedupeKey: table.applyName || ambiguousTableName || (isOracleCompletionDatabase(databaseType) && table.schema) ? applyName : undefined,
|
||||
};
|
||||
})
|
||||
.sort(compareCompletionItems)
|
||||
@@ -4004,7 +4005,7 @@ function buildForeignKeyRelatedTableItems(
|
||||
boost: computeBoost(table.name, context.prefix) + 3600,
|
||||
// Mirror buildTableItems' dedupeKey so an FK candidate and the regular
|
||||
// candidate for the same schema-qualified table collapse to one entry.
|
||||
dedupeKey: ambiguousTableName || (databaseType === "oracle" && table.schema) ? applyName : undefined,
|
||||
dedupeKey: ambiguousTableName || (isOracleCompletionDatabase(databaseType) && table.schema) ? applyName : undefined,
|
||||
};
|
||||
})
|
||||
.sort(compareCompletionItems);
|
||||
@@ -4044,7 +4045,7 @@ function buildObjectItems(context: SqlCompletionContext, objects: SqlCompletionO
|
||||
if (completionQualifierIsReferencedTable(context)) return [];
|
||||
const onlyProcedures = context.contextKind === "exec";
|
||||
const onlyFunctions = context.suggestColumns && context.referencedTables.length > 0 && !context.qualifier;
|
||||
const prioritizeOracleFunctions = databaseType === "oracle" && context.statementKind === "select";
|
||||
const prioritizeOracleFunctions = isOracleCompletionDatabase(databaseType) && context.statementKind === "select";
|
||||
return objects
|
||||
.filter((object) => object.type !== "sequence" && (!onlyProcedures || object.type === "procedure") && (!onlyFunctions || (object.type === "function" && object.name.toLowerCase().startsWith(context.prefix.toLowerCase()))) && objectMatchesCompletionContext(object, context))
|
||||
.map((object) => {
|
||||
@@ -4060,7 +4061,7 @@ function buildObjectItems(context: SqlCompletionContext, objects: SqlCompletionO
|
||||
const detail = [locationDetail, signature ? `(${signature})` : undefined, object.dataType ? `[${object.dataType}]` : undefined].filter(Boolean).join(" ");
|
||||
const schemaBoost = onlyFunctions ? Math.min(object.boost ?? 0, 1000) : (object.boost ?? 0);
|
||||
const typeBoost = routineTypeBoost(object.type, prioritizeOracleFunctions && !onlyFunctions);
|
||||
const baseDedupeKey = object.applyName || (databaseType === "oracle" && object.schema) ? applyName : undefined;
|
||||
const baseDedupeKey = object.applyName || (isOracleCompletionDatabase(databaseType) && object.schema) ? applyName : undefined;
|
||||
return {
|
||||
label: object.name,
|
||||
type: "function" as const,
|
||||
@@ -5470,7 +5471,7 @@ function activeSqlKeywords(databaseType?: DatabaseType): string[] {
|
||||
}
|
||||
|
||||
function isOracleLikeDatabase(databaseType?: DatabaseType): boolean {
|
||||
return databaseType === "oracle" || databaseType === "oceanbase-oracle";
|
||||
return isOracleCompletionDatabase(databaseType);
|
||||
}
|
||||
|
||||
// CQL has no table alias syntax, so Cassandra must never receive `table alias`
|
||||
|
||||
@@ -678,6 +678,59 @@ describe("connectionStore completion assistant", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("maps global OceanBase Oracle tables with safe qualification and schema priority", async () => {
|
||||
const completionAssistantSearch = vi.fn().mockResolvedValue({
|
||||
candidates: [
|
||||
{ name: "ORDERS", kind: "table", schema: "DWD", data_type: "TABLE" },
|
||||
{ name: "ORDERS", kind: "table", schema: "STAGING", data_type: "TABLE" },
|
||||
],
|
||||
incomplete: false,
|
||||
fallback_used: false,
|
||||
});
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
completionAssistantSearch,
|
||||
listTables: vi.fn().mockResolvedValue([]),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
store.connections = [oceanBaseOracleConnection()];
|
||||
store.connectedIds.add("oceanbase-oracle-1");
|
||||
|
||||
const tables = await store.listCompletionTables("oceanbase-oracle-1", "OBORCL", "ORD", 20, "DWD", true, "DWD");
|
||||
|
||||
expect(completionAssistantSearch).toHaveBeenCalledWith(expect.objectContaining({ schema: "DWD", parent_schema: null, global_search: true, mask: "ORD" }));
|
||||
expect(tables).toEqual([expect.objectContaining({ name: "ORDERS", schema: "DWD", applyName: "ORDERS", boost: 2400 }), expect.objectContaining({ name: "ORDERS", schema: "STAGING", applyName: "STAGING.ORDERS", boost: 0 })]);
|
||||
});
|
||||
|
||||
it("scopes OceanBase Oracle table completion when a schema qualifier is present", async () => {
|
||||
const completionAssistantSearch = vi.fn(async (request: { schema?: string | null; parent_schema?: string | null }) => ({
|
||||
candidates: request.parent_schema?.toLowerCase() === "staging" ? [{ name: "ORDERS", kind: "table", schema: "STAGING", data_type: "TABLE" }] : [],
|
||||
incomplete: false,
|
||||
fallback_used: false,
|
||||
}));
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
completionAssistantSearch,
|
||||
listTables: vi.fn().mockResolvedValue([]),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
store.connections = [oceanBaseOracleConnection()];
|
||||
store.connectedIds.add("oceanbase-oracle-1");
|
||||
|
||||
const tables = await store.listCompletionTables("oceanbase-oracle-1", "OBORCL", "", 20, "staging", false, "DWD");
|
||||
|
||||
expect(completionAssistantSearch).toHaveBeenCalledWith(expect.objectContaining({ schema: "staging", parent_schema: "staging", global_search: false, mask: "" }));
|
||||
expect(tables).toEqual([expect.objectContaining({ name: "ORDERS", schema: "STAGING", applyName: "ORDERS", boost: 2400 })]);
|
||||
});
|
||||
|
||||
it("lets Oracle resolve CURRENT_SCHEMA for unqualified column completion", async () => {
|
||||
const completionAssistantSearch = vi.fn().mockResolvedValue({
|
||||
candidates: [],
|
||||
|
||||
@@ -104,7 +104,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 { usesOracleCurrentSchemaCompletion, isOracleCompletionDatabase } from "@/lib/sql/oracleCompletionSession";
|
||||
import { mergeSqlObjectNavigationType, sqlObjectNavigationTypeFromTableType } from "@/lib/sql/sqlNavigation";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { oracleDatabaseLinksFromResult, oracleDatabaseLinksSql, supportsOracleDatabaseLinks } from "@/lib/database/oracleDatabaseLinks";
|
||||
@@ -7997,7 +7997,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
requestRevision = completionCacheRevision(connectionId, database),
|
||||
matchMode: CompletionAssistantMatchMode = "prefix",
|
||||
): Promise<SqlCompletionTable[]> {
|
||||
const oracleAssistant = getConfig(connectionId)?.db_type === "oracle";
|
||||
const oracleAssistant = isOracleCompletionDatabase(getConfig(connectionId)?.db_type);
|
||||
const preferredSchema = oracleAssistant ? completionPreferredSchema(connectionId, globalSearch ? currentSchema : (schema ?? currentSchema)) : schema?.trim() || undefined;
|
||||
const objectKinds: CompletionAssistantObjectKind[] = ["table", "view"];
|
||||
const response = await completionAssistantSearch(
|
||||
@@ -8033,7 +8033,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
matchMode: CompletionAssistantMatchMode = "prefix",
|
||||
): Promise<SqlCompletionObject[]> {
|
||||
const databaseType = getConfig(connectionId)?.db_type;
|
||||
const oracleAssistant = databaseType === "oracle";
|
||||
const oracleAssistant = isOracleCompletionDatabase(databaseType);
|
||||
const requestedSchema = schema?.trim() || currentSchema?.trim() || undefined;
|
||||
const sequenceOnly = objectKinds.length === 1 && objectKinds[0] === "sequence";
|
||||
const preferredSchema = oracleAssistant ? completionPreferredSchema(connectionId, currentSchema) : requestedSchema || (!sequenceOnly && databaseType === "postgres" ? "public" : databaseType === "mysql" ? database : undefined);
|
||||
|
||||
Reference in new issue
Block a user