@@ -41,6 +41,23 @@ export interface FragmentSource {
4141 readPage ( col : ColumnMeta , page : PageInfo ) : Promise < ArrayBuffer > ;
4242}
4343
44+ /** Check if a page can be skipped by checking ALL filter columns' min/max stats.
45+ * Returns true if ANY filter column's stats prove zero rows can match. */
46+ function canSkipPageMultiCol (
47+ columns : ColumnMeta [ ] , pageIdx : number , filters : QueryDescriptor [ "filters" ] ,
48+ ) : boolean {
49+ if ( filters . length === 0 ) return false ;
50+ const colMap = new Map ( columns . map ( c => [ c . name , c ] ) ) ;
51+ for ( const f of filters ) {
52+ const col = colMap . get ( f . column ) ;
53+ if ( ! col ) continue ;
54+ const page = col . pages [ pageIdx ] ;
55+ if ( ! page ) continue ;
56+ if ( canSkipPage ( page , [ f ] , f . column ) ) return true ;
57+ }
58+ return false ;
59+ }
60+
4461// ---------------------------------------------------------------------------
4562// ScanOperator — reads pages from fragments, decodes, yields batches
4663// ---------------------------------------------------------------------------
@@ -110,8 +127,7 @@ export class ScanOperator implements Operator {
110127 const firstCol = frag . columns [ 0 ] ;
111128 if ( ! firstCol ) return - 1 ;
112129 for ( let i = start ; i < firstCol . pages . length ; i ++ ) {
113- const page = firstCol . pages [ i ] ;
114- if ( this . query . vectorSearch || ! canSkipPage ( page , this . query . filters , firstCol . name ) ) {
130+ if ( this . query . vectorSearch || ! canSkipPageMultiCol ( frag . columns , i , this . query . filters ) ) {
115131 return i ;
116132 }
117133 }
@@ -156,9 +172,8 @@ export class ScanOperator implements Operator {
156172 const pi = this . pageIdx ;
157173 this . pageIdx ++ ;
158174
159- // Page-level skip via min/max stats on first column
160- const page = firstCol . pages [ pi ] ;
161- if ( ! this . query . vectorSearch && canSkipPage ( page , this . query . filters , firstCol . name ) ) {
175+ // Page-level skip via min/max stats — check ALL filter columns, not just first
176+ if ( ! this . query . vectorSearch && canSkipPageMultiCol ( frag . columns , pi , this . query . filters ) ) {
162177 this . pagesSkipped += frag . columns . length ;
163178 continue ;
164179 }
@@ -318,7 +333,7 @@ function scanFilterIndices(
318333 return new Uint32Array ( 0 ) ;
319334 }
320335
321- const wasmOp = filter . op !== "in" ? filterOpToWasm ( filter . op ) : - 1 ;
336+ const wasmOp = filter . op !== "in" && filter . op !== "between" ? filterOpToWasm ( filter . op ) : - 1 ;
322337
323338 // Try WASM SIMD path for numeric scalar filters
324339 if ( wasmOp >= 0 && typeof filter . value === "number" &&
@@ -333,6 +348,20 @@ function scanFilterIndices(
333348 }
334349 }
335350
351+ // Try WASM BETWEEN path for numeric range filters
352+ if ( filter . op === "between" && Array . isArray ( filter . value ) && filter . value . length === 2 &&
353+ typeof filter . value [ 0 ] === "number" && typeof filter . value [ 1 ] === "number" &&
354+ ( dtype === "float64" || dtype === "float32" || dtype === "int32" || dtype === "int64" ) ) {
355+ const filterResult = wasmFilterRange (
356+ values , dtype , filter . value [ 0 ] , filter . value [ 1 ] , rowCount , wasm ,
357+ ) ;
358+ if ( filterResult ) {
359+ indices = indices ? wasmIntersect ( indices , filterResult , wasm ) : filterResult ;
360+ if ( indices . length === 0 ) return indices ;
361+ continue ;
362+ }
363+ }
364+
336365 // JS fallback: evaluate filter on current index set
337366 const src = indices ?? Uint32Array . from ( { length : rowCount } , ( _ , i ) => i ) ;
338367 const kept : number [ ] = [ ] ;
@@ -413,6 +442,60 @@ function wasmFilterNumeric(
413442 }
414443}
415444
445+ /** Run WASM BETWEEN (range) filter on decoded numeric values. */
446+ function wasmFilterRange (
447+ values : DecodedValue [ ] ,
448+ dtype : string ,
449+ low : number ,
450+ high : number ,
451+ rowCount : number ,
452+ wasm : WasmEngine ,
453+ ) : Uint32Array | null {
454+ try {
455+ wasm . exports . resetHeap ( ) ;
456+
457+ if ( dtype === "float64" || dtype === "float32" ) {
458+ const dataPtr = wasm . exports . alloc ( rowCount * 8 ) ;
459+ if ( ! dataPtr ) return null ;
460+ const dst = new Float64Array ( wasm . exports . memory . buffer , dataPtr , rowCount ) ;
461+ for ( let i = 0 ; i < rowCount ; i ++ ) dst [ i ] = ( values [ i ] as number ) ?? 0 ;
462+ const outPtr = wasm . exports . alloc ( rowCount * 4 ) ;
463+ if ( ! outPtr ) return null ;
464+ const count = wasm . exports . filterFloat64Range ( dataPtr , rowCount , low , high , outPtr , rowCount ) ;
465+ return new Uint32Array ( wasm . exports . memory . buffer . slice ( outPtr , outPtr + count * 4 ) ) ;
466+ }
467+
468+ if ( dtype === "int64" ) {
469+ const dataPtr = wasm . exports . alloc ( rowCount * 8 ) ;
470+ if ( ! dataPtr ) return null ;
471+ const dst = new BigInt64Array ( wasm . exports . memory . buffer , dataPtr , rowCount ) ;
472+ for ( let i = 0 ; i < rowCount ; i ++ ) {
473+ const v = values [ i ] ;
474+ dst [ i ] = typeof v === "bigint" ? v : BigInt ( ( v as number ) ?? 0 ) ;
475+ }
476+ const outPtr = wasm . exports . alloc ( rowCount * 4 ) ;
477+ if ( ! outPtr ) return null ;
478+ const count = wasm . exports . filterInt64Range ( dataPtr , rowCount , BigInt ( low ) , BigInt ( high ) , outPtr , rowCount ) ;
479+ return new Uint32Array ( wasm . exports . memory . buffer . slice ( outPtr , outPtr + count * 4 ) ) ;
480+ }
481+
482+ if ( dtype === "int32" ) {
483+ const dataPtr = wasm . exports . alloc ( rowCount * 4 ) ;
484+ if ( ! dataPtr ) return null ;
485+ const dst = new Int32Array ( wasm . exports . memory . buffer , dataPtr , rowCount ) ;
486+ for ( let i = 0 ; i < rowCount ; i ++ ) dst [ i ] = ( values [ i ] as number ) ?? 0 ;
487+ const outPtr = wasm . exports . alloc ( rowCount * 4 ) ;
488+ if ( ! outPtr ) return null ;
489+ const count = wasm . exports . filterInt32Range ( dataPtr , rowCount , low , high , outPtr , rowCount ) ;
490+ return new Uint32Array ( wasm . exports . memory . buffer . slice ( outPtr , outPtr + count * 4 ) ) ;
491+ }
492+
493+ return null ;
494+ } catch {
495+ return null ;
496+ }
497+ }
498+
416499/** Intersect two sorted index arrays using WASM. */
417500function wasmIntersect ( a : Uint32Array , b : Uint32Array , wasm : WasmEngine ) : Uint32Array {
418501 try {
@@ -1296,8 +1379,8 @@ export class WasmAggregateOperator implements Operator {
12961379 const pageCount = firstCol . pages . length ;
12971380
12981381 for ( let pi = 0 ; pi < pageCount ; pi ++ ) {
1299- // Page-level skip via min/max stats
1300- if ( canSkipPage ( firstCol . pages [ pi ] , filters , firstCol . name ) ) {
1382+ // Page-level skip via min/max stats — check all filter columns
1383+ if ( canSkipPageMultiCol ( frag . columns , pi , filters ) ) {
13011384 this . pagesSkipped ++ ;
13021385 continue ;
13031386 }
0 commit comments