@tanstack/db 0.0.14 → 0.0.15

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 (198) hide show
  1. package/dist/cjs/collection.cjs +117 -104
  2. package/dist/cjs/collection.cjs.map +1 -1
  3. package/dist/cjs/collection.d.cts +18 -21
  4. package/dist/cjs/index.cjs +35 -13
  5. package/dist/cjs/index.cjs.map +1 -1
  6. package/dist/cjs/index.d.cts +0 -1
  7. package/dist/cjs/query/builder/functions.cjs +107 -0
  8. package/dist/cjs/query/builder/functions.cjs.map +1 -0
  9. package/dist/cjs/query/builder/functions.d.cts +38 -0
  10. package/dist/cjs/query/builder/index.cjs +499 -0
  11. package/dist/cjs/query/builder/index.cjs.map +1 -0
  12. package/dist/cjs/query/builder/index.d.cts +324 -0
  13. package/dist/cjs/query/builder/ref-proxy.cjs +96 -0
  14. package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -0
  15. package/dist/cjs/query/builder/ref-proxy.d.cts +28 -0
  16. package/dist/cjs/query/builder/types.d.cts +80 -0
  17. package/dist/cjs/query/compiler/evaluators.cjs +261 -0
  18. package/dist/cjs/query/compiler/evaluators.cjs.map +1 -0
  19. package/dist/cjs/query/compiler/evaluators.d.cts +11 -0
  20. package/dist/cjs/query/compiler/group-by.cjs +271 -0
  21. package/dist/cjs/query/compiler/group-by.cjs.map +1 -0
  22. package/dist/cjs/query/compiler/group-by.d.cts +7 -0
  23. package/dist/cjs/query/compiler/index.cjs +181 -0
  24. package/dist/cjs/query/compiler/index.cjs.map +1 -0
  25. package/dist/cjs/query/compiler/index.d.cts +15 -0
  26. package/dist/cjs/query/compiler/joins.cjs +116 -0
  27. package/dist/cjs/query/compiler/joins.cjs.map +1 -0
  28. package/dist/cjs/query/compiler/joins.d.cts +11 -0
  29. package/dist/cjs/query/compiler/order-by.cjs +89 -0
  30. package/dist/cjs/query/compiler/order-by.cjs.map +1 -0
  31. package/dist/cjs/query/compiler/order-by.d.cts +9 -0
  32. package/dist/cjs/query/compiler/select.cjs +57 -0
  33. package/dist/cjs/query/compiler/select.cjs.map +1 -0
  34. package/dist/cjs/query/compiler/select.d.cts +15 -0
  35. package/dist/cjs/query/index.d.cts +6 -5
  36. package/dist/cjs/query/ir.cjs +57 -0
  37. package/dist/cjs/query/ir.cjs.map +1 -0
  38. package/dist/cjs/query/ir.d.cts +81 -0
  39. package/dist/cjs/query/live-query-collection.cjs +224 -0
  40. package/dist/cjs/query/live-query-collection.cjs.map +1 -0
  41. package/dist/cjs/query/live-query-collection.d.cts +124 -0
  42. package/dist/cjs/transactions.cjs +20 -13
  43. package/dist/cjs/transactions.cjs.map +1 -1
  44. package/dist/cjs/transactions.d.cts +10 -1
  45. package/dist/cjs/types.d.cts +13 -0
  46. package/dist/esm/collection.d.ts +18 -21
  47. package/dist/esm/collection.js +118 -105
  48. package/dist/esm/collection.js.map +1 -1
  49. package/dist/esm/index.d.ts +0 -1
  50. package/dist/esm/index.js +34 -12
  51. package/dist/esm/index.js.map +1 -1
  52. package/dist/esm/query/builder/functions.d.ts +38 -0
  53. package/dist/esm/query/builder/functions.js +107 -0
  54. package/dist/esm/query/builder/functions.js.map +1 -0
  55. package/dist/esm/query/builder/index.d.ts +324 -0
  56. package/dist/esm/query/builder/index.js +499 -0
  57. package/dist/esm/query/builder/index.js.map +1 -0
  58. package/dist/esm/query/builder/ref-proxy.d.ts +28 -0
  59. package/dist/esm/query/builder/ref-proxy.js +96 -0
  60. package/dist/esm/query/builder/ref-proxy.js.map +1 -0
  61. package/dist/esm/query/builder/types.d.ts +80 -0
  62. package/dist/esm/query/compiler/evaluators.d.ts +11 -0
  63. package/dist/esm/query/compiler/evaluators.js +261 -0
  64. package/dist/esm/query/compiler/evaluators.js.map +1 -0
  65. package/dist/esm/query/compiler/group-by.d.ts +7 -0
  66. package/dist/esm/query/compiler/group-by.js +271 -0
  67. package/dist/esm/query/compiler/group-by.js.map +1 -0
  68. package/dist/esm/query/compiler/index.d.ts +15 -0
  69. package/dist/esm/query/compiler/index.js +181 -0
  70. package/dist/esm/query/compiler/index.js.map +1 -0
  71. package/dist/esm/query/compiler/joins.d.ts +11 -0
  72. package/dist/esm/query/compiler/joins.js +116 -0
  73. package/dist/esm/query/compiler/joins.js.map +1 -0
  74. package/dist/esm/query/compiler/order-by.d.ts +9 -0
  75. package/dist/esm/query/compiler/order-by.js +89 -0
  76. package/dist/esm/query/compiler/order-by.js.map +1 -0
  77. package/dist/esm/query/compiler/select.d.ts +15 -0
  78. package/dist/esm/query/compiler/select.js +57 -0
  79. package/dist/esm/query/compiler/select.js.map +1 -0
  80. package/dist/esm/query/index.d.ts +6 -5
  81. package/dist/esm/query/ir.d.ts +81 -0
  82. package/dist/esm/query/ir.js +57 -0
  83. package/dist/esm/query/ir.js.map +1 -0
  84. package/dist/esm/query/live-query-collection.d.ts +124 -0
  85. package/dist/esm/query/live-query-collection.js +224 -0
  86. package/dist/esm/query/live-query-collection.js.map +1 -0
  87. package/dist/esm/transactions.d.ts +10 -1
  88. package/dist/esm/transactions.js +20 -13
  89. package/dist/esm/transactions.js.map +1 -1
  90. package/dist/esm/types.d.ts +13 -0
  91. package/package.json +3 -4
  92. package/src/collection.ts +152 -129
  93. package/src/index.ts +0 -1
  94. package/src/query/builder/functions.ts +267 -0
  95. package/src/query/builder/index.ts +648 -0
  96. package/src/query/builder/ref-proxy.ts +156 -0
  97. package/src/query/builder/types.ts +278 -0
  98. package/src/query/compiler/evaluators.ts +315 -0
  99. package/src/query/compiler/group-by.ts +428 -0
  100. package/src/query/compiler/index.ts +276 -0
  101. package/src/query/compiler/joins.ts +228 -0
  102. package/src/query/compiler/order-by.ts +139 -0
  103. package/src/query/compiler/select.ts +173 -0
  104. package/src/query/index.ts +64 -5
  105. package/src/query/ir.ts +128 -0
  106. package/src/query/live-query-collection.ts +509 -0
  107. package/src/transactions.ts +27 -16
  108. package/src/types.ts +15 -0
  109. package/dist/cjs/query/compiled-query.cjs +0 -160
  110. package/dist/cjs/query/compiled-query.cjs.map +0 -1
  111. package/dist/cjs/query/compiled-query.d.cts +0 -20
  112. package/dist/cjs/query/evaluators.cjs +0 -161
  113. package/dist/cjs/query/evaluators.cjs.map +0 -1
  114. package/dist/cjs/query/evaluators.d.cts +0 -14
  115. package/dist/cjs/query/extractors.cjs +0 -122
  116. package/dist/cjs/query/extractors.cjs.map +0 -1
  117. package/dist/cjs/query/extractors.d.cts +0 -22
  118. package/dist/cjs/query/functions.cjs +0 -152
  119. package/dist/cjs/query/functions.cjs.map +0 -1
  120. package/dist/cjs/query/functions.d.cts +0 -21
  121. package/dist/cjs/query/group-by.cjs +0 -88
  122. package/dist/cjs/query/group-by.cjs.map +0 -1
  123. package/dist/cjs/query/group-by.d.cts +0 -40
  124. package/dist/cjs/query/joins.cjs +0 -141
  125. package/dist/cjs/query/joins.cjs.map +0 -1
  126. package/dist/cjs/query/joins.d.cts +0 -14
  127. package/dist/cjs/query/order-by.cjs +0 -185
  128. package/dist/cjs/query/order-by.cjs.map +0 -1
  129. package/dist/cjs/query/order-by.d.cts +0 -3
  130. package/dist/cjs/query/pipeline-compiler.cjs +0 -89
  131. package/dist/cjs/query/pipeline-compiler.cjs.map +0 -1
  132. package/dist/cjs/query/pipeline-compiler.d.cts +0 -10
  133. package/dist/cjs/query/query-builder.cjs +0 -307
  134. package/dist/cjs/query/query-builder.cjs.map +0 -1
  135. package/dist/cjs/query/query-builder.d.cts +0 -225
  136. package/dist/cjs/query/schema.d.cts +0 -100
  137. package/dist/cjs/query/select.cjs +0 -130
  138. package/dist/cjs/query/select.cjs.map +0 -1
  139. package/dist/cjs/query/select.d.cts +0 -3
  140. package/dist/cjs/query/types.d.cts +0 -189
  141. package/dist/cjs/query/utils.cjs +0 -154
  142. package/dist/cjs/query/utils.cjs.map +0 -1
  143. package/dist/cjs/query/utils.d.cts +0 -37
  144. package/dist/cjs/utils.cjs +0 -17
  145. package/dist/cjs/utils.cjs.map +0 -1
  146. package/dist/cjs/utils.d.cts +0 -3
  147. package/dist/esm/query/compiled-query.d.ts +0 -20
  148. package/dist/esm/query/compiled-query.js +0 -160
  149. package/dist/esm/query/compiled-query.js.map +0 -1
  150. package/dist/esm/query/evaluators.d.ts +0 -14
  151. package/dist/esm/query/evaluators.js +0 -161
  152. package/dist/esm/query/evaluators.js.map +0 -1
  153. package/dist/esm/query/extractors.d.ts +0 -22
  154. package/dist/esm/query/extractors.js +0 -122
  155. package/dist/esm/query/extractors.js.map +0 -1
  156. package/dist/esm/query/functions.d.ts +0 -21
  157. package/dist/esm/query/functions.js +0 -152
  158. package/dist/esm/query/functions.js.map +0 -1
  159. package/dist/esm/query/group-by.d.ts +0 -40
  160. package/dist/esm/query/group-by.js +0 -88
  161. package/dist/esm/query/group-by.js.map +0 -1
  162. package/dist/esm/query/joins.d.ts +0 -14
  163. package/dist/esm/query/joins.js +0 -141
  164. package/dist/esm/query/joins.js.map +0 -1
  165. package/dist/esm/query/order-by.d.ts +0 -3
  166. package/dist/esm/query/order-by.js +0 -185
  167. package/dist/esm/query/order-by.js.map +0 -1
  168. package/dist/esm/query/pipeline-compiler.d.ts +0 -10
  169. package/dist/esm/query/pipeline-compiler.js +0 -89
  170. package/dist/esm/query/pipeline-compiler.js.map +0 -1
  171. package/dist/esm/query/query-builder.d.ts +0 -225
  172. package/dist/esm/query/query-builder.js +0 -307
  173. package/dist/esm/query/query-builder.js.map +0 -1
  174. package/dist/esm/query/schema.d.ts +0 -100
  175. package/dist/esm/query/select.d.ts +0 -3
  176. package/dist/esm/query/select.js +0 -130
  177. package/dist/esm/query/select.js.map +0 -1
  178. package/dist/esm/query/types.d.ts +0 -189
  179. package/dist/esm/query/utils.d.ts +0 -37
  180. package/dist/esm/query/utils.js +0 -154
  181. package/dist/esm/query/utils.js.map +0 -1
  182. package/dist/esm/utils.d.ts +0 -3
  183. package/dist/esm/utils.js +0 -17
  184. package/dist/esm/utils.js.map +0 -1
  185. package/src/query/compiled-query.ts +0 -234
  186. package/src/query/evaluators.ts +0 -250
  187. package/src/query/extractors.ts +0 -214
  188. package/src/query/functions.ts +0 -297
  189. package/src/query/group-by.ts +0 -139
  190. package/src/query/joins.ts +0 -260
  191. package/src/query/order-by.ts +0 -264
  192. package/src/query/pipeline-compiler.ts +0 -149
  193. package/src/query/query-builder.ts +0 -902
  194. package/src/query/schema.ts +0 -268
  195. package/src/query/select.ts +0 -208
  196. package/src/query/types.ts +0 -418
  197. package/src/query/utils.ts +0 -245
  198. package/src/utils.ts +0 -15
@@ -0,0 +1,648 @@
1
+ import { CollectionImpl } from "../../collection.js"
2
+ import { CollectionRef, QueryRef } from "../ir.js"
3
+ import { createRefProxy, isRefProxy, toExpression } from "./ref-proxy.js"
4
+ import type { NamespacedRow } from "../../types.js"
5
+ import type {
6
+ Aggregate,
7
+ BasicExpression,
8
+ JoinClause,
9
+ OrderBy,
10
+ OrderByClause,
11
+ OrderByDirection,
12
+ QueryIR,
13
+ } from "../ir.js"
14
+ import type {
15
+ Context,
16
+ GroupByCallback,
17
+ JoinOnCallback,
18
+ MergeContext,
19
+ MergeContextWithJoinType,
20
+ OrderByCallback,
21
+ RefProxyForContext,
22
+ ResultTypeFromSelect,
23
+ SchemaFromSource,
24
+ SelectObject,
25
+ Source,
26
+ WhereCallback,
27
+ WithResult,
28
+ } from "./types.js"
29
+
30
+ export class BaseQueryBuilder<TContext extends Context = Context> {
31
+ private readonly query: Partial<QueryIR> = {}
32
+
33
+ constructor(query: Partial<QueryIR> = {}) {
34
+ this.query = { ...query }
35
+ }
36
+
37
+ /**
38
+ * Creates a CollectionRef or QueryRef from a source object
39
+ * @param source - An object with a single key-value pair
40
+ * @param context - Context string for error messages (e.g., "from clause", "join clause")
41
+ * @returns A tuple of [alias, ref] where alias is the source key and ref is the created reference
42
+ */
43
+ private _createRefForSource<TSource extends Source>(
44
+ source: TSource,
45
+ context: string
46
+ ): [string, CollectionRef | QueryRef] {
47
+ if (Object.keys(source).length !== 1) {
48
+ throw new Error(`Only one source is allowed in the ${context}`)
49
+ }
50
+
51
+ const alias = Object.keys(source)[0]!
52
+ const sourceValue = source[alias]
53
+
54
+ let ref: CollectionRef | QueryRef
55
+
56
+ if (sourceValue instanceof CollectionImpl) {
57
+ ref = new CollectionRef(sourceValue, alias)
58
+ } else if (sourceValue instanceof BaseQueryBuilder) {
59
+ const subQuery = sourceValue._getQuery()
60
+ if (!(subQuery as Partial<QueryIR>).from) {
61
+ throw new Error(
62
+ `A sub query passed to a ${context} must have a from clause itself`
63
+ )
64
+ }
65
+ ref = new QueryRef(subQuery, alias)
66
+ } else {
67
+ throw new Error(`Invalid source`)
68
+ }
69
+
70
+ return [alias, ref]
71
+ }
72
+
73
+ /**
74
+ * Specify the source table or subquery for the query
75
+ *
76
+ * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery
77
+ * @returns A QueryBuilder with the specified source
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * // Query from a collection
82
+ * query.from({ users: usersCollection })
83
+ *
84
+ * // Query from a subquery
85
+ * const activeUsers = query.from({ u: usersCollection }).where(({u}) => u.active)
86
+ * query.from({ activeUsers })
87
+ * ```
88
+ */
89
+ from<TSource extends Source>(
90
+ source: TSource
91
+ ): QueryBuilder<{
92
+ baseSchema: SchemaFromSource<TSource>
93
+ schema: SchemaFromSource<TSource>
94
+ fromSourceName: keyof TSource & string
95
+ hasJoins: false
96
+ }> {
97
+ const [, from] = this._createRefForSource(source, `from clause`)
98
+
99
+ return new BaseQueryBuilder({
100
+ ...this.query,
101
+ from,
102
+ }) as any
103
+ }
104
+
105
+ /**
106
+ * Join another table or subquery to the current query
107
+ *
108
+ * @param source - An object with a single key-value pair where the key is the table alias and the value is a Collection or subquery
109
+ * @param onCallback - A function that receives table references and returns the join condition
110
+ * @param type - The type of join: 'inner', 'left', 'right', or 'full' (defaults to 'left')
111
+ * @returns A QueryBuilder with the joined table available
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * // Left join users with posts
116
+ * query
117
+ * .from({ users: usersCollection })
118
+ * .join({ posts: postsCollection }, ({users, posts}) => eq(users.id, posts.userId))
119
+ *
120
+ * // Inner join with explicit type
121
+ * query
122
+ * .from({ u: usersCollection })
123
+ * .join({ p: postsCollection }, ({u, p}) => eq(u.id, p.userId), 'inner')
124
+ * ```
125
+ *
126
+ * // Join with a subquery
127
+ * const activeUsers = query.from({ u: usersCollection }).where(({u}) => u.active)
128
+ * query
129
+ * .from({ activeUsers })
130
+ * .join({ p: postsCollection }, ({u, p}) => eq(u.id, p.userId))
131
+ */
132
+ join<
133
+ TSource extends Source,
134
+ TJoinType extends `inner` | `left` | `right` | `full` = `left`,
135
+ >(
136
+ source: TSource,
137
+ onCallback: JoinOnCallback<
138
+ MergeContext<TContext, SchemaFromSource<TSource>>
139
+ >,
140
+ type: TJoinType = `left` as TJoinType
141
+ ): QueryBuilder<
142
+ MergeContextWithJoinType<TContext, SchemaFromSource<TSource>, TJoinType>
143
+ > {
144
+ const [alias, from] = this._createRefForSource(source, `join clause`)
145
+
146
+ // Create a temporary context for the callback
147
+ const currentAliases = this._getCurrentAliases()
148
+ const newAliases = [...currentAliases, alias]
149
+ const refProxy = createRefProxy(newAliases) as RefProxyForContext<
150
+ MergeContext<TContext, SchemaFromSource<TSource>>
151
+ >
152
+
153
+ // Get the join condition expression
154
+ const onExpression = onCallback(refProxy)
155
+
156
+ // Extract left and right from the expression
157
+ // For now, we'll assume it's an eq function with two arguments
158
+ let left: BasicExpression
159
+ let right: BasicExpression
160
+
161
+ if (
162
+ onExpression.type === `func` &&
163
+ onExpression.name === `eq` &&
164
+ onExpression.args.length === 2
165
+ ) {
166
+ left = onExpression.args[0]!
167
+ right = onExpression.args[1]!
168
+ } else {
169
+ throw new Error(`Join condition must be an equality expression`)
170
+ }
171
+
172
+ const joinClause: JoinClause = {
173
+ from,
174
+ type,
175
+ left,
176
+ right,
177
+ }
178
+
179
+ const existingJoins = this.query.join || []
180
+
181
+ return new BaseQueryBuilder({
182
+ ...this.query,
183
+ join: [...existingJoins, joinClause],
184
+ }) as any
185
+ }
186
+
187
+ /**
188
+ * Filter rows based on a condition
189
+ *
190
+ * @param callback - A function that receives table references and returns an expression
191
+ * @returns A QueryBuilder with the where condition applied
192
+ *
193
+ * @example
194
+ * ```ts
195
+ * // Simple condition
196
+ * query
197
+ * .from({ users: usersCollection })
198
+ * .where(({users}) => gt(users.age, 18))
199
+ *
200
+ * // Multiple conditions
201
+ * query
202
+ * .from({ users: usersCollection })
203
+ * .where(({users}) => and(
204
+ * gt(users.age, 18),
205
+ * eq(users.active, true)
206
+ * ))
207
+ *
208
+ * // Multiple where calls are ANDed together
209
+ * query
210
+ * .from({ users: usersCollection })
211
+ * .where(({users}) => gt(users.age, 18))
212
+ * .where(({users}) => eq(users.active, true))
213
+ * ```
214
+ */
215
+ where(callback: WhereCallback<TContext>): QueryBuilder<TContext> {
216
+ const aliases = this._getCurrentAliases()
217
+ const refProxy = createRefProxy(aliases) as RefProxyForContext<TContext>
218
+ const expression = callback(refProxy)
219
+
220
+ const existingWhere = this.query.where || []
221
+
222
+ return new BaseQueryBuilder({
223
+ ...this.query,
224
+ where: [...existingWhere, expression],
225
+ }) as any
226
+ }
227
+
228
+ /**
229
+ * Filter grouped rows based on aggregate conditions
230
+ *
231
+ * @param callback - A function that receives table references and returns an expression
232
+ * @returns A QueryBuilder with the having condition applied
233
+ *
234
+ * @example
235
+ * ```ts
236
+ * // Filter groups by count
237
+ * query
238
+ * .from({ posts: postsCollection })
239
+ * .groupBy(({posts}) => posts.userId)
240
+ * .having(({posts}) => gt(count(posts.id), 5))
241
+ *
242
+ * // Filter by average
243
+ * query
244
+ * .from({ orders: ordersCollection })
245
+ * .groupBy(({orders}) => orders.customerId)
246
+ * .having(({orders}) => gt(avg(orders.total), 100))
247
+ *
248
+ * // Multiple having calls are ANDed together
249
+ * query
250
+ * .from({ orders: ordersCollection })
251
+ * .groupBy(({orders}) => orders.customerId)
252
+ * .having(({orders}) => gt(count(orders.id), 5))
253
+ * .having(({orders}) => gt(avg(orders.total), 100))
254
+ * ```
255
+ */
256
+ having(callback: WhereCallback<TContext>): QueryBuilder<TContext> {
257
+ const aliases = this._getCurrentAliases()
258
+ const refProxy = createRefProxy(aliases) as RefProxyForContext<TContext>
259
+ const expression = callback(refProxy)
260
+
261
+ const existingHaving = this.query.having || []
262
+
263
+ return new BaseQueryBuilder({
264
+ ...this.query,
265
+ having: [...existingHaving, expression],
266
+ }) as any
267
+ }
268
+
269
+ /**
270
+ * Select specific columns or computed values from the query
271
+ *
272
+ * @param callback - A function that receives table references and returns an object with selected fields or expressions
273
+ * @returns A QueryBuilder that returns only the selected fields
274
+ *
275
+ * @example
276
+ * ```ts
277
+ * // Select specific columns
278
+ * query
279
+ * .from({ users: usersCollection })
280
+ * .select(({users}) => ({
281
+ * name: users.name,
282
+ * email: users.email
283
+ * }))
284
+ *
285
+ * // Select with computed values
286
+ * query
287
+ * .from({ users: usersCollection })
288
+ * .select(({users}) => ({
289
+ * fullName: concat(users.firstName, ' ', users.lastName),
290
+ * ageInMonths: mul(users.age, 12)
291
+ * }))
292
+ *
293
+ * // Select with aggregates (requires GROUP BY)
294
+ * query
295
+ * .from({ posts: postsCollection })
296
+ * .groupBy(({posts}) => posts.userId)
297
+ * .select(({posts, count}) => ({
298
+ * userId: posts.userId,
299
+ * postCount: count(posts.id)
300
+ * }))
301
+ * ```
302
+ */
303
+ select<TSelectObject extends SelectObject>(
304
+ callback: (refs: RefProxyForContext<TContext>) => TSelectObject
305
+ ): QueryBuilder<WithResult<TContext, ResultTypeFromSelect<TSelectObject>>> {
306
+ const aliases = this._getCurrentAliases()
307
+ const refProxy = createRefProxy(aliases) as RefProxyForContext<TContext>
308
+ const selectObject = callback(refProxy)
309
+
310
+ // Check if any tables were spread during the callback
311
+ const spreadSentinels = (refProxy as any).__spreadSentinels as Set<string>
312
+
313
+ // Convert the select object to use expressions, including spread sentinels
314
+ const select: Record<string, BasicExpression | Aggregate> = {}
315
+
316
+ // First, add spread sentinels for any tables that were spread
317
+ for (const spreadAlias of spreadSentinels) {
318
+ const sentinelKey = `__SPREAD_SENTINEL__${spreadAlias}`
319
+ select[sentinelKey] = toExpression(spreadAlias) // Use alias as a simple reference
320
+ }
321
+
322
+ // Then add the explicit select fields
323
+ for (const [key, value] of Object.entries(selectObject)) {
324
+ if (isRefProxy(value)) {
325
+ select[key] = toExpression(value)
326
+ } else if (
327
+ typeof value === `object` &&
328
+ `type` in value &&
329
+ (value.type === `agg` || value.type === `func`)
330
+ ) {
331
+ select[key] = value as BasicExpression | Aggregate
332
+ } else {
333
+ select[key] = toExpression(value)
334
+ }
335
+ }
336
+
337
+ return new BaseQueryBuilder({
338
+ ...this.query,
339
+ select,
340
+ fnSelect: undefined, // remove the fnSelect clause if it exists
341
+ }) as any
342
+ }
343
+
344
+ /**
345
+ * Sort the query results by one or more columns
346
+ *
347
+ * @param callback - A function that receives table references and returns the field to sort by
348
+ * @param direction - Sort direction: 'asc' for ascending, 'desc' for descending (defaults to 'asc')
349
+ * @returns A QueryBuilder with the ordering applied
350
+ *
351
+ * @example
352
+ * ```ts
353
+ * // Sort by a single column
354
+ * query
355
+ * .from({ users: usersCollection })
356
+ * .orderBy(({users}) => users.name)
357
+ *
358
+ * // Sort descending
359
+ * query
360
+ * .from({ users: usersCollection })
361
+ * .orderBy(({users}) => users.createdAt, 'desc')
362
+ *
363
+ * // Multiple sorts (chain orderBy calls)
364
+ * query
365
+ * .from({ users: usersCollection })
366
+ * .orderBy(({users}) => users.lastName)
367
+ * .orderBy(({users}) => users.firstName)
368
+ * ```
369
+ */
370
+ orderBy(
371
+ callback: OrderByCallback<TContext>,
372
+ direction: OrderByDirection = `asc`
373
+ ): QueryBuilder<TContext> {
374
+ const aliases = this._getCurrentAliases()
375
+ const refProxy = createRefProxy(aliases) as RefProxyForContext<TContext>
376
+ const result = callback(refProxy)
377
+
378
+ // Create the new OrderBy structure with expression and direction
379
+ const orderByClause: OrderByClause = {
380
+ expression: toExpression(result),
381
+ direction,
382
+ }
383
+
384
+ const existingOrderBy: OrderBy = this.query.orderBy || []
385
+
386
+ return new BaseQueryBuilder({
387
+ ...this.query,
388
+ orderBy: [...existingOrderBy, orderByClause],
389
+ }) as any
390
+ }
391
+
392
+ /**
393
+ * Group rows by one or more columns for aggregation
394
+ *
395
+ * @param callback - A function that receives table references and returns the field(s) to group by
396
+ * @returns A QueryBuilder with grouping applied (enables aggregate functions in SELECT and HAVING)
397
+ *
398
+ * @example
399
+ * ```ts
400
+ * // Group by a single column
401
+ * query
402
+ * .from({ posts: postsCollection })
403
+ * .groupBy(({posts}) => posts.userId)
404
+ * .select(({posts, count}) => ({
405
+ * userId: posts.userId,
406
+ * postCount: count()
407
+ * }))
408
+ *
409
+ * // Group by multiple columns
410
+ * query
411
+ * .from({ sales: salesCollection })
412
+ * .groupBy(({sales}) => [sales.region, sales.category])
413
+ * .select(({sales, sum}) => ({
414
+ * region: sales.region,
415
+ * category: sales.category,
416
+ * totalSales: sum(sales.amount)
417
+ * }))
418
+ * ```
419
+ */
420
+ groupBy(callback: GroupByCallback<TContext>): QueryBuilder<TContext> {
421
+ const aliases = this._getCurrentAliases()
422
+ const refProxy = createRefProxy(aliases) as RefProxyForContext<TContext>
423
+ const result = callback(refProxy)
424
+
425
+ const newExpressions = Array.isArray(result)
426
+ ? result.map((r) => toExpression(r))
427
+ : [toExpression(result)]
428
+
429
+ // Replace existing groupBy expressions instead of extending them
430
+ return new BaseQueryBuilder({
431
+ ...this.query,
432
+ groupBy: newExpressions,
433
+ }) as any
434
+ }
435
+
436
+ /**
437
+ * Limit the number of rows returned by the query
438
+ * `orderBy` is required for `limit`
439
+ *
440
+ * @param count - Maximum number of rows to return
441
+ * @returns A QueryBuilder with the limit applied
442
+ *
443
+ * @example
444
+ * ```ts
445
+ * // Get top 5 posts by likes
446
+ * query
447
+ * .from({ posts: postsCollection })
448
+ * .orderBy(({posts}) => posts.likes, 'desc')
449
+ * .limit(5)
450
+ * ```
451
+ */
452
+ limit(count: number): QueryBuilder<TContext> {
453
+ return new BaseQueryBuilder({
454
+ ...this.query,
455
+ limit: count,
456
+ }) as any
457
+ }
458
+
459
+ /**
460
+ * Skip a number of rows before returning results
461
+ * `orderBy` is required for `offset`
462
+ *
463
+ * @param count - Number of rows to skip
464
+ * @returns A QueryBuilder with the offset applied
465
+ *
466
+ * @example
467
+ * ```ts
468
+ * // Get second page of results
469
+ * query
470
+ * .from({ posts: postsCollection })
471
+ * .orderBy(({posts}) => posts.createdAt, 'desc')
472
+ * .offset(page * pageSize)
473
+ * .limit(pageSize)
474
+ * ```
475
+ */
476
+ offset(count: number): QueryBuilder<TContext> {
477
+ return new BaseQueryBuilder({
478
+ ...this.query,
479
+ offset: count,
480
+ }) as any
481
+ }
482
+
483
+ // Helper methods
484
+ private _getCurrentAliases(): Array<string> {
485
+ const aliases: Array<string> = []
486
+
487
+ // Add the from alias
488
+ if (this.query.from) {
489
+ aliases.push(this.query.from.alias)
490
+ }
491
+
492
+ // Add join aliases
493
+ if (this.query.join) {
494
+ for (const join of this.query.join) {
495
+ aliases.push(join.from.alias)
496
+ }
497
+ }
498
+
499
+ return aliases
500
+ }
501
+
502
+ /**
503
+ * Functional variants of the query builder
504
+ * These are imperative function that are called for ery row.
505
+ * Warning: that these cannot be optimized by the query compiler, and may prevent
506
+ * some type of optimizations being possible.
507
+ * @example
508
+ * ```ts
509
+ * q.fn.select((row) => ({
510
+ * name: row.user.name.toUpperCase(),
511
+ * age: row.user.age + 1,
512
+ * }))
513
+ * ```
514
+ */
515
+ get fn() {
516
+ const builder = this
517
+ return {
518
+ /**
519
+ * Select fields using a function that operates on each row
520
+ * Warning: This cannot be optimized by the query compiler
521
+ *
522
+ * @param callback - A function that receives a row and returns the selected value
523
+ * @returns A QueryBuilder with functional selection applied
524
+ *
525
+ * @example
526
+ * ```ts
527
+ * // Functional select (not optimized)
528
+ * query
529
+ * .from({ users: usersCollection })
530
+ * .fn.select(row => ({
531
+ * name: row.users.name.toUpperCase(),
532
+ * age: row.users.age + 1,
533
+ * }))
534
+ * ```
535
+ */
536
+ select<TFuncSelectResult>(
537
+ callback: (row: TContext[`schema`]) => TFuncSelectResult
538
+ ): QueryBuilder<WithResult<TContext, TFuncSelectResult>> {
539
+ return new BaseQueryBuilder({
540
+ ...builder.query,
541
+ select: undefined, // remove the select clause if it exists
542
+ fnSelect: callback,
543
+ })
544
+ },
545
+ /**
546
+ * Filter rows using a function that operates on each row
547
+ * Warning: This cannot be optimized by the query compiler
548
+ *
549
+ * @param callback - A function that receives a row and returns a boolean
550
+ * @returns A QueryBuilder with functional filtering applied
551
+ *
552
+ * @example
553
+ * ```ts
554
+ * // Functional where (not optimized)
555
+ * query
556
+ * .from({ users: usersCollection })
557
+ * .fn.where(row => row.users.name.startsWith('A'))
558
+ * ```
559
+ */
560
+ where(
561
+ callback: (row: TContext[`schema`]) => any
562
+ ): QueryBuilder<TContext> {
563
+ return new BaseQueryBuilder({
564
+ ...builder.query,
565
+ fnWhere: [
566
+ ...(builder.query.fnWhere || []),
567
+ callback as (row: NamespacedRow) => any,
568
+ ],
569
+ })
570
+ },
571
+ /**
572
+ * Filter grouped rows using a function that operates on each aggregated row
573
+ * Warning: This cannot be optimized by the query compiler
574
+ *
575
+ * @param callback - A function that receives an aggregated row and returns a boolean
576
+ * @returns A QueryBuilder with functional having filter applied
577
+ *
578
+ * @example
579
+ * ```ts
580
+ * // Functional having (not optimized)
581
+ * query
582
+ * .from({ posts: postsCollection })
583
+ * .groupBy(({posts}) => posts.userId)
584
+ * .fn.having(row => row.count > 5)
585
+ * ```
586
+ */
587
+ having(
588
+ callback: (row: TContext[`schema`]) => any
589
+ ): QueryBuilder<TContext> {
590
+ return new BaseQueryBuilder({
591
+ ...builder.query,
592
+ fnHaving: [
593
+ ...(builder.query.fnHaving || []),
594
+ callback as (row: NamespacedRow) => any,
595
+ ],
596
+ })
597
+ },
598
+ }
599
+ }
600
+
601
+ _getQuery(): QueryIR {
602
+ if (!this.query.from) {
603
+ throw new Error(`Query must have a from clause`)
604
+ }
605
+ return this.query as QueryIR
606
+ }
607
+ }
608
+
609
+ // Internal function to build a query from a callback
610
+ // used by liveQueryCollectionOptions.query
611
+ export function buildQuery<TContext extends Context>(
612
+ fn: (builder: InitialQueryBuilder) => QueryBuilder<TContext>
613
+ ): QueryIR {
614
+ const result = fn(new BaseQueryBuilder())
615
+ return getQueryIR(result)
616
+ }
617
+
618
+ // Internal function to get the QueryIR from a builder
619
+ export function getQueryIR(
620
+ builder: BaseQueryBuilder | QueryBuilder<any> | InitialQueryBuilder
621
+ ): QueryIR {
622
+ return (builder as unknown as BaseQueryBuilder)._getQuery()
623
+ }
624
+
625
+ // Type-only exports for the query builder
626
+ export type InitialQueryBuilder = Pick<BaseQueryBuilder<Context>, `from`>
627
+
628
+ export type InitialQueryBuilderConstructor = new () => InitialQueryBuilder
629
+
630
+ export type QueryBuilder<TContext extends Context> = Omit<
631
+ BaseQueryBuilder<TContext>,
632
+ `from` | `_getQuery`
633
+ >
634
+
635
+ // Main query builder class alias with the constructor type modified to hide all
636
+ // but the from method on the initial instance
637
+ export const Query: InitialQueryBuilderConstructor = BaseQueryBuilder
638
+
639
+ // Helper type to extract context from a QueryBuilder
640
+ export type ExtractContext<T> =
641
+ T extends BaseQueryBuilder<infer TContext>
642
+ ? TContext
643
+ : T extends QueryBuilder<infer TContext>
644
+ ? TContext
645
+ : never
646
+
647
+ // Export the types from types.ts for convenience
648
+ export type { Context, Source, GetResult } from "./types.js"