@tanstack/db 0.1.1 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/dist/cjs/collection.cjs +112 -6
  2. package/dist/cjs/collection.cjs.map +1 -1
  3. package/dist/cjs/collection.d.cts +12 -3
  4. package/dist/cjs/errors.cjs +6 -0
  5. package/dist/cjs/errors.cjs.map +1 -1
  6. package/dist/cjs/errors.d.cts +3 -0
  7. package/dist/cjs/index.cjs +1 -0
  8. package/dist/cjs/index.cjs.map +1 -1
  9. package/dist/cjs/indexes/auto-index.cjs +30 -19
  10. package/dist/cjs/indexes/auto-index.cjs.map +1 -1
  11. package/dist/cjs/indexes/auto-index.d.cts +1 -0
  12. package/dist/cjs/indexes/base-index.cjs.map +1 -1
  13. package/dist/cjs/indexes/base-index.d.cts +2 -1
  14. package/dist/cjs/indexes/btree-index.cjs +29 -3
  15. package/dist/cjs/indexes/btree-index.cjs.map +1 -1
  16. package/dist/cjs/indexes/btree-index.d.cts +7 -0
  17. package/dist/cjs/indexes/index-options.d.cts +1 -1
  18. package/dist/cjs/query/builder/index.cjs +9 -2
  19. package/dist/cjs/query/builder/index.cjs.map +1 -1
  20. package/dist/cjs/query/builder/index.d.cts +2 -2
  21. package/dist/cjs/query/builder/types.d.cts +27 -6
  22. package/dist/cjs/query/compiler/evaluators.cjs +2 -2
  23. package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
  24. package/dist/cjs/query/compiler/evaluators.d.cts +1 -1
  25. package/dist/cjs/query/compiler/group-by.cjs +3 -1
  26. package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
  27. package/dist/cjs/query/compiler/index.cjs +72 -6
  28. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  29. package/dist/cjs/query/compiler/index.d.cts +16 -2
  30. package/dist/cjs/query/compiler/joins.cjs +111 -12
  31. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  32. package/dist/cjs/query/compiler/joins.d.cts +9 -2
  33. package/dist/cjs/query/compiler/order-by.cjs +80 -23
  34. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  35. package/dist/cjs/query/compiler/order-by.d.cts +12 -2
  36. package/dist/cjs/query/ir.cjs.map +1 -1
  37. package/dist/cjs/query/ir.d.cts +2 -1
  38. package/dist/cjs/query/live-query-collection.cjs +196 -23
  39. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  40. package/dist/cjs/types.d.cts +1 -0
  41. package/dist/cjs/utils/btree.cjs +15 -0
  42. package/dist/cjs/utils/btree.cjs.map +1 -1
  43. package/dist/cjs/utils/btree.d.cts +8 -0
  44. package/dist/cjs/utils/comparison.cjs +29 -7
  45. package/dist/cjs/utils/comparison.cjs.map +1 -1
  46. package/dist/cjs/utils/comparison.d.cts +6 -2
  47. package/dist/esm/collection.d.ts +12 -3
  48. package/dist/esm/collection.js +113 -7
  49. package/dist/esm/collection.js.map +1 -1
  50. package/dist/esm/errors.d.ts +3 -0
  51. package/dist/esm/errors.js +6 -0
  52. package/dist/esm/errors.js.map +1 -1
  53. package/dist/esm/index.js +2 -1
  54. package/dist/esm/indexes/auto-index.d.ts +1 -0
  55. package/dist/esm/indexes/auto-index.js +31 -20
  56. package/dist/esm/indexes/auto-index.js.map +1 -1
  57. package/dist/esm/indexes/base-index.d.ts +2 -1
  58. package/dist/esm/indexes/base-index.js.map +1 -1
  59. package/dist/esm/indexes/btree-index.d.ts +7 -0
  60. package/dist/esm/indexes/btree-index.js +29 -3
  61. package/dist/esm/indexes/btree-index.js.map +1 -1
  62. package/dist/esm/indexes/index-options.d.ts +1 -1
  63. package/dist/esm/query/builder/index.d.ts +2 -2
  64. package/dist/esm/query/builder/index.js +9 -2
  65. package/dist/esm/query/builder/index.js.map +1 -1
  66. package/dist/esm/query/builder/types.d.ts +27 -6
  67. package/dist/esm/query/compiler/evaluators.d.ts +1 -1
  68. package/dist/esm/query/compiler/evaluators.js +2 -2
  69. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  70. package/dist/esm/query/compiler/group-by.js +3 -1
  71. package/dist/esm/query/compiler/group-by.js.map +1 -1
  72. package/dist/esm/query/compiler/index.d.ts +16 -2
  73. package/dist/esm/query/compiler/index.js +73 -7
  74. package/dist/esm/query/compiler/index.js.map +1 -1
  75. package/dist/esm/query/compiler/joins.d.ts +9 -2
  76. package/dist/esm/query/compiler/joins.js +114 -15
  77. package/dist/esm/query/compiler/joins.js.map +1 -1
  78. package/dist/esm/query/compiler/order-by.d.ts +12 -2
  79. package/dist/esm/query/compiler/order-by.js +81 -24
  80. package/dist/esm/query/compiler/order-by.js.map +1 -1
  81. package/dist/esm/query/ir.d.ts +2 -1
  82. package/dist/esm/query/ir.js.map +1 -1
  83. package/dist/esm/query/live-query-collection.js +196 -23
  84. package/dist/esm/query/live-query-collection.js.map +1 -1
  85. package/dist/esm/types.d.ts +1 -0
  86. package/dist/esm/utils/btree.d.ts +8 -0
  87. package/dist/esm/utils/btree.js +15 -0
  88. package/dist/esm/utils/btree.js.map +1 -1
  89. package/dist/esm/utils/comparison.d.ts +6 -2
  90. package/dist/esm/utils/comparison.js +30 -8
  91. package/dist/esm/utils/comparison.js.map +1 -1
  92. package/package.json +2 -2
  93. package/src/collection.ts +237 -11
  94. package/src/errors.ts +6 -0
  95. package/src/indexes/auto-index.ts +53 -31
  96. package/src/indexes/base-index.ts +6 -1
  97. package/src/indexes/btree-index.ts +32 -3
  98. package/src/indexes/index-options.ts +2 -2
  99. package/src/query/builder/index.ts +19 -2
  100. package/src/query/builder/types.ts +48 -15
  101. package/src/query/compiler/evaluators.ts +6 -3
  102. package/src/query/compiler/group-by.ts +3 -1
  103. package/src/query/compiler/index.ts +112 -5
  104. package/src/query/compiler/joins.ts +216 -20
  105. package/src/query/compiler/order-by.ts +117 -26
  106. package/src/query/ir.ts +2 -1
  107. package/src/query/live-query-collection.ts +352 -24
  108. package/src/types.ts +1 -0
  109. package/src/utils/btree.ts +17 -0
  110. package/src/utils/comparison.ts +40 -7
