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

@@ -18,6 +18,7 @@ export function sqlSemanticReferencedTables(model: SqlSemanticModel): SqlComplet
.filter((source) => source.kind !== "unknown")
.map((source) => ({
name: source.name,
database: source.metadataTarget?.database,
schema: source.qualifierParts[source.qualifierParts.length - 1],
alias: source.alias,
columns: source.columns,
@@ -180,6 +181,7 @@ export function sqlCompletionContextFromSemantic(model: SqlSemanticModel, base:
const qualifier = model.cursorIntent.qualifierParts.length > 0 ? model.cursorIntent.qualifierParts.join(".") : undefined;
const referencedTables = sqlSemanticReferencedTables(model);
const mutationTarget = semanticMutationTarget(model);
const mutationDatabase = mutationTarget?.metadataTarget?.database;
const mutationSchema = mutationTarget?.qualifierParts[mutationTarget.qualifierParts.length - 1];
const suggestTables = scope.kind === "table" || scope.kind === "schema" || scope.kind === "catalog";
const suggestColumns = scope.kind === "columns";
@@ -203,6 +205,7 @@ export function sqlCompletionContextFromSemantic(model: SqlSemanticModel, base:
selectAliases: projectionAliases.length > 0 ? projectionAliases : base.selectAliases,
referencedTables: referencedTables.length > 0 ? referencedTables : base.referencedTables,
insertTable: model.cursorIntent.kind === "insert_column" ? mutationTarget?.name : base.insertTable,
insertDatabase: model.cursorIntent.kind === "insert_column" ? mutationDatabase : base.insertDatabase,
insertSchema: model.cursorIntent.kind === "insert_column" ? mutationSchema : base.insertSchema,
updateTarget: model.cursorIntent.kind === "update_column" && mutationTarget ? { table: mutationTarget.name, schema: mutationSchema } : base.updateTarget,
deleteTarget: model.cursorIntent.kind === "delete_target" && mutationTarget ? { table: mutationTarget.name, schema: mutationSchema } : base.deleteTarget,
@@ -255,6 +255,7 @@ function tableLookupFor(tables: SqlTableReference[]): Map<string, SqlTableRefere
lookup.set(normalizeName(table.name), table);
if (table.alias) lookup.set(normalizeName(table.alias), table);
if (table.schema) lookup.set(normalizeName(`${table.schema}.${table.name}`), table);
if (table.database && table.schema) lookup.set(normalizeName(`${table.database}.${table.schema}.${table.name}`), table);
}
return lookup;
}
@@ -301,7 +302,7 @@ function columnsForTable(table: SqlTableReference, columnsByTable: Map<string, S
schema: table.schema ?? undefined,
}));
}
const keys = table.schema ? [`${table.schema}.${table.name}`, table.name] : [table.name, ...keysWithTableName(columnsByTable, table.name)];
const keys = table.schema ? [table.database ? `${table.database}.${table.schema}.${table.name}` : undefined, `${table.schema}.${table.name}`, table.name].filter((key): key is string => !!key) : [table.name, ...keysWithTableName(columnsByTable, table.name)];
for (const key of keys) {
const normalizedKey = normalizeName(key);
const columns = columnsByTable.get(key) ?? columnsByTable.get(normalizedKey);
@@ -321,12 +322,12 @@ function rangesIntersect(left: SqlSemanticDiagnosticVisibleRange, right: SqlSema
return left.from < right.to && right.from < left.to;
}
export function tableReferenceKey(table: Pick<SqlTableReference, "name" | "schema">): string {
return normalizeName(table.schema ? `${table.schema}.${table.name}` : table.name);
export function tableReferenceKey(table: Pick<SqlTableReference, "name" | "database" | "schema">): string {
return normalizeName(table.schema ? `${table.database ? `${table.database}.` : ""}${table.schema}.${table.name}` : table.name);
}
function displayTableName(table: SqlTableReference): string {
return table.schema ? `${table.schema}.${table.name}` : table.name;
return table.schema ? `${table.database ? `${table.database}.` : ""}${table.schema}.${table.name}` : table.name;
}
function isCursorAfterTableTrigger(sql: string, cursor: number): boolean {
+6 -1
View File
@@ -409,6 +409,7 @@ function parseTableSource(state: ParseState, nameIndex: number, introducer: stri
columns: cte?.columns ? mergeColumnAliases(cte.columns, alias.columns) : undefined,
columnAliases: alias.columns,
metadataTarget: {
database: qualifierParts.length >= 2 ? qualifierParts[qualifierParts.length - 2] : undefined,
schema: qualifierParts[qualifierParts.length - 1],
table: name,
},
@@ -640,7 +641,11 @@ function buildCursorIntent(tokens: readonly SqlSemanticToken[], cursor: number,
if (
trailing.qualifierParts.length > 0 &&
(previous === "from" || previous === "join" || TABLE_INTRODUCERS.has(previous) || TABLE_INTRODUCERS.has(wordBeforeReplacement) || (!!targetSource && !targetSource.alias && TABLE_INTRODUCERS.has(wordBeforePosition(tokens, targetSource.sourceSpan.start))))
(previous === "from" ||
previous === "join" ||
TABLE_INTRODUCERS.has(previous) ||
TABLE_INTRODUCERS.has(wordBeforeReplacement) ||
(!!targetSource && !targetSource.alias && trailing.replacementRange.start <= targetSource.sourceSpan.end + 1 && TABLE_INTRODUCERS.has(wordBeforePosition(tokens, targetSource.sourceSpan.start))))
) {
const role = dialect.qualifierRole(trailing.qualifierParts, "table");
return { kind: role === "catalog" ? "catalog" : "table", prefix: trailing.prefix, replacementRange: trailing.replacementRange, qualifierParts: trailing.qualifierParts, expectedObjectKinds: ["table", "view"], confidence: "medium" };
@@ -10,6 +10,7 @@ export type SqlSemanticTableReference = SqlTableReference & {
export interface SqlSemanticNavigationTarget {
name: string;
database?: string;
schema?: string;
alias?: string;
columns?: string[];
@@ -51,12 +52,16 @@ function sourceSchema(source: SqlSemanticRowSource): string | undefined {
return source.metadataTarget?.schema ?? source.qualifierParts[source.qualifierParts.length - 1];
}
function sourceDatabase(source: SqlSemanticRowSource): string | undefined {
return source.metadataTarget?.database;
}
function sourceTableName(source: SqlSemanticRowSource): string {
return source.metadataTarget?.table ?? source.name;
}
function sourceReferenceKey(source: SqlSemanticTableReference): string {
return [normalized(source.schema), normalized(source.name), normalized(source.alias), source.span.start_line, source.span.start_column].join(":");
return [normalized(source.database), normalized(source.schema), normalized(source.name), normalized(source.alias), source.span.start_line, source.span.start_column].join(":");
}
function existingScopeId(analysis: SqlReferenceAnalysis): number {
@@ -72,6 +77,7 @@ export function sqlSemanticTableReferences(model: SqlSemanticModel, scopeId = 0)
const schema = sourceSchema(source);
return {
name: sourceTableName(source),
database: sourceDatabase(source),
schema,
alias: source.alias,
span: semanticSpanToSqlTextSpan(model.sql, span),
@@ -95,11 +101,13 @@ export function mergeSqlSemanticReferenceAnalysis(analysis: SqlReferenceAnalysis
for (const table of semanticTables) {
const existing = [...merged.values()].find((candidate) => {
if (normalized(candidate.name) !== normalized(table.name)) return false;
if (candidate.database && table.database && normalized(candidate.database) !== normalized(table.database)) return false;
if (candidate.alias && table.alias && normalized(candidate.alias) !== normalized(table.alias)) return false;
if (candidate.schema && table.schema && normalized(candidate.schema) !== normalized(table.schema)) return false;
return true;
});
if (existing) {
existing.database = existing.database ?? table.database;
existing.schema = existing.schema ?? table.schema;
existing.alias = existing.alias ?? table.alias;
existing.columns = existing.columns ?? table.columns;
@@ -118,6 +126,7 @@ export function mergeSqlSemanticReferenceAnalysis(analysis: SqlReferenceAnalysis
export function sqlSemanticCompletionReferenceTables(model: SqlSemanticModel): SqlCompletionReferencedTable[] {
return sqlSemanticTableReferences(model).map((table) => ({
name: table.name,
database: table.database ?? undefined,
schema: table.schema ?? undefined,
alias: table.alias ?? undefined,
columns: table.columns,
@@ -129,15 +138,17 @@ export function resolveSqlSemanticNavigationTarget(model: SqlSemanticModel, iden
const normalizedParts = identifierParts.map(normalized).filter(Boolean);
const name = normalizedParts[normalizedParts.length - 1];
const qualifier = normalizedParts.length > 1 ? normalizedParts[normalizedParts.length - 2] : undefined;
const database = normalizedParts.length > 2 ? normalizedParts[normalizedParts.length - 3] : undefined;
const source =
model.rowSources.find((candidate) => {
const candidateName = normalized(sourceTableName(candidate));
const candidateAlias = normalized(candidate.alias);
const candidateDatabase = normalized(sourceDatabase(candidate));
const candidateSchema = normalized(sourceSchema(candidate));
if (qualifier) {
if (candidateAlias && candidateAlias === qualifier) return true;
return candidateName === name && (!candidateSchema || candidateSchema === qualifier);
if (!database && candidateAlias && candidateAlias === qualifier) return true;
return candidateName === name && (!candidateSchema || candidateSchema === qualifier) && (!database || !candidateDatabase || candidateDatabase === database);
}
return candidateName === name || candidateAlias === name;
}) ?? null;
@@ -146,6 +157,7 @@ export function resolveSqlSemanticNavigationTarget(model: SqlSemanticModel, iden
const schema = sourceSchema(source);
return {
name: sourceTableName(source),
database: sourceDatabase(source),
schema,
alias: source.alias,
columns: source.columns,
+64 -30
View File
@@ -1255,6 +1255,7 @@ function sqlAliasKeywordWords(...sources: Array<string | undefined>): string[] {
export interface SqlCompletionTable {
name: string;
database?: string;
schema?: string;
type?: SqlObjectNavigationType;
detail?: string;
@@ -1312,6 +1313,7 @@ export type SqlKeywordCase = "preserve" | "upper" | "lower";
export interface SqlCompletionReferencedTable {
name: string;
database?: string;
schema?: string;
alias?: string;
columns?: string[];
@@ -1338,6 +1340,7 @@ export interface SqlCompletionContext {
selectAliases: string[];
referencedTables: SqlCompletionReferencedTable[];
insertTable?: string;
insertDatabase?: string;
insertSchema?: string;
statementKind: SqlStatementKind;
tableTriggerWord?: string;
@@ -1967,6 +1970,7 @@ export function getSqlCompletionContext(sql: string, cursor: number): SqlComplet
selectAliases: prioritizeSelectAliases ? extractSelectAliases(fullStatement) : [],
referencedTables,
insertTable: insertInfo?.table,
insertDatabase: insertInfo?.database,
insertSchema: insertInfo?.schema,
statementKind,
tableTriggerWord: lastWord || undefined,
@@ -2115,6 +2119,15 @@ function parseTrailingIdentifierPart(input: string, endExclusive: number): { sta
return { start, raw: input.slice(start, endExclusive) };
}
if (tailChar === "]") {
let start = end - 1;
while (start >= 0) {
if (input[start] === "[") return { start, raw: input.slice(start, endExclusive) };
start -= 1;
}
return null;
}
let start = end;
while (start >= 0 && /[A-Za-z0-9_$@]/.test(input[start] ?? "")) start -= 1;
start += 1;
@@ -2305,19 +2318,24 @@ function detectComparisonLeftColumn(beforeCursor: string): string | undefined {
return match?.[1];
}
function detectInsertColumnListContext(beforeCursor: string): { table: string; schema?: string } | null {
function detectInsertColumnListContext(beforeCursor: string): { table: string; database?: string; schema?: string } | null {
// Keep quoted identifiers intact so schema/table targets resolve to their
// real names instead of placeholder string contents.
const cleaned = beforeCursor.replace(/'[^']*'/g, "''");
const identifier = '(?:"[^"]+"|`[^`]+`|[A-Za-z_][\\w$]*)';
const identifier = '(?:"[^"]+"|`[^`]+`|\\[[^\\]]+\\]|[A-Za-z_][\\w$]*)';
const qualifiedIdentifier = `${identifier}(?:\\.${identifier}){0,2}`;
const match = new RegExp(`\\binsert\\s+into\\s+(${qualifiedIdentifier})\\s*\\([^)]*$`, "i").exec(cleaned);
if (!match) return null;
const fullTable = match[1];
if (!fullTable) return null;
const [first, second] = splitQualifiedName(fullTable);
if (second) return { table: second, schema: first! };
return { table: first! };
const parts = splitQualifiedNameParts(fullTable);
const table = parts[parts.length - 1];
if (!table) return null;
return {
table,
database: parts.length >= 3 ? parts[parts.length - 3] : undefined,
schema: parts.length >= 2 ? parts[parts.length - 2] : undefined,
};
}
function detectUpdateCompletionContext(beforeCursor: string): { target: { table: string; schema?: string }; afterTarget: boolean; inSetClause: boolean; afterSetAssignments: boolean } | null {
@@ -2576,7 +2594,8 @@ function extractReferencedTables(sql: string): SqlCompletionReferencedTable[] {
]);
// STRAIGHT_JOIN is a standalone MySQL table introducer, not a modifier followed by JOIN.
const pattern = /\b(?:from|join|straight_join|update|apply)\s+((?:"[^"]+"|`[^`]+`|[^\s,;()]+)(?:\.(?:"[^"]+"|`[^`]+`|[^\s,;()]+))?)(?:\s+(?:as\s+)?([A-Za-z_][\w$]*))?/gi;
const identifier = '(?:"[^"]+"|`[^`]+`|\\[[^\\]]+\\]|[A-Za-z_][\\w$@#]*)';
const pattern = new RegExp(`\\b(?:from|join|straight_join|update|apply)\\s+(${identifier}(?:\\.${identifier}){0,3})(?:\\s+(?:as\\s+)?([A-Za-z_][\\w$]*))?`, "gi");
const referenced: SqlCompletionReferencedTable[] = [];
let match: RegExpExecArray | null;
while ((match = pattern.exec(sql)) !== null) {
@@ -2585,7 +2604,7 @@ function extractReferencedTables(sql: string): SqlCompletionReferencedTable[] {
if (alias && ALIAS_BLACKLIST.has(alias.toLowerCase())) {
pattern.lastIndex = match.index + match[0].length - alias.length;
}
const quotedName = !!rawName && (rawName.startsWith('"') || rawName.startsWith("`"));
const quotedName = !!rawName && (rawName.startsWith('"') || rawName.startsWith("`") || rawName.startsWith("["));
if (!quotedName && rawName && ALIAS_BLACKLIST.has(rawName.toLowerCase())) continue;
// Filter out SQL keywords that accidentally matched as aliases
const cleanAlias = alias && !ALIAS_BLACKLIST.has(alias.toLowerCase()) ? alias : undefined;
@@ -2593,9 +2612,15 @@ function extractReferencedTables(sql: string): SqlCompletionReferencedTable[] {
referenced.push({ name: unquoteIdentifier(rawName), alias: cleanAlias });
continue;
}
const [first, second] = splitQualifiedName(rawName);
if (!first) continue;
const table = second ? { schema: first, name: second, alias: cleanAlias } : { name: first, alias: cleanAlias };
const parts = splitQualifiedNameParts(rawName);
const name = parts[parts.length - 1];
if (!name) continue;
const table: SqlCompletionReferencedTable = {
name,
database: parts.length >= 3 ? parts[parts.length - 3] : undefined,
schema: parts.length >= 2 ? parts[parts.length - 2] : undefined,
alias: cleanAlias,
};
referenced.push(table);
}
return referenced;
@@ -2848,24 +2873,33 @@ function splitTopLevel(text: string, separator: string): string[] {
}
function splitQualifiedName(input: string): [string | undefined, string | undefined] {
const unquoted = splitQualifiedNameParts(input);
if (unquoted.length >= 2) return [unquoted[unquoted.length - 2], unquoted[unquoted.length - 1]];
return [unquoted[0], undefined];
}
function splitQualifiedNameParts(input: string): string[] {
const parts: string[] = [];
let current = "";
let inDoubleQuote = false;
let inBacktick = false;
let inBracket = false;
for (let i = 0; i < input.length; i++) {
const ch = input[i];
if (ch === '"' && !inBacktick) {
if (ch === '"' && !inBacktick && !inBracket) {
inDoubleQuote = !inDoubleQuote;
current += ch;
continue;
}
if (ch === "`" && !inDoubleQuote) {
if (ch === "`" && !inDoubleQuote && !inBracket) {
inBacktick = !inBacktick;
current += ch;
continue;
}
if (ch === "." && !inDoubleQuote && !inBacktick) {
if (ch === "[" && !inDoubleQuote && !inBacktick) inBracket = true;
if (ch === "]" && inBracket) inBracket = false;
if (ch === "." && !inDoubleQuote && !inBacktick && !inBracket) {
parts.push(current.trim());
current = "";
} else {
@@ -2874,15 +2908,11 @@ function splitQualifiedName(input: string): [string | undefined, string | undefi
}
if (current.trim()) parts.push(current.trim());
const unquoted = parts.map((p) => unquoteIdentifier(p)).filter(Boolean);
// For three-part names, the completion model can carry schema.table today;
// keep the table as the final segment instead of accidentally using catalog.schema.
if (unquoted.length >= 2) return [unquoted[unquoted.length - 2], unquoted[unquoted.length - 1]];
return [unquoted[0], undefined];
return parts.map((p) => unquoteIdentifier(p)).filter(Boolean);
}
function unquoteIdentifier(value: string): string {
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("`") && value.endsWith("`"))) {
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("`") && value.endsWith("`")) || (value.startsWith("[") && value.endsWith("]"))) {
return value.slice(1, -1);
}
return value;
@@ -3585,11 +3615,12 @@ function columnsForInsertTarget(context: SqlCompletionContext, columnsByTable: M
if (!context.insertTable) return [];
const tableKey = normalizeIdentifierPart(context.insertTable);
const schemaKey = context.insertSchema ? normalizeIdentifierPart(context.insertSchema) : undefined;
const qualifiedKey = schemaKey ? normalizeCompletionKey(`${context.insertSchema}.${context.insertTable}`) : undefined;
const databaseKey = context.insertDatabase ? normalizeIdentifierPart(context.insertDatabase) : undefined;
const qualifiedKey = schemaKey ? normalizeCompletionKey(`${context.insertDatabase ? `${context.insertDatabase}.` : ""}${context.insertSchema}.${context.insertTable}`) : undefined;
return collectCompletionColumns(columnsByTable).filter((column) => {
if (normalizeIdentifierPart(column.table) !== tableKey) return false;
if (!schemaKey) return true;
if (column.schema && normalizeIdentifierPart(column.schema) === schemaKey) return true;
if (!databaseKey && column.schema && normalizeIdentifierPart(column.schema) === schemaKey) return true;
return !!qualifiedKey && normalizeCompletionKey(column.key) === qualifiedKey;
});
}
@@ -3679,33 +3710,36 @@ function hasMatchingReferencedColumnPrefix(context: SqlCompletionContext, column
return context.referencedTables.some((table) => columnsForReferencedTable(table, columnsByTable).some((column) => matchesPrefix(column.name, context.prefix)));
}
function qualifiedTableTargetFromContext(context: SqlCompletionContext): { schema: string; table: string } | null {
function qualifiedTableTargetFromContext(context: SqlCompletionContext): { database?: string; schema: string; table: string } | null {
const parts = context.qualifierParts ?? context.qualifier?.split(".").filter(Boolean) ?? [];
if (parts.length < 2) return null;
const table = parts[parts.length - 1];
const schema = parts[parts.length - 2];
if (!schema || !table) return null;
return { schema, table };
const database = parts.length >= 3 ? parts[parts.length - 3] : undefined;
return { database, schema, table };
}
function referencedTableMatchesColumnQualifier(table: SqlCompletionReferencedTable, qualifier: string, qualifierLower: string, qualifiedTarget: { schema: string; table: string } | null): boolean {
function referencedTableMatchesColumnQualifier(table: SqlCompletionReferencedTable, qualifier: string, qualifierLower: string, qualifiedTarget: { database?: string; schema: string; table: string } | null): boolean {
if (table.alias === qualifier || table.alias?.toLowerCase() === qualifierLower) return true;
if (table.name === qualifier || table.name.toLowerCase() === qualifierLower) return true;
if (!qualifiedTarget) return false;
if (normalizeIdentifierPart(table.name) !== normalizeIdentifierPart(qualifiedTarget.table)) return false;
if (table.database && qualifiedTarget.database && normalizeIdentifierPart(table.database) !== normalizeIdentifierPart(qualifiedTarget.database)) return false;
return !table.schema || normalizeIdentifierPart(table.schema) === normalizeIdentifierPart(qualifiedTarget.schema);
}
function columnMatchesReferencedTable(column: SqlCompletionColumn & { key: string }, table: SqlCompletionReferencedTable): boolean {
if (normalizeIdentifierPart(column.table) !== normalizeIdentifierPart(table.name)) return false;
if (!table.schema) return true;
return columnMatchesQualifiedTable(column, { schema: table.schema, table: table.name });
return columnMatchesQualifiedTable(column, { database: table.database, schema: table.schema, table: table.name });
}
function columnMatchesQualifiedTable(column: SqlCompletionColumn & { key: string }, target: { schema: string; table: string }): boolean {
function columnMatchesQualifiedTable(column: SqlCompletionColumn & { key: string }, target: { database?: string; schema: string; table: string }): boolean {
if (normalizeIdentifierPart(column.table) !== normalizeIdentifierPart(target.table)) return false;
if (column.schema && normalizeIdentifierPart(column.schema) === normalizeIdentifierPart(target.schema)) return true;
return normalizeCompletionKey(column.key) === normalizeCompletionKey(`${target.schema}.${target.table}`);
if (!target.database && column.schema && normalizeIdentifierPart(column.schema) === normalizeIdentifierPart(target.schema)) return true;
const key = `${target.database ? `${target.database}.` : ""}${target.schema}.${target.table}`;
return normalizeCompletionKey(column.key) === normalizeCompletionKey(key);
}
function normalizeCompletionKey(key: string): string {
@@ -3769,7 +3803,7 @@ function buildJoinConditionItems(context: SqlCompletionContext, columnsByTable:
}
function columnsForReferencedTable(table: SqlCompletionReferencedTable, columnsByTable: Map<string, SqlCompletionColumn[]>): SqlCompletionColumn[] {
const keys = table.schema ? [`${table.schema}.${table.name}`, table.name] : [table.name];
const keys = table.schema ? [table.database ? `${table.database}.${table.schema}.${table.name}` : undefined, `${table.schema}.${table.name}`, table.name].filter((key): key is string => !!key) : [table.name];
for (const key of keys) {
const columns = columnsByTable.get(key);
if (columns) return applyReferencedColumnAliases(table, columns);
@@ -3779,7 +3813,7 @@ function columnsForReferencedTable(table: SqlCompletionReferencedTable, columnsB
function foreignKeysForReferencedTable(table: SqlCompletionReferencedTable, foreignKeysByTable?: Map<string, SqlCompletionForeignKey[]>): SqlCompletionForeignKey[] {
if (!foreignKeysByTable) return [];
const keys = table.schema ? [`${table.schema}.${table.name}`, table.name] : [table.name];
const keys = table.schema ? [table.database ? `${table.database}.${table.schema}.${table.name}` : undefined, `${table.schema}.${table.name}`, table.name].filter((key): key is string => !!key) : [table.name];
for (const key of keys) {
const foreignKeys = foreignKeysByTable.get(key);
if (foreignKeys) return foreignKeys;
@@ -20,11 +20,24 @@ export function resolveSqlCompletionTableLookupTarget(options: {
currentDatabase: string;
currentSchema?: string;
supportsDatabaseQualifier: boolean;
completionContext: Pick<SqlCompletionContext, "qualifier" | "prefix" | "suggestTables" | "insertTable">;
supportsDatabaseSchemaQualifier?: boolean;
completionContext: Pick<SqlCompletionContext, "qualifier" | "qualifierParts" | "prefix" | "suggestTables" | "insertTable">;
knownDatabases?: readonly string[];
}): SqlCompletionTableLookupTarget {
const { completionContext } = options;
const qualifier = completionContext.qualifier?.trim();
const qualifierParts = completionContext.qualifierParts?.filter(Boolean) ?? qualifier?.split(".").filter(Boolean) ?? [];
if (options.supportsDatabaseSchemaQualifier && completionContext.suggestTables && !completionContext.insertTable && qualifierParts.length >= 2) {
const databaseQualifier = qualifierParts[qualifierParts.length - 2]!;
const schema = qualifierParts[qualifierParts.length - 1]!;
const database = findExactName(options.knownDatabases, databaseQualifier) ?? databaseQualifier;
return {
database,
schema,
filter: completionContext.prefix,
qualifierDatabase: database,
};
}
const qualifierIsDatabase = options.supportsDatabaseQualifier && !!qualifier && completionContext.suggestTables && !completionContext.insertTable;
if (qualifierIsDatabase) {
+3 -2
View File
@@ -300,7 +300,7 @@ export function splitQualifiedIdentifier(identifier: string): string[] {
}
/** Match identifier against known table names (case-insensitive). Supports qualified identifiers like schema.table. */
export function matchTable<T extends { name: string; schema?: string }>(identifier: string, tables: T[]): T | null {
export function matchTable<T extends { name: string; database?: string; schema?: string }>(identifier: string, tables: T[]): T | null {
const parts = splitQualifiedIdentifier(identifier);
const normalizedIdentifier = parts.length > 0 ? parts.join(".").toLowerCase() : identifier.toLowerCase();
@@ -311,7 +311,8 @@ export function matchTable<T extends { name: string; schema?: string }>(identifi
// Use the final two parts so catalog.schema.table still resolves against schema-scoped metadata.
const qualifier = parts[parts.length - 2].toLowerCase();
const name = parts[parts.length - 1].toLowerCase();
const qualified = tables.find((t) => t.name.toLowerCase() === name && t.schema?.toLowerCase() === qualifier);
const database = parts.length >= 3 ? parts[parts.length - 3].toLowerCase() : undefined;
const qualified = tables.find((t) => t.name.toLowerCase() === name && t.schema?.toLowerCase() === qualifier && (!database || !t.database || t.database.toLowerCase() === database));
if (qualified) return qualified;
}