fix(postgres): support quoted schema autocomplete

This commit is contained in:
t8y2 2026-06-03 17:00:24 +08:00
parent af40af603e
commit 0f40f6cd0c
2 changed files with 111 additions and 7 deletions

View File

@ -0,0 +1,29 @@
import { describe, expect, it } from "vitest";
import { buildSqlCompletionItems, getSqlCompletionContext } from "@/lib/sqlCompletion";
describe("sqlCompletion quoted schema qualifiers", () => {
it("parses quoted PostgreSQL schema names before a dot", () => {
const sql = 'SELECT *\nFROM "order-management".';
const context = getSqlCompletionContext(sql, sql.length);
expect(context.qualifier).toBe("order-management");
expect(context.prefix).toBe("");
expect(context.suggestTables).toBe(true);
expect(context.exclusiveColumnSuggestions).toBe(false);
});
it("suggests tables after a quoted schema qualifier", () => {
const sql = 'SELECT *\nFROM "order-management".';
const items = buildSqlCompletionItems(sql, sql.length, {
dialect: "postgres",
tables: [
{ name: "orders", schema: "order-management", type: "table" },
{ name: "shipments", schema: "order-management", type: "table" },
],
columnsByTable: new Map(),
});
expect(items.some((item) => item.label === "orders" && item.type === "table")).toBe(true);
expect(items.some((item) => item.label === "shipments" && item.type === "table")).toBe(true);
});
});

View File

@ -915,13 +915,10 @@ export function getSqlCompletionContext(sql: string, cursor: number): SqlComplet
const stmtStart = extractStatementStart(sql, cursor);
const beforeCursor = sql.slice(stmtStart, cursor);
const dottedMatch = /([A-Za-z_][\w$]*)\.([A-Za-z_][\w$]*)?$/.exec(beforeCursor);
const plainMatch = /([A-Za-z_][\w$]*)$/.exec(beforeCursor);
const prefix = dottedMatch?.[2] ?? plainMatch?.[1] ?? "";
const qualifier = dottedMatch?.[1];
const bareStart = dottedMatch
? beforeCursor.length - dottedMatch[0].length
: beforeCursor.length - (plainMatch?.[1]?.length ?? 0);
const trailingIdentifier = parseTrailingIdentifierContext(beforeCursor);
const prefix = trailingIdentifier?.prefix ?? "";
const qualifier = trailingIdentifier?.qualifier;
const bareStart = trailingIdentifier?.start ?? beforeCursor.length;
const beforeToken = beforeCursor.slice(0, Math.max(0, bareStart)).trimEnd();
const lastWord = /([A-Za-z_][\w$]*)$/.exec(beforeToken)?.[1]?.toLowerCase() ?? "";
@ -996,6 +993,84 @@ export function getSqlCompletionContext(sql: string, cursor: number): SqlComplet
};
}
function parseTrailingIdentifierContext(input: string): { start: number; prefix: string; qualifier?: string } | null {
let i = input.length - 1;
while (i >= 0 && /\s/.test(input[i] ?? "")) i--;
if (i < 0) return null;
const endsWithDot = input[i] === ".";
const tail = input.slice(0, endsWithDot ? i : i + 1);
if (!tail) {
return endsWithDot ? { start: i, prefix: "" } : null;
}
const parts: string[] = [];
let index = tail.length;
while (index > 0) {
const parsed = parseTrailingIdentifierPart(tail, index);
if (!parsed) break;
parts.unshift(unquoteIdentifier(parsed.raw));
index = parsed.start;
if (index <= 0 || tail[index - 1] !== ".") break;
index -= 1;
}
if (parts.length === 0) return null;
const start = index;
if (parts.length >= 2 || endsWithDot) {
const qualifierParts = endsWithDot ? parts : parts.slice(0, -1);
const prefixPart = endsWithDot ? "" : (parts[parts.length - 1] ?? "");
const qualifierValue = qualifierParts.join(".");
return {
start,
prefix: prefixPart,
qualifier: qualifierValue || undefined,
};
}
return {
start,
prefix: parts[0] ?? "",
};
}
function parseTrailingIdentifierPart(input: string, endExclusive: number): { start: number; raw: string } | null {
if (endExclusive <= 0) return null;
const end = endExclusive - 1;
const tailChar = input[end];
if (!tailChar) return null;
if (tailChar === '"') {
let start = end - 1;
while (start >= 0) {
if (input[start] === '"') {
if (start > 0 && input[start - 1] === '"') {
start -= 2;
continue;
}
return { start, raw: input.slice(start, endExclusive) };
}
start -= 1;
}
return null;
}
if (tailChar === "`") {
const start = input.lastIndexOf("`", end - 1);
if (start < 0) return null;
return { start, raw: input.slice(start, endExclusive) };
}
let start = end;
while (start >= 0 && /[A-Za-z0-9_$]/.test(input[start] ?? "")) start -= 1;
start += 1;
if (start >= endExclusive) return null;
const raw = input.slice(start, endExclusive);
if (!/^[A-Za-z_][\w$]*$/.test(raw)) return null;
return { start, raw };
}
/**
* Check if the content before cursor is in a column-expected context.
*/