package/src/collection.ts CHANGED
@@ -65,6 +65,7 @@ import type { BaseIndex, IndexResolver } from "./indexes/base-index.js"
65
65
  interface PendingSyncedTransaction<T extends object = Record<string, unknown>> {
66
66
  committed: boolean
67
67
  operations: Array<OptimisticChangeMessage<T>>
68
+ truncate?: boolean
68
69
  }
69
70
 
70
71
  /**
@@ -155,8 +156,81 @@ export interface Collection<
155
156
  * sync: { sync: () => {} }
156
157
  * })
157
158
  *
158
- * // Note: You must provide either an explicit type or a schema, but not both.
159
+ * // Note: You can provide an explicit type, a schema, or both. When both are provided, the explicit type takes precedence.
159
160
  */
161
+
162
+ // Overload for when schema is provided - infers schema type
163
+ export function createCollection<
164
+ TSchema extends StandardSchemaV1,
165
+ TKey extends string | number = string | number,
166
+ TUtils extends UtilsRecord = {},
167
+ TFallback extends object = Record<string, unknown>,
168
+ >(
169
+ options: CollectionConfig<
170
+ ResolveType<unknown, TSchema, TFallback>,
171
+ TKey,
172
+ TSchema,
173
+ ResolveInsertInput<unknown, TSchema, TFallback>
174
+ > & {
175
+ schema: TSchema
176
+ utils?: TUtils
177
+ }
178
+ ): Collection<
179
+ ResolveType<unknown, TSchema, TFallback>,
180
+ TKey,
181
+ TUtils,
182
+ TSchema,
183
+ ResolveInsertInput<unknown, TSchema, TFallback>
184
+ >
185
+
186
+ // Overload for when explicit type is provided with schema - explicit type takes precedence
187
+ export function createCollection<
188
+ TExplicit extends object,
189
+ TKey extends string | number = string | number,
190
+ TUtils extends UtilsRecord = {},
191
+ TSchema extends StandardSchemaV1 = StandardSchemaV1,
192
+ TFallback extends object = Record<string, unknown>,
193
+ >(
194
+ options: CollectionConfig<
195
+ ResolveType<TExplicit, TSchema, TFallback>,
196
+ TKey,
197
+ TSchema,
198
+ ResolveInsertInput<TExplicit, TSchema, TFallback>
199
+ > & {
200
+ schema: TSchema
201
+ utils?: TUtils
202
+ }
203
+ ): Collection<
204
+ ResolveType<TExplicit, TSchema, TFallback>,
205
+ TKey,
206
+ TUtils,
207
+ TSchema,
208
+ ResolveInsertInput<TExplicit, TSchema, TFallback>
209
+ >
210
+
211
+ // Overload for when explicit type is provided or no schema
212
+ export function createCollection<
213
+ TExplicit = unknown,
214
+ TKey extends string | number = string | number,
215
+ TUtils extends UtilsRecord = {},
216
+ TSchema extends StandardSchemaV1 = StandardSchemaV1,
217
+ TFallback extends object = Record<string, unknown>,
218
+ >(
219
+ options: CollectionConfig<
220
+ ResolveType<TExplicit, TSchema, TFallback>,
221
+ TKey,
222
+ TSchema,
223
+ ResolveInsertInput<TExplicit, TSchema, TFallback>
224
+ > & { utils?: TUtils }
225
+ ): Collection<
226
+ ResolveType<TExplicit, TSchema, TFallback>,
227
+ TKey,
228
+ TUtils,
229
+ TSchema,
230
+ ResolveInsertInput<TExplicit, TSchema, TFallback>
231
+ >
232
+
233
+ // Implementation
160
234
  export function createCollection<
