|
1 | 1 | 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"; |
3 | 3 | import type { ColumnMeta, PageInfo } from "./types.js"; |
4 | 4 | import type { QueryDescriptor } from "./client.js"; |
5 | 5 | import type { WasmEngine } from "./wasm-engine.js"; |
@@ -243,6 +243,59 @@ describe("canSkipPage", () => { |
243 | 243 | }); |
244 | 244 | }); |
245 | 245 |
|
| 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 | + |
246 | 299 | describe("assembleRows", () => { |
247 | 300 | function makeColumnData(name: string, values: number[]): [string, ArrayBuffer[]] { |
248 | 301 | const buf = new ArrayBuffer(values.length * 4); |
|
0 commit comments