Skip to content

Commit 9034e80

Browse files
committed
test: add canSkipFragment tests for LIKE/NOT LIKE and IS NULL/IS NOT NULL
Verifies that fragment-level pruning correctly aggregates string min/max across pages for LIKE prefix skip, and nullCount/rowCount for null ops.
1 parent 6cd0e4b commit 9034e80

1 file changed

Lines changed: 54 additions & 1 deletion

File tree

src/decode.test.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect } from "vitest";
2-
import { decodePage, assembleRows, canSkipPage, matchesFilter, bigIntReplacer } from "./decode.js";
2+
import { decodePage, assembleRows, canSkipPage, canSkipFragment, matchesFilter, bigIntReplacer } from "./decode.js";
33
import type { ColumnMeta, PageInfo } from "./types.js";
44
import type { QueryDescriptor } from "./client.js";
55
import type { WasmEngine } from "./wasm-engine.js";
@@ -243,6 +243,59 @@ describe("canSkipPage", () => {
243243
});
244244
});
245245

246+
describe("canSkipFragment", () => {
247+
function makeFragment(columns: { name: string; pages: PageInfo[] }[]) {
248+
return { columns: columns.map(c => ({ name: c.name, dataType: "utf8" as const, pages: c.pages })) };
249+
}
250+
251+
const mkPage = (min: string, max: string, rows = 50): PageInfo =>
252+
({ byteOffset: 0n, byteLength: 100, rowCount: rows, nullCount: 0, minValue: min, maxValue: max });
253+
254+
it("skips fragment with LIKE prefix when aggregated string range doesn't overlap", () => {
255+
// Two pages: [apple, banana] and [cherry, date] → fragment range [apple, date]
256+
const frag = makeFragment([{ name: "x", pages: [mkPage("apple", "banana"), mkPage("cherry", "date")] }]);
257+
// Prefix "zoo" is above [apple, date] → skip
258+
expect(canSkipFragment(frag, [{ column: "x", op: "like", value: "zoo%" }])).toBe(true);
259+
// Prefix "aa" is below [apple, date] → skip
260+
expect(canSkipFragment(frag, [{ column: "x", op: "like", value: "aa%" }])).toBe(true);
261+
});
262+
263+
it("does not skip fragment with LIKE prefix when range overlaps", () => {
264+
const frag = makeFragment([{ name: "x", pages: [mkPage("apple", "banana"), mkPage("cherry", "date")] }]);
265+
// Prefix "ch" overlaps [apple, date]
266+
expect(canSkipFragment(frag, [{ column: "x", op: "like", value: "ch%" }])).toBe(false);
267+
// Prefix "ban" overlaps [apple, date]
268+
expect(canSkipFragment(frag, [{ column: "x", op: "like", value: "ban%" }])).toBe(false);
269+
});
270+
271+
it("skips fragment with NOT LIKE when all pages are uniform with matching value", () => {
272+
// All pages have the same single value "hello"
273+
const frag = makeFragment([{ name: "x", pages: [mkPage("hello", "hello"), mkPage("hello", "hello")] }]);
274+
// Fragment range: min=hello, max=hello (uniform) → "hel%" matches → NOT LIKE excludes all → skip
275+
expect(canSkipFragment(frag, [{ column: "x", op: "not_like", value: "hel%" }])).toBe(true);
276+
});
277+
278+
it("does not skip fragment with NOT LIKE when range is non-uniform", () => {
279+
const frag = makeFragment([{ name: "x", pages: [mkPage("hello", "hello"), mkPage("world", "world")] }]);
280+
// Fragment range: [hello, world] — not uniform, can't determine all match
281+
expect(canSkipFragment(frag, [{ column: "x", op: "not_like", value: "hel%" }])).toBe(false);
282+
});
283+
284+
it("skips fragment with IS NULL when total nullCount is 0", () => {
285+
const frag = makeFragment([{ name: "x", pages: [mkPage("a", "b", 50), mkPage("c", "d", 50)] }]);
286+
// Both pages have nullCount=0 → fragment total nullCount=0 → IS NULL finds nothing → skip
287+
expect(canSkipFragment(frag, [{ column: "x", op: "is_null", value: null }])).toBe(true);
288+
});
289+
290+
it("skips fragment with IS NOT NULL when all rows are null", () => {
291+
const allNullPage = (rows: number): PageInfo =>
292+
({ byteOffset: 0n, byteLength: 100, rowCount: rows, nullCount: rows, minValue: undefined, maxValue: undefined });
293+
const frag = makeFragment([{ name: "x", pages: [allNullPage(50), allNullPage(30)] }]);
294+
// Total nullCount (80) = total rowCount (80) → IS NOT NULL finds nothing → skip
295+
expect(canSkipFragment(frag, [{ column: "x", op: "is_not_null", value: null }])).toBe(true);
296+
});
297+
});
298+
246299
describe("assembleRows", () => {
247300
function makeColumnData(name: string, values: number[]): [string, ArrayBuffer[]] {
248301
const buf = new ArrayBuffer(values.length * 4);

0 commit comments

Comments
 (0)