161
235
  TExplicit = unknown,
162
236
  TKey extends string | number = string | number,
@@ -319,9 +393,8 @@ export class CollectionImpl<
319
393
  this.onFirstReadyCallbacks = []
320
394
  callbacks.forEach((callback) => callback())
321
395
 
322
- // If the collection is empty when it becomes ready, emit an empty change event
323
396
  // to notify subscribers (like LiveQueryCollection) that the collection is ready
324
- if (this.size === 0 && this.changeListeners.size > 0) {
397
+ if (this.changeListeners.size > 0) {
325
398
  this.emitEmptyReadyEvent()
326
399
  }
327
400
  }
@@ -486,11 +559,16 @@ export class CollectionImpl<
486
559
 
487
560
  // Check if an item with this key already exists when inserting
488
561
  if (messageWithoutKey.type === `insert`) {
562
+ const insertingIntoExistingSynced = this.syncedData.has(key)
563
+ const hasPendingDeleteForKey = pendingTransaction.operations.some(
564
+ (op) => op.key === key && op.type === `delete`
565
+ )
566
+ const isTruncateTransaction = pendingTransaction.truncate === true
567
+ // Allow insert after truncate in the same transaction even if it existed in syncedData
489
568
  if (
490
- this.syncedData.has(key) &&
491
- !pendingTransaction.operations.some(
492
- (op) => op.key === key && op.type === `delete`
493
- )
569
+ insertingIntoExistingSynced &&
570
+ !hasPendingDeleteForKey &&
571
+ !isTruncateTransaction
494
572
  ) {
495
573
  throw new DuplicateKeySyncError(key, this.id)
496
574
  }
@@ -527,6 +605,28 @@ export class CollectionImpl<
527
605
  markReady: () => {
528
606
  this.markReady()
529
607
  },
608
+ truncate: () => {
609
+ const pendingTransaction =
610
+ this.pendingSyncedTransactions[
611
+ this.pendingSyncedTransactions.length - 1
612
+ ]
613
+ if (!pendingTransaction) {
614
+ throw new NoPendingSyncTransactionWriteError()
615
+ }
616
+ if (pendingTransaction.committed) {
617
+ throw new SyncTransactionAlreadyCommittedWriteError()
618
+ }
619
+
620
+ // Clear all operations from the current transaction
621
+ pendingTransaction.operations = []
622
+
623
+ // Mark the transaction as a truncate operation. During commit, this triggers:
624
+ // - Delete events for all previously synced keys (excluding optimistic-deleted keys)
625
+ // - Clearing of syncedData/syncedMetadata
626
+ // - Subsequent synced ops applied on the fresh base
627
+ // - Finally, optimistic mutations re-applied on top (single batch)
628
+ pendingTransaction.truncate = true
629
+ },
530
630
  })
