import { strict as assert } from "node:assert"; import test from "node:test"; import { buildSqlCompletionItems, getSqlFunctionSignatureHelp, getSqlCompletionResultValidFor, shouldAutoOpenSqlCompletion, extractCteDefinitions, getSqlCompletionContext, recordCompletionSelection, type SqlCompletionColumn, type SqlCompletionTable, } from "../../apps/desktop/src/lib/sqlCompletion.ts"; 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([ [ "public.users", [ { name: "id", table: "users", schema: "public", dataType: "bigint" }, { name: "name", table: "users", schema: "public", dataType: "varchar" }, { name: "email", table: "users", schema: "public", dataType: "varchar" }, ], ], [ "public.orders", [ { name: "id", table: "orders", schema: "public", dataType: "bigint" }, { name: "user_id", table: "orders", schema: "public", dataType: "bigint" }, { name: "status", table: "orders", schema: "public", dataType: "varchar" }, ], ], ]); test("suggests SQL keywords for generic keyword input", () => { const items = buildSqlCompletionItems("sel", 3, { tables, columnsByTable, }); const keyword = items.find((item) => item.type === "keyword" && item.label === "SELECT"); assert.ok(keyword); assert.equal(keyword.type, "keyword"); }); test("suggests matching table names after FROM", () => { const sql = "select * from us"; const items = buildSqlCompletionItems(sql, sql.length, { tables, columnsByTable, }); assert.deepEqual( items.slice(0, 2).map((item) => item.label), ["users", "user_profiles"], ); }); test("ranks prefix matches above substring matches for table names", () => { const sql = "select * from user"; const items = buildSqlCompletionItems(sql, sql.length, { tables, columnsByTable, }); assert.deepEqual( items.filter((item) => item.type === "table").map((item) => item.label), ["users", "user_profiles"], ); }); test("suggests columns for an explicit alias qualifier", () => { const sql = "select u. from public.users u"; const cursor = "select u.".length; const items = buildSqlCompletionItems(sql, cursor, { tables, columnsByTable, }); const columnItems = items.filter((item) => item.type === "column"); assert.deepEqual( columnItems.map((item) => item.label), ["id", "name", "email"], ); }); 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 [varchar]"]], ); }); 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 [varchar]"]], ); }); 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; const items = buildSqlCompletionItems(sql, cursor, { tables, columnsByTable, }); assert.equal(items[0]?.label, "name"); assert.equal(items[0]?.type, "column"); }); test("suggests tables after LEFT JOIN", () => { const sql = "select * from users left join us"; const items = buildSqlCompletionItems(sql, sql.length, { tables, columnsByTable, }); assert.ok(items.some((item) => item.label === "users" && item.type === "table")); assert.ok(items.some((item) => item.label === "user_profiles" && item.type === "table")); }); test("suggests tables after comma in FROM clause", () => { const sql = "select * from users, or"; const items = buildSqlCompletionItems(sql, sql.length, { tables, columnsByTable, }); assert.ok(items.some((item) => item.label === "orders" && item.type === "table")); }); test("suggests keywords when typing without context", () => { const sql = "us"; const items = buildSqlCompletionItems(sql, sql.length, { tables, columnsByTable, }); assert.ok(items.some((item) => item.type === "keyword" && item.label === "USING")); }); test("suggests only matching table names after FROM object input", () => { const sql = "select * from us"; const items = buildSqlCompletionItems(sql, sql.length, { tables, columnsByTable, }); const tableItems = items.filter((item) => item.type === "table"); assert.ok(tableItems.length > 0); assert.deepEqual( tableItems.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, }); const tableItems = items.filter((item) => item.type === "table"); assert.ok(tableItems.length > 0); assert.deepEqual( tableItems.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, }); const tableItems = items.filter((item) => item.type === "table"); assert.deepEqual( tableItems.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, }); const tableItems = items.filter((item) => item.type === "table"); assert.ok(tableItems.length > 0); assert.deepEqual( tableItems.map((item) => item.label), ["users", "user_profiles"], ); }); test("suggests SQL Server IF keyword for conditional DDL", () => { const sql = "DROP TABLE I"; const items = buildSqlCompletionItems(sql, sql.length, { tables, columnsByTable, }); assert.ok(items.some((item) => item.type === "keyword" && item.label === "IF")); }); test("suggests SQL Server IIF and CHOOSE scalar functions", () => { const iifItems = buildSqlCompletionItems("SELECT II", "SELECT II".length, { tables, columnsByTable, }); const chooseItems = buildSqlCompletionItems("SELECT CHO", "SELECT CHO".length, { tables, columnsByTable, }); assert.ok(iifItems.some((item) => item.label === "IIF"), "IIF should appear in completion"); assert.ok(chooseItems.some((item) => item.label === "CHOOSE"), "CHOOSE should appear in completion"); }); test("suggests SQL Server data types in CREATE TABLE column definitions", () => { const sql = "CREATE TABLE dbo.jobs (id "; const items = buildSqlCompletionItems(sql, sql.length, { tables, columnsByTable, }); assert.ok(items.some((item) => item.type === "keyword" && item.label === "INT")); assert.ok(items.some((item) => item.type === "keyword" && item.label === "BIGINT")); assert.ok(items.some((item) => item.type === "keyword" && item.label === "NVARCHAR")); }); test("does not auto-open completion after structural punctuation", () => { for (const sql of ["select count(*)", "select * from users;", "select * from users,"]) { assert.equal(shouldAutoOpenSqlCompletion(sql, sql.length), false, sql); } }); test("auto-opens completion after word characters and explicit dot qualifiers", () => { for (const sql of ["sel", "select * from us", "select u."]) { assert.equal(shouldAutoOpenSqlCompletion(sql, sql.length), true, sql); } }); test("auto-opens table completion immediately after FROM context whitespace", () => { for (const sql of ["select * from ", "select * from users join ", "select * from users, "]) { assert.equal(shouldAutoOpenSqlCompletion(sql, sql.length), true, sql); } }); test("suggests table names for empty FROM context prefix", () => { const items = buildSqlCompletionItems("select * from ", "select * from ".length, { tables, columnsByTable, }); assert.deepEqual( items.slice(0, 4).map((item) => [item.label, item.type]), [ ["users", "table"], ["user_profiles", "table"], ["orders", "table"], ["ticket_summary", "table"], ], ); }); test("suggests matching table names for partial table input", () => { const items = buildSqlCompletionItems("select * from ihli", "select * from ihli".length, { tables: [{ name: "ihli_data", schema: "public", type: "table" }], columnsByTable, }); const tableItems = items.filter((item) => item.type === "table"); assert.deepEqual( tableItems.map((item) => [item.label, item.type, item.detail]), [["ihli_data", "table", "public.ihli_data"]], ); }); test("does not reuse table completion results across typed prefixes", () => { const validFor = getSqlCompletionResultValidFor("select * from ", "select * from ".length); assert.equal(validFor, undefined); }); test("auto-opens completion after ON whitespace for join conditions", () => { const sql = "select * from public.users u join public.orders o on "; assert.equal(shouldAutoOpenSqlCompletion(sql, sql.length), true); }); test("limits table suggestions for large schemas after filtering by prefix", () => { const largeTables: SqlCompletionTable[] = Array.from({ length: 500 }, (_, index) => ({ name: `erp_invoice_${String(index).padStart(4, "0")}`, schema: "dbo", type: "table", })); const sql = "select * from erp_invoice_"; const items = buildSqlCompletionItems(sql, sql.length, { tables: largeTables, columnsByTable, }); const tableItems = items.filter((item) => item.type === "table"); assert.equal(tableItems.length, 200); assert.equal(tableItems[0]?.label, "erp_invoice_0000"); assert.equal(tableItems.at(-1)?.label, "erp_invoice_0199"); }); test("suggests SQL snippets for common abbreviations", () => { const items = buildSqlCompletionItems("sel", 3, { tables, columnsByTable, }); const snippet = items.find((item) => item.type === "snippet" && item.label === "select *"); assert.ok(snippet); assert.equal(snippet.apply, "SELECT *\nFROM table\nLIMIT 100;"); }); test("suggests DATE_FORMAT as parameter snippet", () => { const sql = "select date_"; const items = buildSqlCompletionItems(sql, sql.length, { tables, columnsByTable, }); const snippet = items.find((item) => item.type === "function" && item.label === "DATE_FORMAT"); assert.ok(snippet); assert.equal(snippet.apply, "DATE_FORMAT(${date}, ${format})"); }); test("matches alias qualifier case-insensitively", () => { const sql = "select O. from public.orders o"; const cursor = "select O.".length; const items = buildSqlCompletionItems(sql, cursor, { tables, columnsByTable, }); const columnItems = items.filter((item) => item.type === "column"); assert.deepEqual( columnItems.map((item) => item.label), ["id", "user_id", "status"], ); }); test("suggests referenced columns after ORDER BY", () => { const sql = "select name from public.users u order by na"; const items = buildSqlCompletionItems(sql, sql.length, { tables, columnsByTable, }); assert.equal(items[0]?.label, "name"); assert.equal(items[0]?.type, "column"); }); test("prioritizes select aliases in ORDER BY completion", () => { const sql = "select u.name as display_name, count(*) order_count from public.users u order by "; const items = buildSqlCompletionItems(sql, sql.length, { tables, columnsByTable, }); assert.deepEqual( items.slice(0, 2).map((item) => [item.label, item.detail]), [ ["display_name", "SELECT alias"], ["order_count", "SELECT alias"], ], ); }); test("prioritizes select aliases in GROUP BY completion", () => { const sql = "select u.name as display_name from public.users u group by "; const items = buildSqlCompletionItems(sql, sql.length, { tables, columnsByTable, }); assert.equal(items[0]?.label, "display_name"); assert.equal(items[0]?.detail, "SELECT alias"); }); test("suggests likely join condition snippets after ON", () => { const sql = "select * from public.users u join public.orders o on "; const items = buildSqlCompletionItems(sql, sql.length, { tables, columnsByTable, }); const joinCondition = items.find((item) => item.type === "snippet" && item.label === "u.id = o.user_id"); assert.ok(joinCondition); assert.equal(joinCondition.apply, "u.id = o.user_id"); }); test("suggests likely join condition snippets when joined table owns the id column", () => { const sql = "select * from public.orders o join public.users u on "; const items = buildSqlCompletionItems(sql, sql.length, { tables, columnsByTable, }); const joinCondition = items.find((item) => item.type === "snippet" && item.label === "o.user_id = u.id"); assert.ok(joinCondition); assert.equal(joinCondition.apply, "o.user_id = u.id"); }); test("returns function signature help inside function arguments", () => { const sql = "select date_format(created_at, "; const signature = getSqlFunctionSignatureHelp(sql, sql.length); assert.deepEqual(signature, { name: "DATE_FORMAT", signature: "DATE_FORMAT(date, format)", activeParameter: 1, parameters: ["date", "format"], }); }); test("returns null signature help outside function calls", () => { assert.equal(getSqlFunctionSignatureHelp("select created_at from users", "select created_at".length), null); }); // --- CTE support --- test("extracts CTE names from WITH clause", () => { const ctes = extractCteDefinitions("WITH recent_orders AS (SELECT id FROM orders) SELECT * FROM recent_orders"); assert.equal(ctes.length, 1); assert.equal(ctes[0]?.name, "recent_orders"); }); test("extracts CTE columns from SELECT body", () => { const ctes = extractCteDefinitions("WITH cte AS (SELECT id, name, status FROM users) SELECT * FROM cte"); assert.equal(ctes.length, 1); assert.deepEqual(ctes[0]?.columns, ["id", "name", "status"]); }); test("extracts CTE explicit column list", () => { const ctes = extractCteDefinitions("WITH cte (col1, col2) AS (SELECT 1, 2) SELECT * FROM cte"); assert.equal(ctes.length, 1); assert.deepEqual(ctes[0]?.columns, ["col1", "col2"]); }); test("extracts multiple CTEs", () => { const ctes = extractCteDefinitions( "WITH first AS (SELECT id FROM users), second AS (SELECT id FROM orders) SELECT * FROM first JOIN second", ); assert.equal(ctes.length, 2); assert.equal(ctes[0]?.name, "first"); assert.equal(ctes[1]?.name, "second"); }); test("handles WITH RECURSIVE", () => { const ctes = extractCteDefinitions( "WITH RECURSIVE tree AS (SELECT id, parent_id FROM categories UNION ALL SELECT c.id, c.parent_id FROM categories c JOIN tree t ON c.parent_id = t.id) SELECT * FROM tree", ); assert.equal(ctes.length, 1); assert.equal(ctes[0]?.name, "tree"); }); test("adds CTE tables to referenced tables in context", () => { const sql = "WITH cte AS (SELECT id, name FROM users) SELECT * FROM cte"; const context = getSqlCompletionContext(sql, sql.length); const cteRef = context.referencedTables.find((t) => t.name.toLowerCase() === "cte"); assert.ok(cteRef); assert.ok(cteRef.columns); assert.ok(cteRef.columns!.includes("id")); assert.ok(cteRef.columns!.includes("name")); }); // --- INSERT column list detection --- test("detects INSERT INTO column list context", () => { const context = getSqlCompletionContext("INSERT INTO users (", "INSERT INTO users (".length); assert.equal(context.insertTable, "users"); assert.equal(context.exclusiveColumnSuggestions, true); }); test("detects INSERT INTO with schema-qualified table", () => { const context = getSqlCompletionContext("INSERT INTO public.users (", "INSERT INTO public.users (".length); assert.equal(context.insertTable, "users"); assert.equal(context.insertSchema, "public"); }); test("suggests columns for INSERT INTO target table", () => { const items = buildSqlCompletionItems("INSERT INTO users (", "INSERT INTO users (".length, { tables, columnsByTable, }); const columnItems = items.filter((item) => item.type === "column"); assert.ok(columnItems.length >= 3); assert.ok(columnItems.some((item) => item.label === "id")); assert.ok(columnItems.some((item) => item.label === "name")); assert.ok(columnItems.some((item) => item.label === "email")); }); // --- Column data type in detail --- test("shows column data type in detail", () => { const items = buildSqlCompletionItems("select id from public.users u where u.", "select id from public.users u where u.".length, { tables, columnsByTable, }); const emailColumn = items.find((item) => item.label === "email"); assert.ok(emailColumn); assert.ok(emailColumn.detail!.includes("[varchar]")); }); test("key columns get priority boost in column suggestions", () => { const items = buildSqlCompletionItems("select from public.users u", "select ".length, { tables, columnsByTable, }); const columns = items.filter((item) => item.type === "column"); // id column may be qualified as "users.id" if duplicate exists across tables const idItem = columns.find((item) => item.label === "users.id" || item.label === "id"); const nameItem = columns.find((item) => item.label === "name"); assert.ok(idItem); assert.ok(nameItem); assert.ok(idItem.boost > nameItem.boost, "id column should have higher boost than name"); }); // --- Schema name completion --- test("suggests schema names alongside tables in FROM context", () => { const items = buildSqlCompletionItems("select * from ", "select * from ".length, { tables, columnsByTable, schemas: ["public", "private", "audit"], }); const schemaItems = items.filter((item) => item.type === "schema"); assert.equal(schemaItems.length, 3); assert.ok(schemaItems.some((item) => item.label === "public")); assert.equal(schemaItems[0]?.apply, "public."); }); test("schema items include apply value with trailing dot", () => { const items = buildSqlCompletionItems("select * from pub", "select * from pub".length, { tables, columnsByTable, schemas: ["public"], }); const schemaItems = items.filter((item) => item.type === "schema"); assert.equal(schemaItems.length, 1); assert.equal(schemaItems[0]?.apply, "public."); }); // --- Quoted identifier fix --- test("handles quoted identifiers with dots in splitQualifiedName", () => { const sql = "select * from "; const context = getSqlCompletionContext(sql, sql.length); // Verify context handles quoted identifiers — just ensure no crash assert.ok(context); }); // --- Snippet ranking --- test("snippet boost uses label when label matches prefix better than snippet prefix", () => { const items = buildSqlCompletionItems("select", "select".length, { tables, columnsByTable, }); const snippet = items.find((item) => item.type === "snippet" && item.label === "select *"); assert.ok(snippet); // Snippet should appear in top results even when typing full keyword const topFive = items.slice(0, 5); assert.ok(topFive.some((item) => item.label === "select *"), "select * snippet should be in top 5"); }); // --- Context-aware keyword filtering --- test("hides DDL keywords in SELECT statement context", () => { const items = buildSqlCompletionItems("select * from users where ", "select * from users where ".length, { tables, columnsByTable, }); const keywords = items.filter((item) => item.type === "keyword"); assert.ok(!keywords.some((item) => item.label === "CREATE"), "CREATE should not appear in SELECT context"); assert.ok(!keywords.some((item) => item.label === "ALTER"), "ALTER should not appear in SELECT context"); assert.ok(!keywords.some((item) => item.label === "DROP"), "DROP should not appear in SELECT context"); assert.ok(keywords.some((item) => item.label === "AND"), "AND should appear in SELECT context"); assert.ok(keywords.some((item) => item.label === "OR"), "OR should appear in SELECT context"); }); test("shows DDL keywords in CREATE TABLE context", () => { const sql = "create table "; const items = buildSqlCompletionItems(sql, sql.length, { tables, columnsByTable, }); // In CREATE context, data types should appear assert.ok(items.some((item) => item.label === "INT" || item.label === "BIGINT"), "data types should appear in CREATE"); }); test("filters data type keywords out of SELECT context", () => { const sql = "select "; const items = buildSqlCompletionItems(sql, sql.length, { tables: [{ name: "varchar_test", type: "table" }], columnsByTable, }); // Should not suggest VARCHAR as a keyword in SELECT context const varcharAsKeyword = items.find((item) => item.type === "keyword" && item.label === "VARCHAR"); assert.ok(!varcharAsKeyword, "VARCHAR should not appear as keyword in SELECT context"); }); // --- Qualified column names for duplicates --- test("shows qualified column names when multiple tables share column name", () => { const sql = "select from public.users u join public.orders o on u.id = o.user_id"; const items = buildSqlCompletionItems(sql, "select ".length, { tables, columnsByTable, }); const columns = items.filter((item) => item.type === "column"); assert.ok(columns.some((item) => item.label === "users.id"), "should show users.id"); assert.ok(columns.some((item) => item.label === "orders.id"), "should show orders.id"); assert.ok(columns.some((item) => item.label === "name"), "unique name should remain unqualified"); assert.ok(columns.some((item) => item.label === "user_id"), "unique user_id should remain unqualified"); }); // --- Window function OVER() --- test("suggests ROW_NUMBER with OVER clause", () => { const items = buildSqlCompletionItems("select row_", "select row_".length, { tables, columnsByTable, }); const rn = items.find((item) => item.label === "ROW_NUMBER" && item.type === "function")!; assert.ok(rn); assert.ok(rn.apply!.includes("OVER"), "ROW_NUMBER should include OVER()"); assert.ok(rn.apply!.includes("PARTITION BY"), "ROW_NUMBER should include PARTITION BY"); }); test("suggests RANK with OVER clause", () => { const items = buildSqlCompletionItems("select ra", "select ra".length, { tables, columnsByTable, }); const rank = items.find((item) => item.label === "RANK"); assert.ok(rank); assert.ok(rank.apply!.includes("OVER"), "RANK should include OVER()"); }); // --- Subquery alias support --- test("extracts subquery alias as referenced table", () => { const sql = "select * from (select id, name from users) sub"; const context = getSqlCompletionContext(sql, sql.length); const subRef = context.referencedTables.find((t) => t.name === "sub"); assert.ok(subRef, "subquery alias should be in referenced tables"); }); test("extracts subquery alias columns", () => { const sql = "select s. from (select id, name from users) s"; const context = getSqlCompletionContext(sql, "select s.".length); const sqRef = context.referencedTables.find((t) => t.name === "s"); assert.ok(sqRef); assert.ok(sqRef.columns!.includes("id")); assert.ok(sqRef.columns!.includes("name")); }); // --- Table alias suggestions --- test("suggests table alias after FROM table", () => { const items = buildSqlCompletionItems("select * from users ", "select * from users ".length, { tables, columnsByTable, }); const aliasItem = items.find((item) => item.type === "snippet" && item.detail?.includes("alias for")); assert.ok(aliasItem, "should suggest alias for table"); assert.ok(aliasItem!.apply!.includes("AS"), "alias apply should include AS"); }); // --- CASE snippet --- test("suggests CASE WHEN snippet", () => { const items = buildSqlCompletionItems("case", "case".length, { tables, columnsByTable, }); const caseSnippet = items.find((item) => item.type === "snippet" && item.label === "case when"); assert.ok(caseSnippet); assert.ok(caseSnippet.apply!.includes("CASE"), "should include CASE"); assert.ok(caseSnippet.apply!.includes("WHEN"), "should include WHEN"); assert.ok(caseSnippet.apply!.includes("THEN"), "should include THEN"); assert.ok(caseSnippet.apply!.includes("END"), "should include END"); }); // --- Expanded function signatures --- test("suggests REGEXP_REPLACE with parameters", () => { const items = buildSqlCompletionItems("select regexp_", "select regexp_".length, { tables, columnsByTable, }); const fn = items.find((item) => item.label === "REGEXP_REPLACE" && item.type === "function"); assert.ok(fn); assert.ok(fn.apply!.includes("pattern"), "should include pattern param"); }); test("suggests JSON_EXTRACT with parameters", () => { const items = buildSqlCompletionItems("select json_", "select json_".length, { tables, columnsByTable, }); const fn = items.find((item) => item.label === "JSON_EXTRACT" && item.type === "function"); assert.ok(fn); assert.ok(fn.apply!.includes("json"), "should include json param"); }); // --- Smart GROUP BY suggestions --- test("boosts non-aggregated SELECT columns in GROUP BY context", () => { const sql = "select u.name, count(o.id) from public.users u join public.orders o on u.id = o.user_id group by "; const items = buildSqlCompletionItems(sql, sql.length, { tables, columnsByTable, }); const nonAggItem = items.find((item) => item.label === "name" && item.detail?.includes("non-aggregated")); assert.ok(nonAggItem, "non-aggregated column should appear with GROUP BY hint"); }); test("does not boost aggregated columns in GROUP BY context", () => { const sql = "select u.name, count(o.id) from public.users u join public.orders o on u.id = o.user_id group by "; const items = buildSqlCompletionItems(sql, sql.length, { tables, columnsByTable, }); const aggHint = items.find((item) => item.detail?.includes("non-aggregated") && item.label === "id"); // "id" appears in count(o.id) — it's inside an aggregate, so shouldn't get the non-aggregated boost // Note: id from users is not aggregated and appears as u.name is not aliased assert.ok(!aggHint, "id inside aggregate should not get non-aggregated boost"); }); test("getSqlCompletionContext returns nonAggregatedSelectColumns", () => { const sql = "select name, count(id) from users group by "; const context = getSqlCompletionContext(sql, sql.length); assert.ok(context.isGroupBy, "should detect GROUP BY context"); assert.ok(context.nonAggregatedSelectColumns.includes("name"), "name should be non-aggregated"); assert.ok(!context.nonAggregatedSelectColumns.includes("id"), "id inside COUNT should not be non-aggregated"); }); // --- Better FK join inference --- test("suggests join condition for same FK column in both tables", () => { const colsWithFk = new Map([ ["public.authors", [ { name: "id", table: "authors", schema: "public", dataType: "bigint" }, { name: "publisher_id", table: "authors", schema: "public", dataType: "bigint" }, ]], ["public.books", [ { name: "id", table: "books", schema: "public", dataType: "bigint" }, { name: "publisher_id", table: "books", schema: "public", dataType: "bigint" }, ]], ]); const sql = "select * from public.authors a join public.books b on "; const items = buildSqlCompletionItems(sql, sql.length, { tables: [ { name: "authors", schema: "public", type: "table" }, { name: "books", schema: "public", type: "table" }, ], columnsByTable: colsWithFk, }); const fkJoin = items.find((item) => item.label === "a.publisher_id = b.publisher_id"); assert.ok(fkJoin, "should suggest join on shared FK column publisher_id"); }); test("suggests join condition for parent_id self-reference", () => { const colsWithParent = new Map([ ["public.categories", [ { name: "id", table: "categories", schema: "public", dataType: "bigint" }, { name: "parent_id", table: "categories", schema: "public", dataType: "bigint" }, { name: "name", table: "categories", schema: "public", dataType: "varchar" }, ]], ]); // Self-join const sql = "select * from public.categories c1 join public.categories c2 on "; const items = buildSqlCompletionItems(sql, sql.length, { tables: [ { name: "categories", schema: "public", type: "table" }, ], columnsByTable: colsWithParent, }); const parentJoin = items.find((item) => item.label === "c1.parent_id = c2.id" || item.label === "c2.parent_id = c1.id"); assert.ok(parentJoin, "should suggest parent_id = id for self-reference"); }); test("suggests join condition for created_by → id pattern", () => { const colsWithCreator = new Map([ ["public.users", [ { name: "id", table: "users", schema: "public", dataType: "bigint" }, { name: "name", table: "users", schema: "public", dataType: "varchar" }, ]], ["public.documents", [ { name: "id", table: "documents", schema: "public", dataType: "bigint" }, { name: "created_by", table: "documents", schema: "public", dataType: "bigint" }, ]], ]); const sql = "select * from public.users u join public.documents d on "; const items = buildSqlCompletionItems(sql, sql.length, { tables: [ { name: "users", schema: "public", type: "table" }, { name: "documents", schema: "public", type: "table" }, ], columnsByTable: colsWithCreator, }); const creatorJoin = items.find((item) => item.label === "u.id = d.created_by"); assert.ok(creatorJoin, "should suggest id = created_by join"); }); // --- Fuzzy matching --- test("fuzzy matches table names with character gaps", () => { const items = buildSqlCompletionItems("select * from usrs", "select * from usrs".length, { tables, columnsByTable, }); // "usrs" should fuzzy-match "users" (skip 'e') const tableItems = items.filter((item) => item.type === "table"); assert.ok(tableItems.some((item) => item.label === "users"), "should fuzzy-match users"); }); test("fuzzy matches columns with abbreviation pattern", () => { const sql = "select nm from public.users u"; const items = buildSqlCompletionItems(sql, "select nm".length, { tables, columnsByTable, }); // "nm" should fuzzy-match "name" assert.ok(items.some((item) => item.label === "name" && item.type === "column"), "should fuzzy-match name from 'nm'"); }); test("prefix matches still rank above fuzzy matches", () => { // Use "na" which is an exact prefix for "name" but also fuzzy-matches "ANALYZE" and others const sql = "select na from public.users u"; const items = buildSqlCompletionItems(sql, "select na".length, { tables, columnsByTable, }); // Prefix match "name" should be first assert.equal(items[0]?.label, "name"); }); // --- Type-aware comparison hints --- test("suggests NULL and IS NULL after comparison operator", () => { const sql = "select * from public.users u where u.name = "; const items = buildSqlCompletionItems(sql, sql.length, { tables, columnsByTable, }); assert.ok(items.some((item) => item.label === "NULL"), "should suggest NULL"); assert.ok(items.some((item) => item.label === "IS NULL"), "should suggest IS NULL"); }); test("suggests string snippet for varchar column after =", () => { const sql = "select * from public.users u where u.name = "; const items = buildSqlCompletionItems(sql, sql.length, { tables, columnsByTable, }); const strSnippet = items.find((item) => item.label === "''"); assert.ok(strSnippet, "should suggest string literal snippet for varchar column"); }); // --- SELECT * expansion --- test("shows SELECT * column expansion", () => { const sql = "select *"; const items = buildSqlCompletionItems(sql, sql.length, { tables, columnsByTable, }); const starItem = items.find((item) => item.label === "* → columns"); assert.ok(starItem, "should show column expansion for *"); }); // --- History-based ranking --- test("recordCompletionSelection boosts future ranking", () => { // Record a few selections of a specific table recordCompletionSelection("user_profiles", "table"); recordCompletionSelection("user_profiles", "table"); const items = buildSqlCompletionItems("select * from user", "select * from user".length, { tables, columnsByTable, }); const tableItems = items.filter((item) => item.type === "table"); // user_profiles should now rank higher than users due to history boost assert.equal(tableItems[0]?.label, "user_profiles"); });