turbine-orm 0.4.0 → 0.7.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 (57) hide show
  1. package/README.md +243 -26
  2. package/dist/cjs/cli/config.js +151 -0
  3. package/dist/cjs/cli/index.js +1176 -0
  4. package/dist/cjs/cli/migrate.js +446 -0
  5. package/dist/cjs/cli/ui.js +233 -0
  6. package/dist/cjs/client.js +512 -0
  7. package/dist/cjs/errors.js +293 -0
  8. package/dist/cjs/generate.js +321 -0
  9. package/dist/cjs/index.js +94 -0
  10. package/dist/cjs/introspect.js +287 -0
  11. package/dist/cjs/package.json +1 -0
  12. package/dist/cjs/pipeline.js +78 -0
  13. package/dist/cjs/query.js +1891 -0
  14. package/dist/cjs/schema-builder.js +238 -0
  15. package/dist/cjs/schema-sql.js +509 -0
  16. package/dist/cjs/schema.js +140 -0
  17. package/dist/cjs/serverless.js +110 -0
  18. package/dist/cli/config.js +6 -16
  19. package/dist/cli/index.js +256 -49
  20. package/dist/cli/migrate.d.ts +35 -6
  21. package/dist/cli/migrate.js +124 -76
  22. package/dist/cli/ui.js +5 -9
  23. package/dist/client.d.ts +87 -3
  24. package/dist/client.js +122 -46
  25. package/dist/errors.d.ts +138 -0
  26. package/dist/errors.js +278 -0
  27. package/dist/generate.js +37 -11
  28. package/dist/index.d.ts +10 -8
  29. package/dist/index.js +15 -11
  30. package/dist/introspect.js +3 -5
  31. package/dist/pipeline.js +8 -1
  32. package/dist/query.d.ts +310 -45
  33. package/dist/query.js +565 -237
  34. package/dist/schema-builder.js +91 -23
  35. package/dist/schema-sql.d.ts +6 -2
  36. package/dist/schema-sql.js +180 -26
  37. package/dist/schema.js +4 -1
  38. package/dist/serverless.d.ts +91 -139
  39. package/dist/serverless.js +86 -173
  40. package/package.json +44 -21
  41. package/dist/cli/config.d.ts.map +0 -1
  42. package/dist/cli/index.d.ts.map +0 -1
  43. package/dist/cli/migrate.d.ts.map +0 -1
  44. package/dist/cli/ui.d.ts.map +0 -1
  45. package/dist/client.d.ts.map +0 -1
  46. package/dist/generate.d.ts.map +0 -1
  47. package/dist/index.d.ts.map +0 -1
  48. package/dist/introspect.d.ts.map +0 -1
  49. package/dist/pipeline.d.ts.map +0 -1
  50. package/dist/query.d.ts.map +0 -1
  51. package/dist/schema-builder.d.ts.map +0 -1
  52. package/dist/schema-sql.d.ts.map +0 -1
  53. package/dist/schema.d.ts.map +0 -1
  54. package/dist/serverless.d.ts.map +0 -1
  55. package/dist/types.d.ts +0 -93
  56. package/dist/types.d.ts.map +0 -1
  57. package/dist/types.js +0 -126
package/dist/query.d.ts CHANGED
@@ -5,7 +5,7 @@
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.
@@ -35,6 +35,8 @@ export interface WhereOperator<V = unknown> {
35
35
  contains?: string;
36
36
  startsWith?: string;
37
37
  endsWith?: string;
38
+ /** Set to 'insensitive' to use ILIKE instead of LIKE for string comparisons */
39
+ mode?: 'default' | 'insensitive';
38
40
  }
39
41
  /**
40
42
  * A where value can be:
@@ -59,10 +61,22 @@ export type WhereClause<T> = {
59
61
  /** Relation filters — keyed by relation name, value is { some, every, none } */
60
62
  [relationName: string]: unknown;