531
631
 
532
632
  // Store cleanup function if provided
@@ -884,6 +984,12 @@ export class CollectionImpl<
884
984
  for (const listener of this.changeListeners) {
885
985
  listener([])
886
986
  }
987
+ // Emit to key-specific listeners
988
+ for (const [_key, keyListeners] of this.changeKeyListeners) {
989
+ for (const listener of keyListeners) {
990
+ listener([])
991
+ }
992
+ }
887
993
  }
888
994
 
889
995
  /**
@@ -1076,7 +1182,11 @@ export class CollectionImpl<
1076
1182
  }
1077
1183
  }
1078
1184
 
1079
- if (!hasPersistingTransaction) {
1185
+ const hasTruncateSync = this.pendingSyncedTransactions.some(
1186
+ (t) => t.truncate === true
1187
+ )
1188
+
1189
+ if (!hasPersistingTransaction || hasTruncateSync) {
1080
1190
  // Set flag to prevent redundant optimistic state recalculations
1081
1191
  this.isCommittingSyncTransactions = true
1082
1192
 
@@ -1106,6 +1216,28 @@ export class CollectionImpl<
1106
1216
  const rowUpdateMode = this.config.sync.rowUpdateMode || `partial`
1107
1217
 
1108
1218
  for (const transaction of this.pendingSyncedTransactions) {
1219
+ // Handle truncate operations first
1220
+ if (transaction.truncate) {
1221
+ // TRUNCATE PHASE
1222
+ // 1) Emit a delete for every currently-synced key so downstream listeners/indexes
1223
+ // observe a clear-before-rebuild. We intentionally skip keys already in
1224
+ // optimisticDeletes because their delete was previously emitted by the user.
1225
+ for (const key of this.syncedData.keys()) {
1226
+ if (this.optimisticDeletes.has(key)) continue
1227
+ const previousValue =
1228
+ this.optimisticUpserts.get(key) || this.syncedData.get(key)
1229
+ if (previousValue !== undefined) {
1230
+ events.push({ type: `delete`, key, value: previousValue })
1231
+ }
1232
+ }
1233
+
1234
+ // 2) Clear the authoritative synced base. Subsequent server ops in this
1235
+ // same commit will rebuild the base atomically.
1236
+ this.syncedData.clear()
1237
+ this.syncedMetadata.clear()
1238
+ this.syncedKeys.clear()
1239
+ }
1240
+
1109
1241
  for (const operation of transaction.operations) {
1110
1242
  const key = operation.key as TKey
1111
1243
  this.syncedKeys.add(key)
@@ -1155,7 +1287,101 @@ export class CollectionImpl<
1155
1287
  }
1156
1288
  }
1157
1289
 
1158
- // Clear optimistic state since sync operations will now provide the authoritative data
1290
+ // After applying synced operations, if this commit included a truncate,
1291
+ // re-apply optimistic mutations on top of the fresh synced base. This ensures
1292
+ // the UI preserves local intent while respecting server rebuild semantics.
1293
+ // Ordering: deletes (above) -> server ops (just applied) -> optimistic upserts.
1294
+ const hadTruncate = this.pendingSyncedTransactions.some(
1295
+ (t) => t.truncate === true
1296
+ )
1297
+ if (hadTruncate) {
1298
+ // Avoid duplicating keys that were inserted/updated by synced operations in this commit
1299
+ const syncedInsertedOrUpdatedKeys = new Set<TKey>()
1300
+ for (const t of this.pendingSyncedTransactions) {
1301
+ for (const op of t.operations) {
1302
+ if (op.type === `insert` || op.type === `update`) {
1303
+ syncedInsertedOrUpdatedKeys.add(op.key as TKey)
1304
+ }
1305
+ }
1306
+ }
1307
+
1308
+ // Build re-apply sets from ACTIVE optimistic transactions against the new synced base
1309
+ // We do not copy maps; we compute intent directly from transactions to avoid drift.
1310
+ const reapplyUpserts = new Map<TKey, T>()
1311
+ const reapplyDeletes = new Set<TKey>()
1312
+
1313
+ for (const tx of this.transactions.values()) {
1314
+ if ([`completed`, `failed`].includes(tx.state)) continue
1315
+ for (const mutation of tx.mutations) {
1316
+ if (mutation.collection !== this || !mutation.optimistic) continue
1317
+ const key = mutation.key as TKey
1318
+ switch (mutation.type) {
1319
+ case `insert`:
1320
+ reapplyUpserts.set(key, mutation.modified as T)
1321
+ reapplyDeletes.delete(key)
1322
+ break
1323
+ case `update`: {
1324
+ const base = this.syncedData.get(key)
1325
+ const next = base
1326
+ ? (Object.assign({}, base, mutation.changes) as T)
1327
+ : (mutation.modified as T)
1328
+ reapplyUpserts.set(key, next)
1329
+ reapplyDeletes.delete(key)
1330
+ break
1331
+ }
1332
+ case `delete`:
1333
+ reapplyUpserts.delete(key)
1334
+ reapplyDeletes.add(key)
1335
+ break
1336
+ }
1337
+ }
1338
+ }
1339
+
1340
+ // Emit inserts for re-applied upserts, skipping any keys that have an optimistic delete.
1341
+ // If the server also inserted/updated the same key in this batch, override that value
1342
+ // with the optimistic value to preserve local intent.
1343
+ for (const [key, value] of reapplyUpserts) {
1344
+ if (reapplyDeletes.has(key)) continue
1345
+ if (syncedInsertedOrUpdatedKeys.has(key)) {
1346
+ let foundInsert = false
1347
+ for (let i = events.length - 1; i >= 0; i--) {
1348
+ const evt = events[i]!
1349
+ if (evt.key === key && evt.type === `insert`) {
1350
+ evt.value = value
1351
+ foundInsert = true
1352
+ break
1353
+ }
1354
+ }
1355
+ if (!foundInsert) {
1356
+ events.push({ type: `insert`, key, value })
1357
+ }
1358
+ } else {
1359
+ events.push({ type: `insert`, key, value })
1360
+ }
1361
+ }
1362
+
1363
+ // Finally, ensure we do NOT insert keys that have an outstanding optimistic delete.
1364
+ if (events.length > 0 && reapplyDeletes.size > 0) {
1365
+ const filtered: Array<ChangeMessage<T, TKey>> = []
1366
+ for (const evt of events) {
1367
+ if (evt.type === `insert` && reapplyDeletes.has(evt.key)) {
1368
+ continue
1369
+ }
1370
+ filtered.push(evt)
1371
+ }
1372
+ events.length = 0
1373
+ events.push(...filtered)
1374
+ }
1375
+
1376
+ // Ensure listeners are active before emitting this critical batch
1377
+ if (!this.isReady()) {
1378
+ this.setStatus(`ready`)
1379
+ }
1380
+ }
1381
+
1382
+ // Maintain optimistic state appropriately
1383
+ // Clear optimistic state since sync operations will now provide the authoritative data.
1384
+ // Any still-active user transactions will be re-applied below in recompute.
1159
1385
  this.optimisticUpserts.clear()
1160
1386
  this.optimisticDeletes.clear()
1161
1387
 
@@ -1324,8 +1550,8 @@ export class CollectionImpl<
1324
1550
 
1325
1551
  /**
1326
1552
  * Creates an index on a collection for faster queries.
1327
- * Indexes significantly improve query performance by allowing binary search
1328
- * and range queries instead of full scans.
1553
+ * Indexes significantly improve query performance by allowing constant time lookups
1554
+ * and logarithmic time range queries instead of full scans.
1329
1555
  *
1330
1556
  * @template TResolver - The type of the index resolver (constructor or async loader)
1331
1557
  * @param indexCallback - Function that extracts the indexed value from each item
package/src/errors.ts CHANGED
@@ -377,6 +377,12 @@ export class UnknownFunctionError extends QueryCompilationError {
377
377
  }
378
378
  }
379
379
 
380
+ export class JoinCollectionNotFoundError extends QueryCompilationError {
381
+ constructor(collectionId: string) {
382
+ super(`Collection "${collectionId}" not found during compilation of join`)
383
+ }
384
+ }
385
+
380
386
  // JOIN Operation Errors
381
387
  export class JoinError extends TanStackDBError {
382
388
  constructor(message: string) {
@@ -6,6 +6,57 @@ export interface AutoIndexConfig {
6
6
  autoIndex?: `off` | `eager`
7
7
  }
8
8
 
9
+ function shouldAutoIndex(collection: CollectionImpl<any, any, any, any, any>) {
10
+ // Only proceed if auto-indexing is enabled
11
+ if (collection.config.autoIndex !== `eager`) {
12
+ return false
13
+ }
14
+
15
+ // Don't auto-index during sync operations
16
+ if (
17
+ collection.status === `loading` ||
18
+ collection.status === `initialCommit`
19
+ ) {
20
+ return false
21
+ }
22
+
23
+ return true
24
+ }
25
+
26
+ export function ensureIndexForField<
27
+ T extends Record<string, any>,
28
+ TKey extends string | number,
29
+ >(
30
+ fieldName: string,
31
+ fieldPath: Array<string>,
32
+ collection: CollectionImpl<T, TKey, any, any, any>,
33
+ compareFn?: (a: any, b: any) => number
34
+ ) {
35
+ if (!shouldAutoIndex(collection)) {
36
+ return
37
+ }
38
+
39
+ // Check if we already have an index for this field
40
+ const existingIndex = Array.from(collection.indexes.values()).find((index) =>
41
+ index.matchesField(fieldPath)
42
+ )
43
+
44
+ if (existingIndex) {
45
+ return // Index already exists
46
+ }
47
+
48
+ // Create a new index for this field using the collection's createIndex method
49
+ try {
50
+ collection.createIndex((row) => (row as any)[fieldName], {
51
+ name: `auto_${fieldName}`,
52
+ indexType: BTreeIndex,
53
+ options: compareFn ? { compareFn } : {},
54
+ })
55
+ } catch (error) {
56
+ console.warn(`Failed to create auto-index for field "${fieldName}":`, error)
57
+ }
58
+ }
59
+
9
60
  /**
10
61
  * Analyzes a where expression and creates indexes for all simple operations on single fields
11
62
  */
