@tanstack/db 0.1.3 → 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 +3 -2
- 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 +26 -0
- 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/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 +62 -3
- 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/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/esm/collection.d.ts +3 -2
- 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 +26 -0
- package/dist/esm/indexes/btree-index.js.map +1 -1
- package/dist/esm/indexes/index-options.d.ts +1 -1
- 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 +62 -3
- package/dist/esm/query/compiler/order-by.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/package.json +2 -2
- package/src/collection.ts +163 -10
- 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 +29 -0
- package/src/indexes/index-options.ts +2 -2
- 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 +98 -3
- package/src/query/live-query-collection.ts +352 -24
- package/src/types.ts +1 -0
- package/src/utils/btree.ts +17 -0
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
|
/**
|
|
@@ -392,9 +393,8 @@ export class CollectionImpl<
|
|
|
392
393
|
this.onFirstReadyCallbacks = []
|
|
393
394
|
callbacks.forEach((callback) => callback())
|
|
394
395
|
|
|
395
|
-
// If the collection is empty when it becomes ready, emit an empty change event
|
|
396
396
|
// to notify subscribers (like LiveQueryCollection) that the collection is ready
|
|
397
|
-
if (this.
|
|
397
|
+
if (this.changeListeners.size > 0) {
|
|
398
398
|
this.emitEmptyReadyEvent()
|
|
399
399
|
}
|
|
400
400
|
}
|
|
@@ -559,11 +559,16 @@ export class CollectionImpl<
|
|
|
559
559
|
|
|
560
560
|
// Check if an item with this key already exists when inserting
|
|
561
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
|
|
562
568
|
if (
|
|
563
|
-
|
|
564
|
-
!
|
|
565
|
-
|
|
566
|
-
)
|
|
569
|
+
insertingIntoExistingSynced &&
|
|
570
|
+
!hasPendingDeleteForKey &&
|
|
571
|
+
!isTruncateTransaction
|
|
567
572
|
) {
|
|
568
573
|
throw new DuplicateKeySyncError(key, this.id)
|
|
569
574
|
}
|
|
@@ -600,6 +605,28 @@ export class CollectionImpl<
|
|
|
600
605
|
markReady: () => {
|
|
601
606
|
this.markReady()
|
|
602
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
|
+
},
|
|
603
630
|
})
|
|
604
631
|
|
|
605
632
|
// Store cleanup function if provided
|
|
@@ -957,6 +984,12 @@ export class CollectionImpl<
|
|
|
957
984
|
for (const listener of this.changeListeners) {
|
|
958
985
|
listener([])
|
|
959
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
|
+
}
|
|
960
993
|
}
|
|
961
994
|
|
|
962
995
|
/**
|
|
@@ -1149,7 +1182,11 @@ export class CollectionImpl<
|
|
|
1149
1182
|
}
|
|
1150
1183
|
}
|
|
1151
1184
|
|
|
1152
|
-
|
|
1185
|
+
const hasTruncateSync = this.pendingSyncedTransactions.some(
|
|
1186
|
+
(t) => t.truncate === true
|
|
1187
|
+
)
|
|
1188
|
+
|
|
1189
|
+
if (!hasPersistingTransaction || hasTruncateSync) {
|
|
1153
1190
|
// Set flag to prevent redundant optimistic state recalculations
|
|
1154
1191
|
this.isCommittingSyncTransactions = true
|
|
1155
1192
|
|
|
@@ -1179,6 +1216,28 @@ export class CollectionImpl<
|
|
|
1179
1216
|
const rowUpdateMode = this.config.sync.rowUpdateMode || `partial`
|
|
1180
1217
|
|
|
1181
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
|
+
|
|
1182
1241
|
for (const operation of transaction.operations) {
|
|
1183
1242
|
const key = operation.key as TKey
|
|
1184
1243
|
this.syncedKeys.add(key)
|
|
@@ -1228,7 +1287,101 @@ export class CollectionImpl<
|
|
|
1228
1287
|
}
|
|
1229
1288
|
}
|
|
1230
1289
|
|
|
1231
|
-
//
|
|
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.
|
|
1232
1385
|
this.optimisticUpserts.clear()
|
|
1233
1386
|
this.optimisticDeletes.clear()
|
|
1234
1387
|
|
|
@@ -1397,8 +1550,8 @@ export class CollectionImpl<
|
|
|
1397
1550
|
|
|
1398
1551
|
/**
|
|
1399
1552
|
* Creates an index on a collection for faster queries.
|
|
1400
|
-
* Indexes significantly improve query performance by allowing
|
|
1401
|
-
* 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.
|
|
1402
1555
|
*
|
|
1403
1556
|
* @template TResolver - The type of the index resolver (constructor or async loader)
|
|
1404
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
|
|
@@ -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
|
|
@@ -20,9 +20,12 @@ export type CompiledSingleRowExpression = (item: Record<string, unknown>) => any
|
|
|
20
20
|
* Compiles an expression into an optimized evaluator function.
|
|
21
21
|
* This eliminates branching during evaluation by pre-compiling the expression structure.
|
|
22
22
|
*/
|
|
23
|
-
export function compileExpression(
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
export function compileExpression(
|
|
24
|
+
expr: BasicExpression,
|
|
25
|
+
isSingleRow: boolean = false
|
|
26
|
+
): CompiledExpression | CompiledSingleRowExpression {
|
|
27
|
+
const compiledFn = compileExpressionInternal(expr, isSingleRow)
|
|
28
|
+
return compiledFn
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/**
|
|
@@ -166,7 +166,9 @@ export function processGroupBy(
|
|
|
166
166
|
const mapping = validateAndCreateMapping(groupByClause, selectClause)
|
|
167
167
|
|
|
168
168
|
// Pre-compile groupBy expressions
|
|
169
|
-
const compiledGroupByExpressions = groupByClause.map(
|
|
169
|
+
const compiledGroupByExpressions = groupByClause.map((e) =>
|
|
170
|
+
compileExpression(e)
|
|
171
|
+
)
|
|
170
172
|
|
|
171
173
|
// Create a key extractor function using simple __key_X format
|
|
172
174
|
const keyExtractor = ([, row]: [
|
|
@@ -7,17 +7,21 @@ import {
|
|
|
7
7
|
LimitOffsetRequireOrderByError,
|
|
8
8
|
UnsupportedFromTypeError,
|
|
9
9
|
} from "../../errors.js"
|
|
10
|
+
import { PropRef } from "../ir.js"
|
|
10
11
|
import { compileExpression } from "./evaluators.js"
|
|
11
12
|
import { processJoins } from "./joins.js"
|
|
12
13
|
import { processGroupBy } from "./group-by.js"
|
|
13
14
|
import { processOrderBy } from "./order-by.js"
|
|
14
15
|
import { processSelectToResults } from "./select.js"
|
|
16
|
+
import type { OrderByOptimizationInfo } from "./order-by.js"
|
|
15
17
|
import type {
|
|
16
18
|
BasicExpression,
|
|
17
19
|
CollectionRef,
|
|
18
20
|
QueryIR,
|
|
19
21
|
QueryRef,
|
|
20
22
|
} from "../ir.js"
|
|
23
|
+
import type { LazyCollectionCallbacks } from "./joins.js"
|
|
24
|
+
import type { Collection } from "../../collection.js"
|
|
21
25
|
import type {
|
|
22
26
|
KeyedStream,
|
|
23
27
|
NamespacedAndKeyedStream,
|
|
@@ -29,6 +33,8 @@ import type { QueryCache, QueryMapping } from "./types.js"
|
|
|
29
33
|
* Result of query compilation including both the pipeline and collection-specific WHERE clauses
|
|
30
34
|
*/
|
|
31
35
|
export interface CompilationResult {
|
|
36
|
+
/** The ID of the main collection */
|
|
37
|
+
collectionId: string
|
|
32
38
|
/** The compiled query pipeline */
|
|
33
39
|
pipeline: ResultStream
|
|
34
40
|
/** Map of collection aliases to their WHERE clauses for index optimization */
|
|
@@ -46,6 +52,10 @@ export interface CompilationResult {
|
|
|
46
52
|
export function compileQuery(
|
|
47
53
|
rawQuery: QueryIR,
|
|
48
54
|
inputs: Record<string, KeyedStream>,
|
|
55
|
+
collections: Record<string, Collection<any, any, any, any, any>>,
|
|
56
|
+
callbacks: Record<string, LazyCollectionCallbacks>,
|
|
57
|
+
lazyCollections: Set<string>,
|
|
58
|
+
optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
|
|
49
59
|
cache: QueryCache = new WeakMap(),
|
|
50
60
|
queryMapping: QueryMapping = new WeakMap()
|
|
51
61
|
): CompilationResult {
|
|
@@ -70,9 +80,17 @@ export function compileQuery(
|
|
|
70
80
|
const tables: Record<string, KeyedStream> = {}
|
|
71
81
|
|
|
72
82
|
// Process the FROM clause to get the main table
|
|
73
|
-
const {
|
|
83
|
+
const {
|
|
84
|
+
alias: mainTableAlias,
|
|
85
|
+
input: mainInput,
|
|
86
|
+
collectionId: mainCollectionId,
|
|
87
|
+
} = processFrom(
|
|
74
88
|
query.from,
|
|
75
89
|
allInputs,
|
|
90
|
+
collections,
|
|
91
|
+
callbacks,
|
|
92
|
+
lazyCollections,
|
|
93
|
+
optimizableOrderByCollections,
|
|
76
94
|
cache,
|
|
77
95
|
queryMapping
|
|
78
96
|
)
|
|
@@ -96,10 +114,16 @@ export function compileQuery(
|
|
|
96
114
|
pipeline,
|
|
97
115
|
query.join,
|
|
98
116
|
tables,
|
|
117
|
+
mainCollectionId,
|
|
99
118
|
mainTableAlias,
|
|
100
119
|
allInputs,
|
|
101
120
|
cache,
|
|
102
|
-
queryMapping
|
|
121
|
+
queryMapping,
|
|
122
|
+
collections,
|
|
123
|
+
callbacks,
|
|
124
|
+
lazyCollections,
|
|
125
|
+
optimizableOrderByCollections,
|
|
126
|
+
rawQuery
|
|
103
127
|
)
|
|
104
128
|
}
|
|
105
129
|
|
|
@@ -231,8 +255,11 @@ export function compileQuery(
|
|
|
231
255
|
// Process orderBy parameter if it exists
|
|
232
256
|
if (query.orderBy && query.orderBy.length > 0) {
|
|
233
257
|
const orderedPipeline = processOrderBy(
|
|
258
|
+
rawQuery,
|
|
234
259
|
pipeline,
|
|
235
260
|
query.orderBy,
|
|
261
|
+
collections[mainCollectionId]!,
|
|
262
|
+
optimizableOrderByCollections,
|
|
236
263
|
query.limit,
|
|
237
264
|
query.offset
|
|
238
265
|
)
|
|
@@ -249,6 +276,7 @@ export function compileQuery(
|
|
|
249
276
|
const result = resultPipeline
|
|
250
277
|
// Cache the result before returning (use original query as key)
|
|
251
278
|
const compilationResult = {
|
|
279
|
+
collectionId: mainCollectionId,
|
|
252
280
|
pipeline: result,
|
|
253
281
|
collectionWhereClauses,
|
|
254
282
|
}
|
|
@@ -275,6 +303,7 @@ export function compileQuery(
|
|
|
275
303
|
const result = resultPipeline
|
|
276
304
|
// Cache the result before returning (use original query as key)
|
|
277
305
|
const compilationResult = {
|
|
306
|
+
collectionId: mainCollectionId,
|
|
278
307
|
pipeline: result,
|
|
279
308
|
collectionWhereClauses,
|
|
280
309
|
}
|
|
@@ -289,16 +318,20 @@ export function compileQuery(
|
|
|
289
318
|
function processFrom(
|
|
290
319
|
from: CollectionRef | QueryRef,
|
|
291
320
|
allInputs: Record<string, KeyedStream>,
|
|
321
|
+
collections: Record<string, Collection>,
|
|
322
|
+
callbacks: Record<string, LazyCollectionCallbacks>,
|
|
323
|
+
lazyCollections: Set<string>,
|
|
324
|
+
optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
|
|
292
325
|
cache: QueryCache,
|
|
293
326
|
queryMapping: QueryMapping
|
|
294
|
-
): { alias: string; input: KeyedStream } {
|
|
327
|
+
): { alias: string; input: KeyedStream; collectionId: string } {
|
|
295
328
|
switch (from.type) {
|
|
296
329
|
case `collectionRef`: {
|
|
297
330
|
const input = allInputs[from.collection.id]
|
|
298
331
|
if (!input) {
|
|
299
332
|
throw new CollectionInputNotFoundError(from.collection.id)
|
|
300
333
|
}
|
|
301
|
-
return { alias: from.alias, input }
|
|
334
|
+
return { alias: from.alias, input, collectionId: from.collection.id }
|
|
302
335
|
}
|
|
303
336
|
case `queryRef`: {
|
|
304
337
|
// Find the original query for caching purposes
|
|
@@ -308,6 +341,10 @@ function processFrom(
|
|
|
308
341
|
const subQueryResult = compileQuery(
|
|
309
342
|
originalQuery,
|
|
310
343
|
allInputs,
|
|
344
|
+
collections,
|
|
345
|
+
callbacks,
|
|
346
|
+
lazyCollections,
|
|
347
|
+
optimizableOrderByCollections,
|
|
311
348
|
cache,
|
|
312
349
|
queryMapping
|
|
313
350
|
)
|
|
@@ -324,7 +361,11 @@ function processFrom(
|
|
|
324
361
|
})
|
|
325
362
|
)
|
|
326
363
|
|
|
327
|
-
return {
|
|
364
|
+
return {
|
|
365
|
+
alias: from.alias,
|
|
366
|
+
input: extractedInput,
|
|
367
|
+
collectionId: subQueryResult.collectionId,
|
|
368
|
+
}
|
|
328
369
|
}
|
|
329
370
|
default:
|
|
330
371
|
throw new UnsupportedFromTypeError((from as any).type)
|
|
@@ -380,3 +421,69 @@ function mapNestedQueries(
|
|
|
380
421
|
}
|
|
381
422
|
}
|
|
382
423
|
}
|
|
424
|
+
|
|
425
|
+
function getRefFromAlias(
|
|
426
|
+
query: QueryIR,
|
|
427
|
+
alias: string
|
|
428
|
+
): CollectionRef | QueryRef | void {
|
|
429
|
+
if (query.from.alias === alias) {
|
|
430
|
+
return query.from
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
for (const join of query.join || []) {
|
|
434
|
+
if (join.from.alias === alias) {
|
|
435
|
+
return join.from
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Follows the given reference in a query
|
|
442
|
+
* until its finds the root field the reference points to.
|
|
443
|
+
* @returns The collection, its alias, and the path to the root field in this collection
|
|
444
|
+
*/
|
|
445
|
+
export function followRef(
|
|
446
|
+
query: QueryIR,
|
|
447
|
+
ref: PropRef<any>,
|
|
448
|
+
collection: Collection
|
|
449
|
+
): { collection: Collection; path: Array<string> } | void {
|
|
450
|
+
if (ref.path.length === 0) {
|
|
451
|
+
return
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (ref.path.length === 1) {
|
|
455
|
+
// This field should be part of this collection
|
|
456
|
+
const field = ref.path[0]!
|
|
457
|
+
// is it part of the select clause?
|
|
458
|
+
if (query.select) {
|
|
459
|
+
const selectedField = query.select[field]
|
|
460
|
+
if (selectedField && selectedField.type === `ref`) {
|
|
461
|
+
return followRef(query, selectedField, collection)
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Either this field is not part of the select clause
|
|
466
|
+
// and thus it must be part of the collection itself
|
|
467
|
+
// or it is part of the select but is not a reference
|
|
468
|
+
// so we can stop here and don't have to follow it
|
|
469
|
+
return { collection, path: [field] }
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (ref.path.length > 1) {
|
|
473
|
+
// This is a nested field
|
|
474
|
+
const [alias, ...rest] = ref.path
|
|
475
|
+
const aliasRef = getRefFromAlias(query, alias!)
|
|
476
|
+
if (!aliasRef) {
|
|
477
|
+
return
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (aliasRef.type === `queryRef`) {
|
|
481
|
+
return followRef(aliasRef.query, new PropRef(rest), collection)
|
|
482
|
+
} else {
|
|
483
|
+
// This is a reference to a collection
|
|
484
|
+
// we can't follow it further
|
|
485
|
+
// so the field must be on the collection itself
|
|
486
|
+
return { collection: aliasRef.collection, path: rest }
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|