@tanstack/db 0.1.8 → 0.1.10
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 +29 -30
- package/dist/cjs/collection.cjs.map +1 -1
- package/dist/cjs/collection.d.cts +0 -1
- package/dist/cjs/proxy.cjs +9 -58
- package/dist/cjs/proxy.cjs.map +1 -1
- package/dist/cjs/query/builder/index.cjs +7 -4
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/group-by.cjs +7 -6
- package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/group-by.d.cts +5 -1
- package/dist/cjs/query/compiler/index.cjs +1 -0
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.cjs +13 -5
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/order-by.d.cts +2 -2
- package/dist/cjs/query/live/collection-config-builder.cjs +3 -0
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-config-builder.d.cts +2 -2
- package/dist/cjs/utils.cjs +75 -0
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +5 -0
- package/dist/esm/collection.d.ts +0 -1
- package/dist/esm/collection.js +29 -30
- package/dist/esm/collection.js.map +1 -1
- package/dist/esm/proxy.js +9 -58
- package/dist/esm/proxy.js.map +1 -1
- package/dist/esm/query/builder/index.js +7 -4
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/compiler/group-by.d.ts +5 -1
- package/dist/esm/query/compiler/group-by.js +8 -7
- package/dist/esm/query/compiler/group-by.js.map +1 -1
- package/dist/esm/query/compiler/index.js +1 -0
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/order-by.d.ts +2 -2
- package/dist/esm/query/compiler/order-by.js +13 -5
- package/dist/esm/query/compiler/order-by.js.map +1 -1
- package/dist/esm/query/live/collection-config-builder.d.ts +2 -2
- package/dist/esm/query/live/collection-config-builder.js +3 -0
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/utils.d.ts +5 -0
- package/dist/esm/utils.js +76 -1
- package/dist/esm/utils.js.map +1 -1
- package/package.json +3 -2
- package/src/collection.ts +31 -35
- package/src/proxy.ts +16 -107
- package/src/query/builder/index.ts +11 -6
- package/src/query/compiler/group-by.ts +9 -8
- package/src/query/compiler/index.ts +1 -0
- package/src/query/compiler/order-by.ts +14 -5
- package/src/query/live/collection-config-builder.ts +7 -6
- package/src/utils.ts +125 -0
package/src/collection.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { withArrayChangeTracking, withChangeTracking } from "./proxy"
|
|
2
|
+
import { deepEquals } from "./utils"
|
|
2
3
|
import { SortedMap } from "./SortedMap"
|
|
3
4
|
import {
|
|
4
5
|
createSingleRowRefProxy,
|
|
@@ -1194,8 +1195,29 @@ export class CollectionImpl<
|
|
|
1194
1195
|
}
|
|
1195
1196
|
}
|
|
1196
1197
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1198
|
+
// pending synced transactions could be either `committed` or still open.
|
|
1199
|
+
// we only want to process `committed` transactions here
|
|
1200
|
+
const {
|
|
1201
|
+
committedSyncedTransactions,
|
|
1202
|
+
uncommittedSyncedTransactions,
|
|
1203
|
+
hasTruncateSync,
|
|
1204
|
+
} = this.pendingSyncedTransactions.reduce(
|
|
1205
|
+
(acc, t) => {
|
|
1206
|
+
if (t.committed) {
|
|
1207
|
+
acc.committedSyncedTransactions.push(t)
|
|
1208
|
+
if (t.truncate === true) {
|
|
1209
|
+
acc.hasTruncateSync = true
|
|
1210
|
+
}
|
|
1211
|
+
} else {
|
|
1212
|
+
acc.uncommittedSyncedTransactions.push(t)
|
|
1213
|
+
}
|
|
1214
|
+
return acc
|
|
1215
|
+
},
|
|
1216
|
+
{
|
|
1217
|
+
committedSyncedTransactions: [] as Array<PendingSyncedTransaction<T>>,
|
|
1218
|
+
uncommittedSyncedTransactions: [] as Array<PendingSyncedTransaction<T>>,
|
|
1219
|
+
hasTruncateSync: false,
|
|
1220
|
+
}
|
|
1199
1221
|
)
|
|
1200
1222
|
|
|
1201
1223
|
if (!hasPersistingTransaction || hasTruncateSync) {
|
|
@@ -1204,7 +1226,7 @@ export class CollectionImpl<
|
|
|
1204
1226
|
|
|
1205
1227
|
// First collect all keys that will be affected by sync operations
|
|
1206
1228
|
const changedKeys = new Set<TKey>()
|
|
1207
|
-
for (const transaction of
|
|
1229
|
+
for (const transaction of committedSyncedTransactions) {
|
|
1208
1230
|
for (const operation of transaction.operations) {
|
|
1209
1231
|
changedKeys.add(operation.key as TKey)
|
|
1210
1232
|
}
|
|
@@ -1227,7 +1249,7 @@ export class CollectionImpl<
|
|
|
1227
1249
|
const events: Array<ChangeMessage<T, TKey>> = []
|
|
1228
1250
|
const rowUpdateMode = this.config.sync.rowUpdateMode || `partial`
|
|
1229
1251
|
|
|
1230
|
-
for (const transaction of
|
|
1252
|
+
for (const transaction of committedSyncedTransactions) {
|
|
1231
1253
|
// Handle truncate operations first
|
|
1232
1254
|
if (transaction.truncate) {
|
|
1233
1255
|
// TRUNCATE PHASE
|
|
@@ -1303,13 +1325,10 @@ export class CollectionImpl<
|
|
|
1303
1325
|
// re-apply optimistic mutations on top of the fresh synced base. This ensures
|
|
1304
1326
|
// the UI preserves local intent while respecting server rebuild semantics.
|
|
1305
1327
|
// Ordering: deletes (above) -> server ops (just applied) -> optimistic upserts.
|
|
1306
|
-
|
|
1307
|
-
(t) => t.truncate === true
|
|
1308
|
-
)
|
|
1309
|
-
if (hadTruncate) {
|
|
1328
|
+
if (hasTruncateSync) {
|
|
1310
1329
|
// Avoid duplicating keys that were inserted/updated by synced operations in this commit
|
|
1311
1330
|
const syncedInsertedOrUpdatedKeys = new Set<TKey>()
|
|
1312
|
-
for (const t of
|
|
1331
|
+
for (const t of committedSyncedTransactions) {
|
|
1313
1332
|
for (const op of t.operations) {
|
|
1314
1333
|
if (op.type === `insert` || op.type === `update`) {
|
|
1315
1334
|
syncedInsertedOrUpdatedKeys.add(op.key as TKey)
|
|
@@ -1448,7 +1467,7 @@ export class CollectionImpl<
|
|
|
1448
1467
|
const isRedundantSync =
|
|
1449
1468
|
completedOp &&
|
|
1450
1469
|
newVisibleValue !== undefined &&
|
|
1451
|
-
|
|
1470
|
+
deepEquals(completedOp.value, newVisibleValue)
|
|
1452
1471
|
|
|
1453
1472
|
if (!isRedundantSync) {
|
|
1454
1473
|
if (
|
|
@@ -1472,7 +1491,7 @@ export class CollectionImpl<
|
|
|
1472
1491
|
} else if (
|
|
1473
1492
|
previousVisibleValue !== undefined &&
|
|
1474
1493
|
newVisibleValue !== undefined &&
|
|
1475
|
-
!
|
|
1494
|
+
!deepEquals(previousVisibleValue, newVisibleValue)
|
|
1476
1495
|
) {
|
|
1477
1496
|
events.push({
|
|
1478
1497
|
type: `update`,
|
|
@@ -1495,7 +1514,7 @@ export class CollectionImpl<
|
|
|
1495
1514
|
// End batching and emit all events (combines any batched events with sync events)
|
|
1496
1515
|
this.emitEvents(events, true)
|
|
1497
1516
|
|
|
1498
|
-
this.pendingSyncedTransactions =
|
|
1517
|
+
this.pendingSyncedTransactions = uncommittedSyncedTransactions
|
|
1499
1518
|
|
|
1500
1519
|
// Clear the pre-sync state since sync operations are complete
|
|
1501
1520
|
this.preSyncVisibleState.clear()
|
|
@@ -1715,29 +1734,6 @@ export class CollectionImpl<
|
|
|
1715
1734
|
}
|
|
1716
1735
|
}
|
|
1717
1736
|
|
|
1718
|
-
private deepEqual(a: any, b: any): boolean {
|
|
1719
|
-
if (a === b) return true
|
|
1720
|
-
if (a == null || b == null) return false
|
|
1721
|
-
if (typeof a !== typeof b) return false
|
|
1722
|
-
|
|
1723
|
-
if (typeof a === `object`) {
|
|
1724
|
-
if (Array.isArray(a) !== Array.isArray(b)) return false
|
|
1725
|
-
|
|
1726
|
-
const keysA = Object.keys(a)
|
|
1727
|
-
const keysB = Object.keys(b)
|
|
1728
|
-
if (keysA.length !== keysB.length) return false
|
|
1729
|
-
|
|
1730
|
-
const keysBSet = new Set(keysB)
|
|
1731
|
-
for (const key of keysA) {
|
|
1732
|
-
if (!keysBSet.has(key)) return false
|
|
1733
|
-
if (!this.deepEqual(a[key], b[key])) return false
|
|
1734
|
-
}
|
|
1735
|
-
return true
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
return false
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
1737
|
public validateData(
|
|
1742
1738
|
data: unknown,
|
|
1743
1739
|
type: `insert` | `update`,
|
package/src/proxy.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* and provides a way to retrieve those changes.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { deepEquals, isTemporal } from "./utils"
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* Simple debug utility that only logs when debug mode is enabled
|
|
8
10
|
* Set DEBUG to true in localStorage to enable debug logging
|
|
@@ -133,6 +135,13 @@ function deepClone<T extends unknown>(
|
|
|
133
135
|
return clone as unknown as T
|
|
134
136
|
}
|
|
135
137
|
|
|
138
|
+
// Handle Temporal objects
|
|
139
|
+
if (isTemporal(obj)) {
|
|
140
|
+
// Temporal objects are immutable, so we can return them directly
|
|
141
|
+
// This preserves all their internal state correctly
|
|
142
|
+
return obj
|
|
143
|
+
}
|
|
144
|
+
|
|
136
145
|
const clone = {} as Record<string | symbol, unknown>
|
|
137
146
|
visited.set(obj as object, clone)
|
|
138
147
|
|
|
@@ -156,107 +165,6 @@ function deepClone<T extends unknown>(
|
|
|
156
165
|
return clone as T
|
|
157
166
|
}
|
|
158
167
|
|
|
159
|
-
/**
|
|
160
|
-
* Deep equality check that handles special types like Date, RegExp, Map, and Set
|
|
161
|
-
*/
|
|
162
|
-
function deepEqual<T>(a: T, b: T): boolean {
|
|
163
|
-
// Handle primitive types
|
|
164
|
-
if (a === b) return true
|
|
165
|
-
|
|
166
|
-
// If either is null or not an object, they're not equal
|
|
167
|
-
if (
|
|
168
|
-
a === null ||
|
|
169
|
-
b === null ||
|
|
170
|
-
typeof a !== `object` ||
|
|
171
|
-
typeof b !== `object`
|
|
172
|
-
) {
|
|
173
|
-
return false
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Handle Date objects
|
|
177
|
-
if (a instanceof Date && b instanceof Date) {
|
|
178
|
-
return a.getTime() === b.getTime()
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Handle RegExp objects
|
|
182
|
-
if (a instanceof RegExp && b instanceof RegExp) {
|
|
183
|
-
return a.source === b.source && a.flags === b.flags
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Handle Map objects
|
|
187
|
-
if (a instanceof Map && b instanceof Map) {
|
|
188
|
-
if (a.size !== b.size) return false
|
|
189
|
-
|
|
190
|
-
const entries = Array.from(a.entries())
|
|
191
|
-
for (const [key, val] of entries) {
|
|
192
|
-
if (!b.has(key) || !deepEqual(val, b.get(key))) {
|
|
193
|
-
return false
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return true
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Handle Set objects
|
|
201
|
-
if (a instanceof Set && b instanceof Set) {
|
|
202
|
-
if (a.size !== b.size) return false
|
|
203
|
-
|
|
204
|
-
// Convert to arrays for comparison
|
|
205
|
-
const aValues = Array.from(a)
|
|
206
|
-
const bValues = Array.from(b)
|
|
207
|
-
|
|
208
|
-
// Simple comparison for primitive values
|
|
209
|
-
if (aValues.every((val) => typeof val !== `object`)) {
|
|
210
|
-
return aValues.every((val) => b.has(val))
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// For objects in sets, we need to do a more complex comparison
|
|
214
|
-
// This is a simplified approach and may not work for all cases
|
|
215
|
-
return aValues.length === bValues.length
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Handle arrays
|
|
219
|
-
if (Array.isArray(a) && Array.isArray(b)) {
|
|
220
|
-
if (a.length !== b.length) return false
|
|
221
|
-
|
|
222
|
-
for (let i = 0; i < a.length; i++) {
|
|
223
|
-
if (!deepEqual(a[i], b[i])) return false
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return true
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Handle TypedArrays
|
|
230
|
-
if (
|
|
231
|
-
ArrayBuffer.isView(a) &&
|
|
232
|
-
ArrayBuffer.isView(b) &&
|
|
233
|
-
!(a instanceof DataView) &&
|
|
234
|
-
!(b instanceof DataView)
|
|
235
|
-
) {
|
|
236
|
-
const typedA = a as unknown as TypedArray
|
|
237
|
-
const typedB = b as unknown as TypedArray
|
|
238
|
-
if (typedA.length !== typedB.length) return false
|
|
239
|
-
|
|
240
|
-
for (let i = 0; i < typedA.length; i++) {
|
|
241
|
-
if (typedA[i] !== typedB[i]) return false
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return true
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Handle plain objects
|
|
248
|
-
const keysA = Object.keys(a as object)
|
|
249
|
-
const keysB = Object.keys(b as object)
|
|
250
|
-
|
|
251
|
-
if (keysA.length !== keysB.length) return false
|
|
252
|
-
|
|
253
|
-
return keysA.every(
|
|
254
|
-
(key) =>
|
|
255
|
-
Object.prototype.hasOwnProperty.call(b, key) &&
|
|
256
|
-
deepEqual((a as any)[key], (b as any)[key])
|
|
257
|
-
)
|
|
258
|
-
}
|
|
259
|
-
|
|
260
168
|
let count = 0
|
|
261
169
|
function getProxyCount() {
|
|
262
170
|
count += 1
|
|
@@ -392,7 +300,7 @@ export function createChangeProxy<
|
|
|
392
300
|
)
|
|
393
301
|
|
|
394
302
|
// If the value is not equal to original, something is still changed
|
|
395
|
-
if (!
|
|
303
|
+
if (!deepEquals(currentValue, originalValue)) {
|
|
396
304
|
debugLog(`Property ${String(prop)} is different, returning false`)
|
|
397
305
|
return false
|
|
398
306
|
}
|
|
@@ -411,7 +319,7 @@ export function createChangeProxy<
|
|
|
411
319
|
const originalValue = (state.originalObject as any)[sym]
|
|
412
320
|
|
|
413
321
|
// If the value is not equal to original, something is still changed
|
|
414
|
-
if (!
|
|
322
|
+
if (!deepEquals(currentValue, originalValue)) {
|
|
415
323
|
debugLog(`Symbol property is different, returning false`)
|
|
416
324
|
return false
|
|
417
325
|
}
|
|
@@ -741,12 +649,13 @@ export function createChangeProxy<
|
|
|
741
649
|
return value.bind(ptarget)
|
|
742
650
|
}
|
|
743
651
|
|
|
744
|
-
// If the value is an object, create a proxy for it
|
|
652
|
+
// If the value is an object (but not Date, RegExp, or Temporal), create a proxy for it
|
|
745
653
|
if (
|
|
746
654
|
value &&
|
|
747
655
|
typeof value === `object` &&
|
|
748
656
|
!((value as any) instanceof Date) &&
|
|
749
|
-
!((value as any) instanceof RegExp)
|
|
657
|
+
!((value as any) instanceof RegExp) &&
|
|
658
|
+
!isTemporal(value)
|
|
750
659
|
) {
|
|
751
660
|
// Create a parent reference for the nested object
|
|
752
661
|
const nestedParent = {
|
|
@@ -779,11 +688,11 @@ export function createChangeProxy<
|
|
|
779
688
|
)
|
|
780
689
|
|
|
781
690
|
// Only track the change if the value is actually different
|
|
782
|
-
if (!
|
|
691
|
+
if (!deepEquals(currentValue, value)) {
|
|
783
692
|
// Check if the new value is equal to the original value
|
|
784
693
|
// Important: Use the originalObject to get the true original value
|
|
785
694
|
const originalValue = changeTracker.originalObject[prop as keyof T]
|
|
786
|
-
const isRevertToOriginal =
|
|
695
|
+
const isRevertToOriginal = deepEquals(value, originalValue)
|
|
787
696
|
debugLog(
|
|
788
697
|
`value:`,
|
|
789
698
|
value,
|
|
@@ -14,7 +14,6 @@ import type {
|
|
|
14
14
|
BasicExpression,
|
|
15
15
|
JoinClause,
|
|
16
16
|
OrderBy,
|
|
17
|
-
OrderByClause,
|
|
18
17
|
OrderByDirection,
|
|
19
18
|
QueryIR,
|
|
20
19
|
} from "../ir.js"
|
|
@@ -501,17 +500,23 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
|
|
|
501
500
|
: undefined,
|
|
502
501
|
}
|
|
503
502
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
503
|
+
const makeOrderByClause = (res: any) => {
|
|
504
|
+
return {
|
|
505
|
+
expression: toExpression(res),
|
|
506
|
+
compareOptions: opts,
|
|
507
|
+
}
|
|
508
508
|
}
|
|
509
509
|
|
|
510
|
+
// Create the new OrderBy structure with expression and direction
|
|
511
|
+
const orderByClauses = Array.isArray(result)
|
|
512
|
+
? result.map((r) => makeOrderByClause(r))
|
|
513
|
+
: [makeOrderByClause(result)]
|
|
514
|
+
|
|
510
515
|
const existingOrderBy: OrderBy = this.query.orderBy || []
|
|
511
516
|
|
|
512
517
|
return new BaseQueryBuilder({
|
|
513
518
|
...this.query,
|
|
514
|
-
orderBy: [...existingOrderBy,
|
|
519
|
+
orderBy: [...existingOrderBy, ...orderByClauses],
|
|
515
520
|
}) as any
|
|
516
521
|
}
|
|
517
522
|
|
|
@@ -130,7 +130,7 @@ export function processGroupBy(
|
|
|
130
130
|
if (havingClauses && havingClauses.length > 0) {
|
|
131
131
|
for (const havingClause of havingClauses) {
|
|
132
132
|
const havingExpression = getHavingExpression(havingClause)
|
|
133
|
-
const transformedHavingClause =
|
|
133
|
+
const transformedHavingClause = replaceAggregatesByRefs(
|
|
134
134
|
havingExpression,
|
|
135
135
|
selectClause || {}
|
|
136
136
|
)
|
|
@@ -265,7 +265,7 @@ export function processGroupBy(
|
|
|
265
265
|
if (havingClauses && havingClauses.length > 0) {
|
|
266
266
|
for (const havingClause of havingClauses) {
|
|
267
267
|
const havingExpression = getHavingExpression(havingClause)
|
|
268
|
-
const transformedHavingClause =
|
|
268
|
+
const transformedHavingClause = replaceAggregatesByRefs(
|
|
269
269
|
havingExpression,
|
|
270
270
|
selectClause || {}
|
|
271
271
|
)
|
|
@@ -367,11 +367,12 @@ function getAggregateFunction(aggExpr: Aggregate) {
|
|
|
367
367
|
}
|
|
368
368
|
|
|
369
369
|
/**
|
|
370
|
-
* Transforms
|
|
370
|
+
* Transforms basic expressions and aggregates to replace Agg expressions with references to computed values
|
|
371
371
|
*/
|
|
372
|
-
function
|
|
372
|
+
export function replaceAggregatesByRefs(
|
|
373
373
|
havingExpr: BasicExpression | Aggregate,
|
|
374
|
-
selectClause: Select
|
|
374
|
+
selectClause: Select,
|
|
375
|
+
resultAlias: string = `result`
|
|
375
376
|
): BasicExpression {
|
|
376
377
|
switch (havingExpr.type) {
|
|
377
378
|
case `agg`: {
|
|
@@ -380,7 +381,7 @@ function transformHavingClause(
|
|
|
380
381
|
for (const [alias, selectExpr] of Object.entries(selectClause)) {
|
|
381
382
|
if (selectExpr.type === `agg` && aggregatesEqual(aggExpr, selectExpr)) {
|
|
382
383
|
// Replace with a reference to the computed aggregate
|
|
383
|
-
return new PropRef([
|
|
384
|
+
return new PropRef([resultAlias, alias])
|
|
384
385
|
}
|
|
385
386
|
}
|
|
386
387
|
// If no matching aggregate found in SELECT, throw error
|
|
@@ -392,7 +393,7 @@ function transformHavingClause(
|
|
|
392
393
|
// Transform function arguments recursively
|
|
393
394
|
const transformedArgs = funcExpr.args.map(
|
|
394
395
|
(arg: BasicExpression | Aggregate) =>
|
|
395
|
-
|
|
396
|
+
replaceAggregatesByRefs(arg, selectClause)
|
|
396
397
|
)
|
|
397
398
|
return new Func(funcExpr.name, transformedArgs)
|
|
398
399
|
}
|
|
@@ -404,7 +405,7 @@ function transformHavingClause(
|
|
|
404
405
|
const alias = refExpr.path[0]!
|
|
405
406
|
if (selectClause[alias]) {
|
|
406
407
|
// This is a reference to a SELECT alias, convert to result.alias
|
|
407
|
-
return new PropRef([
|
|
408
|
+
return new PropRef([resultAlias, alias])
|
|
408
409
|
}
|
|
409
410
|
}
|
|
410
411
|
// Return as-is for other refs
|
|
@@ -4,9 +4,10 @@ import { PropRef } from "../ir.js"
|
|
|
4
4
|
import { ensureIndexForField } from "../../indexes/auto-index.js"
|
|
5
5
|
import { findIndexForField } from "../../utils/index-optimization.js"
|
|
6
6
|
import { compileExpression } from "./evaluators.js"
|
|
7
|
+
import { replaceAggregatesByRefs } from "./group-by.js"
|
|
7
8
|
import { followRef } from "./index.js"
|
|
8
9
|
import type { CompiledSingleRowExpression } from "./evaluators.js"
|
|
9
|
-
import type { OrderByClause, QueryIR } from "../ir.js"
|
|
10
|
+
import type { OrderByClause, QueryIR, Select } from "../ir.js"
|
|
10
11
|
import type { NamespacedAndKeyedStream, NamespacedRow } from "../../types.js"
|
|
11
12
|
import type { IStreamBuilder, KeyValue } from "@tanstack/db-ivm"
|
|
12
13
|
import type { BaseIndex } from "../../indexes/base-index.js"
|
|
@@ -33,16 +34,24 @@ export function processOrderBy(
|
|
|
33
34
|
rawQuery: QueryIR,
|
|
34
35
|
pipeline: NamespacedAndKeyedStream,
|
|
35
36
|
orderByClause: Array<OrderByClause>,
|
|
37
|
+
selectClause: Select,
|
|
36
38
|
collection: Collection,
|
|
37
39
|
optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
|
|
38
40
|
limit?: number,
|
|
39
41
|
offset?: number
|
|
40
42
|
): IStreamBuilder<KeyValue<unknown, [NamespacedRow, string]>> {
|
|
41
43
|
// Pre-compile all order by expressions
|
|
42
|
-
const compiledOrderBy = orderByClause.map((clause) =>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
const compiledOrderBy = orderByClause.map((clause) => {
|
|
45
|
+
const clauseWithoutAggregates = replaceAggregatesByRefs(
|
|
46
|
+
clause.expression,
|
|
47
|
+
selectClause,
|
|
48
|
+
`__select_results`
|
|
49
|
+
)
|
|
50
|
+
return {
|
|
51
|
+
compiledExpression: compileExpression(clauseWithoutAggregates),
|
|
52
|
+
compareOptions: clause.compareOptions,
|
|
53
|
+
}
|
|
54
|
+
})
|
|
46
55
|
|
|
47
56
|
// Create a value extractor function for the orderBy operator
|
|
48
57
|
const valueExtractor = (row: NamespacedRow & { __select_results?: any }) => {
|
|
@@ -49,15 +49,11 @@ export class CollectionConfigBuilder<
|
|
|
49
49
|
| undefined
|
|
50
50
|
|
|
51
51
|
// Map of collection IDs to functions that load keys for that lazy collection
|
|
52
|
-
|
|
53
|
-
{}
|
|
52
|
+
lazyCollectionsCallbacks: Record<string, LazyCollectionCallbacks> = {}
|
|
54
53
|
// Set of collection IDs that are lazy collections
|
|
55
54
|
readonly lazyCollections = new Set<string>()
|
|
56
55
|
// Set of collection IDs that include an optimizable ORDER BY clause
|
|
57
|
-
|
|
58
|
-
string,
|
|
59
|
-
OrderByOptimizationInfo
|
|
60
|
-
> = {}
|
|
56
|
+
optimizableOrderByCollections: Record<string, OrderByOptimizationInfo> = {}
|
|
61
57
|
|
|
62
58
|
constructor(
|
|
63
59
|
private readonly config: LiveQueryCollectionConfig<TContext, TResult>
|
|
@@ -168,6 +164,11 @@ export class CollectionConfigBuilder<
|
|
|
168
164
|
this.inputsCache = undefined
|
|
169
165
|
this.pipelineCache = undefined
|
|
170
166
|
this.collectionWhereClausesCache = undefined
|
|
167
|
+
|
|
168
|
+
// Reset lazy collection state
|
|
169
|
+
this.lazyCollections.clear()
|
|
170
|
+
this.optimizableOrderByCollections = {}
|
|
171
|
+
this.lazyCollectionsCallbacks = {}
|
|
171
172
|
}
|
|
172
173
|
}
|
|
173
174
|
|
package/src/utils.ts
CHANGED
|
@@ -2,8 +2,14 @@
|
|
|
2
2
|
* Generic utility functions
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
interface TypedArray {
|
|
6
|
+
length: number
|
|
7
|
+
[index: number]: number
|
|
8
|
+
}
|
|
9
|
+
|
|
5
10
|
/**
|
|
6
11
|
* Deep equality function that compares two values recursively
|
|
12
|
+
* Handles primitives, objects, arrays, Date, RegExp, Map, Set, TypedArrays, and Temporal objects
|
|
7
13
|
*
|
|
8
14
|
* @param a - First value to compare
|
|
9
15
|
* @param b - Second value to compare
|
|
@@ -14,6 +20,8 @@
|
|
|
14
20
|
* deepEquals({ a: 1, b: 2 }, { b: 2, a: 1 }) // true (property order doesn't matter)
|
|
15
21
|
* deepEquals([1, { x: 2 }], [1, { x: 2 }]) // true
|
|
16
22
|
* deepEquals({ a: 1 }, { a: 2 }) // false
|
|
23
|
+
* deepEquals(new Date('2023-01-01'), new Date('2023-01-01')) // true
|
|
24
|
+
* deepEquals(new Map([['a', 1]]), new Map([['a', 1]])) // true
|
|
17
25
|
* ```
|
|
18
26
|
*/
|
|
19
27
|
export function deepEquals(a: any, b: any): boolean {
|
|
@@ -37,6 +45,102 @@ function deepEqualsInternal(
|
|
|
37
45
|
// Handle different types
|
|
38
46
|
if (typeof a !== typeof b) return false
|
|
39
47
|
|
|
48
|
+
// Handle Date objects
|
|
49
|
+
if (a instanceof Date) {
|
|
50
|
+
if (!(b instanceof Date)) return false
|
|
51
|
+
return a.getTime() === b.getTime()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Handle RegExp objects
|
|
55
|
+
if (a instanceof RegExp) {
|
|
56
|
+
if (!(b instanceof RegExp)) return false
|
|
57
|
+
return a.source === b.source && a.flags === b.flags
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Handle Map objects - only if both are Maps
|
|
61
|
+
if (a instanceof Map) {
|
|
62
|
+
if (!(b instanceof Map)) return false
|
|
63
|
+
if (a.size !== b.size) return false
|
|
64
|
+
|
|
65
|
+
// Check for circular references
|
|
66
|
+
if (visited.has(a)) {
|
|
67
|
+
return visited.get(a) === b
|
|
68
|
+
}
|
|
69
|
+
visited.set(a, b)
|
|
70
|
+
|
|
71
|
+
const entries = Array.from(a.entries())
|
|
72
|
+
const result = entries.every(([key, val]) => {
|
|
73
|
+
return b.has(key) && deepEqualsInternal(val, b.get(key), visited)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
visited.delete(a)
|
|
77
|
+
return result
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Handle Set objects - only if both are Sets
|
|
81
|
+
if (a instanceof Set) {
|
|
82
|
+
if (!(b instanceof Set)) return false
|
|
83
|
+
if (a.size !== b.size) return false
|
|
84
|
+
|
|
85
|
+
// Check for circular references
|
|
86
|
+
if (visited.has(a)) {
|
|
87
|
+
return visited.get(a) === b
|
|
88
|
+
}
|
|
89
|
+
visited.set(a, b)
|
|
90
|
+
|
|
91
|
+
// Convert to arrays for comparison
|
|
92
|
+
const aValues = Array.from(a)
|
|
93
|
+
const bValues = Array.from(b)
|
|
94
|
+
|
|
95
|
+
// Simple comparison for primitive values
|
|
96
|
+
if (aValues.every((val) => typeof val !== `object`)) {
|
|
97
|
+
visited.delete(a)
|
|
98
|
+
return aValues.every((val) => b.has(val))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// For objects in sets, we need to do a more complex comparison
|
|
102
|
+
// This is a simplified approach and may not work for all cases
|
|
103
|
+
const result = aValues.length === bValues.length
|
|
104
|
+
visited.delete(a)
|
|
105
|
+
return result
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Handle TypedArrays
|
|
109
|
+
if (
|
|
110
|
+
ArrayBuffer.isView(a) &&
|
|
111
|
+
ArrayBuffer.isView(b) &&
|
|
112
|
+
!(a instanceof DataView) &&
|
|
113
|
+
!(b instanceof DataView)
|
|
114
|
+
) {
|
|
115
|
+
const typedA = a as unknown as TypedArray
|
|
116
|
+
const typedB = b as unknown as TypedArray
|
|
117
|
+
if (typedA.length !== typedB.length) return false
|
|
118
|
+
|
|
119
|
+
for (let i = 0; i < typedA.length; i++) {
|
|
120
|
+
if (typedA[i] !== typedB[i]) return false
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return true
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Handle Temporal objects
|
|
127
|
+
// Check if both are Temporal objects of the same type
|
|
128
|
+
if (isTemporal(a) && isTemporal(b)) {
|
|
129
|
+
const aTag = getStringTag(a)
|
|
130
|
+
const bTag = getStringTag(b)
|
|
131
|
+
|
|
132
|
+
// If they're different Temporal types, they're not equal
|
|
133
|
+
if (aTag !== bTag) return false
|
|
134
|
+
|
|
135
|
+
// Use Temporal's built-in equals method if available
|
|
136
|
+
if (typeof a.equals === `function`) {
|
|
137
|
+
return a.equals(b)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Fallback to toString comparison for other types
|
|
141
|
+
return a.toString() === b.toString()
|
|
142
|
+
}
|
|
143
|
+
|
|
40
144
|
// Handle arrays
|
|
41
145
|
if (Array.isArray(a)) {
|
|
42
146
|
if (!Array.isArray(b) || a.length !== b.length) return false
|
|
@@ -84,3 +188,24 @@ function deepEqualsInternal(
|
|
|
84
188
|
// For primitives that aren't strictly equal
|
|
85
189
|
return false
|
|
86
190
|
}
|
|
191
|
+
|
|
192
|
+
const temporalTypes = [
|
|
193
|
+
`Temporal.Duration`,
|
|
194
|
+
`Temporal.Instant`,
|
|
195
|
+
`Temporal.PlainDate`,
|
|
196
|
+
`Temporal.PlainDateTime`,
|
|
197
|
+
`Temporal.PlainMonthDay`,
|
|
198
|
+
`Temporal.PlainTime`,
|
|
199
|
+
`Temporal.PlainYearMonth`,
|
|
200
|
+
`Temporal.ZonedDateTime`,
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
function getStringTag(a: any): any {
|
|
204
|
+
return a[Symbol.toStringTag]
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Checks if the value is a Temporal object by checking for the Temporal brand */
|
|
208
|
+
export function isTemporal(a: any): boolean {
|
|
209
|
+
const tag = getStringTag(a)
|
|
210
|
+
return typeof tag === `string` && temporalTypes.includes(tag)
|
|
211
|
+
}
|