61
63
  };
64
+ /**
65
+ * Unparameterized with clause — accepts any relation name.
66
+ * Used internally by the query builder at runtime.
67
+ */
62
68
  export interface WithClause {
63
69
  [relation: string]: true | WithOptions;
64
70
  }
65
- export interface WithOptions {
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
+ export type TypedWithClause<R extends object = {}> = [keyof R] extends [never] ? WithClause : {
77
+ [K in keyof R]?: true | WithOptions;
78
+ };
79
+ export interface WithOptions<_T = unknown> {
66
80
  with?: WithClause;
67
81
  where?: Record<string, unknown>;
68
82
  orderBy?: Record<string, OrderDirection>;
@@ -72,56 +86,136 @@ export interface WithOptions {
72
86
  /** Exclude these fields from the relation */
73
87
  omit?: Record<string, boolean>;
74
88
  }
75
- export interface FindUniqueArgs<T> {
89
+ /**
90
+ * Compute the result type when relations are included via `with`.
91
+ *
92
+ * - When R is the default ({}) or W is empty, resolves to plain T.
93
+ * - When R is a real relations map and W specifies relation keys, merges matching
94
+ * relation types from R onto T.
95
+ *
96
+ * @typeParam T - Base entity type (e.g. User)
97
+ * @typeParam R - Relations map (e.g. { posts: Post[]; profile: Profile | null })
98
+ * @typeParam W - The with clause object (e.g. { posts: true })
99
+ */
100
+ export type WithResult<T, R extends object, W> = [keyof R] extends [never] ? T : [keyof W & keyof R] extends [never] ? T : T & Pick<R, keyof W & keyof R>;
101
+ export interface FindUniqueArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>> {
76
102
  where: WhereClause<T>;
77
103
  select?: Record<string, boolean>;
78
104
  omit?: Record<string, boolean>;
79
- with?: WithClause;
105
+ with?: W;
106
+ /** Query timeout in milliseconds. Rejects with an error if exceeded. */
107
+ timeout?: number;
80
108
  }
81
- export interface FindManyArgs<T> {
109
+ export interface FindManyArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>> {
82
110
  where?: WhereClause<T>;
83
111
  select?: Record<string, boolean>;
84
112
  omit?: Record<string, boolean>;
85
113
  orderBy?: Record<string, OrderDirection>;
86
114
  limit?: number;
87
115
  offset?: number;
88
- with?: WithClause;
116
+ with?: W;
89
117
  /** Cursor-based pagination: start after this row */
90
118
  cursor?: Partial<T>;
91
119
  /** Number of records to take (used with cursor) */
92
120
  take?: number;
93
121
  /** De-duplicate results by specified fields */
94
122
  distinct?: (keyof T & string)[];
123
+ /** Query timeout in milliseconds. Rejects with an error if exceeded. */
124
+ timeout?: number;
125
+ }
126
+ export interface FindManyStreamArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>> extends FindManyArgs<T, R, W> {
127
+ /** Number of rows to fetch per internal FETCH batch (default: 100) */
128
+ batchSize?: number;
95
129
  }
96
130
  export interface CreateArgs<T> {
97
131
  data: Partial<T>;
132
+ /** Query timeout in milliseconds. Rejects with an error if exceeded. */
133
+ timeout?: number;
98
134
  }
99
135
  export interface CreateManyArgs<T> {
100
136
  data: Partial<T>[];
101
137
  /** When true, adds ON CONFLICT DO NOTHING to skip duplicate rows */
102
138
  skipDuplicates?: boolean;
139
+ /** Query timeout in milliseconds. Rejects with an error if exceeded. */
140
+ timeout?: number;
103
141
  }
142
+ /**
143
+ * Atomic update operators for a field.
144
+ *
145
+ * `set` works on any type; `increment`, `decrement`, `multiply`, and `divide`
146
+ * are only valid on numeric fields. They generate SQL like
147
+ * `col = col + $n` (and the corresponding `-`, `*`, `/` variants) instead of
148
+ * plain absolute assignments, so they are safe against concurrent writers —
149
+ * the database performs the math atomically.
150
+ *
151
+ * @example
152
+ * db.posts.update({ where: { id: 5 }, data: { viewCount: { increment: 1 } } });
153
+ */
154
+ export type UpdateOperatorInput<V> = {
155
+ set: V;
156
+ } | (V extends number ? {
157
+ increment: number;
158
+ } : never) | (V extends number ? {
159
+ decrement: number;
160
+ } : never) | (V extends number ? {
161
+ multiply: number;
162
+ } : never) | (V extends number ? {
163
+ divide: number;
164
+ } : never);
165
+ /**
166
+ * Update data — each field can be a plain value or an atomic operator object.
167
+ * Back-compatible with `Partial<T>`: plain values still typecheck unchanged.
168
+ */
169
+ export type UpdateInput<T> = {
170
+ [K in keyof T]?: T[K] | UpdateOperatorInput<T[K]>;
171
+ };
104
172
  export interface UpdateArgs<T> {
105
173
  where: WhereClause<T>;
106
- data: Partial<T>;
174
+ data: UpdateInput<T>;
175
+ /** Query timeout in milliseconds. Rejects with an error if exceeded. */
176
+ timeout?: number;
177
+ /**
178
+ * Opt in to running this mutation when `where` resolves to an empty
179
+ * predicate (e.g. `{}` or `{ id: undefined }`). Default `false` — an
180
+ * empty predicate throws `ValidationError` to catch the common case of
181
+ * a filter value accidentally being `undefined`. Set this to `true` only
182
+ * when an unconditional mutation is the intended behaviour.
183
+ */
184
+ allowFullTableScan?: boolean;
107
185
  }
108
186
  export interface UpdateManyArgs<T> {
109
187
  where: WhereClause<T>;
110
- data: Partial<T>;
188
+ data: UpdateInput<T>;
189
+ /** Query timeout in milliseconds. Rejects with an error if exceeded. */
190
+ timeout?: number;
191
+ /** See {@link UpdateArgs.allowFullTableScan}. */
192
+ allowFullTableScan?: boolean;
111
193
  }
112
194
  export interface DeleteArgs<T> {
113
195
  where: WhereClause<T>;
196
+ /** Query timeout in milliseconds. Rejects with an error if exceeded. */
197
+ timeout?: number;
198
+ /** See {@link UpdateArgs.allowFullTableScan}. */
199
+ allowFullTableScan?: boolean;
114
200
  }
115
201
  export interface DeleteManyArgs<T> {
116
202
  where: WhereClause<T>;
203
+ /** Query timeout in milliseconds. Rejects with an error if exceeded. */
204
+ timeout?: number;
205
+ /** See {@link UpdateArgs.allowFullTableScan}. */
206
+ allowFullTableScan?: boolean;
117
207
  }
118
208
  export interface UpsertArgs<T> {
119
209
  where: WhereClause<T>;
120
210
  create: Partial<T>;
121
211
  update: Partial<T>;
212
+ /** Query timeout in milliseconds. Rejects with an error if exceeded. */
213
+ timeout?: number;
122
214
  }
123
215
  export interface CountArgs<T> {
124
216
  where?: WhereClause<T>;
217
+ /** Query timeout in milliseconds. Rejects with an error if exceeded. */
218
+ timeout?: number;
125
219
  }
126
220
  export interface GroupByArgs<T> {
127
221
  by: (keyof T & string)[];
@@ -136,10 +230,10 @@ export interface GroupByArgs<T> {
136
230
  _min?: Partial<Record<keyof T & string, boolean>>;
137
231
  /** Maximum value of fields in each group */
138
232
  _max?: Partial<Record<keyof T & string, boolean>>;
139
- /** Having clause for filtering groups */
140
- having?: Record<string, unknown>;
141
233
  /** Order groups */
142
234
  orderBy?: Record<string, OrderDirection>;
235
+ /** Query timeout in milliseconds. Rejects with an error if exceeded. */
236
+ timeout?: number;
143
237
  }
144
238
  /** Arguments for the standalone aggregate method */
145
239
  export interface AggregateArgs<T> {
@@ -154,6 +248,8 @@ export interface AggregateArgs<T> {
154
248
  _min?: Partial<Record<keyof T & string, boolean>>;
155
249
  /** Maximum value of fields */
156
250
  _max?: Partial<Record<keyof T & string, boolean>>;
251
+ /** Query timeout in milliseconds. Rejects with an error if exceeded. */
252
+ timeout?: number;
157
253
  }
158
254
  /** Result type for aggregate queries */
159
255
  export interface AggregateResult<T> {
@@ -211,7 +307,14 @@ type MiddlewareFn = (params: {
211
307
  action: string;
212
308
  args: Record<string, unknown>;
213
309
  }) => Promise<unknown>) => Promise<unknown>;
214
- export declare class QueryInterface<T extends object> {
310
+ /** Options passed from TurbineClient to QueryInterface */
311
+ export interface QueryInterfaceOptions {
312
+ /** Default LIMIT applied to findMany() when no limit is specified */
313
+ defaultLimit?: number;
314
+ /** Log a warning when findMany() is called without a limit */
315
+ warnOnUnlimited?: boolean;
316
+ }
317
+ export declare class QueryInterface<T extends object, R extends object = {}> {
215
318
  private readonly pool;
216
319
  private readonly table;
217
320
  private readonly schema;
@@ -219,28 +322,54 @@ export declare class QueryInterface<T extends object> {
219
322
  /** SQL template cache: cacheKey → sql string (params are always positional $1,$2,...) */
220
323
  private readonly sqlCache;
221
324
  private readonly middlewares;
222
- private readonly hasNoDateColumns;
223
- constructor(pool: pg.Pool, table: string, schema: SchemaMetadata, middlewares?: MiddlewareFn[]);
325
+ private readonly defaultLimit?;
326
+ private readonly warnOnUnlimited;
327
+ /** Pre-computed column type lookups (avoids linear scans per query) */
328
+ private readonly columnPgTypeMap;
329
+ private readonly columnArrayTypeMap;
330
+ constructor(pool: pg.Pool, table: string, schema: SchemaMetadata, middlewares?: MiddlewareFn[], options?: QueryInterfaceOptions);
331
+ /**
332
+ * Execute a pool.query with an optional timeout.
333
+ * If timeout is set, races the query against a timer and rejects on expiry.
334
+ * pg driver errors are translated to typed Turbine errors via wrapPgError.
335
+ */
336
+ private queryWithTimeout;
224
337
  /**
225
338
  * Execute a query through the middleware chain.
226
339
  * If no middlewares are registered, executes directly.
340
+ *
341
+ * Middleware can inspect and log query parameters, modify results after execution,
342
+ * and measure timing. Note: query SQL is generated before middleware runs, so
343
+ * modifying params.args in middleware will NOT affect the executed SQL.
344
+ * To intercept queries before SQL generation, use the raw() method instead.
227
345
  */
228
346
  private executeWithMiddleware;
347
+ findUnique<W extends TypedWithClause<R> = {}>(args: FindUniqueArgs<T, R, W>): Promise<WithResult<T, R, W> | null>;
348
+ buildFindUnique<W extends TypedWithClause<R> = {}>(args: FindUniqueArgs<T, R, W>): DeferredQuery<T | null>;
349
+ findMany<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): Promise<WithResult<T, R, W>[]>;
350
+ buildFindMany<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): DeferredQuery<T[]>;
229
351
  /**
230
- * Generate a cache key for a query shape.
231
- * Same where-keys + same with-clause = same SQL template.
352
+ * Stream rows from a findMany query using PostgreSQL cursors.
353
+ * Returns an AsyncIterable that yields individual rows, fetching in batches internally.
354
+ *
355
+ * Uses DECLARE CURSOR within a dedicated transaction on a single pooled connection.
356
+ * The cursor is automatically closed and the connection released when iteration
357
+ * completes or is terminated early (e.g. `break` from `for await`).
358
+ *
359
+ * @example
360
+ * ```ts
361
+ * for await (const user of db.users.findManyStream({ where: { orgId: 1 }, batchSize: 500 })) {
362
+ * process.stdout.write(`${user.email}\n`);
363
+ * }
364
+ * ```
232
365
  */
233
- private cacheKey;
234
- findUnique(args: FindUniqueArgs<T>): Promise<T | null>;
235
- buildFindUnique(args: FindUniqueArgs<T>): DeferredQuery<T | null>;
236
- findMany(args?: FindManyArgs<T>): Promise<T[]>;
237
- buildFindMany(args?: FindManyArgs<T>): DeferredQuery<T[]>;
238
- findFirst(args?: FindManyArgs<T>): Promise<T | null>;
239
- buildFindFirst(args?: FindManyArgs<T>): DeferredQuery<T | null>;
240
- findFirstOrThrow(args?: FindManyArgs<T>): Promise<T>;
241
- buildFindFirstOrThrow(args?: FindManyArgs<T>): DeferredQuery<T>;
242
- findUniqueOrThrow(args: FindUniqueArgs<T>): Promise<T>;
243
- buildFindUniqueOrThrow(args: FindUniqueArgs<T>): DeferredQuery<T>;
366
+ findManyStream<W extends TypedWithClause<R> = {}>(args?: FindManyStreamArgs<T, R, W>): AsyncGenerator<WithResult<T, R, W>, void, undefined>;
367
+ findFirst<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): Promise<WithResult<T, R, W> | null>;
368
+ buildFindFirst<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): DeferredQuery<T | null>;
369
+ findFirstOrThrow<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): Promise<WithResult<T, R, W>>;
370
+ buildFindFirstOrThrow<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): DeferredQuery<T>;
371
+ findUniqueOrThrow<W extends TypedWithClause<R> = {}>(args: FindUniqueArgs<T, R, W>): Promise<WithResult<T, R, W>>;
372
+ buildFindUniqueOrThrow<W extends TypedWithClause<R> = {}>(args: FindUniqueArgs<T, R, W>): DeferredQuery<T>;
244
373
  create(args: CreateArgs<T>): Promise<T>;
245
374
  buildCreate(args: CreateArgs<T>): DeferredQuery<T>;
246
375
  createMany(args: CreateManyArgs<T>): Promise<T[]>;
@@ -278,8 +407,33 @@ export declare class QueryInterface<T extends object> {
278
407
  private toColumn;
279
408
  /** Convert camelCase field name to a double-quoted SQL identifier */
280
409
  private toSqlColumn;
410
+ /**
411
+ * Build a single SET clause entry for update/updateMany.
412
+ *
413
+ * Supports plain values and atomic operator objects ({ set, increment,
414
+ * decrement, multiply, divide }). An operator object is detected ONLY when
415
+ * it has EXACTLY one key that is one of the 5 operator keys — this avoids
416
+ * misinterpreting JSON column values like `{ set: 'x' }` as operators
417
+ * (real operator objects always have exactly one key, and a plain JSON
418
+ * payload that happens to have a single `set` key is extremely unusual).
419
+ * Multi-key objects are always treated as plain (JSON) values.
420
+ *
421
+ * Returns the SQL fragment (e.g., `"view_count" = "view_count" + $3`) and
422
+ * pushes any required params onto the shared params array so that WHERE
423
+ * clause numbering continues correctly afterward.
424
+ */
425
+ private buildSetClause;
281
426
  /** Build WHERE clause from a where object (supports operators, NULL, OR) */
282
427
  private buildWhere;
428
+ /**
429
+ * Refuse mutations with an empty predicate unless explicitly opted in.
430
+ *
431
+ * An empty `where` (e.g. `{}` or `{ id: undefined }`) resolves to a
432
+ * mutation with no filter — a common footgun when a caller's filter
433
+ * value accidentally resolves to `undefined`. This guard throws
434
+ * `ValidationError` in that case unless `allowFullTableScan: true`.
435
+ */
436
+ private assertMutationHasPredicate;
283
437
  /**
284
438
  * Build the inner WHERE expression (without the WHERE keyword).
285
439
  * Returns null if no conditions exist.
@@ -305,39 +459,147 @@ export declare class QueryInterface<T extends object> {
305
459
  private buildOrderBy;
306
460
  /** Parse a flat row: convert snake_case to camelCase + Date coercion */
307
461
  private parseRow;
308
- /**
309
- * Fast path: parse a flat row when the table has NO date columns.
310
- * Only renames snake_case -> camelCase via the pre-computed reverseMap.
311
- * Skips all date coercion checks — avoids Set.has() per field per row.
312
- * Used by findUnique/findMany fast paths when hasNoDateColumns is true.
313
- */
314
- private parseRowFast;
315
462
  /** Parse a row that may contain JSON nested relation columns */
316
463
  private parseNestedRow;
317
464
  /**
318
- * Build a SELECT clause with nested relation subqueries.
465
+ * Build a SELECT clause that includes both base columns and nested relation subqueries.
466
+ *
467
+ * For each relation specified in the `with` clause, this method generates a correlated
468
+ * subquery using PostgreSQL's `json_agg(json_build_object(...))` pattern. The result
469
+ * is a single SQL SELECT clause that resolves the full object tree in one query --
470
+ * no N+1 problem.
471
+ *
472
+ * **How it works:**
473
+ * 1. Resolves the base columns for the root table (all columns, or a subset via `columnsList`).
474
+ * 2. Iterates over each key in the `with` clause, looking up the relation definition.
475
+ * 3. For each relation, delegates to {@link buildRelationSubquery} to generate a
476
+ * correlated subquery that returns JSON (array for hasMany, object for belongsTo/hasOne).
477
+ * 4. Each subquery is aliased as the relation name in the final SELECT.
319
478
  *
320
- * Uses json_build_object + json_agg the same approach as the raw-pg benchmark
321
- * queries. Generates a single SQL statement that resolves the full object tree.
479
+ * **aliasCounter:** A shared `{ n: number }` object is passed through all nesting levels.
480
+ * Each call to `buildRelationSubquery` increments it to produce unique table aliases
481
+ * (`t0`, `t1`, `t2`, ...) across arbitrarily deep relation trees, preventing alias
482
+ * collisions in the generated SQL.
322
483
  *
323
- * Nested where values are parameterized via the shared params array to prevent
324
- * SQL injection.
484
+ * **Example output:**
485
+ * ```sql
486
+ * "users"."id", "users"."name", "users"."email",
487
+ * (SELECT COALESCE(json_agg(json_build_object('id', t0."id", 'title', t0."title")), '[]'::json)
488
+ * FROM "posts" t0 WHERE t0."user_id" = "users"."id") AS "posts"
489
+ * ```
490
+ *
491
+ * @param table - The root table name (e.g. `"users"`).
492
+ * @param withClause - An object mapping relation names to their include specs
493
+ * (`true` for default inclusion, or `WithOptions` for select/omit/where/orderBy/limit).
494
+ * @param params - Shared parameter array for parameterized values (`$1`, `$2`, ...).
495
+ * Nested where/limit values are pushed here to prevent SQL injection.
496
+ * @param columnsList - Optional subset of columns to include in the SELECT. When `null`
497
+ * or omitted, all columns from the table's schema metadata are used.
498
+ * @param depth - Current nesting depth, passed through to {@link buildRelationSubquery}
499
+ * for circular-relation detection. Defaults to `0` at the top level.
500
+ * @param path - Breadcrumb trail of relation names traversed so far, used in error
501
+ * messages when circular or too-deep nesting is detected.
502
+ * @returns A complete SELECT clause string (without the `SELECT` keyword) containing
503
+ * base columns and relation subqueries.
325
504
  */
326
505
  private buildSelectWithRelations;
327
506
  /**
328
- * Build a json_agg subquery for a relation.
507
+ * Generate a correlated subquery that returns JSON for a single relation.
508
+ *
509
+ * This is the core of Turbine's single-query nested relation strategy. For a given
510
+ * relation (e.g. `posts` on a `users` query), it produces a self-contained SQL subquery
511
+ * that PostgreSQL evaluates per parent row, returning either a JSON array (hasMany) or
512
+ * a single JSON object (belongsTo / hasOne).
513
+ *
514
+ * ### Algorithm overview
515
+ *
516
+ * 1. **Alias generation:** Allocates a unique alias (`t0`, `t1`, ...) from the shared
517
+ * `aliasCounter` so that deeply nested subqueries never collide.
518
+ *
519
+ * 2. **Column resolution:** Honors `select` / `omit` options to control which columns
520
+ * appear in the output JSON.
521
+ *
522
+ * 3. **`json_build_object`:** Builds a JSON object for each row by mapping camelCase
523
+ * field names to their column values:
524
+ * ```sql
525
+ * json_build_object('id', t0."id", 'title', t0."title", 'createdAt', t0."created_at")
526
+ * ```
527
+ *
528
+ * 4. **`json_agg` wrapping (hasMany):** For one-to-many relations, wraps the
529
+ * `json_build_object` call in `json_agg(...)` to aggregate all matching child rows
530
+ * into a JSON array. Uses `COALESCE(..., '[]'::json)` so the result is never NULL.
531
+ * For belongsTo / hasOne, no aggregation is used -- just the single JSON object
532
+ * with `LIMIT 1`.
329
533
  *
330
- * All user-supplied values in nested where clauses are parameterized
331
- * through the shared params array — no string interpolation.
534
+ * 5. **Correlation (WHERE clause):** Links the subquery to the parent row:
535
+ * - **hasMany:** `alias.foreignKey = parentRef.referenceKey`
536
+ * (e.g. `t0."user_id" = "users"."id"` -- child FK points to parent PK)
537
+ * - **belongsTo / hasOne:** `alias.referenceKey = parentRef.foreignKey`
538
+ * (e.g. `t0."id" = "posts"."author_id"` -- parent FK points to child PK)
332
539
  *
333
- * @param parentRef - The alias (or table name) of the parent in the outer query.
334
- * Used for the correlated WHERE clause: `child.fk = parentRef.pk`.
335
- * @param aliasCounter - Shared counter for generating unique aliases across nested levels.
540
+ * 6. **Recursion:** If the spec includes a nested `with` clause, this method calls
541
+ * itself recursively for each nested relation, passing the current alias as
542
+ * `parentRef`. The nested subquery appears as an additional key in the
543
+ * `json_build_object` call, wrapped in `COALESCE(..., '[]'::json)`.
544
+ * Depth is incremented and capped at 10 to guard against circular relations.
545
+ *
546
+ * 7. **LIMIT / ORDER BY wrapping:** For hasMany relations with `limit` or `orderBy`,
547
+ * the query is restructured into a two-level form:
548
+ * ```sql
549
+ * SELECT COALESCE(json_agg(json_build_object(...)), '[]'::json)
550
+ * FROM (
551
+ * SELECT t0.* FROM "posts" t0
552
+ * WHERE t0."user_id" = "users"."id"
553
+ * ORDER BY t0."created_at" DESC
554
+ * LIMIT $1
555
+ * ) t0i
556
+ * ```
557
+ * This ensures LIMIT and ORDER BY apply to the raw rows *before* `json_agg`
558
+ * aggregation. Without the inner subquery, LIMIT would be meaningless because
559
+ * `json_agg` produces a single aggregated row.
560
+ *
561
+ * 8. **Parameter threading:** All user-supplied values (where filters, limit) are
562
+ * pushed to the shared `params` array with `$N` placeholders. No string
563
+ * interpolation of user data ever occurs -- all identifiers go through
564
+ * `quoteIdent()` and all values are parameterized.
565
+ *
566
+ * ### Example output (hasMany with nested relation)
567
+ * ```sql
568
+ * SELECT COALESCE(json_agg(json_build_object(
569
+ * 'id', t0."id",
570
+ * 'title', t0."title",
571
+ * 'comments', COALESCE((
572
+ * SELECT COALESCE(json_agg(json_build_object('id', t1."id", 'body', t1."body")), '[]'::json)
573
+ * FROM "comments" t1 WHERE t1."post_id" = t0."id"
574
+ * ), '[]'::json)
575
+ * )), '[]'::json) FROM "posts" t0 WHERE t0."user_id" = "users"."id"
576
+ * ```
577
+ *
578
+ * @param relDef - The relation definition from schema metadata (contains `to`, `type`,
579
+ * `foreignKey`, `referenceKey`).
580
+ * @param spec - Either `true` (include with defaults) or a `WithOptions` object that
581
+ * can specify `select`, `omit`, `where`, `orderBy`, `limit`, and nested `with`.
582
+ * @param params - Shared parameter array. User-supplied values are pushed here and
583
+ * referenced as `$1`, `$2`, etc. in the generated SQL.
584
+ * @param parentRef - The alias (e.g. `"t0"`) or table name (e.g. `"users"`) of the
585
+ * parent query. Used to build the correlated WHERE clause that ties
586
+ * child rows to their parent row.
587
+ * @param aliasCounter - Shared mutable counter (`{ n: number }`) for generating unique
588
+ * table aliases (`t0`, `t1`, `t2`, ...) across all nesting levels.
589
+ * Each call increments `n` by 1.
590
+ * @param depth - Current nesting depth (starts at `0`). Incremented on each recursive
591
+ * call. If it reaches 10, a {@link CircularRelationError} is thrown.
592
+ * @param path - Breadcrumb trail of relation/table names traversed so far
593
+ * (e.g. `["users", "posts", "comments"]`). Used in the error message
594
+ * when circular or too-deep nesting is detected.
595
+ * @returns A complete SQL subquery string (without surrounding parentheses) that
596
+ * evaluates to a JSON array (hasMany) or a JSON object (belongsTo/hasOne).
336
597
  */
337
598
  private buildRelationSubquery;
338
599
  /**
339
600
  * Get the Postgres type for a column (e.g. 'jsonb', 'text', '_int4').
340
601
  * Used to detect JSONB/array columns for specialized operators.
602
+ * Uses pre-computed Map for O(1) lookup instead of linear scan.
341
603
  */
342
604
  private getColumnPgType;
343
605
  /**
@@ -355,7 +617,10 @@ export declare class QueryInterface<T extends object> {
355
617
  * Supports: has, hasEvery, hasSome, isEmpty.
356
618
  */
357
619
  private buildArrayFilterClauses;
358
- /** Get the Postgres array type for a column (used by UNNEST in createMany) */
620
+ /**
621
+ * Get the Postgres array type for a column (used by UNNEST in createMany).
622
+ * Uses pre-computed Map for O(1) lookup instead of linear scan.
623
+ */
359
624
  private getColumnArrayType;
360
625
  }
361
626
  export {};