Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,86 @@ describe("semantic SQL completion candidates", () => {
expect(items.filter((item) => item.type === "column").map((item) => item.label)).toEqual(["id", "name", "email"]);
});

it.each([
["PostgreSQL", "postgres", "postgres"],
["SQL Server", "sqlserver", "sqlserver"],
] as const)("uses row-source aliases for %s self-join column collisions", (_label, databaseType, dialect) => {
const columnsByTable = new Map<string, SqlCompletionColumn[]>([["users", ["id", "name"].map((name) => ({ name, table: "users" }))]]);

const { items } = semanticCompletion("SELECT * FROM users u JOIN users v ON u.id = v.id WHERE |", { columnsByTable }, { databaseType, dialect });
const columns = items.filter((item) => item.type === "column");

expect(columns.map((item) => item.label)).toEqual(expect.arrayContaining(["u.id", "u.name", "v.id", "v.name"]));
expect(columns.find((item) => item.label === "u.id")?.apply).toBe("u.id");
expect(columns.find((item) => item.label === "v.id")?.apply).toBe("v.id");
});

it("completes columns for aliases in comma-separated table lists", () => {
const columnsByTable = new Map<string, SqlCompletionColumn[]>([
["table_a", ["id", "name"].map((name) => ({ name, table: "table_a" }))],
["table_b", ["id", "status"].map((name) => ({ name, table: "table_b" }))],
]);

const { context, items } = semanticCompletion("SELECT * FROM table_a a, table_b b WHERE a.id = b.|", { columnsByTable });

expect(context.referencedTables).toEqual(expect.arrayContaining([expect.objectContaining({ name: "table_b", alias: "b" })]));
expect(items.filter((item) => item.type === "column").map((item) => item.label)).toEqual(["id", "status"]);
});

it("completes correlation columns for generic PostgreSQL table functions", () => {
const { context, items } = semanticCompletion("SELECT * FROM generate_series(1, 3) g(value) WHERE g.|", {}, { databaseType: "postgres", dialect: "postgres" });

expect(context.referencedTables).toEqual(expect.arrayContaining([expect.objectContaining({ name: "g", alias: "g" })]));
expect(items.filter((item) => item.type === "column").map((item) => item.label)).toEqual(["value"]);
});

it("completes correlation columns after PostgreSQL WITH ORDINALITY", () => {
const { items } = semanticCompletion("SELECT * FROM generate_series(1, 3) WITH ORDINALITY AS g(value, ord), orders o WHERE g.|", {}, { databaseType: "postgres", dialect: "postgres" });

expect(items.filter((item) => item.type === "column").map((item) => item.label)).toEqual(["value", "ord"]);
});

it("completes later comma-separated sources after a joined table", () => {
const columnsByTable = new Map<string, SqlCompletionColumn[]>([["audit_log", ["event_id", "action"].map((name) => ({ name, table: "audit_log" }))]]);
const { context, items } = semanticCompletion("SELECT * FROM users u JOIN orders o ON o.user_id = u.id, audit_log a WHERE a.|", { columnsByTable }, { databaseType: "postgres", dialect: "postgres" });

expect(context.referencedTables).toEqual(expect.arrayContaining([expect.objectContaining({ name: "audit_log", alias: "a" })]));
expect(items.filter((item) => item.type === "column").map((item) => item.label)).toEqual(["event_id", "action"]);
});

it("completes correlation columns for aliased table sources", () => {
const columnsByTable = new Map<string, SqlCompletionColumn[]>([["table_a", ["source_id", "source_label"].map((name) => ({ name, table: "table_a" }))]]);
const { items } = semanticCompletion("SELECT * FROM table_a a(id, label), table_b b WHERE a.|", { columnsByTable }, { databaseType: "postgres", dialect: "postgres" });

expect(items.filter((item) => item.type === "column").map((item) => item.label)).toEqual(["id", "label"]);
});

it("loads real SQL Server columns after aliased table hints", () => {
const columnsByTable = new Map<string, SqlCompletionColumn[]>([["users", ["id", "name", "email"].map((name) => ({ name, table: "users" }))]]);

const { items } = semanticCompletion("SELECT * FROM users u (NOLOCK) WHERE u.|", { columnsByTable }, { databaseType: "sqlserver", dialect: "sqlserver" });

expect(items.filter((item) => item.type === "column").map((item) => item.label)).toEqual(["id", "name", "email"]);
});

it("merges partial PostgreSQL correlation names with metadata positionally", () => {
const columnsByTable = new Map<string, SqlCompletionColumn[]>([["users", ["id", "name", "email"].map((name) => ({ name, table: "users" }))]]);

const { context, items } = semanticCompletion("SELECT * FROM users u(user_id) WHERE u.|", { columnsByTable }, { databaseType: "postgres", dialect: "postgres" });

expect(context.referencedTables).toEqual(expect.arrayContaining([expect.objectContaining({ name: "users", alias: "u", columns: undefined, columnAliases: ["user_id"] })]));
expect(items.filter((item) => item.type === "column").map((item) => item.label)).toEqual(["user_id", "name", "email"]);
});

