@prisma-next/sql-orm-lane 0.3.0-dev.3 → 0.3.0-dev.5

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 (82) hide show
  1. package/dist/exports/orm.d.ts +3 -5
  2. package/dist/exports/orm.d.ts.map +1 -0
  3. package/dist/index.d.ts +4 -42
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/mutations/delete-builder.d.ts +9 -0
  6. package/dist/mutations/delete-builder.d.ts.map +1 -0
  7. package/dist/mutations/insert-builder.d.ts +7 -0
  8. package/dist/mutations/insert-builder.d.ts.map +1 -0
  9. package/dist/mutations/update-builder.d.ts +9 -0
  10. package/dist/mutations/update-builder.d.ts.map +1 -0
  11. package/dist/orm/builder.d.ts +38 -0
  12. package/dist/orm/builder.d.ts.map +1 -0
  13. package/dist/orm/capabilities.d.ts +3 -0
  14. package/dist/orm/capabilities.d.ts.map +1 -0
  15. package/dist/orm/context.d.ts +5 -0
  16. package/dist/orm/context.d.ts.map +1 -0
  17. package/dist/orm/state.d.ts +45 -0
  18. package/dist/orm/state.d.ts.map +1 -0
  19. package/dist/orm-include-child.d.ts +35 -0
  20. package/dist/orm-include-child.d.ts.map +1 -0
  21. package/dist/orm-relation-filter.d.ts +19 -0
  22. package/dist/orm-relation-filter.d.ts.map +1 -0
  23. package/dist/{orm-DAnGd7z2.d.ts → orm-types.d.ts} +16 -27
  24. package/dist/orm-types.d.ts.map +1 -0
  25. package/dist/orm.d.ts +5 -0
  26. package/dist/orm.d.ts.map +1 -0
  27. package/dist/plan/lowering.d.ts +2 -0
  28. package/dist/plan/lowering.d.ts.map +1 -0
  29. package/dist/plan/plan-assembly.d.ts +24 -0
  30. package/dist/plan/plan-assembly.d.ts.map +1 -0
  31. package/dist/plan/result-typing.d.ts +2 -0
  32. package/dist/plan/result-typing.d.ts.map +1 -0
  33. package/dist/relations/include-plan.d.ts +38 -0
  34. package/dist/relations/include-plan.d.ts.map +1 -0
  35. package/dist/selection/join.d.ts +3 -0
  36. package/dist/selection/join.d.ts.map +1 -0
  37. package/dist/selection/ordering.d.ts +11 -0
  38. package/dist/selection/ordering.d.ts.map +1 -0
  39. package/dist/selection/pagination.d.ts +6 -0
  40. package/dist/selection/pagination.d.ts.map +1 -0
  41. package/dist/selection/predicates.d.ts +10 -0
  42. package/dist/selection/predicates.d.ts.map +1 -0
  43. package/dist/selection/projection.d.ts +28 -0
  44. package/dist/selection/projection.d.ts.map +1 -0
  45. package/dist/selection/select-builder.d.ts +22 -0
  46. package/dist/selection/select-builder.d.ts.map +1 -0
  47. package/dist/types/internal.d.ts +4 -0
  48. package/dist/types/internal.d.ts.map +1 -0
  49. package/dist/utils/ast.d.ts +2 -0
  50. package/dist/utils/ast.d.ts.map +1 -0
  51. package/dist/utils/errors.d.ts +29 -0
  52. package/dist/utils/errors.d.ts.map +1 -0
  53. package/dist/utils/param-descriptor.d.ts +10 -0
  54. package/dist/utils/param-descriptor.d.ts.map +1 -0
  55. package/package.json +16 -16
  56. package/src/exports/orm.ts +12 -0
  57. package/src/index.ts +11 -0
  58. package/src/mutations/delete-builder.ts +87 -0
  59. package/src/mutations/insert-builder.ts +148 -0
  60. package/src/mutations/update-builder.ts +141 -0
  61. package/src/orm/builder.ts +744 -0
  62. package/src/orm/capabilities.ts +14 -0
  63. package/src/orm/context.ts +10 -0
  64. package/src/orm/state.ts +52 -0
  65. package/src/orm-include-child.ts +169 -0
  66. package/src/orm-relation-filter.ts +93 -0
  67. package/src/orm-types.ts +271 -0
  68. package/src/orm.ts +51 -0
  69. package/src/plan/lowering.ts +1 -0
  70. package/src/plan/plan-assembly.ts +297 -0
  71. package/src/plan/result-typing.ts +1 -0
  72. package/src/relations/include-plan.ts +324 -0
  73. package/src/selection/join.ts +13 -0
  74. package/src/selection/ordering.ts +52 -0
  75. package/src/selection/pagination.ts +11 -0
  76. package/src/selection/predicates.ts +120 -0
  77. package/src/selection/projection.ts +136 -0
  78. package/src/selection/select-builder.ts +82 -0
  79. package/src/types/internal.ts +3 -0
  80. package/src/utils/ast.ts +12 -0
  81. package/src/utils/errors.ts +130 -0
  82. package/src/utils/param-descriptor.ts +19 -0
