turbine-orm 0.35.0 → 0.36.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 (68) hide show
  1. package/README.md +18 -16
  2. package/dist/cjs/cli/index.js +109 -16
  3. package/dist/cjs/cli/migrate.js +78 -3
  4. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  5. package/dist/cjs/cli/studio.js +333 -22
  6. package/dist/cjs/cli/ui.js +7 -1
  7. package/dist/cjs/dialect.js +1 -1
  8. package/dist/cjs/generate.js +23 -2
  9. package/dist/cjs/index.js +2 -1
  10. package/dist/cjs/mssql.js +22 -5
  11. package/dist/cjs/powdb.js +41 -1
  12. package/dist/cjs/powql.js +80 -25
  13. package/dist/cjs/query/aggregates.js +683 -0
  14. package/dist/cjs/query/batched-loader.js +2 -0
  15. package/dist/cjs/query/builder.js +297 -4504
  16. package/dist/cjs/query/filters.js +12 -0
  17. package/dist/cjs/query/relations.js +1698 -0
  18. package/dist/cjs/query/where-compile.js +180 -0
  19. package/dist/cjs/query/where.js +1491 -0
  20. package/dist/cjs/query/writes.js +680 -0
  21. package/dist/cjs/schema-builder.js +6 -0
  22. package/dist/cjs/schema-metadata.js +4 -0
  23. package/dist/cjs/schema-sql.js +265 -3
  24. package/dist/cjs/sqlite.js +1 -1
  25. package/dist/cli/index.d.ts +8 -2
  26. package/dist/cli/index.js +111 -18
  27. package/dist/cli/migrate.d.ts +24 -1
  28. package/dist/cli/migrate.js +77 -3
  29. package/dist/cli/studio-ui.generated.js +1 -1
  30. package/dist/cli/studio.d.ts +46 -13
  31. package/dist/cli/studio.js +331 -23
  32. package/dist/cli/ui.js +7 -1
  33. package/dist/dialect.d.ts +15 -6
  34. package/dist/dialect.js +1 -1
  35. package/dist/generate.js +23 -2
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.js +1 -1
  38. package/dist/mssql.js +22 -5
  39. package/dist/powdb.d.ts +20 -0
  40. package/dist/powdb.js +40 -0
  41. package/dist/powql.d.ts +33 -1
  42. package/dist/powql.js +80 -25
  43. package/dist/query/aggregates.d.ts +74 -0
  44. package/dist/query/aggregates.js +641 -0
  45. package/dist/query/batched-loader.d.ts +6 -0
  46. package/dist/query/batched-loader.js +2 -0
  47. package/dist/query/builder.d.ts +62 -829
  48. package/dist/query/builder.js +302 -4509
  49. package/dist/query/deferred.d.ts +7 -0
  50. package/dist/query/filters.d.ts +7 -0
  51. package/dist/query/filters.js +11 -0
  52. package/dist/query/relations.d.ts +441 -0
  53. package/dist/query/relations.js +1627 -0
  54. package/dist/query/types.d.ts +15 -0
  55. package/dist/query/where-compile.d.ts +139 -0
  56. package/dist/query/where-compile.js +175 -0
  57. package/dist/query/where.d.ts +494 -0
  58. package/dist/query/where.js +1431 -0
  59. package/dist/query/writes.d.ts +131 -0
  60. package/dist/query/writes.js +626 -0
  61. package/dist/schema-builder.d.ts +18 -3
  62. package/dist/schema-builder.js +6 -0
  63. package/dist/schema-metadata.js +4 -0
  64. package/dist/schema-sql.d.ts +60 -3
  65. package/dist/schema-sql.js +261 -4
  66. package/dist/schema.d.ts +10 -0
  67. package/dist/sqlite.js +1 -1
  68. package/package.json +2 -2
