|
5 | 5 | encodeColumnarBatch, |
6 | 6 | columnarBatchToRows, |
7 | 7 | concatQMCBBatches, |
| 8 | + columnarKWayMerge, |
| 9 | + sliceColumnarBatch, |
| 10 | + readColumnValue, |
8 | 11 | DTYPE_F64, |
9 | 12 | DTYPE_I64, |
10 | 13 | DTYPE_I32, |
@@ -440,4 +443,148 @@ describe("columnar", () => { |
440 | 443 | expect(arr[n - 1]).toBe((n - 1) * 1.5); |
441 | 444 | }); |
442 | 445 | }); |
| 446 | + |
| 447 | + describe("readColumnValue", () => { |
| 448 | + it("reads f64 values", () => { |
| 449 | + const batch = makeBatch([{ name: "x", type: 1, values: [1.5, 2.5, 3.5] }]); |
| 450 | + expect(readColumnValue(batch.columns[0], 0)).toBe(1.5); |
| 451 | + expect(readColumnValue(batch.columns[0], 2)).toBe(3.5); |
| 452 | + }); |
| 453 | + |
| 454 | + it("reads i64 values as bigint", () => { |
| 455 | + const batch = makeBatch([{ name: "id", type: 0, values: [100n, 200n] }]); |
| 456 | + expect(readColumnValue(batch.columns[0], 0)).toBe(100n); |
| 457 | + expect(readColumnValue(batch.columns[0], 1)).toBe(200n); |
| 458 | + }); |
| 459 | + |
| 460 | + it("reads i32 values", () => { |
| 461 | + const batch = makeBatch([{ name: "n", type: 4, values: [10, 20, 30] }]); |
| 462 | + expect(readColumnValue(batch.columns[0], 1)).toBe(20); |
| 463 | + }); |
| 464 | + |
| 465 | + it("reads string values", () => { |
| 466 | + const batch = makeBatch([{ name: "s", type: 2, values: ["hello", "world"] }]); |
| 467 | + expect(readColumnValue(batch.columns[0], 0)).toBe("hello"); |
| 468 | + expect(readColumnValue(batch.columns[0], 1)).toBe("world"); |
| 469 | + }); |
| 470 | + |
| 471 | + it("reads bool values", () => { |
| 472 | + const batch = makeBatch([{ name: "b", type: 3, values: [true, false, true] }]); |
| 473 | + expect(readColumnValue(batch.columns[0], 0)).toBe(true); |
| 474 | + expect(readColumnValue(batch.columns[0], 1)).toBe(false); |
| 475 | + expect(readColumnValue(batch.columns[0], 2)).toBe(true); |
| 476 | + }); |
| 477 | + }); |
| 478 | + |
| 479 | + describe("sliceColumnarBatch", () => { |
| 480 | + it("slices numeric batch with offset", () => { |
| 481 | + const batch = makeBatch([{ name: "x", type: 1, values: [10, 20, 30, 40, 50] }]); |
| 482 | + const sliced = sliceColumnarBatch(batch, 2); |
| 483 | + expect(sliced.rowCount).toBe(3); |
| 484 | + const rows = columnarBatchToRows(sliced); |
| 485 | + expect(rows.map(r => r.x)).toEqual([30, 40, 50]); |
| 486 | + }); |
| 487 | + |
| 488 | + it("slices with offset and limit", () => { |
| 489 | + const batch = makeBatch([{ name: "x", type: 1, values: [10, 20, 30, 40, 50] }]); |
| 490 | + const sliced = sliceColumnarBatch(batch, 1, 2); |
| 491 | + expect(sliced.rowCount).toBe(2); |
| 492 | + const rows = columnarBatchToRows(sliced); |
| 493 | + expect(rows.map(r => r.x)).toEqual([20, 30]); |
| 494 | + }); |
| 495 | + |
| 496 | + it("slices string batch correctly", () => { |
| 497 | + const batch = makeBatch([{ name: "s", type: 2, values: ["a", "bb", "ccc", "dddd"] }]); |
| 498 | + const sliced = sliceColumnarBatch(batch, 1, 2); |
| 499 | + const rows = columnarBatchToRows(sliced); |
| 500 | + expect(rows.map(r => r.s)).toEqual(["bb", "ccc"]); |
| 501 | + }); |
| 502 | + |
| 503 | + it("slices bool batch correctly", () => { |
| 504 | + const batch = makeBatch([{ name: "b", type: 3, values: [true, false, true, false, true] }]); |
| 505 | + const sliced = sliceColumnarBatch(batch, 2, 2); |
| 506 | + const rows = columnarBatchToRows(sliced); |
| 507 | + expect(rows.map(r => r.b)).toEqual([true, false]); |
| 508 | + }); |
| 509 | + |
| 510 | + it("returns same batch when offset=0 and no limit", () => { |
| 511 | + const batch = makeBatch([{ name: "x", type: 1, values: [1, 2] }]); |
| 512 | + expect(sliceColumnarBatch(batch, 0)).toBe(batch); |
| 513 | + }); |
| 514 | + |
| 515 | + it("returns empty batch when offset exceeds rowCount", () => { |
| 516 | + const batch = makeBatch([{ name: "x", type: 1, values: [1, 2] }]); |
| 517 | + const sliced = sliceColumnarBatch(batch, 10); |
| 518 | + expect(sliced.rowCount).toBe(0); |
| 519 | + }); |
| 520 | + }); |
| 521 | + |
| 522 | + describe("columnarKWayMerge", () => { |
| 523 | + it("merges two sorted numeric batches ascending", () => { |
| 524 | + const b1 = makeBatch([{ name: "x", type: 1, values: [1, 3, 5] }]); |
| 525 | + const b2 = makeBatch([{ name: "x", type: 1, values: [2, 4, 6] }]); |
| 526 | + const merged = columnarKWayMerge([b1, b2], "x", "asc", 100); |
| 527 | + const rows = columnarBatchToRows(merged); |
| 528 | + expect(rows.map(r => r.x)).toEqual([1, 2, 3, 4, 5, 6]); |
| 529 | + }); |
| 530 | + |
| 531 | + it("merges descending", () => { |
| 532 | + const b1 = makeBatch([{ name: "x", type: 1, values: [5, 3, 1] }]); |
| 533 | + const b2 = makeBatch([{ name: "x", type: 1, values: [6, 4, 2] }]); |
| 534 | + const merged = columnarKWayMerge([b1, b2], "x", "desc", 100); |
| 535 | + const rows = columnarBatchToRows(merged); |
| 536 | + expect(rows.map(r => r.x)).toEqual([6, 5, 4, 3, 2, 1]); |
| 537 | + }); |
| 538 | + |
| 539 | + it("respects limit", () => { |
| 540 | + const b1 = makeBatch([{ name: "x", type: 1, values: [1, 3, 5] }]); |
| 541 | + const b2 = makeBatch([{ name: "x", type: 1, values: [2, 4, 6] }]); |
| 542 | + const merged = columnarKWayMerge([b1, b2], "x", "asc", 3); |
| 543 | + expect(merged.rowCount).toBe(3); |
| 544 | + const rows = columnarBatchToRows(merged); |
| 545 | + expect(rows.map(r => r.x)).toEqual([1, 2, 3]); |
| 546 | + }); |
| 547 | + |
| 548 | + it("preserves non-sort columns during merge", () => { |
| 549 | + const b1 = makeBatch([ |
| 550 | + { name: "id", type: 1, values: [1, 3] }, |
| 551 | + { name: "name", type: 2, values: ["alice", "charlie"] }, |
| 552 | + ]); |
| 553 | + const b2 = makeBatch([ |
| 554 | + { name: "id", type: 1, values: [2, 4] }, |
| 555 | + { name: "name", type: 2, values: ["bob", "dave"] }, |
| 556 | + ]); |
| 557 | + const merged = columnarKWayMerge([b1, b2], "id", "asc", 100); |
| 558 | + const rows = columnarBatchToRows(merged); |
| 559 | + expect(rows).toEqual([ |
| 560 | + { id: 1, name: "alice" }, |
| 561 | + { id: 2, name: "bob" }, |
| 562 | + { id: 3, name: "charlie" }, |
| 563 | + { id: 4, name: "dave" }, |
| 564 | + ]); |
| 565 | + }); |
| 566 | + |
| 567 | + it("merges three batches", () => { |
| 568 | + const b1 = makeBatch([{ name: "x", type: 1, values: [1, 4] }]); |
| 569 | + const b2 = makeBatch([{ name: "x", type: 1, values: [2, 5] }]); |
| 570 | + const b3 = makeBatch([{ name: "x", type: 1, values: [3, 6] }]); |
| 571 | + const merged = columnarKWayMerge([b1, b2, b3], "x", "asc", 100); |
| 572 | + const rows = columnarBatchToRows(merged); |
| 573 | + expect(rows.map(r => r.x)).toEqual([1, 2, 3, 4, 5, 6]); |
| 574 | + }); |
| 575 | + |
| 576 | + it("handles empty batches", () => { |
| 577 | + const b1 = makeBatch([{ name: "x", type: 1, values: [1, 2] }]); |
| 578 | + const empty: ColumnarBatch = { columns: [{ name: "x", dtype: DTYPE_F64, data: new ArrayBuffer(0), rowCount: 0 }], rowCount: 0 }; |
| 579 | + const merged = columnarKWayMerge([b1, empty], "x", "asc", 100); |
| 580 | + expect(merged.rowCount).toBe(2); |
| 581 | + }); |
| 582 | + }); |
443 | 583 | }); |
| 584 | + |
| 585 | +/** Helper: build a decoded ColumnarBatch from WASM result format. */ |
| 586 | +function makeBatch(columns: { name: string; type: number; values: (number | bigint | string | boolean)[] }[]): ColumnarBatch { |
| 587 | + const wasm = buildWasmResult(columns); |
| 588 | + const qmcb = wasmResultToQMCB(wasm, 0, wasm.byteLength)!; |
| 589 | + return decodeColumnarBatch(qmcb)!; |
| 590 | +} |
0 commit comments