feat(editor): add SQL semantic diagnostics
This commit is contained in:
1 parent
64c068bbf5
commit
0e189ba0cc
18 files changed
+997
-19
No files matched your search
@@ -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);
|
||||
});
|
||||
Reference in new issue
Block a user