fix(sql): narrow completion suggestions
This commit is contained in:
parent
7a3721952a
commit
af5eb9cb2f
|
|
@ -258,6 +258,7 @@ const SQL_KEYWORDS = [
|
|||
];
|
||||
|
||||
const TABLE_TRIGGER_KEYWORDS = new Set(["from", "join", "update", "into", "table", "describe", "explain", "apply"]);
|
||||
const EXCLUSIVE_TABLE_TRIGGER_KEYWORDS = new Set(["from", "join", "update", "into", "apply"]);
|
||||
const JOIN_MODIFIERS = new Set(["left", "right", "inner", "outer", "cross", "full", "natural"]);
|
||||
const MAX_TABLE_COMPLETION_ITEMS = 200;
|
||||
|
||||
|
|
@ -349,6 +350,8 @@ export interface SqlCompletionContext {
|
|||
suggestColumns: boolean;
|
||||
suggestKeywords: boolean;
|
||||
suggestJoinConditions: boolean;
|
||||
exclusiveTableSuggestions: boolean;
|
||||
exclusiveColumnSuggestions: boolean;
|
||||
prioritizeSelectAliases: boolean;
|
||||
selectAliases: string[];
|
||||
referencedTables: SqlCompletionReferencedTable[];
|
||||
|
|
@ -382,14 +385,16 @@ export function buildSqlCompletionItemsFromContext(
|
|||
): SqlCompletionItem[] {
|
||||
const items: SqlCompletionItem[] = [];
|
||||
|
||||
items.push(...buildSnippetItems(context.prefix));
|
||||
items.push(...buildFunctionSnippetItems(context.prefix));
|
||||
if (!context.exclusiveTableSuggestions && !context.exclusiveColumnSuggestions) {
|
||||
items.push(...buildSnippetItems(context.prefix));
|
||||
items.push(...buildFunctionSnippetItems(context.prefix));
|
||||
}
|
||||
|
||||
if (context.prioritizeSelectAliases) {
|
||||
if (!context.exclusiveTableSuggestions && !context.exclusiveColumnSuggestions && context.prioritizeSelectAliases) {
|
||||
items.push(...buildSelectAliasItems(context));
|
||||
}
|
||||
|
||||
if (context.suggestJoinConditions) {
|
||||
if (!context.exclusiveTableSuggestions && !context.exclusiveColumnSuggestions && context.suggestJoinConditions) {
|
||||
items.push(...buildJoinConditionItems(context, input.columnsByTable));
|
||||
}
|
||||
|
||||
|
|
@ -398,11 +403,11 @@ export function buildSqlCompletionItemsFromContext(
|
|||
items.push(...buildKeywordItems(context.prefix));
|
||||
}
|
||||
|
||||
if (context.suggestColumns) {
|
||||
if (!context.exclusiveTableSuggestions && context.suggestColumns) {
|
||||
items.push(...buildColumnItems(context, input.columnsByTable));
|
||||
}
|
||||
|
||||
if (context.suggestTables) {
|
||||
if (!context.exclusiveColumnSuggestions && context.suggestTables) {
|
||||
items.push(...buildTableItems(context.prefix, input.tables));
|
||||
}
|
||||
|
||||
|
|
@ -492,8 +497,8 @@ export function getSqlCompletionContext(sql: string, cursor: number): SqlComplet
|
|||
const plainMatch = /([A-Za-z_][\w$]*)$/.exec(beforeCursor);
|
||||
const prefix = dottedMatch?.[2] ?? plainMatch?.[1] ?? "";
|
||||
const qualifier = dottedMatch?.[1];
|
||||
const bareStart = qualifier
|
||||
? beforeCursor.length - prefix.length
|
||||
const bareStart = dottedMatch
|
||||
? beforeCursor.length - dottedMatch[0].length
|
||||
: beforeCursor.length - (plainMatch?.[1]?.length ?? 0);
|
||||
const beforeToken = beforeCursor.slice(0, Math.max(0, bareStart)).trimEnd();
|
||||
const lastWord = /([A-Za-z_][\w$]*)$/.exec(beforeToken)?.[1]?.toLowerCase() ?? "";
|
||||
|
|
@ -504,6 +509,11 @@ export function getSqlCompletionContext(sql: string, cursor: number): SqlComplet
|
|||
TABLE_TRIGGER_KEYWORDS.has(lastWord) ||
|
||||
(JOIN_MODIFIERS.has(lastWord) && isFollowedByJoin(beforeToken)) ||
|
||||
isInTableListContext(beforeToken);
|
||||
const exclusiveTableSuggestions =
|
||||
EXCLUSIVE_TABLE_TRIGGER_KEYWORDS.has(lastWord) ||
|
||||
(JOIN_MODIFIERS.has(lastWord) && isFollowedByJoin(beforeToken)) ||
|
||||
isInTableListContext(beforeToken);
|
||||
const exclusiveColumnSuggestions = !!qualifier && !exclusiveTableSuggestions;
|
||||
|
||||
// Check if we're in a context where columns are expected
|
||||
const inColumnContext = isInColumnContext(beforeCursor);
|
||||
|
|
@ -520,8 +530,10 @@ export function getSqlCompletionContext(sql: string, cursor: number): SqlComplet
|
|||
// 2. We're in a column context (WHERE, ON, SELECT, etc.) AND there are referenced tables
|
||||
suggestColumns: !!qualifier || (inColumnContext && referencedTables.length > 0),
|
||||
// Always suggest keywords
|
||||
suggestKeywords: true,
|
||||
suggestKeywords: !exclusiveTableSuggestions && !exclusiveColumnSuggestions,
|
||||
suggestJoinConditions: inJoinConditionContext && referencedTables.length >= 2,
|
||||
exclusiveTableSuggestions,
|
||||
exclusiveColumnSuggestions,
|
||||
prioritizeSelectAliases,
|
||||
selectAliases: prioritizeSelectAliases ? extractSelectAliases(fullStatement) : [],
|
||||
referencedTables,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const tables: SqlCompletionTable[] = [
|
|||
{ name: "users", schema: "public", type: "table" },
|
||||
{ name: "user_profiles", schema: "public", type: "table" },
|
||||
{ name: "orders", schema: "public", type: "table" },
|
||||
{ name: "ticket_summary", schema: "public", type: "view" },
|
||||
];
|
||||
|
||||
const columnsByTable = new Map<string, SqlCompletionColumn[]>([
|
||||
|
|
@ -84,6 +85,33 @@ test("suggests columns for an explicit alias qualifier", () => {
|
|||
);
|
||||
});
|
||||
|
||||
test("suggests only matching columns for an explicit alias qualifier prefix", () => {
|
||||
const sql = "select u.na from public.users u join public.orders o on u.id = o.user_id";
|
||||
const cursor = "select u.na".length;
|
||||
const items = buildSqlCompletionItems(sql, cursor, {
|
||||
tables,
|
||||
columnsByTable,
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
items.map((item) => [item.label, item.type, item.detail]),
|
||||
[["name", "column", "public.users"]],
|
||||
);
|
||||
});
|
||||
|
||||
test("keeps explicit alias column suggestions scoped to the alias table", () => {
|
||||
const sql = "select * from public.users u join public.orders o on u.id = o.user_id where o.st";
|
||||
const items = buildSqlCompletionItems(sql, sql.length, {
|
||||
tables,
|
||||
columnsByTable,
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
items.map((item) => [item.label, item.type, item.detail]),
|
||||
[["status", "column", "public.orders"]],
|
||||
);
|
||||
});
|
||||
|
||||
test("suggests columns from referenced tables in select list", () => {
|
||||
const sql = "select na from public.users u join public.orders o on u.id = o.user_id";
|
||||
const cursor = "select na".length;
|
||||
|
|
@ -127,15 +155,62 @@ test("suggests keywords when typing without context", () => {
|
|||
assert.ok(items.some((item) => item.type === "keyword" && item.label === "USING"));
|
||||
});
|
||||
|
||||
test("always includes keywords alongside table suggestions", () => {
|
||||
test("suggests only matching table names after FROM object input", () => {
|
||||
const sql = "select * from us";
|
||||
const items = buildSqlCompletionItems(sql, sql.length, {
|
||||
tables,
|
||||
columnsByTable,
|
||||
});
|
||||
|
||||
assert.ok(items.some((item) => item.type === "table"));
|
||||
assert.ok(items.some((item) => item.type === "keyword" && item.label === "USING"));
|
||||
assert.ok(items.length > 0);
|
||||
assert.deepEqual([...new Set(items.map((item) => item.type))], ["table"]);
|
||||
assert.deepEqual(
|
||||
items.map((item) => item.label),
|
||||
["users", "user_profiles"],
|
||||
);
|
||||
});
|
||||
|
||||
test("keeps schema-qualified FROM object input in table suggestion mode", () => {
|
||||
const sql = "select * from public.us";
|
||||
const items = buildSqlCompletionItems(sql, sql.length, {
|
||||
tables,
|
||||
columnsByTable,
|
||||
});
|
||||
|
||||
assert.ok(items.length > 0);
|
||||
assert.deepEqual([...new Set(items.map((item) => item.type))], ["table"]);
|
||||
assert.deepEqual(
|
||||
items.map((item) => item.label),
|
||||
["users", "user_profiles"],
|
||||
);
|
||||
});
|
||||
|
||||
test("includes views in exclusive FROM object suggestions", () => {
|
||||
const sql = "select * from tick";
|
||||
const items = buildSqlCompletionItems(sql, sql.length, {
|
||||
tables,
|
||||
columnsByTable,
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
items.map((item) => [item.label, item.type, item.detail]),
|
||||
[["ticket_summary", "table", "public.ticket_summary"]],
|
||||
);
|
||||
});
|
||||
|
||||
test("suggests only table names after JOIN object input", () => {
|
||||
const sql = "select * from users join us";
|
||||
const items = buildSqlCompletionItems(sql, sql.length, {
|
||||
tables,
|
||||
columnsByTable,
|
||||
});
|
||||
|
||||
assert.ok(items.length > 0);
|
||||
assert.deepEqual([...new Set(items.map((item) => item.type))], ["table"]);
|
||||
assert.deepEqual(
|
||||
items.map((item) => item.label),
|
||||
["users", "user_profiles"],
|
||||
);
|
||||
});
|
||||
|
||||
test("suggests SQL Server IF keyword for conditional DDL", () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue