@tanstack/db 0.1.12 → 0.2.0
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/errors.cjs +18 -6
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +9 -3
- package/dist/cjs/index.cjs +3 -1
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.cjs +4 -1
- package/dist/cjs/query/builder/functions.cjs.map +1 -1
- package/dist/cjs/query/builder/functions.d.cts +38 -21
- package/dist/cjs/query/builder/index.cjs +25 -16
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/builder/index.d.cts +8 -8
- package/dist/cjs/query/builder/ref-proxy.cjs +12 -8
- package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
- package/dist/cjs/query/builder/ref-proxy.d.cts +2 -1
- package/dist/cjs/query/builder/types.d.cts +493 -28
- package/dist/cjs/query/compiler/evaluators.cjs +29 -0
- package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
- package/dist/cjs/query/compiler/group-by.cjs +4 -2
- package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
- package/dist/cjs/query/compiler/index.cjs +13 -4
- package/dist/cjs/query/compiler/index.cjs.map +1 -1
- package/dist/cjs/query/compiler/joins.cjs +70 -60
- package/dist/cjs/query/compiler/joins.cjs.map +1 -1
- package/dist/cjs/query/compiler/select.cjs +131 -42
- package/dist/cjs/query/compiler/select.cjs.map +1 -1
- package/dist/cjs/query/compiler/select.d.cts +1 -5
- package/dist/cjs/query/ir.cjs +4 -0
- package/dist/cjs/query/ir.cjs.map +1 -1
- package/dist/cjs/query/ir.d.cts +6 -1
- package/dist/cjs/query/optimizer.cjs +61 -20
- package/dist/cjs/query/optimizer.cjs.map +1 -1
- package/dist/esm/errors.d.ts +9 -3
- package/dist/esm/errors.js +18 -6
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.js +4 -2
- package/dist/esm/query/builder/functions.d.ts +38 -21
- package/dist/esm/query/builder/functions.js +4 -1
- package/dist/esm/query/builder/functions.js.map +1 -1
- package/dist/esm/query/builder/index.d.ts +8 -8
- package/dist/esm/query/builder/index.js +27 -18
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/builder/ref-proxy.d.ts +2 -1
- package/dist/esm/query/builder/ref-proxy.js +12 -8
- package/dist/esm/query/builder/ref-proxy.js.map +1 -1
- package/dist/esm/query/builder/types.d.ts +493 -28
- package/dist/esm/query/compiler/evaluators.js +29 -0
- package/dist/esm/query/compiler/evaluators.js.map +1 -1
- package/dist/esm/query/compiler/group-by.js +4 -2
- package/dist/esm/query/compiler/group-by.js.map +1 -1
- package/dist/esm/query/compiler/index.js +15 -6
- package/dist/esm/query/compiler/index.js.map +1 -1
- package/dist/esm/query/compiler/joins.js +71 -61
- package/dist/esm/query/compiler/joins.js.map +1 -1
- package/dist/esm/query/compiler/select.d.ts +1 -5
- package/dist/esm/query/compiler/select.js +131 -42
- package/dist/esm/query/compiler/select.js.map +1 -1
- package/dist/esm/query/ir.d.ts +6 -1
- package/dist/esm/query/ir.js +4 -0
- package/dist/esm/query/ir.js.map +1 -1
- package/dist/esm/query/optimizer.js +62 -21
- package/dist/esm/query/optimizer.js.map +1 -1
- package/package.json +2 -2
- package/src/errors.ts +17 -10
- package/src/query/builder/functions.ts +176 -108
- package/src/query/builder/index.ts +68 -48
- package/src/query/builder/ref-proxy.ts +14 -20
- package/src/query/builder/types.ts +622 -110
- package/src/query/compiler/evaluators.ts +30 -0
- package/src/query/compiler/group-by.ts +6 -1
- package/src/query/compiler/index.ts +23 -6
- package/src/query/compiler/joins.ts +132 -101
- package/src/query/compiler/select.ts +206 -113
- package/src/query/ir.ts +14 -1
- package/src/query/optimizer.ts +131 -59
|
@@ -1,7 +1,29 @@
|
|
|
1
1
|
import { CollectionImpl } from '../../collection.js';
|
|
2
|
-
import { Aggregate, BasicExpression, OrderByDirection } from '../ir.js';
|
|
2
|
+
import { Aggregate, BasicExpression, Func, OrderByDirection, PropRef, Value } from '../ir.js';
|
|
3
3
|
import { QueryBuilder } from './index.js';
|
|
4
4
|
import { ResolveType } from '../../types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Context - The central state container for query builder operations
|
|
7
|
+
*
|
|
8
|
+
* This interface tracks all the information needed to build and type-check queries:
|
|
9
|
+
*
|
|
10
|
+
* **Schema Management**:
|
|
11
|
+
* - `baseSchema`: The original tables/collections from the `from()` clause
|
|
12
|
+
* - `schema`: Current available tables (expands with joins, contracts with subqueries)
|
|
13
|
+
*
|
|
14
|
+
* **Query State**:
|
|
15
|
+
* - `fromSourceName`: Which table was used in `from()` - needed for optionality logic
|
|
16
|
+
* - `hasJoins`: Whether any joins have been added (affects result type inference)
|
|
17
|
+
* - `joinTypes`: Maps table aliases to their join types for optionality calculations
|
|
18
|
+
*
|
|
19
|
+
* **Result Tracking**:
|
|
20
|
+
* - `result`: The final shape after `select()` - undefined until select is called
|
|
21
|
+
*
|
|
22
|
+
* The context evolves through the query builder chain:
|
|
23
|
+
* 1. `from()` sets baseSchema and schema to the same thing
|
|
24
|
+
* 2. `join()` expands schema and sets hasJoins/joinTypes
|
|
25
|
+
* 3. `select()` sets result to the projected shape
|
|
26
|
+
*/
|
|
5
27
|
export interface Context {
|
|
6
28
|
baseSchema: ContextSchema;
|
|
7
29
|
schema: ContextSchema;
|
|
@@ -10,27 +32,194 @@ export interface Context {
|
|
|
10
32
|
joinTypes?: Record<string, `inner` | `left` | `right` | `full` | `outer` | `cross`>;
|
|
11
33
|
result?: any;
|
|
12
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* ContextSchema - The shape of available tables/collections in a query context
|
|
37
|
+
*
|
|
38
|
+
* This is simply a record mapping table aliases to their TypeScript types.
|
|
39
|
+
* It evolves as the query progresses:
|
|
40
|
+
* - Initial: Just the `from()` table
|
|
41
|
+
* - After joins: Includes all joined tables with proper optionality
|
|
42
|
+
* - In subqueries: May be a subset of the outer query's schema
|
|
43
|
+
*/
|
|
13
44
|
export type ContextSchema = Record<string, unknown>;
|
|
45
|
+
/**
|
|
46
|
+
* Source - Input definition for query builder `from()` clause
|
|
47
|
+
*
|
|
48
|
+
* Maps table aliases to either:
|
|
49
|
+
* - `CollectionImpl`: A database collection/table
|
|
50
|
+
* - `QueryBuilder`: A subquery that can be used as a table
|
|
51
|
+
*
|
|
52
|
+
* Example: `{ users: usersCollection, orders: ordersCollection }`
|
|
53
|
+
*/
|
|
14
54
|
export type Source = {
|
|
15
55
|
[alias: string]: CollectionImpl<any, any> | QueryBuilder<Context>;
|
|
16
56
|
};
|
|
57
|
+
/**
|
|
58
|
+
* InferCollectionType - Extracts the TypeScript type from a CollectionImpl
|
|
59
|
+
*
|
|
60
|
+
* This helper ensures we get the same type that would be used when creating
|
|
61
|
+
* the collection itself. It uses the internal `ResolveType` logic to maintain
|
|
62
|
+
* consistency between collection creation and query type inference.
|
|
63
|
+
*
|
|
64
|
+
* The complex generic parameters extract:
|
|
65
|
+
* - U: The base document type
|
|
66
|
+
* - TSchema: The schema definition
|
|
67
|
+
* - The resolved type combines these with any transforms
|
|
68
|
+
*/
|
|
17
69
|
export type InferCollectionType<T> = T extends CollectionImpl<infer U, any, any, infer TSchema, any> ? ResolveType<U, TSchema, U> : never;
|
|
70
|
+
/**
|
|
71
|
+
* SchemaFromSource - Converts a Source definition into a ContextSchema
|
|
72
|
+
*
|
|
73
|
+
* This transforms the input to `from()` into the schema format used throughout
|
|
74
|
+
* the query builder. For each alias in the source:
|
|
75
|
+
* - Collections → their inferred TypeScript type
|
|
76
|
+
* - Subqueries → their result type (what they would return if executed)
|
|
77
|
+
*
|
|
78
|
+
* The `Prettify` wrapper ensures clean type display in IDEs.
|
|
79
|
+
*/
|
|
18
80
|
export type SchemaFromSource<T extends Source> = Prettify<{
|
|
19
81
|
[K in keyof T]: T[K] extends CollectionImpl<any, any, any, any, any> ? InferCollectionType<T[K]> : T[K] extends QueryBuilder<infer TContext> ? GetResult<TContext> : never;
|
|
20
82
|
}>;
|
|
83
|
+
/**
|
|
84
|
+
* GetAliases - Extracts all table aliases available in a query context
|
|
85
|
+
*
|
|
86
|
+
* Simple utility type that returns the keys of the schema, representing
|
|
87
|
+
* all table/collection aliases that can be referenced in the current query.
|
|
88
|
+
*/
|
|
21
89
|
export type GetAliases<TContext extends Context> = keyof TContext[`schema`];
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
90
|
+
/**
|
|
91
|
+
* WhereCallback - Type for where/having clause callback functions
|
|
92
|
+
*
|
|
93
|
+
* These callbacks receive a `refs` object containing RefProxy instances for
|
|
94
|
+
* all available tables. The callback should return a boolean expression
|
|
95
|
+
* that will be used to filter query results.
|
|
96
|
+
*
|
|
97
|
+
* Example: `(refs) => eq(refs.users.age, 25)`
|
|
98
|
+
*/
|
|
99
|
+
export type WhereCallback<TContext extends Context> = (refs: RefsForContext<TContext>) => any;
|
|
100
|
+
/**
|
|
101
|
+
* SelectValue - Union of all valid values in a select clause
|
|
102
|
+
*
|
|
103
|
+
* This type defines what can be used as values in the object passed to `select()`.
|
|
104
|
+
*
|
|
105
|
+
* **Core Expression Types**:
|
|
106
|
+
* - `BasicExpression`: Function calls like `upper(users.name)`
|
|
107
|
+
* - `Aggregate`: Aggregations like `count()`, `avg()`
|
|
108
|
+
* - `RefProxy/Ref`: Direct field references like `users.name`
|
|
109
|
+
*
|
|
110
|
+
* **JavaScript Literals** (for constant values in projections):
|
|
111
|
+
* - `string`: String literals like `'active'`, `'N/A'`
|
|
112
|
+
* - `number`: Numeric literals like `0`, `42`, `3.14`
|
|
113
|
+
* - `boolean`: Boolean literals `true`, `false`
|
|
114
|
+
* - `null`: Explicit null values
|
|
115
|
+
*
|
|
116
|
+
* **Advanced Features**:
|
|
117
|
+
* - `undefined`: Allows optional projection values
|
|
118
|
+
* - `{ [key: string]: SelectValue }`: Nested object projection
|
|
119
|
+
*
|
|
120
|
+
* The clean Ref type ensures no internal properties are visible to users.
|
|
121
|
+
*
|
|
122
|
+
* Examples:
|
|
123
|
+
* ```typescript
|
|
124
|
+
* select({
|
|
125
|
+
* id: users.id,
|
|
126
|
+
* name: users.name,
|
|
127
|
+
* status: 'active', // string literal
|
|
128
|
+
* priority: 1, // number literal
|
|
129
|
+
* verified: true, // boolean literal
|
|
130
|
+
* notes: null, // explicit null
|
|
131
|
+
* profile: {
|
|
132
|
+
* name: users.name,
|
|
133
|
+
* email: users.email
|
|
134
|
+
* }
|
|
135
|
+
* })
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
type SelectValue = BasicExpression | Aggregate | Ref | RefLeaf<any> | string | number | boolean | null | undefined | {
|
|
139
|
+
[key: string]: SelectValue;
|
|
140
|
+
} | Array<RefLeaf<any>>;
|
|
141
|
+
type SelectShape = {
|
|
142
|
+
[key: string]: SelectValue | SelectShape;
|
|
28
143
|
};
|
|
29
|
-
|
|
144
|
+
/**
|
|
145
|
+
* SelectObject - Wrapper type for select clause objects
|
|
146
|
+
*
|
|
147
|
+
* This ensures that objects passed to `select()` have valid SelectValue types
|
|
148
|
+
* for all their properties. It's a simple wrapper that provides better error
|
|
149
|
+
* messages when invalid selections are attempted.
|
|
150
|
+
*/
|
|
151
|
+
export type SelectObject<T extends SelectShape = SelectShape> = T;
|
|
152
|
+
/**
|
|
153
|
+
* ResultTypeFromSelect - Infers the result type from a select object
|
|
154
|
+
*
|
|
155
|
+
* This complex type transforms the input to `select()` into the actual TypeScript
|
|
156
|
+
* type that the query will return. It handles all the different kinds of values
|
|
157
|
+
* that can appear in a select clause:
|
|
158
|
+
*
|
|
159
|
+
* **Ref/RefProxy Extraction**:
|
|
160
|
+
* - `RefProxy<T>` → `T`: Extracts the underlying type
|
|
161
|
+
* - `Ref<T> | undefined` → `T | undefined`: Preserves optionality
|
|
162
|
+
* - `Ref<T> | null` → `T | null`: Preserves nullability
|
|
163
|
+
*
|
|
164
|
+
* **Expression Types**:
|
|
165
|
+
* - `BasicExpression<T>` → `T`: Function results like `upper()` → `string`
|
|
166
|
+
* - `Aggregate<T>` → `T`: Aggregation results like `count()` → `number`
|
|
167
|
+
*
|
|
168
|
+
* **JavaScript Literals** (pass through as-is):
|
|
169
|
+
* - `string` → `string`: String literals remain strings
|
|
170
|
+
* - `number` → `number`: Numeric literals remain numbers
|
|
171
|
+
* - `boolean` → `boolean`: Boolean literals remain booleans
|
|
172
|
+
* - `null` → `null`: Explicit null remains null
|
|
173
|
+
* - `undefined` → `undefined`: Direct undefined values
|
|
174
|
+
*
|
|
175
|
+
* **Nested Objects** (recursive):
|
|
176
|
+
* - Plain objects are recursively processed to handle nested projections
|
|
177
|
+
* - RefProxy objects are detected and their types extracted
|
|
178
|
+
*
|
|
179
|
+
* Example transformation:
|
|
180
|
+
* ```typescript
|
|
181
|
+
* // Input:
|
|
182
|
+
* { id: Ref<number>, name: Ref<string>, status: 'active', count: 42, profile: { bio: Ref<string> } }
|
|
183
|
+
*
|
|
184
|
+
* // Output:
|
|
185
|
+
* { id: number, name: string, status: 'active', count: 42, profile: { bio: string } }
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
export type ResultTypeFromSelect<TSelectObject> = WithoutRefBrand<Prettify<{
|
|
189
|
+
[K in keyof TSelectObject]: NeedsExtraction<TSelectObject[K]> extends true ? ExtractExpressionType<TSelectObject[K]> : TSelectObject[K] extends Ref<infer _T> ? ExtractRef<TSelectObject[K]> : TSelectObject[K] extends RefLeaf<infer T> ? T : TSelectObject[K] extends RefLeaf<infer T> | undefined ? T | undefined : TSelectObject[K] extends RefLeaf<infer T> | null ? T | null : TSelectObject[K] extends Ref<infer _T> | undefined ? ExtractRef<TSelectObject[K]> | undefined : TSelectObject[K] extends Ref<infer _T> | null ? ExtractRef<TSelectObject[K]> | null : TSelectObject[K] extends Aggregate<infer T> ? T : TSelectObject[K] extends string | number | boolean | null | undefined ? TSelectObject[K] : TSelectObject[K] extends Record<string, any> ? ResultTypeFromSelect<TSelectObject[K]> : never;
|
|
190
|
+
}>>;
|
|
191
|
+
type ExtractRef<T> = Prettify<ResultTypeFromSelect<WithoutRefBrand<T>>>;
|
|
192
|
+
type ExtractExpressionType<T> = T extends PropRef<infer U> ? U : T extends Value<infer U> ? U : T extends Func<infer U> ? U : T extends Aggregate<infer U> ? U : T extends BasicExpression<infer U> ? U : T;
|
|
193
|
+
type NeedsExtraction<T> = T extends PropRef<any> | Value<any> | Func<any> | Aggregate<any> | BasicExpression<any> ? true : false;
|
|
194
|
+
/**
|
|
195
|
+
* OrderByCallback - Type for orderBy clause callback functions
|
|
196
|
+
*
|
|
197
|
+
* Similar to WhereCallback, these receive refs for all available tables
|
|
198
|
+
* and should return expressions that will be used for sorting.
|
|
199
|
+
*
|
|
200
|
+
* Example: `(refs) => refs.users.createdAt`
|
|
201
|
+
*/
|
|
202
|
+
export type OrderByCallback<TContext extends Context> = (refs: RefsForContext<TContext>) => any;
|
|
203
|
+
/**
|
|
204
|
+
* OrderByOptions - Configuration for orderBy operations
|
|
205
|
+
*
|
|
206
|
+
* Combines direction and null handling with string-specific sorting options.
|
|
207
|
+
* The intersection with StringSortOpts allows for either simple lexical sorting
|
|
208
|
+
* or locale-aware sorting with customizable options.
|
|
209
|
+
*/
|
|
30
210
|
export type OrderByOptions = {
|
|
31
211
|
direction?: OrderByDirection;
|
|
32
212
|
nulls?: `first` | `last`;
|
|
33
213
|
} & StringSortOpts;
|
|
214
|
+
/**
|
|
215
|
+
* StringSortOpts - Options for string sorting behavior
|
|
216
|
+
*
|
|
217
|
+
* This discriminated union allows for two types of string sorting:
|
|
218
|
+
* - **Lexical**: Simple character-by-character comparison (default)
|
|
219
|
+
* - **Locale**: Locale-aware sorting with optional customization
|
|
220
|
+
*
|
|
221
|
+
* The union ensures that locale options are only available when locale sorting is selected.
|
|
222
|
+
*/
|
|
34
223
|
export type StringSortOpts = {
|
|
35
224
|
stringSort?: `lexical`;
|
|
36
225
|
} | {
|
|
@@ -38,6 +227,13 @@ export type StringSortOpts = {
|
|
|
38
227
|
locale?: string;
|
|
39
228
|
localeOptions?: object;
|
|
40
229
|
};
|
|
230
|
+
/**
|
|
231
|
+
* CompareOptions - Final resolved options for comparison operations
|
|
232
|
+
*
|
|
233
|
+
* This is the internal type used after all orderBy options have been resolved
|
|
234
|
+
* to their concrete values. Unlike OrderByOptions, all fields are required
|
|
235
|
+
* since defaults have been applied.
|
|
236
|
+
*/
|
|
41
237
|
export type CompareOptions = {
|
|
42
238
|
direction: OrderByDirection;
|
|
43
239
|
nulls: `first` | `last`;
|
|
@@ -45,29 +241,150 @@ export type CompareOptions = {
|
|
|
45
241
|
locale?: string;
|
|
46
242
|
localeOptions?: object;
|
|
47
243
|
};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
244
|
+
/**
|
|
245
|
+
* GroupByCallback - Type for groupBy clause callback functions
|
|
246
|
+
*
|
|
247
|
+
* These callbacks receive refs for all available tables and should return
|
|
248
|
+
* expressions that will be used for grouping query results.
|
|
249
|
+
*
|
|
250
|
+
* Example: `(refs) => refs.orders.status`
|
|
251
|
+
*/
|
|
252
|
+
export type GroupByCallback<TContext extends Context> = (refs: RefsForContext<TContext>) => any;
|
|
253
|
+
/**
|
|
254
|
+
* JoinOnCallback - Type for join condition callback functions
|
|
255
|
+
*
|
|
256
|
+
* These callbacks receive refs for all available tables (including the newly
|
|
257
|
+
* joined table) and should return a boolean expression defining the join condition.
|
|
258
|
+
*
|
|
259
|
+
* Important: The newly joined table is NOT marked as optional in this callback,
|
|
260
|
+
* even for left/right/full joins, because optionality is applied AFTER the join
|
|
261
|
+
* condition is evaluated.
|
|
262
|
+
*
|
|
263
|
+
* Example: `(refs) => eq(refs.users.id, refs.orders.userId)`
|
|
264
|
+
*/
|
|
265
|
+
export type JoinOnCallback<TContext extends Context> = (refs: RefsForContext<TContext>) => any;
|
|
266
|
+
/**
|
|
267
|
+
* RefProxyForContext - Creates ref proxies for all tables/collections in a query context
|
|
268
|
+
*
|
|
269
|
+
* This is the main entry point for creating ref objects in query builder callbacks.
|
|
270
|
+
* It handles optionality by placing undefined/null OUTSIDE the RefProxy to enable
|
|
271
|
+
* JavaScript's optional chaining operator (?.):
|
|
272
|
+
*
|
|
273
|
+
* Examples:
|
|
274
|
+
* - Required field: `RefProxy<User>` → user.name works
|
|
275
|
+
* - Optional field: `RefProxy<User> | undefined` → user?.name works
|
|
276
|
+
* - Nullable field: `RefProxy<User> | null` → user?.name works
|
|
277
|
+
* - Both optional and nullable: `RefProxy<User> | undefined` → user?.name works
|
|
278
|
+
*
|
|
279
|
+
* The key insight is that `RefProxy<User | undefined>` would NOT allow `user?.name`
|
|
280
|
+
* because the undefined is "inside" the proxy, but `RefProxy<User> | undefined`
|
|
281
|
+
* does allow it because the undefined is "outside" the proxy.
|
|
282
|
+
*
|
|
283
|
+
* The logic prioritizes optional chaining by always placing `undefined` outside when
|
|
284
|
+
* a type is both optional and nullable (e.g., `string | null | undefined`).
|
|
285
|
+
*/
|
|
286
|
+
export type RefsForContext<TContext extends Context> = {
|
|
287
|
+
[K in keyof TContext[`schema`]]: IsNonExactOptional<TContext[`schema`][K]> extends true ? IsNonExactNullable<TContext[`schema`][K]> extends true ? // T is both non-exact optional and non-exact nullable (e.g., string | null | undefined)
|
|
288
|
+
Ref<NonNullable<TContext[`schema`][K]>> | undefined : // T is optional (T | undefined) but not exactly undefined, and not nullable
|
|
289
|
+
Ref<NonUndefined<TContext[`schema`][K]>> | undefined : IsNonExactNullable<TContext[`schema`][K]> extends true ? // T is nullable (T | null) but not exactly null, and not optional
|
|
290
|
+
Ref<NonNull<TContext[`schema`][K]>> | null : Ref<TContext[`schema`][K]>;
|
|
52
291
|
};
|
|
292
|
+
/**
|
|
293
|
+
* Type Detection Helpers
|
|
294
|
+
*
|
|
295
|
+
* These helpers distinguish between different kinds of optionality/nullability:
|
|
296
|
+
* - IsExactlyUndefined: T is literally `undefined` (not `string | undefined`)
|
|
297
|
+
* - IsOptional: T includes undefined (like `string | undefined`)
|
|
298
|
+
* - IsExactlyNull: T is literally `null` (not `string | null`)
|
|
299
|
+
* - IsNullable: T includes null (like `string | null`)
|
|
300
|
+
* - IsNonExactOptional: T includes undefined but is not exactly undefined
|
|
301
|
+
* - IsNonExactNullable: T includes null but is not exactly null
|
|
302
|
+
*
|
|
303
|
+
* The [T] extends [undefined] pattern prevents distributive conditional types,
|
|
304
|
+
* ensuring we check the exact type rather than distributing over union members.
|
|
305
|
+
*/
|
|
53
306
|
type IsExactlyUndefined<T> = [T] extends [undefined] ? true : false;
|
|
307
|
+
type IsExactlyNull<T> = [T] extends [null] ? true : false;
|
|
54
308
|
type IsOptional<T> = undefined extends T ? true : false;
|
|
309
|
+
type IsNullable<T> = null extends T ? true : false;
|
|
310
|
+
type IsNonExactOptional<T> = IsOptional<T> extends true ? IsExactlyUndefined<T> extends false ? true : false : false;
|
|
311
|
+
type IsNonExactNullable<T> = IsNullable<T> extends true ? IsExactlyNull<T> extends false ? true : false : false;
|
|
312
|
+
/**
|
|
313
|
+
* Type Extraction Helpers
|
|
314
|
+
*
|
|
315
|
+
* These helpers extract the "useful" part of a type by removing null/undefined:
|
|
316
|
+
* - NonUndefined: `string | undefined` → `string` (preserves null if present)
|
|
317
|
+
* - NonNull: `string | null` → `string` (preserves undefined if present)
|
|
318
|
+
*
|
|
319
|
+
* These are used when we need to handle optional and nullable types separately.
|
|
320
|
+
* For cases where both null and undefined should be removed, use TypeScript's
|
|
321
|
+
* built-in NonNullable<T> instead.
|
|
322
|
+
*/
|
|
55
323
|
type NonUndefined<T> = T extends undefined ? never : T;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
324
|
+
type NonNull<T> = T extends null ? never : T;
|
|
325
|
+
/**
|
|
326
|
+
* Ref - The user-facing ref interface for the query builder
|
|
327
|
+
*
|
|
328
|
+
* This is a clean type that represents a reference to a value in the query,
|
|
329
|
+
* designed for optimal IDE experience without internal implementation details.
|
|
330
|
+
* It provides a recursive interface that allows nested property access while
|
|
331
|
+
* preserving optionality and nullability correctly.
|
|
332
|
+
*
|
|
333
|
+
* When spread in select clauses, it correctly produces the underlying data type
|
|
334
|
+
* without Ref wrappers, enabling clean spread operations.
|
|
335
|
+
*
|
|
336
|
+
* Example usage:
|
|
337
|
+
* ```typescript
|
|
338
|
+
* // Clean interface - no internal properties visible
|
|
339
|
+
* const users: Ref<{ id: number; profile?: { bio: string } }> = { ... }
|
|
340
|
+
* users.id // Ref<number> - clean display
|
|
341
|
+
* users.profile?.bio // Ref<string> - nested optional access works
|
|
342
|
+
*
|
|
343
|
+
* // Spread operations work cleanly:
|
|
344
|
+
* select(({ user }) => ({ ...user })) // Returns User type, not Ref types
|
|
345
|
+
* ```
|
|
346
|
+
*/
|
|
347
|
+
export type Ref<T = any> = {
|
|
348
|
+
[K in keyof T]: IsNonExactOptional<T[K]> extends true ? IsNonExactNullable<T[K]> extends true ? NonNullable<T[K]> extends Record<string, any> ? Ref<NonNullable<T[K]>> | undefined : RefLeaf<NonNullable<T[K]>> | undefined : NonUndefined<T[K]> extends Record<string, any> ? Ref<NonUndefined<T[K]>> | undefined : RefLeaf<NonUndefined<T[K]>> | undefined : IsNonExactNullable<T[K]> extends true ? NonNull<T[K]> extends Record<string, any> ? Ref<NonNull<T[K]>> | null : RefLeaf<NonNull<T[K]>> | null : T[K] extends Record<string, any> ? Ref<T[K]> : RefLeaf<T[K]>;
|
|
349
|
+
} & RefLeaf<T>;
|
|
350
|
+
/**
|
|
351
|
+
* Ref - The user-facing ref type with clean IDE display
|
|
352
|
+
*
|
|
353
|
+
* An opaque branded type that represents a reference to a value in a query.
|
|
354
|
+
* This shows as `Ref<T>` in the IDE without exposing internal structure.
|
|
355
|
+
*
|
|
356
|
+
* Example usage:
|
|
357
|
+
* - Ref<number> displays as `Ref<number>` in IDE
|
|
358
|
+
* - Ref<string> displays as `Ref<string>` in IDE
|
|
359
|
+
* - No internal properties like __refProxy, __path, __type are visible
|
|
360
|
+
*/
|
|
361
|
+
declare const RefBrand: unique symbol;
|
|
362
|
+
export type RefLeaf<T = any> = {
|
|
363
|
+
readonly [RefBrand]?: T;
|
|
364
|
+
};
|
|
365
|
+
type WithoutRefBrand<T> = T extends Record<string, any> ? Omit<T, typeof RefBrand> : T;
|
|
366
|
+
/**
|
|
367
|
+
* MergeContextWithJoinType - Creates a new context after a join operation
|
|
368
|
+
*
|
|
369
|
+
* This is the core type that handles the complex logic of merging schemas
|
|
370
|
+
* when tables are joined, applying the correct optionality based on join type.
|
|
371
|
+
*
|
|
372
|
+
* **Key Responsibilities**:
|
|
373
|
+
* 1. **Schema Merging**: Combines existing schema with newly joined tables
|
|
374
|
+
* 2. **Optionality Logic**: Applies join-specific optionality rules:
|
|
375
|
+
* - `LEFT JOIN`: New table becomes optional
|
|
376
|
+
* - `RIGHT JOIN`: Existing tables become optional
|
|
377
|
+
* - `FULL JOIN`: Both existing and new become optional
|
|
378
|
+
* - `INNER JOIN`: No tables become optional
|
|
379
|
+
* 3. **State Tracking**: Updates hasJoins and joinTypes for future operations
|
|
380
|
+
*
|
|
381
|
+
* **Context Evolution**:
|
|
382
|
+
* - `baseSchema`: Unchanged (always the original `from()` tables)
|
|
383
|
+
* - `schema`: Expanded with new tables and proper optionality
|
|
384
|
+
* - `hasJoins`: Set to true
|
|
385
|
+
* - `joinTypes`: Updated to track this join type
|
|
386
|
+
* - `result`: Preserved from previous operations
|
|
387
|
+
*/
|
|
71
388
|
export type MergeContextWithJoinType<TContext extends Context, TNewSchema extends ContextSchema, TJoinType extends `inner` | `left` | `right` | `full` | `outer` | `cross`> = {
|
|
72
389
|
baseSchema: TContext[`baseSchema`];
|
|
73
390
|
schema: ApplyJoinOptionalityToMergedSchema<TContext[`schema`], TNewSchema, TJoinType, TContext[`fromSourceName`]>;
|
|
@@ -78,24 +395,172 @@ export type MergeContextWithJoinType<TContext extends Context, TNewSchema extend
|
|
|
78
395
|
};
|
|
79
396
|
result: TContext[`result`];
|
|
80
397
|
};
|
|
398
|
+
/**
|
|
399
|
+
* ApplyJoinOptionalityToMergedSchema - Applies optionality rules when merging schemas
|
|
400
|
+
*
|
|
401
|
+
* This type implements the SQL join optionality semantics:
|
|
402
|
+
*
|
|
403
|
+
* **For Existing Tables**:
|
|
404
|
+
* - `RIGHT JOIN` or `FULL JOIN`: Main table (from fromSourceName) becomes optional
|
|
405
|
+
* - Other join types: Existing tables keep their current optionality
|
|
406
|
+
* - Previously joined tables: Keep their already-applied optionality
|
|
407
|
+
*
|
|
408
|
+
* **For New Tables**:
|
|
409
|
+
* - `LEFT JOIN` or `FULL JOIN`: New table becomes optional
|
|
410
|
+
* - `INNER JOIN` or `RIGHT JOIN`: New table remains required
|
|
411
|
+
*
|
|
412
|
+
* **Examples**:
|
|
413
|
+
* ```sql
|
|
414
|
+
* FROM users LEFT JOIN orders -- orders becomes optional
|
|
415
|
+
* FROM users RIGHT JOIN orders -- users becomes optional
|
|
416
|
+
* FROM users FULL JOIN orders -- both become optional
|
|
417
|
+
* FROM users INNER JOIN orders -- both remain required
|
|
418
|
+
* ```
|
|
419
|
+
*
|
|
420
|
+
* The intersection (&) ensures both existing and new schemas are merged
|
|
421
|
+
* into a single type while preserving all table references.
|
|
422
|
+
*/
|
|
81
423
|
export type ApplyJoinOptionalityToMergedSchema<TExistingSchema extends ContextSchema, TNewSchema extends ContextSchema, TJoinType extends `inner` | `left` | `right` | `full` | `outer` | `cross`, TFromSourceName extends string> = {
|
|
82
424
|
[K in keyof TExistingSchema]: K extends TFromSourceName ? TJoinType extends `right` | `full` ? TExistingSchema[K] | undefined : TExistingSchema[K] : TExistingSchema[K];
|
|
83
425
|
} & {
|
|
84
426
|
[K in keyof TNewSchema]: TJoinType extends `left` | `full` ? // New table becomes optional for left and full joins
|
|
85
427
|
TNewSchema[K] | undefined : TNewSchema[K];
|
|
86
428
|
};
|
|
429
|
+
/**
|
|
430
|
+
* GetResult - Determines the final result type of a query
|
|
431
|
+
*
|
|
432
|
+
* This type implements the logic for what a query returns based on its current state:
|
|
433
|
+
*
|
|
434
|
+
* **Priority Order**:
|
|
435
|
+
* 1. **Explicit Result**: If `select()` was called, use the projected type
|
|
436
|
+
* 2. **Join Query**: If joins exist, return all tables with proper optionality
|
|
437
|
+
* 3. **Single Table**: Return just the main table from `from()`
|
|
438
|
+
*
|
|
439
|
+
* **Examples**:
|
|
440
|
+
* ```typescript
|
|
441
|
+
* // Single table query:
|
|
442
|
+
* from({ users }).where(...) // → User[]
|
|
443
|
+
*
|
|
444
|
+
* // Join query without select:
|
|
445
|
+
* from({ users }).leftJoin({ orders }, ...) // → { users: User, orders: Order | undefined }[]
|
|
446
|
+
*
|
|
447
|
+
* // Query with select:
|
|
448
|
+
* from({ users }).select({ id: users.id, name: users.name }) // → { id: number, name: string }[]
|
|
449
|
+
* ```
|
|
450
|
+
*
|
|
451
|
+
* The `Prettify` wrapper ensures clean type display in IDEs by flattening
|
|
452
|
+
* complex intersection types into readable object types.
|
|
453
|
+
*/
|
|
87
454
|
export type GetResult<TContext extends Context> = Prettify<TContext[`result`] extends object ? TContext[`result`] : TContext[`hasJoins`] extends true ? TContext[`schema`] : TContext[`schema`][TContext[`fromSourceName`]]>;
|
|
455
|
+
/**
|
|
456
|
+
* ApplyJoinOptionalityToSchema - Legacy helper for complex join scenarios
|
|
457
|
+
*
|
|
458
|
+
* This type was designed to handle complex scenarios with multiple joins
|
|
459
|
+
* where the optionality of tables might be affected by subsequent joins.
|
|
460
|
+
* Currently used in advanced join logic, but most cases are handled by
|
|
461
|
+
* the simpler `ApplyJoinOptionalityToMergedSchema`.
|
|
462
|
+
*
|
|
463
|
+
* **Logic**:
|
|
464
|
+
* 1. **Main Table**: Becomes optional if ANY right or full join exists in the chain
|
|
465
|
+
* 2. **Joined Tables**: Check their specific join type for optionality
|
|
466
|
+
* 3. **Complex Cases**: Handle scenarios where subsequent joins affect earlier tables
|
|
467
|
+
*
|
|
468
|
+
* This is primarily used for edge cases and may be simplified in future versions
|
|
469
|
+
* as the simpler merge-based approach covers most real-world scenarios.
|
|
470
|
+
*/
|
|
88
471
|
export type ApplyJoinOptionalityToSchema<TSchema extends ContextSchema, TJoinTypes extends Record<string, string>, TFromSourceName extends string> = {
|
|
89
472
|
[K in keyof TSchema]: K extends TFromSourceName ? HasJoinType<TJoinTypes, `right` | `full`> extends true ? TSchema[K] | undefined : TSchema[K] : K extends keyof TJoinTypes ? TJoinTypes[K] extends `left` | `full` ? TSchema[K] | undefined : IsTableMadeOptionalBySubsequentJoins<K, TJoinTypes, TFromSourceName> extends true ? TSchema[K] | undefined : TSchema[K] : TSchema[K];
|
|
90
473
|
};
|
|
474
|
+
/**
|
|
475
|
+
* IsTableMadeOptionalBySubsequentJoins - Checks if later joins affect table optionality
|
|
476
|
+
*
|
|
477
|
+
* This helper determines if a table that was initially required becomes optional
|
|
478
|
+
* due to joins that happen later in the query chain.
|
|
479
|
+
*
|
|
480
|
+
* **Current Implementation**:
|
|
481
|
+
* - Main table: Becomes optional if any right/full joins exist
|
|
482
|
+
* - Joined tables: Not affected by subsequent joins (simplified model)
|
|
483
|
+
*
|
|
484
|
+
* This is a conservative approach that may be extended in the future to handle
|
|
485
|
+
* more complex join interaction scenarios.
|
|
486
|
+
*/
|
|
91
487
|
type IsTableMadeOptionalBySubsequentJoins<TTableAlias extends string | number | symbol, TJoinTypes extends Record<string, string>, TFromSourceName extends string> = TTableAlias extends TFromSourceName ? HasJoinType<TJoinTypes, `right` | `full`> : false;
|
|
488
|
+
/**
|
|
489
|
+
* HasJoinType - Utility to check if any join in a chain matches target types
|
|
490
|
+
*
|
|
491
|
+
* This type searches through all recorded join types to see if any match
|
|
492
|
+
* the specified target types. It's used to implement logic like "becomes optional
|
|
493
|
+
* if ANY right or full join exists in the chain".
|
|
494
|
+
*
|
|
495
|
+
* **How it works**:
|
|
496
|
+
* 1. Maps over all join types, checking each against target types
|
|
497
|
+
* 2. Creates a union of boolean results
|
|
498
|
+
* 3. Uses `true extends Union` pattern to check if any were true
|
|
499
|
+
*
|
|
500
|
+
* **Example**:
|
|
501
|
+
* ```typescript
|
|
502
|
+
* HasJoinType<{ orders: 'left', products: 'right' }, 'right' | 'full'>
|
|
503
|
+
* // → true (because products is a right join)
|
|
504
|
+
* ```
|
|
505
|
+
*/
|
|
92
506
|
export type HasJoinType<TJoinTypes extends Record<string, string>, TTargetTypes extends string> = true extends {
|
|
93
507
|
[K in keyof TJoinTypes]: TJoinTypes[K] extends TTargetTypes ? true : false;
|
|
94
508
|
}[keyof TJoinTypes] ? true : false;
|
|
95
|
-
|
|
509
|
+
/**
|
|
510
|
+
* MergeContextForJoinCallback - Special context for join condition callbacks
|
|
511
|
+
*
|
|
512
|
+
* This type creates a context specifically for the `onCallback` parameter of join operations.
|
|
513
|
+
* The key difference from `MergeContextWithJoinType` is that NO optionality is applied here.
|
|
514
|
+
*
|
|
515
|
+
* **Why No Optionality?**
|
|
516
|
+
* In SQL, join conditions are evaluated BEFORE optionality is determined. Both tables
|
|
517
|
+
* must be treated as available (non-optional) within the join condition itself.
|
|
518
|
+
* Optionality is only applied to the result AFTER the join logic executes.
|
|
519
|
+
*
|
|
520
|
+
* **Example**:
|
|
521
|
+
* ```typescript
|
|
522
|
+
* .from({ users })
|
|
523
|
+
* .leftJoin({ orders }, ({ users, orders }) => {
|
|
524
|
+
* // users is NOT optional here - we can access users.id directly
|
|
525
|
+
* // orders is NOT optional here - we can access orders.userId directly
|
|
526
|
+
* return eq(users.id, orders.userId)
|
|
527
|
+
* })
|
|
528
|
+
* .where(({ orders }) => {
|
|
529
|
+
* // NOW orders is optional because it's after the LEFT JOIN
|
|
530
|
+
* return orders?.status === 'pending'
|
|
531
|
+
* })
|
|
532
|
+
* ```
|
|
533
|
+
*
|
|
534
|
+
* The simple intersection (&) merges schemas without any optionality transformation.
|
|
535
|
+
*/
|
|
536
|
+
export type MergeContextForJoinCallback<TContext extends Context, TNewSchema extends ContextSchema> = {
|
|
537
|
+
baseSchema: TContext[`baseSchema`];
|
|
538
|
+
schema: TContext[`schema`] & TNewSchema;
|
|
539
|
+
fromSourceName: TContext[`fromSourceName`];
|
|
540
|
+
hasJoins: true;
|
|
541
|
+
joinTypes: TContext[`joinTypes`] extends Record<string, any> ? TContext[`joinTypes`] : {};
|
|
542
|
+
result: TContext[`result`];
|
|
543
|
+
};
|
|
544
|
+
/**
|
|
545
|
+
* WithResult - Updates a context with a new result type after select()
|
|
546
|
+
*
|
|
547
|
+
* This utility type is used internally when the `select()` method is called
|
|
548
|
+
* to update the context with the projected result type. It preserves all
|
|
549
|
+
* other context properties while replacing the `result` field.
|
|
550
|
+
*
|
|
551
|
+
* **Usage**:
|
|
552
|
+
* When `select()` is called, the query builder uses this type to create
|
|
553
|
+
* a new context where `result` contains the shape of the selected fields.
|
|
554
|
+
*
|
|
555
|
+
* The double `Prettify` ensures both the overall context and the nested
|
|
556
|
+
* result type display cleanly in IDEs.
|
|
557
|
+
*/
|
|
96
558
|
export type WithResult<TContext extends Context, TResult> = Prettify<Omit<TContext, `result`> & {
|
|
97
559
|
result: Prettify<TResult>;
|
|
98
560
|
}>;
|
|
561
|
+
/**
|
|
562
|
+
* Prettify - Utility type for clean IDE display
|
|
563
|
+
*/
|
|
99
564
|
export type Prettify<T> = {
|
|
100
565
|
[K in keyof T]: T[K];
|
|
101
566
|
} & {};
|
|
@@ -263,6 +263,35 @@ function compileFunction(func, isSingleRow) {
|
|
|
263
263
|
return divisor !== 0 ? (a ?? 0) / divisor : null;
|
|
264
264
|
};
|
|
265
265
|
}
|
|
266
|
+
// Null/undefined checking functions
|
|
267
|
+
case `isUndefined`: {
|
|
268
|
+
const arg = compiledArgs[0];
|
|
269
|
+
return (data) => {
|
|
270
|
+
const value = arg(data);
|
|
271
|
+
return value === void 0;
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
case `isNotUndefined`: {
|
|
275
|
+
const arg = compiledArgs[0];
|
|
276
|
+
return (data) => {
|
|
277
|
+
const value = arg(data);
|
|
278
|
+
return value !== void 0;
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
case `isNull`: {
|
|
282
|
+
const arg = compiledArgs[0];
|
|
283
|
+
return (data) => {
|
|
284
|
+
const value = arg(data);
|
|
285
|
+
return value === null;
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
case `isNotNull`: {
|
|
289
|
+
const arg = compiledArgs[0];
|
|
290
|
+
return (data) => {
|
|
291
|
+
const value = arg(data);
|
|
292
|
+
return value !== null;
|
|
293
|
+
};
|
|
294
|
+
}
|
|
266
295
|
default:
|
|
267
296
|
throw new UnknownFunctionError(func.name);
|
|
268
297
|
}
|