dbx/tests/sqlCompletion.test.ts

193 lines
6.0 KiB
TypeScript

import { strict as assert } from "node:assert";
import test from "node:test";
import {
buildSqlCompletionItems,
shouldAutoOpenSqlCompletion,
type SqlCompletionColumn,
type SqlCompletionTable,
} from "../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" },
];
const columnsByTable = new Map<string, SqlCompletionColumn[]>([
[
"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,
});
assert.equal(items[0]?.label, "SELECT");
assert.equal(items[0]?.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("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 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("always includes keywords alongside table suggestions", () => {
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"));
});
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.type === "keyword" && item.label === "IIF"));
assert.ok(chooseItems.some((item) => item.type === "keyword" && item.label === "CHOOSE"));
});
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("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");
});