368 lines
15 KiB
TypeScript
368 lines
15 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { buildSqlCompletionItems, getSqlCompletionContext, shouldAutoOpenSqlCompletion } from "@/lib/sql/sqlCompletion";
|
|
|
|
describe("sqlCompletion keyword snippets", () => {
|
|
it("auto-opens and suggests SELECT when typing sel", () => {
|
|
const sql = "sel";
|
|
const items = buildSqlCompletionItems(sql, sql.length, {
|
|
tables: [],
|
|
columnsByTable: new Map(),
|
|
});
|
|
|
|
expect(shouldAutoOpenSqlCompletion(sql, sql.length)).toBe(true);
|
|
expect(items).toEqual(expect.arrayContaining([expect.objectContaining({ label: "select *", type: "snippet" }), expect.objectContaining({ label: "SELECT", type: "keyword" })]));
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|
|
|
|
describe("sqlCompletion table targets", () => {
|
|
it("does not suggest aliases while completing an empty FROM target before LIMIT", () => {
|
|
const sql = "SELECT *\nFROM \nLIMIT 100;";
|
|
const cursor = "SELECT *\nFROM ".length;
|
|
const items = buildSqlCompletionItems(sql, cursor, {
|
|
tables: [{ name: "users", type: "table" }],
|
|
columnsByTable: new Map(),
|
|
});
|
|
|
|
expect(items.some((item) => item.type === "snippet" && item.detail === "alias for LIMIT")).toBe(false);
|
|
expect(items.some((item) => item.type === "table" && item.label === "users")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("sqlCompletion table aliases", () => {
|
|
it("applies generated aliases to table completions when enabled", () => {
|
|
const sql = "SELECT * FROM ord";
|
|
const items = buildSqlCompletionItems(sql, sql.length, {
|
|
tables: [{ name: "order_items", type: "table" }],
|
|
columnsByTable: new Map(),
|
|
autoAliasTables: true,
|
|
});
|
|
|
|
const table = items.find((item) => item.label === "order_items" && item.type === "table");
|
|
expect(table?.apply).toBe("order_items AS oi");
|
|
});
|
|
|
|
it("omits AS from Oracle table alias completions", () => {
|
|
const sql = "SELECT * FROM ord";
|
|
const items = buildSqlCompletionItems(sql, sql.length, {
|
|
tables: [{ name: "order_items", type: "table" }],
|
|
columnsByTable: new Map(),
|
|
databaseType: "oracle",
|
|
autoAliasTables: true,
|
|
});
|
|
|
|
const table = items.find((item) => item.label === "order_items" && item.type === "table");
|
|
expect(table?.apply).toBe("order_items oi");
|
|
});
|
|
|
|
it("keeps plain table completions when generated aliases are disabled", () => {
|
|
const sql = "SELECT * FROM ord";
|
|
const items = buildSqlCompletionItems(sql, sql.length, {
|
|
tables: [{ name: "order_items", type: "table" }],
|
|
columnsByTable: new Map(),
|
|
autoAliasTables: false,
|
|
});
|
|
|
|
const table = items.find((item) => item.label === "order_items" && item.type === "table");
|
|
expect(table?.apply).toBe("order_items");
|
|
});
|
|
|
|
it("omits AS from Oracle alias suggestions", () => {
|
|
const sql = "SELECT * FROM order_items ";
|
|
const items = buildSqlCompletionItems(sql, sql.length, {
|
|
tables: [{ name: "order_items", type: "table" }],
|
|
columnsByTable: new Map(),
|
|
databaseType: "oracle",
|
|
});
|
|
|
|
const alias = items.find((item) => item.type === "snippet" && item.detail === "alias for order_items");
|
|
expect(alias?.apply).toBe("oi ");
|
|
});
|
|
|
|
it("uses a numbered alias when the generated table alias already exists", () => {
|
|
const sql = "SELECT * FROM order_items oi JOIN ord";
|
|
const items = buildSqlCompletionItems(sql, sql.length, {
|
|
tables: [{ name: "order_items", type: "table" }],
|
|
columnsByTable: new Map(),
|
|
autoAliasTables: true,
|
|
});
|
|
|
|
const table = items.find((item) => item.label === "order_items" && item.type === "table");
|
|
expect(table?.apply).toBe("order_items AS oi2");
|
|
});
|
|
|
|
it("applies generated aliases in comma-separated FROM table lists", () => {
|
|
const sql = "SELECT * FROM users u, ord";
|
|
const items = buildSqlCompletionItems(sql, sql.length, {
|
|
tables: [{ name: "order_items", type: "table" }],
|
|
columnsByTable: new Map(),
|
|
autoAliasTables: true,
|
|
});
|
|
|
|
const table = items.find((item) => item.label === "order_items" && item.type === "table");
|
|
expect(table?.apply).toBe("order_items AS oi");
|
|
});
|
|
|
|
it("does not apply generated aliases to non-query table completions", () => {
|
|
const sql = "INSERT INTO ord";
|
|
const items = buildSqlCompletionItems(sql, sql.length, {
|
|
tables: [{ name: "order_items", type: "table" }],
|
|
columnsByTable: new Map(),
|
|
autoAliasTables: true,
|
|
});
|
|
|
|
const table = items.find((item) => item.label === "order_items" && item.type === "table");
|
|
expect(table?.apply).toBe("order_items");
|
|
});
|
|
});
|
|
|
|
describe("sqlCompletion scoped context classification", () => {
|
|
it("classifies JOIN table contexts", () => {
|
|
const sql = "SELECT * FROM users u JOIN ";
|
|
const context = getSqlCompletionContext(sql, sql.length);
|
|
|
|
expect(context.contextKind).toBe("join");
|
|
expect(context.suggestTables).toBe(true);
|
|
expect(context.exclusiveTableSuggestions).toBe(true);
|
|
});
|
|
|
|
it("classifies alias-qualified column contexts", () => {
|
|
const sql = "SELECT * FROM users u WHERE u.";
|
|
const context = getSqlCompletionContext(sql, sql.length);
|
|
|
|
expect(context.contextKind).toBe("alias_column");
|
|
expect(context.qualifier).toBe("u");
|
|
expect(context.suggestColumns).toBe(true);
|
|
});
|
|
|
|
it("keeps alias-qualified column context after select-list subqueries", () => {
|
|
const sql = `
|
|
SELECT
|
|
p.id,
|
|
p.create_user_name 'creator',
|
|
(SELECT t.\`code\` FROM sys_user t WHERE t.user_id = p.apply_user_id) 'creator_code',
|
|
p.
|
|
FROM sys_process p
|
|
LIMIT 10
|
|
`;
|
|
const cursor = sql.indexOf("p.\n FROM");
|
|
const context = getSqlCompletionContext(sql, cursor + 2);
|
|
|
|
expect(context.contextKind).toBe("alias_column");
|
|
expect(context.qualifier).toBe("p");
|
|
expect(context.suggestTables).toBe(false);
|
|
expect(context.exclusiveTableSuggestions).toBe(false);
|
|
expect(context.suggestColumns).toBe(true);
|
|
});
|
|
|
|
it("suggests alias columns after select-list subqueries instead of tables", () => {
|
|
const sql = `
|
|
SELECT
|
|
p.id,
|
|
p.create_user_name 'creator',
|
|
(SELECT t.\`code\` FROM sys_user t WHERE t.user_id = p.apply_user_id) 'creator_code',
|
|
p.
|
|
FROM sys_process p
|
|
LIMIT 10
|
|
`;
|
|
const cursor = sql.indexOf("p.\n FROM") + 2;
|
|
const items = buildSqlCompletionItems(sql, cursor, {
|
|
dialect: "mysql",
|
|
tables: [
|
|
{ name: "act_evt_log", type: "table" },
|
|
{ name: "sys_process", type: "table" },
|
|
{ name: "sys_user", type: "table" },
|
|
],
|
|
columnsByTable: new Map([
|
|
[
|
|
"sys_process",
|
|
[
|
|
{ name: "id", table: "sys_process" },
|
|
{ name: "create_user_name", table: "sys_process" },
|
|
{ name: "apply_user_id", table: "sys_process" },
|
|
],
|
|
],
|
|
["sys_user", [{ name: "code", table: "sys_user" }]],
|
|
]),
|
|
});
|
|
|
|
const columnLabels = items.filter((item) => item.type === "column").map((item) => item.label);
|
|
expect(columnLabels).toEqual(expect.arrayContaining(["id", "create_user_name", "apply_user_id"]));
|
|
expect(items[0]?.type).toBe("column");
|
|
expect(items.some((item) => item.type === "table")).toBe(false);
|
|
expect(items.some((item) => item.type === "keyword")).toBe(false);
|
|
});
|
|
|
|
it("classifies unqualified WHERE field input as column context", () => {
|
|
const sql = "SELECT * FROM A1User WHERE userc";
|
|
const context = getSqlCompletionContext(sql, sql.length);
|
|
|
|
expect(context.contextKind).toBe("column");
|
|
expect(context.prefix).toBe("userc");
|
|
expect(context.referencedTables).toEqual(expect.arrayContaining([expect.objectContaining({ name: "A1User" })]));
|
|
expect(context.suggestColumns).toBe(true);
|
|
expect(context.suggestRoutines).toBe(false);
|
|
});
|
|
|
|
it("auto-opens column completion after WHERE whitespace before LIMIT", () => {
|
|
const sql = "SELECT *\nFROM t_0001 AS t0 WHERE \nLIMIT 100;";
|
|
const cursor = "SELECT *\nFROM t_0001 AS t0 WHERE ".length;
|
|
const context = getSqlCompletionContext(sql, cursor);
|
|
|
|
expect(context.contextKind).toBe("column");
|
|
expect(context.prefix).toBe("");
|
|
expect(context.referencedTables).toEqual(expect.arrayContaining([expect.objectContaining({ name: "t_0001", alias: "t0" })]));
|
|
expect(context.suggestColumns).toBe(true);
|
|
expect(shouldAutoOpenSqlCompletion(sql, cursor)).toBe(true);
|
|
});
|
|
|
|
it("classifies CALL routine contexts", () => {
|
|
const sql = "CALL usp_";
|
|
const context = getSqlCompletionContext(sql, sql.length);
|
|
|
|
expect(context.contextKind).toBe("exec");
|
|
expect(context.suggestRoutines).toBe(true);
|
|
expect(context.exclusiveRoutineSuggestions).toBe(true);
|
|
});
|
|
|
|
it("classifies INSERT column-list contexts", () => {
|
|
const sql = "INSERT INTO dbo.Users (";
|
|
const context = getSqlCompletionContext(sql, sql.length);
|
|
|
|
expect(context.contextKind).toBe("column");
|
|
expect(context.insertSchema).toBe("dbo");
|
|
expect(context.insertTable).toBe("Users");
|
|
expect(context.exclusiveColumnSuggestions).toBe(true);
|
|
});
|
|
|
|
it("classifies UPDATE SET column contexts", () => {
|
|
const sql = "UPDATE dbo.Users SET ";
|
|
const context = getSqlCompletionContext(sql, sql.length);
|
|
|
|
expect(context.contextKind).toBe("column");
|
|
expect(context.updateTarget).toEqual({ schema: "dbo", table: "Users" });
|
|
expect(context.suggestColumns).toBe(true);
|
|
});
|
|
|
|
it("extracts statement-local table aliases", () => {
|
|
const sql = "SELECT * FROM dbo.Users u JOIN Orders AS o ON o.user_id = u.id WHERE u.";
|
|
const context = getSqlCompletionContext(sql, sql.length);
|
|
|
|
expect(context.referencedTables).toEqual(expect.arrayContaining([expect.objectContaining({ schema: "dbo", name: "Users", alias: "u" }), expect.objectContaining({ name: "Orders", alias: "o" })]));
|
|
});
|
|
|
|
it("treats schema-qualified table prefixes in FROM as table completion input", () => {
|
|
const sql = "SELECT * FROM dws_game_sdk_base.di";
|
|
const context = getSqlCompletionContext(sql, sql.length);
|
|
|
|
expect(context.qualifier).toBe("dws_game_sdk_base");
|
|
expect(context.prefix).toBe("di");
|
|
expect(context.suggestTables).toBe(true);
|
|
expect(context.exclusiveTableSuggestions).toBe(true);
|
|
expect(context.suggestColumns).toBe(true);
|
|
});
|
|
|
|
it("exposes CTEs as table-like referenced tables", () => {
|
|
const sql = "WITH recent_orders(id, total) AS (SELECT id, total FROM orders) SELECT * FROM recent_orders ro WHERE ro.";
|
|
const context = getSqlCompletionContext(sql, sql.length);
|
|
|
|
expect(context.referencedTables).toEqual(expect.arrayContaining([expect.objectContaining({ name: "recent_orders", columns: ["id", "total"] }), expect.objectContaining({ name: "recent_orders", alias: "ro" })]));
|
|
});
|
|
|
|
it("extracts subquery aliases and projected columns", () => {
|
|
const sql = "SELECT * FROM (SELECT id, name AS user_name FROM users) sq WHERE sq.";
|
|
const context = getSqlCompletionContext(sql, sql.length);
|
|
|
|
expect(context.referencedTables).toEqual(expect.arrayContaining([expect.objectContaining({ name: "sq", alias: "sq", columns: ["id", "user_name"] })]));
|
|
});
|
|
|
|
it("suggests columns for cross-database qualified table references", () => {
|
|
const sql = "SELECT * FROM current_orders WHERE reporting.orders.";
|
|
const context = getSqlCompletionContext(sql, sql.length);
|
|
const items = buildSqlCompletionItems(sql, sql.length, {
|
|
tables: [],
|
|
columnsByTable: new Map([
|
|
[
|
|
"reporting.orders",
|
|
[
|
|
{ name: "id", table: "orders", schema: "reporting", dataType: "int" },
|
|
{ name: "status", table: "orders", schema: "reporting", dataType: "varchar" },
|
|
],
|
|
],
|
|
["archive.orders", [{ name: "archived_at", table: "orders", schema: "archive", dataType: "datetime" }]],
|
|
]),
|
|
});
|
|
|
|
expect(context.qualifier).toBe("reporting.orders");
|
|
expect(context.qualifierParts).toEqual(["reporting", "orders"]);
|
|
expect(context.suggestColumns).toBe(true);
|
|
expect(items).toEqual(expect.arrayContaining([expect.objectContaining({ label: "id", type: "column" }), expect.objectContaining({ label: "status", type: "column" })]));
|
|
expect(items.some((item) => item.label === "archived_at")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("sqlCompletion scoped metadata ranking", () => {
|
|
it("ranks exact and prefix table matches ahead of contains/fuzzy matches", () => {
|
|
const sql = "SELECT * FROM Temp";
|
|
const items = buildSqlCompletionItems(sql, sql.length, {
|
|
dialect: "sqlserver",
|
|
tables: [
|
|
{ name: "ArchiveTempTable", schema: "dbo", type: "table" },
|
|
{ name: "TempAudit", schema: "dbo", type: "table" },
|
|
{ name: "Temp", schema: "dbo", type: "table" },
|
|
{ name: "Template", schema: "dbo", type: "table" },
|
|
],
|
|
columnsByTable: new Map(),
|
|
}).filter((item) => item.type === "table");
|
|
|
|
expect(items.map((item) => item.label).slice(0, 3)).toEqual(["Temp", "Template", "TempAudit"]);
|
|
expect(items.some((item) => item.label === "ArchiveTempTable")).toBe(true);
|
|
});
|
|
|
|
it("keeps large table catalogs bounded", () => {
|
|
const tables = Array.from({ length: 500 }, (_, index) => ({ name: `TempTable_${String(index).padStart(3, "0")}`, schema: "dbo", type: "table" as const }));
|
|
const sql = "SELECT * FROM Temp";
|
|
const items = buildSqlCompletionItems(sql, sql.length, { dialect: "sqlserver", tables, columnsByTable: new Map() }).filter((item) => item.type === "table");
|
|
|
|
expect(items.length).toBeLessThanOrEqual(200);
|
|
expect(items[0]?.label).toBe("TempTable_000");
|
|
});
|
|
|
|
it("ranks real Oracle tables before built-in table functions in FROM contexts", () => {
|
|
const sql = "SELECT * FROM ";
|
|
const items = buildSqlCompletionItems(sql, sql.length, {
|
|
databaseType: "oracle",
|
|
tables: [{ name: "ORDERS_10K", schema: "DBX_TEST", type: "table" }],
|
|
columnsByTable: new Map(),
|
|
});
|
|
|
|
expect(items.findIndex((item) => item.label === "ORDERS_10K")).toBeLessThan(items.findIndex((item) => item.label === "TABLE"));
|
|
});
|
|
});
|