feat(editor): add SQL semantic diagnostics

This commit is contained in:
t8y2 committed 2026-05-22 14:22:58 +08:00
1 parent 64c068bbf5
commit 0e189ba0cc
18 files changed
+997 -19

No files matched your search

+42
View File
@@ -3,6 +3,7 @@ import test from "node:test";
import {
buildSqlCompletionItems,
getSqlFunctionSignatureHelp,
getSqlCompletionResultValidFor,
shouldAutoOpenSqlCompletion,
type SqlCompletionColumn,
type SqlCompletionTable,
@@ -261,6 +262,47 @@ test("auto-opens completion after word characters and explicit dot qualifiers",
}
});
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,
});
assert.deepEqual(
items.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 ";
@@ -0,0 +1,95 @@
import { strict as assert } from "node:assert";
import test from "node:test";
import {
buildSqlParserErrorDiagnostic,
buildSqlSemanticDiagnostics,
areSqlSemanticDiagnosticsEqual,
shouldRunSqlSemanticDiagnostics,
} from "../../apps/desktop/src/lib/sqlSemanticDiagnostics.ts";
import type { SqlReferenceAnalysis } from "../../apps/desktop/src/types/database.ts";
const span = (startColumn: number, endColumn: number) => ({
start_line: 1,
start_column: startColumn,
end_line: 1,
end_column: endColumn,
});
test("flags missing qualified columns against the referenced table", () => {
const analysis: SqlReferenceAnalysis = {
tables: [{ name: "users", alias: "u", span: span(23, 27) }],
columns: [
{ name: "missing", qualifier: "u", span: span(10, 16) },
{ name: "id", qualifier: "u", span: span(34, 35) },
],
};
const diagnostics = buildSqlSemanticDiagnostics(analysis, {
tables: [{ name: "users", type: "table" }],
columnsByTable: new Map([["users", [{ name: "id", table: "users" }]]]),
});
assert.deepEqual(
diagnostics.map((diagnostic) => diagnostic.message),
["Unknown column u.missing"],
);
});
test("does not flag unqualified columns when multiple tables make ownership ambiguous", () => {
const analysis: SqlReferenceAnalysis = {
tables: [
{ name: "users", alias: "u", span: span(25, 29) },
{ name: "orders", alias: "o", span: span(35, 40) },
],
columns: [{ name: "id", span: span(8, 9) }],
};
const diagnostics = buildSqlSemanticDiagnostics(analysis, {
tables: [
{ name: "users", type: "table" },
{ name: "orders", type: "table" },
],
columnsByTable: new Map(),
});
assert.deepEqual(diagnostics, []);
});
test("builds a syntax diagnostic from parser errors with line and column", () => {
const diagnostic = buildSqlParserErrorDiagnostic(
"Expected: end of statement, found: FOM at Line: 1, Column: 10",
"SELECT * FOM",
);
assert.equal(diagnostic?.message, "Expected: end of statement, found: FOM at Line: 1, Column: 10");
assert.deepEqual(diagnostic?.span, span(10, 12));
});
test("compares diagnostics by severity message and span", () => {
const diagnostics = [
{
message: "Unknown column u.missing",
severity: "warning" as const,
span: span(10, 16),
},
];
assert.equal(
areSqlSemanticDiagnosticsEqual(
diagnostics,
diagnostics.map((item) => ({ ...item })),
),
true,
);
assert.equal(
areSqlSemanticDiagnosticsEqual(diagnostics, [{ ...diagnostics[0], message: "Unknown column u.name" }]),
false,
);
});
test("defers diagnostics while the cursor is in table completion context", () => {
assert.equal(shouldRunSqlSemanticDiagnostics("select * from ", "select * from ".length), false);
assert.equal(shouldRunSqlSemanticDiagnostics("select * from us", "select * from us".length), false);
assert.equal(shouldRunSqlSemanticDiagnostics("select u.", "select u.".length), false);
assert.equal(shouldRunSqlSemanticDiagnostics("select * from users where missing = 1", 42), true);
});