11import { compareKeys } from '@tanstack/db-ivm'
22import { BTree } from '../utils/btree.js'
3- import { defaultComparator , normalizeValue } from '../utils/comparison.js'
3+ import {
4+ defaultComparator ,
5+ denormalizeUndefined ,
6+ normalizeForBTree ,
7+ } from '../utils/comparison.js'
48import { BaseIndex } from './base-index.js'
59import type { CompareOptions } from '../query/builder/types.js'
610import type { BasicExpression } from '../query/ir.js'
@@ -29,7 +33,7 @@ export interface RangeQueryOptions {
2933 * This maintains items in sorted order and provides efficient range operations
3034 */
3135export class BTreeIndex <
32- TKey extends string | number = string | number ,
36+ TKey extends string | number | undefined = string | number | undefined ,
3337> extends BaseIndex < TKey > {
3438 public readonly supportedOperations = new Set < IndexOperation > ( [
3539 `eq` ,
@@ -55,7 +59,16 @@ export class BTreeIndex<
5559 options ?: any ,
5660 ) {
5761 super ( id , expression , name , options )
58- this . compareFn = options ?. compareFn ?? defaultComparator
62+
63+ // Get the base compare function
64+ const baseCompareFn = options ?. compareFn ?? defaultComparator
65+
66+ // Wrap it to denormalize sentinels before comparison
67+ // This ensures UNDEFINED_SENTINEL is converted back to undefined
68+ // before being passed to the baseCompareFn (which can be user-provided and is unaware of the UNDEFINED_SENTINEL)
69+ this . compareFn = ( a : any , b : any ) =>
70+ baseCompareFn ( denormalizeUndefined ( a ) , denormalizeUndefined ( b ) )
71+
5972 if ( options ?. compareOptions ) {
6073 this . compareOptions = options ! . compareOptions
6174 }
@@ -78,7 +91,7 @@ export class BTreeIndex<
7891 }
7992
8093 // Normalize the value for Map key usage
81- const normalizedValue = normalizeValue ( indexedValue )
94+ const normalizedValue = normalizeForBTree ( indexedValue )
8295
8396 // Check if this value already exists
8497 if ( this . valueMap . has ( normalizedValue ) ) {
@@ -111,7 +124,7 @@ export class BTreeIndex<
111124 }
112125
113126 // Normalize the value for Map key usage
114- const normalizedValue = normalizeValue ( indexedValue )
127+ const normalizedValue = normalizeForBTree ( indexedValue )
115128
116129 if ( this . valueMap . has ( normalizedValue ) ) {
117130 const keySet = this . valueMap . get ( normalizedValue ) !
@@ -207,7 +220,7 @@ export class BTreeIndex<
207220 * Performs an equality lookup
208221 */
209222 equalityLookup ( value : any ) : Set < TKey > {
210- const normalizedValue = normalizeValue ( value )
223+ const normalizedValue = normalizeForBTree ( value )
211224 return new Set ( this . valueMap . get ( normalizedValue ) ?? [ ] )
212225 }
213226
@@ -219,10 +232,15 @@ export class BTreeIndex<
219232 const { from, to, fromInclusive = true , toInclusive = true } = options
220233 const result = new Set < TKey > ( )
221234
222- const normalizedFrom = normalizeValue ( from )
223- const normalizedTo = normalizeValue ( to )
224- const fromKey = normalizedFrom ?? this . orderedEntries . minKey ( )
225- const toKey = normalizedTo ?? this . orderedEntries . maxKey ( )
235+ // Check if from/to were explicitly provided (even if undefined)
236+ // vs not provided at all (should use min/max key)
237+ const hasFrom = `from` in options
238+ const hasTo = `to` in options
239+
240+ const fromKey = hasFrom
241+ ? normalizeForBTree ( from )
242+ : this . orderedEntries . minKey ( )
243+ const toKey = hasTo ? normalizeForBTree ( to ) : this . orderedEntries . maxKey ( )
226244
227245 this . orderedEntries . forRange (
228246 fromKey ,
@@ -250,29 +268,43 @@ export class BTreeIndex<
250268 */
251269 rangeQueryReversed ( options : RangeQueryOptions = { } ) : Set < TKey > {
252270 const { from, to, fromInclusive = true , toInclusive = true } = options
271+ const hasFrom = `from` in options
272+ const hasTo = `to` in options
273+
274+ // Swap from/to for reversed query, respecting explicit undefined values
253275 return this . rangeQuery ( {
254- from : to ?? this . orderedEntries . maxKey ( ) ,
255- to : from ?? this . orderedEntries . minKey ( ) ,
276+ from : hasTo ? to : this . orderedEntries . maxKey ( ) ,
277+ to : hasFrom ? from : this . orderedEntries . minKey ( ) ,
256278 fromInclusive : toInclusive ,
257279 toInclusive : fromInclusive ,
258280 } )
259281 }
260282
283+ /**
284+ * Internal method for taking items from the index.
285+ * @param n - The number of items to return
286+ * @param nextPair - Function to get the next pair from the BTree
287+ * @param from - Already normalized! undefined means "start from beginning/end", sentinel means "start from the key undefined"
288+ * @param filterFn - Optional filter function
289+ * @param reversed - Whether to reverse the order of keys within each value
290+ */
261291 private takeInternal (
262292 n : number ,
263293 nextPair : ( k ?: any ) => [ any , any ] | undefined ,
264- from ? : any ,
294+ from : any ,
265295 filterFn ?: ( key : TKey ) => boolean ,
266296 reversed : boolean = false ,
267297 ) : Array < TKey > {
268298 const keysInResult : Set < TKey > = new Set ( )
269299 const result : Array < TKey > = [ ]
270300 let pair : [ any , any ] | undefined
271- let key = normalizeValue ( from )
301+ let key = from // Use as-is - it's already normalized by the caller
272302
273303 while ( ( pair = nextPair ( key ) ) !== undefined && result . length < n ) {
274304 key = pair [ 0 ]
275- const keys = this . valueMap . get ( key )
305+ const keys = this . valueMap . get ( key ) as
306+ | Set < Exclude < TKey , undefined > >
307+ | undefined
276308 if ( keys && keys . size > 0 ) {
277309 // Sort keys for deterministic order, reverse if needed
278310 const sorted = Array . from ( keys ) . sort ( compareKeys )
@@ -291,29 +323,60 @@ export class BTreeIndex<
291323 }
292324
293325 /**
294- * Returns the next n items after the provided item or the first n items if no from item is provided .
326+ * Returns the next n items after the provided item.
295327 * @param n - The number of items to return
296- * @param from - The item to start from (exclusive). Starts from the smallest item (inclusive) if not provided.
297- * @returns The next n items after the provided key. Returns the first n items if no from item is provided.
328+ * @param from - The item to start from (exclusive).
329+ * @returns The next n items after the provided key.
298330 */
299- take ( n : number , from ? : any , filterFn ?: ( key : TKey ) => boolean ) : Array < TKey > {
331+ take ( n : number , from : any , filterFn ?: ( key : TKey ) => boolean ) : Array < TKey > {
300332 const nextPair = ( k ?: any ) => this . orderedEntries . nextHigherPair ( k )
301- return this . takeInternal ( n , nextPair , from , filterFn )
333+ // Normalize the from value
334+ const normalizedFrom = normalizeForBTree ( from )
335+ return this . takeInternal ( n , nextPair , normalizedFrom , filterFn )
302336 }
303337
304338 /**
305- * Returns the next n items **before** the provided item (in descending order) or the last n items if no from item is provided .
339+ * Returns the first n items from the beginning .
306340 * @param n - The number of items to return
307- * @param from - The item to start from (exclusive). Starts from the largest item (inclusive) if not provided.
308- * @returns The next n items **before** the provided key. Returns the last n items if no from item is provided.
341+ * @param filterFn - Optional filter function
342+ * @returns The first n items
343+ */
344+ takeFromStart ( n : number , filterFn ?: ( key : TKey ) => boolean ) : Array < TKey > {
345+ const nextPair = ( k ?: any ) => this . orderedEntries . nextHigherPair ( k )
346+ // Pass undefined to mean "start from beginning" (BTree's native behavior)
347+ return this . takeInternal ( n , nextPair , undefined , filterFn )
348+ }
349+
350+ /**
351+ * Returns the next n items **before** the provided item (in descending order).
352+ * @param n - The number of items to return
353+ * @param from - The item to start from (exclusive). Required.
354+ * @returns The next n items **before** the provided key.
309355 */
310356 takeReversed (
311357 n : number ,
312- from ?: any ,
358+ from : any ,
359+ filterFn ?: ( key : TKey ) => boolean ,
360+ ) : Array < TKey > {
361+ const nextPair = ( k ?: any ) => this . orderedEntries . nextLowerPair ( k )
362+ // Normalize the from value
363+ const normalizedFrom = normalizeForBTree ( from )
364+ return this . takeInternal ( n , nextPair , normalizedFrom , filterFn , true )
365+ }
366+
367+ /**
368+ * Returns the last n items from the end.
369+ * @param n - The number of items to return
370+ * @param filterFn - Optional filter function
371+ * @returns The last n items
372+ */
373+ takeReversedFromEnd (
374+ n : number ,
313375 filterFn ?: ( key : TKey ) => boolean ,
314376 ) : Array < TKey > {
315377 const nextPair = ( k ?: any ) => this . orderedEntries . nextLowerPair ( k )
316- return this . takeInternal ( n , nextPair , from , filterFn , true )
378+ // Pass undefined to mean "start from end" (BTree's native behavior)
379+ return this . takeInternal ( n , nextPair , undefined , filterFn , true )
317380 }
318381
319382 /**
@@ -323,7 +386,7 @@ export class BTreeIndex<
323386 const result = new Set < TKey > ( )
324387
325388 for ( const value of values ) {
326- const normalizedValue = normalizeValue ( value )
389+ const normalizedValue = normalizeForBTree ( value )
327390 const keys = this . valueMap . get ( normalizedValue )
328391 if ( keys ) {
329392 keys . forEach ( ( key ) => result . add ( key ) )
@@ -341,17 +404,25 @@ export class BTreeIndex<
341404 get orderedEntriesArray ( ) : Array < [ any , Set < TKey > ] > {
342405 return this . orderedEntries
343406 . keysArray ( )
344- . map ( ( key ) => [ key , this . valueMap . get ( key ) ?? new Set ( ) ] )
407+ . map ( ( key ) => [
408+ denormalizeUndefined ( key ) ,
409+ this . valueMap . get ( key ) ?? new Set ( ) ,
410+ ] )
345411 }
346412
347413 get orderedEntriesArrayReversed ( ) : Array < [ any , Set < TKey > ] > {
348- return this . takeReversed ( this . orderedEntries . size ) . map ( ( key ) => [
349- key ,
414+ return this . takeReversedFromEnd ( this . orderedEntries . size ) . map ( ( key ) => [
415+ denormalizeUndefined ( key ) ,
350416 this . valueMap . get ( key ) ?? new Set ( ) ,
351417 ] )
352418 }
353419
354420 get valueMapData ( ) : Map < any , Set < TKey > > {
355- return this . valueMap
421+ // Return a new Map with denormalized keys
422+ const result = new Map < any , Set < TKey > > ( )
423+ for ( const [ key , value ] of this . valueMap ) {
424+ result . set ( denormalizeUndefined ( key ) , value )
425+ }
426+ return result
356427 }
357428}
0 commit comments