it("completes an unquoted SQL Server table named lateral", () => {
const columnsByTable = new Map<string, SqlCompletionColumn[]>([["lateral", ["id", "value"].map((name) => ({ name, table: "lateral" }))]]);

const { context, items } = semanticCompletion("SELECT * FROM lateral l WHERE l.|", { columnsByTable }, { databaseType: "sqlserver", dialect: "sqlserver" });

expect(context.referencedTables).toEqual(expect.arrayContaining([expect.objectContaining({ name: "lateral", alias: "l" })]));
expect(items.filter((item) => item.type === "column").map((item) => item.label)).toEqual(["id", "value"]);
});

it("uses CTE projected columns without remote metadata", () => {
const { items, context } = semanticCompletion("WITH recent_orders(id, total) AS (SELECT id, total FROM orders) SELECT * FROM recent_orders ro WHERE ro.|");

Expand Down
102 changes: 102 additions & 0 deletions apps/desktop/src/lib/__tests__/sql/semantic/model.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,108 @@ describe("sqlSemanticModel baseline fixtures", () => {
expect(scope.kind).toBe("table");
});

it("extracts aliases from completed comma-separated table lists", () => {
const { sql, cursor } = sqlFixtureCursor("SELECT * FROM table_a a, table_b b WHERE a.id = b.|");
const model = buildSqlSemanticModel(sql, cursor);

expect(model.rowSources).toEqual(expect.arrayContaining([expect.objectContaining({ name: "table_a", alias: "a" }), expect.objectContaining({ name: "table_b", alias: "b" })]));
expect(model.cursorIntent).toEqual(expect.objectContaining({ kind: "alias_column", qualifierParts: ["b"] }));
});

it("consumes correlation column lists before parsing later comma-separated sources", () => {
const { sql, cursor } = sqlFixtureCursor("SELECT * FROM table_a a(id), table_b b, table_c c WHERE c.|");
const model = buildSqlSemanticModel(sql, cursor, { databaseType: "postgres" });

expect(model.rowSources).toEqual(expect.arrayContaining([expect.objectContaining({ name: "table_a", alias: "a", columns: undefined, columnAliases: ["id"] }), expect.objectContaining({ name: "table_b", alias: "b" }), expect.objectContaining({ name: "table_c", alias: "c" })]));
expect(model.cursorIntent).toEqual(expect.objectContaining({ kind: "alias_column", qualifierParts: ["c"] }));
});

it("parses generic PostgreSQL table functions and their correlation columns", () => {
const { sql, cursor } = sqlFixtureCursor("SELECT * FROM generate_series(1, 3) g(value) WHERE g.|");
const model = buildSqlSemanticModel(sql, cursor, { databaseType: "postgres" });

expect(model.rowSources).toEqual(expect.arrayContaining([expect.objectContaining({ kind: "table_function", name: "g", alias: "g", columns: ["value"] })]));
expect(model.cursorIntent).toEqual(expect.objectContaining({ kind: "alias_column", qualifierParts: ["g"] }));
});

it("consumes LATERAL functions before parsing later comma-separated sources", () => {
const { sql, cursor } = sqlFixtureCursor("SELECT * FROM users u, LATERAL generate_series(1, 3) g(value), orders o WHERE o.|");
const model = buildSqlSemanticModel(sql, cursor, { databaseType: "postgres" });

expect(model.rowSources).toEqual(expect.arrayContaining([expect.objectContaining({ name: "users", alias: "u" }), expect.objectContaining({ kind: "table_function", name: "g", alias: "g", columns: ["value"] }), expect.objectContaining({ name: "orders", alias: "o" })]));
expect(model.cursorIntent).toEqual(expect.objectContaining({ kind: "alias_column", qualifierParts: ["o"] }));
});

it("consumes WITH ORDINALITY before function aliases and later sources", () => {
const { sql, cursor } = sqlFixtureCursor("SELECT * FROM generate_series(1, 3) WITH ORDINALITY AS g(value, ord), orders o WHERE o.|");
const model = buildSqlSemanticModel(sql, cursor, { databaseType: "postgres" });

expect(model.rowSources).toEqual(expect.arrayContaining([expect.objectContaining({ kind: "table_function", name: "g", alias: "g", columns: ["value", "ord"] }), expect.objectContaining({ name: "orders", alias: "o" })]));
expect(model.cursorIntent).toEqual(expect.objectContaining({ kind: "alias_column", qualifierParts: ["o"] }));
});

it("parses comma-separated sources after a complete joined table", () => {
const { sql, cursor } = sqlFixtureCursor("SELECT * FROM users u JOIN orders o ON o.user_id = u.id, audit_log a WHERE a.|");
const model = buildSqlSemanticModel(sql, cursor, { databaseType: "postgres" });

expect(model.rowSources).toEqual(expect.arrayContaining([expect.objectContaining({ name: "users", alias: "u" }), expect.objectContaining({ name: "orders", alias: "o" }), expect.objectContaining({ name: "audit_log", alias: "a" })]));
expect(model.cursorIntent).toEqual(expect.objectContaining({ kind: "alias_column", qualifierParts: ["a"] }));
});

it("does not parse commas in later SELECT clauses as row sources", () => {
const { sql, cursor } = sqlFixtureCursor("SELECT * FROM users u WINDOW w1 AS (PARTITION BY u.id), w2 AS (PARTITION BY u.id)|");
const model = buildSqlSemanticModel(sql, cursor, { databaseType: "postgres" });

expect(model.rowSources).toEqual(expect.arrayContaining([expect.objectContaining({ name: "users", alias: "u" })]));
expect(model.rowSources.some((source) => source.name === "w2")).toBe(false);
});

it("keeps LATERAL subqueries available as row sources", () => {
const { sql, cursor } = sqlFixtureCursor("SELECT * FROM users u, LATERAL (SELECT u.id AS user_id) s WHERE s.|");
const model = buildSqlSemanticModel(sql, cursor, { databaseType: "postgres" });

expect(model.rowSources).toEqual(expect.arrayContaining([expect.objectContaining({ kind: "subquery", name: "s", alias: "s", columns: ["user_id"] })]));
expect(model.cursorIntent).toEqual(expect.objectContaining({ kind: "alias_column", qualifierParts: ["s"] }));
});

it("does not classify SQL Server table hints as generic table functions", () => {
const { sql, cursor } = sqlFixtureCursor("SELECT * FROM users (NOLOCK) WHERE users.|");
const model = buildSqlSemanticModel(sql, cursor, { databaseType: "sqlserver" });

expect(model.rowSources).toEqual(expect.arrayContaining([expect.objectContaining({ kind: "table", name: "users" })]));
expect(model.rowSources.some((source) => source.kind === "table_function")).toBe(false);
});

it("keeps aliased SQL Server table hints separate from correlation columns", () => {
const { sql, cursor } = sqlFixtureCursor("SELECT * FROM users u (NOLOCK), orders o WHERE u.|");
const model = buildSqlSemanticModel(sql, cursor, { databaseType: "sqlserver" });

expect(model.rowSources).toEqual(expect.arrayContaining([expect.objectContaining({ kind: "table", name: "users", alias: "u", columns: undefined, columnAliases: undefined }), expect.objectContaining({ kind: "table", name: "orders", alias: "o" })]));
expect(model.cursorIntent).toEqual(expect.objectContaining({ kind: "alias_column", qualifierParts: ["u"] }));
});

it("consumes SQL Server WITH table hints without treating WITH as an alias", () => {
const { sql, cursor } = sqlFixtureCursor("SELECT * FROM users WITH (NOLOCK), orders o WHERE users.|");
const model = buildSqlSemanticModel(sql, cursor, { databaseType: "sqlserver" });

expect(model.rowSources).toEqual(expect.arrayContaining([expect.objectContaining({ kind: "table", name: "users", alias: undefined, columns: undefined }), expect.objectContaining({ kind: "table", name: "orders", alias: "o" })]));
});

it("keeps partial PostgreSQL correlation names separate from the source schema", () => {
const { sql, cursor } = sqlFixtureCursor("SELECT * FROM users u(user_id) WHERE u.|");
const model = buildSqlSemanticModel(sql, cursor, { databaseType: "postgres" });

expect(model.rowSources).toEqual(expect.arrayContaining([expect.objectContaining({ kind: "table", name: "users", alias: "u", columns: undefined, columnAliases: ["user_id"], metadataTarget: { table: "users" } })]));
});

it("treats LATERAL as a regular SQL Server table name", () => {
const { sql, cursor } = sqlFixtureCursor("SELECT * FROM lateral l WHERE l.|");
const model = buildSqlSemanticModel(sql, cursor, { databaseType: "sqlserver" });

expect(model.rowSources).toEqual(expect.arrayContaining([expect.objectContaining({ kind: "table", name: "lateral", alias: "l" })]));
expect(model.cursorIntent).toEqual(expect.objectContaining({ kind: "alias_column", qualifierParts: ["l"] }));
});

it("classifies alias-qualified star with replacement range", () => {
const { sql, cursor } = sqlFixtureCursor("SELECT u.*| FROM users u");
const model = buildSqlSemanticModel(sql, cursor);
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/lib/sql/semantic/completion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export function sqlSemanticReferencedTables(model: SqlSemanticModel): SqlComplet
schema: source.qualifierParts[source.qualifierParts.length - 1],
alias: source.alias,
columns: source.columns,
columnAliases: source.columnAliases,
}));
}

Expand Down
Loading
Loading