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
|
@@ -45,7 +45,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
45
45
|
};
|
|
46
46
|
})();
|
|
47
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
-
exports.QueryInterface = void 0;
|
|
48
|
+
exports.QueryInterface = exports.AUTO_TO_ONE_JOIN_ROWS_MAX = exports.AUTO_TO_ONE_JOIN_ROWS_MIN = exports.AUTO_TO_ONE_JOIN_MAX_ROWS = exports.AUTO_ASSUMED_ROUND_TRIP_MS = exports.AUTO_JOIN_PENALTY_MS_PER_ROW = void 0;
|
|
49
49
|
const dialect_js_1 = require("../dialect.js");
|
|
50
50
|
const errors_js_1 = require("../errors.js");
|
|
51
51
|
const index_advisor_js_1 = require("../index-advisor.js");
|
|
@@ -105,6 +105,71 @@ function cacheCrossCheckMode() {
|
|
|
105
105
|
* so this set is only ever touched on the `'sampled'` path.
|
|
106
106
|
*/
|
|
107
107
|
const loggedCacheMismatchFingerprints = new Set();
|
|
108
|
+
/**
|
|
109
|
+
* Marginal cost of keeping a to-one relation on the JOIN plan, per parent row.
|
|
110
|
+
*
|
|
111
|
+
* A to-one relation compiled into the join plan is a CORRELATED subquery: the
|
|
112
|
+
* engine re-evaluates it once per parent row, so the join plan costs roughly
|
|
113
|
+
* `AUTO_JOIN_PENALTY_MS_PER_ROW * parentRows` more CPU than one flat follow-up
|
|
114
|
+
* query, no matter how well indexed the correlation column is. The batched plan
|
|
115
|
+
* pays that back as a second statement, i.e. one extra round trip.
|
|
116
|
+
*
|
|
117
|
+
* Break-even is therefore, to a first approximation:
|
|
118
|
+
*
|
|
119
|
+
* parentRows = roundTripMs / AUTO_JOIN_PENALTY_MS_PER_ROW
|
|
120
|
+
*
|
|
121
|
+
* Measured (PostgreSQL 17, hasOne over a UNIQUE FK, 10K-row parent table,
|
|
122
|
+
* median of 15 reps per point) at two very different link speeds:
|
|
123
|
+
*
|
|
124
|
+
* link RTT penalty/row break-even observed crossover
|
|
125
|
+
* ─────────────────── ──────── ───────────── ──────────── ──────────────────
|
|
126
|
+
* loopback TCP 0.118ms 0.000711ms ~166-236 between 200 and 400
|
|
127
|
+
* +1ms/direction 2.683ms 0.000717ms ~3744-3993 between 3000 and 5000
|
|
128
|
+
*
|
|
129
|
+
* The two things that matters most in that table: the per-row penalty is
|
|
130
|
+
* essentially IDENTICAL across the two links (it is a property of the plan, not
|
|
131
|
+
* the wire), while the break-even moved by 17x. So the break-even is a function
|
|
132
|
+
* of the deployment's round-trip time and NOTHING ELSE that is knowable at plan
|
|
133
|
+
* time. That is why this is expressed as a per-row cost and a round-trip time
|
|
134
|
+
* rather than as a hard-coded row count: a row count tuned on a Unix socket is
|
|
135
|
+
* off by ~20x for a cross-region deployment, and vice versa. Concretely, the
|
|
136
|
+
* previously shipped flat `1000` was simultaneously too HIGH on loopback
|
|
137
|
+
* (up to 1.44x slower than the better plan just under the cliff) and too LOW
|
|
138
|
+
* over a 2.7ms link (1.26x slower just above it).
|
|
139
|
+
*/
|
|
140
|
+
exports.AUTO_JOIN_PENALTY_MS_PER_ROW = 0.0007;
|
|
141
|
+
/**
|
|
142
|
+
* Round-trip time assumed before this process has observed a real one, chosen
|
|
143
|
+
* as a typical same-region managed-Postgres latency (app and database in one
|
|
144
|
+
* region over TCP). It is stated as a LATENCY rather than a row count so the
|
|
145
|
+
* assumption is visible and re-derivable: at
|
|
146
|
+
* {@link AUTO_JOIN_PENALTY_MS_PER_ROW} it yields exactly the 1000-row default
|
|
147
|
+
* this heuristic has always shipped, so an unmeasured process behaves exactly
|
|
148
|
+
* as before.
|
|
149
|
+
*/
|
|
150
|
+
exports.AUTO_ASSUMED_ROUND_TRIP_MS = 0.7;
|
|
151
|
+
/**
|
|
152
|
+
* Default parent-row ceiling under which `'auto'` keeps a to-one relation on
|
|
153
|
+
* the single-statement join plan: {@link AUTO_ASSUMED_ROUND_TRIP_MS} divided by
|
|
154
|
+
* {@link AUTO_JOIN_PENALTY_MS_PER_ROW}. Used until the process has measured its
|
|
155
|
+
* own round-trip time, and whenever measurement is unavailable.
|
|
156
|
+
*/
|
|
157
|
+
exports.AUTO_TO_ONE_JOIN_MAX_ROWS = Math.round(exports.AUTO_ASSUMED_ROUND_TRIP_MS / exports.AUTO_JOIN_PENALTY_MS_PER_ROW);
|
|
158
|
+
/**
|
|
159
|
+
* Clamps on the MEASURED threshold (an explicit `autoToOneJoinMaxRows` is an
|
|
160
|
+
* instruction, not an estimate, and bypasses both).
|
|
161
|
+
*
|
|
162
|
+
* The lower clamp matters: on a very fast link the formula can drop the
|
|
163
|
+
* threshold to a few dozen rows, and the sweep shows the join plan winning by
|
|
164
|
+
* up to 1.83x on a handful of parent rows, where the second statement's fixed
|
|
165
|
+
* cost dwarfs everything. Holding the floor at 100 rows keeps those small
|
|
166
|
+
* queries on the join plan; the cost of doing so, in the band where batched has
|
|
167
|
+
* just started to win, is under 1.2x. The upper clamp is a sanity bound for a
|
|
168
|
+
* pathological latency reading (a 70ms measurement would otherwise ask for
|
|
169
|
+
* 100K rows).
|
|
170
|
+
*/
|
|
171
|
+
exports.AUTO_TO_ONE_JOIN_ROWS_MIN = 100;
|
|
172
|
+
exports.AUTO_TO_ONE_JOIN_ROWS_MAX = 100_000;
|
|
108
173
|
/**
|
|
109
174
|
* Strict structural equality for a single SQL parameter value. Handles the
|
|
110
175
|
* value shapes Turbine binds: primitives (incl. `NaN` and `bigint`), `null`/
|
|
@@ -253,6 +318,23 @@ class QueryInterface {
|
|
|
253
318
|
relationLoadStrategy;
|
|
254
319
|
/** Client-level default for {@link applyStableRelationOrder} (off unless configured). */
|
|
255
320
|
stableRelationOrder;
|
|
321
|
+
/**
|
|
322
|
+
* Client-level opt-in: apply an implicit primary-key ascending `ORDER BY` to a
|
|
323
|
+
* `findMany` that paginates (`limit`/`take`/`offset`) but declares no
|
|
324
|
+
* `orderBy`. OFF by default in core, see {@link applyImplicitPkOrdering}.
|
|
325
|
+
*/
|
|
326
|
+
implicitPkOrdering;
|
|
327
|
+
/**
|
|
328
|
+
* Explicitly configured parent-row ceiling for the `'auto'` to-one rule, or
|
|
329
|
+
* `undefined` to derive it from the observed round-trip time. See
|
|
330
|
+
* {@link autoToOneThreshold}.
|
|
331
|
+
*/
|
|
332
|
+
autoToOneJoinMaxRowsOption;
|
|
333
|
+
/**
|
|
334
|
+
* Deployment round-trip time in milliseconds, from which the to-one threshold
|
|
335
|
+
* is derived. See {@link autoToOneThreshold}.
|
|
336
|
+
*/
|
|
337
|
+
autoRoundTripMs;
|
|
256
338
|
/** Nested-relation JSON encoding: 'object' (default) or 'positional'. */
|
|
257
339
|
jsonEncoding;
|
|
258
340
|
/**
|
|
@@ -374,6 +456,12 @@ class QueryInterface {
|
|
|
374
456
|
this.dialect = options?.dialect ?? dialect_js_1.postgresDialect;
|
|
375
457
|
this.relationLoadStrategy = options?.relationLoadStrategy ?? 'auto';
|
|
376
458
|
this.stableRelationOrder = options?.stableRelationOrder === true;
|
|
459
|
+
this.implicitPkOrdering = options?.implicitPkOrdering === true;
|
|
460
|
+
const autoToOne = options?.autoToOneJoinMaxRows;
|
|
461
|
+
this.autoToOneJoinMaxRowsOption =
|
|
462
|
+
autoToOne !== undefined && Number.isFinite(autoToOne) && autoToOne >= 0 ? Math.floor(autoToOne) : undefined;
|
|
463
|
+
const rtt = options?.autoRoundTripMs;
|
|
464
|
+
this.autoRoundTripMs = rtt !== undefined && Number.isFinite(rtt) && rtt > 0 ? rtt : undefined;
|
|
377
465
|
this.jsonEncoding = options?.jsonEncoding ?? 'object';
|
|
378
466
|
// Only retain the map when it has at least one entry, so `globalFilters`
|
|
379
467
|
// stays `undefined` (and every merge path a no-op) for the common case.
|
|
@@ -440,7 +528,8 @@ class QueryInterface {
|
|
|
440
528
|
camelDateFieldCache: this.camelDateFieldCache,
|
|
441
529
|
limitOneClause: () => this.limitOneClause(),
|
|
442
530
|
buildPagination: (limitPh, offsetPh, hasOrderBy) => this.buildPagination(limitPh, offsetPh, hasOrderBy),
|
|
443
|
-
paginationRef: (value, params) => this.paginationRef(value, params),
|
|
531
|
+
paginationRef: (value, params, arg) => this.paginationRef(value, params, arg),
|
|
532
|
+
paginationValue: (value, arg) => this.paginationValue(value, arg),
|
|
444
533
|
};
|
|
445
534
|
}
|
|
446
535
|
/** Quote an identifier through the active SQL dialect. */
|
|
@@ -491,19 +580,33 @@ class QueryInterface {
|
|
|
491
580
|
}
|
|
492
581
|
return ' LIMIT 1';
|
|
493
582
|
}
|
|
583
|
+
/**
|
|
584
|
+
* Coerce a LIMIT/OFFSET argument and validate it as a non-negative safe
|
|
585
|
+
* integer. Numeric strings (`'5'`) coerce; everything else (`NaN`, a
|
|
586
|
+
* non-numeric string, a negative, a fractional or out-of-safe-range number)
|
|
587
|
+
* throws {@link ValidationError} (E003) naming the argument and the table.
|
|
588
|
+
*
|
|
589
|
+
* This runs on EVERY pagination path, parameterized as well as inlined: a
|
|
590
|
+
* bound `NaN` serializes as SQL NULL, and Postgres reads `LIMIT NULL` as
|
|
591
|
+
* "no limit", so an unvalidated value silently turns a paginated query into
|
|
592
|
+
* a full-table read (and a bad OFFSET silently disappears).
|
|
593
|
+
*/
|
|
594
|
+
paginationValue(value, arg) {
|
|
595
|
+
const n = Number(value);
|
|
596
|
+
if (!Number.isSafeInteger(n) || n < 0) {
|
|
597
|
+
throw new errors_js_1.ValidationError(`[turbine] ${arg} on "${this.table}" must be a non-negative integer, received: ${String(value)}`);
|
|
598
|
+
}
|
|
599
|
+
return n;
|
|
600
|
+
}
|
|
494
601
|
/**
|
|
495
602
|
* Validate a LIMIT/OFFSET value as a non-negative integer and return it as an
|
|
496
603
|
* inline SQL literal. Used only on `dialect.inlineLimitOffset` engines (MySQL).
|
|
497
604
|
* The input is always a Turbine-controlled pagination value, never a raw user
|
|
498
|
-
* string
|
|
605
|
+
* string, and this guard guarantees the output is `String` of a validated
|
|
499
606
|
* integer, so inlining cannot inject SQL.
|
|
500
607
|
*/
|
|
501
|
-
limitOffsetLiteral(value) {
|
|
502
|
-
|
|
503
|
-
if (!Number.isInteger(n) || n < 0) {
|
|
504
|
-
throw new errors_js_1.ValidationError(`LIMIT/OFFSET must be a non-negative integer, received: ${String(value)}`);
|
|
505
|
-
}
|
|
506
|
-
return String(n);
|
|
608
|
+
limitOffsetLiteral(value, arg) {
|
|
609
|
+
return String(this.paginationValue(value, arg));
|
|
507
610
|
}
|
|
508
611
|
/**
|
|
509
612
|
* Resolve a LIMIT/OFFSET value to either an inline literal (no param pushed, on
|
|
@@ -512,11 +615,11 @@ class QueryInterface {
|
|
|
512
615
|
* the param order stays mirrored; PG/SQLite/SQL Server keep parameterizing and
|
|
513
616
|
* stay byte-identical.
|
|
514
617
|
*/
|
|
515
|
-
paginationRef(value, params) {
|
|
618
|
+
paginationRef(value, params, arg) {
|
|
516
619
|
if (this.dialect.inlineLimitOffset) {
|
|
517
|
-
return this.limitOffsetLiteral(value);
|
|
620
|
+
return this.limitOffsetLiteral(value, arg);
|
|
518
621
|
}
|
|
519
|
-
params.push(
|
|
622
|
+
params.push(this.paginationValue(value, arg));
|
|
520
623
|
return this.p(params.length);
|
|
521
624
|
}
|
|
522
625
|
/**
|
|
@@ -605,6 +708,180 @@ class QueryInterface {
|
|
|
605
708
|
return out ?? withClause;
|
|
606
709
|
}
|
|
607
710
|
// -------------------------------------------------------------------------
|
|
711
|
+
// Deterministic pagination (unordered LIMIT/OFFSET)
|
|
712
|
+
// -------------------------------------------------------------------------
|
|
713
|
+
/**
|
|
714
|
+
* The primary key of this table as an ascending `orderBy`, in DECLARATION
|
|
715
|
+
* order (a composite PK orders on every column), or `undefined` for a PK-less
|
|
716
|
+
* table. Field names are the camelCase accessor names, so the emitted SQL goes
|
|
717
|
+
* through the normal column mapping.
|
|
718
|
+
*/
|
|
719
|
+
pkOrderBy() {
|
|
720
|
+
const pk = this.tableMeta.primaryKey ?? [];
|
|
721
|
+
if (pk.length === 0)
|
|
722
|
+
return undefined;
|
|
723
|
+
const fields = pk.map((c) => this.tableMeta.reverseColumnMap[c] ?? c);
|
|
724
|
+
return fields.length === 1 ? { [fields[0]]: 'asc' } : fields.map((f) => ({ [f]: 'asc' }));
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* The field names a `cursor` actually seeks on (its own keys with a defined
|
|
728
|
+
* value), in the canonical sorted order the cursor conditions are built in.
|
|
729
|
+
* Empty for a missing cursor or one whose every value is `undefined` (which
|
|
730
|
+
* emits no seek condition at all, so it does not paginate).
|
|
731
|
+
*/
|
|
732
|
+
cursorFields(cursor) {
|
|
733
|
+
if (!cursor || typeof cursor !== 'object')
|
|
734
|
+
return [];
|
|
735
|
+
return Object.entries(cursor)
|
|
736
|
+
.filter(([, v]) => v !== undefined)
|
|
737
|
+
.map(([k]) => k)
|
|
738
|
+
.sort();
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* The ascending ordering implied by a `cursor`, or `undefined` when the shape
|
|
742
|
+
* is too ambiguous to order safely.
|
|
743
|
+
*
|
|
744
|
+
* A cursor seek emits `col > $n` per field (`<` when the orderBy says desc),
|
|
745
|
+
* so the ONLY ordering coherent with it is on the cursor's own field: ordering
|
|
746
|
+
* a seek on column X by column Y walks the table in an order the seek does not
|
|
747
|
+
* follow, which skips and repeats rows just as badly as no order at all. That
|
|
748
|
+
* is why this orders on the cursor field rather than blindly on the primary
|
|
749
|
+
* key when the two differ.
|
|
750
|
+
*
|
|
751
|
+
* Returns `undefined` (warn, inject nothing) for two shapes:
|
|
752
|
+
* - a MULTI-field cursor. `a > $1 AND b > $2` is a conjunction, not a proper
|
|
753
|
+
* composite keyset seek (`(a, b) > ($1, $2)`), so no single ORDER BY makes
|
|
754
|
+
* it correct. Injecting `(a asc, b asc)` would dress a broken seek up as a
|
|
755
|
+
* sound one.
|
|
756
|
+
* - a field that does not resolve to a real column. Column validation belongs
|
|
757
|
+
* to the normal build path, which raises a precise error; synthesizing an
|
|
758
|
+
* ORDER BY on it here would only change which error the caller sees.
|
|
759
|
+
*/
|
|
760
|
+
cursorOrderBy(cursor) {
|
|
761
|
+
const fields = this.cursorFields(cursor);
|
|
762
|
+
if (fields.length !== 1)
|
|
763
|
+
return undefined;
|
|
764
|
+
const field = fields[0];
|
|
765
|
+
try {
|
|
766
|
+
this.toColumn(field);
|
|
767
|
+
}
|
|
768
|
+
catch {
|
|
769
|
+
return undefined;
|
|
770
|
+
}
|
|
771
|
+
return { [field]: 'asc' };
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Whether a findMany paginates (`limit` / `take` / `offset` / `cursor`) but
|
|
775
|
+
* declares no ordering, which makes the returned page NON-DETERMINISTIC:
|
|
776
|
+
* Postgres is free to return different rows for the same unordered `LIMIT`
|
|
777
|
+
* once the heap changes underneath it, so a row can appear on two pages or on
|
|
778
|
+
* none.
|
|
779
|
+
*
|
|
780
|
+
* `cursor` counts, and is the worst case rather than an exception: a keyset
|
|
781
|
+
* seek with no ORDER BY is exactly this bug (`WHERE id > $1 LIMIT $2` walks
|
|
782
|
+
* the heap in whatever order the plan happens to produce). An empty orderBy
|
|
783
|
+
* (`[]`, or an object whose every value is `undefined`) counts as absent,
|
|
784
|
+
* because it emits no ordering.
|
|
785
|
+
*
|
|
786
|
+
* `distinct` is still excluded: that path re-orders in an outer wrapper around
|
|
787
|
+
* a `DISTINCT ON` whose ordering picks the representative row, so an implicit
|
|
788
|
+
* key would change which rows come back, not just their order.
|
|
789
|
+
*/
|
|
790
|
+
isUnorderedPage(args) {
|
|
791
|
+
if (!args)
|
|
792
|
+
return false;
|
|
793
|
+
if (args.distinct !== undefined)
|
|
794
|
+
return false;
|
|
795
|
+
if (!isEmptyOrderBy(args.orderBy))
|
|
796
|
+
return false;
|
|
797
|
+
if (args.limit !== undefined || args.take !== undefined || args.offset !== undefined)
|
|
798
|
+
return true;
|
|
799
|
+
return this.cursorFields(args.cursor).length > 0;
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Opt-in (`implicitPkOrdering`) primary-key ascending ordering for a paginating
|
|
803
|
+
* findMany that declares no `orderBy`, making its pages deterministic.
|
|
804
|
+
*
|
|
805
|
+
* OFF by default in CORE, deliberately: turning it on would add an `ORDER BY`
|
|
806
|
+
* to SQL that existing applications already emit, changing both the rows a
|
|
807
|
+
* given page returns and the plan the engine picks. That is a breaking change
|
|
808
|
+
* in everything but the type signature, so it waits for a major. The
|
|
809
|
+
* `turbine-orm/prisma-compat` layer defaults it ON instead, because reproducing
|
|
810
|
+
* Prisma's semantics is that layer's whole contract.
|
|
811
|
+
*
|
|
812
|
+
* An explicit `orderBy` always wins, a PK-less table is left alone (nothing
|
|
813
|
+
* stable to order by), and a composite PK orders on every column in
|
|
814
|
+
* declaration order. A `cursor` query orders on the CURSOR's field instead
|
|
815
|
+
* (see {@link cursorOrderBy}), and is left alone when that shape is ambiguous.
|
|
816
|
+
* `distinct` shapes are skipped (see {@link isUnorderedPage}). With the flag
|
|
817
|
+
* off this returns `undefined` before touching anything, so the emitted SQL is
|
|
818
|
+
* byte-identical to before.
|
|
819
|
+
*/
|
|
820
|
+
implicitPkOrderBy(args) {
|
|
821
|
+
if (!this.implicitPkOrdering)
|
|
822
|
+
return undefined;
|
|
823
|
+
if (!this.isUnorderedPage(args))
|
|
824
|
+
return undefined;
|
|
825
|
+
if (this.cursorFields(args?.cursor).length > 0)
|
|
826
|
+
return this.cursorOrderBy(args?.cursor);
|
|
827
|
+
return this.pkOrderBy();
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Dev-only, once per query shape: an unordered paginating findMany returns a
|
|
831
|
+
* non-deterministic page (see {@link isUnorderedPage}), and on real data it is
|
|
832
|
+
* also usually the slower plan (an unordered `LIMIT` can discard tens of
|
|
833
|
+
* thousands of heap rows that an index scan on the key would have skipped).
|
|
834
|
+
*
|
|
835
|
+
* Gated exactly like the other dev diagnostics (silent under
|
|
836
|
+
* `NODE_ENV=production`) and consistent with `warnOnUnlimited`: a per-call
|
|
837
|
+
* `warnOnUnlimited: false` silences it, `true` forces it past a config-level
|
|
838
|
+
* opt-out, and a config/per-table `warnOnUnlimited: false` silences it. Deduped
|
|
839
|
+
* process-wide through the shared warn registry, so it can never spam.
|
|
840
|
+
*
|
|
841
|
+
* Suppressed only when `implicitPkOrdering` will ACTUALLY order this query.
|
|
842
|
+
* The flag being on is not enough: a PK-less table and an ambiguous
|
|
843
|
+
* multi-field cursor both get no injected ordering, and those are precisely
|
|
844
|
+
* the shapes that still need saying out loud.
|
|
845
|
+
*/
|
|
846
|
+
maybeWarnUnorderedPage(args) {
|
|
847
|
+
if (process.env.NODE_ENV === 'production')
|
|
848
|
+
return;
|
|
849
|
+
if (this.implicitPkOrdering && this.implicitPkOrderBy(args) !== undefined)
|
|
850
|
+
return;
|
|
851
|
+
const perCall = args?.warnOnUnlimited;
|
|
852
|
+
if (perCall === false)
|
|
853
|
+
return;
|
|
854
|
+
if (perCall === undefined && !this.warnOnUnlimited)
|
|
855
|
+
return;
|
|
856
|
+
if (!this.isUnorderedPage(args))
|
|
857
|
+
return;
|
|
858
|
+
const cursorFields = this.cursorFields(args?.cursor);
|
|
859
|
+
const shape = [
|
|
860
|
+
cursorFields.length > 0 ? 'cursor' : '',
|
|
861
|
+
args?.limit !== undefined ? 'limit' : '',
|
|
862
|
+
args?.take !== undefined ? 'take' : '',
|
|
863
|
+
args?.offset !== undefined ? 'offset' : '',
|
|
864
|
+
]
|
|
865
|
+
.filter(Boolean)
|
|
866
|
+
.join('+');
|
|
867
|
+
if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.unorderedPage, `${this.table}|${shape}`))
|
|
868
|
+
return;
|
|
869
|
+
const asOrderBy = (fields) => fields.length === 1 ? `{ ${fields[0]}: 'asc' }` : `[${fields.map((f) => `{ ${f}: 'asc' }`).join(', ')}]`;
|
|
870
|
+
const pk = (this.tableMeta.primaryKey ?? []).map((c) => this.tableMeta.reverseColumnMap[c] ?? c);
|
|
871
|
+
// A cursor seeks on its own field, so that is the ordering to recommend:
|
|
872
|
+
// the primary key would be the wrong advice whenever the two differ.
|
|
873
|
+
const orderFields = cursorFields.length > 0 ? cursorFields : pk;
|
|
874
|
+
const suggestion = orderFields.length > 0 ? `Add \`orderBy: ${asOrderBy(orderFields)}\`` : 'Add an `orderBy` on a unique column';
|
|
875
|
+
const seek = cursorFields.length > 0
|
|
876
|
+
? 'a `cursor` seek with no orderBy compares against rows the engine is free to return in any order, so ' +
|
|
877
|
+
'paging with it skips and repeats rows: '
|
|
878
|
+
: '';
|
|
879
|
+
console.warn(`[turbine] findMany on "${this.table}" paginates (${shape}) with no orderBy: ${seek}the page is NOT ` +
|
|
880
|
+
'deterministic (the same query can return different rows as the table changes, so a row may appear ' +
|
|
881
|
+
`on two pages or on none). ${suggestion}, or set \`implicitPkOrdering: true\` in the client config ` +
|
|
882
|
+
'to order paginated queries automatically (by the cursor field, or by the primary key).');
|
|
883
|
+
}
|
|
884
|
+
// -------------------------------------------------------------------------
|
|
608
885
|
// relationLoadStrategy: 'auto', per-relation batched fallback when the
|
|
609
886
|
// introspected metadata proves a probe is unindexed (finding 13).
|
|
610
887
|
// -------------------------------------------------------------------------
|
|
@@ -692,13 +969,69 @@ class QueryInterface {
|
|
|
692
969
|
return { unindexed, eligible, miss };
|
|
693
970
|
}
|
|
694
971
|
/**
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
*
|
|
972
|
+
* Whether a query's parent set is potentially large at plan time, which is the
|
|
973
|
+
* only cardinality signal available before the base query runs. A `findMany`
|
|
974
|
+
* with no `limit`/`take` (or one above {@link autoToOneJoinMaxRows}) can return
|
|
975
|
+
* an arbitrary number of parent rows; a small `limit` bounds it. `findUnique` /
|
|
976
|
+
* `findFirst` pass `false` explicitly (their parent set is one row).
|
|
977
|
+
*/
|
|
978
|
+
autoParentSetLarge(args) {
|
|
979
|
+
const limit = args?.take ?? args?.limit ?? this.defaultLimit;
|
|
980
|
+
return limit === undefined || limit > this.autoToOneThreshold();
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* The parent-row count at which `'auto'` stops preferring the single-statement
|
|
984
|
+
* join for a to-one relation.
|
|
985
|
+
*
|
|
986
|
+
* Resolution order:
|
|
987
|
+
* 1. an explicit `autoToOneJoinMaxRows` — an instruction, used verbatim
|
|
988
|
+
* (no clamping: the caller has measured their own workload);
|
|
989
|
+
* 2. the configured `autoRoundTripMs` divided by
|
|
990
|
+
* {@link AUTO_JOIN_PENALTY_MS_PER_ROW}, clamped to
|
|
991
|
+
* [{@link AUTO_TO_ONE_JOIN_ROWS_MIN}, {@link AUTO_TO_ONE_JOIN_ROWS_MAX}];
|
|
992
|
+
* 3. {@link AUTO_TO_ONE_JOIN_MAX_ROWS}, which is that same division applied
|
|
993
|
+
* to {@link AUTO_ASSUMED_ROUND_TRIP_MS}.
|
|
994
|
+
*
|
|
995
|
+
* Deriving it rather than hard-coding a row count is the whole point: the
|
|
996
|
+
* sweep in {@link AUTO_JOIN_PENALTY_MS_PER_ROW} shows the break-even moving
|
|
997
|
+
* 17x between a loopback link and a 2.7ms one while the per-row penalty stays
|
|
998
|
+
* put, so any single constant is wrong for someone by more than the margin it
|
|
999
|
+
* is trying to save. Placing the switch AT the break-even is also what removes
|
|
1000
|
+
* the old cliff: two plans that cost the same at the boundary make the regret
|
|
1001
|
+
* there ~1.0x, rising only as the true row count moves away from it — where
|
|
1002
|
+
* the previous fixed 1000 put its WORST case (1.44x measured) immediately
|
|
1003
|
+
* below its own switch point.
|
|
1004
|
+
*
|
|
1005
|
+
* Cheap enough to recompute per call (a division and two comparisons over
|
|
1006
|
+
* readonly fields), so there is no cached copy to invalidate.
|
|
1007
|
+
*/
|
|
1008
|
+
autoToOneThreshold() {
|
|
1009
|
+
if (this.autoToOneJoinMaxRowsOption !== undefined)
|
|
1010
|
+
return this.autoToOneJoinMaxRowsOption;
|
|
1011
|
+
if (this.autoRoundTripMs === undefined)
|
|
1012
|
+
return exports.AUTO_TO_ONE_JOIN_MAX_ROWS;
|
|
1013
|
+
const rows = Math.round(this.autoRoundTripMs / exports.AUTO_JOIN_PENALTY_MS_PER_ROW);
|
|
1014
|
+
return Math.min(exports.AUTO_TO_ONE_JOIN_ROWS_MAX, Math.max(exports.AUTO_TO_ONE_JOIN_ROWS_MIN, rows));
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Partition a top-level `with` clause under `'auto'`. A relation routes to
|
|
1018
|
+
* `batchedWith` when it is fully batched-eligible AND either
|
|
1019
|
+
*
|
|
1020
|
+
* 1. its subtree has a PROVEN unindexed probe (index metadata only), or
|
|
1021
|
+
* 2. it is TO-ONE and the parent set is potentially large
|
|
1022
|
+
* ({@link AUTO_TO_ONE_JOIN_MAX_ROWS}), since a correlated to-one subquery
|
|
1023
|
+
* is re-evaluated per parent row no matter how well indexed it is.
|
|
1024
|
+
*
|
|
1025
|
+
* Everything else (indexed to-many, composite-key, unknown) stays in `joinWith`
|
|
1026
|
+
* (byte-identical join). The reserved `_count` key falls back on rule 1 only,
|
|
1027
|
+
* and only for a large parent set: an inline `_count` is one correlated
|
|
1028
|
+
* `COUNT(*)` per parent row, so the grouped follow-up wins exactly when there
|
|
1029
|
+
* are many parents, while for a handful of parents the extra round-trip costs
|
|
1030
|
+
* more than the repeated (small) scans. Also returns the engaged relations for
|
|
1031
|
+
* the dev note.
|
|
700
1032
|
*/
|
|
701
|
-
partitionWithForAuto(withClause) {
|
|
1033
|
+
partitionWithForAuto(withClause, parentSetLarge) {
|
|
1034
|
+
const hasIndexInfo = (0, index_advisor_js_1.schemaHasIndexInfo)(this.schema);
|
|
702
1035
|
const joinWith = {};
|
|
703
1036
|
const batchedWith = {};
|
|
704
1037
|
const engaged = [];
|
|
@@ -707,9 +1040,9 @@ class QueryInterface {
|
|
|
707
1040
|
continue;
|
|
708
1041
|
if (key === '_count') {
|
|
709
1042
|
const cv = this.autoCountVerdict(spec, this.tableMeta);
|
|
710
|
-
if (cv.unindexed && cv.eligible) {
|
|
1043
|
+
if (hasIndexInfo && parentSetLarge && cv.unindexed && cv.eligible) {
|
|
711
1044
|
batchedWith[key] = spec;
|
|
712
|
-
engaged.push({ relation: '_count', miss: cv.miss });
|
|
1045
|
+
engaged.push({ relation: '_count', reason: 'unindexed', miss: cv.miss });
|
|
713
1046
|
}
|
|
714
1047
|
else {
|
|
715
1048
|
joinWith[key] = spec;
|
|
@@ -722,9 +1055,16 @@ class QueryInterface {
|
|
|
722
1055
|
continue;
|
|
723
1056
|
}
|
|
724
1057
|
const v = this.autoSubtreeVerdict(rel, spec, 0);
|
|
725
|
-
|
|
1058
|
+
const unindexedFallback = hasIndexInfo && v.unindexed;
|
|
1059
|
+
const toOne = rel.type === 'belongsTo' || rel.type === 'hasOne';
|
|
1060
|
+
const cardinalityFallback = toOne && parentSetLarge;
|
|
1061
|
+
if (v.eligible && (unindexedFallback || cardinalityFallback)) {
|
|
726
1062
|
batchedWith[key] = spec;
|
|
727
|
-
engaged.push({
|
|
1063
|
+
engaged.push({
|
|
1064
|
+
relation: key,
|
|
1065
|
+
reason: unindexedFallback ? 'unindexed' : 'to-one-cardinality',
|
|
1066
|
+
miss: unindexedFallback ? v.miss : undefined,
|
|
1067
|
+
});
|
|
728
1068
|
}
|
|
729
1069
|
else {
|
|
730
1070
|
joinWith[key] = spec;
|
|
@@ -736,17 +1076,18 @@ class QueryInterface {
|
|
|
736
1076
|
* Plan the `'auto'` split for a query's `with` clause: normalize stable order,
|
|
737
1077
|
* partition, and return the split ONLY when at least one relation falls back
|
|
738
1078
|
* to batched. Returns `null` (→ run the plain join path, byte-identical, same
|
|
739
|
-
* cache keys) when
|
|
1079
|
+
* cache keys) when nothing qualifies.
|
|
740
1080
|
*/
|
|
741
|
-
planAuto(withArg, stableFlag) {
|
|
742
|
-
//
|
|
743
|
-
//
|
|
744
|
-
|
|
1081
|
+
planAuto(withArg, stableFlag, parentSetLarge) {
|
|
1082
|
+
// Without DB-backed index info (code-first / defineSchema-only) no probe can
|
|
1083
|
+
// be PROVEN unindexed; the to-one cardinality rule does not depend on index
|
|
1084
|
+
// metadata, so it still applies.
|
|
1085
|
+
if (!(0, index_advisor_js_1.schemaHasIndexInfo)(this.schema) && !parentSetLarge)
|
|
745
1086
|
return null;
|
|
746
1087
|
const withClause = this.resolveStableOrder(stableFlag)
|
|
747
1088
|
? this.applyStableRelationOrder(withArg, this.table)
|
|
748
1089
|
: withArg;
|
|
749
|
-
const split = this.partitionWithForAuto(withClause);
|
|
1090
|
+
const split = this.partitionWithForAuto(withClause, parentSetLarge);
|
|
750
1091
|
if (Object.keys(split.batchedWith).length === 0)
|
|
751
1092
|
return null;
|
|
752
1093
|
return split;
|
|
@@ -758,6 +1099,13 @@ class QueryInterface {
|
|
|
758
1099
|
for (const e of engaged) {
|
|
759
1100
|
if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.autoStrategy, `${this.table}.${e.relation}`))
|
|
760
1101
|
continue;
|
|
1102
|
+
if (e.reason === 'to-one-cardinality') {
|
|
1103
|
+
console.warn(`[turbine] auto strategy: to-one relation "${e.relation}" on "${this.table}" loads batched ` +
|
|
1104
|
+
`(the query is unbounded or its limit exceeds ${this.autoToOneThreshold()} rows, and a correlated ` +
|
|
1105
|
+
'to-one subquery is re-evaluated per parent row). Bound the query with a smaller `limit`, tune ' +
|
|
1106
|
+
"`autoToOneJoinMaxRows`, or set `relationLoadStrategy: 'join'` to force the single-statement plan.");
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
761
1109
|
const probe = e.miss
|
|
762
1110
|
? `probe "${e.miss.table}"(${e.miss.columns.join(', ')}) has no covering index`
|
|
763
1111
|
: 'a probe in its subtree has no covering index';
|
|
@@ -1176,7 +1524,9 @@ class QueryInterface {
|
|
|
1176
1524
|
if (strategy === 'batched')
|
|
1177
1525
|
return this.runFindUniqueBatched(args);
|
|
1178
1526
|
if (strategy === 'auto') {
|
|
1179
|
-
|
|
1527
|
+
// findUnique's parent set is a single row: the join plan's correlated
|
|
1528
|
+
// subqueries run once, so the cardinality rule never applies here.
|
|
1529
|
+
const split = this.planAuto(args.with, args.stableRelationOrder, false);
|
|
1180
1530
|
if (split)
|
|
1181
1531
|
return this.runAutoSplit(args, split, true);
|
|
1182
1532
|
}
|
|
@@ -1225,6 +1575,12 @@ class QueryInterface {
|
|
|
1225
1575
|
args = { ...args, with: normalized };
|
|
1226
1576
|
}
|
|
1227
1577
|
const includePii = args.includePii === true;
|
|
1578
|
+
// findUnique is never flatten-planned: it reads ONE parent row, so the
|
|
1579
|
+
// correlated subquery already runs exactly once and a join buys nothing.
|
|
1580
|
+
// Say so, because the caller did ask for a strategy that is not running.
|
|
1581
|
+
if (args.with && this.resolveLoadStrategy(args.relationLoadStrategy) === 'flatten') {
|
|
1582
|
+
this.warnFlattenBlocked('findUnique reads a single parent row, where the correlated subquery already runs once');
|
|
1583
|
+
}
|
|
1228
1584
|
const columnsList = this.resolveColumns(args.select, args.omit, includePii);
|
|
1229
1585
|
// A global filter turns the where into `{ AND: [...] }`, which the
|
|
1230
1586
|
// `isSimpleWhere` test below rejects → the general (buildWhereClause) path
|
|
@@ -1337,6 +1693,7 @@ class QueryInterface {
|
|
|
1337
1693
|
// -------------------------------------------------------------------------
|
|
1338
1694
|
async findMany(args) {
|
|
1339
1695
|
this.maybeWarnUnlimited(args);
|
|
1696
|
+
this.maybeWarnUnorderedPage(args);
|
|
1340
1697
|
// Dev-only: warn on deeply nested with clauses
|
|
1341
1698
|
if (process.env.NODE_ENV !== 'production') {
|
|
1342
1699
|
if (args?.with) {
|
|
@@ -1353,7 +1710,7 @@ class QueryInterface {
|
|
|
1353
1710
|
if (strategy === 'batched')
|
|
1354
1711
|
return this.runFindManyBatched(args);
|
|
1355
1712
|
if (strategy === 'auto') {
|
|
1356
|
-
const split = this.planAuto(args.with, args.stableRelationOrder);
|
|
1713
|
+
const split = this.planAuto(args.with, args.stableRelationOrder, this.autoParentSetLarge(args));
|
|
1357
1714
|
if (split)
|
|
1358
1715
|
return this.runAutoSplit(args, split, false);
|
|
1359
1716
|
}
|
|
@@ -1473,6 +1830,23 @@ class QueryInterface {
|
|
|
1473
1830
|
if (normalized !== args.with)
|
|
1474
1831
|
args = { ...args, with: normalized };
|
|
1475
1832
|
}
|
|
1833
|
+
// An empty orderBy carries no ordering, so treat it as ABSENT rather than
|
|
1834
|
+
// emitting a bare `ORDER BY` with nothing after it (a syntax error at the
|
|
1835
|
+
// following LIMIT). This matters most for the documented escape hatch from
|
|
1836
|
+
// implicit ordering, "pass an explicit orderBy": callers who assemble that
|
|
1837
|
+
// array conditionally end up passing `[]`. Normalized here, before the
|
|
1838
|
+
// implicit-ordering and fingerprinting steps, so every downstream path sees
|
|
1839
|
+
// one shape.
|
|
1840
|
+
if (args?.orderBy !== undefined && isEmptyOrderBy(args.orderBy)) {
|
|
1841
|
+
args = { ...args, orderBy: undefined };
|
|
1842
|
+
}
|
|
1843
|
+
// Deterministic pagination (opt-in): fill a PK-asc orderBy into a paginating
|
|
1844
|
+
// query that declares none, BEFORE fingerprinting so the ordered and
|
|
1845
|
+
// unordered shapes get distinct cache entries. No-op unless
|
|
1846
|
+
// `implicitPkOrdering` is enabled (see implicitPkOrderBy).
|
|
1847
|
+
const implicitOrder = this.implicitPkOrderBy(args);
|
|
1848
|
+
if (implicitOrder)
|
|
1849
|
+
args = { ...args, orderBy: implicitOrder };
|
|
1476
1850
|
// `distinct` + relation orderBy is refused up front (E003): the distinct
|
|
1477
1851
|
// path re-orders in an outer wrapper (`... AS "<table>_distinct" ORDER BY
|
|
1478
1852
|
// <userOrder>`) where a correlated relation subquery (pick-row, `_count`,
|
|
@@ -1535,7 +1909,16 @@ class QueryInterface {
|
|
|
1535
1909
|
// `withFp` does NOT capture; it is projection-invariant). So it MUST be its
|
|
1536
1910
|
// own cache-key segment: a cached no-PII statement must never serve an
|
|
1537
1911
|
// `includePii` call, nor vice versa.
|
|
1538
|
-
|
|
1912
|
+
// `relationLoadStrategy: 'flatten'` compiles eligible to-one relations to
|
|
1913
|
+
// LEFT JOINs instead of correlated subqueries — a completely different
|
|
1914
|
+
// statement for the same `with` shape, which `withFp` (strategy-blind)
|
|
1915
|
+
// does not distinguish. So the plan gets its own cache-key segment, exactly
|
|
1916
|
+
// like `pii=`: a join-planned template must never serve a flatten-planned
|
|
1917
|
+
// call. Absent (the default), the segment is empty and every existing cache
|
|
1918
|
+
// key is byte-identical to before.
|
|
1919
|
+
const flattenPlan = this.planFlatten(args, includePii);
|
|
1920
|
+
const flattenFp = flattenPlan ? `|fl=${flattenPlan.signature}` : '';
|
|
1921
|
+
const ck = `fm:${whereFp}|c=${colKey}|o=${orderFp}|l=${limitFp}|off=${offsetFp}|cur=${cursorFp}|d=${distinctFp}|w=${withFp}|pii=${includePii ? 1 : 0}${flattenFp}${this.globalFilterCacheSegment()}`;
|
|
1539
1922
|
const params = [];
|
|
1540
1923
|
const buildSql = (freshParams) => {
|
|
1541
1924
|
// Fresh build: generates SQL and populates freshParams
|
|
@@ -1552,9 +1935,15 @@ class QueryInterface {
|
|
|
1552
1935
|
distinctCols = args.distinct.map((k) => this.toSqlColumn(k));
|
|
1553
1936
|
distinctPrefix = `DISTINCT ON (${distinctCols.join(', ')}) `;
|
|
1554
1937
|
}
|
|
1938
|
+
// Join-sink for `relationLoadStrategy: 'flatten'`. Filled while the SELECT
|
|
1939
|
+
// list is built (so the flattened relations' ON-clause params interleave
|
|
1940
|
+
// with the `with` params in one traversal, which the collect path
|
|
1941
|
+
// mirrors) and spliced into the FROM clause at assembly. Stays empty for
|
|
1942
|
+
// every other plan → byte-identical SQL.
|
|
1943
|
+
const relationJoins = [];
|
|
1555
1944
|
let selectClause;
|
|
1556
1945
|
if (args?.with) {
|
|
1557
|
-
selectClause = this.buildSelectWithRelations(this.table, args.with, freshParams, columnsList, undefined, undefined, includePii);
|
|
1946
|
+
selectClause = this.buildSelectWithRelations(this.table, args.with, freshParams, columnsList, undefined, undefined, includePii, flattenPlan ? { plan: flattenPlan, joinSink: relationJoins } : undefined);
|
|
1558
1947
|
}
|
|
1559
1948
|
else if (columnsList) {
|
|
1560
1949
|
selectClause = columnsList.map((c) => `${qt}.${this.q(c)}`).join(', ');
|
|
@@ -1620,7 +2009,7 @@ class QueryInterface {
|
|
|
1620
2009
|
const orderBySql = args?.orderBy
|
|
1621
2010
|
? ` ORDER BY ${this.buildOrderBy(args.orderBy, freshParams, lateralJoins)}`
|
|
1622
2011
|
: '';
|
|
1623
|
-
sql = `SELECT ${distinctPrefix}${selectClause} FROM ${qt}${lateralJoins.join('')}${tail}${orderBySql}`;
|
|
2012
|
+
sql = `SELECT ${distinctPrefix}${selectClause} FROM ${qt}${relationJoins.join('')}${lateralJoins.join('')}${tail}${orderBySql}`;
|
|
1624
2013
|
}
|
|
1625
2014
|
// Pagination — push params in the same order the collect path mirrors
|
|
1626
2015
|
// (limit before offset); the SQL TEXT shape is dialect-owned via
|
|
@@ -1628,10 +2017,10 @@ class QueryInterface {
|
|
|
1628
2017
|
let limitPh;
|
|
1629
2018
|
let offsetPh;
|
|
1630
2019
|
if (effectiveLimit !== undefined) {
|
|
1631
|
-
limitPh = this.paginationRef(effectiveLimit, freshParams);
|
|
2020
|
+
limitPh = this.paginationRef(effectiveLimit, freshParams, 'limit');
|
|
1632
2021
|
}
|
|
1633
2022
|
if (args?.offset !== undefined) {
|
|
1634
|
-
offsetPh = this.paginationRef(args.offset, freshParams);
|
|
2023
|
+
offsetPh = this.paginationRef(args.offset, freshParams, 'skip/offset');
|
|
1635
2024
|
}
|
|
1636
2025
|
sql += this.buildPagination(limitPh, offsetPh, !!args?.orderBy);
|
|
1637
2026
|
return sql;
|
|
@@ -1644,7 +2033,7 @@ class QueryInterface {
|
|
|
1644
2033
|
}
|
|
1645
2034
|
// 2. WITH relation params
|
|
1646
2035
|
if (args?.with) {
|
|
1647
|
-
this.collectWithParams(args.with, params);
|
|
2036
|
+
this.collectWithParams(args.with, params, undefined, flattenPlan);
|
|
1648
2037
|
}
|
|
1649
2038
|
// 3. Cursor params — sorted (canonical) order, matching cursorFp and the build path.
|
|
1650
2039
|
if (args?.cursor) {
|
|
@@ -1661,15 +2050,18 @@ class QueryInterface {
|
|
|
1661
2050
|
// 5. LIMIT param — skipped when the dialect inlines pagination (build path
|
|
1662
2051
|
// mirrors via paginationRef → no placeholder, no param).
|
|
1663
2052
|
if (effectiveLimit !== undefined && !this.dialect.inlineLimitOffset) {
|
|
1664
|
-
|
|
2053
|
+
// Validate here too: on a cache HIT the build path never runs, and a
|
|
2054
|
+
// warmed template would otherwise bind an unvalidated NaN (= SQL NULL,
|
|
2055
|
+
// i.e. no limit at all).
|
|
2056
|
+
params.push(this.paginationValue(effectiveLimit, 'limit'));
|
|
1665
2057
|
}
|
|
1666
2058
|
// 6. OFFSET param — same inline gate as LIMIT above.
|
|
1667
2059
|
if (args?.offset !== undefined && !this.dialect.inlineLimitOffset) {
|
|
1668
|
-
params.push(
|
|
2060
|
+
params.push(this.paginationValue(args.offset, 'skip/offset'));
|
|
1669
2061
|
}
|
|
1670
2062
|
this.crossCheckCache('findMany', ck, entry, buildSql, params);
|
|
1671
2063
|
// Build the row parser once (positional shapes are computed here, not per row).
|
|
1672
|
-
const parseWith = args?.with ? this.makeNestedParser(args.with, includePii) : null;
|
|
2064
|
+
const parseWith = args?.with ? this.makeNestedParser(args.with, includePii, flattenPlan) : null;
|
|
1673
2065
|
return {
|
|
1674
2066
|
sql: entry.sql,
|
|
1675
2067
|
params,
|
|
@@ -1692,14 +2084,19 @@ class QueryInterface {
|
|
|
1692
2084
|
* method fall back to the full cursor path.
|
|
1693
2085
|
*
|
|
1694
2086
|
* **Cursor path:** Uses DECLARE CURSOR within a dedicated transaction on a
|
|
1695
|
-
* single pooled connection. The cursor is
|
|
1696
|
-
* connection released when iteration completes
|
|
1697
|
-
* (
|
|
2087
|
+
* single pooled connection. The cursor is CLOSEd (in the dialect's `finally`)
|
|
2088
|
+
* and the connection released both when iteration completes normally and when
|
|
2089
|
+
* it ends early (`break` from `for await`). An error mid-stream skips the
|
|
2090
|
+
* CLOSE and rolls back instead, which drops the cursor with the transaction.
|
|
1698
2091
|
*
|
|
1699
|
-
* **Snapshot semantics note:**
|
|
1700
|
-
*
|
|
1701
|
-
*
|
|
1702
|
-
*
|
|
2092
|
+
* **Snapshot semantics note:** Outside a transaction the speculative
|
|
2093
|
+
* fast-path runs unwrapped, and an overflow opens the cursor in its own
|
|
2094
|
+
* transaction, so the two fetches span two separate snapshots. Wrapping the
|
|
2095
|
+
* call in `$transaction` gives strict single-snapshot semantics: both the
|
|
2096
|
+
* speculative fetch and the cursor then run on the caller's connection
|
|
2097
|
+
* inside the caller's transaction (the cursor path issues no BEGIN/COMMIT of
|
|
2098
|
+
* its own and releases nothing, so the caller's transaction is intact when
|
|
2099
|
+
* iteration finishes).
|
|
1703
2100
|
*
|
|
1704
2101
|
* @example
|
|
1705
2102
|
* ```ts
|
|
@@ -1712,7 +2109,14 @@ class QueryInterface {
|
|
|
1712
2109
|
const batchSize = Math.max(1, Math.floor(Number(args?.batchSize ?? 1000)));
|
|
1713
2110
|
const hasRelations = !!args?.with;
|
|
1714
2111
|
// Build the positional-aware relation parser once for the whole stream.
|
|
1715
|
-
|
|
2112
|
+
// Same flatten plan buildFindMany compiles below. The plan is a pure
|
|
2113
|
+
// function of the schema, the `with` shape and `includePii` — never of
|
|
2114
|
+
// `limit` — so the batch-size override the speculative fetch applies cannot
|
|
2115
|
+
// change it, and the stream's parser matches the emitted SQL.
|
|
2116
|
+
const streamFlattenPlan = hasRelations ? this.planFlatten(args, args?.includePii === true) : null;
|
|
2117
|
+
const parseWith = hasRelations
|
|
2118
|
+
? this.makeNestedParser(args.with, args?.includePii === true, streamFlattenPlan)
|
|
2119
|
+
: null;
|
|
1716
2120
|
// --- Speculative first fetch: try to satisfy the entire drain in one RTT ---
|
|
1717
2121
|
const speculativeDeferred = this.buildFindMany({
|
|
1718
2122
|
...args,
|
|
@@ -1729,13 +2133,24 @@ class QueryInterface {
|
|
|
1729
2133
|
}
|
|
1730
2134
|
// --- Overflow: fall back to cursor path from scratch ---
|
|
1731
2135
|
const deferred = this.buildFindMany(args);
|
|
1732
|
-
// Acquire a dedicated connection
|
|
2136
|
+
// Acquire a dedicated connection: cursors require a single connection in a
|
|
1733
2137
|
// transaction. The dialect owns the streaming SQL (Postgres: BEGIN → DECLARE
|
|
1734
2138
|
// … NO SCROLL CURSOR FOR → FETCH n → CLOSE → COMMIT, ROLLBACK on error); we
|
|
1735
2139
|
// just parse + yield the row batches it produces.
|
|
1736
|
-
|
|
2140
|
+
//
|
|
2141
|
+
// Inside a caller-owned transaction there is nothing to check out: the
|
|
2142
|
+
// transaction-scoped pool pins every query to the transaction's own
|
|
2143
|
+
// connection, so `pool.query` already IS that connection. The stream rides
|
|
2144
|
+
// on it, is never released here, and the dialect is told to emit no
|
|
2145
|
+
// transaction control of its own (`ambientTransaction`).
|
|
2146
|
+
const client = this.txScoped ? null : await this.pool.connect();
|
|
2147
|
+
const conn = client ?? {
|
|
2148
|
+
query: async (text, values) => (await this.pool.query(text, values)),
|
|
2149
|
+
};
|
|
1737
2150
|
try {
|
|
1738
|
-
for await (const batch of this.dialect.openStream(
|
|
2151
|
+
for await (const batch of this.dialect.openStream(conn, deferred.sql, deferred.params, batchSize, {
|
|
2152
|
+
ambientTransaction: this.txScoped,
|
|
2153
|
+
})) {
|
|
1739
2154
|
for (const row of batch) {
|
|
1740
2155
|
yield (parseWith ? parseWith(row) : this.parseRow(row, this.table));
|
|
1741
2156
|
}
|
|
@@ -1746,7 +2161,7 @@ class QueryInterface {
|
|
|
1746
2161
|
throw (0, errors_js_1.wrapPgError)(err);
|
|
1747
2162
|
}
|
|
1748
2163
|
finally {
|
|
1749
|
-
client
|
|
2164
|
+
client?.release();
|
|
1750
2165
|
}
|
|
1751
2166
|
}
|
|
1752
2167
|
// -------------------------------------------------------------------------
|
|
@@ -1762,7 +2177,8 @@ class QueryInterface {
|
|
|
1762
2177
|
return (rows[0] ?? null);
|
|
1763
2178
|
}
|
|
1764
2179
|
if (strategy === 'auto') {
|
|
1765
|
-
|
|
2180
|
+
// findFirst is findMany + LIMIT 1: a one-row parent set.
|
|
2181
|
+
const split = this.planAuto(args.with, args.stableRelationOrder, false);
|
|
1766
2182
|
if (split) {
|
|
1767
2183
|
const rows = (await this.runAutoSplit({ ...args, limit: 1 }, split, false));
|
|
1768
2184
|
return (rows[0] ?? null);
|
|
@@ -1911,11 +2327,22 @@ class QueryInterface {
|
|
|
1911
2327
|
}
|
|
1912
2328
|
async runInImplicitTx(fn) {
|
|
1913
2329
|
const client = await this.pool.connect();
|
|
2330
|
+
let began = false;
|
|
1914
2331
|
try {
|
|
1915
2332
|
await client.query(this.dialect.beginStatement());
|
|
2333
|
+
began = true;
|
|
1916
2334
|
const { TransactionClient } = await Promise.resolve().then(() => __importStar(require('../client.js')));
|
|
2335
|
+
const tx = new TransactionClient(
|
|
1917
2336
|
// biome-ignore lint/suspicious/noExplicitAny: MiddlewareFn and Middleware are structurally identical
|
|
1918
|
-
|
|
2337
|
+
client, this.schema,
|
|
2338
|
+
// biome-ignore lint/suspicious/noExplicitAny: MiddlewareFn and Middleware are structurally identical
|
|
2339
|
+
this.middlewares, this.options,
|
|
2340
|
+
// Pass the source pool so its read-only guard + capabilities carry into
|
|
2341
|
+
// the transaction-scoped proxy pool (see createTxPool). Without it a
|
|
2342
|
+
// read-only client's nested writes bypass the E018 guard and an
|
|
2343
|
+
// older-engine client falls back to the full capability set inside the
|
|
2344
|
+
// implicit transaction.
|
|
2345
|
+
this.pool);
|
|
1919
2346
|
// biome-ignore lint/suspicious/noExplicitAny: TransactionClient satisfies NestedWriteContext['tx'] at runtime
|
|
1920
2347
|
const ctx = { schema: this.schema, tx: tx };
|
|
1921
2348
|
const result = await fn(ctx);
|
|
@@ -1923,11 +2350,15 @@ class QueryInterface {
|
|
|
1923
2350
|
return result;
|
|
1924
2351
|
}
|
|
1925
2352
|
catch (err) {
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
2353
|
+
// Only roll back a transaction we actually opened: a failed BEGIN must
|
|
2354
|
+
// not emit a stray ROLLBACK on a connection that never began one.
|
|
2355
|
+
if (began) {
|
|
2356
|
+
try {
|
|
2357
|
+
await client.query(this.dialect.rollbackStatement());
|
|
2358
|
+
}
|
|
2359
|
+
catch {
|
|
2360
|
+
// Best-effort rollback: connection may have died.
|
|
2361
|
+
}
|
|
1931
2362
|
}
|
|
1932
2363
|
throw err;
|
|
1933
2364
|
}
|
|
@@ -2074,8 +2505,8 @@ class QueryInterface {
|
|
|
2074
2505
|
withFingerprint(withClause, table, depth = 0) {
|
|
2075
2506
|
return relationsMod.withFingerprint(this.ctx, withClause, table, depth);
|
|
2076
2507
|
}
|
|
2077
|
-
collectWithParams(withClause, params, table) {
|
|
2078
|
-
relationsMod.collectWithParams(this.ctx, withClause, params, table);
|
|
2508
|
+
collectWithParams(withClause, params, table, flattenPlan) {
|
|
2509
|
+
relationsMod.collectWithParams(this.ctx, withClause, params, table, flattenPlan);
|
|
2079
2510
|
}
|
|
2080
2511
|
orderByEntryFingerprint(d, targetTable) {
|
|
2081
2512
|
return relationsMod.orderByEntryFingerprint(this.ctx, d, targetTable);
|
|
@@ -2107,11 +2538,75 @@ class QueryInterface {
|
|
|
2107
2538
|
getCamelDateFields(table, meta) {
|
|
2108
2539
|
return relationsMod.getCamelDateFields(this.ctx, table, meta);
|
|
2109
2540
|
}
|
|
2110
|
-
makeNestedParser(withClause, includePii) {
|
|
2111
|
-
return relationsMod.makeNestedParser(this.ctx, withClause, includePii);
|
|
2541
|
+
makeNestedParser(withClause, includePii, flattenPlan) {
|
|
2542
|
+
return relationsMod.makeNestedParser(this.ctx, withClause, includePii, flattenPlan);
|
|
2543
|
+
}
|
|
2544
|
+
buildSelectWithRelations(table, withClause, params, columnsList, depth, path, includePii, flatten) {
|
|
2545
|
+
return relationsMod.buildSelectWithRelations(this.ctx, table, withClause, params, columnsList, depth, path, includePii, flatten);
|
|
2112
2546
|
}
|
|
2113
|
-
|
|
2114
|
-
|
|
2547
|
+
/**
|
|
2548
|
+
* Compile the `relationLoadStrategy: 'flatten'` plan for a findMany-shaped
|
|
2549
|
+
* query, or `null` to emit exactly the SQL (and cache key) the default
|
|
2550
|
+
* strategy emits.
|
|
2551
|
+
*
|
|
2552
|
+
* `'flatten'` compiles an eligible to-one relation to a `LEFT JOIN` with a
|
|
2553
|
+
* prefixed scalar projection instead of a correlated `json_build_object`
|
|
2554
|
+
* subquery. The correlated form is re-evaluated once per parent row, so its
|
|
2555
|
+
* cost scales with the parent set no matter how well the FK is indexed; the
|
|
2556
|
+
* join does not, and unlike `'batched'` it stays a single round trip.
|
|
2557
|
+
*
|
|
2558
|
+
* It is an EXPLICIT opt-in: `'auto'` is unchanged and never selects it.
|
|
2559
|
+
*
|
|
2560
|
+
* Query-shape gates (any of these routes the WHOLE query back to the default
|
|
2561
|
+
* strategy, silently and byte-identically):
|
|
2562
|
+
* - the resolved strategy is not `'flatten'`;
|
|
2563
|
+
* - `jsonEncoding: 'positional'` (a flattened relation emits no JSON at all,
|
|
2564
|
+
* so the two encodings are not composed in this version);
|
|
2565
|
+
* - the dialect owns relation-subquery generation
|
|
2566
|
+
* (`dialect.buildRelationSubquery`, i.e. SQL Server's `FOR JSON PATH`);
|
|
2567
|
+
* - `distinct` (the `DISTINCT ON` rewrite re-orders in an outer wrapper, and
|
|
2568
|
+
* the extra projected columns have not been proven safe there).
|
|
2569
|
+
*
|
|
2570
|
+
* `limit` / `offset` / `cursor` / `orderBy` need no gate: every flattened join
|
|
2571
|
+
* is over a PROVABLY UNIQUE target key, so it matches at most one row per
|
|
2572
|
+
* parent and cannot change the parent row count that pagination applies to.
|
|
2573
|
+
*
|
|
2574
|
+
* Per-relation eligibility lives in `planFlattenWith` / `planFlattenNode`.
|
|
2575
|
+
* Only the findMany family is planned (`findMany`, `findFirst`,
|
|
2576
|
+
* `findManyStream`, and pipelined `buildFindMany`); `findUnique` reads a single
|
|
2577
|
+
* parent row, where the correlated subquery runs exactly once, so it stays on
|
|
2578
|
+
* the default path.
|
|
2579
|
+
*/
|
|
2580
|
+
planFlatten(args, includePii) {
|
|
2581
|
+
const withClause = args?.with;
|
|
2582
|
+
if (!withClause)
|
|
2583
|
+
return null;
|
|
2584
|
+
if (this.resolveLoadStrategy(args?.relationLoadStrategy) !== 'flatten')
|
|
2585
|
+
return null;
|
|
2586
|
+
// Query-level refusals: the whole plan is off, so name the reason once for
|
|
2587
|
+
// the query rather than once per relation (relations.ts warns per relation
|
|
2588
|
+
// for the eligibility rules it owns).
|
|
2589
|
+
const queryLevelBlock = this.jsonEncoding === 'positional'
|
|
2590
|
+
? "`jsonEncoding: 'positional'` is active, and a flattened relation emits no JSON to encode"
|
|
2591
|
+
: this.dialect.buildRelationSubquery
|
|
2592
|
+
? `the ${this.dialect.name} dialect generates relation subqueries itself`
|
|
2593
|
+
: args?.distinct && args.distinct.length > 0
|
|
2594
|
+
? 'the query uses `distinct`'
|
|
2595
|
+
: undefined;
|
|
2596
|
+
if (queryLevelBlock) {
|
|
2597
|
+
this.warnFlattenBlocked(queryLevelBlock);
|
|
2598
|
+
return null;
|
|
2599
|
+
}
|
|
2600
|
+
return relationsMod.planFlattenWith(this.ctx, this.table, withClause, includePii);
|
|
2601
|
+
}
|
|
2602
|
+
/** Dev-only once-only note that `'flatten'` was refused for the whole query. */
|
|
2603
|
+
warnFlattenBlocked(reason) {
|
|
2604
|
+
if (process.env.NODE_ENV === 'production')
|
|
2605
|
+
return;
|
|
2606
|
+
if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.flattenFallback, `${this.table}|query|${reason}`))
|
|
2607
|
+
return;
|
|
2608
|
+
console.warn(`[turbine] relationLoadStrategy: 'flatten' did not engage on "${this.table}": ${reason}. ` +
|
|
2609
|
+
'Every relation loads via the correlated subquery instead (same rows, same values, different plan).');
|
|
2115
2610
|
}
|
|
2116
2611
|
/** Convert camelCase field name to snake_case column name (unquoted, for non-SQL uses) */
|
|
2117
2612
|
toColumn(field) {
|
|
@@ -2302,7 +2797,18 @@ class QueryInterface {
|
|
|
2302
2797
|
const value = row[col];
|
|
2303
2798
|
const field = reverseMap[col] ?? col; // fall back to raw col name, not regex
|
|
2304
2799
|
// Top-level rows are snake_case (dateCols); nested rows are camelCase (camelDateFields).
|
|
2305
|
-
|
|
2800
|
+
//
|
|
2801
|
+
// An ARRAY value is excluded: `dateColumns` includes array-of-date
|
|
2802
|
+
// columns (`date[]`, `timestamp[]`, `timestamptz[]`), for which the
|
|
2803
|
+
// driver already hands back a `Date[]`. Coercing it ran
|
|
2804
|
+
// `new Date(String(theArray))` and replaced the whole array with a
|
|
2805
|
+
// single Invalid Date — the column was unreadable on every strategy.
|
|
2806
|
+
// The join strategy's string arrays are handled upstream instead, by
|
|
2807
|
+
// the JSON-wire decode in relations.ts.
|
|
2808
|
+
if ((dateCols.has(col) || camelDateFields.has(field)) &&
|
|
2809
|
+
value !== null &&
|
|
2810
|
+
!(value instanceof Date) &&
|
|
2811
|
+
!Array.isArray(value)) {
|
|
2306
2812
|
// Offset-less strings (Postgres `timestamp`, json_agg output) are
|
|
2307
2813
|
// pinned to UTC so results don't depend on the server's time zone.
|
|
2308
2814
|
parsed[field] = this.utcTimestamps ? (0, utils_js_1.parseDbDate)(String(value)) : new Date(value);
|