@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.
- package/dist/cjs/collection.cjs +117 -104
- package/dist/cjs/collection.cjs.map +1 -1
- package/dist/cjs/collection.d.cts +18 -21
- package/dist/cjs/index.cjs +35 -13
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +0 -1
- package/dist/cjs/query/builder/functions.cjs +107 -0
- package/dist/cjs/query/builder/functions.cjs.map +1 -0
- package/dist/cjs/query/builder/functions.d.cts +38 -0
- package/dist/cjs/query/builder/index.cjs +499 -0
- package/dist/cjs/query/builder/index.cjs.map +1 -0
- package/dist/cjs/query/builder/index.d.cts +324 -0
- package/dist/cjs/query/builder/ref-proxy.cjs +96 -0
- package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -0
- package/dist/cjs/query/builder/ref-proxy.d.cts +28 -0
- package/dist/cjs/query/builder/types.d.cts +80 -0
- package/dist/cjs/query/compiler/evaluators.cjs +261 -0
- package/dist/cjs/query/compiler/evaluators.cjs.map +1 -0
- package/dist/cjs/query/compiler/evaluators.d.cts +11 -0
- package/dist/cjs/query/compiler/group-by.cjs +271 -0
- package/dist/cjs/query/compiler/group-by.cjs.map +1 -0
- package/dist/cjs/query/compiler/group-by.d.cts +7 -0
- package/dist/cjs/query/compiler/index.cjs +181 -0
- package/dist/cjs/query/compiler/index.cjs.map +1 -0
- package/dist/cjs/query/compiler/index.d.cts +15 -0
- package/dist/cjs/query/compiler/joins.cjs +116 -0
- package/dist/cjs/query/compiler/joins.cjs.map +1 -0
- package/dist/cjs/query/compiler/joins.d.cts +11 -0
- package/dist/cjs/query/compiler/order-by.cjs +89 -0
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -0
- package/dist/cjs/query/compiler/order-by.d.cts +9 -0
- package/dist/cjs/query/compiler/select.cjs +57 -0
- package/dist/cjs/query/compiler/select.cjs.map +1 -0
- package/dist/cjs/query/compiler/select.d.cts +15 -0
- package/dist/cjs/query/index.d.cts +6 -5
- package/dist/cjs/query/ir.cjs +57 -0
- package/dist/cjs/query/ir.cjs.map +1 -0
- package/dist/cjs/query/ir.d.cts +81 -0
- package/dist/cjs/query/live-query-collection.cjs +224 -0
- package/dist/cjs/query/live-query-collection.cjs.map +1 -0
- package/dist/cjs/query/live-query-collection.d.cts +124 -0
- package/dist/cjs/transactions.cjs +20 -13
- package/dist/cjs/transactions.cjs.map +1 -1
- package/dist/cjs/transactions.d.cts +10 -1
- package/dist/cjs/types.d.cts +13 -0
- package/dist/esm/collection.d.ts +18 -21
- package/dist/esm/collection.js +118 -105
- package/dist/esm/collection.js.map +1 -1
- package/dist/esm/index.d.ts +0 -1
- package/dist/esm/index.js +34 -12
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/query/builder/functions.d.ts +38 -0
- package/dist/esm/query/builder/functions.js +107 -0
- package/dist/esm/query/builder/functions.js.map +1 -0
- package/dist/esm/query/builder/index.d.ts +324 -0
- package/dist/esm/query/builder/index.js +499 -0
- package/dist/esm/query/builder/index.js.map +1 -0
- package/dist/esm/query/builder/ref-proxy.d.ts +28 -0
- package/dist/esm/query/builder/ref-proxy.js +96 -0
- package/dist/esm/query/builder/ref-proxy.js.map +1 -0
- package/dist/esm/query/builder/types.d.ts +80 -0
- package/dist/esm/query/compiler/evaluators.d.ts +11 -0
- package/dist/esm/query/compiler/evaluators.js +261 -0
- package/dist/esm/query/compiler/evaluators.js.map +1 -0
- package/dist/esm/query/compiler/group-by.d.ts +7 -0
- package/dist/esm/query/compiler/group-by.js +271 -0
- package/dist/esm/query/compiler/group-by.js.map +1 -0
- package/dist/esm/query/compiler/index.d.ts +15 -0
- package/dist/esm/query/compiler/index.js +181 -0
- package/dist/esm/query/compiler/index.js.map +1 -0
- package/dist/esm/query/compiler/joins.d.ts +11 -0
- package/dist/esm/query/compiler/joins.js +116 -0
- package/dist/esm/query/compiler/joins.js.map +1 -0
- package/dist/esm/query/compiler/order-by.d.ts +9 -0
- package/dist/esm/query/compiler/order-by.js +89 -0
- package/dist/esm/query/compiler/order-by.js.map +1 -0
- package/dist/esm/query/compiler/select.d.ts +15 -0
- package/dist/esm/query/compiler/select.js +57 -0
- package/dist/esm/query/compiler/select.js.map +1 -0
- package/dist/esm/query/index.d.ts +6 -5
- package/dist/esm/query/ir.d.ts +81 -0
- package/dist/esm/query/ir.js +57 -0
- package/dist/esm/query/ir.js.map +1 -0
- package/dist/esm/query/live-query-collection.d.ts +124 -0
- package/dist/esm/query/live-query-collection.js +224 -0
- package/dist/esm/query/live-query-collection.js.map +1 -0
- package/dist/esm/transactions.d.ts +10 -1
- package/dist/esm/transactions.js +20 -13
- package/dist/esm/transactions.js.map +1 -1
- package/dist/esm/types.d.ts +13 -0
- package/package.json +3 -4
- package/src/collection.ts +152 -129
- package/src/index.ts +0 -1
- package/src/query/builder/functions.ts +267 -0
- package/src/query/builder/index.ts +648 -0
- package/src/query/builder/ref-proxy.ts +156 -0
- package/src/query/builder/types.ts +278 -0
- package/src/query/compiler/evaluators.ts +315 -0
- package/src/query/compiler/group-by.ts +428 -0
- package/src/query/compiler/index.ts +276 -0
- package/src/query/compiler/joins.ts +228 -0
- package/src/query/compiler/order-by.ts +139 -0
- package/src/query/compiler/select.ts +173 -0
- package/src/query/index.ts +64 -5
- package/src/query/ir.ts +128 -0
- package/src/query/live-query-collection.ts +509 -0
- package/src/transactions.ts +27 -16
- package/src/types.ts +15 -0
- package/dist/cjs/query/compiled-query.cjs +0 -160
- package/dist/cjs/query/compiled-query.cjs.map +0 -1
- package/dist/cjs/query/compiled-query.d.cts +0 -20
- package/dist/cjs/query/evaluators.cjs +0 -161
- package/dist/cjs/query/evaluators.cjs.map +0 -1
- package/dist/cjs/query/evaluators.d.cts +0 -14
- package/dist/cjs/query/extractors.cjs +0 -122
- package/dist/cjs/query/extractors.cjs.map +0 -1
- package/dist/cjs/query/extractors.d.cts +0 -22
- package/dist/cjs/query/functions.cjs +0 -152
- package/dist/cjs/query/functions.cjs.map +0 -1
- package/dist/cjs/query/functions.d.cts +0 -21
- package/dist/cjs/query/group-by.cjs +0 -88
- package/dist/cjs/query/group-by.cjs.map +0 -1
- package/dist/cjs/query/group-by.d.cts +0 -40
- package/dist/cjs/query/joins.cjs +0 -141
- package/dist/cjs/query/joins.cjs.map +0 -1
- package/dist/cjs/query/joins.d.cts +0 -14
- package/dist/cjs/query/order-by.cjs +0 -185
- package/dist/cjs/query/order-by.cjs.map +0 -1
- package/dist/cjs/query/order-by.d.cts +0 -3
- package/dist/cjs/query/pipeline-compiler.cjs +0 -89
- package/dist/cjs/query/pipeline-compiler.cjs.map +0 -1
- package/dist/cjs/query/pipeline-compiler.d.cts +0 -10
- package/dist/cjs/query/query-builder.cjs +0 -307
- package/dist/cjs/query/query-builder.cjs.map +0 -1
- package/dist/cjs/query/query-builder.d.cts +0 -225
- package/dist/cjs/query/schema.d.cts +0 -100
- package/dist/cjs/query/select.cjs +0 -130
- package/dist/cjs/query/select.cjs.map +0 -1
- package/dist/cjs/query/select.d.cts +0 -3
- package/dist/cjs/query/types.d.cts +0 -189
- package/dist/cjs/query/utils.cjs +0 -154
- package/dist/cjs/query/utils.cjs.map +0 -1
- package/dist/cjs/query/utils.d.cts +0 -37
- package/dist/cjs/utils.cjs +0 -17
- package/dist/cjs/utils.cjs.map +0 -1
- package/dist/cjs/utils.d.cts +0 -3
- package/dist/esm/query/compiled-query.d.ts +0 -20
- package/dist/esm/query/compiled-query.js +0 -160
- package/dist/esm/query/compiled-query.js.map +0 -1
- package/dist/esm/query/evaluators.d.ts +0 -14
- package/dist/esm/query/evaluators.js +0 -161
- package/dist/esm/query/evaluators.js.map +0 -1
- package/dist/esm/query/extractors.d.ts +0 -22
- package/dist/esm/query/extractors.js +0 -122
- package/dist/esm/query/extractors.js.map +0 -1
- package/dist/esm/query/functions.d.ts +0 -21
- package/dist/esm/query/functions.js +0 -152
- package/dist/esm/query/functions.js.map +0 -1
- package/dist/esm/query/group-by.d.ts +0 -40
- package/dist/esm/query/group-by.js +0 -88
- package/dist/esm/query/group-by.js.map +0 -1
- package/dist/esm/query/joins.d.ts +0 -14
- package/dist/esm/query/joins.js +0 -141
- package/dist/esm/query/joins.js.map +0 -1
- package/dist/esm/query/order-by.d.ts +0 -3
- package/dist/esm/query/order-by.js +0 -185
- package/dist/esm/query/order-by.js.map +0 -1
- package/dist/esm/query/pipeline-compiler.d.ts +0 -10
- package/dist/esm/query/pipeline-compiler.js +0 -89
- package/dist/esm/query/pipeline-compiler.js.map +0 -1
- package/dist/esm/query/query-builder.d.ts +0 -225
- package/dist/esm/query/query-builder.js +0 -307
- package/dist/esm/query/query-builder.js.map +0 -1
- package/dist/esm/query/schema.d.ts +0 -100
- package/dist/esm/query/select.d.ts +0 -3
- package/dist/esm/query/select.js +0 -130
- package/dist/esm/query/select.js.map +0 -1
- package/dist/esm/query/types.d.ts +0 -189
- package/dist/esm/query/utils.d.ts +0 -37
- package/dist/esm/query/utils.js +0 -154
- package/dist/esm/query/utils.js.map +0 -1
- package/dist/esm/utils.d.ts +0 -3
- package/dist/esm/utils.js +0 -17
- package/dist/esm/utils.js.map +0 -1
- package/src/query/compiled-query.ts +0 -234
- package/src/query/evaluators.ts +0 -250
- package/src/query/extractors.ts +0 -214
- package/src/query/functions.ts +0 -297
- package/src/query/group-by.ts +0 -139
- package/src/query/joins.ts +0 -260
- package/src/query/order-by.ts +0 -264
- package/src/query/pipeline-compiler.ts +0 -149
- package/src/query/query-builder.ts +0 -902
- package/src/query/schema.ts +0 -268
- package/src/query/select.ts +0 -208
- package/src/query/types.ts +0 -418
- package/src/query/utils.ts +0 -245
- 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"
|