turbine-orm 0.9.1 → 0.10.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 (49) hide show
  1. package/README.md +35 -12
  2. package/dist/adapters/cockroachdb.d.ts +40 -0
  3. package/dist/adapters/cockroachdb.js +172 -0
  4. package/dist/adapters/index.d.ts +107 -0
  5. package/dist/adapters/index.js +83 -0
  6. package/dist/adapters/yugabytedb.d.ts +52 -0
  7. package/dist/adapters/yugabytedb.js +156 -0
  8. package/dist/cjs/adapters/cockroachdb.js +174 -0
  9. package/dist/cjs/adapters/index.js +87 -0
  10. package/dist/cjs/adapters/yugabytedb.js +158 -0
  11. package/dist/cjs/cli/index.js +2 -1
  12. package/dist/cjs/cli/migrate.js +18 -12
  13. package/dist/cjs/cli/studio.js +12 -11
  14. package/dist/cjs/client.js +3 -3
  15. package/dist/cjs/generate.js +8 -1
  16. package/dist/cjs/index.js +10 -3
  17. package/dist/cjs/introspect.js +46 -18
  18. package/dist/cjs/query/builder.js +2658 -0
  19. package/dist/cjs/query/index.js +21 -0
  20. package/dist/cjs/query/types.js +7 -0
  21. package/dist/cjs/query/utils.js +140 -0
  22. package/dist/cjs/schema-sql.js +26 -26
  23. package/dist/cjs/schema.js +8 -0
  24. package/dist/cli/config.d.ts +11 -0
  25. package/dist/cli/index.js +2 -1
  26. package/dist/cli/migrate.d.ts +3 -0
  27. package/dist/cli/migrate.js +17 -11
  28. package/dist/cli/studio.d.ts +4 -0
  29. package/dist/cli/studio.js +6 -5
  30. package/dist/client.d.ts +1 -1
  31. package/dist/client.js +1 -1
  32. package/dist/generate.js +8 -1
  33. package/dist/index.d.ts +4 -2
  34. package/dist/index.js +3 -2
  35. package/dist/introspect.js +46 -18
  36. package/dist/pipeline-submittable.d.ts +1 -1
  37. package/dist/pipeline.d.ts +1 -1
  38. package/dist/query/builder.d.ts +498 -0
  39. package/dist/query/builder.js +2655 -0
  40. package/dist/query/index.d.ts +13 -0
  41. package/dist/query/index.js +10 -0
  42. package/dist/query/types.d.ts +365 -0
  43. package/dist/query/types.js +7 -0
  44. package/dist/query/utils.d.ts +68 -0
  45. package/dist/query/utils.js +131 -0
  46. package/dist/schema-sql.js +1 -1
  47. package/dist/schema.d.ts +6 -4
  48. package/dist/schema.js +7 -0
  49. package/package.json +14 -2
@@ -66,13 +66,16 @@ const SQL_FOREIGN_KEYS = `
66
66
  const SQL_UNIQUE_CONSTRAINTS = `
67
67
  SELECT
68
68
  tc.table_name,
69
- kcu.column_name
69
+ tc.constraint_name,
70
+ kcu.column_name,
71
+ kcu.ordinal_position
70
72
  FROM information_schema.table_constraints tc
71
73
  JOIN information_schema.key_column_usage kcu
72
74
  ON tc.constraint_name = kcu.constraint_name
73
75
  AND tc.table_schema = kcu.table_schema
74
76
  WHERE tc.constraint_type = 'UNIQUE'
75
77
  AND tc.table_schema = $1
78
+ ORDER BY tc.table_name, tc.constraint_name, kcu.ordinal_position
76
79
  `;
77
80
  const SQL_INDEXES = `
78
81
  SELECT tablename, indexname, indexdef
@@ -153,14 +156,22 @@ export async function introspect(options) {
153
156
  pkByTable.get(row.table_name).push(row.column_name);
154
157
  }
155
158
  // ----- Group unique constraints by table -----
159
+ // Group rows by (table_name, constraint_name) to correctly handle multi-column unique constraints
156
160
  const uniqueByTable = new Map();
161
+ const uniqueConstraintGroups = new Map();
157
162
  for (const row of uniqueResult.rows) {
158
163
  if (!tableSet.has(row.table_name))
159
164
  continue;
160
- if (!uniqueByTable.has(row.table_name))
161
- uniqueByTable.set(row.table_name, []);
162
- // Each unique constraint may be multi-column; for simplicity, treat as single-col here
163
- uniqueByTable.get(row.table_name).push([row.column_name]);
165
+ const key = `${row.table_name}::${row.constraint_name}`;
166
+ if (!uniqueConstraintGroups.has(key)) {
167
+ uniqueConstraintGroups.set(key, { table: row.table_name, columns: [] });
168
+ }
169
+ uniqueConstraintGroups.get(key).columns.push(row.column_name);
170
+ }
171
+ for (const { table, columns } of uniqueConstraintGroups.values()) {
172
+ if (!uniqueByTable.has(table))
173
+ uniqueByTable.set(table, []);
174
+ uniqueByTable.get(table).push(columns);
164
175
  }
165
176
  // ----- Group indexes by table -----
166
177
  const indexesByTable = new Map();
@@ -187,17 +198,25 @@ export async function introspect(options) {
187
198
  enums[row.typname] = [];
188
199
  enums[row.typname].push(row.enumlabel);
189
200
  }
190
- const foreignKeys = [];
201
+ const fkGroups = new Map();
191
202
  for (const row of fkResult.rows) {
192
203
  if (!tableSet.has(row.source_table) || !tableSet.has(row.target_table))
193
204
  continue;
194
- foreignKeys.push({
195
- sourceTable: row.source_table,
196
- sourceColumn: row.source_column,
197
- targetTable: row.target_table,
198
- targetColumn: row.target_column,
199
- });
205
+ const key = row.constraint_name;
206
+ if (!fkGroups.has(key)) {
207
+ fkGroups.set(key, {
208
+ sourceTable: row.source_table,
209
+ sourceColumns: [],
210
+ targetTable: row.target_table,
211
+ targetColumns: [],
212
+ constraintName: key,
213
+ });
214
+ }
215
+ const entry = fkGroups.get(key);
216
+ entry.sourceColumns.push(row.source_column);
217
+ entry.targetColumns.push(row.target_column);
200
218
  }
219
+ const foreignKeys = Array.from(fkGroups.values());
201
220
  // ----- Build relations from foreign keys -----
202
221
  // Count FKs per (source, target) pair for disambiguation
203
222
  const fkCounts = new Map();
