@tanstack/db 0.4.3 → 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 (168) 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 +3 -4
  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/joins.cjs +6 -6
  44. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  45. package/dist/cjs/query/compiler/order-by.cjs +4 -5
  46. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  47. package/dist/cjs/query/compiler/order-by.d.cts +2 -2
  48. package/dist/cjs/query/compiler/select.cjs +1 -1
  49. package/dist/cjs/query/compiler/select.cjs.map +1 -1
  50. package/dist/cjs/query/index.d.cts +1 -1
  51. package/dist/cjs/query/ir.cjs.map +1 -1
  52. package/dist/cjs/query/ir.d.cts +1 -0
  53. package/dist/cjs/query/live/collection-config-builder.cjs +3 -2
  54. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  55. package/dist/cjs/query/live/collection-config-builder.d.cts +2 -2
  56. package/dist/cjs/query/live/collection-subscriber.cjs +2 -3
  57. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  58. package/dist/cjs/query/live/types.d.cts +4 -0
  59. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  60. package/dist/cjs/query/live-query-collection.d.cts +7 -4
  61. package/dist/cjs/query/optimizer.cjs +2 -4
  62. package/dist/cjs/query/optimizer.cjs.map +1 -1
  63. package/dist/cjs/transactions.cjs +2 -3
  64. package/dist/cjs/transactions.cjs.map +1 -1
  65. package/dist/cjs/types.d.cts +13 -0
  66. package/dist/cjs/utils/btree.cjs +1 -1
  67. package/dist/cjs/utils/btree.cjs.map +1 -1
  68. package/dist/cjs/utils/index-optimization.cjs +7 -2
  69. package/dist/cjs/utils/index-optimization.cjs.map +1 -1
  70. package/dist/cjs/utils/index-optimization.d.cts +3 -2
  71. package/dist/cjs/utils.cjs +6 -0
  72. package/dist/cjs/utils.cjs.map +1 -1
  73. package/dist/cjs/utils.d.cts +2 -3
  74. package/dist/esm/collection/change-events.js +1 -1
  75. package/dist/esm/collection/change-events.js.map +1 -1
  76. package/dist/esm/collection/changes.js +7 -3
  77. package/dist/esm/collection/changes.js.map +1 -1
  78. package/dist/esm/collection/events.js +3 -6
  79. package/dist/esm/collection/events.js.map +1 -1
  80. package/dist/esm/collection/index.d.ts +16 -4
  81. package/dist/esm/collection/index.js +4 -1
  82. package/dist/esm/collection/index.js.map +1 -1
  83. package/dist/esm/collection/mutations.js +13 -20
  84. package/dist/esm/collection/mutations.js.map +1 -1
  85. package/dist/esm/collection/state.js +9 -2
  86. package/dist/esm/collection/state.js.map +1 -1
  87. package/dist/esm/collection/subscription.d.ts +2 -2
  88. package/dist/esm/collection/subscription.js +3 -4
  89. package/dist/esm/collection/subscription.js.map +1 -1
  90. package/dist/esm/collection/sync.js +10 -2
  91. package/dist/esm/collection/sync.js.map +1 -1
  92. package/dist/esm/indexes/auto-index.d.ts +2 -1
  93. package/dist/esm/indexes/auto-index.js +4 -3
  94. package/dist/esm/indexes/auto-index.js.map +1 -1
  95. package/dist/esm/indexes/base-index.d.ts +47 -2
  96. package/dist/esm/indexes/base-index.js +26 -0
  97. package/dist/esm/indexes/base-index.js.map +1 -1
  98. package/dist/esm/indexes/btree-index.d.ts +15 -0
  99. package/dist/esm/indexes/btree-index.js +45 -9
  100. package/dist/esm/indexes/btree-index.js.map +1 -1
  101. package/dist/esm/indexes/lazy-index.js +3 -6
  102. package/dist/esm/indexes/lazy-index.js.map +1 -1
  103. package/dist/esm/indexes/reverse-index.d.ts +30 -0
  104. package/dist/esm/indexes/reverse-index.js +78 -0
  105. package/dist/esm/indexes/reverse-index.js.map +1 -0
  106. package/dist/esm/proxy.js +1 -1
  107. package/dist/esm/proxy.js.map +1 -1
  108. package/dist/esm/query/builder/index.d.ts +16 -1
  109. package/dist/esm/query/builder/index.js +21 -0
  110. package/dist/esm/query/builder/index.js.map +1 -1
  111. package/dist/esm/query/builder/types.d.ts +7 -0
  112. package/dist/esm/query/compiler/evaluators.js +1 -1
  113. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  114. package/dist/esm/query/compiler/group-by.js +3 -11
  115. package/dist/esm/query/compiler/group-by.js.map +1 -1
  116. package/dist/esm/query/compiler/joins.js +6 -6
  117. package/dist/esm/query/compiler/joins.js.map +1 -1
  118. package/dist/esm/query/compiler/order-by.d.ts +2 -2
  119. package/dist/esm/query/compiler/order-by.js +4 -5
  120. package/dist/esm/query/compiler/order-by.js.map +1 -1
  121. package/dist/esm/query/compiler/select.js +1 -1
  122. package/dist/esm/query/compiler/select.js.map +1 -1
  123. package/dist/esm/query/index.d.ts +1 -1
  124. package/dist/esm/query/ir.d.ts +1 -0
  125. package/dist/esm/query/ir.js.map +1 -1
  126. package/dist/esm/query/live/collection-config-builder.d.ts +2 -2
  127. package/dist/esm/query/live/collection-config-builder.js +3 -2
  128. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  129. package/dist/esm/query/live/collection-subscriber.js +2 -3
  130. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  131. package/dist/esm/query/live/types.d.ts +4 -0
  132. package/dist/esm/query/live-query-collection.d.ts +7 -4
  133. package/dist/esm/query/live-query-collection.js.map +1 -1
  134. package/dist/esm/query/optimizer.js +2 -4
  135. package/dist/esm/query/optimizer.js.map +1 -1
  136. package/dist/esm/transactions.js +2 -3
  137. package/dist/esm/transactions.js.map +1 -1
  138. package/dist/esm/types.d.ts +13 -0
  139. package/dist/esm/utils/btree.js +1 -1
  140. package/dist/esm/utils/btree.js.map +1 -1
  141. package/dist/esm/utils/index-optimization.d.ts +3 -2
  142. package/dist/esm/utils/index-optimization.js +7 -2
  143. package/dist/esm/utils/index-optimization.js.map +1 -1
  144. package/dist/esm/utils.d.ts +2 -3
  145. package/dist/esm/utils.js +6 -0
  146. package/dist/esm/utils.js.map +1 -1
  147. package/package.json +1 -1
  148. package/src/collection/changes.ts +10 -4
  149. package/src/collection/index.ts +38 -5
  150. package/src/collection/state.ts +22 -5
  151. package/src/collection/subscription.ts +3 -3
  152. package/src/collection/sync.ts +17 -2
  153. package/src/indexes/auto-index.ts +8 -3
  154. package/src/indexes/base-index.ts +94 -4
  155. package/src/indexes/btree-index.ts +58 -7
  156. package/src/indexes/reverse-index.ts +120 -0
  157. package/src/query/builder/index.ts +30 -2
  158. package/src/query/builder/types.ts +12 -0
  159. package/src/query/compiler/group-by.ts +1 -10
  160. package/src/query/compiler/order-by.ts +15 -18
  161. package/src/query/index.ts +1 -0
  162. package/src/query/ir.ts +1 -0
  163. package/src/query/live/collection-config-builder.ts +3 -2
  164. package/src/query/live/types.ts +5 -0
  165. package/src/query/live-query-collection.ts +34 -8
  166. package/src/types.ts +22 -0
  167. package/src/utils/index-optimization.ts +19 -5
  168. package/src/utils.ts +8 -0
@@ -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
 
@@ -9,7 +9,7 @@ import type { CompiledSingleRowExpression } from "./evaluators.js"
9
9
  import type { OrderByClause, QueryIR, Select } from "../ir.js"
10
10
  import type { NamespacedAndKeyedStream, NamespacedRow } from "../../types.js"
11
11
  import type { IStreamBuilder, KeyValue } from "@tanstack/db-ivm"
12
- import type { BaseIndex } from "../../indexes/base-index.js"
12
+ import type { IndexInterface } from "../../indexes/base-index.js"
13
13
  import type { Collection } from "../../collection/index.js"
14
14
 
15
15
  export type OrderByOptimizationInfo = {
@@ -20,7 +20,7 @@ export type OrderByOptimizationInfo = {
20
20
  b: Record<string, unknown> | null | undefined
21
21
  ) => number
22
22
  valueExtractorForRawRow: (row: Record<string, unknown>) => any
23
- index: BaseIndex<string | number>
23
+ index: IndexInterface<string | number>
24
24
  dataNeeded?: () => number
25
25
  }
26
26
 
@@ -54,18 +54,12 @@ export function processOrderBy(
54
54
 
55
55
  // Create a value extractor function for the orderBy operator
56
56
  const valueExtractor = (row: NamespacedRow & { __select_results?: any }) => {
57
- // For ORDER BY expressions, we need to provide access to both:
58
- // 1. The original namespaced row data (for direct table column references)
59
- // 2. The __select_results (for SELECT alias references)
60
-
61
- // Create a merged context for expression evaluation
62
- const orderByContext = { ...row }
63
-
64
- // If there are select results, merge them at the top level for alias access
65
- if (row.__select_results) {
66
- // Add select results as top-level properties for alias access
67
- Object.assign(orderByContext, row.__select_results)
68
- }
57
+ // The namespaced row contains:
58
+ // 1. Table aliases as top-level properties (e.g., row["tableName"])
59
+ // 2. SELECT results in __select_results (e.g., row.__select_results["aggregateAlias"])
60
+ // The replaceAggregatesByRefs function has already transformed any aggregate expressions
61
+ // that match SELECT aggregates to use the __select_results namespace.
62
+ const orderByContext = row
69
63
 
70
64
  if (orderByClause.length > 1) {
71
65
  // For multiple orderBy columns, create a composite key
@@ -132,6 +126,7 @@ export function processOrderBy(
132
126
  fieldName,
133
127
  followRefResult.path,
134
128
  followRefCollection,
129
+ clause.compareOptions,
135
130
  compare
136
131
  )
137
132
  }
@@ -150,10 +145,12 @@ export function processOrderBy(
150
145
  return compare(extractedA, extractedB)
151
146
  }
152
147
 
153
- const index: BaseIndex<string | number> | undefined = findIndexForField(
154
- followRefCollection.indexes,
155
- followRefResult.path
156
- )
148
+ const index: IndexInterface<string | number> | undefined =
149
+ findIndexForField(
150
+ followRefCollection.indexes,
151
+ followRefResult.path,
152
+ clause.compareOptions
153
+ )
157
154
 
158
155
  if (index && index.supports(`gt`)) {
159
156
  // We found an index that we can use to lazily load ordered data
@@ -9,6 +9,7 @@ export {
9
9
  type Context,
10
10
  type Source,
11
11
  type GetResult,
12
+ type InferResultType,
12
13
  } from "./builder/index.js"
13
14
 
14
15
  // Expression functions exports
package/src/query/ir.ts CHANGED
@@ -17,6 +17,7 @@ export interface QueryIR {
17
17
  limit?: Limit
18
18
  offset?: Offset
19
19
  distinct?: true
20
+ singleResult?: true
20
21
 
21
22
  // Functional variants
22
23
  fnSelect?: (row: NamespacedRow) => any
@@ -7,7 +7,7 @@ import type { RootStreamBuilder } from "@tanstack/db-ivm"
7
7
  import type { OrderByOptimizationInfo } from "../compiler/order-by.js"
8
8
  import type { Collection } from "../../collection/index.js"
9
9
  import type {
10
- CollectionConfig,
10
+ CollectionConfigSingleRowOption,
11
11
  KeyedStream,
12
12
  ResultStream,
13
13
  SyncConfig,
@@ -79,7 +79,7 @@ export class CollectionConfigBuilder<
79
79
  this.compileBasePipeline()
80
80
  }
81
81
 
82
- getConfig(): CollectionConfig<TResult> {
82
+ getConfig(): CollectionConfigSingleRowOption<TResult> {
83
83
  return {
84
84
  id: this.id,
85
85
  getKey:
@@ -93,6 +93,7 @@ export class CollectionConfigBuilder<
93
93
  onUpdate: this.config.onUpdate,
94
94
  onDelete: this.config.onDelete,
95
95
  startSync: this.config.startSync,
96
+ singleResult: this.query.singleResult,
96
97
  }
97
98
  }
98
99
 
@@ -90,4 +90,9 @@ export interface LiveQueryCollectionConfig<
90
90
  * GC time for the collection
91
91
  */
92
92
  gcTime?: number
93
+
94
+ /**
95
+ * If enabled the collection will return a single object instead of an array
96
+ */
97
+ singleResult?: true
93
98
  }
@@ -3,9 +3,29 @@ import { CollectionConfigBuilder } from "./live/collection-config-builder.js"
3
3
  import type { LiveQueryCollectionConfig } from "./live/types.js"
4
4
  import type { InitialQueryBuilder, QueryBuilder } from "./builder/index.js"
5
5
  import type { Collection } from "../collection/index.js"
6
- import type { CollectionConfig, UtilsRecord } from "../types.js"
6
+ import type {
7
+ CollectionConfig,
8
+ CollectionConfigSingleRowOption,
9
+ NonSingleResult,
10
+ SingleResult,
11
+ UtilsRecord,
12
+ } from "../types.js"
7
13
  import type { Context, GetResult } from "./builder/types.js"
8
14
 
15
+ type CollectionConfigForContext<
16
+ TContext extends Context,
17
+ TResult extends object,
18
+ > = TContext extends SingleResult
19
+ ? CollectionConfigSingleRowOption<TResult> & SingleResult
20
+ : CollectionConfigSingleRowOption<TResult> & NonSingleResult
21
+
22
+ type CollectionForContext<
23
+ TContext extends Context,
24
+ TResult extends object,
25
+ > = TContext extends SingleResult
26
+ ? Collection<TResult> & SingleResult
27
+ : Collection<TResult> & NonSingleResult
28
+
9
29
  /**
10
30
  * Creates live query collection options for use with createCollection
11
31
  *
@@ -35,12 +55,15 @@ export function liveQueryCollectionOptions<
35
55
  TResult extends object = GetResult<TContext>,
36
56
  >(
37
57
  config: LiveQueryCollectionConfig<TContext, TResult>
38
- ): CollectionConfig<TResult> {
58
+ ): CollectionConfigForContext<TContext, TResult> {
39
59
  const collectionConfigBuilder = new CollectionConfigBuilder<
40
60
  TContext,
41
61
  TResult
42
62
  >(config)
43
- return collectionConfigBuilder.getConfig()
63
+ return collectionConfigBuilder.getConfig() as CollectionConfigForContext<
64
+ TContext,
65
+ TResult
66
+ >
44
67
  }
45
68
 
46
69
  /**
@@ -83,7 +106,7 @@ export function createLiveQueryCollection<
83
106
  TResult extends object = GetResult<TContext>,
84
107
  >(
85
108
  query: (q: InitialQueryBuilder) => QueryBuilder<TContext>
86
- ): Collection<TResult, string | number, {}>
109
+ ): CollectionForContext<TContext, TResult>
87
110
 
88
111
  // Overload 2: Accept full config object with optional utilities
89
112
  export function createLiveQueryCollection<
@@ -92,7 +115,7 @@ export function createLiveQueryCollection<
92
115
  TUtils extends UtilsRecord = {},
93
116
  >(
94
117
  config: LiveQueryCollectionConfig<TContext, TResult> & { utils?: TUtils }
95
- ): Collection<TResult, string | number, TUtils>
118
+ ): CollectionForContext<TContext, TResult>
96
119
 
97
120
  // Implementation
98
121
  export function createLiveQueryCollection<
@@ -103,7 +126,7 @@ export function createLiveQueryCollection<
103
126
  configOrQuery:
104
127
  | (LiveQueryCollectionConfig<TContext, TResult> & { utils?: TUtils })
105
128
  | ((q: InitialQueryBuilder) => QueryBuilder<TContext>)
106
- ): Collection<TResult, string | number, TUtils> {
129
+ ): CollectionForContext<TContext, TResult> {
107
130
  // Determine if the argument is a function (query) or a config object
108
131
  if (typeof configOrQuery === `function`) {
109
132
  // Simple query function case
@@ -113,7 +136,10 @@ export function createLiveQueryCollection<
113
136
  ) => QueryBuilder<TContext>,
114
137
  }
115
138
  const options = liveQueryCollectionOptions<TContext, TResult>(config)
116
- return bridgeToCreateCollection(options)
139
+ return bridgeToCreateCollection(options) as CollectionForContext<
140
+ TContext,
141
+ TResult
142
+ >
117
143
  } else {
118
144
  // Config object case
119
145
  const config = configOrQuery as LiveQueryCollectionConfig<
@@ -124,7 +150,7 @@ export function createLiveQueryCollection<
124
150
  return bridgeToCreateCollection({
125
151
  ...options,
126
152
  utils: config.utils,
127
- })
153
+ }) as CollectionForContext<TContext, TResult>
128
154
  }
129
155
  }
130
156
 
package/src/types.ts CHANGED
@@ -503,6 +503,28 @@ export interface CollectionConfig<
503
503
  sync: SyncConfig<T, TKey>
504
504
  }
505
505
 
506
+ export type SingleResult = {
507
+ singleResult: true
508
+ }
509
+
510
+ export type NonSingleResult = {
511
+ singleResult?: never
512
+ }
513
+
514
+ export type MaybeSingleResult = {
515
+ /**
516
+ * If enabled the collection will return a single object instead of an array
517
+ */
518
+ singleResult?: true
519
+ }
520
+
521
+ // Only used for live query collections
522
+ export type CollectionConfigSingleRowOption<
523
+ T extends object = Record<string, unknown>,
524
+ TKey extends string | number = string | number,
525
+ TSchema extends StandardSchemaV1 = never,
526
+ > = CollectionConfig<T, TKey, TSchema> & MaybeSingleResult
527
+
506
528
  export type ChangesPayload<T extends object = Record<string, unknown>> = Array<
507
529
  ChangeMessage<T>
508
530
  >
@@ -15,7 +15,14 @@
15
15
  * - Optimizes IN array expressions
16
16
  */
17
17
 
18
- import type { BaseIndex, IndexOperation } from "../indexes/base-index.js"
18
+ import { DEFAULT_COMPARE_OPTIONS } from "../utils.js"
19
+ import { ReverseIndex } from "../indexes/reverse-index.js"
20
+ import type { CompareOptions } from "../query/builder/types.js"
21
+ import type {
22
+ BaseIndex,
23
+ IndexInterface,
24
+ IndexOperation,
25
+ } from "../indexes/base-index.js"
19
26
  import type { BasicExpression } from "../query/ir.js"
20
27
 
21
28
  /**
@@ -30,11 +37,18 @@ export interface OptimizationResult<TKey> {
30
37
  * Finds an index that matches a given field path
31
38
  */
32
39
  export function findIndexForField<TKey extends string | number>(
33
- indexes: Map<number, BaseIndex<TKey>>,
34
- fieldPath: Array<string>
35
- ): BaseIndex<TKey> | undefined {
40
+ indexes: Map<number, IndexInterface<TKey>>,
41
+ fieldPath: Array<string>,
42
+ compareOptions: CompareOptions = DEFAULT_COMPARE_OPTIONS
43
+ ): IndexInterface<TKey> | undefined {
36
44
  for (const index of indexes.values()) {
37
- if (index.matchesField(fieldPath)) {
45
+ if (
46
+ index.matchesField(fieldPath) &&
47
+ index.matchesCompareOptions(compareOptions)
48
+ ) {
49
+ if (!index.matchesDirection(compareOptions.direction)) {
50
+ return new ReverseIndex(index)
51
+ }
38
52
  return index
39
53
  }
40
54
  }
package/src/utils.ts CHANGED
@@ -2,6 +2,8 @@
2
2
  * Generic utility functions
3
3
  */
4
4
 
5
+ import type { CompareOptions } from "./query/builder/types"
6
+
5
7
  interface TypedArray {
6
8
  length: number
7
9
  [index: number]: number
@@ -209,3 +211,9 @@ export function isTemporal(a: any): boolean {
209
211
  const tag = getStringTag(a)
210
212
  return typeof tag === `string` && temporalTypes.includes(tag)
211
213
  }
214
+
215
+ export const DEFAULT_COMPARE_OPTIONS: CompareOptions = {
216
+ direction: `asc`,
217
+ nulls: `first`,
218
+ stringSort: `locale`,
219
+ }