225 lines
9.1 KiB
TypeScript
225 lines
9.1 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
|
|
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("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("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("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"] })]));
|
|
});
|
|
});
|
|
|
|
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");
|
|
});
|
|
});
|