@tanstack/db 0.4.2 → 0.4.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 (180) hide show
  1. package/dist/cjs/collection/change-events.cjs +1 -1
  2. package/dist/cjs/collection/change-events.cjs.map +1 -1
  3. package/dist/cjs/collection/changes.cjs +7 -3
  4. package/dist/cjs/collection/changes.cjs.map +1 -1
  5. package/dist/cjs/collection/events.cjs +3 -6
  6. package/dist/cjs/collection/events.cjs.map +1 -1
  7. package/dist/cjs/collection/index.cjs +4 -1
  8. package/dist/cjs/collection/index.cjs.map +1 -1
  9. package/dist/cjs/collection/index.d.cts +16 -4
  10. package/dist/cjs/collection/mutations.cjs +13 -20
  11. package/dist/cjs/collection/mutations.cjs.map +1 -1
  12. package/dist/cjs/collection/state.cjs +9 -2
  13. package/dist/cjs/collection/state.cjs.map +1 -1
  14. package/dist/cjs/collection/subscription.cjs +4 -5
  15. package/dist/cjs/collection/subscription.cjs.map +1 -1
  16. package/dist/cjs/collection/subscription.d.cts +2 -2
  17. package/dist/cjs/collection/sync.cjs +10 -2
  18. package/dist/cjs/collection/sync.cjs.map +1 -1
  19. package/dist/cjs/indexes/auto-index.cjs +4 -3
  20. package/dist/cjs/indexes/auto-index.cjs.map +1 -1
  21. package/dist/cjs/indexes/auto-index.d.cts +2 -1
  22. package/dist/cjs/indexes/base-index.cjs +26 -0
  23. package/dist/cjs/indexes/base-index.cjs.map +1 -1
  24. package/dist/cjs/indexes/base-index.d.cts +47 -2
  25. package/dist/cjs/indexes/btree-index.cjs +45 -9
  26. package/dist/cjs/indexes/btree-index.cjs.map +1 -1
  27. package/dist/cjs/indexes/btree-index.d.cts +15 -0
  28. package/dist/cjs/indexes/lazy-index.cjs +3 -6
  29. package/dist/cjs/indexes/lazy-index.cjs.map +1 -1
  30. package/dist/cjs/indexes/reverse-index.cjs +78 -0
  31. package/dist/cjs/indexes/reverse-index.cjs.map +1 -0
  32. package/dist/cjs/indexes/reverse-index.d.cts +30 -0
  33. package/dist/cjs/proxy.cjs +1 -1
  34. package/dist/cjs/proxy.cjs.map +1 -1
  35. package/dist/cjs/query/builder/index.cjs +21 -0
  36. package/dist/cjs/query/builder/index.cjs.map +1 -1
  37. package/dist/cjs/query/builder/index.d.cts +16 -1
  38. package/dist/cjs/query/builder/types.d.cts +7 -0
  39. package/dist/cjs/query/compiler/evaluators.cjs +1 -1
  40. package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
  41. package/dist/cjs/query/compiler/group-by.cjs +2 -10
  42. package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
  43. package/dist/cjs/query/compiler/index.cjs +2 -39
  44. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  45. package/dist/cjs/query/compiler/index.d.cts +1 -0
  46. package/dist/cjs/query/compiler/joins.cjs +15 -14
  47. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  48. package/dist/cjs/query/compiler/joins.d.cts +2 -1
  49. package/dist/cjs/query/compiler/order-by.cjs +8 -10
  50. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  51. package/dist/cjs/query/compiler/order-by.d.cts +2 -2
  52. package/dist/cjs/query/compiler/select.cjs +1 -1
  53. package/dist/cjs/query/compiler/select.cjs.map +1 -1
  54. package/dist/cjs/query/index.d.cts +1 -1
  55. package/dist/cjs/query/ir.cjs +38 -0
  56. package/dist/cjs/query/ir.cjs.map +1 -1
  57. package/dist/cjs/query/ir.d.cts +11 -1
  58. package/dist/cjs/query/live/collection-config-builder.cjs +3 -2
  59. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  60. package/dist/cjs/query/live/collection-config-builder.d.cts +2 -2
  61. package/dist/cjs/query/live/collection-subscriber.cjs +2 -3
  62. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  63. package/dist/cjs/query/live/types.d.cts +4 -0
  64. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  65. package/dist/cjs/query/live-query-collection.d.cts +7 -4
  66. package/dist/cjs/query/optimizer.cjs +2 -4
  67. package/dist/cjs/query/optimizer.cjs.map +1 -1
  68. package/dist/cjs/transactions.cjs +2 -3
  69. package/dist/cjs/transactions.cjs.map +1 -1
  70. package/dist/cjs/types.d.cts +13 -0
  71. package/dist/cjs/utils/btree.cjs +1 -1
  72. package/dist/cjs/utils/btree.cjs.map +1 -1
  73. package/dist/cjs/utils/index-optimization.cjs +7 -2
  74. package/dist/cjs/utils/index-optimization.cjs.map +1 -1
  75. package/dist/cjs/utils/index-optimization.d.cts +3 -2
  76. package/dist/cjs/utils.cjs +6 -0
  77. package/dist/cjs/utils.cjs.map +1 -1
  78. package/dist/cjs/utils.d.cts +2 -3
  79. package/dist/esm/collection/change-events.js +1 -1
  80. package/dist/esm/collection/change-events.js.map +1 -1
  81. package/dist/esm/collection/changes.js +7 -3
  82. package/dist/esm/collection/changes.js.map +1 -1
  83. package/dist/esm/collection/events.js +3 -6
  84. package/dist/esm/collection/events.js.map +1 -1
  85. package/dist/esm/collection/index.d.ts +16 -4
  86. package/dist/esm/collection/index.js +4 -1
  87. package/dist/esm/collection/index.js.map +1 -1
  88. package/dist/esm/collection/mutations.js +13 -20
  89. package/dist/esm/collection/mutations.js.map +1 -1
  90. package/dist/esm/collection/state.js +9 -2
  91. package/dist/esm/collection/state.js.map +1 -1
  92. package/dist/esm/collection/subscription.d.ts +2 -2
  93. package/dist/esm/collection/subscription.js +4 -5
  94. package/dist/esm/collection/subscription.js.map +1 -1
  95. package/dist/esm/collection/sync.js +10 -2
  96. package/dist/esm/collection/sync.js.map +1 -1
  97. package/dist/esm/indexes/auto-index.d.ts +2 -1
  98. package/dist/esm/indexes/auto-index.js +4 -3
  99. package/dist/esm/indexes/auto-index.js.map +1 -1
  100. package/dist/esm/indexes/base-index.d.ts +47 -2
  101. package/dist/esm/indexes/base-index.js +26 -0
  102. package/dist/esm/indexes/base-index.js.map +1 -1
  103. package/dist/esm/indexes/btree-index.d.ts +15 -0
  104. package/dist/esm/indexes/btree-index.js +45 -9
  105. package/dist/esm/indexes/btree-index.js.map +1 -1
  106. package/dist/esm/indexes/lazy-index.js +3 -6
  107. package/dist/esm/indexes/lazy-index.js.map +1 -1
  108. package/dist/esm/indexes/reverse-index.d.ts +30 -0
  109. package/dist/esm/indexes/reverse-index.js +78 -0
  110. package/dist/esm/indexes/reverse-index.js.map +1 -0
  111. package/dist/esm/proxy.js +1 -1
  112. package/dist/esm/proxy.js.map +1 -1
  113. package/dist/esm/query/builder/index.d.ts +16 -1
  114. package/dist/esm/query/builder/index.js +21 -0
  115. package/dist/esm/query/builder/index.js.map +1 -1
  116. package/dist/esm/query/builder/types.d.ts +7 -0
  117. package/dist/esm/query/compiler/evaluators.js +1 -1
  118. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  119. package/dist/esm/query/compiler/group-by.js +3 -11
  120. package/dist/esm/query/compiler/group-by.js.map +1 -1
  121. package/dist/esm/query/compiler/index.d.ts +1 -0
  122. package/dist/esm/query/compiler/index.js +4 -41
  123. package/dist/esm/query/compiler/index.js.map +1 -1
  124. package/dist/esm/query/compiler/joins.d.ts +2 -1
  125. package/dist/esm/query/compiler/joins.js +15 -14
  126. package/dist/esm/query/compiler/joins.js.map +1 -1
  127. package/dist/esm/query/compiler/order-by.d.ts +2 -2
  128. package/dist/esm/query/compiler/order-by.js +5 -7
  129. package/dist/esm/query/compiler/order-by.js.map +1 -1
  130. package/dist/esm/query/compiler/select.js +1 -1
  131. package/dist/esm/query/compiler/select.js.map +1 -1
  132. package/dist/esm/query/index.d.ts +1 -1
  133. package/dist/esm/query/ir.d.ts +11 -1
  134. package/dist/esm/query/ir.js +38 -0
  135. package/dist/esm/query/ir.js.map +1 -1
  136. package/dist/esm/query/live/collection-config-builder.d.ts +2 -2
  137. package/dist/esm/query/live/collection-config-builder.js +3 -2
  138. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  139. package/dist/esm/query/live/collection-subscriber.js +2 -3
  140. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  141. package/dist/esm/query/live/types.d.ts +4 -0
  142. package/dist/esm/query/live-query-collection.d.ts +7 -4
  143. package/dist/esm/query/live-query-collection.js.map +1 -1
  144. package/dist/esm/query/optimizer.js +2 -4
  145. package/dist/esm/query/optimizer.js.map +1 -1
  146. package/dist/esm/transactions.js +2 -3
  147. package/dist/esm/transactions.js.map +1 -1
  148. package/dist/esm/types.d.ts +13 -0
  149. package/dist/esm/utils/btree.js +1 -1
  150. package/dist/esm/utils/btree.js.map +1 -1
  151. package/dist/esm/utils/index-optimization.d.ts +3 -2
  152. package/dist/esm/utils/index-optimization.js +7 -2
  153. package/dist/esm/utils/index-optimization.js.map +1 -1
  154. package/dist/esm/utils.d.ts +2 -3
  155. package/dist/esm/utils.js +6 -0
  156. package/dist/esm/utils.js.map +1 -1
  157. package/package.json +1 -1
  158. package/src/collection/changes.ts +10 -4
  159. package/src/collection/index.ts +38 -5
  160. package/src/collection/state.ts +22 -5
  161. package/src/collection/subscription.ts +4 -4
  162. package/src/collection/sync.ts +17 -2
  163. package/src/indexes/auto-index.ts +8 -3
  164. package/src/indexes/base-index.ts +94 -4
  165. package/src/indexes/btree-index.ts +58 -7
  166. package/src/indexes/reverse-index.ts +120 -0
  167. package/src/query/builder/index.ts +30 -2
  168. package/src/query/builder/types.ts +12 -0
  169. package/src/query/compiler/group-by.ts +1 -10
  170. package/src/query/compiler/index.ts +4 -1
  171. package/src/query/compiler/joins.ts +13 -8
  172. package/src/query/compiler/order-by.ts +16 -20
  173. package/src/query/index.ts +1 -0
  174. package/src/query/ir.ts +68 -1
  175. package/src/query/live/collection-config-builder.ts +3 -2
  176. package/src/query/live/types.ts +5 -0
  177. package/src/query/live-query-collection.ts +34 -8
  178. package/src/types.ts +22 -0
  179. package/src/utils/index-optimization.ts +19 -5
  180. package/src/utils.ts +8 -0
