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
|
@@ -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 = 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;
|
|
48
|
+
exports.QueryInterface = exports.AUTO_COUNT_BATCH_MIN_PARENT_ROWS = 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");
|
|
@@ -170,6 +170,82 @@ exports.AUTO_TO_ONE_JOIN_MAX_ROWS = Math.round(exports.AUTO_ASSUMED_ROUND_TRIP_M
|
|
|
170
170
|
*/
|
|
171
171
|
exports.AUTO_TO_ONE_JOIN_ROWS_MIN = 100;
|
|
172
172
|
exports.AUTO_TO_ONE_JOIN_ROWS_MAX = 100_000;
|
|
173
|
+
/**
|
|
174
|
+
* WHY THIS IS A CONFIGURED LATENCY AND NOT A MEASURED ONE.
|
|
175
|
+
*
|
|
176
|
+
* The obvious next step from the formula above is to have the client measure
|
|
177
|
+
* its own round-trip time and derive the threshold at runtime. That was built
|
|
178
|
+
* and benchmarked, and it is NOT what ships, for a reason worth recording so it
|
|
179
|
+
* is not re-litigated blind:
|
|
180
|
+
*
|
|
181
|
+
* Every query's wall time is `roundTrip + serverWork`, and nothing in a
|
|
182
|
+
* duration distinguishes the two. An all-time MINIMUM reads a lucky packet
|
|
183
|
+
* (1.489ms on a link whose real per-statement cost was 2.862ms) and lands the
|
|
184
|
+
* threshold at half the true break-even. A MEDIAN over recent durations is
|
|
185
|
+
* accurate when the workload is cheap queries, but the workload being planned
|
|
186
|
+
* for here is precisely the expensive one: in the verification sweep the ring
|
|
187
|
+
* filled with 10-17ms relation queries, the estimate inflated, and `'auto'`
|
|
188
|
+
* held an 8,000-row query on the join plan, 1.30x slower than the better plan,
|
|
189
|
+
* WORSE than the fixed constant it replaced. Capping the median against a
|
|
190
|
+
* multiple of the floor mitigates it but turns the whole thing into a pair of
|
|
191
|
+
* magic numbers tuned against two synthetic links, which is the same mistake as
|
|
192
|
+
* a socket-tuned row count wearing a different hat.
|
|
193
|
+
*
|
|
194
|
+
* Round-trip time is a deployment fact, not a runtime discovery: it is fixed by
|
|
195
|
+
* where the app runs relative to the database, the operator knows it (or gets
|
|
196
|
+
* it from one `ping`), and it does not change between queries. So it is
|
|
197
|
+
* configuration. That also keeps plan selection deterministic, which matters
|
|
198
|
+
* for a library whose documented guarantee is that the strategy changes the
|
|
199
|
+
* plan and never the result.
|
|
200
|
+
*/
|
|
201
|
+
/**
|
|
202
|
+
* The smallest plan-time parent-row bound at which `'auto'` moves a relation
|
|
203
|
+
* `_count` on a PROVEN-UNINDEXED probe to the grouped follow-up. Deliberately
|
|
204
|
+
* 2, i.e. "everything except a parent set provably bounded at one row".
|
|
205
|
+
*
|
|
206
|
+
* `_count` does NOT share the to-one break-even formula above, because its two
|
|
207
|
+
* plans do not differ by a small per-row penalty. Writing S for one scan of the
|
|
208
|
+
* child table and RTT for a round trip:
|
|
209
|
+
*
|
|
210
|
+
* inline(N) = N x S (a correlated COUNT(*) per parent row; see
|
|
211
|
+
* buildRelationCountExpr in relations.ts, the
|
|
212
|
+
* inline form is NOT a grouped scan)
|
|
213
|
+
* batched(N) = S + RTT (one `COUNT(*) ... GROUP BY fk` follow-up)
|
|
214
|
+
*
|
|
215
|
+
* so the crossover sits at `N = 1 + RTT/S` and, decisively, the two regrets are
|
|
216
|
+
* not comparable in kind:
|
|
217
|
+
*
|
|
218
|
+
* - choosing batched when inline would have won costs at most RTT, once, and
|
|
219
|
+
* ONLY at N = 1 (at N = 1 the difference is exactly RTT, and it shrinks to
|
|
220
|
+
* zero immediately after);
|
|
221
|
+
* - choosing inline when batched would have won costs (N - 1) x S, which is
|
|
222
|
+
* unbounded in the parent count.
|
|
223
|
+
*
|
|
224
|
+
* Measured on an UNINDEXED FK (PostgreSQL 16, 200K-row child table, 10K-row
|
|
225
|
+
* parent table, median of 11 interleaved reps per point, loopback;
|
|
226
|
+
* benchmarks/bench-count-strategy.ts):
|
|
227
|
+
*
|
|
228
|
+
* parents 1 2 3 5 20 100 1000 10000
|
|
229
|
+
* inline 4.3ms 8.3ms 12.3ms 20.1ms 79.2ms 417.5ms 3.06s 31.06s
|
|
230
|
+
* batched 5.2ms 4.8ms 4.8ms 5.2ms 6.1ms 11.5ms 9.9ms 28.4ms
|
|
231
|
+
* winner inline batched batched batched batched batched batched batched
|
|
232
|
+
* ratio 1.22x 1.73x 2.54x 3.84x 13.05x 36.42x 310.92x 1093.35x
|
|
233
|
+
*
|
|
234
|
+
* Inline wins exactly one cell, by 0.9ms, then loses the next by 1.73x and the
|
|
235
|
+
* last by 1093x. A skewed child distribution (half the rows on ten parents)
|
|
236
|
+
* moves nothing: same crossover at 2, same 1179x at 10,000. So the useful
|
|
237
|
+
* threshold is not a tunable row count, it is the one row where inline provably
|
|
238
|
+
* cannot lose. There is deliberately no config knob: the entire regret this rule
|
|
239
|
+
* can produce is one round trip, which is less than any knob would be worth, and
|
|
240
|
+
* `relationLoadStrategy: 'join'` already forces the single-statement plan.
|
|
241
|
+
*
|
|
242
|
+
* This applies ONLY to a probe the introspected index metadata PROVES unindexed.
|
|
243
|
+
* An INDEXED `_count` stays inline at every size measured (inline wins 1.30x to
|
|
244
|
+
* 2.06x from 1 to 10,000 parents, because the per-parent subquery collapses to
|
|
245
|
+
* an index-only scan costing ~0.001ms), and the partition below never demotes
|
|
246
|
+
* it.
|
|
247
|
+
*/
|
|
248
|
+
exports.AUTO_COUNT_BATCH_MIN_PARENT_ROWS = 2;
|
|
173
249
|
/**
|
|
174
250
|
* Strict structural equality for a single SQL parameter value. Handles the
|
|
175
251
|
* value shapes Turbine binds: primitives (incl. `NaN` and `bigint`), `null`/
|
|
@@ -472,15 +548,41 @@ class QueryInterface {
|
|
|
472
548
|
this.txScoped = options?._txScoped ?? false;
|
|
473
549
|
this.options = options;
|
|
474
550
|
// Pre-compute column type lookup maps (TASK-26)
|
|
551
|
+
//
|
|
552
|
+
// Metadata can carry a column's database type in EITHER of two places: on
|
|
553
|
+
// the column entry (`dialectType` / `pgType`, what introspection and
|
|
554
|
+
// `turbine generate` emit) or in the table-level `dialectTypes` / `pgTypes`
|
|
555
|
+
// maps. Reading only the column entry is not a harmless miss for metadata
|
|
556
|
+
// that populates just the table-level maps: an unresolved type makes
|
|
557
|
+
// `coerceWriteValue` return a bound `Date` by identity, so a zone-less
|
|
558
|
+
// `date` / `timestamp` column stores the PROCESS's local calendar fields
|
|
559
|
+
// (and, because the read path pins UTC, a turbine-only round trip hides
|
|
560
|
+
// it). Consult both, column entry first. This costs one extra own-property
|
|
561
|
+
// lookup per column ONCE per QueryInterface, nothing per query.
|
|
475
562
|
this.columnPgTypeMap = new Map();
|
|
476
563
|
this.columnArrayTypeMap = new Map();
|
|
477
564
|
this.crossSchemaTypeColumns = new Set();
|
|
565
|
+
const tableDialectTypes = this.tableMeta.dialectTypes;
|
|
566
|
+
const tablePgTypes = this.tableMeta.pgTypes;
|
|
478
567
|
for (const col of this.tableMeta.columns) {
|
|
479
|
-
|
|
480
|
-
|
|
568
|
+
const dbType = col.dialectType ??
|
|
569
|
+
col.pgType ??
|
|
570
|
+
(tableDialectTypes && (0, utils_js_1.ownLookup)(tableDialectTypes, col.name)) ??
|
|
571
|
+
(tablePgTypes && (0, utils_js_1.ownLookup)(tablePgTypes, col.name));
|
|
572
|
+
if (dbType !== undefined)
|
|
573
|
+
this.columnPgTypeMap.set(col.name, dbType);
|
|
574
|
+
// The array map has the same shape of gap, but TableMetadata carries NO
|
|
575
|
+
// table-level array-type map (only `dialectTypes` / `pgTypes`), so there
|
|
576
|
+
// is nothing to fall back to. Deriving one from the resolved base type
|
|
577
|
+
// would invent an UNNEST cast the metadata never declared, so the column
|
|
578
|
+
// entry stays the only source.
|
|
579
|
+
const arrayType = col.arrayType ?? col.pgArrayType;
|
|
580
|
+
if (arrayType !== undefined)
|
|
581
|
+
this.columnArrayTypeMap.set(col.name, arrayType);
|
|
481
582
|
if (col.pgTypeSchema !== undefined)
|
|
482
583
|
this.crossSchemaTypeColumns.add(col.name);
|
|
483
584
|
}
|
|
585
|
+
this.warnUntypedColumns();
|
|
484
586
|
// Bind the shared WHERE-walk view once. `tableMeta` is immutable after this
|
|
485
587
|
// point; the method wrappers forward to the (private) instance methods so
|
|
486
588
|
// the walk needs no public accessors on the class.
|
|
@@ -504,6 +606,18 @@ class QueryInterface {
|
|
|
504
606
|
scopedHostCache: this.scopedHostCache,
|
|
505
607
|
columnPgTypeMap: this.columnPgTypeMap,
|
|
506
608
|
columnArrayTypeMap: this.columnArrayTypeMap,
|
|
609
|
+
// The `utcTimestamps: false` opt-out has to reach the write/where web:
|
|
610
|
+
// omitting it here left `qi.utcTimestamps` undefined for the whole
|
|
611
|
+
// module, where `!== false` reads as opted IN, so the write side ignored
|
|
612
|
+
// the flag and rewrote binds the caller had opted out of.
|
|
613
|
+
//
|
|
614
|
+
// This half of the flag is PER CLIENT. The read half is not: it is the
|
|
615
|
+
// pg OID 1114 type parser, which `pg.types.setTypeParser` installs once
|
|
616
|
+
// per process. Two clients in one process therefore cannot hold
|
|
617
|
+
// different values, and TurbineClient refuses the second one rather than
|
|
618
|
+
// building a client whose writes and reads disagree (see
|
|
619
|
+
// `assertUtcTimestampsAgree` in client.ts).
|
|
620
|
+
utcTimestamps: this.utcTimestamps,
|
|
507
621
|
crossSchemaTypeColumns: this.crossSchemaTypeColumns,
|
|
508
622
|
get currentSkip() {
|
|
509
623
|
return self.currentSkip;
|
|
@@ -534,6 +648,73 @@ class QueryInterface {
|
|
|
534
648
|
paginationValue: (value, arg) => this.paginationValue(value, arg),
|
|
535
649
|
};
|
|
536
650
|
}
|
|
651
|
+
/**
|
|
652
|
+
* Dev-only, once per table: the columns whose database type is absent from
|
|
653
|
+
* BOTH the column entry and the table-level type maps, the residual case
|
|
654
|
+
* after the two-source resolution above.
|
|
655
|
+
*
|
|
656
|
+
* The set is deliberately every untyped column, not the `dateColumns`
|
|
657
|
+
* members. An unresolved type is precisely the state in which Turbine cannot
|
|
658
|
+
* say WHICH kind of column it is, so restricting the scan to `dateColumns`
|
|
659
|
+
* got it wrong in both directions: that set carries `timestamptz` (whose
|
|
660
|
+
* bind was never affected, since binding the `Date` is the correct thing to
|
|
661
|
+
* do for it) and omits `time` / `timetz` entirely (deliberately, see
|
|
662
|
+
* `timeOfDayKind` in schema.ts), which is the one kind that fails LOUDLY
|
|
663
|
+
* rather than silently. The message therefore names the columns and states
|
|
664
|
+
* what each kind does, rather than asserting a kind it cannot know.
|
|
665
|
+
*
|
|
666
|
+
* What is actually at stake per kind, all of it `coerceWriteValue` returning
|
|
667
|
+
* the bound `Date` by identity for want of a type:
|
|
668
|
+
* - zone-less `date` / `timestamp`: the driver serializes with the
|
|
669
|
+
* PROCESS's offset, so the column stores local calendar fields. Nothing
|
|
670
|
+
* surfaces at runtime, and a turbine-only round trip reads the same value
|
|
671
|
+
* back (the read path shifts by the same offset), so only an outside
|
|
672
|
+
* reader sees the drift.
|
|
673
|
+
* - `time` / `timetz`: the driver serializes a full ISO timestamp, which
|
|
674
|
+
* Postgres rejects with `22007 invalid input syntax for type time`.
|
|
675
|
+
* - `timestamptz` and every non-temporal type: unaffected.
|
|
676
|
+
*
|
|
677
|
+
* PostgreSQL only: the UTC bind rewrite is Postgres-gated (see
|
|
678
|
+
* `utcDateTimeWrites` in writes.ts), so on the other engines a missing type
|
|
679
|
+
* changes nothing about how a `Date` is bound. Suppressed under
|
|
680
|
+
* `NODE_ENV=production` like the other dev diagnostics and deduped through
|
|
681
|
+
* the shared registry, so a hot table logs one line for the process.
|
|
682
|
+
*
|
|
683
|
+
* Cannot throw on odd metadata: it walks `tableMeta.columns`, the array the
|
|
684
|
+
* constructor loop above has already iterated (and that client.ts validates
|
|
685
|
+
* as an array), never `dateColumns`, which is a `Set` in every first-party
|
|
686
|
+
* metadata path but arrives as a plain object from JSON-round-tripped
|
|
687
|
+
* metadata. A dev-only diagnostic that crashes a shape production would serve
|
|
688
|
+
* is worse than the bug it reports.
|
|
689
|
+
*/
|
|
690
|
+
warnUntypedColumns() {
|
|
691
|
+
if (process.env.NODE_ENV === 'production')
|
|
692
|
+
return;
|
|
693
|
+
if (this.dialect.name !== 'postgresql')
|
|
694
|
+
return;
|
|
695
|
+
const untyped = [];
|
|
696
|
+
for (const col of this.tableMeta.columns) {
|
|
697
|
+
if (this.columnPgTypeMap.get(col.name) === undefined)
|
|
698
|
+
untyped.push(col.name);
|
|
699
|
+
}
|
|
700
|
+
if (untyped.length === 0)
|
|
701
|
+
return;
|
|
702
|
+
if (!(0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.untypedDateColumn, this.table))
|
|
703
|
+
return;
|
|
704
|
+
// Bound the line on a wide table: the fix is per table, not per column, so
|
|
705
|
+
// the first few names are enough to recognize the metadata that produced it.
|
|
706
|
+
const MAX_NAMED = 12;
|
|
707
|
+
const named = untyped.slice(0, MAX_NAMED).join(', ');
|
|
708
|
+
const rest = untyped.length > MAX_NAMED ? ` (+${untyped.length - MAX_NAMED} more)` : '';
|
|
709
|
+
console.warn(`[turbine] table "${this.table}": no database type in metadata for column(s) ${named}${rest} (neither the ` +
|
|
710
|
+
"column entry's `dialectType`/`pgType` nor the table-level `dialectTypes`/`pgTypes` map). Turbine cannot " +
|
|
711
|
+
'tell which of them are zone-less, so a `Date` written to one is bound by the driver as-is: right for ' +
|
|
712
|
+
"`timestamptz`, but a zone-less `date`/`timestamp` column then stores the PROCESS's local calendar fields " +
|
|
713
|
+
'rather than UTC (silently, since the read path shifts back by the same offset), and a `time`/`timetz` ' +
|
|
714
|
+
'column rejects the value outright (`22007 invalid input syntax for type time`). Columns whose type IS ' +
|
|
715
|
+
'resolved, every `timestamptz` among them, are unaffected. Regenerate the metadata with ' +
|
|
716
|
+
'`npx turbine generate`, or set the column types in your `defineSchema` definition.');
|
|
717
|
+
}
|
|
537
718
|
/** Quote an identifier through the active SQL dialect. */
|
|
538
719
|
q(name) {
|
|
539
720
|
return this.dialect.quoteIdentifier(name);
|
|
@@ -978,8 +1159,20 @@ class QueryInterface {
|
|
|
978
1159
|
* `findFirst` pass `false` explicitly (their parent set is one row).
|
|
979
1160
|
*/
|
|
980
1161
|
autoParentSetLarge(args) {
|
|
981
|
-
const
|
|
982
|
-
return
|
|
1162
|
+
const bound = this.autoParentBound(args);
|
|
1163
|
+
return bound === undefined || bound > this.autoToOneThreshold();
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* The plan-time UPPER BOUND on the parent-row count, or `undefined` when the
|
|
1167
|
+
* query is unbounded. This is the raw number behind
|
|
1168
|
+
* {@link autoParentSetLarge}; the `_count` rule needs the number itself
|
|
1169
|
+
* because its threshold ({@link AUTO_COUNT_BATCH_MIN_PARENT_ROWS}) is two
|
|
1170
|
+
* rows rather than the to-one break-even. `findUnique` / `findFirst` pass `1`
|
|
1171
|
+
* directly: their parent set is one row as a matter of the statement's shape,
|
|
1172
|
+
* not an estimate.
|
|
1173
|
+
*/
|
|
1174
|
+
autoParentBound(args) {
|
|
1175
|
+
return args?.take ?? args?.limit ?? this.defaultLimit;
|
|
983
1176
|
}
|
|
984
1177
|
/**
|
|
985
1178
|
* The parent-row count at which `'auto'` stops preferring the single-statement
|
|
@@ -1026,13 +1219,13 @@ class QueryInterface {
|
|
|
1026
1219
|
*
|
|
1027
1220
|
* Everything else (indexed to-many, composite-key, unknown) stays in `joinWith`
|
|
1028
1221
|
* (byte-identical join). The reserved `_count` key falls back on rule 1 only,
|
|
1029
|
-
* and
|
|
1030
|
-
*
|
|
1031
|
-
*
|
|
1032
|
-
*
|
|
1033
|
-
* the dev note.
|
|
1222
|
+
* and on its OWN size rule: an inline `_count` is one correlated `COUNT(*)`
|
|
1223
|
+
* per parent row over an unindexed child table, so the grouped follow-up wins
|
|
1224
|
+
* from {@link AUTO_COUNT_BATCH_MIN_PARENT_ROWS} parent rows upward and inline
|
|
1225
|
+
* is preferred only when the parent set is provably bounded below that. Also
|
|
1226
|
+
* returns the engaged relations for the dev note.
|
|
1034
1227
|
*/
|
|
1035
|
-
partitionWithForAuto(withClause, parentSetLarge) {
|
|
1228
|
+
partitionWithForAuto(withClause, parentSetLarge, parentBound) {
|
|
1036
1229
|
const hasIndexInfo = (0, index_advisor_js_1.schemaHasIndexInfo)(this.schema);
|
|
1037
1230
|
const joinWith = {};
|
|
1038
1231
|
const batchedWith = {};
|
|
@@ -1042,7 +1235,8 @@ class QueryInterface {
|
|
|
1042
1235
|
continue;
|
|
1043
1236
|
if (key === '_count') {
|
|
1044
1237
|
const cv = this.autoCountVerdict(spec, this.tableMeta);
|
|
1045
|
-
|
|
1238
|
+
const countWorthBatching = parentBound === undefined || parentBound >= exports.AUTO_COUNT_BATCH_MIN_PARENT_ROWS;
|
|
1239
|
+
if (hasIndexInfo && countWorthBatching && cv.unindexed && cv.eligible) {
|
|
1046
1240
|
batchedWith[key] = spec;
|
|
1047
1241
|
engaged.push({ relation: '_count', reason: 'unindexed', miss: cv.miss });
|
|
1048
1242
|
}
|
|
@@ -1080,7 +1274,7 @@ class QueryInterface {
|
|
|
1080
1274
|
* to batched. Returns `null` (→ run the plain join path, byte-identical, same
|
|
1081
1275
|
* cache keys) when nothing qualifies.
|
|
1082
1276
|
*/
|
|
1083
|
-
planAuto(withArg, stableFlag, parentSetLarge) {
|
|
1277
|
+
planAuto(withArg, stableFlag, parentSetLarge, parentBound) {
|
|
1084
1278
|
// Without DB-backed index info (code-first / defineSchema-only) no probe can
|
|
1085
1279
|
// be PROVEN unindexed; the to-one cardinality rule does not depend on index
|
|
1086
1280
|
// metadata, so it still applies.
|
|
@@ -1089,7 +1283,7 @@ class QueryInterface {
|
|
|
1089
1283
|
const withClause = this.resolveStableOrder(stableFlag)
|
|
1090
1284
|
? this.applyStableRelationOrder(withArg, this.table)
|
|
1091
1285
|
: withArg;
|
|
1092
|
-
const split = this.partitionWithForAuto(withClause, parentSetLarge);
|
|
1286
|
+
const split = this.partitionWithForAuto(withClause, parentSetLarge, parentBound);
|
|
1093
1287
|
if (Object.keys(split.batchedWith).length === 0)
|
|
1094
1288
|
return null;
|
|
1095
1289
|
return split;
|
|
@@ -1111,7 +1305,15 @@ class QueryInterface {
|
|
|
1111
1305
|
const probe = e.miss
|
|
1112
1306
|
? `probe "${e.miss.table}"(${e.miss.columns.join(', ')}) has no covering index`
|
|
1113
1307
|
: 'a probe in its subtree has no covering index';
|
|
1114
|
-
|
|
1308
|
+
// The `_count` case is the one people read as a needless demotion, because
|
|
1309
|
+
// the follow-up statement is a grouped COUNT and the inline form looks
|
|
1310
|
+
// like it would be one too. It is not: state the shape it replaced.
|
|
1311
|
+
const why = e.relation === '_count'
|
|
1312
|
+
? ' The inline form is one correlated COUNT(*) re-evaluated per parent row, so on an unindexed ' +
|
|
1313
|
+
`probe it is one full scan of "${e.miss?.table ?? 'the child table'}" per parent row; the ` +
|
|
1314
|
+
'follow-up is one grouped scan for the whole page.'
|
|
1315
|
+
: '';
|
|
1316
|
+
console.warn(`[turbine] auto strategy: relation "${e.relation}" on "${this.table}" loads batched (${probe}).${why} ` +
|
|
1115
1317
|
"Create the covering index (or set `relationLoadStrategy: 'join'` to force the single-statement " +
|
|
1116
1318
|
'plan); run `npx turbine doctor` for the exact CREATE INDEX SQL.');
|
|
1117
1319
|
}
|
|
@@ -1527,8 +1729,10 @@ class QueryInterface {
|
|
|
1527
1729
|
return this.runFindUniqueBatched(args);
|
|
1528
1730
|
if (strategy === 'auto') {
|
|
1529
1731
|
// findUnique's parent set is a single row: the join plan's correlated
|
|
1530
|
-
// subqueries run once, so the cardinality rule never applies here
|
|
1531
|
-
|
|
1732
|
+
// subqueries run once, so the cardinality rule never applies here, and
|
|
1733
|
+
// an inline `_count` is one scan against the batched plan's one scan
|
|
1734
|
+
// plus a round trip.
|
|
1735
|
+
const split = this.planAuto(args.with, args.stableRelationOrder, false, 1);
|
|
1532
1736
|
if (split)
|
|
1533
1737
|
return this.runAutoSplit(args, split, true);
|
|
1534
1738
|
}
|
|
@@ -1712,7 +1916,7 @@ class QueryInterface {
|
|
|
1712
1916
|
if (strategy === 'batched')
|
|
1713
1917
|
return this.runFindManyBatched(args);
|
|
1714
1918
|
if (strategy === 'auto') {
|
|
1715
|
-
const split = this.planAuto(args.with, args.stableRelationOrder, this.autoParentSetLarge(args));
|
|
1919
|
+
const split = this.planAuto(args.with, args.stableRelationOrder, this.autoParentSetLarge(args), this.autoParentBound(args));
|
|
1716
1920
|
if (split)
|
|
1717
1921
|
return this.runAutoSplit(args, split, false);
|
|
1718
1922
|
}
|
|
@@ -2221,7 +2425,7 @@ class QueryInterface {
|
|
|
2221
2425
|
}
|
|
2222
2426
|
if (strategy === 'auto') {
|
|
2223
2427
|
// findFirst is findMany + LIMIT 1: a one-row parent set.
|
|
2224
|
-
const split = this.planAuto(args.with, args.stableRelationOrder, false);
|
|
2428
|
+
const split = this.planAuto(args.with, args.stableRelationOrder, false, 1);
|
|
2225
2429
|
if (split) {
|
|
2226
2430
|
const rows = (await this.runAutoSplit({ ...args, limit: 1 }, split, false));
|
|
2227
2431
|
return (rows[0] ?? null);
|
|
Binary file
|
|
@@ -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/cjs/query/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* former monolithic `import { … } from './query.js'`.
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.QueryInterface = exports.AUTO_TO_ONE_JOIN_ROWS_MIN = exports.AUTO_TO_ONE_JOIN_ROWS_MAX = exports.AUTO_TO_ONE_JOIN_MAX_ROWS = exports.AUTO_JOIN_PENALTY_MS_PER_ROW = exports.AUTO_ASSUMED_ROUND_TRIP_MS = exports.sqlToPreparedName = exports.quoteIdent = exports.OPERATOR_KEYS = exports.LRUCache = exports.fnv1a64Hex = exports.escSingleQuote = exports.escapeLike = exports.buildCorrelation = exports.postgresDialect = void 0;
|
|
10
|
+
exports.QueryInterface = exports.AUTO_TO_ONE_JOIN_ROWS_MIN = exports.AUTO_TO_ONE_JOIN_ROWS_MAX = exports.AUTO_TO_ONE_JOIN_MAX_ROWS = exports.AUTO_JOIN_PENALTY_MS_PER_ROW = exports.AUTO_COUNT_BATCH_MIN_PARENT_ROWS = exports.AUTO_ASSUMED_ROUND_TRIP_MS = exports.sqlToPreparedName = exports.quoteIdent = exports.OPERATOR_KEYS = exports.LRUCache = exports.fnv1a64Hex = exports.escSingleQuote = exports.escapeLike = exports.buildCorrelation = exports.postgresDialect = void 0;
|
|
11
11
|
var dialect_js_1 = require("../dialect.js");
|
|
12
12
|
Object.defineProperty(exports, "postgresDialect", { enumerable: true, get: function () { return dialect_js_1.postgresDialect; } });
|
|
13
13
|
var utils_js_1 = require("./utils.js");
|
|
@@ -21,6 +21,7 @@ Object.defineProperty(exports, "quoteIdent", { enumerable: true, get: function (
|
|
|
21
21
|
Object.defineProperty(exports, "sqlToPreparedName", { enumerable: true, get: function () { return utils_js_1.sqlToPreparedName; } });
|
|
22
22
|
var builder_js_1 = require("./builder.js");
|
|
23
23
|
Object.defineProperty(exports, "AUTO_ASSUMED_ROUND_TRIP_MS", { enumerable: true, get: function () { return builder_js_1.AUTO_ASSUMED_ROUND_TRIP_MS; } });
|
|
24
|
+
Object.defineProperty(exports, "AUTO_COUNT_BATCH_MIN_PARENT_ROWS", { enumerable: true, get: function () { return builder_js_1.AUTO_COUNT_BATCH_MIN_PARENT_ROWS; } });
|
|
24
25
|
Object.defineProperty(exports, "AUTO_JOIN_PENALTY_MS_PER_ROW", { enumerable: true, get: function () { return builder_js_1.AUTO_JOIN_PENALTY_MS_PER_ROW; } });
|
|
25
26
|
Object.defineProperty(exports, "AUTO_TO_ONE_JOIN_MAX_ROWS", { enumerable: true, get: function () { return builder_js_1.AUTO_TO_ONE_JOIN_MAX_ROWS; } });
|
|
26
27
|
Object.defineProperty(exports, "AUTO_TO_ONE_JOIN_ROWS_MAX", { enumerable: true, get: function () { return builder_js_1.AUTO_TO_ONE_JOIN_ROWS_MAX; } });
|
|
@@ -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
|
};
|
|
@@ -106,4 +106,14 @@ exports.WARN_NS = {
|
|
|
106
106
|
* engage is indistinguishable from one that does nothing, so say so once.
|
|
107
107
|
*/
|
|
108
108
|
flattenFallback: 'flattenFallback',
|
|
109
|
+
/**
|
|
110
|
+
* Columns whose database type resolves from NEITHER the column entry nor the
|
|
111
|
+
* table-level type maps, so Turbine cannot tell which of them are zone-less
|
|
112
|
+
* and a bound `Date` is left to the driver rather than rewritten to a UTC
|
|
113
|
+
* literal (builder.ts `warnUntypedColumns`). The scan covers every column,
|
|
114
|
+
* not just `dateColumns`, because an unresolved type is precisely the state
|
|
115
|
+
* in which the kind is unknown; the warning emits one line per table listing
|
|
116
|
+
* the offenders, not one per column. The namespace name is historical.
|
|
117
|
+
*/
|
|
118
|
+
untypedDateColumn: 'untypedDateColumn',
|
|
109
119
|
};
|
package/dist/cjs/query/writes.js
CHANGED
|
@@ -134,6 +134,36 @@ function buildReselectByWhere(qi, whereObj) {
|
|
|
134
134
|
const where = clause ? ` WHERE ${clause}` : '';
|
|
135
135
|
return { sql: `SELECT ${writeReselectSelection(qi)} FROM ${qi.q(qi.table)}${where}`, params };
|
|
136
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Build the all-defaults INSERT for a `data` that names no column, via the
|
|
139
|
+
* dialect's `buildDefaultValuesInsertStatement` hook.
|
|
140
|
+
*
|
|
141
|
+
* `INSERT INTO t () VALUES ()` (what the column-driven builders render for an
|
|
142
|
+
* empty `data`) is a syntax error everywhere but MySQL, and the correct
|
|
143
|
+
* statement differs per engine, so the shape lives behind the dialect seam.
|
|
144
|
+
* `{}` is easy to reach honestly, a handler that assembles its payload from
|
|
145
|
+
* optional request fields produces it on a request that supplied none of them,
|
|
146
|
+
* so the row of pure defaults is inserted and returned rather than crashing.
|
|
147
|
+
* The empty-`update` no-op in {@link buildUpdate} has the same rationale.
|
|
148
|
+
*
|
|
149
|
+
* A table with no usable defaults still fails, correctly, with the database's
|
|
150
|
+
* own NOT NULL violation (E010): nothing here pre-empts that.
|
|
151
|
+
*
|
|
152
|
+
* A dialect predating the hook raises E017 rather than emitting SQL its engine
|
|
153
|
+
* will reject.
|
|
154
|
+
*/
|
|
155
|
+
function buildDefaultValuesInsert(qi, rowCount, skipDuplicates) {
|
|
156
|
+
const build = qi.dialect.buildDefaultValuesInsertStatement;
|
|
157
|
+
if (!build) {
|
|
158
|
+
throw new errors_js_1.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`.');
|
|
159
|
+
}
|
|
160
|
+
return build.call(qi.dialect, {
|
|
161
|
+
table: qi.q(qi.table),
|
|
162
|
+
rowCount,
|
|
163
|
+
skipDuplicates,
|
|
164
|
+
returning: writeReturningColumns(qi),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
137
167
|
function buildCreate(qi, args) {
|
|
138
168
|
assertWritable(qi, 'create');
|
|
139
169
|
assertNoGeneratedColumns(qi, args.data, 'create');
|
|
@@ -142,12 +172,15 @@ function buildCreate(qi, args) {
|
|
|
142
172
|
const params = entries.map(([k, v]) => coerceWriteValue(qi, k, v));
|
|
143
173
|
// Enum columns get an explicit `::"EnumName"` cast (see enumTypeForColumn).
|
|
144
174
|
const placeholders = entries.map(([k], i) => `${qi.p(i + 1)}${whereMod.enumCastSuffix(qi, qi.toColumn(k))}`);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
175
|
+
// `data: {}` (or all-undefined) names no column: insert a row of defaults.
|
|
176
|
+
const sql = entries.length === 0
|
|
177
|
+
? buildDefaultValuesInsert(qi, 1)
|
|
178
|
+
: qi.dialect.buildInsertStatement({
|
|
179
|
+
table: qi.q(qi.table),
|
|
180
|
+
columns,
|
|
181
|
+
valuePlaceholders: placeholders,
|
|
182
|
+
returning: writeReturningColumns(qi),
|
|
183
|
+
});
|
|
151
184
|
return {
|
|
152
185
|
sql,
|
|
153
186
|
params,
|
|
@@ -194,6 +227,69 @@ function makeCreateReselect(qi, insertSql, insertParams, data) {
|
|
|
194
227
|
return exec(`SELECT ${writeReselectSelection(qi)} FROM ${qi.q(qi.table)}${where}`, selParams);
|
|
195
228
|
};
|
|
196
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* The fields a write's `data` object actually names.
|
|
232
|
+
*
|
|
233
|
+
* A key whose value is `undefined` is NOT named: single-row {@link buildCreate}
|
|
234
|
+
* filters those out of its column list, so the column takes its declared
|
|
235
|
+
* default. `createMany` reads its rows through this same helper, so
|
|
236
|
+
* `{ n: undefined }` and `{}` mean the identical thing on both paths.
|
|
237
|
+
*/
|
|
238
|
+
function definedKeys(row) {
|
|
239
|
+
return Object.keys(row).filter((k) => row[k] !== undefined);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Refuse a `createMany` whose rows do not all name the SAME fields.
|
|
243
|
+
*
|
|
244
|
+
* `createMany` compiles ONE statement, and its column list is taken from the
|
|
245
|
+
* first row alone, so a row that disagrees with the first row loses data in
|
|
246
|
+
* both directions and silently:
|
|
247
|
+
*
|
|
248
|
+
* - a field the first row names and a later row OMITS is bound as NULL,
|
|
249
|
+
* overwriting that column's declared default (and failing outright on a NOT
|
|
250
|
+
* NULL column that has one);
|
|
251
|
+
* - a field only a LATER row names is not in the column list at all, so its
|
|
252
|
+
* value never reaches the database.
|
|
253
|
+
*
|
|
254
|
+
* Neither shape is expressible as a single statement on every engine. The
|
|
255
|
+
* row-major `VALUES` form can carry a per-cell `DEFAULT` keyword on
|
|
256
|
+
* PostgreSQL, MySQL and SQL Server, but SQLite has no such grammar (`VALUES
|
|
257
|
+
* (1, DEFAULT)` is a parse error there), and adopting it would also drop
|
|
258
|
+
* PostgreSQL off the column-major `UNNEST` form that binds one array per
|
|
259
|
+
* column rather than one placeholder per cell. So the honest answer is a
|
|
260
|
+
* refusal that names the offending row and its differing columns.
|
|
261
|
+
*
|
|
262
|
+
* A single row is trivially uniform, and rows that all name the same fields
|
|
263
|
+
* (the overwhelmingly common shape) cost one `Object.keys` pass and emit
|
|
264
|
+
* byte-identical SQL.
|
|
265
|
+
*/
|
|
266
|
+
function assertUniformCreateManyRows(qi, rows, firstKeys) {
|
|
267
|
+
const expected = new Set(firstKeys);
|
|
268
|
+
for (let i = 1; i < rows.length; i++) {
|
|
269
|
+
const rowKeys = definedKeys(rows[i]);
|
|
270
|
+
const unexpected = rowKeys.filter((k) => !expected.has(k));
|
|
271
|
+
// No stranger and the same count means the same set (object keys are unique).
|
|
272
|
+
if (unexpected.length === 0 && rowKeys.length === expected.size)
|
|
273
|
+
continue;
|
|
274
|
+
const present = new Set(rowKeys);
|
|
275
|
+
const missing = firstKeys.filter((k) => !present.has(k));
|
|
276
|
+
const parts = [];
|
|
277
|
+
if (missing.length > 0)
|
|
278
|
+
parts.push(`does not supply ${quoteList(missing)}`);
|
|
279
|
+
if (unexpected.length > 0)
|
|
280
|
+
parts.push(`supplies ${quoteList(unexpected)}, which the first row does not`);
|
|
281
|
+
throw new errors_js_1.ValidationError(`[turbine] createMany on "${qi.table}": row ${i} ${parts.join(' and ')}. ` +
|
|
282
|
+
'Every row must supply the same fields: createMany builds ONE statement whose column list comes from the ' +
|
|
283
|
+
"first row, so a field a later row omits would be written as NULL over that column's default, and a field " +
|
|
284
|
+
'only a later row names would be dropped. Supply the field explicitly on every row (a field set to ' +
|
|
285
|
+
'`undefined` counts as omitted, exactly as it does in `create`), or split the call into one createMany per ' +
|
|
286
|
+
'row shape.');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/** `a, b` → `"a", "b"`, for the field lists in {@link assertUniformCreateManyRows}. */
|
|
290
|
+
function quoteList(fields) {
|
|
291
|
+
return fields.map((f) => `"${f}"`).join(', ');
|
|
292
|
+
}
|
|
197
293
|
function buildCreateMany(qi, args) {
|
|
198
294
|
const qt = qi.q(qi.table);
|
|
199
295
|
if (args.data.length === 0) {
|
|
@@ -208,7 +304,19 @@ function buildCreateMany(qi, args) {
|
|
|
208
304
|
for (const row of args.data) {
|
|
209
305
|
assertNoGeneratedColumns(qi, row, 'createMany');
|
|
210
306
|
}
|
|
211
|
-
const keys =
|
|
307
|
+
const keys = definedKeys(args.data[0]);
|
|
308
|
+
assertUniformCreateManyRows(qi, args.data, keys);
|
|
309
|
+
// No column named by the first row: every row is pure defaults (the bulk
|
|
310
|
+
// counterpart of `create({ data: {} })`, see buildDefaultValuesInsert). The
|
|
311
|
+
// uniformity check above has already established that EVERY row is empty.
|
|
312
|
+
if (keys.length === 0) {
|
|
313
|
+
return {
|
|
314
|
+
sql: buildDefaultValuesInsert(qi, args.data.length, args.skipDuplicates),
|
|
315
|
+
params: [],
|
|
316
|
+
transform: (result) => result.rows.map((row) => parseWriteRow(qi, row)),
|
|
317
|
+
tag: `${qi.table}.createMany`,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
212
320
|
const columns = keys.map((k) => qi.toColumn(k));
|
|
213
321
|
const rowValues = args.data.map((row) => {
|
|
214
322
|
const record = row;
|
package/dist/cjs/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/cjs/sqlite.js
CHANGED
|
@@ -490,6 +490,17 @@ exports.sqliteDialect = {
|
|
|
490
490
|
params: input.rowValues.flat(),
|
|
491
491
|
};
|
|
492
492
|
},
|
|
493
|
+
buildDefaultValuesInsertStatement(input) {
|
|
494
|
+
// SQLite's `DEFAULT VALUES` inserts exactly one row and the engine accepts
|
|
495
|
+
// neither `VALUES (DEFAULT, …)` nor MySQL's empty `VALUES ()` tuple, so
|
|
496
|
+
// there is no multi-row all-defaults statement to emit.
|
|
497
|
+
if (input.rowCount !== 1) {
|
|
498
|
+
throw new errors_js_1.UnsupportedFeatureError(`createMany with ${input.rowCount} empty data rows`, 'sqlite', 'SQLite INSERT … DEFAULT VALUES inserts a single row and has no multi-row form; ' +
|
|
499
|
+
'issue one create({ data: {} }) per row.');
|
|
500
|
+
}
|
|
501
|
+
const conflict = input.skipDuplicates ? ' ON CONFLICT DO NOTHING' : '';
|
|
502
|
+
return `INSERT INTO ${input.table} DEFAULT VALUES${conflict}${this.buildReturningClause(input.returning)}`;
|
|
503
|
+
},
|
|
493
504
|
buildUpsertStatement(input) {
|
|
494
505
|
return (`INSERT INTO ${input.table} (${input.insertColumns.join(', ')}) VALUES (${input.valuePlaceholders.join(', ')})` +
|
|
495
506
|
` ON CONFLICT (${input.conflictColumns.join(', ')}) DO UPDATE SET ${input.updateSetClauses.join(', ')}` +
|
|
@@ -786,7 +797,8 @@ function openSqliteDatabase(target, options) {
|
|
|
786
797
|
*
|
|
787
798
|
* @param target A SQLite file path, `':memory:'`, or an open `DatabaseSync`.
|
|
788
799
|
* @param schema Introspected or hand-written {@link SchemaMetadata}.
|
|
789
|
-
* @param options Optional pragmas
|
|
800
|
+
* @param options Optional SQLite pragmas plus any client-level
|
|
801
|
+
* {@link TurbineConfig} field (see {@link EngineClientConfig}).
|
|
790
802
|
*
|
|
791
803
|
* @example
|
|
792
804
|
* ```ts
|
|
@@ -797,12 +809,14 @@ function openSqliteDatabase(target, options) {
|
|
|
797
809
|
function turbineSqlite(target, schema, options = {}) {
|
|
798
810
|
const db = typeof target === 'string' ? openSqliteDatabase(target, options) : target;
|
|
799
811
|
const pool = new SqlitePool(db);
|
|
812
|
+
// Everything that is not a SQLite pragma is client config and rides through
|
|
813
|
+
// untouched, so a new TurbineConfig option works here the day it lands. The
|
|
814
|
+
// engine-owned keys come last, so no caller can unbind the dialect.
|
|
815
|
+
const { wal: _wal, busyTimeoutMs: _busyTimeoutMs, foreignKeys: _foreignKeys, ...clientConfig } = options;
|
|
800
816
|
return new client_js_1.TurbineClient({
|
|
817
|
+
...clientConfig,
|
|
801
818
|
pool,
|
|
802
819
|
dialect: exports.sqliteDialect,
|
|
803
820
|
preparedStatements: false,
|
|
804
|
-
logging: options.logging,
|
|
805
|
-
defaultLimit: options.defaultLimit,
|
|
806
|
-
warnOnUnlimited: options.warnOnUnlimited,
|
|
807
821
|
}, schema);
|
|
808
822
|
}
|