Skip to content

Commit 6bdde55

Browse files
authored
Index that uses B+ tree (#302)
1 parent cd30e09 commit 6bdde55

File tree

6 files changed

+1069
-98
lines changed

6 files changed

+1069
-98
lines changed

.changeset/better-owls-judge.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tanstack/db": patch
3+
---
4+
5+
Remove OrderedIndex in favor of more efficient BTree index.

packages/db/src/collection.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
createSingleRowRefProxy,
55
toExpression,
66
} from "./query/builder/ref-proxy"
7-
import { OrderedIndex } from "./indexes/ordered-index.js"
7+
import { BTreeIndex } from "./indexes/btree-index.js"
88
import { IndexProxy, LazyIndexWrapper } from "./indexes/lazy-index.js"
99
import { ensureIndexForExpression } from "./indexes/auto-index.js"
1010
import { createTransaction, getActiveTransaction } from "./transactions"
@@ -1297,12 +1297,12 @@ export class CollectionImpl<
12971297
* @returns An index proxy that provides access to the index when ready
12981298
*
12991299
* @example
1300-
* // Create a default ordered index
1300+
* // Create a default B+ tree index
13011301
* const ageIndex = collection.createIndex((row) => row.age)
13021302
*
13031303
* // Create a ordered index with custom options
13041304
* const ageIndex = collection.createIndex((row) => row.age, {
1305-
* indexType: OrderedIndex,
1305+
* indexType: BTreeIndex,
13061306
* options: { compareFn: customComparator },
13071307
* name: 'age_btree'
13081308
* })
@@ -1316,9 +1316,7 @@ export class CollectionImpl<
13161316
* options: { language: 'en' }
13171317
* })
13181318
*/
1319-
public createIndex<
1320-
TResolver extends IndexResolver<TKey> = typeof OrderedIndex,
1321-
>(
1319+
public createIndex<TResolver extends IndexResolver<TKey> = typeof BTreeIndex>(
13221320
indexCallback: (row: SingleRowRefProxy<T>) => any,
13231321
config: IndexOptions<TResolver> = {}
13241322
): IndexProxy<TKey> {
@@ -1329,8 +1327,8 @@ export class CollectionImpl<
13291327
const indexExpression = indexCallback(singleRowRefProxy)
13301328
const expression = toExpression(indexExpression)
13311329

1332-
// Default to OrderedIndex if no type specified
1333-
const resolver = config.indexType ?? (OrderedIndex as unknown as TResolver)
1330+
// Default to BTreeIndex if no type specified
1331+
const resolver = config.indexType ?? (BTreeIndex as unknown as TResolver)
13341332

13351333
// Create lazy wrapper
13361334
const lazyIndex = new LazyIndexWrapper<TKey>(
@@ -1344,13 +1342,13 @@ export class CollectionImpl<
13441342

13451343
this.lazyIndexes.set(indexId, lazyIndex)
13461344

1347-
// For OrderedIndex, resolve immediately and synchronously
1348-
if ((resolver as unknown) === OrderedIndex) {
1345+
// For BTreeIndex, resolve immediately and synchronously
1346+
if ((resolver as unknown) === BTreeIndex) {
13491347
try {
13501348
const resolvedIndex = lazyIndex.getResolved()
13511349
this.resolvedIndexes.set(indexId, resolvedIndex)
13521350
} catch (error) {
1353-
console.warn(`Failed to resolve OrderedIndex:`, error)
1351+
console.warn(`Failed to resolve BTreeIndex:`, error)
13541352
}
13551353
} else if (typeof resolver === `function` && resolver.prototype) {
13561354
// Other synchronous constructors - resolve immediately

packages/db/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export * from "./errors"
1212

1313
// Index system exports
1414
export * from "./indexes/base-index.js"
15-
export * from "./indexes/ordered-index.js"
15+
export * from "./indexes/btree-index.js"
1616
export * from "./indexes/lazy-index.js"
1717
export { type IndexOptions } from "./indexes/index-options.js"
1818

packages/db/src/indexes/auto-index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { OrderedIndex } from "./ordered-index"
1+
import { BTreeIndex } from "./btree-index"
22
import type { BasicExpression } from "../query/ir"
33
import type { CollectionImpl } from "../collection"
44

@@ -46,7 +46,7 @@ export function ensureIndexForExpression<
4646
try {
4747
collection.createIndex((row) => (row as any)[fieldName], {
4848
name: `auto_${fieldName}`,
49-
indexType: OrderedIndex,
49+
indexType: BTreeIndex,
5050
})
5151
} catch (error) {
5252
console.warn(

packages/db/src/indexes/ordered-index.ts renamed to packages/db/src/indexes/btree-index.ts

Lines changed: 42 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { ascComparator } from "../utils/comparison.js"
2-
import { findInsertPosition } from "../utils/array-utils.js"
2+
import { BTree } from "../utils/btree.js"
33
import { BaseIndex } from "./base-index.js"
4+
import type { BasicExpression } from "../query/ir.js"
45
import type { IndexOperation } from "./base-index.js"
56

67
/**
78
* Options for Ordered index
89
*/
9-
export interface OrderedIndexOptions {
10+
export interface BTreeIndexOptions {
1011
compareFn?: (a: any, b: any) => number
1112
}
1213

@@ -21,10 +22,10 @@ export interface RangeQueryOptions {
2122
}
2223

2324
/**
24-
* Ordered index for sorted data with range queries
25+
* B+Tree index for sorted data with range queries
2526
* This maintains items in sorted order and provides efficient range operations
2627
*/
27-
export class OrderedIndex<
28+
export class BTreeIndex<
2829
TKey extends string | number = string | number,
2930
> extends BaseIndex<TKey> {
3031
public readonly supportedOperations = new Set<IndexOperation>([
@@ -37,15 +38,26 @@ export class OrderedIndex<
3738
])
3839

3940
// Internal data structures - private to hide implementation details
40-
private orderedEntries: Array<[any, Set<TKey>]> = []
41-
private valueMap = new Map<any, Set<TKey>>()
41+
// The `orderedEntries` B+ tree is used for efficient range queries
42+
// The `valueMap` is used for O(1) lookups of PKs by indexed value
43+
private orderedEntries: BTree<any, undefined> // we don't associate values with the keys of the B+ tree (the keys are indexed values)
44+
private valueMap = new Map<any, Set<TKey>>() // instead we store a mapping of indexed values to a set of PKs
4245
private indexedKeys = new Set<TKey>()
4346
private compareFn: (a: any, b: any) => number = ascComparator
4447

45-
protected initialize(options?: OrderedIndexOptions): void {
48+
constructor(
49+
id: number,
50+
expression: BasicExpression,
51+
name?: string,
52+
options?: any
53+
) {
54+
super(id, expression, name, options)
4655
this.compareFn = options?.compareFn ?? ascComparator
56+
this.orderedEntries = new BTree(this.compareFn)
4757
}
4858

59+
protected initialize(_options?: BTreeIndexOptions): void {}
60+
4961
/**
5062
* Adds a value to the index
5163
*/
@@ -67,14 +79,7 @@ export class OrderedIndex<
6779
// Create new set for this value
6880
const keySet = new Set<TKey>([key])
6981
this.valueMap.set(indexedValue, keySet)
70-
71-
// Find correct position in ordered entries using binary search
72-
const insertIndex = findInsertPosition(
73-
this.orderedEntries,
74-
indexedValue,
75-
this.compareFn
76-
)
77-
this.orderedEntries.splice(insertIndex, 0, [indexedValue, keySet])
82+
this.orderedEntries.set(indexedValue, undefined)
7883
}
7984

8085
this.indexedKeys.add(key)
@@ -104,13 +109,8 @@ export class OrderedIndex<
104109
if (keySet.size === 0) {
105110
this.valueMap.delete(indexedValue)
106111

107-
// Find and remove from ordered entries
108-
const index = this.orderedEntries.findIndex(
109-
([value]) => this.compareFn(value, indexedValue) === 0
110-
)
111-
if (index !== -1) {
112-
this.orderedEntries.splice(index, 1)
113-
}
112+
// Remove from ordered entries
113+
this.orderedEntries.delete(indexedValue)
114114
}
115115
}
116116

@@ -141,7 +141,7 @@ export class OrderedIndex<
141141
* Clears all data from the index
142142
*/
143143
clear(): void {
144-
this.orderedEntries = []
144+
this.orderedEntries.clear()
145145
this.valueMap.clear()
146146
this.indexedKeys.clear()
147147
this.updateTimestamp()
@@ -175,7 +175,7 @@ export class OrderedIndex<
175175
result = this.inArrayLookup(value)
176176
break
177177
default:
178-
throw new Error(`Operation ${operation} not supported by OrderedIndex`)
178+
throw new Error(`Operation ${operation} not supported by BTreeIndex`)
179179
}
180180

181181
this.trackLookup(startTime)
@@ -206,70 +206,26 @@ export class OrderedIndex<
206206
const { from, to, fromInclusive = true, toInclusive = true } = options
207207
const result = new Set<TKey>()
208208

209-
if (this.orderedEntries.length === 0) {
210-
return result
211-
}
212-
213-
// Find start position
214-
let startIndex = 0
215-
if (from !== undefined) {
216-
const fromInsertIndex = findInsertPosition(
217-
this.orderedEntries,
218-
from,
219-
this.compareFn
220-
)
221-
222-
if (fromInclusive) {
223-
// Include values equal to 'from'
224-
startIndex = fromInsertIndex
225-
} else {
226-
// Exclude values equal to 'from'
227-
startIndex = fromInsertIndex
228-
// Skip the value if it exists at this position
229-
if (
230-
startIndex < this.orderedEntries.length &&
231-
this.compareFn(this.orderedEntries[startIndex]![0], from) === 0
232-
) {
233-
startIndex++
209+
const fromKey = from ?? this.orderedEntries.minKey()
210+
const toKey = to ?? this.orderedEntries.maxKey()
211+
212+
this.orderedEntries.forRange(
213+
fromKey,
214+
toKey,
215+
toInclusive,
216+
(indexedValue, _) => {
217+
if (!fromInclusive && this.compareFn(indexedValue, from) === 0) {
218+
// the B+ tree `forRange` method does not support exclusive lower bounds
219+
// so we need to exclude it manually
220+
return
234221
}
235-
}
236-
}
237-
238-
// Find end position
239-
let endIndex = this.orderedEntries.length
240-
if (to !== undefined) {
241-
const toInsertIndex = findInsertPosition(
242-
this.orderedEntries,
243-
to,
244-
this.compareFn
245-
)
246222

247-
if (toInclusive) {
248-
// Include values equal to 'to'
249-
endIndex = toInsertIndex
250-
// Include the value if it exists at this position
251-
if (
252-
toInsertIndex < this.orderedEntries.length &&
253-
this.compareFn(this.orderedEntries[toInsertIndex]![0], to) === 0
254-
) {
255-
endIndex = toInsertIndex + 1
223+
const keys = this.valueMap.get(indexedValue)
224+
if (keys) {
225+
keys.forEach((key) => result.add(key))
256226
}
257-
} else {
258-
// Exclude values equal to 'to'
259-
endIndex = toInsertIndex
260227
}
261-
}
262-
263-
// Ensure startIndex doesn't exceed endIndex
264-
if (startIndex >= endIndex) {
265-
return result
266-
}
267-
268-
// Collect keys from the range
269-
for (let i = startIndex; i < endIndex; i++) {
270-
const keys = this.orderedEntries[i]![1]
271-
keys.forEach((key) => result.add(key))
272-
}
228+
)
273229

274230
return result
275231
}
@@ -297,6 +253,8 @@ export class OrderedIndex<
297253

298254
get orderedEntriesArray(): Array<[any, Set<TKey>]> {
299255
return this.orderedEntries
256+
.keysArray()
257+
.map((key) => [key, this.valueMap.get(key) ?? new Set()])
300258
}
301259

302260
get valueMapData(): Map<any, Set<TKey>> {

0 commit comments

Comments
 (0)