feat(editor): support SQL Server cross-database completion

This commit is contained in:
zipg authored and GitHub committed 2026-07-22 15:08:44 +08:00
1 parent dfab2d09ed
commit 63dd0e88b5
19 files changed
+438 -75

No files matched your search

@@ -71,7 +71,7 @@ import { sqlSemanticTableNameSpansForSyntaxTree } from "@/lib/editor/codemirrorS
import { startsQueryEditorRectangularSelection } from "@/lib/editor/queryEditorPointerSelection";
import { LARGE_PASTE_HISTORY_USER_EVENT, normalizeQueryEditorPasteText, recoverableNativePasteSuffix, shouldRecoverLargeTauriPaste } from "@/lib/editor/queryEditorLargePaste";
import type { StatementExecutionMarker } from "@/lib/tabs/tabPresentation";
import { isSchemaAware, isSingleDatabase, supportsSqlInListPaste } from "@/lib/database/databaseFeatureSupport";
import { isSchemaAware, isSingleDatabase, supportsDatabaseSchemaQualifier, supportsSqlInListPaste } from "@/lib/database/databaseFeatureSupport";
import { metadataSchemaForConnection } from "@/lib/database/jdbcDialect";
import { usesLocalOnlyEditorCompletionMetadata, usesOnDemandOnlyEditorColumnMetadata } from "@/lib/metadata/completionMetadataPolicy";
import { queryContextObjectActions, queryContextObjectRoute, queryTableCandidateAtSqlPosition, resolveQueryContextCandidateDatabase, resolveQueryContextObjectTarget, type QueryContextObjectAction } from "@/lib/sql/queryCursorTableTarget";
@@ -1444,9 +1444,10 @@ function identifierRangeAt(sql: string, pos: number): { from: number; to: number
return { from, to, text };
}
function completionCacheKey(table: { name: string; schema?: string | null }) {
function completionCacheKey(table: { name: string; database?: string | null; schema?: string | null }) {
const schema = table.schema ?? props.schema;
return schema ? `${schema}.${table.name}` : table.name;
const database = supportsDatabaseSchemaQualifierCompletion() ? table.database : undefined;
return schema ? `${database ? `${database}.` : ""}${schema}.${table.name}` : table.name;
}
const pendingInsertValueHintColumnLoads = new Set<string>();
@@ -1510,6 +1511,10 @@ function supportsDatabaseQualifierCompletion(): boolean {
return !!props.databaseType && !isSchemaAware(props.databaseType) && !isSingleDatabase(props.databaseType);
}
function supportsDatabaseSchemaQualifierCompletion(): boolean {
return supportsDatabaseSchemaQualifier(props.databaseType);
}
function usesLocalOnlyCompletionMetadata(): boolean {
return usesLocalOnlyEditorCompletionMetadata(props.databaseType);
}
@@ -1524,30 +1529,35 @@ function allowsOnDemandQualifiedTableCompletion(prefix: string): boolean {
return prefix.trim().length >= PRESTO_ON_DEMAND_TABLE_COMPLETION_MIN_PREFIX;
}
function completionMetadataTarget(table: { name: string; schema?: string | null }): { database: string; schema?: string } | null {
function completionMetadataTarget(table: { name: string; database?: string | null; schema?: string | null }): { database: string; schema?: string } | null {
if (props.database == null) return null;
if (supportsDatabaseSchemaQualifierCompletion() && table.database) {
return { database: table.database, schema: table.schema ?? undefined };
}
if (supportsDatabaseQualifierCompletion() && table.schema) {
return { database: table.schema };
}
return { database: props.database, schema: table.schema ?? props.schema };
}
function isVirtualCompletionTableReference(table: { name: string; schema?: string | null }): boolean {
function isVirtualCompletionTableReference(table: { name: string; database?: string | null; schema?: string | null }): boolean {
return isSqlVirtualTableReference(table, props.databaseType);
}
function completionQualifiedTableTarget(completionContext: ReturnType<typeof getSqlCompletionContext>): { name: string; schema: string } | null {
function completionQualifiedTableTarget(completionContext: ReturnType<typeof getSqlCompletionContext>): { name: string; database?: string; schema: string } | null {
if (!completionContext.suggestColumns) return null;
const parts = completionContext.qualifierParts ?? completionContext.qualifier?.split(".").filter(Boolean) ?? [];
if (parts.length < 2) return null;
const name = parts[parts.length - 1];
const schema = parts[parts.length - 2];
if (!name || !schema) return null;
return { name, schema };
const database = supportsDatabaseSchemaQualifierCompletion() && parts.length >= 3 ? parts[parts.length - 3] : undefined;
return { name, database, schema };
}
function completionTablesMatch(left: { name: string; schema?: string | null }, right: { name: string; schema?: string | null }) {
function completionTablesMatch(left: { name: string; database?: string | null; schema?: string | null }, right: { name: string; database?: string | null; schema?: string | null }) {
if (left.name.toLowerCase() !== right.name.toLowerCase()) return false;
if (left.database && right.database && left.database.toLowerCase() !== right.database.toLowerCase()) return false;
if (!left.schema || !right.schema) return true;
return left.schema.toLowerCase() === right.schema.toLowerCase();
}
@@ -1565,7 +1575,7 @@ async function findExactSemanticDiagnosticTable(table: SqlTableReference): Promi
return remoteMatches.find((item) => completionTablesMatch(item, table)) ?? null;
}
async function ensureColumnsForTable(table: { name: string; schema?: string | null }): Promise<boolean> {
async function ensureColumnsForTable(table: { name: string; database?: string | null; schema?: string | null }): Promise<boolean> {
if (isVirtualCompletionTableReference(table)) return false;
const cacheKey = completionCacheKey(table);
if (cachedColumnsByTable.has(cacheKey)) return true;
@@ -1583,7 +1593,7 @@ function isMissingTableMetadataError(error: unknown) {
return message.includes("42s02") || message.includes("1146") || message.includes("doesn't exist") || message.includes("does not exist") || message.includes("unknown table");
}
async function ensureForeignKeysForTable(table: { name: string; schema?: string | null }) {
async function ensureForeignKeysForTable(table: { name: string; database?: string | null; schema?: string | null }) {
if (isVirtualCompletionTableReference(table)) return;
const cacheKey = completionCacheKey(table);
if (cachedForeignKeysByTable.has(cacheKey) || !props.connectionId || props.database == null) return;
@@ -1598,7 +1608,7 @@ async function ensureForeignKeysForTable(table: { name: string; schema?: string
}
}
async function ensureForeignKeysForTables(tables: Array<{ name: string; schema?: string | null }>) {
async function ensureForeignKeysForTables(tables: Array<{ name: string; database?: string | null; schema?: string | null }>) {
const seen = new Set<string>();
const uniqueTables = tables.filter((table) => {
const key = completionCacheKey(table).toLowerCase();
@@ -2542,6 +2552,7 @@ function buildLocalSqlCompletionResult(completionContext: ReturnType<typeof getS
currentDatabase: props.database,
currentSchema: props.schema,
supportsDatabaseQualifier: supportsDatabaseQualifierCompletion(),
supportsDatabaseSchemaQualifier: supportsDatabaseSchemaQualifierCompletion(),
completionContext,
knownDatabases: databaseNames,
});
@@ -2556,10 +2567,11 @@ function buildLocalSqlCompletionResult(completionContext: ReturnType<typeof getS
const columnsByTable = new Map<string, SqlCompletionColumn[]>();
if (completionContext.insertTable) {
const insertDatabase = (supportsDatabaseSchemaQualifierCompletion() ? completionContext.insertDatabase : undefined) ?? props.database;
const insertSchema = completionContext.insertSchema ?? props.schema;
const insertColumns = usesOracleSessionCompletionColumns(insertSchema) ? [] : connectionStore.lookupLocalCompletionColumns(props.connectionId, props.database, completionContext.insertTable, insertSchema);
const insertColumns = usesOracleSessionCompletionColumns(insertSchema) ? [] : connectionStore.lookupLocalCompletionColumns(props.connectionId, insertDatabase, completionContext.insertTable, insertSchema);
if (insertColumns.length > 0) {
columnsByTable.set(insertSchema ? `${insertSchema}.${completionContext.insertTable}` : completionContext.insertTable, insertColumns);
columnsByTable.set(completionCacheKey({ name: completionContext.insertTable, database: completionContext.insertDatabase, schema: insertSchema }), insertColumns);
}
}
@@ -2593,7 +2605,7 @@ function buildLocalSqlCompletionResult(completionContext: ReturnType<typeof getS
);
continue;
}
const cacheKey = refTable.schema ? `${refTable.schema}.${refTable.name}` : refTable.name;
const cacheKey = completionCacheKey(refTable);
const cached = cachedColumnsByTable.get(cacheKey);
if (cached) {
columnsByTable.set(cacheKey, cached);
@@ -2643,6 +2655,7 @@ function scheduleCompletionMetadataRefresh(completionContext: ReturnType<typeof
currentDatabase: database,
currentSchema: props.schema,
supportsDatabaseQualifier: supportsDatabaseQualifierCompletion(),
supportsDatabaseSchemaQualifier: supportsDatabaseSchemaQualifierCompletion(),
completionContext,
knownDatabases: localCompletionDatabaseNames(completionContext),
});
@@ -2651,9 +2664,10 @@ function scheduleCompletionMetadataRefresh(completionContext: ReturnType<typeof
void connectionStore
.refreshCompletionTables(connectionId, tableLookupTarget.database, tableLookupTarget.filter, MAX_COMPLETION_TABLES, tableLookupTarget.schema, globalOracleTableSearch, props.schema)
.then((tables) => {
cachedTables = mergeCompletionTables(cachedTables, tables);
const scopedTables = tables.map((table) => ({ ...table, database: table.database ?? tableLookupTarget.database }));
cachedTables = mergeCompletionTables(cachedTables, scopedTables);
if (completionContext.suggestTables && completionContext.referencedTables.length > 0) {
void ensureForeignKeysForTables([...completionContext.referencedTables, ...tables.slice(0, MAX_JOIN_FK_PREFETCH_TABLES)]);
void ensureForeignKeysForTables([...completionContext.referencedTables, ...scopedTables.slice(0, MAX_JOIN_FK_PREFETCH_TABLES)]);
}
})
.catch(() => {});
@@ -2676,10 +2690,11 @@ function scheduleCompletionMetadataRefresh(completionContext: ReturnType<typeof
}
if (!onDemandOnlyColumns && completionContext.insertTable) {
const insertTable = completionContext.insertTable;
void refreshCompletionColumnsForEditor(connectionId, database, insertTable, completionContext.insertSchema ?? props.schema)
const insertDatabase = (supportsDatabaseSchemaQualifierCompletion() ? completionContext.insertDatabase : undefined) ?? database;
void refreshCompletionColumnsForEditor(connectionId, insertDatabase, insertTable, completionContext.insertSchema ?? props.schema)
.then((columns) => {
const insertSchema = completionContext.insertSchema ?? props.schema;
cachedColumnsByTable.set(insertSchema ? `${insertSchema}.${insertTable}` : insertTable, columns);
cachedColumnsByTable.set(completionCacheKey({ name: insertTable, database: completionContext.insertDatabase, schema: insertSchema }), columns);
})
.catch(() => {});
}
@@ -2699,7 +2714,7 @@ function scheduleCompletionMetadataRefresh(completionContext: ReturnType<typeof
for (const refTable of completionContext.referencedTables) {
if (isVirtualCompletionTableReference(refTable)) continue;
if (refTable.columns && refTable.columns.length > 0) continue;
const cacheKey = refTable.schema ? `${refTable.schema}.${refTable.name}` : refTable.name;
const cacheKey = completionCacheKey(refTable);
if (cacheKey === qualifiedColumnCacheKey) continue;
if (cachedColumnsByTable.has(cacheKey)) continue;
const target = completionMetadataTarget(refTable);
@@ -2718,9 +2733,9 @@ function scheduleCompletionMetadataRefresh(completionContext: ReturnType<typeof
function mergeCompletionTables(existing: SqlCompletionTable[], incoming: SqlCompletionTable[]): SqlCompletionTable[] {
const merged = [...existing];
const indexes = new Map(existing.map((table, index) => [`${table.schema ?? ""}.${table.name}`.toLowerCase(), index]));
const indexes = new Map(existing.map((table, index) => [`${table.database ?? ""}.${table.schema ?? ""}.${table.name}`.toLowerCase(), index]));
for (const table of incoming) {
const key = `${table.schema ?? ""}.${table.name}`.toLowerCase();
const key = `${table.database ?? ""}.${table.schema ?? ""}.${table.name}`.toLowerCase();
const index = indexes.get(key);
if (index == null) {
indexes.set(key, merged.length);
@@ -2743,10 +2758,11 @@ function withCompletionLatencyBudget<T>(remote: Promise<T>, local: T): Promise<T
}
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 local = connectionStore.lookupLocalCompletionTables(connectionId, database, filter, limit, globalSearch ? undefined : schema).map((table) => ({ ...table, database: table.database ?? database }));
const remote = connectionStore.listCompletionTables(connectionId, database, filter, limit, schema, globalSearch, props.schema).then((tables) => {
cachedTables = mergeCompletionTables(cachedTables, tables);
return tables;
const scopedTables = tables.map((table) => ({ ...table, database: table.database ?? database }));
cachedTables = mergeCompletionTables(cachedTables, scopedTables);
return scopedTables;
});
if (local.length === 0) return remote;
return withCompletionLatencyBudget(remote, local);
@@ -2807,11 +2823,12 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
let insertColumnsByTable = new Map<string, SqlCompletionColumn[]>();
if (completionContext.insertTable) {
try {
const insertCols = await listCompletionColumnsForEditor(props.connectionId!, props.database!, completionContext.insertTable, completionContext.insertSchema ?? props.schema);
const insertDatabase = (supportsDatabaseSchemaQualifierCompletion() ? completionContext.insertDatabase : undefined) ?? props.database!;
const insertCols = await listCompletionColumnsForEditor(props.connectionId!, insertDatabase, completionContext.insertTable, completionContext.insertSchema ?? props.schema);
if (epoch !== completionEpoch) return null;
if (insertCols.length > 0) {
const insertSchema = completionContext.insertSchema ?? props.schema;
const insertKey = insertSchema ? `${insertSchema}.${completionContext.insertTable}` : completionContext.insertTable;
const insertKey = completionCacheKey({ name: completionContext.insertTable, database: completionContext.insertDatabase, schema: insertSchema });
insertColumnsByTable.set(insertKey, insertCols);
}
} catch {
@@ -2833,6 +2850,7 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
currentDatabase: props.database!,
currentSchema: props.schema,
supportsDatabaseQualifier: supportsDatabaseQualifierCompletion(),
supportsDatabaseSchemaQualifier: supportsDatabaseSchemaQualifierCompletion(),
completionContext,
knownDatabases: databaseNames,
});
@@ -2842,6 +2860,9 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
? 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 (localOnlyMetadata && tables.length === 0 && supportsDatabaseSchemaQualifierCompletion() && (completionContext.qualifierParts?.length ?? 0) >= 2 && allowsOnDemandQualifiedTableCompletion(completionContext.prefix)) {
tables = await listCompletionTablesWithLatencyBudget(props.connectionId!, tableLookupTarget.database, tableLookupTarget.filter, PRESTO_ON_DEMAND_TABLE_COMPLETION_LIMIT, tableLookupTarget.schema);
}
if (epoch !== completionEpoch) return null;
const shouldLoadObjects = shouldLoadCompletionObjects(completionContext);
@@ -2942,7 +2963,7 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
refs.map(async (refTable) => {
if (isVirtualCompletionTableReference(refTable)) return;
if (refTable.columns && refTable.columns.length > 0) return;
const cacheKey = refTable.schema ? `${refTable.schema}.${refTable.name}` : refTable.name;
const cacheKey = completionCacheKey(refTable);
if (cachedColumnsByTable.has(cacheKey)) return;
try {
const target = completionMetadataTarget(refTable);
@@ -2986,7 +3007,7 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
);
continue;
}
const cacheKey = refTable.schema ? `${refTable.schema}.${refTable.name}` : refTable.name;
const cacheKey = completionCacheKey(refTable);
const cached = cachedColumnsByTable.get(cacheKey);
if (cached) {
columnsByTable.set(cacheKey, cached);
@@ -3797,14 +3818,15 @@ onMounted(async () => {
}> = [];
for (const refTable of tablesToCheck) {
const cacheKey = refTable.schema ? `${refTable.schema}.${refTable.name}` : refTable.name;
const cacheKey = completionCacheKey(refTable);
// Use persistent column cache; fetch only if missing
let cols = cachedColumnsByTable.get(cacheKey);
if (!cols) {
try {
const columnSchema = refTable.schema ?? props.schema;
cols = await listCompletionColumnsForEditor(props.connectionId!, props.database!, refTable.name, columnSchema);
const target = completionMetadataTarget(refTable);
if (!target) continue;
cols = await listCompletionColumnsForEditor(props.connectionId!, target.database, refTable.name, target.schema);
cachedColumnsByTable.set(cacheKey, cols);
} catch {
continue;