feat(editor): support SQL Server cross-database completion
This commit is contained in:
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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user