turbine-orm 0.5.0 → 0.7.1

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 (50) hide show
  1. package/README.md +292 -26
  2. package/dist/cjs/cli/config.js +5 -15
  3. package/dist/cjs/cli/index.js +311 -43
  4. package/dist/cjs/cli/loader.js +129 -0
  5. package/dist/cjs/cli/migrate.js +96 -47
  6. package/dist/cjs/cli/ui.js +5 -9
  7. package/dist/cjs/client.js +158 -49
  8. package/dist/cjs/errors.js +424 -0
  9. package/dist/cjs/generate.js +145 -14
  10. package/dist/cjs/index.js +43 -20
  11. package/dist/cjs/introspect.js +3 -5
  12. package/dist/cjs/pipeline.js +9 -2
  13. package/dist/cjs/query.js +544 -115
  14. package/dist/cjs/schema-builder.js +150 -30
  15. package/dist/cjs/schema-sql.js +241 -37
  16. package/dist/cjs/schema.js +5 -2
  17. package/dist/cjs/serverless.js +88 -176
  18. package/dist/cli/config.js +6 -16
  19. package/dist/cli/index.js +316 -48
  20. package/dist/cli/loader.d.ts +45 -0
  21. package/dist/cli/loader.js +91 -0
  22. package/dist/cli/migrate.d.ts +13 -2
  23. package/dist/cli/migrate.js +97 -48
  24. package/dist/cli/ui.d.ts +1 -1
  25. package/dist/cli/ui.js +5 -9
  26. package/dist/client.d.ts +92 -4
  27. package/dist/client.js +158 -49
  28. package/dist/errors.d.ts +225 -0
  29. package/dist/errors.js +405 -0
  30. package/dist/generate.d.ts +7 -1
  31. package/dist/generate.js +148 -18
  32. package/dist/index.d.ts +11 -9
  33. package/dist/index.js +16 -12
  34. package/dist/introspect.d.ts +1 -1
  35. package/dist/introspect.js +4 -6
  36. package/dist/pipeline.d.ts +1 -1
  37. package/dist/pipeline.js +9 -2
  38. package/dist/query.d.ts +374 -38
  39. package/dist/query.js +545 -116
  40. package/dist/schema-builder.d.ts +38 -5
  41. package/dist/schema-builder.js +150 -31
  42. package/dist/schema-sql.d.ts +7 -3
  43. package/dist/schema-sql.js +241 -37
  44. package/dist/schema.d.ts +1 -1
  45. package/dist/schema.js +5 -2
  46. package/dist/serverless.d.ts +92 -139
  47. package/dist/serverless.js +87 -173
  48. package/package.json +33 -16
  49. package/dist/types.d.ts +0 -93
  50. package/dist/types.js +0 -126
package/dist/query.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
- * @batadata/turbine — Query builder
2
+ * turbine-orm — Query builder
3
3
  *
4
4
  * Each table accessor (db.users, db.posts, etc.) returns a QueryInterface<T>
5
5
  * that builds parameterized SQL and executes it through the connection pool.
6
6
  *
7
7
  * Nested relations use json_build_object + json_agg subqueries for single-query
8
- * resolution — the technique that benchmarks proved 2-3x faster than Prisma.
8
+ * resolution — a PostgreSQL-native approach that eliminates N+1 query patterns.
9
9
  *
10
10
  * Schema-driven: all column names, types, and relations come from introspected
11
11
  * metadata — nothing is hardcoded.
@@ -61,11 +61,37 @@ export type WhereClause<T> = {
61
61
  /** Relation filters — keyed by relation name, value is { some, every, none } */
62
62
  [relationName: string]: unknown;
63
63
  };
64
+ /**
65
+ * Unparameterized with clause — accepts any relation name.
66
+ * Used internally by the query builder at runtime.
67
+ */
64
68
  export interface WithClause {
65
69
  [relation: string]: true | WithOptions;
66
70
  }