@@ -209,10 +228,17 @@ export async function introspect(options) {
209
228
  for (const fk of foreignKeys) {
210
229
  const pairKey = `${fk.sourceTable}→${fk.targetTable}`;
211
230
  const needsDisambiguation = (fkCounts.get(pairKey) ?? 0) > 1;
231
+ // For single-column FKs, keep string form for backwards compatibility.
232
+ // For multi-column (composite) FKs, use array form.
233
+ const foreignKey = fk.sourceColumns.length === 1 ? fk.sourceColumns[0] : fk.sourceColumns;
234
+ const referenceKey = fk.targetColumns.length === 1 ? fk.targetColumns[0] : fk.targetColumns;
212
235
  // --- belongsTo on the source (child) table ---
213
236
  // e.g. posts.user_id → users.id creates posts.user (belongsTo)
237
+ // For composite FKs with disambiguation, use the constraint name
214
238
  const belongsToName = needsDisambiguation
215
- ? snakeToCamel(fk.sourceColumn.replace(/_id$/, ''))
239
+ ? fk.sourceColumns.length === 1
240
+ ? snakeToCamel(fk.sourceColumns[0].replace(/_id$/, ''))
241
+ : snakeToCamel(fk.constraintName.replace(/^fk_/, '').replace(/_fkey$/, ''))
216
242
  : singularize(snakeToCamel(fk.targetTable));
217
243
  if (!relationsByTable.has(fk.sourceTable))
218
244
  relationsByTable.set(fk.sourceTable, {});
@@ -221,13 +247,15 @@ export async function introspect(options) {
221
247
  name: belongsToName,
222
248
  from: fk.sourceTable,
223
249
  to: fk.targetTable,
224
- foreignKey: fk.sourceColumn,
225
- referenceKey: fk.targetColumn,
250
+ foreignKey,
251
+ referenceKey,
226
252
  };
227
253
  // --- hasMany on the target (parent) table ---
228
254
  // e.g. posts.user_id → users.id creates users.posts (hasMany)
229
255
  const hasManyName = needsDisambiguation
230
- ? snakeToCamel(`${fk.sourceTable}_by_${fk.sourceColumn.replace(/_id$/, '')}`)
256
+ ? fk.sourceColumns.length === 1
257
+ ? snakeToCamel(`${fk.sourceTable}_by_${fk.sourceColumns[0].replace(/_id$/, '')}`)
258
+ : snakeToCamel(`${fk.sourceTable}_by_${fk.constraintName.replace(/^fk_/, '').replace(/_fkey$/, '')}`)
231
259
  : snakeToCamel(fk.sourceTable);
232
260
  if (!relationsByTable.has(fk.targetTable))
233
261
  relationsByTable.set(fk.targetTable, {});
@@ -236,8 +264,8 @@ export async function introspect(options) {
236
264
  name: hasManyName,
237
265
  from: fk.targetTable,
238
266
  to: fk.sourceTable,
239
- foreignKey: fk.sourceColumn,
240
- referenceKey: fk.targetColumn,
267
+ foreignKey,
268
+ referenceKey,
241
269
  };
242
270
  }
243
271
  // ----- Assemble TableMetadata for each table -----
@@ -17,7 +17,7 @@
17
17
  * to handle N queries in a single pipeline.
18
18
  */
19
19
  import type { EventEmitter } from 'node:events';
20
- import type { DeferredQuery } from './query.js';
20
+ import type { DeferredQuery } from './query/index.js';
21
21
  /** The pg Connection object — an EventEmitter with wire-protocol methods */
22
22
  export interface PgConnection extends EventEmitter {
23
23
  stream: {
@@ -19,7 +19,7 @@
19
19
  * Hyperdrive), mock pools in tests, and any pool that doesn't expose pg internals.
20
20
  */
21
21
  import type pg from 'pg';
22
- import type { DeferredQuery } from './query.js';
22
+ import type { DeferredQuery } from './query/index.js';
23
23
  export interface PipelineOptions {
24
24
  /**
25
25
  * Whether to wrap the pipeline in a transaction (default: true).
@@ -0,0 +1,498 @@
1
+ /**
2
+ * turbine-orm — Query builder
3
+ *
4
+ * Each table accessor (db.users, db.posts, etc.) returns a QueryInterface<T>
5
+ * that builds parameterized SQL and executes it through the connection pool.
6
+ *
7
+ * Nested relations use json_build_object + json_agg subqueries for single-query
8
+ * resolution — a PostgreSQL-native approach that eliminates N+1 query patterns.
9
+ *
10
+ * Schema-driven: all column names, types, and relations come from introspected
11
+ * metadata — nothing is hardcoded.
12
+ */
13
+ import type pg from 'pg';
14
+ import type { SchemaMetadata } from '../schema.js';
15
+ import type { AggregateArgs, AggregateResult, CountArgs, CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GroupByArgs, TypedWithClause, UpdateArgs, UpdateManyArgs, UpsertArgs, WithClause, WithResult } from './types.js';
16
+ export interface DeferredQuery<T> {
17
+ /** SQL text with $1, $2 placeholders */
18
+ sql: string;
19
+ /** Bound parameter values */
20
+ params: unknown[];
21
+ /** How to transform the raw pg.QueryResult into the final value */
22
+ transform: (result: pg.QueryResult) => T;
23
+ /** Tag for debugging / logging */
24
+ tag: string;
25
+ /** Prepared statement name (t_<16hex>). Set when SQL cache is enabled. */
26
+ preparedName?: string;
27
+ }
28
+ /** Middleware function type — imported from client to avoid circular deps */
29
+ export type MiddlewareFn = (params: {
30
+ model: string;
31
+ action: string;
32
+ args: Record<string, unknown>;
33
+ }, next: (params: {
34
+ model: string;
35
+ action: string;
36
+ args: Record<string, unknown>;
37
+ }) => Promise<unknown>) => Promise<unknown>;
38
+ /** Options passed from TurbineClient to QueryInterface */
39
+ export interface QueryInterfaceOptions {
40
+ /** Default LIMIT applied to findMany() when no limit is specified */
41
+ defaultLimit?: number;
42
+ /**
43
+ * Log a one-time warning when {@link QueryInterface.findMany} is called
44
+ * without a `limit`. Defaults to `true` so that accidental unbounded
45
+ * queries are surfaced loudly during development. Pass `false` to silence
46
+ * the warning entirely (e.g. for CLI tooling that intentionally streams
47
+ * full tables).
48
+ */
49
+ warnOnUnlimited?: boolean;
50
+ /**
51
+ * Enable prepared statements. When true, queries are submitted with a
52
+ * `{ name, text, values }` object to the pg driver, which caches the
53
+ * parse+plan on the server per connection.
54
+ *
55
+ * Default: `true` for Turbine-owned pools, `false` for external pools
56
+ * (serverless drivers may not support named statements).
57
+ */
58
+ preparedStatements?: boolean;
59
+ /**
60
+ * Enable the SQL template cache. When true, repeated queries with the
61
+ * same shape (same keys, operators, relations — different values) reuse
62
+ * cached SQL text instead of rebuilding from scratch.
63
+ *
64
+ * Default: `true`. Set to `false` as a nuclear kill switch.
65
+ */
66
+ sqlCache?: boolean;
67
+ }
68
+ export declare class QueryInterface<T extends object, R extends object = {}> {
69
+ private readonly pool;
70
+ private readonly table;
71
+ private readonly schema;
72
+ private readonly tableMeta;
73
+ /** SQL template cache: cacheKey → SqlCacheEntry (sql + prepared statement name) */
74
+ private readonly sqlTemplateCache;
75
+ private readonly middlewares;
76
+ private readonly defaultLimit?;
77
+ private readonly warnOnUnlimited;
78
+ private readonly preparedStatementsEnabled;
79
+ private readonly sqlCacheEnabled;
80
+ /**
81
+ * Tracks tables that have already triggered an unlimited-query warning so
82
+ * the user is not spammed once per row. Per-instance state — each
83
+ * QueryInterface is bound to a single table, so this set will only ever
84
+ * contain at most one entry, but using a Set keeps the API consistent with
85
+ * the audit's "Set<string>" guidance and leaves room for future
86
+ * cross-table sharing.
87
+ */
88
+ private readonly warnedTables;
89
+ /** Cache hit/miss counters for diagnostics */
90
+ private cacheHits;
91
+ private cacheMisses;
92
+ /** Pre-computed column type lookups (avoids linear scans per query) */
93
+ private readonly columnPgTypeMap;
94
+ private readonly columnArrayTypeMap;
95
+ /** Tracks tables that have already triggered a deep-with warning (one-time) */
96
+ private readonly deepWithWarned;
97
+ constructor(pool: pg.Pool, table: string, schema: SchemaMetadata, middlewares?: MiddlewareFn[], options?: QueryInterfaceOptions);
98
+ /**
99
+ * Return cache hit/miss statistics for this QueryInterface instance.
100
+ * Useful for monitoring and benchmarking.
101
+ */
102
+ cacheStats(): {
103
+ hits: number;
104
+ misses: number;
105
+ hitRate: number;
106
+ size: number;
107
+ };
108
+ /**
109
+ * Look up or build a SQL template in the cache.
110
+ * On miss, calls `build()` to generate the SQL, stores the entry, and returns it.
111
+ * On hit, increments counters and returns the cached entry.
112
+ *
113
+ * When `sqlCache` is disabled, always calls `build()` without caching.
114
+ */
115
+ private acquireSql;
116
+ /**
117
+ * Reset the per-instance unlimited-query warning dedupe set.
118
+ * Exposed for tests so a single test process can verify the warning fires
119
+ * exactly once per table without bleeding state between assertions.
120
+ */
121
+ resetUnlimitedWarnings(): void;
122
+ /**
123
+ * Execute a pool.query with an optional timeout.
124
+ * If timeout is set, races the query against a timer and rejects on expiry.
125
+ * pg driver errors are translated to typed Turbine errors via wrapPgError.
126
+ */
127
+ private queryWithTimeout;
128
+ /**
129
+ * Execute a query through the middleware chain.
130
+ * If no middlewares are registered, executes directly.
131
+ *
132
+ * Middleware can inspect and log query parameters, modify results after execution,
133
+ * and measure timing. Note: query SQL is generated before middleware runs, so
134
+ * modifying params.args in middleware will NOT affect the executed SQL.
135
+ * To intercept queries before SQL generation, use the raw() method instead.
136
+ */
137
+ private executeWithMiddleware;
138
+ findUnique<W extends TypedWithClause<R> = {}>(args: FindUniqueArgs<T, R, W>): Promise<WithResult<T, R, W> | null>;
139
+ buildFindUnique<W extends TypedWithClause<R> = {}>(args: FindUniqueArgs<T, R, W>): DeferredQuery<T | null>;
140
+ findMany<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): Promise<WithResult<T, R, W>[]>;
141
+ /**
142
+ * Emit a one-time `console.warn` when {@link findMany} is called without an
143
+ * explicit `limit`/`take` and `warnOnUnlimited` has not been disabled.
144
+ *
145
+ * Deduped per QueryInterface instance via {@link warnedTables} so a busy
146
+ * loop calling `db.users.findMany()` thousands of times only logs once.
147
+ * Suppressed when `defaultLimit` is configured (the caller has already
148
+ * opted in to a bounded query) and when the user passed an explicit
149
+ * `limit`, `take`, or `cursor`.
150
+ */
151
+ private maybeWarnUnlimited;
152
+ /**
153
+ * Recursively measure the maximum depth of a `with` clause tree.
154
+ * Used by the dev-only deep-with warning guard.
155
+ */
156
+ private measureWithDepth;
157
+ buildFindMany<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): DeferredQuery<T[]>;
158
+ /**
159
+ * Stream rows from a findMany query using PostgreSQL cursors.
160
+ * Returns an AsyncIterable that yields individual rows, fetching in batches internally.
161
+ *
162
+ * **Speculative fast-path:** Before opening a cursor, issues a single
163
+ * `SELECT ... LIMIT batchSize+1`. If the result fits within `batchSize`,
164
+ * all rows are yielded immediately with zero cursor overhead (no BEGIN /
165
+ * DECLARE / CLOSE / COMMIT). Only when the result overflows does the
166
+ * method fall back to the full cursor path.
167
+ *
168
+ * **Cursor path:** Uses DECLARE CURSOR within a dedicated transaction on a
169
+ * single pooled connection. The cursor is automatically closed and the
170
+ * connection released when iteration completes or is terminated early
171
+ * (e.g. `break` from `for await`).
172
+ *
173
+ * **Snapshot semantics note:** The speculative fast-path runs outside a
174
+ * transaction. If the result overflows and the cursor path is opened, the
175
+ * cursor runs in its own transaction — spanning two separate snapshots.
176
+ * For strict single-snapshot semantics, wrap the call in `$transaction`.
177
+ *
178
+ * @example
179
+ * ```ts
180
+ * for await (const user of db.users.findManyStream({ where: { orgId: 1 }, batchSize: 500 })) {
181
+ * process.stdout.write(`${user.email}\n`);
182
+ * }
183
+ * ```
184
+ */
185
+ findManyStream<W extends TypedWithClause<R> = {}>(args?: FindManyStreamArgs<T, R, W>): AsyncGenerator<WithResult<T, R, W>, void, undefined>;
186
+ findFirst<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): Promise<WithResult<T, R, W> | null>;
187
+ buildFindFirst<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): DeferredQuery<T | null>;
188
+ findFirstOrThrow<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): Promise<WithResult<T, R, W>>;
189
+ buildFindFirstOrThrow<W extends TypedWithClause<R> = {}>(args?: FindManyArgs<T, R, W>): DeferredQuery<T>;
190
+ findUniqueOrThrow<W extends TypedWithClause<R> = {}>(args: FindUniqueArgs<T, R, W>): Promise<WithResult<T, R, W>>;
191
+ buildFindUniqueOrThrow<W extends TypedWithClause<R> = {}>(args: FindUniqueArgs<T, R, W>): DeferredQuery<T>;
192
+ create(args: CreateArgs<T>): Promise<T>;
193
+ buildCreate(args: CreateArgs<T>): DeferredQuery<T>;
194
+ createMany(args: CreateManyArgs<T>): Promise<T[]>;
195
+ buildCreateMany(args: CreateManyArgs<T>): DeferredQuery<T[]>;
196
+ update(args: UpdateArgs<T>): Promise<T>;
197
+ buildUpdate(args: UpdateArgs<T>): DeferredQuery<T>;
198
+ delete(args: DeleteArgs<T>): Promise<T>;
199
+ buildDelete(args: DeleteArgs<T>): DeferredQuery<T>;
200
+ upsert(args: UpsertArgs<T>): Promise<T>;
201
+ buildUpsert(args: UpsertArgs<T>): DeferredQuery<T>;
202
+ updateMany(args: UpdateManyArgs<T>): Promise<{
203
+ count: number;
204
+ }>;
205
+ buildUpdateMany(args: UpdateManyArgs<T>): DeferredQuery<{
206
+ count: number;
207
+ }>;
208
+ deleteMany(args: DeleteManyArgs<T>): Promise<{
209
+ count: number;
210
+ }>;
211
+ buildDeleteMany(args: DeleteManyArgs<T>): DeferredQuery<{
212
+ count: number;
213
+ }>;
214
+ count(args?: CountArgs<T>): Promise<number>;
215
+ buildCount(args?: CountArgs<T>): DeferredQuery<number>;
216
+ groupBy(args: GroupByArgs<T>): Promise<Record<string, unknown>[]>;
217
+ buildGroupBy(args: GroupByArgs<T>): DeferredQuery<Record<string, unknown>[]>;
218
+ aggregate(args: AggregateArgs<T>): Promise<AggregateResult<T>>;
219
+ buildAggregate(args: AggregateArgs<T>): DeferredQuery<AggregateResult<T>>;
220
+ /**
221
+ * Resolve select/omit options into a list of snake_case column names.
222
+ * Returns null if neither is provided (meaning all columns).
223
+ */
224
+ private resolveColumns;
225
+ /** Convert camelCase field name to snake_case column name (unquoted, for non-SQL uses) */
226
+ private toColumn;
227
+ /** Convert camelCase field name to a double-quoted SQL identifier */
228
+ private toSqlColumn;
229
+ /**
230
+ * Build a single SET clause entry for update/updateMany.
231
+ *
232
+ * Supports plain values and atomic operator objects ({ set, increment,
233
+ * decrement, multiply, divide }). An operator object is detected ONLY when
234
+ * it has EXACTLY one key that is one of the 5 operator keys — this avoids
235
+ * misinterpreting JSON column values like `{ set: 'x' }` as operators
236
+ * (real operator objects always have exactly one key, and a plain JSON
237
+ * payload that happens to have a single `set` key is extremely unusual).
238
+ * Multi-key objects are always treated as plain (JSON) values.
239
+ *
240
+ * Returns the SQL fragment (e.g., `"view_count" = "view_count" + $3`) and
241
+ * pushes any required params onto the shared params array so that WHERE
242
+ * clause numbering continues correctly afterward.
243
+ */
244
+ private buildSetClause;
245
+ /**
246
+ * Produce a value-invariant fingerprint of a where clause.
247
+ * Same keys + same operator shapes + same combinator structure => same string.
248
+ * Different values (e.g. id=1 vs id=999) => identical fingerprint.
249
+ *
250
+ * @internal Exposed as package-private for testing via class access.
251
+ */
252
+ fingerprintWhere(where: Record<string, unknown>): string;
253
+ /**
254
+ * Fingerprint a relation filter sub-where for some/every/none.
255
+ */
256
+ private fingerprintRelFilter;
257
+ /**
258
+ * Walk a where clause and push ONLY values into `params`, in the EXACT same
259
+ * order that `buildWhereClause` pushes them. Used on cache hit to fill params
260
+ * without rebuilding SQL.
261
+ *
262
+ * @internal Exposed as package-private for testing.
263
+ */
264
+ collectWhereParams(where: Record<string, unknown>, params: unknown[]): void;
265
+ /** Collect params from a relation filter sub-where. Mirrors buildSubWhereForRelation. */
266
+ private collectRelFilterParams;
267
+ /** Collect params from operator clauses. Mirrors buildOperatorClauses. */
268
+ private collectOperatorParams;
269
+ /** Collect params from JSON filter. Mirrors buildJsonFilterClauses. */
270
+ private collectJsonFilterParams;
271
+ /** Collect params from array filter. Mirrors buildArrayFilterClauses. */
272
+ private collectArrayFilterParams;
273
+ /**
274
+ * Produce a fingerprint for a `with` clause tree. Recursion mirrors
275
+ * buildSelectWithRelations / buildRelationSubquery.
276
+ *
277
+ * @internal Exposed as package-private for testing.
278
+ */
279
+ withFingerprint(withClause: WithClause | undefined, table?: string, depth?: number): string;
280
+ /**
281
+ * Collect params from a `with` clause tree. Mirrors buildSelectWithRelations +
282
+ * buildRelationSubquery param-push order.
283
+ */
284
+ private collectWithParams;
285
+ /**
286
+ * Collect params from a single relation subquery. Mirrors buildRelationSubquery.
287
+ */
288
+ private collectRelationSubqueryParams;
289
+ /**
290
+ * Fingerprint SET clauses for update/updateMany.
291
+ * Captures key names + operator types (set/increment/etc) but not values.
292
+ */
293
+ private fingerprintSet;
294
+ /**
295
+ * Collect SET params for update/updateMany. Mirrors buildSetClause param order.
296
+ */
297
+ private collectSetParams;
298
+ /** Build WHERE clause from a where object (supports operators, NULL, OR) */
299
+ private buildWhere;
300
+ /**
301
+ * Refuse mutations with an empty predicate unless explicitly opted in.
302
+ *
303
+ * An empty `where` (e.g. `{}` or `{ id: undefined }`) resolves to a
304
+ * mutation with no filter — a common footgun when a caller's filter
305
+ * value accidentally resolves to `undefined`. This guard throws
306
+ * `ValidationError` in that case unless `allowFullTableScan: true`.
307
+ */
308
+ private assertMutationHasPredicate;
309
+ /**
310
+ * Build the inner WHERE expression (without the WHERE keyword).
311
+ * Returns null if no conditions exist.
312
+ * Supports: equality, operators, NULL, OR, AND, NOT, relation filters (some/every/none).
313
+ */
314
+ private buildWhereClause;
315
+ /**
316
+ * Build relation filter SQL: WHERE EXISTS / NOT EXISTS subquery
317
+ * Supports: some (EXISTS), every (NOT EXISTS ... NOT), none (NOT EXISTS)
318
+ */
319
+ private buildRelationFilter;
320
+ /**
321
+ * Build WHERE clause conditions for a relation filter subquery.
322
+ * Uses the target table's column mapping to resolve field names.
323
+ */
324
+ private buildSubWhereForRelation;
325
+ /**
326
+ * Build SQL clauses for a single operator object on a column.
327
+ * Each operator key becomes its own clause, all ANDed together.
328
+ */
329
+ private buildOperatorClauses;
330
+ /** Build ORDER BY clause from an object */
331
+ private buildOrderBy;
332
+ /** Parse a flat row: convert snake_case to camelCase + Date coercion */
333
+ private parseRow;
334
+ /** Parse a row that may contain JSON nested relation columns */
335
+ private parseNestedRow;
336
+ /**
337
+ * Build a SELECT clause that includes both base columns and nested relation subqueries.
338
+ *
339
+ * For each relation specified in the `with` clause, this method generates a correlated
340
+ * subquery using PostgreSQL's `json_agg(json_build_object(...))` pattern. The result
341
+ * is a single SQL SELECT clause that resolves the full object tree in one query --
342
+ * no N+1 problem.
343
+ *
344
+ * **How it works:**
345
+ * 1. Resolves the base columns for the root table (all columns, or a subset via `columnsList`).
346
+ * 2. Iterates over each key in the `with` clause, looking up the relation definition.
347
+ * 3. For each relation, delegates to {@link buildRelationSubquery} to generate a
348
+ * correlated subquery that returns JSON (array for hasMany, object for belongsTo/hasOne).
349
+ * 4. Each subquery is aliased as the relation name in the final SELECT.
350
+ *
351
+ * **aliasCounter:** A shared `{ n: number }` object is passed through all nesting levels.
352
+ * Each call to `buildRelationSubquery` increments it to produce unique table aliases
353
+ * (`t0`, `t1`, `t2`, ...) across arbitrarily deep relation trees, preventing alias
354
+ * collisions in the generated SQL.
355
+ *
356
+ * **Example output:**
357
+ * ```sql
358
+ * "users"."id", "users"."name", "users"."email",
359
+ * (SELECT COALESCE(json_agg(json_build_object('id', t0."id", 'title', t0."title")), '[]'::json)
360
+ * FROM "posts" t0 WHERE t0."user_id" = "users"."id") AS "posts"
361
+ * ```
362
+ *
363
+ * @param table - The root table name (e.g. `"users"`).
364
+ * @param withClause - An object mapping relation names to their include specs
365
+ * (`true` for default inclusion, or `WithOptions` for select/omit/where/orderBy/limit).
366
+ * @param params - Shared parameter array for parameterized values (`$1`, `$2`, ...).
367
+ * Nested where/limit values are pushed here to prevent SQL injection.
368
+ * @param columnsList - Optional subset of columns to include in the SELECT. When `null`
369
+ * or omitted, all columns from the table's schema metadata are used.
370
+ * @param depth - Current nesting depth, passed through to {@link buildRelationSubquery}
371
+ * for circular-relation detection. Defaults to `0` at the top level.
372
+ * @param path - Breadcrumb trail of relation names traversed so far, used in error
373
+ * messages when circular or too-deep nesting is detected.
374
+ * @returns A complete SELECT clause string (without the `SELECT` keyword) containing
375
+ * base columns and relation subqueries.
376
+ */
377
+ private buildSelectWithRelations;
378
+ /**
379
+ * Generate a correlated subquery that returns JSON for a single relation.
380
+ *
381
+ * This is the core of Turbine's single-query nested relation strategy. For a given
382
+ * relation (e.g. `posts` on a `users` query), it produces a self-contained SQL subquery
383
+ * that PostgreSQL evaluates per parent row, returning either a JSON array (hasMany) or
384
+ * a single JSON object (belongsTo / hasOne).
385
+ *
386
+ * ### Algorithm overview
387
+ *
388
+ * 1. **Alias generation:** Allocates a unique alias (`t0`, `t1`, ...) from the shared
389
+ * `aliasCounter` so that deeply nested subqueries never collide.
390
+ *
391
+ * 2. **Column resolution:** Honors `select` / `omit` options to control which columns
392
+ * appear in the output JSON.
393
+ *
394
+ * 3. **`json_build_object`:** Builds a JSON object for each row by mapping camelCase
395
+ * field names to their column values:
396
+ * ```sql
397
+ * json_build_object('id', t0."id", 'title', t0."title", 'createdAt', t0."created_at")
398
+ * ```
399
+ *
400
+ * 4. **`json_agg` wrapping (hasMany):** For one-to-many relations, wraps the
401
+ * `json_build_object` call in `json_agg(...)` to aggregate all matching child rows
402
+ * into a JSON array. Uses `COALESCE(..., '[]'::json)` so the result is never NULL.
403
+ * For belongsTo / hasOne, no aggregation is used -- just the single JSON object
404
+ * with `LIMIT 1`.
405
+ *
406
+ * 5. **Correlation (WHERE clause):** Links the subquery to the parent row:
407
+ * - **hasMany:** `alias.foreignKey = parentRef.referenceKey`
408
+ * (e.g. `t0."user_id" = "users"."id"` -- child FK points to parent PK)
409
+ * - **belongsTo / hasOne:** `alias.referenceKey = parentRef.foreignKey`
410
+ * (e.g. `t0."id" = "posts"."author_id"` -- parent FK points to child PK)
411
+ *
412
+ * 6. **Recursion:** If the spec includes a nested `with` clause, this method calls
413
+ * itself recursively for each nested relation, passing the current alias as
414
+ * `parentRef`. The nested subquery appears as an additional key in the
415
+ * `json_build_object` call, wrapped in `COALESCE(..., '[]'::json)`.
416
+ * Depth is incremented and capped at 10 to guard against circular relations.
417
+ *
418
+ * 7. **LIMIT / ORDER BY wrapping:** For hasMany relations with `limit` or `orderBy`,
419
+ * the query is restructured into a two-level form:
420
+ * ```sql
421
+ * SELECT COALESCE(json_agg(json_build_object(...)), '[]'::json)
422
+ * FROM (
423
+ * SELECT t0.* FROM "posts" t0
424
+ * WHERE t0."user_id" = "users"."id"
425
+ * ORDER BY t0."created_at" DESC
426
+ * LIMIT $1
427
+ * ) t0i
428
+ * ```
429
+ * This ensures LIMIT and ORDER BY apply to the raw rows *before* `json_agg`
430
+ * aggregation. Without the inner subquery, LIMIT would be meaningless because
431
+ * `json_agg` produces a single aggregated row.
432
+ *
433
+ * 8. **Parameter threading:** All user-supplied values (where filters, limit) are
434
+ * pushed to the shared `params` array with `$N` placeholders. No string
435
+ * interpolation of user data ever occurs -- all identifiers go through
436
+ * `quoteIdent()` and all values are parameterized.
437
+ *
438
+ * ### Example output (hasMany with nested relation)
439
+ * ```sql
440
+ * SELECT COALESCE(json_agg(json_build_object(
441
+ * 'id', t0."id",
442
+ * 'title', t0."title",
443
+ * 'comments', COALESCE((
444
+ * SELECT COALESCE(json_agg(json_build_object('id', t1."id", 'body', t1."body")), '[]'::json)
445
+ * FROM "comments" t1 WHERE t1."post_id" = t0."id"
446
+ * ), '[]'::json)
447
+ * )), '[]'::json) FROM "posts" t0 WHERE t0."user_id" = "users"."id"
448
+ * ```
449
+ *
450
+ * @param relDef - The relation definition from schema metadata (contains `to`, `type`,
451
+ * `foreignKey`, `referenceKey`).
452
+ * @param spec - Either `true` (include with defaults) or a `WithOptions` object that
453
+ * can specify `select`, `omit`, `where`, `orderBy`, `limit`, and nested `with`.
454
+ * @param params - Shared parameter array. User-supplied values are pushed here and
455
+ * referenced as `$1`, `$2`, etc. in the generated SQL.
456
+ * @param parentRef - The alias (e.g. `"t0"`) or table name (e.g. `"users"`) of the
457
+ * parent query. Used to build the correlated WHERE clause that ties
458
+ * child rows to their parent row.
459
+ * @param aliasCounter - Shared mutable counter (`{ n: number }`) for generating unique
460
+ * table aliases (`t0`, `t1`, `t2`, ...) across all nesting levels.
461
+ * Each call increments `n` by 1.
462
+ * @param depth - Current nesting depth (starts at `0`). Incremented on each recursive
463
+ * call. If it reaches 10, a {@link CircularRelationError} is thrown.
464
+ * @param path - Breadcrumb trail of relation/table names traversed so far
465
+ * (e.g. `["users", "posts", "comments"]`). Used in the error message
466
+ * when circular or too-deep nesting is detected.
467
+ * @returns A complete SQL subquery string (without surrounding parentheses) that
468
+ * evaluates to a JSON array (hasMany) or a JSON object (belongsTo/hasOne).
469
+ */
470
+ private buildRelationSubquery;
471
+ /**
472
+ * Get the Postgres type for a column (e.g. 'jsonb', 'text', '_int4').
473
+ * Used to detect JSONB/array columns for specialized operators.
474
+ * Uses pre-computed Map for O(1) lookup instead of linear scan.
475
+ */
476
+ private getColumnPgType;
477
+ /**
478
+ * Get the Postgres base element type for an array column.
479
+ * E.g. '_text' → 'text', '_int4' → 'integer'
480
+ */
481
+ private getArrayElementType;
482
+ /**
483
+ * Build SQL clauses for JSONB filter operators on a column.
484
+ * Supports: path, equals, contains, hasKey.
485
+ */
486
+ private buildJsonFilterClauses;
487
+ /**
488
+ * Build SQL clauses for Array filter operators on a column.
489
+ * Supports: has, hasEvery, hasSome, isEmpty.
490
+ */
491
+ private buildArrayFilterClauses;
492
+ /**
493
+ * Get the Postgres array type for a column (used by UNNEST in createMany).
494
+ * Uses pre-computed Map for O(1) lookup instead of linear scan.
495
+ */
496
+ private getColumnArrayType;
497
+ }
498
+ //# sourceMappingURL=builder.d.ts.map