157 lines
9.6 KiB
TypeScript
157 lines
9.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { sqlSemanticCompletionScope, sqlSemanticLocalColumnsByTable, sqlSemanticProjectionAliasColumns } from "@/lib/sqlSemanticCompletion";
|
|
import { SQL_SEMANTIC_BASELINE_FIXTURES, sqlFixtureCursor } from "@/lib/sqlSemanticFixtures";
|
|
import { buildSqlSemanticModel } from "@/lib/sqlSemanticModel";
|
|
|
|
describe("sqlSemanticModel baseline fixtures", () => {
|
|
for (const fixture of SQL_SEMANTIC_BASELINE_FIXTURES) {
|
|
it(fixture.name, () => {
|
|
const { sql, cursor } = sqlFixtureCursor(fixture.sql);
|
|
const model = buildSqlSemanticModel(sql, cursor, { databaseType: fixture.databaseType });
|
|
const scope = sqlSemanticCompletionScope(model);
|
|
|
|
expect(model.statement.kind).toBe(fixture.expected.statementKind);
|
|
expect(model.cursorIntent.kind).toBe(fixture.expected.cursorKind);
|
|
expect(scope.kind).toBe(fixture.expected.completionScope);
|
|
expect(model.cursorIntent.prefix).toBe(fixture.expected.prefix);
|
|
expect(model.cursorIntent.qualifierParts).toEqual(fixture.expected.qualifierParts ?? []);
|
|
expect(model.cursorIntent.confidence).toBe(fixture.expected.confidence);
|
|
|
|
for (const expectedSource of fixture.expected.rowSources ?? []) {
|
|
const expectedObject = Object.fromEntries(Object.entries(expectedSource).filter(([, value]) => value !== undefined));
|
|
expect(model.rowSources).toEqual(expect.arrayContaining([expect.objectContaining(expectedObject)]));
|
|
}
|
|
|
|
if (fixture.expected.completionLabels) {
|
|
const labels = [...sqlSemanticLocalColumnsByTable(model).values()].flat().map((column) => column.name);
|
|
expect(labels).toEqual(expect.arrayContaining(fixture.expected.completionLabels));
|
|
}
|
|
});
|
|
}
|
|
|
|
it("does not mix row sources from inactive statements", () => {
|
|
const { sql, cursor } = sqlFixtureCursor("select * from users u; select * from orders o where o.|");
|
|
const model = buildSqlSemanticModel(sql, cursor);
|
|
|
|
expect(model.rowSources.some((source) => source.name === "orders")).toBe(true);
|
|
expect(model.rowSources.some((source) => source.name === "users")).toBe(false);
|
|
});
|
|
|
|
it("does not expose CTE body tables as outer query row sources", () => {
|
|
const { sql, cursor } = sqlFixtureCursor("WITH recent_orders AS (SELECT id FROM orders) SELECT * FROM recent_orders ro WHERE ro.|");
|
|
const model = buildSqlSemanticModel(sql, cursor);
|
|
|
|
expect(model.rowSources.some((source) => source.name === "recent_orders")).toBe(true);
|
|
expect(model.rowSources.some((source) => source.name === "orders")).toBe(false);
|
|
});
|
|
|
|
it("does not expose subquery body tables as outer query row sources", () => {
|
|
const { sql, cursor } = sqlFixtureCursor("SELECT * FROM (SELECT id FROM users) sq WHERE sq.|");
|
|
const model = buildSqlSemanticModel(sql, cursor);
|
|
|
|
expect(model.rowSources).toEqual(expect.arrayContaining([expect.objectContaining({ name: "sq", kind: "subquery" })]));
|
|
expect(model.rowSources.some((source) => source.name === "users")).toBe(false);
|
|
});
|
|
|
|
it("suppresses completion inside string literals without metadata scope", () => {
|
|
const { sql, cursor } = sqlFixtureCursor("SELECT 'u.|' FROM users");
|
|
const model = buildSqlSemanticModel(sql, cursor);
|
|
const scope = sqlSemanticCompletionScope(model);
|
|
|
|
expect(model.cursorIntent.kind).toBe("suppressed");
|
|
expect(scope.useRemoteMetadata).toBe(false);
|
|
});
|
|
|
|
it("classifies table references after comma-separated table lists", () => {
|
|
const { sql, cursor } = sqlFixtureCursor("SELECT * FROM users u, ord|");
|
|
const model = buildSqlSemanticModel(sql, cursor);
|
|
const scope = sqlSemanticCompletionScope(model);
|
|
|
|
expect(model.cursorIntent.kind).toBe("table");
|
|
expect(model.cursorIntent.prefix).toBe("ord");
|
|
expect(scope.kind).toBe("table");
|
|
});
|
|
|
|
it("classifies alias-qualified star with replacement range", () => {
|
|
const { sql, cursor } = sqlFixtureCursor("SELECT u.*| FROM users u");
|
|
const model = buildSqlSemanticModel(sql, cursor);
|
|
|
|
expect(model.cursorIntent.kind).toBe("star");
|
|
expect(model.cursorIntent.prefix).toBe("*");
|
|
expect(model.cursorIntent.qualifierParts).toEqual(["u"]);
|
|
expect(sql.slice(model.cursorIntent.replacementRange.start, model.cursorIntent.replacementRange.end)).toBe("*");
|
|
});
|
|
|
|
it("returns low-confidence keyword fallback for unknown SQL", () => {
|
|
const { sql, cursor } = sqlFixtureCursor("explain analyze |");
|
|
const model = buildSqlSemanticModel(sql, cursor);
|
|
const scope = sqlSemanticCompletionScope(model);
|
|
|
|
expect(model.cursorIntent.kind).toBe("keyword");
|
|
expect(model.cursorIntent.confidence).toBe("low");
|
|
expect(scope.useRemoteMetadata).toBe(false);
|
|
});
|
|
|
|
it("exposes PostgreSQL projection aliases in ORDER BY but not WHERE", () => {
|
|
const orderBy = sqlFixtureCursor("select total_amount as total from orders order by to|");
|
|
const where = sqlFixtureCursor("select total_amount as total from orders where to|");
|
|
|
|
expect(sqlSemanticProjectionAliasColumns(buildSqlSemanticModel(orderBy.sql, orderBy.cursor, { databaseType: "postgres" })).map((column) => column.name)).toContain("total");
|
|
expect(sqlSemanticProjectionAliasColumns(buildSqlSemanticModel(where.sql, where.cursor, { databaseType: "postgres" })).map((column) => column.name)).not.toContain("total");
|
|
});
|
|
|
|
it("exposes MySQL projection aliases in GROUP BY and HAVING", () => {
|
|
const groupBy = sqlFixtureCursor("select total_amount as total from orders group by to|");
|
|
const having = sqlFixtureCursor("select total_amount as total from orders having to|");
|
|
|
|
expect(sqlSemanticProjectionAliasColumns(buildSqlSemanticModel(groupBy.sql, groupBy.cursor, { databaseType: "mysql" })).map((column) => column.name)).toContain("total");
|
|
expect(sqlSemanticProjectionAliasColumns(buildSqlSemanticModel(having.sql, having.cursor, { databaseType: "mysql" })).map((column) => column.name)).toContain("total");
|
|
});
|
|
|
|
it("keeps dialect-specific identifier normalization and qualifier scopes", () => {
|
|
const sqlServer = sqlFixtureCursor("SELECT * FROM [dbo].[Users] u WHERE u.|");
|
|
const postgres = sqlFixtureCursor('SELECT total AS "Order Total" FROM "Sales"."Orders" o ORDER BY "Order|');
|
|
const mysql = sqlFixtureCursor("SELECT * FROM `analytics`.`events` e WHERE e.|");
|
|
const sqlite = sqlFixtureCursor("SELECT * FROM main.users u WHERE u.|");
|
|
|
|
expect(buildSqlSemanticModel(sqlServer.sql, sqlServer.cursor, { databaseType: "sqlserver" }).rowSources[0]).toEqual(expect.objectContaining({ name: "Users", qualifierParts: ["dbo"], alias: "u" }));
|
|
expect(sqlSemanticProjectionAliasColumns(buildSqlSemanticModel(postgres.sql, postgres.cursor, { databaseType: "postgres" })).map((column) => column.name)).toContain("Order Total");
|
|
expect(buildSqlSemanticModel(mysql.sql, mysql.cursor, { databaseType: "mysql" }).rowSources[0]).toEqual(expect.objectContaining({ name: "events", qualifierParts: ["analytics"], alias: "e" }));
|
|
expect(buildSqlSemanticModel(sqlite.sql, sqlite.cursor, { databaseType: "sqlite" }).rowSources[0]).toEqual(expect.objectContaining({ name: "users", qualifierParts: ["main"], alias: "u" }));
|
|
});
|
|
|
|
it("covers SQL Server case-insensitive bracket and multi-part qualifier contexts", () => {
|
|
const { sql, cursor } = sqlFixtureCursor("SELECT * FROM [ServerOne].[AppDb].[dbo].[Users] U WHERE u.na|");
|
|
const model = buildSqlSemanticModel(sql, cursor, { databaseType: "sqlserver" });
|
|
|
|
expect(model.rowSources[0]).toEqual(expect.objectContaining({ name: "Users", qualifierParts: ["ServerOne", "AppDb", "dbo"], alias: "U" }));
|
|
expect(model.cursorIntent.kind).toBe("alias_column");
|
|
expect(model.cursorIntent.qualifierParts).toEqual(["u"]);
|
|
});
|
|
|
|
it("covers PostgreSQL lower-case folding with CTEs and ORDER BY projection aliases", () => {
|
|
const { sql, cursor } = sqlFixtureCursor("WITH RecentOrders AS (SELECT id, total FROM orders) SELECT total AS total_alias FROM RecentOrders ro ORDER BY total_|");
|
|
const model = buildSqlSemanticModel(sql, cursor, { databaseType: "postgres" });
|
|
|
|
expect(model.rowSources).toEqual(expect.arrayContaining([expect.objectContaining({ name: "recentorders", alias: "ro", columns: ["id", "total"] })]));
|
|
expect(sqlSemanticProjectionAliasColumns(model).map((column) => column.name)).toContain("total_alias");
|
|
});
|
|
|
|
it("covers MySQL database-qualified backticks and projection alias visibility", () => {
|
|
const groupBy = sqlFixtureCursor("SELECT amount AS total FROM `analytics`.`events` e GROUP BY to|");
|
|
const where = sqlFixtureCursor("SELECT amount AS total FROM `analytics`.`events` e WHERE to|");
|
|
|
|
expect(buildSqlSemanticModel(groupBy.sql, groupBy.cursor, { databaseType: "mysql" }).rowSources[0]).toEqual(expect.objectContaining({ name: "events", qualifierParts: ["analytics"], alias: "e" }));
|
|
expect(sqlSemanticProjectionAliasColumns(buildSqlSemanticModel(groupBy.sql, groupBy.cursor, { databaseType: "mysql" })).map((column) => column.name)).toContain("total");
|
|
expect(sqlSemanticProjectionAliasColumns(buildSqlSemanticModel(where.sql, where.cursor, { databaseType: "mysql" })).map((column) => column.name)).not.toContain("total");
|
|
});
|
|
|
|
it("covers SQLite and DuckDB schema-light local row-source behavior", () => {
|
|
const sqlite = sqlFixtureCursor("SELECT * FROM main.users u WHERE u.|");
|
|
const duckdb = sqlFixtureCursor("SELECT * FROM read_csv('users.csv') csv WHERE csv.|");
|
|
|
|
expect(sqlSemanticCompletionScope(buildSqlSemanticModel(sqlite.sql, sqlite.cursor, { databaseType: "sqlite" })).useRemoteMetadata).toBe(true);
|
|
expect(buildSqlSemanticModel(duckdb.sql, duckdb.cursor, { databaseType: "duckdb" }).rowSources[0]).toEqual(expect.objectContaining({ kind: "table_function", name: "csv", alias: "csv" }));
|
|
});
|
|
});
|