67
- export interface WithOptions {
68
- with?: WithClause;
71
+ /**
72
+ * Relation-aware with clause. When R (the relations map) is provided,
73
+ * only keys from R are autocompleted. Used in public method signatures
74
+ * so the compiler can narrow the return type.
75
+ *
76
+ * For typed maps, each relation accepts either `true` (default include) or a
77
+ * {@link WithOptions} object whose nested `with` is keyed against the relation
78
+ * target's own relations interface — this is what enables deep
79
+ * `WithResult` inference.
80
+ */
81
+ export type TypedWithClause<R extends object = {}> = [keyof R] extends [never] ? WithClause : {
82
+ [K in keyof R]?: true | WithOptions<RelationRelations<R[K]> & object>;
83
+ };
84
+ /**
85
+ * Options for an included relation.
86
+ *
87
+ * Generic over `NestedR` — the relations interface of the *target* entity —
88
+ * so the nested `with` clause is autocompleted with the correct relation keys
89
+ * and so {@link WithResult} can recursively infer the return type. Defaults to
90
+ * `{}` (no relation suggestions) for callers that use the unparameterized
91
+ * {@link WithClause}.
92
+ */
93
+ export interface WithOptions<NestedR extends object = {}> {
94
+ with?: TypedWithClause<NestedR>;
69
95
  where?: Record<string, unknown>;
70
96
  orderBy?: Record<string, OrderDirection>;
71
97
  limit?: number;
@@ -74,22 +100,103 @@ export interface WithOptions {
74
100
  /** Exclude these fields from the relation */
75
101
  omit?: Record<string, boolean>;
76
102
  }
77
- export interface FindUniqueArgs<T> {
103
+ /**
104
+ * A relation descriptor used by generated `*Relations` interfaces to make deep
105
+ * `with` clause inference work. It bundles three pieces of information that
106
+ * `WithResult` needs to recurse through nested relations:
107
+ *
108
+ * - `__target` — the target entity type (e.g. `Post`)
109
+ * - `__cardinality`— `'many'` for hasMany, `'one'` for belongsTo / hasOne
110
+ * - `__relations` — the target entity's relations interface (for further recursion)
111
+ *
112
+ * **Generator contract (Track 3):** the code generator emits `*Relations`
113
+ * interfaces in the following shape so that `WithResult` can walk arbitrary
114
+ * nesting depth:
115
+ *
116
+ * ```ts
117
+ * export interface UserRelations {
118
+ * posts: RelationDescriptor<Post, 'many', PostRelations>;
119
+ * profile: RelationDescriptor<Profile, 'one', ProfileRelations>;
120
+ * }
121
+ * ```
122
+ *
123
+ * The brand fields are phantom — they exist only for type inference and have
124
+ * no runtime representation. The runtime always sees the parsed entity values
125
+ * (arrays for hasMany, single object or null for belongsTo / hasOne) — see the
126
+ * cardinality projection inside {@link WithResult}.
127
+ *
128
+ * **Backward compatibility:** legacy generated code emitted bare types
129
+ * (`posts: Post[]`, `profile: Profile | null`). `WithResult` still accepts that
130
+ * shape via a fallback branch — it just cannot recurse into nested `with` for
131
+ * those relations until the generator is updated.
132
+ *
133
+ * @typeParam Target - The entity type the relation points at.
134
+ * @typeParam Cardinality - `'many'` (array) or `'one'` (single object | null).
135
+ * @typeParam Relations - The target entity's own `*Relations` interface, or
136
+ * `{}` if the target has no relations of its own.
137
+ */
138
+ export interface RelationDescriptor<Target, Cardinality extends 'one' | 'many', Relations extends object = {}> {
139
+ readonly __target?: Target;
140
+ readonly __cardinality?: Cardinality;
141
+ readonly __relations?: Relations;
142
+ }
143
+ /** Extract the target entity from a relation descriptor or bare relation type. */
144
+ type RelationTarget<Rel> = Rel extends RelationDescriptor<infer Target, infer _C, infer _R> ? Target : Rel extends Array<infer Item> ? Item : Rel extends infer One | null ? One : Rel;
145
+ /** Extract the target's relations map from a relation descriptor (or `{}` for bare types). */
146
+ type RelationRelations<Rel> = Rel extends RelationDescriptor<infer _T, infer _C, infer R> ? R : {};
147
+ /** Project the target type into its runtime shape (array for many, single for one). */
148
+ type ApplyCardinality<Rel, Resolved> = Rel extends RelationDescriptor<infer _T, infer Cardinality, infer _R> ? Cardinality extends 'many' ? Resolved[] : Resolved | null : Rel extends Array<infer _Item> ? Resolved[] : Resolved | null;
149
+ /**
150
+ * Compute the result type when relations are included via `with`.
151
+ *
152
+ * Recursively walks the `with` clause, looking up each relation in `R` and:
153
+ *
154
+ * 1. If the relation is included with `true` (or no nested `with`), the
155
+ * relation's bare resolved type is grafted onto `T` (e.g. `posts: Post[]`).
156
+ * 2. If the relation is included with a nested `with: {...}`, the recursion
157
+ * looks up the target entity's relations interface (via the
158
+ * {@link RelationDescriptor} brand fields the generator emits) and
159
+ * recursively applies `WithResult` to the nested target. Cardinality is
160
+ * re-applied at each level so a hasMany relation stays an array even after
161
+ * deep nesting.
162
+ *
163
+ * **When `R` is `{}` (the default):** the recursion short-circuits and the
164
+ * function returns plain `T` — preserving the existing untyped escape hatch
165
+ * for callers that have not generated typed clients.
166
+ *
167
+ * **When `R` does not contain the requested relations:** the unknown keys are
168
+ * ignored (the runtime will throw a `RelationError`, but the type system stays
169
+ * permissive so the legacy `WithClause` index signature still typechecks).
170
+ *
171
+ * @typeParam T - Base entity type (e.g. `User`).
172
+ * @typeParam R - Relations map for `T` (e.g.
173
+ * `{ posts: RelationDescriptor<Post, 'many', PostRelations>; ... }`).
174
+ * Legacy bare shapes (`{ posts: Post[]; profile: Profile | null }`)
175
+ * are also accepted, but cannot recurse beyond one level.
176
+ * @typeParam W - The `with` clause the user passed (e.g. `{ posts: true }` or
177
+ * `{ posts: { with: { comments: true } } }`).
178
+ */
179
+ export type WithResult<T, R extends object, W> = [keyof R] extends [never] ? T : W extends object ? [keyof W & keyof R] extends [never] ? T : T & {
180
+ [K in keyof W & keyof R]: W[K] extends true ? ApplyCardinality<R[K], RelationTarget<R[K]>> : W[K] extends {
181
+ with?: infer NestedW;
182
+ } ? NestedW extends object ? ApplyCardinality<R[K], WithResult<RelationTarget<R[K]>, RelationRelations<R[K]> & object, NestedW>> : ApplyCardinality<R[K], RelationTarget<R[K]>> : ApplyCardinality<R[K], RelationTarget<R[K]>>;
183
+ } : T;
184
+ export interface FindUniqueArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>> {
78
185
  where: WhereClause<T>;
79
186
  select?: Record<string, boolean>;
80
187
  omit?: Record<string, boolean>;
81
- with?: WithClause;
188
+ with?: W;
82
189
  /** Query timeout in milliseconds. Rejects with an error if exceeded. */
83
190
  timeout?: number;
84
191
  }
85
- export interface FindManyArgs<T> {
192
+ export interface FindManyArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>> {
86
193
  where?: WhereClause<T>;
87
194
  select?: Record<string, boolean>;
88
195
  omit?: Record<string, boolean>;
89
196
  orderBy?: Record<string, OrderDirection>;
90
197
  limit?: number;
91
198
  offset?: number;
92
- with?: WithClause;
199
+ with?: W;
93
200
  /** Cursor-based pagination: start after this row */
94
201
  cursor?: Partial<T>;
95
202
  /** Number of records to take (used with cursor) */
@@ -99,6 +206,10 @@ export interface FindManyArgs<T> {
99
206
  /** Query timeout in milliseconds. Rejects with an error if exceeded. */
100
207
  timeout?: number;
101
208
  }
209
+ export interface FindManyStreamArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>> extends FindManyArgs<T, R, W> {
210
+ /** Number of rows to fetch per internal FETCH batch (default: 100) */
211
+ batchSize?: number;
212
+ }
102
213
  export interface CreateArgs<T> {
103
214
  data: Partial<T>;
104
215
  /** Query timeout in milliseconds. Rejects with an error if exceeded. */
@@ -111,27 +222,71 @@ export interface CreateManyArgs<T> {
111
222
  /** Query timeout in milliseconds. Rejects with an error if exceeded. */
112
223
  timeout?: number;
113
224
  }
225
+ /**
226
+ * Atomic update operators for a field.
227
+ *
228
+ * `set` works on any type; `increment`, `decrement`, `multiply`, and `divide`
229
+ * are only valid on numeric fields. They generate SQL like
230
+ * `col = col + $n` (and the corresponding `-`, `*`, `/` variants) instead of
231
+ * plain absolute assignments, so they are safe against concurrent writers —
232
+ * the database performs the math atomically.
233
+ *
234
+ * @example
235
+ * db.posts.update({ where: { id: 5 }, data: { viewCount: { increment: 1 } } });
236
+ */
237
+ export type UpdateOperatorInput<V> = {
238
+ set: V;
239
+ } | (V extends number ? {
240
+ increment: number;
241
+ } : never) | (V extends number ? {
242
+ decrement: number;
243
+ } : never) | (V extends number ? {
244
+ multiply: number;
245
+ } : never) | (V extends number ? {
246
+ divide: number;
247
+ } : never);
248
+ /**
249
+ * Update data — each field can be a plain value or an atomic operator object.
250
+ * Back-compatible with `Partial<T>`: plain values still typecheck unchanged.
251
+ */
252
+ export type UpdateInput<T> = {
253
+ [K in keyof T]?: T[K] | UpdateOperatorInput<T[K]>;
254
+ };
114
255
  export interface UpdateArgs<T> {
115
256
  where: WhereClause<T>;
116
- data: Partial<T>;
257
+ data: UpdateInput<T>;
117
258
  /** Query timeout in milliseconds. Rejects with an error if exceeded. */
118
259
  timeout?: number;
260
+ /**
261
+ * Opt in to running this mutation when `where` resolves to an empty
262
+ * predicate (e.g. `{}` or `{ id: undefined }`). Default `false` — an
263
+ * empty predicate throws `ValidationError` to catch the common case of
264
+ * a filter value accidentally being `undefined`. Set this to `true` only
265
+ * when an unconditional mutation is the intended behaviour.
266
+ */
267
+ allowFullTableScan?: boolean;
119
268
  }
120
269
  export interface UpdateManyArgs<T> {
121
270
  where: WhereClause<T>;
122
- data: Partial<T>;
271
+ data: UpdateInput<T>;
123
272
  /** Query timeout in milliseconds. Rejects with an error if exceeded. */
124
273
  timeout?: number;
274
+ /** See {@link UpdateArgs.allowFullTableScan}. */
275
+ allowFullTableScan?: boolean;
125
276
  }
126
277
  export interface DeleteArgs<T> {
127
278
  where: WhereClause<T>;
128
279
  /** Query timeout in milliseconds. Rejects with an error if exceeded. */
129
280
  timeout?: number;
281
+ /** See {@link UpdateArgs.allowFullTableScan}. */
282
+ allowFullTableScan?: boolean;
130
283
  }
131
284
  export interface DeleteManyArgs<T> {
132
285
  where: WhereClause<T>;
133
286
  /** Query timeout in milliseconds. Rejects with an error if exceeded. */
134
287
  timeout?: number;
288
+ /** See {@link UpdateArgs.allowFullTableScan}. */
289
+ allowFullTableScan?: boolean;
135
290
  }
136
291
  export interface UpsertArgs<T> {
137
292
  where: WhereClause<T>;
@@ -158,8 +313,6 @@ export interface GroupByArgs<T> {
158
313
  _min?: Partial<Record<keyof T & string, boolean>>;
159
314
  /** Maximum value of fields in each group */
160
315
  _max?: Partial<Record<keyof T & string, boolean>>;
161
- /** Having clause for filtering groups */
162
- having?: Record<string, unknown>;
163
316
  /** Order groups */
164
317
  orderBy?: Record<string, OrderDirection>;
165
318
  /** Query timeout in milliseconds. Rejects with an error if exceeded. */
@@ -241,10 +394,16 @@ type MiddlewareFn = (params: {
241
394
  export interface QueryInterfaceOptions {
242
395
  /** Default LIMIT applied to findMany() when no limit is specified */
243
396
  defaultLimit?: number;
244
- /** Log a warning when findMany() is called without a limit */
397
+ /**
398
+ * Log a one-time warning when {@link QueryInterface.findMany} is called
399
+ * without a `limit`. Defaults to `true` so that accidental unbounded
400
+ * queries are surfaced loudly during development. Pass `false` to silence
401
+ * the warning entirely (e.g. for CLI tooling that intentionally streams
402
+ * full tables).
403
+ */
245
404
  warnOnUnlimited?: boolean;
246
405
  }
247
- export declare class QueryInterface<T extends object> {
406
+ export declare class QueryInterface<T extends object, R extends object = {}> {
248
407
  private readonly pool;
249
408
  private readonly table;
250
409
  private readonly schema;
@@ -254,13 +413,29 @@ export declare class QueryInterface<T extends object> {
254
413
  private readonly middlewares;
255
414
  private readonly defaultLimit?;
256
415
  private readonly warnOnUnlimited;
416
+ /**
417
+ * Tracks tables that have already triggered an unlimited-query warning so
418
+ * the user is not spammed once per row. Per-instance state — each
419
+ * QueryInterface is bound to a single table, so this set will only ever
420
+ * contain at most one entry, but using a Set keeps the API consistent with
421
+ * the audit's "Set<string>" guidance and leaves room for future
422
+ * cross-table sharing.
423
+ */
424
+ private readonly warnedTables;
257
425
  /** Pre-computed column type lookups (avoids linear scans per query) */
258
426
  private readonly columnPgTypeMap;
259
427
  private readonly columnArrayTypeMap;
260
428
  constructor(pool: pg.Pool, table: string, schema: SchemaMetadata, middlewares?: MiddlewareFn[], options?: QueryInterfaceOptions);
429
+ /**
430
+ * Reset the per-instance unlimited-query warning dedupe set.
431
+ * Exposed for tests so a single test process can verify the warning fires
432
+ * exactly once per table without bleeding state between assertions.
433
+ */
434
+ resetUnlimitedWarnings(): void;
261
435
  /**
262
436
  * Execute a pool.query with an optional timeout.
263
437
  * If timeout is set, races the query against a timer and rejects on expiry.
438
+ * pg driver errors are translated to typed Turbine errors via wrapPgError.
264
439
  */
265
440
  private queryWithTimeout;
266
441
  /**
@@ -273,21 +448,43 @@ export declare class QueryInterface<T extends object> {
273
448
  * To intercept queries before SQL generation, use the raw() method instead.
274
449
  */
275
450
  private executeWithMiddleware;
451
+ findUnique<W extends TypedWithClause<R> = {}>(args: FindUniqueArgs<T, R, W>): Promise<WithResult<T, R, W> | null>;
452
+ buildFindUnique<W extends TypedWithClause<R> = {}>(args: FindUniqueArgs<T, R, W>): DeferredQuery<T | null>;
453
+ findMany<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): Promise<WithResult<T, R, W>[]>;
276
454
  /**
277
- * Generate a cache key for a query shape.
278
- * Same where-keys + same with-clause = same SQL template.
455
+ * Emit a one-time `console.warn` when {@link findMany} is called without an
456
+ * explicit `limit`/`take` and `warnOnUnlimited` has not been disabled.
457
+ *
458
+ * Deduped per QueryInterface instance via {@link warnedTables} so a busy
459
+ * loop calling `db.users.findMany()` thousands of times only logs once.
460
+ * Suppressed when `defaultLimit` is configured (the caller has already
461
+ * opted in to a bounded query) and when the user passed an explicit
462
+ * `limit`, `take`, or `cursor`.
463
+ */
464
+ private maybeWarnUnlimited;
465
+ buildFindMany<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): DeferredQuery<T[]>;
466
+ /**
467
+ * Stream rows from a findMany query using PostgreSQL cursors.
468
+ * Returns an AsyncIterable that yields individual rows, fetching in batches internally.
469
+ *
470
+ * Uses DECLARE CURSOR within a dedicated transaction on a single pooled connection.
471
+ * The cursor is automatically closed and the connection released when iteration
472
+ * completes or is terminated early (e.g. `break` from `for await`).
473
+ *
474
+ * @example
475
+ * ```ts
476
+ * for await (const user of db.users.findManyStream({ where: { orgId: 1 }, batchSize: 500 })) {
477
+ * process.stdout.write(`${user.email}\n`);
478
+ * }
479
+ * ```
279
480
  */
280
- private cacheKey;
281
- findUnique(args: FindUniqueArgs<T>): Promise<T | null>;
282
- buildFindUnique(args: FindUniqueArgs<T>): DeferredQuery<T | null>;
283
- findMany(args?: FindManyArgs<T>): Promise<T[]>;
284
- buildFindMany(args?: FindManyArgs<T>): DeferredQuery<T[]>;
285
- findFirst(args?: FindManyArgs<T>): Promise<T | null>;
286
- buildFindFirst(args?: FindManyArgs<T>): DeferredQuery<T | null>;
287
- findFirstOrThrow(args?: FindManyArgs<T>): Promise<T>;
288
- buildFindFirstOrThrow(args?: FindManyArgs<T>): DeferredQuery<T>;
289
- findUniqueOrThrow(args: FindUniqueArgs<T>): Promise<T>;
290
- buildFindUniqueOrThrow(args: FindUniqueArgs<T>): DeferredQuery<T>;
481
+ findManyStream<W extends TypedWithClause<R> = {}>(args?: FindManyStreamArgs<T, R, W>): AsyncGenerator<WithResult<T, R, W>, void, undefined>;
482
+ findFirst<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): Promise<WithResult<T, R, W> | null>;
483
+ buildFindFirst<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): DeferredQuery<T | null>;
484
+ findFirstOrThrow<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): Promise<WithResult<T, R, W>>;
485
+ buildFindFirstOrThrow<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): DeferredQuery<T>;
486
+ findUniqueOrThrow<W extends TypedWithClause<R> = {}>(args: FindUniqueArgs<T, R, W>): Promise<WithResult<T, R, W>>;
487
+ buildFindUniqueOrThrow<W extends TypedWithClause<R> = {}>(args: FindUniqueArgs<T, R, W>): DeferredQuery<T>;
291
488
  create(args: CreateArgs<T>): Promise<T>;
292
489
  buildCreate(args: CreateArgs<T>): DeferredQuery<T>;
293
490
  createMany(args: CreateManyArgs<T>): Promise<T[]>;
@@ -325,8 +522,33 @@ export declare class QueryInterface<T extends object> {
325
522
  private toColumn;
326
523
  /** Convert camelCase field name to a double-quoted SQL identifier */
327
524
  private toSqlColumn;
525
+ /**
526
+ * Build a single SET clause entry for update/updateMany.
527
+ *
528
+ * Supports plain values and atomic operator objects ({ set, increment,
529
+ * decrement, multiply, divide }). An operator object is detected ONLY when
530
+ * it has EXACTLY one key that is one of the 5 operator keys — this avoids
531
+ * misinterpreting JSON column values like `{ set: 'x' }` as operators
532
+ * (real operator objects always have exactly one key, and a plain JSON
533
+ * payload that happens to have a single `set` key is extremely unusual).
534
+ * Multi-key objects are always treated as plain (JSON) values.
535
+ *
536
+ * Returns the SQL fragment (e.g., `"view_count" = "view_count" + $3`) and
537
+ * pushes any required params onto the shared params array so that WHERE
538
+ * clause numbering continues correctly afterward.
539
+ */
540
+ private buildSetClause;
328
541
  /** Build WHERE clause from a where object (supports operators, NULL, OR) */
329
542
  private buildWhere;
543
+ /**
544
+ * Refuse mutations with an empty predicate unless explicitly opted in.
545
+ *
546
+ * An empty `where` (e.g. `{}` or `{ id: undefined }`) resolves to a
547
+ * mutation with no filter — a common footgun when a caller's filter
548
+ * value accidentally resolves to `undefined`. This guard throws
549
+ * `ValidationError` in that case unless `allowFullTableScan: true`.
550
+ */
551
+ private assertMutationHasPredicate;
330
552
  /**
331
553
  * Build the inner WHERE expression (without the WHERE keyword).
332
554
  * Returns null if no conditions exist.
@@ -355,24 +577,138 @@ export declare class QueryInterface<T extends object> {
355
577
  /** Parse a row that may contain JSON nested relation columns */
356
578
  private parseNestedRow;
357
579
  /**
358
- * Build a SELECT clause with nested relation subqueries.
580
+ * Build a SELECT clause that includes both base columns and nested relation subqueries.
581
+ *
582
+ * For each relation specified in the `with` clause, this method generates a correlated
583
+ * subquery using PostgreSQL's `json_agg(json_build_object(...))` pattern. The result
584
+ * is a single SQL SELECT clause that resolves the full object tree in one query --
585
+ * no N+1 problem.
359
586
  *
360
- * Uses json_build_object + json_agg — the same approach as the raw-pg benchmark
361
- * queries. Generates a single SQL statement that resolves the full object tree.
587
+ * **How it works:**
588
+ * 1. Resolves the base columns for the root table (all columns, or a subset via `columnsList`).
589
+ * 2. Iterates over each key in the `with` clause, looking up the relation definition.
590
+ * 3. For each relation, delegates to {@link buildRelationSubquery} to generate a
591
+ * correlated subquery that returns JSON (array for hasMany, object for belongsTo/hasOne).
592
+ * 4. Each subquery is aliased as the relation name in the final SELECT.
362
593
  *
363
- * Nested where values are parameterized via the shared params array to prevent
364
- * SQL injection.
594
+ * **aliasCounter:** A shared `{ n: number }` object is passed through all nesting levels.
595
+ * Each call to `buildRelationSubquery` increments it to produce unique table aliases
596
+ * (`t0`, `t1`, `t2`, ...) across arbitrarily deep relation trees, preventing alias
597
+ * collisions in the generated SQL.
598
+ *
599
+ * **Example output:**
600
+ * ```sql
601
+ * "users"."id", "users"."name", "users"."email",
602
+ * (SELECT COALESCE(json_agg(json_build_object('id', t0."id", 'title', t0."title")), '[]'::json)
603
+ * FROM "posts" t0 WHERE t0."user_id" = "users"."id") AS "posts"
604
+ * ```
605
+ *
606
+ * @param table - The root table name (e.g. `"users"`).
607
+ * @param withClause - An object mapping relation names to their include specs
608
+ * (`true` for default inclusion, or `WithOptions` for select/omit/where/orderBy/limit).
609
+ * @param params - Shared parameter array for parameterized values (`$1`, `$2`, ...).
610
+ * Nested where/limit values are pushed here to prevent SQL injection.
611
+ * @param columnsList - Optional subset of columns to include in the SELECT. When `null`
612
+ * or omitted, all columns from the table's schema metadata are used.
613
+ * @param depth - Current nesting depth, passed through to {@link buildRelationSubquery}
614
+ * for circular-relation detection. Defaults to `0` at the top level.
615
+ * @param path - Breadcrumb trail of relation names traversed so far, used in error
616
+ * messages when circular or too-deep nesting is detected.
617
+ * @returns A complete SELECT clause string (without the `SELECT` keyword) containing
618
+ * base columns and relation subqueries.
365
619
  */
366
620
  private buildSelectWithRelations;
367
621
  /**
368
- * Build a json_agg subquery for a relation.
622
+ * Generate a correlated subquery that returns JSON for a single relation.
623
+ *
624
+ * This is the core of Turbine's single-query nested relation strategy. For a given
625
+ * relation (e.g. `posts` on a `users` query), it produces a self-contained SQL subquery
626
+ * that PostgreSQL evaluates per parent row, returning either a JSON array (hasMany) or
627
+ * a single JSON object (belongsTo / hasOne).
628
+ *
629
+ * ### Algorithm overview
630
+ *
631
+ * 1. **Alias generation:** Allocates a unique alias (`t0`, `t1`, ...) from the shared
632
+ * `aliasCounter` so that deeply nested subqueries never collide.
633
+ *
634
+ * 2. **Column resolution:** Honors `select` / `omit` options to control which columns
635
+ * appear in the output JSON.
636
+ *
637
+ * 3. **`json_build_object`:** Builds a JSON object for each row by mapping camelCase
638
+ * field names to their column values:
639
+ * ```sql
640
+ * json_build_object('id', t0."id", 'title', t0."title", 'createdAt', t0."created_at")
641
+ * ```
642
+ *
643
+ * 4. **`json_agg` wrapping (hasMany):** For one-to-many relations, wraps the
644
+ * `json_build_object` call in `json_agg(...)` to aggregate all matching child rows
645
+ * into a JSON array. Uses `COALESCE(..., '[]'::json)` so the result is never NULL.
646
+ * For belongsTo / hasOne, no aggregation is used -- just the single JSON object
647
+ * with `LIMIT 1`.
648
+ *
649
+ * 5. **Correlation (WHERE clause):** Links the subquery to the parent row:
650
+ * - **hasMany:** `alias.foreignKey = parentRef.referenceKey`
651
+ * (e.g. `t0."user_id" = "users"."id"` -- child FK points to parent PK)
652
+ * - **belongsTo / hasOne:** `alias.referenceKey = parentRef.foreignKey`
653
+ * (e.g. `t0."id" = "posts"."author_id"` -- parent FK points to child PK)
654
+ *
655
+ * 6. **Recursion:** If the spec includes a nested `with` clause, this method calls
656
+ * itself recursively for each nested relation, passing the current alias as
657
+ * `parentRef`. The nested subquery appears as an additional key in the
658
+ * `json_build_object` call, wrapped in `COALESCE(..., '[]'::json)`.
659
+ * Depth is incremented and capped at 10 to guard against circular relations.
660
+ *
661
+ * 7. **LIMIT / ORDER BY wrapping:** For hasMany relations with `limit` or `orderBy`,
662
+ * the query is restructured into a two-level form:
663
+ * ```sql
664
+ * SELECT COALESCE(json_agg(json_build_object(...)), '[]'::json)
665
+ * FROM (
666
+ * SELECT t0.* FROM "posts" t0
667
+ * WHERE t0."user_id" = "users"."id"
668
+ * ORDER BY t0."created_at" DESC
669
+ * LIMIT $1
670
+ * ) t0i
671
+ * ```
672
+ * This ensures LIMIT and ORDER BY apply to the raw rows *before* `json_agg`
673
+ * aggregation. Without the inner subquery, LIMIT would be meaningless because
674
+ * `json_agg` produces a single aggregated row.
675
+ *
676
+ * 8. **Parameter threading:** All user-supplied values (where filters, limit) are
677
+ * pushed to the shared `params` array with `$N` placeholders. No string
678
+ * interpolation of user data ever occurs -- all identifiers go through
679
+ * `quoteIdent()` and all values are parameterized.
369
680
  *
370
- * All user-supplied values in nested where clauses are parameterized
371
- * through the shared params array — no string interpolation.
681
+ * ### Example output (hasMany with nested relation)
682
+ * ```sql
683
+ * SELECT COALESCE(json_agg(json_build_object(
684
+ * 'id', t0."id",
685
+ * 'title', t0."title",
686
+ * 'comments', COALESCE((
687
+ * SELECT COALESCE(json_agg(json_build_object('id', t1."id", 'body', t1."body")), '[]'::json)
688
+ * FROM "comments" t1 WHERE t1."post_id" = t0."id"
689
+ * ), '[]'::json)
690
+ * )), '[]'::json) FROM "posts" t0 WHERE t0."user_id" = "users"."id"
691
+ * ```
372
692
  *
373
- * @param parentRef - The alias (or table name) of the parent in the outer query.
374
- * Used for the correlated WHERE clause: `child.fk = parentRef.pk`.
375
- * @param aliasCounter - Shared counter for generating unique aliases across nested levels.
693
+ * @param relDef - The relation definition from schema metadata (contains `to`, `type`,
694
+ * `foreignKey`, `referenceKey`).
695
+ * @param spec - Either `true` (include with defaults) or a `WithOptions` object that
696
+ * can specify `select`, `omit`, `where`, `orderBy`, `limit`, and nested `with`.
697
+ * @param params - Shared parameter array. User-supplied values are pushed here and
698
+ * referenced as `$1`, `$2`, etc. in the generated SQL.
699
+ * @param parentRef - The alias (e.g. `"t0"`) or table name (e.g. `"users"`) of the
700
+ * parent query. Used to build the correlated WHERE clause that ties
701
+ * child rows to their parent row.
702
+ * @param aliasCounter - Shared mutable counter (`{ n: number }`) for generating unique
703
+ * table aliases (`t0`, `t1`, `t2`, ...) across all nesting levels.
704
+ * Each call increments `n` by 1.
705
+ * @param depth - Current nesting depth (starts at `0`). Incremented on each recursive
706
+ * call. If it reaches 10, a {@link CircularRelationError} is thrown.
707
+ * @param path - Breadcrumb trail of relation/table names traversed so far
708
+ * (e.g. `["users", "posts", "comments"]`). Used in the error message
709
+ * when circular or too-deep nesting is detected.
710
+ * @returns A complete SQL subquery string (without surrounding parentheses) that
711
+ * evaluates to a JSON array (hasMany) or a JSON object (belongsTo/hasOne).
376
712
  */
377
713
  private buildRelationSubquery;
378
714
  /**