Skip to content

Commit 95b4459

Browse files
committed
fix: queryToSql now emits DISTINCT and COUNT(DISTINCT col) for WASM SQL
queryToSql silently dropped the DISTINCT keyword, causing WASM to return all rows for DISTINCT queries (TS DistinctOperator caught it, but at the cost of transferring extra data). Also fixed count_distinct generating invalid COUNT_DISTINCT() instead of COUNT(DISTINCT col).
1 parent 876f911 commit 95b4459

File tree

2 files changed

+59
-6
lines changed

2 files changed

+59
-6
lines changed

src/wasm-engine.integration.test.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Skips gracefully if binary not found.
55
*/
66
import { describe, it, expect, beforeAll } from "vitest";
7-
import { instantiateWasm, type WasmEngine } from "./wasm-engine.js";
7+
import { instantiateWasm, queryToSql, type WasmEngine } from "./wasm-engine.js";
88
import type { QueryDescriptor } from "./client.js";
99
import type { PageInfo } from "./types.js";
1010
import * as fs from "node:fs/promises";
@@ -343,3 +343,49 @@ describe.skipIf(!hasWasm)("WASM integration", () => {
343343
});
344344
});
345345
});
346+
347+
// Pure-TS tests — no WASM binary required
348+
describe("queryToSql", () => {
349+
const base = { table: "t", filters: [] as any[], projections: [] as string[] };
350+
351+
it("generates SELECT DISTINCT", () => {
352+
const sql = queryToSql({ ...base, distinct: ["a", "b"] });
353+
expect(sql).toBe('SELECT DISTINCT * FROM t');
354+
});
355+
356+
it("generates SELECT DISTINCT with projections", () => {
357+
const sql = queryToSql({ ...base, distinct: ["a"], projections: ["a", "b"] });
358+
expect(sql).toBe('SELECT DISTINCT a, b FROM t');
359+
});
360+
361+
it("generates COUNT(DISTINCT col) for count_distinct", () => {
362+
const sql = queryToSql({ ...base, aggregates: [{ fn: "count_distinct", column: "user_id" }] });
363+
expect(sql).toContain("COUNT(DISTINCT user_id)");
364+
});
365+
366+
it("generates COUNT(*) for count with star", () => {
367+
const sql = queryToSql({ ...base, aggregates: [{ fn: "count", column: "*" }] });
368+
expect(sql).toContain("COUNT(*)");
369+
});
370+
371+
it("includes OR filterGroups", () => {
372+
const sql = queryToSql({
373+
...base,
374+
filterGroups: [
375+
[{ column: "a", op: "eq", value: 1 }],
376+
[{ column: "b", op: "eq", value: 2 }],
377+
],
378+
});
379+
expect(sql).toContain("(a = 1 OR b = 2)");
380+
});
381+
382+
it("handles NOT IN filter", () => {
383+
const sql = queryToSql({ ...base, filters: [{ column: "id", op: "not_in", value: [1, 2] }] });
384+
expect(sql).toContain("NOT IN (1, 2)");
385+
});
386+
387+
it("handles NOT BETWEEN filter", () => {
388+
const sql = queryToSql({ ...base, filters: [{ column: "x", op: "not_between", value: [10, 20] }] });
389+
expect(sql).toContain("NOT BETWEEN 10 AND 20");
390+
});
391+
});

src/wasm-engine.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,21 +1027,28 @@ function filterToSql(f: FilterOp): string {
10271027
return `${quote(f.column)} ${COMPARISON_OP_MAP[f.op] ?? "="} ${val}`;
10281028
}
10291029

1030-
function queryToSql(query: QueryDescriptor): string {
1030+
/** @internal — exported for testing */
1031+
export function queryToSql(query: QueryDescriptor): string {
10311032
const parts: string[] = [];
10321033

1034+
const distinctKw = query.distinct?.length ? "DISTINCT " : "";
10331035
if (query.aggregates?.length) {
10341036
const exprs: string[] = [];
10351037
if (query.groupBy) exprs.push(...query.groupBy.map(quote));
10361038
for (const agg of query.aggregates) {
1037-
const expr = `${agg.fn.toUpperCase()}(${agg.column === "*" ? "*" : quote(agg.column)})`;
1039+
let expr: string;
1040+
if (agg.fn === "count_distinct") {
1041+
expr = `COUNT(DISTINCT ${agg.column === "*" ? "*" : quote(agg.column)})`;
1042+
} else {
1043+
expr = `${agg.fn.toUpperCase()}(${agg.column === "*" ? "*" : quote(agg.column)})`;
1044+
}
10381045
exprs.push(agg.alias ? `${expr} AS ${quote(agg.alias)}` : expr);
10391046
}
1040-
parts.push(`SELECT ${exprs.join(", ")}`);
1047+
parts.push(`SELECT ${distinctKw}${exprs.join(", ")}`);
10411048
} else if (query.projections.length > 0) {
1042-
parts.push(`SELECT ${query.projections.map(quote).join(", ")}`);
1049+
parts.push(`SELECT ${distinctKw}${query.projections.map(quote).join(", ")}`);
10431050
} else {
1044-
parts.push("SELECT *");
1051+
parts.push(`SELECT ${distinctKw}*`);
10451052
}
10461053

10471054
parts.push(`FROM ${quote(query.table)}`);

0 commit comments

Comments
 (0)