@@ -98,6 +98,13 @@ export interface QueryInterfaceOptions {
98
98
  * outside production, so it never touches the production hot path. Set the
99
99
  * env var `TURBINE_DISABLE_CACHE_CHECK=1` to opt out when dev traffic is
100
100
  * perf-sensitive.
101
+ *
102
+ * Production sampling: the check is off in production by default. Set
103
+ * `TURBINE_CACHE_CHECK_SAMPLE` to a float in `(0,1]` to re-verify that
104
+ * fraction of cache hits under real load (e.g. `0.001` for one in a
105
+ * thousand). A sampled mismatch logs `console.error` once per distinct
106
+ * fingerprint AND throws the same `ValidationError` (E003). `0`, unset, or an
107
+ * unparseable value keeps the check fully off.
101
108
  */
102
109
  sqlCache?: boolean;
103
110
  /** SQL dialect implementation. Defaults to PostgreSQL. */
@@ -112,6 +112,13 @@ export declare const ARRAY_OPERATOR_KEYS: Set<string>;
112
112
  * carve out.
113
113
  */
114
114
  export declare const ARRAY_UNIQUE_KEYS: Set<string>;
115
+ /**
116
+ * Value-invariant shape fingerprint for an {@link ArrayFilter} (the INNER part,
117
+ * without the `arr(...)` wrapper the where fingerprint adds). The boolean
118
+ * `isEmpty` operator changes the SQL shape (`= '{}'` vs `<> '{}'`), so its
119
+ * concrete value is part of the shape; the other operators are value-invariant.
120
+ */
121
+ export declare function fingerprintArrayFilterShape(filter: ArrayFilter): string;
115
122
  /** Check if a value is an Array filter object */
116
123
  export declare function isArrayFilter(value: unknown): value is ArrayFilter;
117
124
  /**
@@ -194,6 +194,17 @@ export const ARRAY_OPERATOR_KEYS = new Set(['has', 'hasEvery', 'hasSome', 'isEmp
194
194
  * carve out.
195
195
  */
196
196
  export const ARRAY_UNIQUE_KEYS = new Set(['has', 'hasEvery', 'hasSome', 'isEmpty']);
197
+ /**
198
+ * Value-invariant shape fingerprint for an {@link ArrayFilter} (the INNER part,
199
+ * without the `arr(...)` wrapper the where fingerprint adds). The boolean
200
+ * `isEmpty` operator changes the SQL shape (`= '{}'` vs `<> '{}'`), so its
201
+ * concrete value is part of the shape; the other operators are value-invariant.
202
+ */
203
+ export function fingerprintArrayFilterShape(filter) {
204
+ const keys = Object.keys(filter).sort();
205
+ const suffix = filter.isEmpty === undefined ? '' : `:empty=${filter.isEmpty ? 'true' : 'false'}`;
206
+ return `${keys.join(',')}${suffix}`;
207
+ }
197
208
  /** Check if a value is an Array filter object */
198
209
  export function isArrayFilter(value) {
199
210
  if (value === null ||
@@ -0,0 +1,441 @@
1
+ /**
2
+ * turbine-orm: relation + orderBy compilation (extracted from builder.ts)
3
+ *
4
+ * The json_agg nested-relation machinery (buildSelectWithRelations,
5
+ * buildRelationSubquery, buildManyToManySubquery), the positional-encoding
6
+ * shapes + nested-row parser, the full orderBy surface (plain / JSON-path /
7
+ * vector KNN / relation _count / pick-row), relation _count expressions and
8
+ * their global-filter params, and the with-clause fingerprint + param
9
+ * collectors. All functions take a {@link BuilderCtx} first argument; WHERE
10
+ * compilation is reused from where.ts (whereMod.*), the PII column set from
11
+ * writes.ts (writesMod.*), and the remaining primitives stay class-resident,
12
+ * reached through the ctx. See builder.ts for the thin delegating methods and
13
+ * the findMany/findUnique execute assembly.
14
+ */
15
+ import { ValidationError } from '../errors.js';
16
+ import type { RelationDef, TableMetadata } from '../schema.js';
17
+ import type { JsonPathOrderBy, OrderByClause, RelationPickOrderBy, WithClause, WithOptions } from './types.js';
18
+ import type { BuilderCtx } from './where.js';
19
+ /**
20
+ * Decode descriptor for `jsonEncoding: 'positional'`. Built during SQL
21
+ * generation (see {@link buildRelationShape}) and consumed by the
22
+ * transform to map key-less positional arrays back to keyed objects.
23
+ *
24
+ * - `keys` — camelCase field names in emitted array position, INCLUDING nested
25
+ * relation slots (a nested relation occupies one more position after the
26
+ * scalar columns, in `sortedEntries(with)` order).
27
+ * - `nested` — sub-shape for each key in `keys` that is itself a relation slot.
28
+ * - `cardinality` — `'one'` (belongsTo/hasOne, a single positional array or
29
+ * null) vs `'many'` (an array of positional arrays).
30
+ */
31
+ export interface RelationShape {
32
+ keys: string[];
33
+ nested: Record<string, RelationShape>;
34
+ cardinality: 'many' | 'one';
35
+ }
36
+ /**
37
+ * Resolve select/omit options into a list of snake_case column names.
38
+ * Returns null if neither is provided (meaning all columns).
39
+ */
40
+ export declare function resolveColumns(qi: BuilderCtx, select?: Record<string, boolean>, omit?: Record<string, boolean>, includePii?: boolean): string[] | null;
41
+ /**
42
+ * Produce a fingerprint for a `with` clause tree. Recursion mirrors
43
+ * buildSelectWithRelations / buildRelationSubquery.
44
+ *
45
+ * @internal Exposed as package-private for testing.
46
+ */
47
+ export declare function withFingerprint(qi: BuilderCtx, withClause: WithClause | undefined, table?: string, depth?: number): string;
48
+ /**
49
+ * Collect params from a `with` clause tree. Mirrors buildSelectWithRelations +
50
+ * buildRelationSubquery param-push order.
51
+ */
52
+ export declare function collectWithParams(qi: BuilderCtx, withClause: WithClause, params: unknown[], table?: string): void;
53
+ /**
54
+ * Collect params from a single relation subquery. Mirrors buildRelationSubquery.
55
+ */
56
+ export declare function collectRelationSubqueryParams(qi: BuilderCtx, relDef: RelationDef, spec: true | WithOptions, params: unknown[], _parentRef: string, depth?: number): void;
57
+ /**
58
+ * Value-shape fingerprint for a single orderBy entry, so two queries whose
59
+ * ORDER BY differs only in nulls placement, vector metric, or relation-count
60
+ * vs relation-column never collide on one cached SQL string. Captures the
61
+ * SQL-shaping bits (direction, nulls, metric, relation keys) — never values.
62
+ */
63
+ export declare function orderByEntryFingerprint(qi: BuilderCtx, d: unknown, targetTable?: string): string;
64
+ export declare function buildOrderBy(qi: BuilderCtx, orderBy: OrderByClause, params?: unknown[], lateralSink?: string[]): string;
65
+ /**
66
+ * True when an orderBy value is a relation-ordering object: a plain object
67
+ * that is neither a vector KNN ordering nor an {@link OrderBySpec}. Its key
68
+ * in the orderBy clause is a relation name.
69
+ */
70
+ export declare function isRelationOrderByValue(_qi: BuilderCtx, value: unknown): boolean;
71
+ /**
72
+ * Render the ` NULLS FIRST` / ` NULLS LAST` suffix for a column ordering.
73
+ * Only PostgreSQL and SQLite support the `NULLS FIRST/LAST` grammar — on any
74
+ * other engine a caller asking for explicit nulls placement gets a clear
75
+ * {@link UnsupportedFeatureError} (E017) instead of broken SQL.
76
+ */
77
+ export declare function nullsSuffix(qi: BuilderCtx, nulls: 'first' | 'last' | undefined): string;
78
+ /**
79
+ * Resolve an orderBy key to its snake_case column via the table's columnMap
80
+ * (camelToSnake fallback), throwing the SAME unknown-field E003 the top-level
81
+ * where path uses. Shared by top-level JSON-path ordering and every nested
82
+ * relation orderBy path so nested orderBy accepts exactly what top-level
83
+ * accepts (the 0.30.x bug: nested orderBy skipped the columnMap and rejected
84
+ * camelCase-named DB columns like "sortOrder").
85
+ */
86
+ export declare function resolveOrderByColumn(_qi: BuilderCtx, table: string, meta: TableMetadata, key: string): string;
87
+ /**
88
+ * Validate a {@link JsonPathOrderBy} entry: column must exist AND be
89
+ * json/jsonb, path must be a non-empty array of keys/indexes: and return
90
+ * the resolved column. Shared by the SQL-build path
91
+ * ({@link buildJsonPathOrderEntry}) and the cache-hit param-collect mirrors
92
+ * so both always throw identically.
93
+ */
94
+ export declare function validateJsonPathOrderBy(qi: BuilderCtx, table: string, meta: TableMetadata, field: string, spec: JsonPathOrderBy): string;
95
+ /**
96
+ * Compile one {@link JsonPathOrderBy} entry:
97
+ * `("col" #>> $n::text[])::numeric ASC`: the numeric cast only with
98
+ * `type: 'numeric'` (default is text comparison), the extraction routed
99
+ * through the dialect's JSON hook exactly like the JSON where-filters, the
100
+ * path bound as ONE text[] param (mirrored by the order-param collectors).
101
+ * `prefix` scopes the column (`''` top-level, `t0.` inside a relation
102
+ * subquery).
103
+ */
104
+ export declare function buildJsonPathOrderEntry(qi: BuilderCtx, table: string, meta: TableMetadata, field: string, spec: JsonPathOrderBy, prefix: string, params?: unknown[]): string;
105
+ /**
106
+ * Compile a relation ordering term. For a to-many relation the only allowed
107
+ * key is `_count`, which becomes a correlated `COUNT(*)` subquery. For a
108
+ * to-one relation each entry names a target column and becomes a correlated
109
+ * scalar subquery (supporting {@link OrderBySpec} nulls placement).
110
+ *
111
+ * Validation: relation must exist (E005); to-many only allows `_count`, and
112
+ * to-one only allows real target columns (E003).
113
+ *
114
+ * `ctx` generalizes the term beyond the root table: inside a relation
115
+ * subquery's orderBy the relations live on the TARGET table's metadata and
116
+ * the correlation parent is the relation's alias, not `qi.table`.
117
+ */
118
+ export declare function buildRelationOrderBy(qi: BuilderCtx, relName: string, value: Record<string, unknown>, alias: string, params?: unknown[], ctx?: {
119
+ meta: TableMetadata;
120
+ table: string;
121
+ parentRef: string;
122
+ }, lateralSink?: string[]): string;
123
+ /**
124
+ * Validate a {@link RelationPickOrderBy} entry's scope and shape. Shared by
125
+ * the SQL-build path ({@link buildRelationPickOrderBy}) and the cache-hit
126
+ * param-collect mirror ({@link collectRelationPickOrderParams}) so both
127
+ * always throw identically:
128
+ *
129
+ * - `nested` (inside a relation subquery's orderBy or a pick.orderBy):
130
+ * top-level findMany only in this release (E003),
131
+ * - manyToMany: not supported (E003 naming the limitation),
132
+ * - to-one: order by the target column directly instead (E003),
133
+ * - `pick.orderBy` is REQUIRED (deterministic row choice),
134
+ * - `by` must be a target column name or a `{ field, path }` JSON-path spec.
135
+ */
136
+ export declare function pickOrderNestedError(_qi: BuilderCtx, relName: string): ValidationError;
137
+ export declare function validatePickOrderBy(qi: BuilderCtx, relName: string, relDef: RelationDef, spec: RelationPickOrderBy, nested: boolean): void;
138
+ /**
139
+ * Compile a {@link RelationPickOrderBy} term: a correlated scalar subquery
140
+ * that picks ONE related row (`ORDER BY <pick.orderBy> LIMIT 1`, optionally
141
+ * filtered by `pick.where` and the target's global filter) and surfaces one
142
+ * value from it (a plain target column or a JSON-path extraction) as the
143
+ * parent ORDER BY key:
144
+ *
145
+ * ```sql
146
+ * (SELECT ord0."data" #>> $1::text[] FROM "versions" ord0
147
+ * WHERE ord0."instance_id" = "instances"."id" AND ord0."is_current" = $2
148
+ * ORDER BY ord0."created_at" DESC LIMIT 1) ASC NULLS LAST
149
+ * ```
150
+ *
151
+ * Param-push order (mirrored EXACTLY by
152
+ * {@link collectRelationPickOrderParams}): `by` JSON path (if any) →
153
+ * target global filter → `pick.where` → `pick.orderBy` JSON paths.
154
+ */
155
+ export declare function buildRelationPickOrderBy(qi: BuilderCtx, relName: string, relDef: RelationDef, spec: RelationPickOrderBy, alias: string, parentRef: string, params?: unknown[], lateralSink?: string[]): string;
156
+ /**
157
+ * Compile the shared inner pieces of a pick-row ordering against `childAlias`
158
+ * (the table alias the related row is read from): the `by` value expression,
159
+ * the correlation + target global filter + `pick.where` predicate, and the
160
+ * `pick.orderBy` clause. Factored out of {@link buildRelationPickOrderBy} so
161
+ * the subquery and lateral plans build IDENTICAL pieces in the SAME param
162
+ * push order (`by` JSON path → target global filter → `pick.where` →
163
+ * `pick.orderBy` JSON paths), which is why the collect mirror
164
+ * ({@link collectRelationPickOrderParams}) is plan-agnostic.
165
+ */
166
+ export declare function compilePickPieces(qi: BuilderCtx, relDef: RelationDef, targetMeta: TableMetadata, spec: RelationPickOrderBy, childAlias: string, parentRef: string, params: unknown[]): {
167
+ byExpr: string;
168
+ where: string;
169
+ orderClause: string;
170
+ };
171
+ /**
172
+ * Param-collect mirror of {@link buildRelationPickOrderBy}: re-runs the same
173
+ * validation (a warmed cache can never skip it), then pushes in the same
174
+ * order: `by` JSON path → target global filter → `pick.where` →
175
+ * `pick.orderBy` JSON paths.
176
+ */
177
+ export declare function collectRelationPickOrderParams(qi: BuilderCtx, relName: string, relDef: RelationDef, spec: RelationPickOrderBy, params: unknown[]): void;
178
+ /**
179
+ * Compile the ORDER BY terms of a relation `with` clause against the
180
+ * relation's table alias. One unified path for every relation shape
181
+ * (hasMany / manyToMany / belongsTo / hasOne) supporting exactly what the
182
+ * top-level orderBy accepts at this level:
183
+ *
184
+ * - scalar columns via columnMap resolution (camelToSnake fallback) with
185
+ * {@link OrderBySpec} nulls placement,
186
+ * - {@link JsonPathOrderBy} entries (path bound as one text[] param),
187
+ * - relation ordering on the TARGET's relations (`_count` for to-many, a
188
+ * target column for to-one), correlated to the relation alias,
189
+ * - vector KNN ordering stays top-level-only (E003, same as before).
190
+ *
191
+ * Param pushes (JSON paths, relation-order global filters) MUST be mirrored,
192
+ * in the same order, by {@link collectRelationOrderParams}.
193
+ */
194
+ export declare function buildRelationOrderClause(qi: BuilderCtx, targetTable: string, targetMeta: TableMetadata, alias: string, orderEntries: [string, unknown][], params: unknown[]): string;
195
+ /**
196
+ * Param-collect mirror of {@link buildRelationOrderClause}: JSON-path
197
+ * entries push their path (one text[] param each); relation-order entries
198
+ * mirror {@link collectOrderByParams}' relation branch (count / to-one
199
+ * global-filter params); scalar entries push nothing but re-run the same
200
+ * column validation so a warmed cache can never skip it.
201
+ */
202
+ export declare function collectRelationOrderParams(qi: BuilderCtx, targetTable: string, targetMeta: TableMetadata, orderEntries: [string, unknown][], params: unknown[]): void;
203
+ /**
204
+ * Build a correlated `(SELECT COUNT(*) …)` scalar subquery for a to-many
205
+ * relation, correlated to `parentRef`. hasMany counts child rows via the FK;
206
+ * manyToMany counts junction rows via the source key. Shared by the `_count`
207
+ * `with` key and to-many relation orderBy.
208
+ *
209
+ * When `params` is supplied and the target has a global filter, it is
210
+ * AND-merged so the count only sees surviving rows (a soft-deleted child is
211
+ * not counted): hasMany filters the counted rows directly; manyToMany adds an
212
+ * `EXISTS` on the target through the junction (the junction rows themselves
213
+ * carry no filter). Params are mirrored by {@link collectRelationCountParams}.
214
+ */
215
+ export declare function buildRelationCountExpr(qi: BuilderCtx, relDef: RelationDef, parentRef: string, alias: string, params?: unknown[]): string;
216
+ /**
217
+ * `EXISTS (SELECT 1 FROM <target> <talias> WHERE <join> AND <gf>)` restricting
218
+ * a manyToMany `_count` to targets that survive their global filter. `''` when
219
+ * the target has no filter. Pushes gf params; mirror:
220
+ * {@link collectManyToManyTargetGlobalFilter}.
221
+ */
222
+ export declare function manyToManyTargetGlobalFilterExists(qi: BuilderCtx, relDef: RelationDef, alias: string, jalias: string, params: unknown[]): string;
223
+ /** Param-collect mirror of {@link manyToManyTargetGlobalFilterExists}. */
224
+ export declare function collectManyToManyTargetGlobalFilter(qi: BuilderCtx, relDef: RelationDef, params: unknown[]): void;
225
+ /**
226
+ * Param-collect mirror of {@link buildRelationCountExpr}'s global-filter
227
+ * params (hasMany direct filter, or manyToMany EXISTS-on-target). Only pushes
228
+ * when a filter applies — no-op otherwise.
229
+ */
230
+ export declare function collectRelationCountParams(qi: BuilderCtx, relDef: RelationDef, params: unknown[]): void;
231
+ export declare function getCamelDateFields(qi: BuilderCtx, table: string, meta: TableMetadata): Set<string>;
232
+ /** Parse a row that may contain JSON nested relation columns */
233
+ export declare function parseNestedRow(qi: BuilderCtx, row: Record<string, unknown>, table: string): Record<string, unknown>;
234
+ /**
235
+ * Resolve the emitted column list for a relation, honoring `select` / `omit`.
236
+ * Shared by {@link buildRelationSubquery} (json order) and
237
+ * {@link buildRelationShape} (decode key order) so they can never diverge.
238
+ */
239
+ export declare function resolveTargetColumns(qi: BuilderCtx, spec: true | WithOptions, targetMeta: TableMetadata, includePii?: boolean): string[];
240
+ /**
241
+ * Render a single relation row's JSON: a keyed object (`'object'`) or a
242
+ * positional array (`'positional'`). The array drops the keys but keeps the
243
+ * exact expression order, so {@link RelationShape.keys} maps positions back.
244
+ */
245
+ export declare function buildJsonRow(qi: BuilderCtx, jsonPairs: [key: string, expr: string][]): string;
246
+ /**
247
+ * Build the top-level relation shapes for a `with` clause, mirroring
248
+ * {@link buildSelectWithRelations}: same relation iteration order, same
249
+ * per-relation column resolution, same nested recursion.
250
+ */
251
+ export declare function buildRelationShapes(qi: BuilderCtx, table: string, withClause: WithClause, includePii?: boolean): Record<string, RelationShape>;
252
+ /**
253
+ * Recursively describe one relation's positional layout: the camelCase key
254
+ * order (scalar columns first, then nested relation slots in the same order
255
+ * {@link buildRelationSubquery} appends them), the nested sub-shapes, and the
256
+ * cardinality (single object for belongsTo/hasOne, array for the rest).
257
+ */
258
+ export declare function buildRelationShape(qi: BuilderCtx, relDef: RelationDef, spec: true | WithOptions, parentMeta: TableMetadata, includePii?: boolean): RelationShape;
259
+ /**
260
+ * Build the row parser for a `with` clause. In object mode this is just
261
+ * {@link parseNestedRow}. In positional mode it decodes each relation's
262
+ * positional arrays into the object form first (shapes built once, not per
263
+ * row), then delegates to parseNestedRow for date/snake-camel coercion.
264
+ */
265
+ export declare function makeNestedParser(qi: BuilderCtx, withClause: WithClause, includePii?: boolean): (row: Record<string, unknown>) => Record<string, unknown>;
266
+ /**
267
+ * Return a shallow copy of a top-level row with each relation column decoded
268
+ * from its positional array(s) into the object representation. Only relation
269
+ * columns are positional — base scalar columns stay object-keyed — so the
270
+ * result is exactly what the object encoding would have handed parseNestedRow.
271
+ */
272
+ export declare function decodePositionalRelations(qi: BuilderCtx, row: Record<string, unknown>, shapes: Record<string, RelationShape>): Record<string, unknown>;
273
+ /**
274
+ * Decode one relation's positional JSON value. `json_agg` returns the value as
275
+ * a JSON string at the top level (JSON.parse once); nested relation slots are
276
+ * already-parsed arrays. A `'many'` value is an array of positional arrays; a
277
+ * `'one'` value is a single positional array or null.
278
+ */
279
+ export declare function decodePositionalValue(qi: BuilderCtx, raw: unknown, shape: RelationShape): unknown;
280
+ /** Map one positional array back to a keyed object using the shape's key order. */
281
+ export declare function decodePositionalObject(qi: BuilderCtx, arr: unknown, shape: RelationShape): unknown;
282
+ /**
283
+ * Build a SELECT clause that includes both base columns and nested relation subqueries.
284
+ *
285
+ * For each relation specified in the `with` clause, this method generates a correlated
286
+ * subquery using PostgreSQL's `json_agg(json_build_object(...))` pattern. The result
287
+ * is a single SQL SELECT clause that resolves the full object tree in one query --
288
+ * no N+1 problem.
289
+ *
290
+ * **How it works:**
291
+ * 1. Resolves the base columns for the root table (all columns, or a subset via `columnsList`).
292
+ * 2. Iterates over each key in the `with` clause, looking up the relation definition.
293
+ * 3. For each relation, delegates to {@link buildRelationSubquery} to generate a
294
+ * correlated subquery that returns JSON (array for hasMany, object for belongsTo/hasOne).
295
+ * 4. Each subquery is aliased as the relation name in the final SELECT.
296
+ *
297
+ * **aliasCounter:** A shared `{ n: number }` object is passed through all nesting levels.
298
+ * Each call to `buildRelationSubquery` increments it to produce unique table aliases
299
+ * (`t0`, `t1`, `t2`, ...) across arbitrarily deep relation trees, preventing alias
300
+ * collisions in the generated SQL.
301
+ *
302
+ * **Example output:**
303
+ * ```sql
304
+ * "users"."id", "users"."name", "users"."email",
305
+ * (SELECT COALESCE(json_agg(json_build_object('id', t0."id", 'title', t0."title")), '[]'::json)
306
+ * FROM "posts" t0 WHERE t0."user_id" = "users"."id") AS "posts"
307
+ * ```
308
+ *
309
+ * @param table - The root table name (e.g. `"users"`).
310
+ * @param withClause - An object mapping relation names to their include specs
311
+ * (`true` for default inclusion, or `WithOptions` for select/omit/where/orderBy/limit).
312
+ * @param params - Shared parameter array for parameterized values (`$1`, `$2`, ...).
313
+ * Nested where/limit values are pushed here to prevent SQL injection.
314
+ * @param columnsList - Optional subset of columns to include in the SELECT. When `null`
315
+ * or omitted, all columns from the table's schema metadata are used.
316
+ * @param depth - Current nesting depth, passed through to {@link buildRelationSubquery}
317
+ * for circular-relation detection. Defaults to `0` at the top level.
318
+ * @param path - Breadcrumb trail of relation names traversed so far, used in error
319
+ * messages when circular or too-deep nesting is detected.
320
+ * @returns A complete SELECT clause string (without the `SELECT` keyword) containing
321
+ * base columns and relation subqueries.
322
+ */
323
+ export declare function buildSelectWithRelations(qi: BuilderCtx, table: string, withClause: WithClause, params: unknown[], columnsList?: string[] | null, depth?: number, path?: string[], includePii?: boolean): string;
324
+ /**
325
+ * Generate a correlated subquery that returns JSON for a single relation.
326
+ *
327
+ * This is the core of Turbine's single-query nested relation strategy. For a given
328
+ * relation (e.g. `posts` on a `users` query), it produces a self-contained SQL subquery
329
+ * that PostgreSQL evaluates per parent row, returning either a JSON array (hasMany) or
330
+ * a single JSON object (belongsTo / hasOne).
331
+ *
332
+ * ### Algorithm overview
333
+ *
334
+ * 1. **Alias generation:** Allocates a unique alias (`t0`, `t1`, ...) from the shared
335
+ * `aliasCounter` so that deeply nested subqueries never collide.
336
+ *
337
+ * 2. **Column resolution:** Honors `select` / `omit` options to control which columns
338
+ * appear in the output JSON.
339
+ *
340
+ * 3. **`json_build_object`:** Builds a JSON object for each row by mapping camelCase
341
+ * field names to their column values:
342
+ * ```sql
343
+ * json_build_object('id', t0."id", 'title', t0."title", 'createdAt', t0."created_at")
344
+ * ```
345
+ *
346
+ * 4. **`json_agg` wrapping (hasMany):** For one-to-many relations, wraps the
347
+ * `json_build_object` call in `json_agg(...)` to aggregate all matching child rows
348
+ * into a JSON array. Uses `COALESCE(..., '[]'::json)` so the result is never NULL.
349
+ * For belongsTo / hasOne, no aggregation is used -- just the single JSON object
350
+ * with `LIMIT 1`.
351
+ *
352
+ * 5. **Correlation (WHERE clause):** Links the subquery to the parent row:
353
+ * - **hasMany:** `alias.foreignKey = parentRef.referenceKey`
354
+ * (e.g. `t0."user_id" = "users"."id"` -- child FK points to parent PK)
355
+ * - **belongsTo / hasOne:** `alias.referenceKey = parentRef.foreignKey`
356
+ * (e.g. `t0."id" = "posts"."author_id"` -- parent FK points to child PK)
357
+ *
358
+ * 6. **Recursion:** If the spec includes a nested `with` clause, this method calls
359
+ * itself recursively for each nested relation, passing the current alias as
360
+ * `parentRef`. The nested subquery appears as an additional key in the
361
+ * `json_build_object` call, wrapped in `COALESCE(..., '[]'::json)`.
362
+ * Depth is incremented and capped at 10 to guard against circular relations.
363
+ *
364
+ * 7. **LIMIT / ORDER BY wrapping:** For hasMany relations with `limit` or `orderBy`,
365
+ * the query is restructured into a two-level form:
366
+ * ```sql
367
+ * SELECT COALESCE(json_agg(json_build_object(...)), '[]'::json)
368
+ * FROM (
369
+ * SELECT t0.* FROM "posts" t0
370
+ * WHERE t0."user_id" = "users"."id"
371
+ * ORDER BY t0."created_at" DESC
372
+ * LIMIT $1
373
+ * ) t0i
374
+ * ```
375
+ * This ensures LIMIT and ORDER BY apply to the raw rows *before* `json_agg`
376
+ * aggregation. Without the inner subquery, LIMIT would be meaningless because
377
+ * `json_agg` produces a single aggregated row.
378
+ *
379
+ * 8. **Parameter threading:** All user-supplied values (where filters, limit) are
380
+ * pushed to the shared `params` array with `$N` placeholders. No string
381
+ * interpolation of user data ever occurs -- all identifiers go through
382
+ * `qi.q()` and all values are parameterized.
383
+ *
384
+ * ### Example output (hasMany with nested relation)
385
+ * ```sql
386
+ * SELECT COALESCE(json_agg(json_build_object(
387
+ * 'id', t0."id",
388
+ * 'title', t0."title",
389
+ * 'comments', COALESCE((
390
+ * SELECT COALESCE(json_agg(json_build_object('id', t1."id", 'body', t1."body")), '[]'::json)
391
+ * FROM "comments" t1 WHERE t1."post_id" = t0."id"
392
+ * ), '[]'::json)
393
+ * )), '[]'::json) FROM "posts" t0 WHERE t0."user_id" = "users"."id"
394
+ * ```
395
+ *
396
+ * @param relDef - The relation definition from schema metadata (contains `to`, `type`,
397
+ * `foreignKey`, `referenceKey`).
398
+ * @param spec - Either `true` (include with defaults) or a `WithOptions` object that
399
+ * can specify `select`, `omit`, `where`, `orderBy`, `limit`, and nested `with`.
400
+ * @param params - Shared parameter array. User-supplied values are pushed here and
401
+ * referenced as `$1`, `$2`, etc. in the generated SQL.
402
+ * @param parentRef - The alias (e.g. `"t0"`) or table name (e.g. `"users"`) of the
403
+ * parent query. Used to build the correlated WHERE clause that ties
404
+ * child rows to their parent row.
405
+ * @param aliasCounter - Shared mutable counter (`{ n: number }`) for generating unique
406
+ * table aliases (`t0`, `t1`, `t2`, ...) across all nesting levels.
407
+ * Each call increments `n` by 1.
408
+ * @param depth - Current nesting depth (starts at `0`). Incremented on each recursive
409
+ * call. If it reaches 10, a {@link CircularRelationError} is thrown.
410
+ * @param path - Breadcrumb trail of relation/table names traversed so far
411
+ * (e.g. `["users", "posts", "comments"]`). Used in the error message
412
+ * when circular or too-deep nesting is detected.
413
+ * @returns A complete SQL subquery string (without surrounding parentheses) that
414
+ * evaluates to a JSON array (hasMany) or a JSON object (belongsTo/hasOne).
415
+ */
416
+ export declare function buildRelationSubquery(qi: BuilderCtx, relDef: RelationDef, spec: true | WithOptions, params: unknown[], parentRef: string, aliasCounter: {
417
+ n: number;
418
+ }, depth?: number, path?: string[], includePii?: boolean): string;
419
+ /**
420
+ * Build the json_agg subquery for a `manyToMany` relation, JOINing the target
421
+ * table through a junction (join) table.
422
+ *
423
+ * Shape (no LIMIT/ORDER):
424
+ * ```sql
425
+ * SELECT COALESCE(json_agg(json_build_object(...)), '[]'::json)
426
+ * FROM <target> <talias>
427
+ * JOIN <junction> <jalias> ON <jalias>.<targetKey> = <talias>.<targetPK>
428
+ * WHERE <jalias>.<sourceKey> = <parentRef>.<referenceKey>
429
+ * ```
430
+ *
431
+ * With LIMIT/ORDER, the rows are wrapped in an inner subquery so the LIMIT
432
+ * applies BEFORE aggregation (identical strategy to hasMany).
433
+ *
434
+ * Cardinality is always 'many' → empty-array fallback, never NULL.
435
+ *
436
+ * IMPORTANT: every `params.push` here MUST be mirrored, in the same order, in
437
+ * {@link collectRelationSubqueryParams} or pipeline batching will desync.
438
+ */
439
+ export declare function buildManyToManySubquery(qi: BuilderCtx, relDef: RelationDef, spec: true | WithOptions, params: unknown[], parentRef: string, aliasCounter: {
440
+ n: number;
441
+ }, currentDepth: number, currentPath: string[], talias: string, targetMeta: TableMetadata, targetColumns: string[], includePii?: boolean): string;