turbine-orm 0.49.0 → 0.50.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.
- package/README.md +122 -39
- package/dist/cjs/adapters/cockroachdb.d.ts +39 -0
- package/dist/cjs/adapters/index.d.ts +110 -0
- package/dist/cjs/adapters/yugabytedb.d.ts +51 -0
- package/dist/cjs/cli/config.d.ts +181 -0
- package/dist/cjs/cli/config.js +32 -6
- package/dist/cjs/cli/destructive.d.ts +38 -0
- package/dist/cjs/cli/index.d.ts +359 -0
- package/dist/cjs/cli/index.js +228 -56
- package/dist/cjs/cli/loader.d.ts +61 -0
- package/dist/cjs/cli/mcp.d.ts +42 -0
- package/dist/cjs/cli/migrate.d.ts +356 -0
- package/dist/cjs/cli/migrate.js +131 -40
- package/dist/cjs/cli/observe-ui.d.ts +1 -0
- package/dist/cjs/cli/observe-ui.js +14 -5
- package/dist/cjs/cli/observe.d.ts +25 -0
- package/dist/cjs/cli/observe.js +49 -12
- package/dist/cjs/cli/pii-tags.d.ts +53 -0
- package/dist/cjs/cli/prisma-report.d.ts +33 -0
- package/dist/cjs/cli/prisma-report.js +73 -0
- package/dist/cjs/cli/prisma-resolve.d.ts +106 -0
- package/dist/cjs/cli/prisma-resolve.js +1 -0
- package/dist/cjs/cli/prisma-schema.d.ts +176 -0
- package/dist/cjs/cli/prisma-schema.js +82 -4
- package/dist/cjs/cli/rate-limit.d.ts +32 -0
- package/dist/cjs/cli/rate-limit.js +45 -0
- package/dist/cjs/cli/studio-demo.d.ts +43 -0
- package/dist/cjs/cli/studio-ui.generated.d.ts +1 -0
- package/dist/cjs/cli/studio.d.ts +207 -0
- package/dist/cjs/cli/studio.js +136 -71
- package/dist/cjs/cli/ui.d.ts +73 -0
- package/dist/cjs/cli/ui.js +51 -9
- package/dist/cjs/client.d.ts +837 -0
- package/dist/cjs/client.js +3 -0
- package/dist/cjs/dialect.d.ts +516 -0
- package/dist/cjs/dialect.js +37 -12
- package/dist/cjs/errors.d.ts +370 -0
- package/dist/cjs/generate.d.ts +137 -0
- package/dist/cjs/generate.js +39 -6
- package/dist/cjs/index-advisor.d.ts +153 -0
- package/dist/cjs/index-stats.d.ts +384 -0
- package/dist/cjs/index.d.ts +55 -0
- package/dist/cjs/index.js +7 -2
- package/dist/cjs/introspect.d.ts +269 -0
- package/dist/cjs/mssql.d.ts +232 -0
- package/dist/cjs/mssql.js +6 -0
- package/dist/cjs/mysql.d.ts +173 -0
- package/dist/cjs/mysql.js +16 -0
- package/dist/cjs/nested-write.d.ts +96 -0
- package/dist/cjs/nested-write.js +414 -24
- package/dist/cjs/observe.d.ts +115 -0
- package/dist/cjs/optional-peer-import.d.cts +72 -0
- package/dist/cjs/pipeline-submittable.d.ts +93 -0
- package/dist/cjs/pipeline.d.ts +71 -0
- package/dist/cjs/powdb-introspect.d.ts +84 -0
- package/dist/cjs/powdb.d.ts +931 -0
- package/dist/cjs/powdb.js +106 -21
- package/dist/cjs/powql.d.ts +592 -0
- package/dist/cjs/powql.js +42 -6
- package/dist/cjs/prisma-compat.d.ts +283 -0
- package/dist/cjs/prisma-compat.js +167 -9
- package/dist/cjs/query/aggregates.d.ts +92 -0
- package/dist/cjs/query/aggregates.js +7 -3
- package/dist/cjs/query/batched-loader.d.ts +193 -0
- package/dist/cjs/query/builder.d.ts +849 -0
- package/dist/cjs/query/builder.js +571 -65
- package/dist/cjs/query/compound-unique.d.ts +51 -0
- package/dist/cjs/query/deferred.d.ts +223 -0
- package/dist/cjs/query/filters.d.ts +201 -0
- package/dist/cjs/query/index.d.ts +14 -0
- package/dist/cjs/query/index.js +6 -1
- package/dist/cjs/query/relations.d.ts +609 -0
- package/dist/cjs/query/relations.js +693 -46
- package/dist/cjs/query/types.d.ts +1300 -0
- package/dist/cjs/query/utils.d.ts +209 -0
- package/dist/cjs/query/utils.js +208 -1
- package/dist/cjs/query/warn-registry.d.ts +68 -0
- package/dist/cjs/query/warn-registry.js +9 -0
- package/dist/cjs/query/where-compile.d.ts +139 -0
- package/dist/cjs/query/where.d.ts +548 -0
- package/dist/cjs/query/where.js +58 -22
- package/dist/cjs/query/writes.d.ts +172 -0
- package/dist/cjs/query/writes.js +105 -12
- package/dist/cjs/realtime.d.ts +70 -0
- package/dist/cjs/schema-builder.d.ts +354 -0
- package/dist/cjs/schema-metadata.d.ts +83 -0
- package/dist/cjs/schema-sql.d.ts +217 -0
- package/dist/cjs/schema-sql.js +23 -5
- package/dist/cjs/schema.d.ts +356 -0
- package/dist/cjs/schema.js +125 -0
- package/dist/cjs/seed.d.ts +15 -0
- package/dist/cjs/serverless.d.ts +142 -0
- package/dist/cjs/sqlite.d.ts +143 -0
- package/dist/cjs/sqlite.js +4 -0
- package/dist/cjs/typed-sql.d.ts +102 -0
- package/dist/cli/config.d.ts +18 -4
- package/dist/cli/config.js +31 -6
- package/dist/cli/index.d.ts +123 -0
- package/dist/cli/index.js +223 -58
- package/dist/cli/migrate.d.ts +59 -10
- package/dist/cli/migrate.js +128 -41
- package/dist/cli/observe-ui.d.ts +1 -1
- package/dist/cli/observe-ui.js +14 -5
- package/dist/cli/observe.d.ts +7 -1
- package/dist/cli/observe.js +48 -12
- package/dist/cli/prisma-report.d.ts +14 -0
- package/dist/cli/prisma-report.js +72 -0
- package/dist/cli/prisma-resolve.d.ts +6 -0
- package/dist/cli/prisma-resolve.js +1 -0
- package/dist/cli/prisma-schema.d.ts +62 -2
- package/dist/cli/prisma-schema.js +81 -4
- package/dist/cli/rate-limit.d.ts +32 -0
- package/dist/cli/rate-limit.js +40 -0
- package/dist/cli/studio.d.ts +5 -5
- package/dist/cli/studio.js +135 -70
- package/dist/cli/ui.d.ts +1 -1
- package/dist/cli/ui.js +51 -9
- package/dist/client.d.ts +40 -0
- package/dist/client.js +3 -0
- package/dist/dialect.d.ts +17 -1
- package/dist/dialect.js +37 -12
- package/dist/generate.js +40 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mssql.js +6 -0
- package/dist/mysql.js +16 -0
- package/dist/nested-write.d.ts +2 -0
- package/dist/nested-write.js +415 -25
- package/dist/powdb.d.ts +4 -2
- package/dist/powdb.js +106 -21
- package/dist/powql.d.ts +5 -0
- package/dist/powql.js +42 -6
- package/dist/prisma-compat.d.ts +2 -0
- package/dist/prisma-compat.js +166 -8
- package/dist/query/aggregates.js +7 -3
- package/dist/query/builder.d.ts +292 -21
- package/dist/query/builder.js +570 -64
- package/dist/query/deferred.d.ts +39 -0
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.js +1 -1
- package/dist/query/relations.d.ts +173 -5
- package/dist/query/relations.js +688 -47
- package/dist/query/types.d.ts +123 -39
- package/dist/query/utils.d.ts +116 -0
- package/dist/query/utils.js +198 -0
- package/dist/query/warn-registry.d.ts +9 -0
- package/dist/query/warn-registry.js +9 -0
- package/dist/query/where.d.ts +38 -1
- package/dist/query/where.js +58 -23
- package/dist/query/writes.d.ts +42 -1
- package/dist/query/writes.js +104 -13
- package/dist/schema-sql.d.ts +14 -0
- package/dist/schema-sql.js +23 -5
- package/dist/schema.d.ts +38 -0
- package/dist/schema.js +123 -0
- package/dist/sqlite.js +4 -0
- package/package.json +77 -28
|
@@ -0,0 +1,609 @@
|
|
|
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, flattenPlan?: FlattenPlan | null): 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
|
+
/**
|
|
233
|
+
* Parse a row that may contain JSON nested relation columns.
|
|
234
|
+
*
|
|
235
|
+
* `fromJson` says where THIS row's own scalar columns came from. A root row
|
|
236
|
+
* (join, batched, flatten) is read straight off the driver, so `false`; a row
|
|
237
|
+
* decoded out of a `json_agg`/`json_build_object` column is `true` and gets
|
|
238
|
+
* its divergent scalars decoded back to the driver's representation first (see
|
|
239
|
+
* the JSON-wire section above). Recursion into a relation column is always
|
|
240
|
+
* `true`, which is exactly right on every strategy: `batched` and `flatten`
|
|
241
|
+
* hand this function driver rows, but any relation still nested INSIDE one of
|
|
242
|
+
* those rows arrived as a correlated JSON subquery.
|
|
243
|
+
*/
|
|
244
|
+
export declare function parseNestedRow(qi: BuilderCtx, row: Record<string, unknown>, table: string, fromJson?: boolean): Record<string, unknown>;
|
|
245
|
+
/**
|
|
246
|
+
* Resolve the emitted column list for a relation, honoring `select` / `omit`.
|
|
247
|
+
* Shared by {@link buildRelationSubquery} (json order) and
|
|
248
|
+
* {@link buildRelationShape} (decode key order) so they can never diverge.
|
|
249
|
+
*/
|
|
250
|
+
export declare function resolveTargetColumns(qi: BuilderCtx, spec: true | WithOptions, targetMeta: TableMetadata, includePii?: boolean): string[];
|
|
251
|
+
/**
|
|
252
|
+
* Render a single relation row's JSON: a keyed object (`'object'`) or a
|
|
253
|
+
* positional array (`'positional'`). The array drops the keys but keeps the
|
|
254
|
+
* exact expression order, so {@link RelationShape.keys} maps positions back.
|
|
255
|
+
*/
|
|
256
|
+
export declare function buildJsonRow(qi: BuilderCtx, jsonPairs: [key: string, expr: string][]): string;
|
|
257
|
+
/**
|
|
258
|
+
* Build the top-level relation shapes for a `with` clause, mirroring
|
|
259
|
+
* {@link buildSelectWithRelations}: same relation iteration order, same
|
|
260
|
+
* per-relation column resolution, same nested recursion.
|
|
261
|
+
*/
|
|
262
|
+
export declare function buildRelationShapes(qi: BuilderCtx, table: string, withClause: WithClause, includePii?: boolean): Record<string, RelationShape>;
|
|
263
|
+
/**
|
|
264
|
+
* Recursively describe one relation's positional layout: the camelCase key
|
|
265
|
+
* order (scalar columns first, then nested relation slots in the same order
|
|
266
|
+
* {@link buildRelationSubquery} appends them), the nested sub-shapes, and the
|
|
267
|
+
* cardinality (single object for belongsTo/hasOne, array for the rest).
|
|
268
|
+
*/
|
|
269
|
+
export declare function buildRelationShape(qi: BuilderCtx, relDef: RelationDef, spec: true | WithOptions, parentMeta: TableMetadata, includePii?: boolean): RelationShape;
|
|
270
|
+
/**
|
|
271
|
+
* Build the row parser for a `with` clause. In object mode this is just
|
|
272
|
+
* {@link parseNestedRow}. In positional mode it decodes each relation's
|
|
273
|
+
* positional arrays into the object form first (shapes built once, not per
|
|
274
|
+
* row), then delegates to parseNestedRow for date/snake-camel coercion.
|
|
275
|
+
*/
|
|
276
|
+
export declare function makeNestedParser(qi: BuilderCtx, withClause: WithClause, includePii?: boolean, flattenPlan?: FlattenPlan | null): (row: Record<string, unknown>) => Record<string, unknown>;
|
|
277
|
+
/**
|
|
278
|
+
* Return a shallow copy of a top-level row with each relation column decoded
|
|
279
|
+
* from its positional array(s) into the object representation. Only relation
|
|
280
|
+
* columns are positional — base scalar columns stay object-keyed — so the
|
|
281
|
+
* result is exactly what the object encoding would have handed parseNestedRow.
|
|
282
|
+
*/
|
|
283
|
+
export declare function decodePositionalRelations(qi: BuilderCtx, row: Record<string, unknown>, shapes: Record<string, RelationShape>): Record<string, unknown>;
|
|
284
|
+
/**
|
|
285
|
+
* Decode one relation's positional JSON value. `json_agg` returns the value as
|
|
286
|
+
* a JSON string at the top level (JSON.parse once); nested relation slots are
|
|
287
|
+
* already-parsed arrays. A `'many'` value is an array of positional arrays; a
|
|
288
|
+
* `'one'` value is a single positional array or null.
|
|
289
|
+
*/
|
|
290
|
+
export declare function decodePositionalValue(qi: BuilderCtx, raw: unknown, shape: RelationShape): unknown;
|
|
291
|
+
/** Map one positional array back to a keyed object using the shape's key order. */
|
|
292
|
+
export declare function decodePositionalObject(qi: BuilderCtx, arr: unknown, shape: RelationShape): unknown;
|
|
293
|
+
/** One relation `'flatten'` declined, with the reason, for the dev warning. */
|
|
294
|
+
export interface FlattenReject {
|
|
295
|
+
/** Dotted path from the root table's `with`, e.g. `order.customer`. */
|
|
296
|
+
relation: string;
|
|
297
|
+
reason: string;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* One entry in a flattened relation's nested `with`, in
|
|
301
|
+
* `sortedEntries(spec.with)` order. `'flat'` is another LEFT JOIN; `'json'` is
|
|
302
|
+
* a correlated subquery projected as a single JSON column, exactly as the
|
|
303
|
+
* default strategy would have emitted it (with the join alias as its parent
|
|
304
|
+
* reference). Ordering is preserved so the assembled object's key order matches
|
|
305
|
+
* the join strategy's byte for byte.
|
|
306
|
+
*/
|
|
307
|
+
export interface FlattenSlot {
|
|
308
|
+
relName: string;
|
|
309
|
+
kind: 'flat' | 'json';
|
|
310
|
+
}
|
|
311
|
+
/** A single to-one relation compiled as a LEFT JOIN plus a prefixed projection. */
|
|
312
|
+
export interface FlattenNode {
|
|
313
|
+
relName: string;
|
|
314
|
+
relDef: RelationDef;
|
|
315
|
+
spec: true | WithOptions;
|
|
316
|
+
targetTable: string;
|
|
317
|
+
targetMeta: TableMetadata;
|
|
318
|
+
/** Output-name prefix, and the derived-table alias for a top-level node (`f0`). */
|
|
319
|
+
alias: string;
|
|
320
|
+
/** Alias of the real target table inside the derived table (`f0s`). */
|
|
321
|
+
srcAlias: string;
|
|
322
|
+
/** Target-side correlation columns (provably unique — see {@link provableUniqueTargetKey}). */
|
|
323
|
+
keyColumns: string[];
|
|
324
|
+
/**
|
|
325
|
+
* Match discriminator. A top-level node projects the constant `1` (its
|
|
326
|
+
* derived-table row exists only when the target matched, and the outer LEFT
|
|
327
|
+
* JOIN null-extends it otherwise); a nested node projects the predicate
|
|
328
|
+
* `(src.key IS NOT NULL)`. Either way the discriminator carries no cell
|
|
329
|
+
* VALUE, so a PII-tagged key column never leaves the database merely because
|
|
330
|
+
* a discriminator was needed: no PII column enters the wire, the projection
|
|
331
|
+
* or the assembled object unless the caller selected it or passed
|
|
332
|
+
* `includePii`. Without it, "the join matched nothing" would be
|
|
333
|
+
* indistinguishable from "the row exists and every projected column is NULL".
|
|
334
|
+
*/
|
|
335
|
+
discAlias: string;
|
|
336
|
+
/** `[snake_case column, output alias]` for every projected column. */
|
|
337
|
+
cols: [column: string, sqlAlias: string][];
|
|
338
|
+
/** Nested `with` entries in emission order. */
|
|
339
|
+
slots: FlattenSlot[];
|
|
340
|
+
/** Nested flattened relations, keyed by relation name (the `'flat'` slots). */
|
|
341
|
+
children: Record<string, FlattenNode>;
|
|
342
|
+
/** Output alias of each `'json'` slot's correlated subquery column. */
|
|
343
|
+
jsonAliases: Record<string, string>;
|
|
344
|
+
/**
|
|
345
|
+
* Top-level nodes only: the correlation columns re-exposed by the derived
|
|
346
|
+
* table (`f0__$c0`, ...) so the OUTER join condition can reference them
|
|
347
|
+
* without the real column names entering the outer scope. Never surfaced to
|
|
348
|
+
* the caller.
|
|
349
|
+
*/
|
|
350
|
+
corrAliases: string[];
|
|
351
|
+
/** Nesting depth, matching {@link buildRelationSubquery}'s depth accounting. */
|
|
352
|
+
depth: number;
|
|
353
|
+
/** Breadcrumb trail for {@link CircularRelationError} parity. */
|
|
354
|
+
path: string[];
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* The compiled `'flatten'` plan for one `with` clause: which top-level
|
|
358
|
+
* relations become LEFT JOINs, and a signature that keys the SQL template
|
|
359
|
+
* cache. Relations absent from `nodes` compile exactly as they do today.
|
|
360
|
+
*/
|
|
361
|
+
export interface FlattenPlan {
|
|
362
|
+
nodes: Record<string, FlattenNode>;
|
|
363
|
+
/** Every SELECT-list alias the plan introduces (flat rows carry them all). */
|
|
364
|
+
aliases: string[];
|
|
365
|
+
/** Structural signature for the `fl=` SQL-cache-key segment. */
|
|
366
|
+
signature: string;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* The target-side correlation columns of a to-one relation, but ONLY when the
|
|
370
|
+
* schema PROVES they are unique on the target table. This is the row-
|
|
371
|
+
* multiplication guard: a LEFT JOIN over a non-unique key silently duplicates
|
|
372
|
+
* parent rows, which would change results rather than just the plan.
|
|
373
|
+
*
|
|
374
|
+
* Proof sources, all exact set matches (a unique index on `(a, b)` does NOT
|
|
375
|
+
* make `a` unique):
|
|
376
|
+
* - the target's primary key,
|
|
377
|
+
* - a declared unique constraint (`uniqueColumns`),
|
|
378
|
+
* - a full, non-expression UNIQUE index. Partial unique indexes are refused
|
|
379
|
+
* (they only constrain the rows matching their predicate) and PowDB
|
|
380
|
+
* doc-field expression indexes are refused (they index a JSON path, not the
|
|
381
|
+
* raw column).
|
|
382
|
+
*
|
|
383
|
+
* Returns `null` for to-many / manyToMany relations and for anything it cannot
|
|
384
|
+
* prove, which routes the relation back to the correlated subquery.
|
|
385
|
+
*
|
|
386
|
+
* NULLs need no special handling: a unique constraint permits repeated NULLs,
|
|
387
|
+
* but `target.key = parent.fk` never matches a NULL key, so a null-keyed target
|
|
388
|
+
* row can never join.
|
|
389
|
+
*/
|
|
390
|
+
export declare function provableUniqueTargetKey(relDef: RelationDef, targetMeta: TableMetadata): string[] | null;
|
|
391
|
+
/**
|
|
392
|
+
* Compile the `'flatten'` plan for a top-level `with` clause, or return `null`
|
|
393
|
+
* when nothing in it is eligible (in which case the caller emits exactly the SQL
|
|
394
|
+
* it emits today, down to the cache key).
|
|
395
|
+
*
|
|
396
|
+
* The plan is a pure function of the schema, the `with` clause shape and
|
|
397
|
+
* `includePii` — never of any bound value — so the build path, the cache-hit
|
|
398
|
+
* param-collect path and the row assembler can each recompute it and agree.
|
|
399
|
+
*/
|
|
400
|
+
export declare function planFlattenWith(qi: BuilderCtx, table: string, withClause: WithClause, includePii?: boolean): FlattenPlan | null;
|
|
401
|
+
/**
|
|
402
|
+
* Emit one TOP-LEVEL flattened relation: a derived table holding its whole
|
|
403
|
+
* to-one subtree, joined to the parent on the re-exposed correlation columns,
|
|
404
|
+
* plus the pass-through projection of every name that subtree contributes.
|
|
405
|
+
*
|
|
406
|
+
* ```sql
|
|
407
|
+
* LEFT JOIN (
|
|
408
|
+
* SELECT 1 AS "f0__$k", f0s."id" AS "f0__$c0",
|
|
409
|
+
* f0s."id" AS "f0__id", f0s."name" AS "f0__name",
|
|
410
|
+
* (f1s."id" IS NOT NULL) AS "f1__$k", f1s."code" AS "f1__code"
|
|
411
|
+
* FROM "orgs" f0s
|
|
412
|
+
* LEFT JOIN "regions" f1s ON f1s."id" = f0s."region_id"
|
|
413
|
+
* WHERE f0s."deleted" = $1
|
|
414
|
+
* ) f0 ON f0."f0__$c0" = "users"."org_id"
|
|
415
|
+
* ```
|
|
416
|
+
*
|
|
417
|
+
* See the derived-table note above for why the join cannot expose the target's
|
|
418
|
+
* real column names.
|
|
419
|
+
*/
|
|
420
|
+
export declare function emitFlattenNode(qi: BuilderCtx, node: FlattenNode, parentRef: string, params: unknown[], joinSink: string[], selectSink: string[], aliasCounter: {
|
|
421
|
+
n: number;
|
|
422
|
+
}, includePii?: boolean): void;
|
|
423
|
+
/** Param-collect mirror of {@link emitFlattenNode}. */
|
|
424
|
+
export declare function collectFlattenNodeParams(qi: BuilderCtx, node: FlattenNode, params: unknown[]): void;
|
|
425
|
+
/**
|
|
426
|
+
* Rebuild one flattened relation's object from a flat row.
|
|
427
|
+
*
|
|
428
|
+
* Returns `null` when the discriminator is NULL (the LEFT JOIN matched
|
|
429
|
+
* nothing), which is the ONLY signal that distinguishes "no related row" from
|
|
430
|
+
* "a related row whose every projected column is NULL".
|
|
431
|
+
*
|
|
432
|
+
* The sub-row is keyed by the target's raw snake_case column names and handed
|
|
433
|
+
* to {@link parseNestedRow}, so the flattened object goes through the very same
|
|
434
|
+
* camelCase mapping, Date coercion and nested-JSON parsing the join strategy's
|
|
435
|
+
* `json_build_object` output does. Nested flattened relations are assigned
|
|
436
|
+
* after the parse, over placeholder keys inserted in slot order, so the
|
|
437
|
+
* assembled object's key order matches the join strategy's exactly.
|
|
438
|
+
*/
|
|
439
|
+
export declare function assembleFlattenNode(qi: BuilderCtx, row: Record<string, unknown>, node: FlattenNode): Record<string, unknown> | null;
|
|
440
|
+
/**
|
|
441
|
+
* Row parser for a `'flatten'` plan: strip the prefixed join columns out of the
|
|
442
|
+
* flat row (inserting a placeholder at the position each relation's block
|
|
443
|
+
* started, so key order is preserved), parse the remainder exactly as the join
|
|
444
|
+
* strategy does, then assemble each flattened relation.
|
|
445
|
+
*/
|
|
446
|
+
export declare function makeFlattenParser(qi: BuilderCtx, plan: FlattenPlan): (row: Record<string, unknown>) => Record<string, unknown>;
|
|
447
|
+
/**
|
|
448
|
+
* Build a SELECT clause that includes both base columns and nested relation subqueries.
|
|
449
|
+
*
|
|
450
|
+
* For each relation specified in the `with` clause, this method generates a correlated
|
|
451
|
+
* subquery using PostgreSQL's `json_agg(json_build_object(...))` pattern. The result
|
|
452
|
+
* is a single SQL SELECT clause that resolves the full object tree in one query --
|
|
453
|
+
* no N+1 problem.
|
|
454
|
+
*
|
|
455
|
+
* **How it works:**
|
|
456
|
+
* 1. Resolves the base columns for the root table (all columns, or a subset via `columnsList`).
|
|
457
|
+
* 2. Iterates over each key in the `with` clause, looking up the relation definition.
|
|
458
|
+
* 3. For each relation, delegates to {@link buildRelationSubquery} to generate a
|
|
459
|
+
* correlated subquery that returns JSON (array for hasMany, object for belongsTo/hasOne).
|
|
460
|
+
* 4. Each subquery is aliased as the relation name in the final SELECT.
|
|
461
|
+
*
|
|
462
|
+
* **aliasCounter:** A shared `{ n: number }` object is passed through all nesting levels.
|
|
463
|
+
* Each call to `buildRelationSubquery` increments it to produce unique table aliases
|
|
464
|
+
* (`t0`, `t1`, `t2`, ...) across arbitrarily deep relation trees, preventing alias
|
|
465
|
+
* collisions in the generated SQL.
|
|
466
|
+
*
|
|
467
|
+
* **Example output:**
|
|
468
|
+
* ```sql
|
|
469
|
+
* "users"."id", "users"."name", "users"."email",
|
|
470
|
+
* (SELECT COALESCE(json_agg(json_build_object('id', t0."id", 'title', t0."title")), '[]'::json)
|
|
471
|
+
* FROM "posts" t0 WHERE t0."user_id" = "users"."id") AS "posts"
|
|
472
|
+
* ```
|
|
473
|
+
*
|
|
474
|
+
* @param table - The root table name (e.g. `"users"`).
|
|
475
|
+
* @param withClause - An object mapping relation names to their include specs
|
|
476
|
+
* (`true` for default inclusion, or `WithOptions` for select/omit/where/orderBy/limit).
|
|
477
|
+
* @param params - Shared parameter array for parameterized values (`$1`, `$2`, ...).
|
|
478
|
+
* Nested where/limit values are pushed here to prevent SQL injection.
|
|
479
|
+
* @param columnsList - Optional subset of columns to include in the SELECT. When `null`
|
|
480
|
+
* or omitted, all columns from the table's schema metadata are used.
|
|
481
|
+
* @param depth - Current nesting depth, passed through to {@link buildRelationSubquery}
|
|
482
|
+
* for circular-relation detection. Defaults to `0` at the top level.
|
|
483
|
+
* @param path - Breadcrumb trail of relation names traversed so far, used in error
|
|
484
|
+
* messages when circular or too-deep nesting is detected.
|
|
485
|
+
* @returns A complete SELECT clause string (without the `SELECT` keyword) containing
|
|
486
|
+
* base columns and relation subqueries.
|
|
487
|
+
*/
|
|
488
|
+
export declare function buildSelectWithRelations(qi: BuilderCtx, table: string, withClause: WithClause, params: unknown[], columnsList?: string[] | null, depth?: number, path?: string[], includePii?: boolean, flatten?: {
|
|
489
|
+
plan: FlattenPlan;
|
|
490
|
+
joinSink: string[];
|
|
491
|
+
}): string;
|
|
492
|
+
/**
|
|
493
|
+
* Generate a correlated subquery that returns JSON for a single relation.
|
|
494
|
+
*
|
|
495
|
+
* This is the core of Turbine's single-query nested relation strategy. For a given
|
|
496
|
+
* relation (e.g. `posts` on a `users` query), it produces a self-contained SQL subquery
|
|
497
|
+
* that PostgreSQL evaluates per parent row, returning either a JSON array (hasMany) or
|
|
498
|
+
* a single JSON object (belongsTo / hasOne).
|
|
499
|
+
*
|
|
500
|
+
* ### Algorithm overview
|
|
501
|
+
*
|
|
502
|
+
* 1. **Alias generation:** Allocates a unique alias (`t0`, `t1`, ...) from the shared
|
|
503
|
+
* `aliasCounter` so that deeply nested subqueries never collide.
|
|
504
|
+
*
|
|
505
|
+
* 2. **Column resolution:** Honors `select` / `omit` options to control which columns
|
|
506
|
+
* appear in the output JSON.
|
|
507
|
+
*
|
|
508
|
+
* 3. **`json_build_object`:** Builds a JSON object for each row by mapping camelCase
|
|
509
|
+
* field names to their column values:
|
|
510
|
+
* ```sql
|
|
511
|
+
* json_build_object('id', t0."id", 'title', t0."title", 'createdAt', t0."created_at")
|
|
512
|
+
* ```
|
|
513
|
+
*
|
|
514
|
+
* 4. **`json_agg` wrapping (hasMany):** For one-to-many relations, wraps the
|
|
515
|
+
* `json_build_object` call in `json_agg(...)` to aggregate all matching child rows
|
|
516
|
+
* into a JSON array. Uses `COALESCE(..., '[]'::json)` so the result is never NULL.
|
|
517
|
+
* For belongsTo / hasOne, no aggregation is used -- just the single JSON object
|
|
518
|
+
* with `LIMIT 1`.
|
|
519
|
+
*
|
|
520
|
+
* 5. **Correlation (WHERE clause):** Links the subquery to the parent row:
|
|
521
|
+
* - **hasMany:** `alias.foreignKey = parentRef.referenceKey`
|
|
522
|
+
* (e.g. `t0."user_id" = "users"."id"` -- child FK points to parent PK)
|
|
523
|
+
* - **belongsTo / hasOne:** `alias.referenceKey = parentRef.foreignKey`
|
|
524
|
+
* (e.g. `t0."id" = "posts"."author_id"` -- parent FK points to child PK)
|
|
525
|
+
*
|
|
526
|
+
* 6. **Recursion:** If the spec includes a nested `with` clause, this method calls
|
|
527
|
+
* itself recursively for each nested relation, passing the current alias as
|
|
528
|
+
* `parentRef`. The nested subquery appears as an additional key in the
|
|
529
|
+
* `json_build_object` call, wrapped in `COALESCE(..., '[]'::json)`.
|
|
530
|
+
* Depth is incremented and capped at 10 to guard against circular relations.
|
|
531
|
+
*
|
|
532
|
+
* 7. **LIMIT / ORDER BY wrapping:** For hasMany relations with `limit` or `orderBy`,
|
|
533
|
+
* the query is restructured into a two-level form:
|
|
534
|
+
* ```sql
|
|
535
|
+
* SELECT COALESCE(json_agg(json_build_object(...)), '[]'::json)
|
|
536
|
+
* FROM (
|
|
537
|
+
* SELECT t0.* FROM "posts" t0
|
|
538
|
+
* WHERE t0."user_id" = "users"."id"
|
|
539
|
+
* ORDER BY t0."created_at" DESC
|
|
540
|
+
* LIMIT $1
|
|
541
|
+
* ) t0i
|
|
542
|
+
* ```
|
|
543
|
+
* This ensures LIMIT and ORDER BY apply to the raw rows *before* `json_agg`
|
|
544
|
+
* aggregation. Without the inner subquery, LIMIT would be meaningless because
|
|
545
|
+
* `json_agg` produces a single aggregated row.
|
|
546
|
+
*
|
|
547
|
+
* 8. **Parameter threading:** All user-supplied values (where filters, limit) are
|
|
548
|
+
* pushed to the shared `params` array with `$N` placeholders. No string
|
|
549
|
+
* interpolation of user data ever occurs -- all identifiers go through
|
|
550
|
+
* `qi.q()` and all values are parameterized.
|
|
551
|
+
*
|
|
552
|
+
* ### Example output (hasMany with nested relation)
|
|
553
|
+
* ```sql
|
|
554
|
+
* SELECT COALESCE(json_agg(json_build_object(
|
|
555
|
+
* 'id', t0."id",
|
|
556
|
+
* 'title', t0."title",
|
|
557
|
+
* 'comments', COALESCE((
|
|
558
|
+
* SELECT COALESCE(json_agg(json_build_object('id', t1."id", 'body', t1."body")), '[]'::json)
|
|
559
|
+
* FROM "comments" t1 WHERE t1."post_id" = t0."id"
|
|
560
|
+
* ), '[]'::json)
|
|
561
|
+
* )), '[]'::json) FROM "posts" t0 WHERE t0."user_id" = "users"."id"
|
|
562
|
+
* ```
|
|
563
|
+
*
|
|
564
|
+
* @param relDef - The relation definition from schema metadata (contains `to`, `type`,
|
|
565
|
+
* `foreignKey`, `referenceKey`).
|
|
566
|
+
* @param spec - Either `true` (include with defaults) or a `WithOptions` object that
|
|
567
|
+
* can specify `select`, `omit`, `where`, `orderBy`, `limit`, and nested `with`.
|
|
568
|
+
* @param params - Shared parameter array. User-supplied values are pushed here and
|
|
569
|
+
* referenced as `$1`, `$2`, etc. in the generated SQL.
|
|
570
|
+
* @param parentRef - The alias (e.g. `"t0"`) or table name (e.g. `"users"`) of the
|
|
571
|
+
* parent query. Used to build the correlated WHERE clause that ties
|
|
572
|
+
* child rows to their parent row.
|
|
573
|
+
* @param aliasCounter - Shared mutable counter (`{ n: number }`) for generating unique
|
|
574
|
+
* table aliases (`t0`, `t1`, `t2`, ...) across all nesting levels.
|
|
575
|
+
* Each call increments `n` by 1.
|
|
576
|
+
* @param depth - Current nesting depth (starts at `0`). Incremented on each recursive
|
|
577
|
+
* call. If it reaches 10, a {@link CircularRelationError} is thrown.
|
|
578
|
+
* @param path - Breadcrumb trail of relation/table names traversed so far
|
|
579
|
+
* (e.g. `["users", "posts", "comments"]`). Used in the error message
|
|
580
|
+
* when circular or too-deep nesting is detected.
|
|
581
|
+
* @returns A complete SQL subquery string (without surrounding parentheses) that
|
|
582
|
+
* evaluates to a JSON array (hasMany) or a JSON object (belongsTo/hasOne).
|
|
583
|
+
*/
|
|
584
|
+
export declare function buildRelationSubquery(qi: BuilderCtx, relDef: RelationDef, spec: true | WithOptions, params: unknown[], parentRef: string, aliasCounter: {
|
|
585
|
+
n: number;
|
|
586
|
+
}, depth?: number, path?: string[], includePii?: boolean): string;
|
|
587
|
+
/**
|
|
588
|
+
* Build the json_agg subquery for a `manyToMany` relation, JOINing the target
|
|
589
|
+
* table through a junction (join) table.
|
|
590
|
+
*
|
|
591
|
+
* Shape (no LIMIT/ORDER):
|
|
592
|
+
* ```sql
|
|
593
|
+
* SELECT COALESCE(json_agg(json_build_object(...)), '[]'::json)
|
|
594
|
+
* FROM <target> <talias>
|
|
595
|
+
* JOIN <junction> <jalias> ON <jalias>.<targetKey> = <talias>.<targetPK>
|
|
596
|
+
* WHERE <jalias>.<sourceKey> = <parentRef>.<referenceKey>
|
|
597
|
+
* ```
|
|
598
|
+
*
|
|
599
|
+
* With LIMIT/ORDER, the rows are wrapped in an inner subquery so the LIMIT
|
|
600
|
+
* applies BEFORE aggregation (identical strategy to hasMany).
|
|
601
|
+
*
|
|
602
|
+
* Cardinality is always 'many' → empty-array fallback, never NULL.
|
|
603
|
+
*
|
|
604
|
+
* IMPORTANT: every `params.push` here MUST be mirrored, in the same order, in
|
|
605
|
+
* {@link collectRelationSubqueryParams} or pipeline batching will desync.
|
|
606
|
+
*/
|
|
607
|
+
export declare function buildManyToManySubquery(qi: BuilderCtx, relDef: RelationDef, spec: true | WithOptions, params: unknown[], parentRef: string, aliasCounter: {
|
|
608
|
+
n: number;
|
|
609
|
+
}, currentDepth: number, currentPath: string[], talias: string, targetMeta: TableMetadata, targetColumns: string[], includePii?: boolean): string;
|