@tanstack/db 0.1.11 → 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.
Files changed (74) hide show
  1. package/dist/cjs/errors.cjs +18 -6
  2. package/dist/cjs/errors.cjs.map +1 -1
  3. package/dist/cjs/errors.d.cts +9 -3
  4. package/dist/cjs/index.cjs +3 -1
  5. package/dist/cjs/index.cjs.map +1 -1
  6. package/dist/cjs/query/builder/functions.cjs +4 -1
  7. package/dist/cjs/query/builder/functions.cjs.map +1 -1
  8. package/dist/cjs/query/builder/functions.d.cts +38 -21
  9. package/dist/cjs/query/builder/index.cjs +25 -16
  10. package/dist/cjs/query/builder/index.cjs.map +1 -1
  11. package/dist/cjs/query/builder/index.d.cts +8 -8
  12. package/dist/cjs/query/builder/ref-proxy.cjs +12 -8
  13. package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
  14. package/dist/cjs/query/builder/ref-proxy.d.cts +2 -1
  15. package/dist/cjs/query/builder/types.d.cts +493 -28
  16. package/dist/cjs/query/compiler/evaluators.cjs +29 -0
  17. package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
  18. package/dist/cjs/query/compiler/group-by.cjs +4 -2
  19. package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
  20. package/dist/cjs/query/compiler/index.cjs +13 -4
  21. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  22. package/dist/cjs/query/compiler/joins.cjs +70 -61
  23. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  24. package/dist/cjs/query/compiler/select.cjs +131 -42
  25. package/dist/cjs/query/compiler/select.cjs.map +1 -1
  26. package/dist/cjs/query/compiler/select.d.cts +1 -5
  27. package/dist/cjs/query/ir.cjs +4 -0
  28. package/dist/cjs/query/ir.cjs.map +1 -1
  29. package/dist/cjs/query/ir.d.cts +6 -1
  30. package/dist/cjs/query/optimizer.cjs +61 -20
  31. package/dist/cjs/query/optimizer.cjs.map +1 -1
  32. package/dist/esm/errors.d.ts +9 -3
  33. package/dist/esm/errors.js +18 -6
  34. package/dist/esm/errors.js.map +1 -1
  35. package/dist/esm/index.js +4 -2
  36. package/dist/esm/query/builder/functions.d.ts +38 -21
  37. package/dist/esm/query/builder/functions.js +4 -1
  38. package/dist/esm/query/builder/functions.js.map +1 -1
  39. package/dist/esm/query/builder/index.d.ts +8 -8
  40. package/dist/esm/query/builder/index.js +27 -18
  41. package/dist/esm/query/builder/index.js.map +1 -1
  42. package/dist/esm/query/builder/ref-proxy.d.ts +2 -1
  43. package/dist/esm/query/builder/ref-proxy.js +12 -8
  44. package/dist/esm/query/builder/ref-proxy.js.map +1 -1
  45. package/dist/esm/query/builder/types.d.ts +493 -28
  46. package/dist/esm/query/compiler/evaluators.js +29 -0
  47. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  48. package/dist/esm/query/compiler/group-by.js +4 -2
  49. package/dist/esm/query/compiler/group-by.js.map +1 -1
  50. package/dist/esm/query/compiler/index.js +15 -6
  51. package/dist/esm/query/compiler/index.js.map +1 -1
  52. package/dist/esm/query/compiler/joins.js +71 -62
  53. package/dist/esm/query/compiler/joins.js.map +1 -1
  54. package/dist/esm/query/compiler/select.d.ts +1 -5
  55. package/dist/esm/query/compiler/select.js +131 -42
  56. package/dist/esm/query/compiler/select.js.map +1 -1
  57. package/dist/esm/query/ir.d.ts +6 -1
  58. package/dist/esm/query/ir.js +4 -0
  59. package/dist/esm/query/ir.js.map +1 -1
  60. package/dist/esm/query/optimizer.js +62 -21
  61. package/dist/esm/query/optimizer.js.map +1 -1
  62. package/package.json +2 -2
  63. package/src/errors.ts +17 -10
  64. package/src/query/builder/functions.ts +176 -108
  65. package/src/query/builder/index.ts +68 -48
  66. package/src/query/builder/ref-proxy.ts +14 -20
  67. package/src/query/builder/types.ts +622 -110
  68. package/src/query/compiler/evaluators.ts +30 -0
  69. package/src/query/compiler/group-by.ts +6 -1
  70. package/src/query/compiler/index.ts +23 -6
  71. package/src/query/compiler/joins.ts +132 -104
  72. package/src/query/compiler/select.ts +206 -113
  73. package/src/query/ir.ts +14 -1
  74. 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
- export type WhereCallback<TContext extends Context> = (refs: RefProxyForContext<TContext>) => any;
23
- export type SelectObject<T extends Record<string, BasicExpression | Aggregate | RefProxy | RefProxyFor<any>> = Record<string, BasicExpression | Aggregate | RefProxy | RefProxyFor<any>>> = T;
24
- export type ResultTypeFromSelect<TSelectObject> = {
25
- [K in keyof TSelectObject]: TSelectObject[K] extends RefProxy<infer T> ? T : TSelectObject[K] extends BasicExpression<infer T> ? T : TSelectObject[K] extends Aggregate<infer T> ? T : TSelectObject[K] extends RefProxyFor<infer T> ? T : TSelectObject[K] extends undefined ? undefined : TSelectObject[K] extends {
26
- __type: infer U;
27
- } ? U : never;
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
- export type OrderByCallback<TContext extends Context> = (refs: RefProxyForContext<TContext>) => any;
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
- export type GroupByCallback<TContext extends Context> = (refs: RefProxyForContext<TContext>) => any;
49
- export type JoinOnCallback<TContext extends Context> = (refs: RefProxyForContext<TContext>) => any;
50
- export type RefProxyForContext<TContext extends Context> = {
51
- [K in keyof TContext[`schema`]]: RefProxyFor<TContext[`schema`][K]>;
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
- export type RefProxyFor<T> = OmitRefProxy<IsExactlyUndefined<T> extends true ? RefProxy<T> : IsOptional<T> extends true ? NonUndefined<T> extends Record<string, any> ? {
57
- [K in keyof NonUndefined<T>]-?: NonUndefined<T>[K] extends Record<string, any> ? RefProxyFor<NonUndefined<T>[K]> & RefProxy<NonUndefined<T>[K] | undefined> : RefProxy<NonUndefined<T>[K] | undefined>;
58
- } & RefProxy<T> : RefProxy<T> : T extends Record<string, any> ? {
59
- [K in keyof T]-?: undefined extends T[K] ? T[K] extends Record<string, any> ? RefProxyFor<T[K]> & RefProxy<T[K]> : RefProxy<T[K]> : T[K] extends Record<string, any> ? RefProxyFor<T[K]> & RefProxy<T[K]> : RefProxy<T[K]>;
60
- } & RefProxy<T> : RefProxy<T>>;
61
- export type Ref<T> = RefProxyFor<T>;
62
- type OmitRefProxy<T> = Omit<T, `__refProxy` | `__path` | `__type`>;
63
- export interface RefProxy<T = any> {
64
- /** @internal */
65
- readonly __refProxy: true;
66
- /** @internal */
67
- readonly __path: Array<string>;
68
- /** @internal */
69
- readonly __type: T;
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
- export type MergeContext<TContext extends Context, TNewSchema extends ContextSchema> = MergeContextWithJoinType<TContext, TNewSchema, `left`>;
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
  }