feat(oracle): improve multi-schema completion

This commit is contained in:
zipg authored and GitHub committed 2026-07-14 12:35:13 +08:00
1 parent daf541d6d2
commit 88d05a0a19
9 files changed
+934 -70

No files matched your search

@@ -1233,7 +1233,7 @@ async function findExactSemanticDiagnosticTable(table: SqlTableReference): Promi
const localExact = localMatches.find((item) => completionTablesMatch(item, table));
if (localExact) return localExact;
const remoteMatches = await connectionStore.listCompletionTables(props.connectionId, target.database, table.name, MAX_COMPLETION_TABLES, target.schema);
const remoteMatches = await connectionStore.listCompletionTables(props.connectionId, target.database, table.name, MAX_COMPLETION_TABLES, target.schema, false, props.schema);
cachedTables = mergeCompletionTables(cachedTables, remoteMatches);
return remoteMatches.find((item) => completionTablesMatch(item, table)) ?? null;
}
@@ -1362,12 +1362,12 @@ async function resolveSqlHoverTooltip(currentView: EditorViewType, pos: number)
if (cachedTables.length === 0) {
cachedTables = usesLocalOnlyCompletionMetadata()
? connectionStore.lookupLocalCompletionTables(props.connectionId, props.database, tableLookupName, MAX_COMPLETION_TABLES, props.schema)
: await connectionStore.listCompletionTables(props.connectionId, props.database, tableLookupName, MAX_COMPLETION_TABLES, props.schema);
: await connectionStore.listCompletionTables(props.connectionId, props.database, tableLookupName, MAX_COMPLETION_TABLES, props.schema, false, props.schema);
}
let table = matchTable(qualifiedTableLookup, cachedTables) ?? matchTable(tableLookupName, cachedTables) ?? matchTable(identifier, cachedTables) ?? matchTable(name, cachedTables);
if (!table && !usesLocalOnlyCompletionMetadata()) {
const hoverTables = await connectionStore.listCompletionTables(props.connectionId, props.database, tableLookupName, MAX_COMPLETION_TABLES, semanticTarget?.schema ?? props.schema);
const hoverTables = await connectionStore.listCompletionTables(props.connectionId, props.database, tableLookupName, MAX_COMPLETION_TABLES, semanticTarget?.schema ?? props.schema, false, props.schema);
cachedTables = [...cachedTables, ...hoverTables];
table = matchTable(qualifiedTableLookup, hoverTables) ?? matchTable(tableLookupName, hoverTables) ?? matchTable(identifier, hoverTables) ?? matchTable(name, hoverTables);
}
@@ -2019,6 +2019,7 @@ async function provideSqlCompletions(context: CompletionContext) {
snippets: settingsStore.editorSettings.snippets,
dialect: props.dialect,
databaseType: props.databaseType,
currentSchema: props.schema,
keywordCase: settingsStore.editorSettings.sqlFormatter.keywordCase,
autoAliasTables: settingsStore.editorSettings.autoAliasTables,
});
@@ -2038,6 +2039,7 @@ async function provideSqlCompletions(context: CompletionContext) {
snippets: settingsStore.editorSettings.snippets,
dialect: props.dialect,
databaseType: props.databaseType,
currentSchema: props.schema,
keywordCase: settingsStore.editorSettings.sqlFormatter.keywordCase,
autoAliasTables: settingsStore.editorSettings.autoAliasTables,
});
@@ -2150,12 +2152,11 @@ function buildLocalSqlCompletionResult(completionContext: ReturnType<typeof getS
completionContext,
knownDatabases: databaseNames,
});
const tables = shouldLoadTables ? connectionStore.lookupLocalCompletionTables(props.connectionId, tableLookupTarget.database, tableLookupTarget.filter, MAX_COMPLETION_TABLES, tableLookupTarget.schema) : cachedTables;
const globalOracleTableSearch = props.databaseType === "oracle" && completionContext.suggestTables && !completionContext.qualifier;
const tables = shouldLoadTables ? connectionStore.lookupLocalCompletionTables(props.connectionId, tableLookupTarget.database, tableLookupTarget.filter, MAX_COMPLETION_TABLES, globalOracleTableSearch ? undefined : tableLookupTarget.schema) : cachedTables;
const shouldLoadObjects = completionContext.suggestRoutines || completionContext.exclusiveRoutineSuggestions || (!!completionContext.qualifier && !completionContext.exclusiveColumnSuggestions);
const completionObjects = shouldLoadObjects
? connectionStore.lookupLocalCompletionObjects(props.connectionId, props.database, completionContext.qualifier || completionContext.prefix, MAX_COMPLETION_TABLES, completionContext.qualifier && !completionContext.exclusiveColumnSuggestions ? completionContext.qualifier : props.schema)
: cachedCompletionObjects;
const shouldLoadObjects = shouldLoadCompletionObjects(completionContext);
const completionObjects = shouldLoadObjects ? lookupLocalCompletionObjectsForContext(completionContext) : cachedCompletionObjects;
const schemaNames =
completionContext.suggestTables && !completionContext.qualifier && !completionContext.insertTable ? mergeCompletionQualifierNames(connectionStore.lookupLocalCompletionSchemas(props.connectionId, props.database, completionContext.prefix, MAX_COMPLETION_TABLES), databaseNames) : [];
@@ -2225,6 +2226,7 @@ function buildLocalSqlCompletionResult(completionContext: ReturnType<typeof getS
snippets: settingsStore.editorSettings.snippets,
dialect: props.dialect,
databaseType: props.databaseType,
currentSchema: props.schema,
keywordCase: settingsStore.editorSettings.sqlFormatter.keywordCase,
autoAliasTables: settingsStore.editorSettings.autoAliasTables,
});
@@ -2247,8 +2249,9 @@ function scheduleCompletionMetadataRefresh(completionContext: ReturnType<typeof
knownDatabases: localCompletionDatabaseNames(completionContext),
});
if (!localOnlyMetadata && (completionContext.suggestTables || (!!completionContext.qualifier && !isReferencedTableQualifier(completionContext)))) {
const globalOracleTableSearch = props.databaseType === "oracle" && completionContext.suggestTables && !completionContext.qualifier;
void connectionStore
.refreshCompletionTables(connectionId, tableLookupTarget.database, tableLookupTarget.filter, MAX_COMPLETION_TABLES, tableLookupTarget.schema)
.refreshCompletionTables(connectionId, tableLookupTarget.database, tableLookupTarget.filter, MAX_COMPLETION_TABLES, tableLookupTarget.schema, globalOracleTableSearch, props.schema)
.then((tables) => {
cachedTables = mergeCompletionTables(cachedTables, tables);
if (completionContext.suggestTables && completionContext.referencedTables.length > 0) {
@@ -2257,9 +2260,8 @@ function scheduleCompletionMetadataRefresh(completionContext: ReturnType<typeof
})
.catch(() => {});
}
if (!localOnlyMetadata && (completionContext.suggestRoutines || completionContext.exclusiveRoutineSuggestions || (!!completionContext.qualifier && !completionContext.exclusiveColumnSuggestions))) {
void connectionStore
.refreshCompletionObjects(connectionId, database, completionContext.prefix, MAX_COMPLETION_TABLES, props.schema)
if (!localOnlyMetadata && shouldLoadCompletionObjects(completionContext)) {
void listCompletionObjectsForContext(completionContext)
.then((objects) => {
cachedCompletionObjects = mergeCompletionObjects(cachedCompletionObjects, objects);
})
@@ -2315,14 +2317,18 @@ function scheduleCompletionMetadataRefresh(completionContext: ReturnType<typeof
}
}
function mergeCompletionTables(existing: Array<{ name: string; schema?: string; type?: "table" | "view" }>, incoming: Array<{ name: string; schema?: string; type?: "table" | "view" }>) {
function mergeCompletionTables(existing: SqlCompletionTable[], incoming: SqlCompletionTable[]): SqlCompletionTable[] {
const merged = [...existing];
const seen = new Set(existing.map((table) => `${table.schema ?? ""}.${table.name}`.toLowerCase()));
const indexes = new Map(existing.map((table, index) => [`${table.schema ?? ""}.${table.name}`.toLowerCase(), index]));
for (const table of incoming) {
const key = `${table.schema ?? ""}.${table.name}`.toLowerCase();
if (seen.has(key)) continue;
seen.add(key);
merged.push(table);
const index = indexes.get(key);
if (index == null) {
indexes.set(key, merged.length);
merged.push(table);
} else {
merged[index] = { ...merged[index], ...table };
}
}
return merged;
}
@@ -2331,9 +2337,9 @@ function withCompletionLatencyBudget<T>(remote: Promise<T>, local: T): Promise<T
return Promise.race([remote, new Promise<T>((resolve) => setTimeout(() => resolve(local), COMPLETION_REMOTE_LATENCY_BUDGET_MS))]);
}
function listCompletionTablesWithLatencyBudget(connectionId: string, database: string, filter: string, limit: number, schema?: string): Promise<SqlCompletionTable[]> {
const local = connectionStore.lookupLocalCompletionTables(connectionId, database, filter, limit, schema);
const remote = connectionStore.listCompletionTables(connectionId, database, filter, limit, schema).then((tables) => {
function listCompletionTablesWithLatencyBudget(connectionId: string, database: string, filter: string, limit: number, schema?: string, globalSearch = false): Promise<SqlCompletionTable[]> {
const local = connectionStore.lookupLocalCompletionTables(connectionId, database, filter, limit, globalSearch ? undefined : schema);
const remote = connectionStore.listCompletionTables(connectionId, database, filter, limit, schema, globalSearch, props.schema).then((tables) => {
cachedTables = mergeCompletionTables(cachedTables, tables);
return tables;
});
@@ -2341,6 +2347,43 @@ function listCompletionTablesWithLatencyBudget(connectionId: string, database: s
return withCompletionLatencyBudget(remote, local);
}
interface RoutineCompletionTarget {
schema?: string;
parentName?: string;
globalSearch?: boolean;
}
function shouldLoadCompletionObjects(completionContext: ReturnType<typeof getSqlCompletionContext>): boolean {
const routineContext = completionContext.suggestRoutines || completionContext.exclusiveRoutineSuggestions || (!!completionContext.qualifier && !completionContext.exclusiveColumnSuggestions);
return routineContext && !isReferencedTableQualifier(completionContext);
}
function oracleRoutineCompletionTargets(completionContext: ReturnType<typeof getSqlCompletionContext>): RoutineCompletionTarget[] {
const parts = (completionContext.qualifierParts?.length ? completionContext.qualifierParts : completionContext.qualifier?.split("."))?.filter(Boolean) ?? [];
if (parts.length === 0) return [{ schema: props.schema, globalSearch: true }];
if (parts.length === 1) {
return [{ schema: props.schema, parentName: parts[0] }, { schema: parts[0] }];
}
return [{ schema: parts[parts.length - 2], parentName: parts[parts.length - 1] }];
}
function lookupLocalCompletionObjectsForContext(completionContext: ReturnType<typeof getSqlCompletionContext>): SqlCompletionObject[] {
if (!props.connectionId || props.database == null) return [];
if (props.databaseType === "oracle") {
return connectionStore.lookupLocalCompletionObjects(props.connectionId, props.database, completionContext.prefix, MAX_COMPLETION_TABLES);
}
return connectionStore.lookupLocalCompletionObjects(props.connectionId, props.database, completionContext.qualifier || completionContext.prefix, MAX_COMPLETION_TABLES, completionContext.qualifier && !completionContext.exclusiveColumnSuggestions ? completionContext.qualifier : props.schema);
}
async function listCompletionObjectsForContext(completionContext: ReturnType<typeof getSqlCompletionContext>): Promise<SqlCompletionObject[]> {
if (!props.connectionId || props.database == null) return [];
if (props.databaseType !== "oracle") {
return connectionStore.listCompletionObjects(props.connectionId, props.database, completionContext.qualifier || completionContext.prefix, MAX_COMPLETION_TABLES, props.schema);
}
const groups = await Promise.all(oracleRoutineCompletionTargets(completionContext).map((target) => connectionStore.listCompletionObjects(props.connectionId!, props.database!, completionContext.prefix, MAX_COMPLETION_TABLES, target.schema, target.parentName, target.globalSearch, props.schema)));
return groups.reduce((objects, group) => mergeCompletionObjects(objects, group), [] as SqlCompletionObject[]);
}
async function performAsyncCompletionWithResult(epoch: number, completionContext: ReturnType<typeof getSqlCompletionContext>, fullDoc: string, position: number) {
const localOnlyMetadata = usesLocalOnlyCompletionMetadata();
const onDemandOnlyColumns = usesOnDemandOnlyCompletionColumns();
@@ -2377,22 +2420,19 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
completionContext,
knownDatabases: databaseNames,
});
const globalOracleTableSearch = props.databaseType === "oracle" && completionContext.suggestTables && !completionContext.qualifier;
let tables = shouldLoadTables
? localOnlyMetadata
? connectionStore.lookupLocalCompletionTables(props.connectionId!, tableLookupTarget.database, tableLookupTarget.filter, MAX_COMPLETION_TABLES, tableLookupTarget.schema)
: await listCompletionTablesWithLatencyBudget(props.connectionId!, tableLookupTarget.database, tableLookupTarget.filter, MAX_COMPLETION_TABLES, tableLookupTarget.schema)
? connectionStore.lookupLocalCompletionTables(props.connectionId!, tableLookupTarget.database, tableLookupTarget.filter, MAX_COMPLETION_TABLES, globalOracleTableSearch ? undefined : tableLookupTarget.schema)
: await listCompletionTablesWithLatencyBudget(props.connectionId!, tableLookupTarget.database, tableLookupTarget.filter, MAX_COMPLETION_TABLES, tableLookupTarget.schema, globalOracleTableSearch)
: cachedTables;
if (epoch !== completionEpoch) return null;
const shouldLoadObjects = completionContext.suggestRoutines || completionContext.exclusiveRoutineSuggestions || (!!completionContext.qualifier && !completionContext.exclusiveColumnSuggestions);
let completionObjects = shouldLoadObjects
? localOnlyMetadata
? connectionStore.lookupLocalCompletionObjects(props.connectionId!, props.database!, completionContext.qualifier || completionContext.prefix, MAX_COMPLETION_TABLES, props.schema)
: await connectionStore.listCompletionObjects(props.connectionId!, props.database!, completionContext.qualifier || completionContext.prefix, MAX_COMPLETION_TABLES, props.schema)
: cachedCompletionObjects;
const shouldLoadObjects = shouldLoadCompletionObjects(completionContext);
let completionObjects = shouldLoadObjects ? (localOnlyMetadata ? lookupLocalCompletionObjectsForContext(completionContext) : await listCompletionObjectsForContext(completionContext)) : cachedCompletionObjects;
if (epoch !== completionEpoch) return null;
if (!localOnlyMetadata && completionContext.qualifier && completionObjects.length === 0) {
if (props.databaseType !== "oracle" && !localOnlyMetadata && completionContext.qualifier && completionObjects.length === 0) {
const schemaObjects = await connectionStore.listCompletionObjects(props.connectionId!, props.database!, completionContext.prefix, MAX_COMPLETION_TABLES, completionContext.qualifier);
if (schemaObjects.length > 0) {
completionObjects = schemaObjects;
@@ -2445,7 +2485,7 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
});
const unresolvedRefs = refs.filter((rt) => !rt.schema && !rt.columns);
if (!localOnlyMetadata && unresolvedRefs.length > 0) {
const lookupGroups = await Promise.all(unresolvedRefs.map((rt) => connectionStore.listCompletionTables(props.connectionId!, props.database!, rt.name, 20, props.schema)));
const lookupGroups = await Promise.all(unresolvedRefs.map((rt) => connectionStore.listCompletionTables(props.connectionId!, props.database!, rt.name, 20, props.schema, false, props.schema)));
if (epoch !== completionEpoch) return null;
const lookupTables = lookupGroups.flat();
refs = refs.map((rt) => {
@@ -2564,6 +2604,7 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
snippets: settingsStore.editorSettings.snippets,
dialect: props.dialect,
databaseType: props.databaseType,
currentSchema: props.schema,
keywordCase: settingsStore.editorSettings.sqlFormatter.keywordCase,
autoAliasTables: settingsStore.editorSettings.autoAliasTables,
});
@@ -2584,12 +2625,16 @@ function isTableNameCompletionContext(completionContext: ReturnType<typeof getSq
function mergeCompletionObjects(existing: SqlCompletionObject[], incoming: SqlCompletionObject[]) {
const merged = [...existing];
const seen = new Set(existing.map((object) => `${object.type}:${object.schema ?? ""}:${object.name}:${object.parentName ?? ""}`.toLowerCase()));
const indexes = new Map(existing.map((object, index) => [`${object.type}:${object.schema ?? ""}:${object.name}:${object.parentName ?? ""}`.toLowerCase(), index]));
for (const object of incoming) {
const key = `${object.type}:${object.schema ?? ""}:${object.name}:${object.parentName ?? ""}`.toLowerCase();
if (seen.has(key)) continue;
seen.add(key);
merged.push(object);
const index = indexes.get(key);
if (index == null) {
indexes.set(key, merged.length);
merged.push(object);
} else {
merged[index] = { ...merged[index], ...object };
}
}
return merged;
}
@@ -3098,7 +3143,7 @@ onMounted(async () => {
// Some metadata providers only accept table-name masks, not schema.table masks.
cachedTables = usesLocalOnlyCompletionMetadata()
? connectionStore.lookupLocalCompletionTables(props.connectionId!, props.database!, tableLookupFilter, MAX_COMPLETION_TABLES, props.schema)
: await connectionStore.listCompletionTables(props.connectionId!, props.database!, tableLookupFilter, MAX_COMPLETION_TABLES, props.schema);
: await connectionStore.listCompletionTables(props.connectionId!, props.database!, tableLookupFilter, MAX_COMPLETION_TABLES, props.schema, false, props.schema);
}
// 1. Check if it's a table name