@@ -217,7 +217,11 @@ export class CollectionStateManager<
217
217
  triggeredByUserAction: boolean = false
218
218
  ): void {
219
219
  // Skip redundant recalculations when we're in the middle of committing sync transactions
220
- if (this.isCommittingSyncTransactions) {
220
+ // While the sync pipeline is replaying a large batch we still want to honour
221
+ // fresh optimistic mutations from the UI. Only skip recompute for the
222
+ // internal sync-driven redraws; user-triggered work (triggeredByUserAction)
223
+ // must run so live queries stay responsive during long commits.
224
+ if (this.isCommittingSyncTransactions && !triggeredByUserAction) {
221
225
  return
222
226
  }
223
227
 
@@ -708,10 +712,23 @@ export class CollectionStateManager<
708
712
 
709
713
  // Check if this sync operation is redundant with a completed optimistic operation
710
714
  const completedOp = completedOptimisticOps.get(key)
711
- const isRedundantSync =
712
- completedOp &&
713
- newVisibleValue !== undefined &&
714
- deepEquals(completedOp.value, newVisibleValue)
715
+ let isRedundantSync = false
716
+
717
+ if (completedOp) {
718
+ if (
719
+ completedOp.type === `delete` &&
720
+ previousVisibleValue !== undefined &&
721
+ newVisibleValue === undefined &&
722
+ deepEquals(completedOp.value, previousVisibleValue)
723
+ ) {
724
+ isRedundantSync = true
725
+ } else if (
726
+ newVisibleValue !== undefined &&
727
+ deepEquals(completedOp.value, newVisibleValue)
728
+ ) {
729
+ isRedundantSync = true
730
+ }
731
+ }
715
732
 
716
733
  if (!isRedundantSync) {
717
734
  if (
@@ -1,11 +1,11 @@
1
1
  import { ensureIndexForExpression } from "../indexes/auto-index.js"
2
- import { and } from "../query/index.js"
2
+ import { and } from "../query/builder/functions.js"
3
3
  import {
4
4
  createFilterFunctionFromExpression,
5
5
  createFilteredCallback,
6
6
  } from "./change-events.js"
7
7
  import type { BasicExpression } from "../query/ir.js"
8
- import type { BaseIndex } from "../indexes/base-index.js"
8
+ import type { IndexInterface } from "../indexes/base-index.js"
9
9
  import type { ChangeMessage } from "../types.js"
10
10
  import type { CollectionImpl } from "./index.js"
11
11
 
@@ -38,7 +38,7 @@ export class CollectionSubscription {
38
38
 
39
39
  private filteredCallback: (changes: Array<ChangeMessage<any, any>>) => void
40
40
 
41
- private orderByIndex: BaseIndex<string | number> | undefined
41
+ private orderByIndex: IndexInterface<string | number> | undefined
42
42
 
43
43
  constructor(
44
44
  private collection: CollectionImpl<any, any, any, any, any>,
@@ -65,7 +65,7 @@ export class CollectionSubscription {
65
65
  : this.callback
66
66
  }
67
67
 
68
- setOrderByIndex(index: BaseIndex<any>) {
68
+ setOrderByIndex(index: IndexInterface<any>) {
69
69
  this.orderByIndex = index
70
70
  }
71
71
 
@@ -7,6 +7,7 @@ import {
7
7
  SyncTransactionAlreadyCommittedError,
8
8
  SyncTransactionAlreadyCommittedWriteError,
9
9
  } from "../errors"
10
+ import { deepEquals } from "../utils"
10
11
  import type { StandardSchemaV1 } from "@standard-schema/spec"
11
12
  import type { ChangeMessage, CollectionConfig } from "../types"
12
13
  import type { CollectionImpl } from "./index.js"
@@ -84,6 +85,8 @@ export class CollectionSyncManager<
84
85
  }
85
86
  const key = this.config.getKey(messageWithoutKey.value)
86
87
 
88
+ let messageType = messageWithoutKey.type
89
+
87
90
  // Check if an item with this key already exists when inserting
88
91
  if (messageWithoutKey.type === `insert`) {
89
92
  const insertingIntoExistingSynced = state.syncedData.has(key)
@@ -96,17 +99,29 @@ export class CollectionSyncManager<
96
99
  !hasPendingDeleteForKey &&
97
100
  !isTruncateTransaction
98
101
  ) {
99
- throw new DuplicateKeySyncError(key, this.id)
102
+ const existingValue = state.syncedData.get(key)
103
+ if (
104
+ existingValue !== undefined &&
105
+ deepEquals(existingValue, messageWithoutKey.value)
106
+ ) {
107
+ // The "insert" is an echo of a value we already have locally.
108
+ // Treat it as an update so we preserve optimistic intent without
109
+ // throwing a duplicate-key error during reconciliation.
110
+ messageType = `update`
111
+ } else {
112
+ throw new DuplicateKeySyncError(key, this.id)
113
+ }
100
114
  }
101
115
  }
102
116
 
103
117
  const message: ChangeMessage<TOutput> = {
104
118
  ...messageWithoutKey,
119
+ type: messageType,
105
120
  key,
106
121
  }
107
122
  pendingTransaction.operations.push(message)
108
123
 
109
- if (messageWithoutKey.type === `delete`) {
124
+ if (messageType === `delete`) {
110
125
  pendingTransaction.deletedKeys.add(key)
111
126
  }
112
127
  },
@@ -1,4 +1,6 @@
1
+ import { DEFAULT_COMPARE_OPTIONS } from "../utils"
1
2
  import { BTreeIndex } from "./btree-index"
3
+ import type { CompareOptions } from "../query/builder/types"
2
4
  import type { BasicExpression } from "../query/ir"
3
5
  import type { CollectionImpl } from "../collection/index.js"
4
6
 
@@ -30,6 +32,7 @@ export function ensureIndexForField<
30
32
  fieldName: string,
31
33
  fieldPath: Array<string>,
32
34
  collection: CollectionImpl<T, TKey, any, any, any>,
35
+ compareOptions: CompareOptions = DEFAULT_COMPARE_OPTIONS,
33
36
  compareFn?: (a: any, b: any) => number
34
37
  ) {
35
38
  if (!shouldAutoIndex(collection)) {
@@ -37,8 +40,10 @@ export function ensureIndexForField<
37
40
  }
38
41
 
39
42
  // Check if we already have an index for this field
40
- const existingIndex = Array.from(collection.indexes.values()).find((index) =>
41
- index.matchesField(fieldPath)
43
+ const existingIndex = Array.from(collection.indexes.values()).find(
44
+ (index) =>
45
+ index.matchesField(fieldPath) &&
46
+ index.matchesCompareOptions(compareOptions)
42
47
  )
43
48
 
44
49
  if (existingIndex) {
@@ -50,7 +55,7 @@ export function ensureIndexForField<
50
55
  collection.createIndex((row) => (row as any)[fieldName], {
51
56
  name: `auto_${fieldName}`,
52
57
  indexType: BTreeIndex,
53
- options: compareFn ? { compareFn } : {},
58
+ options: compareFn ? { compareFn, compareOptions } : {},
54
59
  })
55
60
  } catch (error) {
56
61
  console.warn(`Failed to create auto-index for field "${fieldName}":`, error)
@@ -1,6 +1,9 @@
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 { DEFAULT_COMPARE_OPTIONS, deepEquals } from "../utils.js"
4
+ import type { RangeQueryOptions } from "./btree-index.js"
5
+ import type { CompareOptions } from "../query/builder/types.js"
6
+ import type { BasicExpression, OrderByDirection } from "../query/ir.js"
4
7
 
5
8
  /**
6
9
  * Operations that indexes can support, imported from available comparison functions
@@ -22,12 +25,57 @@ export interface IndexStats {
22
25
  readonly lastUpdated: Date
23
26
  }
24
27
 
28
+ export interface IndexInterface<
29
+ TKey extends string | number = string | number,
30
+ > {
31
+ add: (key: TKey, item: any) => void
32
+ remove: (key: TKey, item: any) => void
33
+ update: (key: TKey, oldItem: any, newItem: any) => void
34
+
35
+ build: (entries: Iterable<[TKey, any]>) => void
36
+ clear: () => void
37
+
38
+ lookup: (operation: IndexOperation, value: any) => Set<TKey>
39
+
40
+ equalityLookup: (value: any) => Set<TKey>
41
+ inArrayLookup: (values: Array<any>) => Set<TKey>
42
+
43
+ rangeQuery: (options: RangeQueryOptions) => Set<TKey>
44
+ rangeQueryReversed: (options: RangeQueryOptions) => Set<TKey>
45
+
46
+ take: (
47
+ n: number,
48
+ from?: TKey,
49
+ filterFn?: (key: TKey) => boolean
50
+ ) => Array<TKey>
51
+ takeReversed: (
52
+ n: number,
53
+ from?: TKey,
54
+ filterFn?: (key: TKey) => boolean
55
+ ) => Array<TKey>
56
+
57
+ get keyCount(): number
58
+ get orderedEntriesArray(): Array<[any, Set<TKey>]>
59
+ get orderedEntriesArrayReversed(): Array<[any, Set<TKey>]>
60
+
61
+ get indexedKeysSet(): Set<TKey>
62
+ get valueMapData(): Map<any, Set<TKey>>
63
+
64
+ supports: (operation: IndexOperation) => boolean
65
+
66
+ matchesField: (fieldPath: Array<string>) => boolean
67
+ matchesCompareOptions: (compareOptions: CompareOptions) => boolean
68
+ matchesDirection: (direction: OrderByDirection) => boolean
69
+
70
+ getStats: () => IndexStats
71
+ }
72
+
25
73
  /**
26
74
  * Base abstract class that all index types extend
27
75
  */
28
- export abstract class BaseIndex<
29
- TKey extends string | number = string | number,
30
- > {
76
+ export abstract class BaseIndex<TKey extends string | number = string | number>
77
+ implements IndexInterface<TKey>
78
+ {
31
79
  public readonly id: number
32
80
  public readonly name?: string
33
81
  public readonly expression: BasicExpression
@@ -36,6 +84,7 @@ export abstract class BaseIndex<
36
84
  protected lookupCount = 0
37
85
  protected totalLookupTime = 0
38
86
  protected lastUpdated = new Date()
87
+ protected compareOptions: CompareOptions
39
88
 
40
89
  constructor(
41
90
  id: number,
@@ -45,6 +94,7 @@ export abstract class BaseIndex<
45
94
  ) {
46
95
  this.id = id
47
96
  this.expression = expression
97
+ this.compareOptions = DEFAULT_COMPARE_OPTIONS
48
98
  this.name = name
49
99
  this.initialize(options)
50
100
  }
@@ -61,7 +111,20 @@ export abstract class BaseIndex<
61
111
  from?: TKey,
62
112
  filterFn?: (key: TKey) => boolean
63
113
  ): Array<TKey>
114
+ abstract takeReversed(
115
+ n: number,
116
+ from?: TKey,
117
+ filterFn?: (key: TKey) => boolean
118
+ ): Array<TKey>
64
119
  abstract get keyCount(): number
120
+ abstract equalityLookup(value: any): Set<TKey>
121
+ abstract inArrayLookup(values: Array<any>): Set<TKey>
122
+ abstract rangeQuery(options: RangeQueryOptions): Set<TKey>
123
+ abstract rangeQueryReversed(options: RangeQueryOptions): Set<TKey>
124
+ abstract get orderedEntriesArray(): Array<[any, Set<TKey>]>
125
+ abstract get orderedEntriesArrayReversed(): Array<[any, Set<TKey>]>
126
+ abstract get indexedKeysSet(): Set<TKey>
127
+ abstract get valueMapData(): Map<any, Set<TKey>>
65
128
 
66
129
  // Common methods
67
130
  supports(operation: IndexOperation): boolean {
@@ -76,6 +139,33 @@ export abstract class BaseIndex<
76
139
  )
77
140
  }
78
141
 
142
+ /**
143
+ * Checks if the compare options match the index's compare options.
144
+ * The direction is ignored because the index can be reversed if the direction is different.
145
+ */
146
+ matchesCompareOptions(compareOptions: CompareOptions): boolean {
147
+ const thisCompareOptionsWithoutDirection = {
148
+ ...this.compareOptions,
149
+ direction: undefined,
150
+ }
151
+ const compareOptionsWithoutDirection = {
152
+ ...compareOptions,
153
+ direction: undefined,
154
+ }
155
+
156
+ return deepEquals(
157
+ thisCompareOptionsWithoutDirection,
158
+ compareOptionsWithoutDirection
159
+ )
160
+ }
161
+
162
+ /**
163
+ * Checks if the index matches the provided direction.
164
+ */
165
+ matchesDirection(direction: OrderByDirection): boolean {
166
+ return this.compareOptions.direction === direction
167
+ }
168
+
79
169
  getStats(): IndexStats {
80
170
  return {
81
171
  entryCount: this.keyCount,
@@ -1,6 +1,7 @@
1
1
  import { BTree } from "../utils/btree.js"
2
2
  import { defaultComparator, normalizeValue } from "../utils/comparison.js"
3
3
  import { BaseIndex } from "./base-index.js"
4
+ import type { CompareOptions } from "../query/builder/types.js"
4
5
  import type { BasicExpression } from "../query/ir.js"
5
6
  import type { IndexOperation } from "./base-index.js"
6
7
 
@@ -9,6 +10,7 @@ import type { IndexOperation } from "./base-index.js"
9
10
  */
10
11
  export interface BTreeIndexOptions {
11
12
  compareFn?: (a: any, b: any) => number
13
+ compareOptions?: CompareOptions
12
14
  }
13
15
 
14
16
  /**
@@ -53,6 +55,9 @@ export class BTreeIndex<
53
55
  ) {
54
56
  super(id, expression, name, options)
55
57
  this.compareFn = options?.compareFn ?? defaultComparator
58
+ if (options?.compareOptions) {
59
+ this.compareOptions = options!.compareOptions
60
+ }
56
61
  this.orderedEntries = new BTree(this.compareFn)
57
62
  }
58
63
 
@@ -240,18 +245,31 @@ export class BTreeIndex<
240
245
  }
241
246
 
242
247
  /**
243
- * Returns the next n items after the provided item or the first n items if no from item is provided.
244
- * @param n - The number of items to return
245
- * @param from - The item to start from (exclusive). Starts from the smallest item (inclusive) if not provided.
246
- * @returns The next n items after the provided key. Returns the first n items if no from item is provided.
248
+ * Performs a reversed range query
247
249
  */
248
- take(n: number, from?: any, filterFn?: (key: TKey) => boolean): Array<TKey> {
250
+ rangeQueryReversed(options: RangeQueryOptions = {}): Set<TKey> {
251
+ const { from, to, fromInclusive = true, toInclusive = true } = options
252
+ return this.rangeQuery({
253
+ from: to ?? this.orderedEntries.maxKey(),
254
+ to: from ?? this.orderedEntries.minKey(),
255
+ fromInclusive: toInclusive,
256
+ toInclusive: fromInclusive,
257
+ })
258
+ }
259
+
260
+ private takeInternal(
261
+ n: number,
262
+ nextPair: (k?: any) => [any, any] | undefined,
263
+ from?: any,
264
+ filterFn?: (key: TKey) => boolean
265
+ ): Array<TKey> {
249
266
  const keysInResult: Set<TKey> = new Set()
250
267
  const result: Array<TKey> = []
251
- const nextKey = (k?: any) => this.orderedEntries.nextHigherKey(k)
268
+ let pair: [any, any] | undefined
252
269
  let key = normalizeValue(from)
253
270
 
254
- while ((key = nextKey(key)) && result.length < n) {
271
+ while ((pair = nextPair(key)) !== undefined && result.length < n) {
272
+ key = pair[0]
255
273
  const keys = this.valueMap.get(key)
256
274
  if (keys) {
257
275
  const it = keys.values()
@@ -268,6 +286,32 @@ export class BTreeIndex<
268
286
  return result
269
287
  }
270
288
 
289
+ /**
290
+ * Returns the next n items after the provided item or the first n items if no from item is provided.
291
+ * @param n - The number of items to return
292
+ * @param from - The item to start from (exclusive). Starts from the smallest item (inclusive) if not provided.
293
+ * @returns The next n items after the provided key. Returns the first n items if no from item is provided.
294
+ */
295
+ take(n: number, from?: any, filterFn?: (key: TKey) => boolean): Array<TKey> {
296
+ const nextPair = (k?: any) => this.orderedEntries.nextHigherPair(k)
297
+ return this.takeInternal(n, nextPair, from, filterFn)
298
+ }
299
+
300
+ /**
301
+ * Returns the next n items **before** the provided item (in descending order) or the last n items if no from item is provided.
302
+ * @param n - The number of items to return
303
+ * @param from - The item to start from (exclusive). Starts from the largest item (inclusive) if not provided.
304
+ * @returns The next n items **before** the provided key. Returns the last n items if no from item is provided.
305
+ */
306
+ takeReversed(
307
+ n: number,
308
+ from?: any,
309
+ filterFn?: (key: TKey) => boolean
310
+ ): Array<TKey> {
311
+ const nextPair = (k?: any) => this.orderedEntries.nextLowerPair(k)
312
+ return this.takeInternal(n, nextPair, from, filterFn)
313
+ }
314
+
271
315
  /**
272
316
  * Performs an IN array lookup
273
317
  */
@@ -296,6 +340,13 @@ export class BTreeIndex<
296
340
  .map((key) => [key, this.valueMap.get(key) ?? new Set()])
297
341
  }
298
342
 
343
+ get orderedEntriesArrayReversed(): Array<[any, Set<TKey>]> {
344
+ return this.takeReversed(this.orderedEntries.size).map((key) => [
345
+ key,
346
+ this.valueMap.get(key) ?? new Set(),
347
+ ])
348
+ }
349
+
299
350
  get valueMapData(): Map<any, Set<TKey>> {
300
351
  return this.valueMap
301
352
  }
@@ -0,0 +1,120 @@
1
+ import type { CompareOptions } from "../query/builder/types"
2
+ import type { OrderByDirection } from "../query/ir"
3
+ import type { IndexInterface, IndexOperation, IndexStats } from "./base-index"
4
+ import type { RangeQueryOptions } from "./btree-index"
5
+
6
+ export class ReverseIndex<TKey extends string | number>
7
+ implements IndexInterface<TKey>
8
+ {
9
+ private originalIndex: IndexInterface<TKey>
10
+
11
+ constructor(index: IndexInterface<TKey>) {
12
+ this.originalIndex = index
13
+ }
14
+
15
+ // Define the reversed operations
16
+
17
+ lookup(operation: IndexOperation, value: any): Set<TKey> {
18
+ const reverseOperation =
19
+ operation === `gt`
20
+ ? `lt`
21
+ : operation === `gte`
22
+ ? `lte`
23
+ : operation === `lt`
24
+ ? `gt`
25
+ : operation === `lte`
26
+ ? `gte`
27
+ : operation
28
+ return this.originalIndex.lookup(reverseOperation, value)
29
+ }
30
+
31
+ rangeQuery(options: RangeQueryOptions = {}): Set<TKey> {
32
+ return this.originalIndex.rangeQueryReversed(options)
33
+ }
34
+
35
+ rangeQueryReversed(options: RangeQueryOptions = {}): Set<TKey> {
36
+ return this.originalIndex.rangeQuery(options)
37
+ }
38
+
39
+ take(n: number, from?: any, filterFn?: (key: TKey) => boolean): Array<TKey> {
40
+ return this.originalIndex.takeReversed(n, from, filterFn)
41
+ }
42
+
43
+ takeReversed(
44
+ n: number,
45
+ from?: any,
46
+ filterFn?: (key: TKey) => boolean
47
+ ): Array<TKey> {
48
+ return this.originalIndex.take(n, from, filterFn)
49
+ }
50
+
51
+ get orderedEntriesArray(): Array<[any, Set<TKey>]> {
52
+ return this.originalIndex.orderedEntriesArrayReversed
53
+ }
54
+
55
+ get orderedEntriesArrayReversed(): Array<[any, Set<TKey>]> {
56
+ return this.originalIndex.orderedEntriesArray
57
+ }
58
+
59
+ // All operations below delegate to the original index
60
+
61
+ supports(operation: IndexOperation): boolean {
62
+ return this.originalIndex.supports(operation)
63
+ }
64
+
65
+ matchesField(fieldPath: Array<string>): boolean {
66
+ return this.originalIndex.matchesField(fieldPath)
67
+ }
68
+
69
+ matchesCompareOptions(compareOptions: CompareOptions): boolean {
70
+ return this.originalIndex.matchesCompareOptions(compareOptions)
71
+ }
72
+
73
+ matchesDirection(direction: OrderByDirection): boolean {
74
+ return this.originalIndex.matchesDirection(direction)
75
+ }
76
+
77
+ getStats(): IndexStats {
78
+ return this.originalIndex.getStats()
79
+ }
80
+
81
+ add(key: TKey, item: any): void {
82
+ this.originalIndex.add(key, item)
83
+ }
84
+
85
+ remove(key: TKey, item: any): void {
86
+ this.originalIndex.remove(key, item)
87
+ }
88
+
89
+ update(key: TKey, oldItem: any, newItem: any): void {
90
+ this.originalIndex.update(key, oldItem, newItem)
91
+ }
92
+
93
+ build(entries: Iterable<[TKey, any]>): void {
94
+ this.originalIndex.build(entries)
95
+ }
96
+
97
+ clear(): void {
98
+ this.originalIndex.clear()
99
+ }
100
+
101
+ get keyCount(): number {
102
+ return this.originalIndex.keyCount
103
+ }
104
+
105
+ equalityLookup(value: any): Set<TKey> {
106
+ return this.originalIndex.equalityLookup(value)
107
+ }
108
+
109
+ inArrayLookup(values: Array<any>): Set<TKey> {
110
+ return this.originalIndex.inArrayLookup(values)
111
+ }
112
+
113
+ get indexedKeysSet(): Set<TKey> {
114
+ return this.originalIndex.indexedKeysSet
115
+ }
116
+
117
+ get valueMapData(): Map<any, Set<TKey>> {
118
+ return this.originalIndex.valueMapData
119
+ }
120
+ }
@@ -16,7 +16,7 @@ import {
16
16
  SubQueryMustHaveFromClauseError,
17
17
  } from "../../errors.js"
18
18
  import { createRefProxy, toExpression } from "./ref-proxy.js"
19
- import type { NamespacedRow } from "../../types.js"
19
+ import type { NamespacedRow, SingleResult } from "../../types.js"
20
20
  import type {
21
21
  Aggregate,
22
22
  BasicExpression,
@@ -615,6 +615,28 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
615
615
  }) as any
616
616
  }
617
617
 
618
+ /**
619
+ * Specify that the query should return a single result
620
+ * @returns A QueryBuilder that returns the first result
621
+ *
622
+ * @example
623
+ * ```ts
624
+ * // Get the user matching the query
625
+ * query
626
+ * .from({ users: usersCollection })
627
+ * .where(({users}) => eq(users.id, 1))
628
+ * .findOne()
629
+ *```
630
+ */
631
+ findOne(): QueryBuilder<TContext & SingleResult> {
632
+ return new BaseQueryBuilder({
633
+ ...this.query,
634
+ // TODO: enforcing return only one result with also a default orderBy if none is specified
635
+ // limit: 1,
636
+ singleResult: true,
637
+ })
638
+ }
639
+
618
640
  // Helper methods
619
641
  private _getCurrentAliases(): Array<string> {
620
642
  const aliases: Array<string> = []
@@ -817,4 +839,10 @@ export type ExtractContext<T> =
817
839
  : never
818
840
 
819
841
  // Export the types from types.ts for convenience
820
- export type { Context, Source, GetResult, RefLeaf as Ref } from "./types.js"
842
+ export type {
843
+ Context,
844
+ Source,
845
+ GetResult,
846
+ RefLeaf as Ref,
847
+ InferResultType,
848
+ } from "./types.js"
@@ -1,4 +1,5 @@
1
1
  import type { CollectionImpl } from "../../collection/index.js"
2
+ import type { SingleResult } from "../../types.js"
2
3
  import type {
3
4
  Aggregate,
4
5
  BasicExpression,
@@ -47,6 +48,8 @@ export interface Context {
47
48
  >
48
49
  // The result type after select (if select has been called)
49
50
  result?: any
51
+ // Single result only (if findOne has been called)
52
+ singleResult?: boolean
50
53
  }
51
54
 
52
55
  /**
@@ -571,6 +574,7 @@ export type MergeContextWithJoinType<
571
574
  [K in keyof TNewSchema & string]: TJoinType
572
575
  }
573
576
  result: TContext[`result`]
577
+ singleResult: TContext[`singleResult`] extends true ? true : false
574
578
  }
575
579
 
576
580
  /**
@@ -621,6 +625,14 @@ export type ApplyJoinOptionalityToMergedSchema<
621
625
  TNewSchema[K]
622
626
  }
623
627
 
628
+ /**
629
+ * Utility type to infer the query result size (single row or an array)
630
+ */
631
+ export type InferResultType<TContext extends Context> =
632
+ TContext extends SingleResult
633
+ ? GetResult<TContext> | undefined
634
+ : Array<GetResult<TContext>>
635
+
624
636
  /**
625
637
  * GetResult - Determines the final result type of a query
626
638
  *
@@ -417,16 +417,7 @@ export function replaceAggregatesByRefs(
417
417
  }
418
418
 
419
419
  case `ref`: {
420
- const refExpr = havingExpr
421
- // Check if this is a direct reference to a SELECT alias
422
- if (refExpr.path.length === 1) {
423
- const alias = refExpr.path[0]!
424
- if (selectClause[alias]) {
425
- // This is a reference to a SELECT alias, convert to result.alias
426
- return new PropRef([resultAlias, alias])
427
- }
428
- }
429
- // Return as-is for other refs
420
+ // Non-aggregate refs are passed through unchanged (they reference table columns)
430
421
  return havingExpr as BasicExpression
431
422
  }
432
423
 
@@ -127,7 +127,8 @@ export function compileQuery(
127
127
  callbacks,
128
128
  lazyCollections,
129
129
  optimizableOrderByCollections,
130
- rawQuery
130
+ rawQuery,
131
+ compileQuery
131
132
  )
132
133
  }
133
134
 
@@ -512,3 +513,5 @@ export function followRef(
512
513
  }
513
514
  }
514
515
  }
516
+
517
+ export type CompileQueryFn = typeof compileQuery