From 26a30fe1dcdef32dbd5734138d49100bfbb1294d Mon Sep 17 00:00:00 2001 From: Jason Larabie Date: Fri, 3 Jul 2026 10:48:51 -0700 Subject: [PATCH 1/2] Fix TS composite btree range scans --- .../bindings-typescript/src/server/runtime.ts | 17 +++++-- .../tests/index_prefix_filter.test.ts | 48 +++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/crates/bindings-typescript/src/server/runtime.ts b/crates/bindings-typescript/src/server/runtime.ts index e5d782a71e3..f0aeb2fe86e 100644 --- a/crates/bindings-typescript/src/server/runtime.ts +++ b/crates/bindings-typescript/src/server/runtime.ts @@ -896,6 +896,15 @@ export function makeTableView( } as PointIndex; } else { // numColumns != 1 + const isCompleteScalarKey = (range: any[]): boolean => { + // Preserve the point-scan path only for complete scalar keys. + // A complete key with a Range in the final position is still a range + // scan over that column with equality over the preceding prefix. + return ( + range.length === numColumns && !(range[range.length - 1] instanceof Range) + ); + }; + const serializeRange = ( buffer: ResizableBuffer, range: any[] @@ -920,12 +929,12 @@ export function makeTableView( writeBound(term.from); const rstartLen = writer.offset - rstartOffset; writeBound(term.to); - const rendLen = writer.offset - rstartLen; + const rendLen = writer.offset - rstartOffset - rstartLen; return [rstartOffset, prefix_elems, rstartLen, rendLen]; } else { writer.writeU8(0); serializeTerm(writer, term); - const rstartLen = writer.offset; + const rstartLen = writer.offset - rstartOffset; const rendLen = 0; return [rstartOffset, prefix_elems, rstartLen, rendLen]; } @@ -936,7 +945,7 @@ export function makeTableView( // one-column prefix scan; normalize it to a single-element array so // `.length` and `serializeRange` see a prefix rather than NaN. if (!Array.isArray(range)) range = [range]; - if (range.length === numColumns) { + if (isCompleteScalarKey(range)) { const buf = LEAF_BUF; const point_len = serializePoint(buf, range); const iter_id = sys.datastore_index_scan_point_bsatn( @@ -958,7 +967,7 @@ export function makeTableView( }, delete: (range: any[]): u32 => { if (!Array.isArray(range)) range = [range]; - if (range.length === numColumns) { + if (isCompleteScalarKey(range)) { const buf = LEAF_BUF; const point_len = serializePoint(buf, range); return sys.datastore_delete_by_index_scan_point_bsatn( diff --git a/crates/bindings-typescript/tests/index_prefix_filter.test.ts b/crates/bindings-typescript/tests/index_prefix_filter.test.ts index d7a31b1babe..97484b53011 100644 --- a/crates/bindings-typescript/tests/index_prefix_filter.test.ts +++ b/crates/bindings-typescript/tests/index_prefix_filter.test.ts @@ -56,6 +56,32 @@ function tallyView() { return view.by_board_def; } +function tallyCountView() { + const ctx = new ModuleContext(); + const tally = table( + { + name: 'tally', + indexes: [ + { + accessor: 'by_board_count', + algorithm: 'btree', + columns: ['boardId', 'count'] as const, + }, + ] as const, + }, + { + id: t.u64().primaryKey().autoInc(), + boardId: t.u64(), + defId: t.string(), + count: t.u64(), + } + ); + + const rawTableDef = tally.tableDef(ctx, 'tally'); + const view = makeTableView(ctx.typespace, rawTableDef) as any; + return view.by_board_count; +} + describe('multi-column index one-column prefix scan', () => { it('full two-column key works (point scan)', () => { const index = tallyView(); @@ -81,9 +107,31 @@ describe('multi-column index one-column prefix scan', () => { expect([...index.filter(range)]).toEqual([]); }); + it('full key with Range in final column works (range scan)', () => { + const index = tallyCountView(); + const range = new Range( + { tag: 'included', value: 10n }, + { tag: 'included', value: 20n } + ); + + expect(() => [...index.filter([1n, range])]).not.toThrow(); + expect([...index.filter([1n, range])]).toEqual([]); + }); + it('delete() accepts a bare-scalar one-column prefix', () => { const index = tallyView(); expect(() => index.delete(1n)).not.toThrow(); expect(index.delete(1n)).toBe(0); }); + + it('delete() accepts a full key with Range in final column', () => { + const index = tallyCountView(); + const range = new Range( + { tag: 'included', value: 10n }, + { tag: 'included', value: 20n } + ); + + expect(() => index.delete([1n, range])).not.toThrow(); + expect(index.delete([1n, range])).toBe(0); + }); }); From c8a3b896f711a0c7ce168304d6ccb82c25067ebc Mon Sep 17 00:00:00 2001 From: Jason Larabie Date: Fri, 3 Jul 2026 11:03:43 -0700 Subject: [PATCH 2/2] Linting --- crates/bindings-typescript/src/server/runtime.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/bindings-typescript/src/server/runtime.ts b/crates/bindings-typescript/src/server/runtime.ts index f0aeb2fe86e..df12fa6fe90 100644 --- a/crates/bindings-typescript/src/server/runtime.ts +++ b/crates/bindings-typescript/src/server/runtime.ts @@ -901,7 +901,8 @@ export function makeTableView( // A complete key with a Range in the final position is still a range // scan over that column with equality over the preceding prefix. return ( - range.length === numColumns && !(range[range.length - 1] instanceof Range) + range.length === numColumns && + !(range[range.length - 1] instanceof Range) ); };