turbine-orm 0.51.0 → 0.52.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 +33 -5
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/client.d.ts +106 -2
- package/dist/cjs/client.js +111 -5
- package/dist/cjs/dialect.d.ts +33 -0
- package/dist/cjs/dialect.js +14 -0
- package/dist/cjs/engine-config.d.ts +49 -0
- package/dist/cjs/engine-config.js +19 -0
- package/dist/cjs/index-advisor.js +0 -0
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +3 -2
- package/dist/cjs/mssql.d.ts +8 -3
- package/dist/cjs/mssql.js +22 -3
- package/dist/cjs/mysql.d.ts +7 -3
- package/dist/cjs/mysql.js +20 -3
- package/dist/cjs/nested-write.d.ts +31 -0
- package/dist/cjs/nested-write.js +80 -2
- package/dist/cjs/powdb-introspect.d.ts +10 -1
- package/dist/cjs/powdb-introspect.js +10 -1
- package/dist/cjs/powdb.d.ts +116 -6
- package/dist/cjs/powdb.js +169 -10
- package/dist/cjs/powql.d.ts +161 -1
- package/dist/cjs/powql.js +299 -19
- package/dist/cjs/prisma-compat.d.ts +54 -8
- package/dist/cjs/prisma-compat.js +136 -20
- package/dist/cjs/query/batched-loader.d.ts +7 -0
- package/dist/cjs/query/batched-loader.js +97 -15
- package/dist/cjs/query/builder.d.ts +131 -5
- package/dist/cjs/query/builder.js +223 -19
- package/dist/cjs/query/compound-unique.js +0 -0
- package/dist/cjs/query/index.d.ts +1 -1
- package/dist/cjs/query/index.js +2 -1
- package/dist/cjs/query/warn-registry.d.ts +10 -0
- package/dist/cjs/query/warn-registry.js +10 -0
- package/dist/cjs/query/writes.js +115 -7
- package/dist/cjs/sqlite.d.ts +10 -4
- package/dist/cjs/sqlite.js +18 -4
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/client.d.ts +106 -2
- package/dist/client.js +111 -5
- package/dist/dialect.d.ts +33 -0
- package/dist/dialect.js +14 -0
- package/dist/engine-config.d.ts +49 -0
- package/dist/engine-config.js +18 -0
- package/dist/index-advisor.js +0 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mssql.d.ts +8 -3
- package/dist/mssql.js +22 -3
- package/dist/mysql.d.ts +7 -3
- package/dist/mysql.js +20 -3
- package/dist/nested-write.d.ts +31 -0
- package/dist/nested-write.js +79 -2
- package/dist/powdb-introspect.d.ts +10 -1
- package/dist/powdb-introspect.js +10 -1
- package/dist/powdb.d.ts +116 -6
- package/dist/powdb.js +167 -9
- package/dist/powql.d.ts +161 -1
- package/dist/powql.js +299 -19
- package/dist/prisma-compat.d.ts +54 -8
- package/dist/prisma-compat.js +136 -20
- package/dist/query/batched-loader.d.ts +7 -0
- package/dist/query/batched-loader.js +98 -16
- package/dist/query/builder.d.ts +131 -5
- package/dist/query/builder.js +222 -18
- package/dist/query/compound-unique.js +0 -0
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.js +1 -1
- package/dist/query/warn-registry.d.ts +10 -0
- package/dist/query/warn-registry.js +10 -0
- package/dist/query/writes.js +116 -8
- package/dist/sqlite.d.ts +10 -4
- package/dist/sqlite.js +19 -5
- package/package.json +3 -3
package/dist/query/builder.js
CHANGED
|
@@ -134,6 +134,82 @@ export const AUTO_TO_ONE_JOIN_MAX_ROWS = Math.round(AUTO_ASSUMED_ROUND_TRIP_MS /
|
|
|
134
134
|
*/
|
|
135
135
|
export const AUTO_TO_ONE_JOIN_ROWS_MIN = 100;
|
|
136
136
|
export const AUTO_TO_ONE_JOIN_ROWS_MAX = 100_000;
|
|
137
|
+
/**
|
|
138
|
+
* WHY THIS IS A CONFIGURED LATENCY AND NOT A MEASURED ONE.
|
|
139
|
+
*
|
|
140
|
+
* The obvious next step from the formula above is to have the client measure
|
|
141
|
+
* its own round-trip time and derive the threshold at runtime. That was built
|
|
142
|
+
* and benchmarked, and it is NOT what ships, for a reason worth recording so it
|
|
143
|
+
* is not re-litigated blind:
|
|
144
|
+
*
|
|
145
|
+
* Every query's wall time is `roundTrip + serverWork`, and nothing in a
|
|
146
|
+
* duration distinguishes the two. An all-time MINIMUM reads a lucky packet
|
|
147
|
+
* (1.489ms on a link whose real per-statement cost was 2.862ms) and lands the
|
|
148
|
+
* threshold at half the true break-even. A MEDIAN over recent durations is
|
|
149
|
+
* accurate when the workload is cheap queries, but the workload being planned
|
|
150
|
+
* for here is precisely the expensive one: in the verification sweep the ring
|
|
151
|
+
* filled with 10-17ms relation queries, the estimate inflated, and `'auto'`
|
|
152
|
+
* held an 8,000-row query on the join plan, 1.30x slower than the better plan,
|
|
153
|
+
* WORSE than the fixed constant it replaced. Capping the median against a
|
|
154
|
+
* multiple of the floor mitigates it but turns the whole thing into a pair of
|
|
155
|
+
* magic numbers tuned against two synthetic links, which is the same mistake as
|
|
156
|
+
* a socket-tuned row count wearing a different hat.
|
|
157
|
+
*
|
|
158
|
+
* Round-trip time is a deployment fact, not a runtime discovery: it is fixed by
|
|
159
|
+
* where the app runs relative to the database, the operator knows it (or gets
|
|
160
|
+
* it from one `ping`), and it does not change between queries. So it is
|
|
161
|
+
* configuration. That also keeps plan selection deterministic, which matters
|
|
162
|
+
* for a library whose documented guarantee is that the strategy changes the
|
|
163
|
+
* plan and never the result.
|
|
164
|
+
*/
|
|
165
|
+
/**
|
|
166
|
+
* The smallest plan-time parent-row bound at which `'auto'` moves a relation
|
|
167
|
+
* `_count` on a PROVEN-UNINDEXED probe to the grouped follow-up. Deliberately
|
|
168
|
+
* 2, i.e. "everything except a parent set provably bounded at one row".
|
|
169
|
+
*
|
|
170
|
+
* `_count` does NOT share the to-one break-even formula above, because its two
|
|
171
|
+
* plans do not differ by a small per-row penalty. Writing S for one scan of the
|
|
172
|
+
* child table and RTT for a round trip:
|
|
173
|
+
*
|
|
174
|
+
* inline(N) = N x S (a correlated COUNT(*) per parent row; see
|
|
175
|
+
* buildRelationCountExpr in relations.ts, the
|
|
176
|
+
* inline form is NOT a grouped scan)
|
|
177
|
+
* batched(N) = S + RTT (one `COUNT(*) ... GROUP BY fk` follow-up)
|
|
178
|
+
*
|
|
179
|
+
* so the crossover sits at `N = 1 + RTT/S` and, decisively, the two regrets are
|
|
180
|
+
* not comparable in kind:
|
|
181
|
+
*
|
|
182
|
+
* - choosing batched when inline would have won costs at most RTT, once, and
|
|
183
|
+
* ONLY at N = 1 (at N = 1 the difference is exactly RTT, and it shrinks to
|
|
184
|
+
* zero immediately after);
|
|
185
|
+
* - choosing inline when batched would have won costs (N - 1) x S, which is
|
|
186
|
+
* unbounded in the parent count.
|
|
187
|
+
*
|
|
188
|
+
* Measured on an UNINDEXED FK (PostgreSQL 16, 200K-row child table, 10K-row
|
|
189
|
+
* parent table, median of 11 interleaved reps per point, loopback;
|
|
190
|
+
* benchmarks/bench-count-strategy.ts):
|
|
191
|
+
*
|
|
192
|
+
* parents 1 2 3 5 20 100 1000 10000
|
|
193
|
+
* inline 4.3ms 8.3ms 12.3ms 20.1ms 79.2ms 417.5ms 3.06s 31.06s
|
|
194
|
+
* batched 5.2ms 4.8ms 4.8ms 5.2ms 6.1ms 11.5ms 9.9ms 28.4ms
|
|
195
|
+
* winner inline batched batched batched batched batched batched batched
|
|
196
|
+
* ratio 1.22x 1.73x 2.54x 3.84x 13.05x 36.42x 310.92x 1093.35x
|
|
197
|
+
*
|
|
198
|
+
* Inline wins exactly one cell, by 0.9ms, then loses the next by 1.73x and the
|
|
199
|
+
* last by 1093x. A skewed child distribution (half the rows on ten parents)
|
|
200
|
+
* moves nothing: same crossover at 2, same 1179x at 10,000. So the useful
|
|
201
|
+
* threshold is not a tunable row count, it is the one row where inline provably
|
|
202
|
+
* cannot lose. There is deliberately no config knob: the entire regret this rule
|
|
203
|
+
* can produce is one round trip, which is less than any knob would be worth, and
|
|
204
|
+
* `relationLoadStrategy: 'join'` already forces the single-statement plan.
|
|
205
|
+
*
|
|
206
|
+
* This applies ONLY to a probe the introspected index metadata PROVES unindexed.
|
|
207
|
+
* An INDEXED `_count` stays inline at every size measured (inline wins 1.30x to
|
|
208
|
+
* 2.06x from 1 to 10,000 parents, because the per-parent subquery collapses to
|
|
209
|
+
* an index-only scan costing ~0.001ms), and the partition below never demotes
|
|
210
|
+
* it.
|
|
211
|
+
*/
|
|
212
|
+
export const AUTO_COUNT_BATCH_MIN_PARENT_ROWS = 2;
|
|
137
213
|
/**
|
|
138
214
|
* Strict structural equality for a single SQL parameter value. Handles the
|
|
139
215
|
* value shapes Turbine binds: primitives (incl. `NaN` and `bigint`), `null`/
|
|
@@ -436,15 +512,41 @@ export class QueryInterface {
|
|
|
436
512
|
this.txScoped = options?._txScoped ?? false;
|
|
437
513
|
this.options = options;
|
|
438
514
|
// Pre-compute column type lookup maps (TASK-26)
|
|
515
|
+
//
|
|
516
|
+
// Metadata can carry a column's database type in EITHER of two places: on
|
|
517
|
+
// the column entry (`dialectType` / `pgType`, what introspection and
|
|
518
|
+
// `turbine generate` emit) or in the table-level `dialectTypes` / `pgTypes`
|
|
519
|
+
// maps. Reading only the column entry is not a harmless miss for metadata
|
|
520
|
+
// that populates just the table-level maps: an unresolved type makes
|
|
521
|
+
// `coerceWriteValue` return a bound `Date` by identity, so a zone-less
|
|
522
|
+
// `date` / `timestamp` column stores the PROCESS's local calendar fields
|
|
523
|
+
// (and, because the read path pins UTC, a turbine-only round trip hides
|
|
524
|
+
// it). Consult both, column entry first. This costs one extra own-property
|
|
525
|
+
// lookup per column ONCE per QueryInterface, nothing per query.
|
|
439
526
|
this.columnPgTypeMap = new Map();
|
|
440
527
|
this.columnArrayTypeMap = new Map();
|
|
441
528
|
this.crossSchemaTypeColumns = new Set();
|
|
529
|
+
const tableDialectTypes = this.tableMeta.dialectTypes;
|
|
530
|
+
const tablePgTypes = this.tableMeta.pgTypes;
|
|
442
531
|
for (const col of this.tableMeta.columns) {
|
|
443
|
-
|
|
444
|
-
|
|
532
|
+
const dbType = col.dialectType ??
|
|
533
|
+
col.pgType ??
|
|
534
|
+
(tableDialectTypes && ownLookup(tableDialectTypes, col.name)) ??
|
|
535
|
+
(tablePgTypes && ownLookup(tablePgTypes, col.name));
|
|
536
|
+
if (dbType !== undefined)
|
|
537
|
+
this.columnPgTypeMap.set(col.name, dbType);
|
|
538
|
+
// The array map has the same shape of gap, but TableMetadata carries NO
|
|
539
|
+
// table-level array-type map (only `dialectTypes` / `pgTypes`), so there
|
|
540
|
+
// is nothing to fall back to. Deriving one from the resolved base type
|
|
541
|
+
// would invent an UNNEST cast the metadata never declared, so the column
|
|
542
|
+
// entry stays the only source.
|
|
543
|
+
const arrayType = col.arrayType ?? col.pgArrayType;
|
|
544
|
+
if (arrayType !== undefined)
|
|
545
|
+
this.columnArrayTypeMap.set(col.name, arrayType);
|
|
445
546
|
if (col.pgTypeSchema !== undefined)
|
|
446
547
|
this.crossSchemaTypeColumns.add(col.name);
|
|
447
548
|
}
|
|
549
|
+
this.warnUntypedColumns();
|
|
448
550
|
// Bind the shared WHERE-walk view once. `tableMeta` is immutable after this
|
|
449
551
|
// point; the method wrappers forward to the (private) instance methods so
|
|
450
552
|
// the walk needs no public accessors on the class.
|
|
@@ -468,6 +570,18 @@ export class QueryInterface {
|
|
|
468
570
|
scopedHostCache: this.scopedHostCache,
|
|
469
571
|
columnPgTypeMap: this.columnPgTypeMap,
|
|
470
572
|
columnArrayTypeMap: this.columnArrayTypeMap,
|
|
573
|
+
// The `utcTimestamps: false` opt-out has to reach the write/where web:
|
|
574
|
+
// omitting it here left `qi.utcTimestamps` undefined for the whole
|
|
575
|
+
// module, where `!== false` reads as opted IN, so the write side ignored
|
|
576
|
+
// the flag and rewrote binds the caller had opted out of.
|
|
577
|
+
//
|
|
578
|
+
// This half of the flag is PER CLIENT. The read half is not: it is the
|
|
579
|
+
// pg OID 1114 type parser, which `pg.types.setTypeParser` installs once
|
|
580
|
+
// per process. Two clients in one process therefore cannot hold
|
|
581
|
+
// different values, and TurbineClient refuses the second one rather than
|
|
582
|
+
// building a client whose writes and reads disagree (see
|
|
583
|
+
// `assertUtcTimestampsAgree` in client.ts).
|
|
584
|
+
utcTimestamps: this.utcTimestamps,
|
|
471
585
|
crossSchemaTypeColumns: this.crossSchemaTypeColumns,
|
|
472
586
|
get currentSkip() {
|
|
473
587
|
return self.currentSkip;
|
|
@@ -498,6 +612,73 @@ export class QueryInterface {
|
|
|
498
612
|
paginationValue: (value, arg) => this.paginationValue(value, arg),
|
|
499
613
|
};
|
|
500
614
|
}
|
|
615
|
+
/**
|
|
616
|
+
* Dev-only, once per table: the columns whose database type is absent from
|
|
617
|
+
* BOTH the column entry and the table-level type maps, the residual case
|
|
618
|
+
* after the two-source resolution above.
|
|
619
|
+
*
|
|
620
|
+
* The set is deliberately every untyped column, not the `dateColumns`
|
|
621
|
+
* members. An unresolved type is precisely the state in which Turbine cannot
|
|
622
|
+
* say WHICH kind of column it is, so restricting the scan to `dateColumns`
|
|
623
|
+
* got it wrong in both directions: that set carries `timestamptz` (whose
|
|
624
|
+
* bind was never affected, since binding the `Date` is the correct thing to
|
|
625
|
+
* do for it) and omits `time` / `timetz` entirely (deliberately, see
|
|
626
|
+
* `timeOfDayKind` in schema.ts), which is the one kind that fails LOUDLY
|
|
627
|
+
* rather than silently. The message therefore names the columns and states
|
|
628
|
+
* what each kind does, rather than asserting a kind it cannot know.
|
|
629
|
+
*
|
|
630
|
+
* What is actually at stake per kind, all of it `coerceWriteValue` returning
|
|
631
|
+
* the bound `Date` by identity for want of a type:
|
|
632
|
+
* - zone-less `date` / `timestamp`: the driver serializes with the
|
|
633
|
+
* PROCESS's offset, so the column stores local calendar fields. Nothing
|
|
634
|
+
* surfaces at runtime, and a turbine-only round trip reads the same value
|
|
635
|
+
* back (the read path shifts by the same offset), so only an outside
|
|
636
|
+
* reader sees the drift.
|
|
637
|
+
* - `time` / `timetz`: the driver serializes a full ISO timestamp, which
|
|
638
|
+
* Postgres rejects with `22007 invalid input syntax for type time`.
|
|
639
|
+
* - `timestamptz` and every non-temporal type: unaffected.
|
|
640
|
+
*
|
|
641
|
+
* PostgreSQL only: the UTC bind rewrite is Postgres-gated (see
|
|
642
|
+
* `utcDateTimeWrites` in writes.ts), so on the other engines a missing type
|
|
643
|
+
* changes nothing about how a `Date` is bound. Suppressed under
|
|
644
|
+
* `NODE_ENV=production` like the other dev diagnostics and deduped through
|
|
645
|
+
* the shared registry, so a hot table logs one line for the process.
|
|
646
|
+
*
|
|
647
|
+
* Cannot throw on odd metadata: it walks `tableMeta.columns`, the array the
|
|
648
|
+
* constructor loop above has already iterated (and that client.ts validates
|
|
649
|
+
* as an array), never `dateColumns`, which is a `Set` in every first-party
|
|
650
|
+
* metadata path but arrives as a plain object from JSON-round-tripped
|
|
651
|
+
* metadata. A dev-only diagnostic that crashes a shape production would serve
|
|
652
|
+
* is worse than the bug it reports.
|
|
653
|
+
*/
|
|
654
|
+
warnUntypedColumns() {
|
|
655
|
+
if (process.env.NODE_ENV === 'production')
|
|
656
|
+
return;
|
|
657
|
+
if (this.dialect.name !== 'postgresql')
|
|
658
|
+
return;
|
|
659
|
+
const untyped = [];
|
|
660
|
+
for (const col of this.tableMeta.columns) {
|
|
661
|
+
if (this.columnPgTypeMap.get(col.name) === undefined)
|
|
662
|
+
untyped.push(col.name);
|
|
663
|
+
}
|
|
664
|
+
if (untyped.length === 0)
|
|
665
|
+
return;
|
|
666
|
+
if (!shouldWarnOnce(WARN_NS.untypedDateColumn, this.table))
|
|
667
|
+
return;
|
|
668
|
+
// Bound the line on a wide table: the fix is per table, not per column, so
|
|
669
|
+
// the first few names are enough to recognize the metadata that produced it.
|
|
670
|
+
const MAX_NAMED = 12;
|
|
671
|
+
const named = untyped.slice(0, MAX_NAMED).join(', ');
|
|
672
|
+
const rest = untyped.length > MAX_NAMED ? ` (+${untyped.length - MAX_NAMED} more)` : '';
|
|
673
|
+
console.warn(`[turbine] table "${this.table}": no database type in metadata for column(s) ${named}${rest} (neither the ` +
|
|
674
|
+
"column entry's `dialectType`/`pgType` nor the table-level `dialectTypes`/`pgTypes` map). Turbine cannot " +
|
|
675
|
+
'tell which of them are zone-less, so a `Date` written to one is bound by the driver as-is: right for ' +
|
|
676
|
+
"`timestamptz`, but a zone-less `date`/`timestamp` column then stores the PROCESS's local calendar fields " +
|
|
677
|
+
'rather than UTC (silently, since the read path shifts back by the same offset), and a `time`/`timetz` ' +
|
|
678
|
+
'column rejects the value outright (`22007 invalid input syntax for type time`). Columns whose type IS ' +
|
|
679
|
+
'resolved, every `timestamptz` among them, are unaffected. Regenerate the metadata with ' +
|
|
680
|
+
'`npx turbine generate`, or set the column types in your `defineSchema` definition.');
|
|
681
|
+
}
|
|
501
682
|
/** Quote an identifier through the active SQL dialect. */
|
|
502
683
|
q(name) {
|
|
503
684
|
return this.dialect.quoteIdentifier(name);
|
|
@@ -942,8 +1123,20 @@ export class QueryInterface {
|
|
|
942
1123
|
* `findFirst` pass `false` explicitly (their parent set is one row).
|
|
943
1124
|
*/
|
|
944
1125
|
autoParentSetLarge(args) {
|
|
945
|
-
const
|
|
946
|
-
return
|
|
1126
|
+
const bound = this.autoParentBound(args);
|
|
1127
|
+
return bound === undefined || bound > this.autoToOneThreshold();
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* The plan-time UPPER BOUND on the parent-row count, or `undefined` when the
|
|
1131
|
+
* query is unbounded. This is the raw number behind
|
|
1132
|
+
* {@link autoParentSetLarge}; the `_count` rule needs the number itself
|
|
1133
|
+
* because its threshold ({@link AUTO_COUNT_BATCH_MIN_PARENT_ROWS}) is two
|
|
1134
|
+
* rows rather than the to-one break-even. `findUnique` / `findFirst` pass `1`
|
|
1135
|
+
* directly: their parent set is one row as a matter of the statement's shape,
|
|
1136
|
+
* not an estimate.
|
|
1137
|
+
*/
|
|
1138
|
+
autoParentBound(args) {
|
|
1139
|
+
return args?.take ?? args?.limit ?? this.defaultLimit;
|
|
947
1140
|
}
|
|
948
1141
|
/**
|
|
949
1142
|
* The parent-row count at which `'auto'` stops preferring the single-statement
|
|
@@ -990,13 +1183,13 @@ export class QueryInterface {
|
|
|
990
1183
|
*
|
|
991
1184
|
* Everything else (indexed to-many, composite-key, unknown) stays in `joinWith`
|
|
992
1185
|
* (byte-identical join). The reserved `_count` key falls back on rule 1 only,
|
|
993
|
-
* and
|
|
994
|
-
*
|
|
995
|
-
*
|
|
996
|
-
*
|
|
997
|
-
* the dev note.
|
|
1186
|
+
* and on its OWN size rule: an inline `_count` is one correlated `COUNT(*)`
|
|
1187
|
+
* per parent row over an unindexed child table, so the grouped follow-up wins
|
|
1188
|
+
* from {@link AUTO_COUNT_BATCH_MIN_PARENT_ROWS} parent rows upward and inline
|
|
1189
|
+
* is preferred only when the parent set is provably bounded below that. Also
|
|
1190
|
+
* returns the engaged relations for the dev note.
|
|
998
1191
|
*/
|
|
999
|
-
partitionWithForAuto(withClause, parentSetLarge) {
|
|
1192
|
+
partitionWithForAuto(withClause, parentSetLarge, parentBound) {
|
|
1000
1193
|
const hasIndexInfo = schemaHasIndexInfo(this.schema);
|
|
1001
1194
|
const joinWith = {};
|
|
1002
1195
|
const batchedWith = {};
|
|
@@ -1006,7 +1199,8 @@ export class QueryInterface {
|
|
|
1006
1199
|
continue;
|
|
1007
1200
|
if (key === '_count') {
|
|
1008
1201
|
const cv = this.autoCountVerdict(spec, this.tableMeta);
|
|
1009
|
-
|
|
1202
|
+
const countWorthBatching = parentBound === undefined || parentBound >= AUTO_COUNT_BATCH_MIN_PARENT_ROWS;
|
|
1203
|
+
if (hasIndexInfo && countWorthBatching && cv.unindexed && cv.eligible) {
|
|
1010
1204
|
batchedWith[key] = spec;
|
|
1011
1205
|
engaged.push({ relation: '_count', reason: 'unindexed', miss: cv.miss });
|
|
1012
1206
|
}
|
|
@@ -1044,7 +1238,7 @@ export class QueryInterface {
|
|
|
1044
1238
|
* to batched. Returns `null` (→ run the plain join path, byte-identical, same
|
|
1045
1239
|
* cache keys) when nothing qualifies.
|
|
1046
1240
|
*/
|
|
1047
|
-
planAuto(withArg, stableFlag, parentSetLarge) {
|
|
1241
|
+
planAuto(withArg, stableFlag, parentSetLarge, parentBound) {
|
|
1048
1242
|
// Without DB-backed index info (code-first / defineSchema-only) no probe can
|
|
1049
1243
|
// be PROVEN unindexed; the to-one cardinality rule does not depend on index
|
|
1050
1244
|
// metadata, so it still applies.
|
|
@@ -1053,7 +1247,7 @@ export class QueryInterface {
|
|
|
1053
1247
|
const withClause = this.resolveStableOrder(stableFlag)
|
|
1054
1248
|
? this.applyStableRelationOrder(withArg, this.table)
|
|
1055
1249
|
: withArg;
|
|
1056
|
-
const split = this.partitionWithForAuto(withClause, parentSetLarge);
|
|
1250
|
+
const split = this.partitionWithForAuto(withClause, parentSetLarge, parentBound);
|
|
1057
1251
|
if (Object.keys(split.batchedWith).length === 0)
|
|
1058
1252
|
return null;
|
|
1059
1253
|
return split;
|
|
@@ -1075,7 +1269,15 @@ export class QueryInterface {
|
|
|
1075
1269
|
const probe = e.miss
|
|
1076
1270
|
? `probe "${e.miss.table}"(${e.miss.columns.join(', ')}) has no covering index`
|
|
1077
1271
|
: 'a probe in its subtree has no covering index';
|
|
1078
|
-
|
|
1272
|
+
// The `_count` case is the one people read as a needless demotion, because
|
|
1273
|
+
// the follow-up statement is a grouped COUNT and the inline form looks
|
|
1274
|
+
// like it would be one too. It is not: state the shape it replaced.
|
|
1275
|
+
const why = e.relation === '_count'
|
|
1276
|
+
? ' The inline form is one correlated COUNT(*) re-evaluated per parent row, so on an unindexed ' +
|
|
1277
|
+
`probe it is one full scan of "${e.miss?.table ?? 'the child table'}" per parent row; the ` +
|
|
1278
|
+
'follow-up is one grouped scan for the whole page.'
|
|
1279
|
+
: '';
|
|
1280
|
+
console.warn(`[turbine] auto strategy: relation "${e.relation}" on "${this.table}" loads batched (${probe}).${why} ` +
|
|
1079
1281
|
"Create the covering index (or set `relationLoadStrategy: 'join'` to force the single-statement " +
|
|
1080
1282
|
'plan); run `npx turbine doctor` for the exact CREATE INDEX SQL.');
|
|
1081
1283
|
}
|
|
@@ -1491,8 +1693,10 @@ export class QueryInterface {
|
|
|
1491
1693
|
return this.runFindUniqueBatched(args);
|
|
1492
1694
|
if (strategy === 'auto') {
|
|
1493
1695
|
// findUnique's parent set is a single row: the join plan's correlated
|
|
1494
|
-
// subqueries run once, so the cardinality rule never applies here
|
|
1495
|
-
|
|
1696
|
+
// subqueries run once, so the cardinality rule never applies here, and
|
|
1697
|
+
// an inline `_count` is one scan against the batched plan's one scan
|
|
1698
|
+
// plus a round trip.
|
|
1699
|
+
const split = this.planAuto(args.with, args.stableRelationOrder, false, 1);
|
|
1496
1700
|
if (split)
|
|
1497
1701
|
return this.runAutoSplit(args, split, true);
|
|
1498
1702
|
}
|
|
@@ -1676,7 +1880,7 @@ export class QueryInterface {
|
|
|
1676
1880
|
if (strategy === 'batched')
|
|
1677
1881
|
return this.runFindManyBatched(args);
|
|
1678
1882
|
if (strategy === 'auto') {
|
|
1679
|
-
const split = this.planAuto(args.with, args.stableRelationOrder, this.autoParentSetLarge(args));
|
|
1883
|
+
const split = this.planAuto(args.with, args.stableRelationOrder, this.autoParentSetLarge(args), this.autoParentBound(args));
|
|
1680
1884
|
if (split)
|
|
1681
1885
|
return this.runAutoSplit(args, split, false);
|
|
1682
1886
|
}
|
|
@@ -2185,7 +2389,7 @@ export class QueryInterface {
|
|
|
2185
2389
|
}
|
|
2186
2390
|
if (strategy === 'auto') {
|
|
2187
2391
|
// findFirst is findMany + LIMIT 1: a one-row parent set.
|
|
2188
|
-
const split = this.planAuto(args.with, args.stableRelationOrder, false);
|
|
2392
|
+
const split = this.planAuto(args.with, args.stableRelationOrder, false, 1);
|
|
2189
2393
|
if (split) {
|
|
2190
2394
|
const rows = (await this.runAutoSplit({ ...args, limit: 1 }, split, false));
|
|
2191
2395
|
return (rows[0] ?? null);
|
|
Binary file
|
package/dist/query/index.d.ts
CHANGED
|
@@ -11,4 +11,4 @@ export { postgresDialect } from '../dialect.js';
|
|
|
11
11
|
export type { SqlCacheEntry } from './utils.js';
|
|
12
12
|
export { buildCorrelation, escapeLike, escSingleQuote, fnv1a64Hex, LRUCache, OPERATOR_KEYS, quoteIdent, sqlToPreparedName, } from './utils.js';
|
|
13
13
|
export type { DeferredQuery, MiddlewareFn, QueryEvent, QueryEventListener, QueryInterfaceOptions, ReselectExecutor, } from './builder.js';
|
|
14
|
-
export { AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, QueryInterface, } from './builder.js';
|
|
14
|
+
export { AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, QueryInterface, } from './builder.js';
|
package/dist/query/index.js
CHANGED
|
@@ -7,4 +7,4 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export { postgresDialect } from '../dialect.js';
|
|
9
9
|
export { buildCorrelation, escapeLike, escSingleQuote, fnv1a64Hex, LRUCache, OPERATOR_KEYS, quoteIdent, sqlToPreparedName, } from './utils.js';
|
|
10
|
-
export { AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, QueryInterface, } from './builder.js';
|
|
10
|
+
export { AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, QueryInterface, } from './builder.js';
|
|
@@ -65,4 +65,14 @@ export declare const WARN_NS: {
|
|
|
65
65
|
* engage is indistinguishable from one that does nothing, so say so once.
|
|
66
66
|
*/
|
|
67
67
|
readonly flattenFallback: "flattenFallback";
|
|
68
|
+
/**
|
|
69
|
+
* Columns whose database type resolves from NEITHER the column entry nor the
|
|
70
|
+
* table-level type maps, so Turbine cannot tell which of them are zone-less
|
|
71
|
+
* and a bound `Date` is left to the driver rather than rewritten to a UTC
|
|
72
|
+
* literal (builder.ts `warnUntypedColumns`). The scan covers every column,
|
|
73
|
+
* not just `dateColumns`, because an unresolved type is precisely the state
|
|
74
|
+
* in which the kind is unknown; the warning emits one line per table listing
|
|
75
|
+
* the offenders, not one per column. The namespace name is historical.
|
|
76
|
+
*/
|
|
77
|
+
readonly untypedDateColumn: "untypedDateColumn";
|
|
68
78
|
};
|
|
@@ -100,4 +100,14 @@ export const WARN_NS = {
|
|
|
100
100
|
* engage is indistinguishable from one that does nothing, so say so once.
|
|
101
101
|
*/
|
|
102
102
|
flattenFallback: 'flattenFallback',
|
|
103
|
+
/**
|
|
104
|
+
* Columns whose database type resolves from NEITHER the column entry nor the
|
|
105
|
+
* table-level type maps, so Turbine cannot tell which of them are zone-less
|
|
106
|
+
* and a bound `Date` is left to the driver rather than rewritten to a UTC
|
|
107
|
+
* literal (builder.ts `warnUntypedColumns`). The scan covers every column,
|
|
108
|
+
* not just `dateColumns`, because an unresolved type is precisely the state
|
|
109
|
+
* in which the kind is unknown; the warning emits one line per table listing
|
|
110
|
+
* the offenders, not one per column. The namespace name is historical.
|
|
111
|
+
*/
|
|
112
|
+
untypedDateColumn: 'untypedDateColumn',
|
|
103
113
|
};
|
package/dist/query/writes.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* stay class-resident, reached through the ctx. See builder.ts for the thin
|
|
11
11
|
* delegating methods and the async execute wrappers.
|
|
12
12
|
*/
|
|
13
|
-
import { NotFoundError, OptimisticLockError, ValidationError } from '../errors.js';
|
|
13
|
+
import { NotFoundError, OptimisticLockError, UnsupportedFeatureError, ValidationError } from '../errors.js';
|
|
14
14
|
import { camelToSnake, snakeToCamel } from '../schema.js';
|
|
15
15
|
import { expandCompoundUniqueWhere } from './compound-unique.js';
|
|
16
16
|
import { isUnmatchedPlainObject, UPDATE_OPERATOR_KEYS } from './filters.js';
|
|
@@ -77,6 +77,36 @@ export function buildReselectByWhere(qi, whereObj) {
|
|
|
77
77
|
const where = clause ? ` WHERE ${clause}` : '';
|
|
78
78
|
return { sql: `SELECT ${writeReselectSelection(qi)} FROM ${qi.q(qi.table)}${where}`, params };
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Build the all-defaults INSERT for a `data` that names no column, via the
|
|
82
|
+
* dialect's `buildDefaultValuesInsertStatement` hook.
|
|
83
|
+
*
|
|
84
|
+
* `INSERT INTO t () VALUES ()` (what the column-driven builders render for an
|
|
85
|
+
* empty `data`) is a syntax error everywhere but MySQL, and the correct
|
|
86
|
+
* statement differs per engine, so the shape lives behind the dialect seam.
|
|
87
|
+
* `{}` is easy to reach honestly, a handler that assembles its payload from
|
|
88
|
+
* optional request fields produces it on a request that supplied none of them,
|
|
89
|
+
* so the row of pure defaults is inserted and returned rather than crashing.
|
|
90
|
+
* The empty-`update` no-op in {@link buildUpdate} has the same rationale.
|
|
91
|
+
*
|
|
92
|
+
* A table with no usable defaults still fails, correctly, with the database's
|
|
93
|
+
* own NOT NULL violation (E010): nothing here pre-empts that.
|
|
94
|
+
*
|
|
95
|
+
* A dialect predating the hook raises E017 rather than emitting SQL its engine
|
|
96
|
+
* will reject.
|
|
97
|
+
*/
|
|
98
|
+
function buildDefaultValuesInsert(qi, rowCount, skipDuplicates) {
|
|
99
|
+
const build = qi.dialect.buildDefaultValuesInsertStatement;
|
|
100
|
+
if (!build) {
|
|
101
|
+
throw new UnsupportedFeatureError('create/createMany with an empty data object', qi.dialect.name, 'This dialect has no all-defaults INSERT form; name at least one column in `data`.');
|
|
102
|
+
}
|
|
103
|
+
return build.call(qi.dialect, {
|
|
104
|
+
table: qi.q(qi.table),
|
|
105
|
+
rowCount,
|
|
106
|
+
skipDuplicates,
|
|
107
|
+
returning: writeReturningColumns(qi),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
80
110
|
export function buildCreate(qi, args) {
|
|
81
111
|
assertWritable(qi, 'create');
|
|
82
112
|
assertNoGeneratedColumns(qi, args.data, 'create');
|
|
@@ -85,12 +115,15 @@ export function buildCreate(qi, args) {
|
|
|
85
115
|
const params = entries.map(([k, v]) => coerceWriteValue(qi, k, v));
|
|
86
116
|
// Enum columns get an explicit `::"EnumName"` cast (see enumTypeForColumn).
|
|
87
117
|
const placeholders = entries.map(([k], i) => `${qi.p(i + 1)}${whereMod.enumCastSuffix(qi, qi.toColumn(k))}`);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
118
|
+
// `data: {}` (or all-undefined) names no column: insert a row of defaults.
|
|
119
|
+
const sql = entries.length === 0
|
|
120
|
+
? buildDefaultValuesInsert(qi, 1)
|
|
121
|
+
: qi.dialect.buildInsertStatement({
|
|
122
|
+
table: qi.q(qi.table),
|
|
123
|
+
columns,
|
|
124
|
+
valuePlaceholders: placeholders,
|
|
125
|
+
returning: writeReturningColumns(qi),
|
|
126
|
+
});
|
|
94
127
|
return {
|
|
95
128
|
sql,
|
|
96
129
|
params,
|
|
@@ -137,6 +170,69 @@ export function makeCreateReselect(qi, insertSql, insertParams, data) {
|
|
|
137
170
|
return exec(`SELECT ${writeReselectSelection(qi)} FROM ${qi.q(qi.table)}${where}`, selParams);
|
|
138
171
|
};
|
|
139
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* The fields a write's `data` object actually names.
|
|
175
|
+
*
|
|
176
|
+
* A key whose value is `undefined` is NOT named: single-row {@link buildCreate}
|
|
177
|
+
* filters those out of its column list, so the column takes its declared
|
|
178
|
+
* default. `createMany` reads its rows through this same helper, so
|
|
179
|
+
* `{ n: undefined }` and `{}` mean the identical thing on both paths.
|
|
180
|
+
*/
|
|
181
|
+
function definedKeys(row) {
|
|
182
|
+
return Object.keys(row).filter((k) => row[k] !== undefined);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Refuse a `createMany` whose rows do not all name the SAME fields.
|
|
186
|
+
*
|
|
187
|
+
* `createMany` compiles ONE statement, and its column list is taken from the
|
|
188
|
+
* first row alone, so a row that disagrees with the first row loses data in
|
|
189
|
+
* both directions and silently:
|
|
190
|
+
*
|
|
191
|
+
* - a field the first row names and a later row OMITS is bound as NULL,
|
|
192
|
+
* overwriting that column's declared default (and failing outright on a NOT
|
|
193
|
+
* NULL column that has one);
|
|
194
|
+
* - a field only a LATER row names is not in the column list at all, so its
|
|
195
|
+
* value never reaches the database.
|
|
196
|
+
*
|
|
197
|
+
* Neither shape is expressible as a single statement on every engine. The
|
|
198
|
+
* row-major `VALUES` form can carry a per-cell `DEFAULT` keyword on
|
|
199
|
+
* PostgreSQL, MySQL and SQL Server, but SQLite has no such grammar (`VALUES
|
|
200
|
+
* (1, DEFAULT)` is a parse error there), and adopting it would also drop
|
|
201
|
+
* PostgreSQL off the column-major `UNNEST` form that binds one array per
|
|
202
|
+
* column rather than one placeholder per cell. So the honest answer is a
|
|
203
|
+
* refusal that names the offending row and its differing columns.
|
|
204
|
+
*
|
|
205
|
+
* A single row is trivially uniform, and rows that all name the same fields
|
|
206
|
+
* (the overwhelmingly common shape) cost one `Object.keys` pass and emit
|
|
207
|
+
* byte-identical SQL.
|
|
208
|
+
*/
|
|
209
|
+
function assertUniformCreateManyRows(qi, rows, firstKeys) {
|
|
210
|
+
const expected = new Set(firstKeys);
|
|
211
|
+
for (let i = 1; i < rows.length; i++) {
|
|
212
|
+
const rowKeys = definedKeys(rows[i]);
|
|
213
|
+
const unexpected = rowKeys.filter((k) => !expected.has(k));
|
|
214
|
+
// No stranger and the same count means the same set (object keys are unique).
|
|
215
|
+
if (unexpected.length === 0 && rowKeys.length === expected.size)
|
|
216
|
+
continue;
|
|
217
|
+
const present = new Set(rowKeys);
|
|
218
|
+
const missing = firstKeys.filter((k) => !present.has(k));
|
|
219
|
+
const parts = [];
|
|
220
|
+
if (missing.length > 0)
|
|
221
|
+
parts.push(`does not supply ${quoteList(missing)}`);
|
|
222
|
+
if (unexpected.length > 0)
|
|
223
|
+
parts.push(`supplies ${quoteList(unexpected)}, which the first row does not`);
|
|
224
|
+
throw new ValidationError(`[turbine] createMany on "${qi.table}": row ${i} ${parts.join(' and ')}. ` +
|
|
225
|
+
'Every row must supply the same fields: createMany builds ONE statement whose column list comes from the ' +
|
|
226
|
+
"first row, so a field a later row omits would be written as NULL over that column's default, and a field " +
|
|
227
|
+
'only a later row names would be dropped. Supply the field explicitly on every row (a field set to ' +
|
|
228
|
+
'`undefined` counts as omitted, exactly as it does in `create`), or split the call into one createMany per ' +
|
|
229
|
+
'row shape.');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** `a, b` → `"a", "b"`, for the field lists in {@link assertUniformCreateManyRows}. */
|
|
233
|
+
function quoteList(fields) {
|
|
234
|
+
return fields.map((f) => `"${f}"`).join(', ');
|
|
235
|
+
}
|
|
140
236
|
export function buildCreateMany(qi, args) {
|
|
141
237
|
const qt = qi.q(qi.table);
|
|
142
238
|
if (args.data.length === 0) {
|
|
@@ -151,7 +247,19 @@ export function buildCreateMany(qi, args) {
|
|
|
151
247
|
for (const row of args.data) {
|
|
152
248
|
assertNoGeneratedColumns(qi, row, 'createMany');
|
|
153
249
|
}
|
|
154
|
-
const keys =
|
|
250
|
+
const keys = definedKeys(args.data[0]);
|
|
251
|
+
assertUniformCreateManyRows(qi, args.data, keys);
|
|
252
|
+
// No column named by the first row: every row is pure defaults (the bulk
|
|
253
|
+
// counterpart of `create({ data: {} })`, see buildDefaultValuesInsert). The
|
|
254
|
+
// uniformity check above has already established that EVERY row is empty.
|
|
255
|
+
if (keys.length === 0) {
|
|
256
|
+
return {
|
|
257
|
+
sql: buildDefaultValuesInsert(qi, args.data.length, args.skipDuplicates),
|
|
258
|
+
params: [],
|
|
259
|
+
transform: (result) => result.rows.map((row) => parseWriteRow(qi, row)),
|
|
260
|
+
tag: `${qi.table}.createMany`,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
155
263
|
const columns = keys.map((k) => qi.toColumn(k));
|
|
156
264
|
const rowValues = args.data.map((row) => {
|
|
157
265
|
const record = row;
|
package/dist/sqlite.d.ts
CHANGED
|
@@ -46,8 +46,9 @@
|
|
|
46
46
|
* ```
|
|
47
47
|
*/
|
|
48
48
|
import type { DatabaseSync } from 'node:sqlite';
|
|
49
|
-
import { type PgCompatPool, type PgCompatPoolClient, TurbineClient
|
|
49
|
+
import { type PgCompatPool, type PgCompatPoolClient, TurbineClient } from './client.js';
|
|
50
50
|
import { type Dialect, type IntrospectOptions } from './dialect.js';
|
|
51
|
+
import type { EngineClientConfig } from './engine-config.js';
|
|
51
52
|
import { type SchemaMetadata, type TableMetadata } from './schema.js';
|
|
52
53
|
/** pg-style query argument: a SQL string or a `{ text, values }` config object. */
|
|
53
54
|
type QueryArg = string | {
|
|
@@ -109,8 +110,12 @@ export declare function introspectSqliteDatabase(db: DatabaseSync, options?: {
|
|
|
109
110
|
* are per-handle), so codegen should target a real file.
|
|
110
111
|
*/
|
|
111
112
|
export declare function introspectSqlite(options: IntrospectOptions): Promise<SchemaMetadata>;
|
|
112
|
-
/**
|
|
113
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Options for {@link turbineSqlite}: every client-level {@link TurbineConfig}
|
|
115
|
+
* field the engine can honour (see {@link EngineClientConfig}) plus the SQLite
|
|
116
|
+
* pragmas below.
|
|
117
|
+
*/
|
|
118
|
+
export interface TurbineSqliteOptions extends EngineClientConfig {
|
|
114
119
|
/**
|
|
115
120
|
* Enable WAL journal mode for file databases (better read concurrency).
|
|
116
121
|
* Ignored for `':memory:'`. Default: `true`.
|
|
@@ -131,7 +136,8 @@ export interface TurbineSqliteOptions extends Pick<TurbineConfig, 'logging' | 'd
|
|
|
131
136
|
*
|
|
132
137
|
* @param target A SQLite file path, `':memory:'`, or an open `DatabaseSync`.
|
|
133
138
|
* @param schema Introspected or hand-written {@link SchemaMetadata}.
|
|
134
|
-
* @param options Optional pragmas
|
|
139
|
+
* @param options Optional SQLite pragmas plus any client-level
|
|
140
|
+
* {@link TurbineConfig} field (see {@link EngineClientConfig}).
|
|
135
141
|
*
|
|
136
142
|
* @example
|
|
137
143
|
* ```ts
|
package/dist/sqlite.js
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
import { createRequire } from 'node:module';
|
|
49
49
|
import { TurbineClient } from './client.js';
|
|
50
50
|
import { postgresDialect, } from './dialect.js';
|
|
51
|
-
import { ConnectionError } from './errors.js';
|
|
51
|
+
import { ConnectionError, UnsupportedFeatureError } from './errors.js';
|
|
52
52
|
import { applyTableFilters, deriveEngineRelations } from './introspect.js';
|
|
53
53
|
import { isDateType, snakeToCamel, } from './schema.js';
|
|
54
54
|
let cachedDatabaseSync;
|
|
@@ -482,6 +482,17 @@ export const sqliteDialect = {
|
|
|
482
482
|
params: input.rowValues.flat(),
|
|
483
483
|
};
|
|
484
484
|
},
|
|
485
|
+
buildDefaultValuesInsertStatement(input) {
|
|
486
|
+
// SQLite's `DEFAULT VALUES` inserts exactly one row and the engine accepts
|
|
487
|
+
// neither `VALUES (DEFAULT, …)` nor MySQL's empty `VALUES ()` tuple, so
|
|
488
|
+
// there is no multi-row all-defaults statement to emit.
|
|
489
|
+
if (input.rowCount !== 1) {
|
|
490
|
+
throw new UnsupportedFeatureError(`createMany with ${input.rowCount} empty data rows`, 'sqlite', 'SQLite INSERT … DEFAULT VALUES inserts a single row and has no multi-row form; ' +
|
|
491
|
+
'issue one create({ data: {} }) per row.');
|
|
492
|
+
}
|
|
493
|
+
const conflict = input.skipDuplicates ? ' ON CONFLICT DO NOTHING' : '';
|
|
494
|
+
return `INSERT INTO ${input.table} DEFAULT VALUES${conflict}${this.buildReturningClause(input.returning)}`;
|
|
495
|
+
},
|
|
485
496
|
buildUpsertStatement(input) {
|
|
486
497
|
return (`INSERT INTO ${input.table} (${input.insertColumns.join(', ')}) VALUES (${input.valuePlaceholders.join(', ')})` +
|
|
487
498
|
` ON CONFLICT (${input.conflictColumns.join(', ')}) DO UPDATE SET ${input.updateSetClauses.join(', ')}` +
|
|
@@ -778,7 +789,8 @@ function openSqliteDatabase(target, options) {
|
|
|
778
789
|
*
|
|
779
790
|
* @param target A SQLite file path, `':memory:'`, or an open `DatabaseSync`.
|
|
780
791
|
* @param schema Introspected or hand-written {@link SchemaMetadata}.
|
|
781
|
-
* @param options Optional pragmas
|
|
792
|
+
* @param options Optional SQLite pragmas plus any client-level
|
|
793
|
+
* {@link TurbineConfig} field (see {@link EngineClientConfig}).
|
|
782
794
|
*
|
|
783
795
|
* @example
|
|
784
796
|
* ```ts
|
|
@@ -789,12 +801,14 @@ function openSqliteDatabase(target, options) {
|
|
|
789
801
|
export function turbineSqlite(target, schema, options = {}) {
|
|
790
802
|
const db = typeof target === 'string' ? openSqliteDatabase(target, options) : target;
|
|
791
803
|
const pool = new SqlitePool(db);
|
|
804
|
+
// Everything that is not a SQLite pragma is client config and rides through
|
|
805
|
+
// untouched, so a new TurbineConfig option works here the day it lands. The
|
|
806
|
+
// engine-owned keys come last, so no caller can unbind the dialect.
|
|
807
|
+
const { wal: _wal, busyTimeoutMs: _busyTimeoutMs, foreignKeys: _foreignKeys, ...clientConfig } = options;
|
|
792
808
|
return new TurbineClient({
|
|
809
|
+
...clientConfig,
|
|
793
810
|
pool,
|
|
794
811
|
dialect: sqliteDialect,
|
|
795
812
|
preparedStatements: false,
|
|
796
|
-
logging: options.logging,
|
|
797
|
-
defaultLimit: options.defaultLimit,
|
|
798
|
-
warnOnUnlimited: options.warnOnUnlimited,
|
|
799
813
|
}, schema);
|
|
800
814
|
}
|