@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.
- package/dist/cjs/collection.cjs +112 -6
- package/dist/cjs/collection.cjs.map +1 -1
- package/dist/cjs/collection.d.cts +12 -3
- package/dist/cjs/errors.cjs +6 -0
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +3 -0
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/indexes/auto-index.cjs +30 -19
- package/dist/cjs/indexes/auto-index.cjs.map +1 -1
- package/dist/cjs/indexes/auto-index.d.cts +1 -0
- package/dist/cjs/indexes/base-index.cjs.map +1 -1
- package/dist/cjs/indexes/base-index.d.cts +2 -1
- package/dist/cjs/indexes/btree-index.cjs +29 -3
- package/dist/cjs/indexes/btree-index.cjs.map +1 -1
- package/dist/cjs/indexes/btree-index.d.cts +7 -0
- package/dist/cjs/indexes/index-options.d.cts +1 -1
- package/dist/cjs/query/builder/index.cjs +9 -2
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/builder/index.d.cts +2 -2
- package/dist/cjs/query/builder/types.d.cts +27 -6
- package/dist/cjs/query/compiler/evaluators.cjs +2 -2
- package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
- package/dist/cjs/query/compiler/evaluators.d.cts +1 -1
- package/dist/cjs/query/compiler/group-by.cjs +3 -1
- package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.cjs +72 -6
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.d.cts +16 -2
- package/dist/cjs/query/compiler/joins.cjs +111 -12
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/compiler/joins.d.cts +9 -2
- package/dist/cjs/query/compiler/order-by.cjs +80 -23
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.d.cts +12 -2
- package/dist/cjs/query/ir.cjs.map +1 -1
- package/dist/cjs/query/ir.d.cts +2 -1
- package/dist/cjs/query/live-query-collection.cjs +196 -23
- package/dist/cjs/query/live-query-collection.cjs.map +1 -1
- package/dist/cjs/types.d.cts +1 -0
- package/dist/cjs/utils/btree.cjs +15 -0
- package/dist/cjs/utils/btree.cjs.map +1 -1
- package/dist/cjs/utils/btree.d.cts +8 -0
- package/dist/cjs/utils/comparison.cjs +29 -7
- package/dist/cjs/utils/comparison.cjs.map +1 -1
- package/dist/cjs/utils/comparison.d.cts +6 -2
- package/dist/esm/collection.d.ts +12 -3
- package/dist/esm/collection.js +113 -7
- package/dist/esm/collection.js.map +1 -1
- package/dist/esm/errors.d.ts +3 -0
- package/dist/esm/errors.js +6 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/indexes/auto-index.d.ts +1 -0
- package/dist/esm/indexes/auto-index.js +31 -20
- package/dist/esm/indexes/auto-index.js.map +1 -1
- package/dist/esm/indexes/base-index.d.ts +2 -1
- package/dist/esm/indexes/base-index.js.map +1 -1
- package/dist/esm/indexes/btree-index.d.ts +7 -0
- package/dist/esm/indexes/btree-index.js +29 -3
- package/dist/esm/indexes/btree-index.js.map +1 -1
- package/dist/esm/indexes/index-options.d.ts +1 -1
- package/dist/esm/query/builder/index.d.ts +2 -2
- package/dist/esm/query/builder/index.js +9 -2
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/builder/types.d.ts +27 -6
- package/dist/esm/query/compiler/evaluators.d.ts +1 -1
- package/dist/esm/query/compiler/evaluators.js +2 -2
- package/dist/esm/query/compiler/evaluators.js.map +1 -1
- package/dist/esm/query/compiler/group-by.js +3 -1
- package/dist/esm/query/compiler/group-by.js.map +1 -1
- package/dist/esm/query/compiler/index.d.ts +16 -2
- package/dist/esm/query/compiler/index.js +73 -7
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/joins.d.ts +9 -2
- package/dist/esm/query/compiler/joins.js +114 -15
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/compiler/order-by.d.ts +12 -2
- package/dist/esm/query/compiler/order-by.js +81 -24
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/ir.d.ts +2 -1
- package/dist/esm/query/ir.js.map +1 -1
- package/dist/esm/query/live-query-collection.js +196 -23
- package/dist/esm/query/live-query-collection.js.map +1 -1
- package/dist/esm/types.d.ts +1 -0
- package/dist/esm/utils/btree.d.ts +8 -0
- package/dist/esm/utils/btree.js +15 -0
- package/dist/esm/utils/btree.js.map +1 -1
- package/dist/esm/utils/comparison.d.ts +6 -2
- package/dist/esm/utils/comparison.js +30 -8
- package/dist/esm/utils/comparison.js.map +1 -1
- package/package.json +2 -2
- package/src/collection.ts +237 -11
- package/src/errors.ts +6 -0
- package/src/indexes/auto-index.ts +53 -31
- package/src/indexes/base-index.ts +6 -1
- package/src/indexes/btree-index.ts +32 -3
- package/src/indexes/index-options.ts +2 -2
- package/src/query/builder/index.ts +19 -2
- package/src/query/builder/types.ts +48 -15
- package/src/query/compiler/evaluators.ts +6 -3
- package/src/query/compiler/group-by.ts +3 -1
- package/src/query/compiler/index.ts +112 -5
- package/src/query/compiler/joins.ts +216 -20
- package/src/query/compiler/order-by.ts +117 -26
- package/src/query/ir.ts +2 -1
- package/src/query/live-query-collection.ts +352 -24
- package/src/types.ts +1 -0
- package/src/utils/btree.ts +17 -0
- 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
|
|
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.
|
|
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
|
-
|
|
491
|
-
!
|
|
492
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 ??
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
507
|
+
compareOptions: opts,
|
|
491
508
|
}
|
|
492
509
|
|
|
493
510
|
const existingOrderBy: OrderBy = this.query.orderBy || []
|