@@ -0,0 +1,744 @@
1
+ import type { ParamDescriptor } from '@prisma-next/contract/types';
2
+ import { planInvalid } from '@prisma-next/plan';
3
+ import type { SqlContract, SqlStorage } from '@prisma-next/sql-contract/types';
4
+ import type { SelectAst, TableRef } from '@prisma-next/sql-relational-core/ast';
5
+ import type { SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
6
+ import type { QueryLaneContext } from '@prisma-next/sql-relational-core/query-lane-context';
7
+ import { schema } from '@prisma-next/sql-relational-core/schema';
8
+ import type {
9
+ AnyBinaryBuilder,
10
+ AnyColumnBuilder,
11
+ AnyOrderBuilder,
12
+ BinaryBuilder,
13
+ BuildOptions,
14
+ InferNestedProjectionRow,
15
+ NestedProjection,
16
+ OrderBuilder,
17
+ } from '@prisma-next/sql-relational-core/types';
18
+ import { buildDeletePlan } from '../mutations/delete-builder';
19
+ import { buildInsertPlan } from '../mutations/insert-builder';
20
+ import { buildUpdatePlan } from '../mutations/update-builder';
21
+ import type { OrmIncludeChildBuilder } from '../orm-include-child';
22
+ import { OrmIncludeChildBuilderImpl } from '../orm-include-child';
23
+ import { OrmRelationFilterBuilderImpl } from '../orm-relation-filter';
24
+ import type {
25
+ IncludeAccumulator,
26
+ ModelColumnAccessor,
27
+ OrmBuilderOptions,
28
+ OrmIncludeAccessor,
29
+ OrmModelBuilder,
30
+ OrmRelationFilterBuilder,
31
+ OrmWhereProperty,
32
+ } from '../orm-types';
33
+ import { buildMeta, type MetaBuildArgs } from '../plan/plan-assembly';
34
+ import {
35
+ buildExistsSubqueries,
36
+ buildIncludeAsts,
37
+ combineWhereClauses,
38
+ } from '../relations/include-plan';
39
+ import { buildOrderByClause } from '../selection/ordering';
40
+ import { buildWhereExpr } from '../selection/predicates';
41
+ import { buildProjectionState, type ProjectionInput } from '../selection/projection';
42
+ import { buildProjectionItems, buildSelectAst } from '../selection/select-builder';
43
+ import { createTableRef } from '../utils/ast';
44
+ import { errorModelNotFound, errorTableNotFound, errorUnknownTable } from '../utils/errors';
45
+ import { createOrmContext } from './context';
46
+ import type { OrmIncludeState, RelationFilter } from './state';
47
+
48
+ export class OrmModelBuilderImpl<
49
+ TContract extends SqlContract<SqlStorage>,
50
+ CodecTypes extends Record<string, { output: unknown }> = Record<string, never>,
51
+ ModelName extends string = string,
52
+ Includes extends Record<string, unknown> = Record<string, never>,
53
+ Row = unknown,
54
+ > implements OrmModelBuilder<TContract, CodecTypes, ModelName, Includes, Row>
55
+ {
56
+ private readonly context: QueryLaneContext<TContract>;
57
+ private readonly contract: TContract;
58
+ private readonly modelName: ModelName;
59
+ private table: TableRef;
60
+ private wherePredicate: AnyBinaryBuilder | undefined = undefined;
61
+ private relationFilters: RelationFilter[] = [];
62
+ private includes: OrmIncludeState[] = [];
63
+ private orderByExpr: AnyOrderBuilder | undefined = undefined;
64
+ private limitValue: number | undefined = undefined;
65
+ private offsetValue: number | undefined = undefined;
66
+ private projection: Record<string, AnyColumnBuilder | boolean | NestedProjection> | undefined =
67
+ undefined;
68
+
69
+ constructor(options: OrmBuilderOptions<TContract>, modelName: ModelName) {
70
+ this.context = options.context;
71
+ this.contract = options.context.contract;
72
+ this.modelName = modelName;
73
+
74
+ const tableName = this.contract.mappings.modelToTable?.[modelName];
75
+ if (!tableName) {
76
+ errorModelNotFound(modelName);
77
+ }
78
+
79
+ const schemaHandle = schema(options.context);
80
+ const table = schemaHandle.tables[tableName];
81
+ if (!table) {
82
+ errorTableNotFound(tableName);
83
+ }
84
+ this.table = table;
85
+ }
86
+
87
+ get where(): OrmWhereProperty<TContract, CodecTypes, ModelName, Includes, Row> {
88
+ const whereFn = (
89
+ fn: (model: ModelColumnAccessor<TContract, CodecTypes, ModelName>) => AnyBinaryBuilder,
90
+ ): OrmModelBuilder<TContract, CodecTypes, ModelName, Includes, Row> => {
91
+ const builder = new OrmModelBuilderImpl<TContract, CodecTypes, ModelName, Includes, Row>(
92
+ { context: this.context },
93
+ this.modelName,
94
+ );
95
+ builder['table'] = this.table;
96
+ builder.wherePredicate = fn(this._getModelAccessor());
97
+ builder.relationFilters = this.relationFilters;
98
+ builder.includes = this.includes;
99
+ builder.orderByExpr = this.orderByExpr;
100
+ builder.limitValue = this.limitValue;
101
+ builder.offsetValue = this.offsetValue;
102
+ builder.projection = this.projection;
103
+ return builder;
104
+ };
105
+
106
+ // Add related property using Proxy
107
+ const related = this._createRelatedProxy();
108
+
109
+ return Object.assign(whereFn, { related }) as OrmWhereProperty<
110
+ TContract,
111
+ CodecTypes,
112
+ ModelName,
113
+ Includes,
114
+ Row
115
+ >;
116
+ }
117
+
118
+ get include(): OrmIncludeAccessor<TContract, CodecTypes, ModelName, Includes, Row> {
119
+ return this._createIncludeProxy();
120
+ }
121
+
122
+ private _createIncludeProxy(): OrmIncludeAccessor<
123
+ TContract,
124
+ CodecTypes,
125
+ ModelName,
126
+ Includes,
127
+ Row
128
+ > {
129
+ const self = this;
130
+ // Relations are keyed by table name, not model name
131
+ const tableName = this.contract.mappings.modelToTable?.[this.modelName];
132
+ if (!tableName) {
133
+ return {} as OrmIncludeAccessor<TContract, CodecTypes, ModelName, Includes, Row>;
134
+ }
135
+ const modelRelations = this.contract.relations?.[tableName];
136
+ if (!modelRelations || typeof modelRelations !== 'object') {
137
+ return {} as OrmIncludeAccessor<TContract, CodecTypes, ModelName, Includes, Row>;
138
+ }
139
+
140
+ return new Proxy({} as OrmIncludeAccessor<TContract, CodecTypes, ModelName, Includes, Row>, {
141
+ get(_target, prop) {
142
+ if (typeof prop !== 'string') {
143
+ return undefined;
144
+ }
145
+
146
+ const relation = (modelRelations as Record<string, { to?: string }>)[prop];
147
+ if (!relation || typeof relation !== 'object' || !('to' in relation)) {
148
+ throw planInvalid(`Relation ${prop} not found on model ${self.modelName}`);
149
+ }
150
+
151
+ const childModelName = relation.to as string;
152
+ const relationDef = relation as {
153
+ to: string;
154
+ cardinality: string;
155
+ on: { parentCols: readonly string[]; childCols: readonly string[] };
156
+ };
157
+
158
+ const relationName = prop as keyof typeof modelRelations & string;
159
+
160
+ const includeFn = (<ChildRow>(
161
+ child: (
162
+ child: OrmIncludeChildBuilder<TContract, CodecTypes, typeof childModelName>,
163
+ ) => OrmIncludeChildBuilder<TContract, CodecTypes, typeof childModelName, ChildRow>,
164
+ ) => {
165
+ return self._applyInclude<typeof relationName, ChildRow>(
166
+ relationName,
167
+ childModelName,
168
+ child,
169
+ relationDef,
170
+ );
171
+ }) as OrmIncludeAccessor<
172
+ TContract,
173
+ CodecTypes,
174
+ ModelName,
175
+ Includes,
176
+ Row
177
+ >[typeof relationName];
178
+
179
+ return includeFn;
180
+ },
181
+ });
182
+ }
183
+
184
+ private _applyInclude<RelationName extends string, ChildRow>(
185
+ relationName: RelationName,
186
+ childModelName: string,
187
+ childBuilderFn: (
188
+ child: OrmIncludeChildBuilder<TContract, CodecTypes, string>,
189
+ ) => OrmIncludeChildBuilder<TContract, CodecTypes, string, ChildRow>,
190
+ relationDef: {
191
+ to: string;
192
+ cardinality: string;
193
+ on: { parentCols: readonly string[]; childCols: readonly string[] };
194
+ },
195
+ ): OrmModelBuilder<
196
+ TContract,
197
+ CodecTypes,
198
+ ModelName,
199
+ IncludeAccumulator<Includes, RelationName, ChildRow>,
200
+ Row
201
+ > {
202
+ // Get child table
203
+ const childTableName = this.contract.mappings.modelToTable?.[childModelName];
204
+ if (!childTableName) {
205
+ errorModelNotFound(childModelName);
206
+ }
207
+ const childTable: TableRef = { kind: 'table', name: childTableName };
208
+
209
+ // Create child builder and apply callback
210
+ const childBuilder = new OrmIncludeChildBuilderImpl<TContract, CodecTypes, string>(
211
+ { context: this.context },
212
+ childModelName,
213
+ );
214
+ const builtChild = childBuilderFn(
215
+ childBuilder as OrmIncludeChildBuilder<TContract, CodecTypes, string>,
216
+ );
217
+ const childState = (
218
+ builtChild as OrmIncludeChildBuilderImpl<TContract, CodecTypes, string, ChildRow>
219
+ ).getState();
220
+
221
+ // Store the include
222
+ // Note: Child projection validation happens in findMany() when compiling to includeMany
223
+ const includeState: OrmIncludeState = {
224
+ relationName,
225
+ childModelName,
226
+ childTable,
227
+ childWhere: childState.childWhere,
228
+ childOrderBy: childState.childOrderBy,
229
+ childLimit: childState.childLimit,
230
+ childProjection: childState.childProjection,
231
+ alias: relationName,
232
+ relation: relationDef,
233
+ };
234
+
235
+ const builder = new OrmModelBuilderImpl<
236
+ TContract,
237
+ CodecTypes,
238
+ ModelName,
239
+ IncludeAccumulator<Includes, RelationName, ChildRow>,
240
+ Row
241
+ >({ context: this.context }, this.modelName);
242
+ builder['table'] = this.table;
243
+ builder.wherePredicate = this.wherePredicate;
244
+ builder.relationFilters = this.relationFilters;
245
+ builder.includes = [...this.includes, includeState];
246
+ builder.orderByExpr = this.orderByExpr;
247
+ builder.limitValue = this.limitValue;
248
+ builder.offsetValue = this.offsetValue;
249
+ builder.projection = this.projection;
250
+ return builder;
251
+ }
252
+
253
+ private _createRelatedProxy(): OrmWhereProperty<
254
+ TContract,
255
+ CodecTypes,
256
+ ModelName,
257
+ Includes,
258
+ Row
259
+ >['related'] {
260
+ const self = this;
261
+ // Relations are keyed by table name, not model name
262
+ const tableName = this.contract.mappings.modelToTable?.[this.modelName];
263
+ if (!tableName) {
264
+ return {} as OrmWhereProperty<TContract, CodecTypes, ModelName, Includes, Row>['related'];
265
+ }
266
+ const modelRelations = this.contract.relations?.[tableName];
267
+ if (!modelRelations || typeof modelRelations !== 'object') {
268
+ return {} as OrmWhereProperty<TContract, CodecTypes, ModelName, Includes, Row>['related'];
269
+ }
270
+
271
+ return new Proxy(
272
+ {} as OrmWhereProperty<TContract, CodecTypes, ModelName, Includes, Row>['related'],
273
+ {
274
+ get(_target, prop) {
275
+ if (typeof prop !== 'string') {
276
+ return undefined;
277
+ }
278
+
279
+ const relation = (modelRelations as Record<string, { to?: string }>)[prop];
280
+ if (!relation || typeof relation !== 'object' || !('to' in relation)) {
281
+ throw planInvalid(`Relation ${prop} not found on model ${self.modelName}`);
282
+ }
283
+
284
+ const childModelName = relation.to as string;
285
+ const relationDef = relation as {
286
+ to: string;
287
+ cardinality: string;
288
+ on: { parentCols: readonly string[]; childCols: readonly string[] };
289
+ };
290
+ const filterBuilder = new OrmRelationFilterBuilderImpl<
291
+ TContract,
292
+ CodecTypes,
293
+ typeof childModelName
294
+ >({ context: self.context }, childModelName);
295
+ // Expose model accessor directly on the builder for convenience
296
+ const modelAccessor = filterBuilder.getModelAccessor();
297
+ const builderWithAccessor = Object.assign(
298
+ filterBuilder,
299
+ modelAccessor,
300
+ ) as OrmRelationFilterBuilder<TContract, CodecTypes, typeof childModelName> &
301
+ ModelColumnAccessor<TContract, CodecTypes, typeof childModelName>;
302
+
303
+ return {
304
+ some: (
305
+ fn: (
306
+ child:
307
+ | OrmRelationFilterBuilder<TContract, CodecTypes, typeof childModelName>
308
+ | ModelColumnAccessor<TContract, CodecTypes, typeof childModelName>,
309
+ ) =>
310
+ | OrmRelationFilterBuilder<TContract, CodecTypes, typeof childModelName>
311
+ | AnyBinaryBuilder,
312
+ ) => {
313
+ const result = fn(builderWithAccessor);
314
+ // If result is a AnyBinaryBuilder, wrap it in a builder
315
+ if (result && 'kind' in result && result.kind === 'binary') {
316
+ const wrappedBuilder = new OrmRelationFilterBuilderImpl<
317
+ TContract,
318
+ CodecTypes,
319
+ typeof childModelName
320
+ >({ context: self.context }, childModelName);
321
+ wrappedBuilder['wherePredicate'] = result as AnyBinaryBuilder;
322
+ return self._applyRelationFilter(
323
+ prop,
324
+ childModelName,
325
+ 'some',
326
+ () => wrappedBuilder,
327
+ relationDef,
328
+ );
329
+ }
330
+ return self._applyRelationFilter(
331
+ prop,
332
+ childModelName,
333
+ 'some',
334
+ () =>
335
+ result as OrmRelationFilterBuilder<TContract, CodecTypes, typeof childModelName>,
336
+ relationDef,
337
+ );
338
+ },
339
+ none: (
340
+ fn: (
341
+ child:
342
+ | OrmRelationFilterBuilder<TContract, CodecTypes, typeof childModelName>
343
+ | ModelColumnAccessor<TContract, CodecTypes, typeof childModelName>,
344
+ ) =>
345
+ | OrmRelationFilterBuilder<TContract, CodecTypes, typeof childModelName>
346
+ | AnyBinaryBuilder,
347
+ ) => {
348
+ const result = fn(builderWithAccessor);
349
+ if (result && 'kind' in result && result.kind === 'binary') {
350
+ const wrappedBuilder = new OrmRelationFilterBuilderImpl<
351
+ TContract,
352
+ CodecTypes,
353
+ typeof childModelName
354
+ >({ context: self.context }, childModelName);
355
+ wrappedBuilder['wherePredicate'] = result as AnyBinaryBuilder;
356
+ return self._applyRelationFilter(
357
+ prop,
358
+ childModelName,
359
+ 'none',
360
+ () => wrappedBuilder,
361
+ relationDef,
362
+ );
363
+ }
364
+ return self._applyRelationFilter(
365
+ prop,
366
+ childModelName,
367
+ 'none',
368
+ () =>
369
+ result as OrmRelationFilterBuilder<TContract, CodecTypes, typeof childModelName>,
370
+ relationDef,
371
+ );
372
+ },
373
+ every: (
374
+ fn: (
375
+ child:
376
+ | OrmRelationFilterBuilder<TContract, CodecTypes, typeof childModelName>
377
+ | ModelColumnAccessor<TContract, CodecTypes, typeof childModelName>,
378
+ ) =>
379
+ | OrmRelationFilterBuilder<TContract, CodecTypes, typeof childModelName>
380
+ | AnyBinaryBuilder,
381
+ ) => {
382
+ const result = fn(builderWithAccessor);
383
+ if (result && 'kind' in result && result.kind === 'binary') {
384
+ const wrappedBuilder = new OrmRelationFilterBuilderImpl<
385
+ TContract,
386
+ CodecTypes,
387
+ typeof childModelName
388
+ >({ context: self.context }, childModelName);
389
+ wrappedBuilder['wherePredicate'] = result as AnyBinaryBuilder;
390
+ return self._applyRelationFilter(
391
+ prop,
392
+ childModelName,
393
+ 'every',
394
+ () => wrappedBuilder,
395
+ relationDef,
396
+ );
397
+ }
398
+ return self._applyRelationFilter(
399
+ prop,
400
+ childModelName,
401
+ 'every',
402
+ () =>
403
+ result as OrmRelationFilterBuilder<TContract, CodecTypes, typeof childModelName>,
404
+ relationDef,
405
+ );
406
+ },
407
+ };
408
+ },
409
+ },
410
+ );
411
+ }
412
+
413
+ private _applyRelationFilter(
414
+ relationName: string,
415
+ childModelName: string,
416
+ filterType: 'some' | 'none' | 'every',
417
+ fn: (
418
+ child: OrmRelationFilterBuilder<TContract, CodecTypes, string>,
419
+ ) => OrmRelationFilterBuilder<TContract, CodecTypes, string>,
420
+ relationDef: {
421
+ to: string;
422
+ cardinality: string;
423
+ on: { parentCols: readonly string[]; childCols: readonly string[] };
424
+ },
425
+ ): OrmModelBuilder<TContract, CodecTypes, ModelName, Includes, Row> {
426
+ // Create a relation filter builder and apply the callback
427
+ const filterBuilder = new OrmRelationFilterBuilderImpl<TContract, CodecTypes, string>(
428
+ { context: this.context },
429
+ childModelName,
430
+ );
431
+ const appliedFilter = fn(
432
+ filterBuilder as OrmRelationFilterBuilder<TContract, CodecTypes, string>,
433
+ );
434
+ const childWhere = (
435
+ appliedFilter as OrmRelationFilterBuilderImpl<TContract, CodecTypes, string>
436
+ ).getWherePredicate();
437
+
438
+ // Store the relation filter
439
+ const relationFilter: RelationFilter = {
440
+ relationName,
441
+ childModelName,
442
+ filterType,
443
+ childWhere,
444
+ relation: relationDef,
445
+ };
446
+
447
+ const builder = new OrmModelBuilderImpl<TContract, CodecTypes, ModelName, Includes, Row>(
448
+ { context: this.context },
449
+ this.modelName,
450
+ );
451
+ builder['table'] = this.table;
452
+ builder.wherePredicate = this.wherePredicate;
453
+ builder.relationFilters = [...this.relationFilters, relationFilter];
454
+ builder.includes = this.includes;
455
+ builder.orderByExpr = this.orderByExpr;
456
+ builder.limitValue = this.limitValue;
457
+ builder.offsetValue = this.offsetValue;
458
+ builder.projection = this.projection;
459
+ return builder;
460
+ }
461
+
462
+ orderBy(
463
+ fn: (model: ModelColumnAccessor<TContract, CodecTypes, ModelName>) => OrderBuilder,
464
+ ): OrmModelBuilder<TContract, CodecTypes, ModelName, Includes, Row> {
465
+ const builder = new OrmModelBuilderImpl<TContract, CodecTypes, ModelName, Includes, Row>(
466
+ { context: this.context },
467
+ this.modelName,
468
+ );
469
+ builder['table'] = this.table;
470
+ builder.wherePredicate = this.wherePredicate;
471
+ builder.relationFilters = this.relationFilters;
472
+ builder.includes = this.includes;
473
+ builder.orderByExpr = fn(this._getModelAccessor());
474
+ builder.limitValue = this.limitValue;
475
+ builder.offsetValue = this.offsetValue;
476
+ builder.projection = this.projection;
477
+ return builder;
478
+ }
479
+
480
+ take(n: number): OrmModelBuilder<TContract, CodecTypes, ModelName, Includes, Row> {
481
+ const builder = new OrmModelBuilderImpl<TContract, CodecTypes, ModelName, Includes, Row>(
482
+ { context: this.context },
483
+ this.modelName,
484
+ );
485
+ builder['table'] = this.table;
486
+ builder.wherePredicate = this.wherePredicate;
487
+ builder.relationFilters = this.relationFilters;
488
+ builder.includes = this.includes;
489
+ builder.orderByExpr = this.orderByExpr;
490
+ builder.limitValue = n;
491
+ builder.offsetValue = this.offsetValue;
492
+ builder.projection = this.projection;
493
+ return builder;
494
+ }
495
+
496
+ skip(n: number): OrmModelBuilder<TContract, CodecTypes, ModelName, Includes, Row> {
497
+ // TODO: SQL lane doesn't support offset yet - this is a placeholder
498
+ // When offset is added to SelectAst, implement it here
499
+ const builder = new OrmModelBuilderImpl<TContract, CodecTypes, ModelName, Includes, Row>(
500
+ { context: this.context },
501
+ this.modelName,
502
+ );
503
+ builder['table'] = this.table;
504
+ builder.wherePredicate = this.wherePredicate;
505
+ builder.relationFilters = this.relationFilters;
506
+ builder.includes = this.includes;
507
+ builder.orderByExpr = this.orderByExpr;
508
+ builder.limitValue = this.limitValue;
509
+ builder.offsetValue = n;
510
+ builder.projection = this.projection;
511
+ return builder;
512
+ }
513
+
514
+ select<Projection extends Record<string, AnyColumnBuilder | boolean | NestedProjection>>(
515
+ fn: (model: ModelColumnAccessor<TContract, CodecTypes, ModelName>) => Projection,
516
+ ): OrmModelBuilder<
517
+ TContract,
518
+ CodecTypes,
519
+ ModelName,
520
+ Includes,
521
+ InferNestedProjectionRow<Projection, CodecTypes, Includes>
522
+ > {
523
+ const builder = new OrmModelBuilderImpl<
524
+ TContract,
525
+ CodecTypes,
526
+ ModelName,
527
+ Includes,
528
+ InferNestedProjectionRow<Projection, CodecTypes, Includes>
529
+ >({ context: this.context }, this.modelName);
530
+ builder['table'] = this.table;
531
+ builder.wherePredicate = this.wherePredicate;
532
+ builder.relationFilters = this.relationFilters;
533
+ builder.includes = this.includes;
534
+ builder.orderByExpr = this.orderByExpr;
535
+ builder.limitValue = this.limitValue;
536
+ builder.offsetValue = this.offsetValue;
537
+ builder.projection = fn(this._getModelAccessor());
538
+ return builder as OrmModelBuilder<
539
+ TContract,
540
+ CodecTypes,
541
+ ModelName,
542
+ Includes,
543
+ InferNestedProjectionRow<Projection, CodecTypes, Includes>
544
+ >;
545
+ }
546
+
547
+ findMany(options?: BuildOptions): SqlQueryPlan<Row> {
548
+ const paramsMap = (options?.params ?? {}) as Record<string, unknown>;
549
+ const contractTable = this.contract.storage.tables[this.table.name];
550
+
551
+ if (!contractTable) {
552
+ errorUnknownTable(this.table.name);
553
+ }
554
+
555
+ const paramDescriptors: ParamDescriptor[] = [];
556
+ const paramValues: unknown[] = [];
557
+ const paramCodecs: Record<string, string> = {};
558
+
559
+ // Build projection state
560
+ const projectionInput: ProjectionInput =
561
+ this.projection ??
562
+ (() => {
563
+ const modelAccessor = this._getModelAccessor();
564
+ const defaultProjection: Record<string, AnyColumnBuilder> = {};
565
+ for (const fieldName in modelAccessor) {
566
+ defaultProjection[fieldName] = modelAccessor[fieldName];
567
+ }
568
+ return defaultProjection;
569
+ })();
570
+
571
+ // Build includes AST
572
+ const { includesAst, includesForMeta } = buildIncludeAsts({
573
+ includes: this.includes,
574
+ contract: this.contract,
575
+ context: this.context,
576
+ modelName: this.modelName,
577
+ paramsMap,
578
+ paramDescriptors,
579
+ paramValues,
580
+ paramCodecs,
581
+ });
582
+
583
+ // Build projection state
584
+ const projectionState = buildProjectionState(
585
+ this.table,
586
+ projectionInput,
587
+ includesForMeta.length > 0
588
+ ? (includesForMeta as unknown as Parameters<typeof buildProjectionState>[2])
589
+ : undefined,
590
+ );
591
+
592
+ // Build where clause
593
+ const whereResult = this.wherePredicate
594
+ ? buildWhereExpr(this.wherePredicate, this.contract, paramsMap, paramDescriptors, paramValues)
595
+ : undefined;
596
+ const whereExpr = whereResult?.expr;
597
+ if (whereResult?.codecId && whereResult.paramName) {
598
+ paramCodecs[whereResult.paramName] = whereResult.codecId;
599
+ }
600
+
601
+ // Build orderBy clause
602
+ const orderByClause = buildOrderByClause(this.orderByExpr);
603
+
604
+ // Build main projection items
605
+ const projectEntries = buildProjectionItems(projectionState, includesForMeta);
606
+
607
+ // Build SELECT AST
608
+ const ast = buildSelectAst({
609
+ table: this.table,
610
+ projectEntries,
611
+ ...(includesAst.length > 0 ? { includesAst } : {}),
612
+ ...(whereExpr ? { whereExpr } : {}),
613
+ ...(orderByClause ? { orderByClause } : {}),
614
+ ...(this.limitValue !== undefined ? { limit: this.limitValue } : {}),
615
+ });
616
+
617
+ // Build plan metadata
618
+ const planMeta = buildMeta({
619
+ contract: this.contract,
620
+ table: createTableRef(this.table.name),
621
+ projection: projectionState,
622
+ includes: includesForMeta.length > 0 ? includesForMeta : undefined,
623
+ paramDescriptors,
624
+ paramCodecs: Object.keys(paramCodecs).length > 0 ? paramCodecs : undefined,
625
+ where: this.wherePredicate as BinaryBuilder | undefined,
626
+ orderBy: this.orderByExpr,
627
+ } as MetaBuildArgs);
628
+
629
+ // Compile relation filters to EXISTS subqueries and combine with main where clause
630
+ if (this.relationFilters.length > 0) {
631
+ const existsExprs = buildExistsSubqueries(
632
+ this.relationFilters,
633
+ this.contract,
634
+ this.modelName,
635
+ options,
636
+ );
637
+ if (existsExprs.length > 0) {
638
+ const combinedWhere = combineWhereClauses(ast.where, existsExprs);
639
+ const modifiedAst: SelectAst = {
640
+ ...ast,
641
+ ...(combinedWhere !== undefined ? { where: combinedWhere } : {}),
642
+ };
643
+ return Object.freeze({
644
+ ast: modifiedAst,
645
+ params: paramValues,
646
+ meta: {
647
+ ...planMeta,
648
+ lane: 'orm',
649
+ },
650
+ });
651
+ }
652
+ }
653
+
654
+ return Object.freeze({
655
+ ast,
656
+ params: paramValues,
657
+ meta: {
658
+ ...planMeta,
659
+ lane: 'orm',
660
+ },
661
+ });
662
+ }
663
+
664
+ findFirst(options?: BuildOptions): SqlQueryPlan<Row> {
665
+ const queryPlan = this.take(1).findMany(options);
666
+ return queryPlan;
667
+ }
668
+
669
+ findUnique(
670
+ where: (model: ModelColumnAccessor<TContract, CodecTypes, ModelName>) => AnyBinaryBuilder,
671
+ options?: BuildOptions,
672
+ ): SqlQueryPlan<Row> {
673
+ return this.where(where).take(1).findMany(options);
674
+ }
675
+
676
+ create(data: Record<string, unknown>, options?: BuildOptions): SqlQueryPlan<number> {
677
+ const context = createOrmContext(this.context);
678
+ return buildInsertPlan(context, this.modelName, data, options);
679
+ }
680
+
681
+ update(
682
+ where: (model: ModelColumnAccessor<TContract, CodecTypes, ModelName>) => AnyBinaryBuilder,
683
+ data: Record<string, unknown>,
684
+ options?: BuildOptions,
685
+ ): SqlQueryPlan<number> {
686
+ const context = createOrmContext(this.context);
687
+ return buildUpdatePlan<TContract, CodecTypes, ModelName>(
688
+ context,
689
+ this.modelName,
690
+ where,
691
+ () => this._getModelAccessor(),
692
+ data,
693
+ options,
694
+ );
695
+ }
696
+
697
+ delete(
698
+ where: (model: ModelColumnAccessor<TContract, CodecTypes, ModelName>) => AnyBinaryBuilder,
699
+ options?: BuildOptions,
700
+ ): SqlQueryPlan<number> {
701
+ const context = createOrmContext(this.context);
702
+ return buildDeletePlan<TContract, CodecTypes, ModelName>(
703
+ context,
704
+ this.modelName,
705
+ where,
706
+ () => this._getModelAccessor(),
707
+ options,
708
+ );
709
+ }
710
+
711
+ private _getModelAccessor(): ModelColumnAccessor<TContract, CodecTypes, ModelName> {
712
+ const tableName = this.contract.mappings.modelToTable?.[this.modelName];
713
+ if (!tableName) {
714
+ errorModelNotFound(this.modelName);
715
+ }
716
+ const schemaHandle = schema(this.context);
717
+ const table = schemaHandle.tables[tableName];
718
+ if (!table) {
719
+ errorTableNotFound(tableName);
720
+ }
721
+
722
+ const accessor: Record<string, AnyColumnBuilder> = {};
723
+ const model = this.contract.models[this.modelName];
724
+ if (!model || typeof model !== 'object' || !('fields' in model)) {
725
+ throw planInvalid(`Model ${this.modelName} does not have fields`);
726
+ }
727
+ const modelFields = model.fields as Record<string, { column?: string }>;
728
+
729
+ for (const fieldName in modelFields) {
730
+ const field = modelFields[fieldName];
731
+ if (!field) continue;
732
+ const columnName =
733
+ this.contract.mappings.fieldToColumn?.[this.modelName]?.[fieldName] ??
734
+ field.column ??
735
+ fieldName;
736
+ const column = table.columns[columnName];
737
+ if (column) {
738
+ accessor[fieldName] = column as AnyColumnBuilder;
739
+ }
740
+ }
741
+
742
+ return accessor as ModelColumnAccessor<TContract, CodecTypes, ModelName>;
743
+ }
744
+ }