@@ -16,16 +67,7 @@ export function ensureIndexForExpression<
16
67
  expression: BasicExpression,
17
68
  collection: CollectionImpl<T, TKey, any, any, any>
18
69
  ): void {
19
- // Only proceed if auto-indexing is enabled
20
- if (collection.config.autoIndex !== `eager`) {
21
- return
22
- }
23
-
24
- // Don't auto-index during sync operations
25
- if (
26
- collection.status === `loading` ||
27
- collection.status === `initialCommit`
28
- ) {
70
+ if (!shouldAutoIndex(collection)) {
29
71
  return
30
72
  }
31
73
 
@@ -33,27 +75,7 @@ export function ensureIndexForExpression<
33
75
  const indexableExpressions = extractIndexableExpressions(expression)
34
76
 
35
77
  for (const { fieldName, fieldPath } of indexableExpressions) {
36
- // Check if we already have an index for this field
37
- const existingIndex = Array.from(collection.indexes.values()).find(
38
- (index) => index.matchesField(fieldPath)
39
- )
40
-
41
- if (existingIndex) {
42
- continue // Index already exists
43
- }
44
-
45
- // Create a new index for this field using the collection's createIndex method
46
- try {
47
- collection.createIndex((row) => (row as any)[fieldName], {
48
- name: `auto_${fieldName}`,
49
- indexType: BTreeIndex,
50
- })
51
- } catch (error) {
52
- console.warn(
53
- `Failed to create auto-index for field "${fieldName}":`,
54
- error
55
- )
56
- }
78
+ ensureIndexForField(fieldName, fieldPath, collection)
57
79
  }
58
80
  }
59
81
 
@@ -1,6 +1,6 @@
1
1
  import { compileSingleRowExpression } from "../query/compiler/evaluators.js"
2
2
  import { comparisonFunctions } from "../query/builder/functions.js"
3
- import type { BasicExpression } from "../query/ir.js"
3
+ import type { BasicExpression, OrderByDirection } from "../query/ir.js"
4
4
 
5
5
  /**
6
6
  * Operations that indexes can support, imported from available comparison functions
@@ -56,6 +56,11 @@ export abstract class BaseIndex<
56
56
  abstract build(entries: Iterable<[TKey, any]>): void
57
57
  abstract clear(): void
58
58
  abstract lookup(operation: IndexOperation, value: any): Set<TKey>
59
+ abstract take(
60
+ n: number,
61
+ direction?: OrderByDirection,
62
+ from?: TKey
63
+ ): Array<TKey>
59
64
  abstract get keyCount(): number
60
65
 
61
66
  // Common methods
@@ -1,5 +1,5 @@
1
- import { ascComparator } from "../utils/comparison.js"
2
1
  import { BTree } from "../utils/btree.js"
2
+ import { defaultComparator } from "../utils/comparison.js"
3
3
  import { BaseIndex } from "./base-index.js"
4
4
  import type { BasicExpression } from "../query/ir.js"
5
5
  import type { IndexOperation } from "./base-index.js"
@@ -43,7 +43,7 @@ export class BTreeIndex<
43
43
  private orderedEntries: BTree<any, undefined> // we don't associate values with the keys of the B+ tree (the keys are indexed values)
44
44
  private valueMap = new Map<any, Set<TKey>>() // instead we store a mapping of indexed values to a set of PKs
45
45
  private indexedKeys = new Set<TKey>()
46
- private compareFn: (a: any, b: any) => number = ascComparator
46
+ private compareFn: (a: any, b: any) => number = defaultComparator
47
47
 
48
48
  constructor(
49
49
  id: number,
@@ -52,7 +52,7 @@ export class BTreeIndex<
52
52
  options?: any
53
53
  ) {
54
54
  super(id, expression, name, options)
55
- this.compareFn = options?.compareFn ?? ascComparator
55
+ this.compareFn = options?.compareFn ?? defaultComparator
56
56
  this.orderedEntries = new BTree(this.compareFn)
57
57
  }
58
58
 
@@ -230,6 +230,35 @@ export class BTreeIndex<
230
230
  return result
231
231
  }
232
232
 
233
+ /**
234
+ * Returns the next n items after the provided item or the first n items if no from item is provided.
235
+ * @param n - The number of items to return
236
+ * @param from - The item to start from (exclusive). Starts from the smallest item (inclusive) if not provided.
237
+ * @returns The next n items after the provided key. Returns the first n items if no from item is provided.
238
+ */
239
+ take(n: number, from?: any): Array<TKey> {
240
+ const keysInResult: Set<TKey> = new Set()
241
+ const result: Array<TKey> = []
242
+ const nextKey = (k?: any) => this.orderedEntries.nextHigherKey(k)
243
+ let key = from
244
+
245
+ while ((key = nextKey(key)) && result.length < n) {
246
+ const keys = this.valueMap.get(key)
247
+ if (keys) {
248
+ const it = keys.values()
249
+ let ks: TKey | undefined
250
+ while (result.length < n && (ks = it.next().value)) {
251
+ if (!keysInResult.has(ks)) {
252
+ result.push(ks)
253
+ keysInResult.add(ks)
254
+ }
255
+ }
256
+ }
257
+ }
258
+
259
+ return result
260
+ }
261
+
233
262
  /**
234
263
  * Performs an IN array lookup
235
264
  */
@@ -8,7 +8,7 @@ export interface IndexOptions<TResolver extends IndexResolver = IndexResolver> {
8
8
  indexType?: TResolver
9
9
  options?: TResolver extends IndexConstructor<any>
10
10
  ? TResolver extends new (
11
- id: string,
11
+ id: number,
12
12
  expr: any,
13
13
  name?: string,
14
14
  options?: infer O
@@ -17,7 +17,7 @@ export interface IndexOptions<TResolver extends IndexResolver = IndexResolver> {
17
17
  : never
18
18
  : TResolver extends () => Promise<infer TCtor>
19
19
  ? TCtor extends new (
20
- id: string,
20
+ id: number,
21
21
  expr: any,
22
22
  name?: string,
23
23
  options?: infer O
@@ -19,12 +19,14 @@ import type {
19
19
  QueryIR,
20
20
  } from "../ir.js"
21
21
  import type {
22
+ CompareOptions,
22
23
  Context,
23
24
  GroupByCallback,
24
25
  JoinOnCallback,
25
26
  MergeContext,
26
27
  MergeContextWithJoinType,
27
28
  OrderByCallback,
29
+ OrderByOptions,
28
30
  RefProxyForContext,
29
31
  ResultTypeFromSelect,
30
32
  SchemaFromSource,
@@ -478,16 +480,31 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
478
480
  */
479
481
  orderBy(
480
482
  callback: OrderByCallback<TContext>,
481
- direction: OrderByDirection = `asc`
483
+ options: OrderByDirection | OrderByOptions = `asc`
482
484
  ): QueryBuilder<TContext> {
483
485
  const aliases = this._getCurrentAliases()
484
486
  const refProxy = createRefProxy(aliases) as RefProxyForContext<TContext>
485
487
  const result = callback(refProxy)
486
488
 
489
+ const opts: CompareOptions =
490
+ typeof options === `string`
491
+ ? { direction: options, nulls: `first`, stringSort: `locale` }
492
+ : {
493
+ direction: options.direction ?? `asc`,
494
+ nulls: options.nulls ?? `first`,
495
+ stringSort: options.stringSort ?? `locale`,
496
+ locale:
497
+ options.stringSort === `locale` ? options.locale : undefined,
498
+ localeOptions:
499
+ options.stringSort === `locale`
500
+ ? options.localeOptions
501
+ : undefined,
502
+ }
503
+
487
504
  // Create the new OrderBy structure with expression and direction
488
505
  const orderByClause: OrderByClause = {
489
506
  expression: toExpression(result),
490
- direction,
507
+ compareOptions: opts,
491
508
  }
492
509
 
493
510
  const existingOrderBy: OrderBy = this.query.orderBy || []