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
|
@@ -26,7 +26,10 @@
|
|
|
26
26
|
* , the un-awaited delegate calls defer to Turbine's `build*()` methods and
|
|
27
27
|
* run atomically through the core batch `$transaction([...])` path.
|
|
28
28
|
* - **Raw SQL**: `$queryRaw` / `$executeRaw` tagged templates (with
|
|
29
|
-
* `Prisma.sql`-style nested-fragment flattening) and the `*Unsafe` variants
|
|
29
|
+
* `Prisma.sql`-style nested-fragment flattening) and the `*Unsafe` variants,
|
|
30
|
+
* on the client AND on the transaction client, where they run on the
|
|
31
|
+
* transaction's own connection so a mixed raw + delegate `$transaction` stays
|
|
32
|
+
* atomic.
|
|
30
33
|
* - **Result reshaping**: `_count` objects keyed back to Prisma relation names,
|
|
31
34
|
* and to-one relations surfaced as `object | null`.
|
|
32
35
|
*
|
|
@@ -86,11 +89,27 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
86
89
|
exports.CLIENT_RESERVED_KEYS = exports.COMPAT_DEFERRED = exports.Prisma = void 0;
|
|
87
90
|
exports.createPrismaCompatClient = createPrismaCompatClient;
|
|
88
91
|
const errors_js_1 = require("./errors.js");
|
|
92
|
+
const nested_write_js_1 = require("./nested-write.js");
|
|
89
93
|
const warn_registry_js_1 = require("./query/warn-registry.js");
|
|
90
94
|
// ---------------------------------------------------------------------------
|
|
91
95
|
// Prisma.sql-style raw fragments (local, minimal, never imports @prisma/client)
|
|
92
96
|
// ---------------------------------------------------------------------------
|
|
93
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Brand marking an object as a raw-SQL fragment whose `strings` are spliced
|
|
99
|
+
* VERBATIM into the emitted statement (see `flattenTemplate`). Deliberately a
|
|
100
|
+
* module-private `Symbol()` and NOT `Symbol.for(...)`: a registry symbol is
|
|
101
|
+
* reachable by name from anywhere in the process, so any dependency could mint
|
|
102
|
+
* an object that flattens as trusted SQL. With a private symbol the only way to
|
|
103
|
+
* obtain a fragment is to call `Prisma.sql` / `Prisma.join` / `Prisma.raw` from
|
|
104
|
+
* this module.
|
|
105
|
+
*
|
|
106
|
+
* The fragment check is fail-CLOSED: an object that does not carry this exact
|
|
107
|
+
* symbol is bound as a `$N` parameter, never spliced. That is also what makes
|
|
108
|
+
* the (contrived) dual-package case safe rather than dangerous, a fragment
|
|
109
|
+
* built by the ESM copy of this module and executed by the CJS copy binds as a
|
|
110
|
+
* parameter instead of composing.
|
|
111
|
+
*/
|
|
112
|
+
const SQL_FRAGMENT = Symbol('turbine.prismaCompat.sqlFragment');
|
|
94
113
|
function isSqlFragment(x) {
|
|
95
114
|
return typeof x === 'object' && x !== null && x[SQL_FRAGMENT] === true;
|
|
96
115
|
}
|
|
@@ -1069,6 +1088,40 @@ async function upsertLookupFirst(qi, t) {
|
|
|
1069
1088
|
return qi.update({ where: t.where, data: t.update });
|
|
1070
1089
|
return qi.create({ data: t.create });
|
|
1071
1090
|
}
|
|
1091
|
+
/**
|
|
1092
|
+
* The row shapes a translated `createMany` has to insert, as contiguous runs
|
|
1093
|
+
* that each name the same fields (see {@link createManyShapeRuns}).
|
|
1094
|
+
*
|
|
1095
|
+
* Prisma accepts rows of DIFFERENT shapes in one `createMany` and emits a single
|
|
1096
|
+
* INSERT over the UNION of the named columns, binding its own schema-level
|
|
1097
|
+
* `@default` for a field a row omits and `null` for one it has no default for
|
|
1098
|
+
* (verified against @prisma/client 7.9.0 on PostgreSQL 16). Core `createMany`
|
|
1099
|
+
* refuses a mixed batch instead, because it has no per-cell DEFAULT form on
|
|
1100
|
+
* every engine and would otherwise write NULL over the column's default. Runs
|
|
1101
|
+
* bridge the two: one core `createMany` per run, all of them inside one
|
|
1102
|
+
* transaction, so a ported call site keeps working and the `{ count }` it gets
|
|
1103
|
+
* back is the number of rows actually inserted.
|
|
1104
|
+
*
|
|
1105
|
+
* The one place this is not byte-for-byte Prisma is a column whose DEFAULT lives
|
|
1106
|
+
* in the DATABASE and not in the Prisma schema: Prisma binds null there and
|
|
1107
|
+
* loses the default, a run lets the column default apply. Prisma's own answer is
|
|
1108
|
+
* the lossy one, so the divergence only ever adds back a value the caller never
|
|
1109
|
+
* asked to overwrite.
|
|
1110
|
+
*/
|
|
1111
|
+
function createManyRunsOf(t) {
|
|
1112
|
+
return (0, nested_write_js_1.createManyShapeRuns)((Array.isArray(t.data) ? t.data : []));
|
|
1113
|
+
}
|
|
1114
|
+
/** One core `createMany` per run, in order, summing Prisma's `{ count }`. */
|
|
1115
|
+
async function createManyByRun(qi, t, runs) {
|
|
1116
|
+
let count = 0;
|
|
1117
|
+
for (const run of runs) {
|
|
1118
|
+
const args = { data: run };
|
|
1119
|
+
if (t.skipDuplicates)
|
|
1120
|
+
args.skipDuplicates = true;
|
|
1121
|
+
count += (await qi.createMany(args)).length;
|
|
1122
|
+
}
|
|
1123
|
+
return { count };
|
|
1124
|
+
}
|
|
1072
1125
|
function makeDelegate(ctx, mm, getQI, runInTx) {
|
|
1073
1126
|
const pe = ctx.options.prismaErrorCodes;
|
|
1074
1127
|
// Build a lazy Prisma-style promise for one delegate call. Crucially, the
|
|
@@ -1144,7 +1197,23 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
|
|
|
1144
1197
|
if (args.skipDuplicates)
|
|
1145
1198
|
t.skipDuplicates = true;
|
|
1146
1199
|
return t;
|
|
1147
|
-
}, (qi, t) =>
|
|
1200
|
+
}, (qi, t) => {
|
|
1201
|
+
// Rows that all name the same fields are one statement, exactly as
|
|
1202
|
+
// before. A mixed batch, which Prisma accepts, becomes one createMany
|
|
1203
|
+
// per contiguous same-shape run inside ONE transaction, so the call
|
|
1204
|
+
// stays all-or-nothing and the count is the total actually inserted.
|
|
1205
|
+
const runs = createManyRunsOf(t);
|
|
1206
|
+
if (runs.length <= 1)
|
|
1207
|
+
return qi.createMany(t).then((r) => ({ count: r.length }));
|
|
1208
|
+
return runInTx((table) => createManyByRun(table(mm.table), t, runs));
|
|
1209
|
+
}, {
|
|
1210
|
+
build: (qi, t) => qi.buildCreateMany(t),
|
|
1211
|
+
reshape: (raw) => ({ count: raw.length }),
|
|
1212
|
+
// A mixed batch is more than one statement, so the array
|
|
1213
|
+
// $transaction([...]) form runs the whole array sequentially in a tx.
|
|
1214
|
+
nested: (t) => createManyRunsOf(t).length > 1,
|
|
1215
|
+
execInTx: (table, t) => createManyByRun(table(mm.table), t, createManyRunsOf(t)),
|
|
1216
|
+
}),
|
|
1148
1217
|
update: (args) => defer(() => {
|
|
1149
1218
|
const a = requireWhere(args, 'update');
|
|
1150
1219
|
return {
|
|
@@ -1259,6 +1328,31 @@ function flattenTemplate(strings, values, ph) {
|
|
|
1259
1328
|
append(strings, values);
|
|
1260
1329
|
return { text, params };
|
|
1261
1330
|
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Build the four Prisma raw methods over ONE executor. Both the client-level and
|
|
1333
|
+
* the transaction-scoped surfaces come from this function, so they cannot drift:
|
|
1334
|
+
* same fragment flattening, same placeholder generation, same return shapes
|
|
1335
|
+
* (`$queryRaw` → rows, `$executeRaw` → affected-row count). The only difference
|
|
1336
|
+
* is which connection the executor runs on.
|
|
1337
|
+
*/
|
|
1338
|
+
function makeRawSurface(exec, ph) {
|
|
1339
|
+
return {
|
|
1340
|
+
$queryRaw: async (strings, ...values) => {
|
|
1341
|
+
const { text, params } = flattenTemplate(strings, values, ph);
|
|
1342
|
+
return (await exec(text, params)).rows;
|
|
1343
|
+
},
|
|
1344
|
+
$queryRawUnsafe: async (sql, ...params) => {
|
|
1345
|
+
return (await exec(sql, params)).rows;
|
|
1346
|
+
},
|
|
1347
|
+
$executeRaw: async (strings, ...values) => {
|
|
1348
|
+
const { text, params } = flattenTemplate(strings, values, ph);
|
|
1349
|
+
return (await exec(text, params)).rowCount ?? 0;
|
|
1350
|
+
},
|
|
1351
|
+
$executeRawUnsafe: async (sql, ...params) => {
|
|
1352
|
+
return (await exec(sql, params)).rowCount ?? 0;
|
|
1353
|
+
},
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1262
1356
|
// ---------------------------------------------------------------------------
|
|
1263
1357
|
// createPrismaCompatClient
|
|
1264
1358
|
// ---------------------------------------------------------------------------
|
|
@@ -1314,6 +1408,11 @@ function junctionModels(ctx, map, tableToModel) {
|
|
|
1314
1408
|
if (alias)
|
|
1315
1409
|
seen.add(alias);
|
|
1316
1410
|
}
|
|
1411
|
+
// Junction names already registered by an earlier relation. A many-to-many
|
|
1412
|
+
// pair is normally DECLARED ON BOTH SIDES, so the same junction table is
|
|
1413
|
+
// reached twice; the second visit must be a silent no-op, not a collision (it
|
|
1414
|
+
// would be colliding with its own registration).
|
|
1415
|
+
const registered = new Set();
|
|
1317
1416
|
for (const table of Object.values(ctx.schema.tables)) {
|
|
1318
1417
|
for (const rel of Object.values(table.relations ?? {})) {
|
|
1319
1418
|
if (rel.type !== 'manyToMany')
|
|
@@ -1321,15 +1420,18 @@ function junctionModels(ctx, map, tableToModel) {
|
|
|
1321
1420
|
const name = rel.through?.table;
|
|
1322
1421
|
if (!name || !ctx.schema.tables[name] || tableToModel.has(name))
|
|
1323
1422
|
continue;
|
|
1423
|
+
if (registered.has(name))
|
|
1424
|
+
continue;
|
|
1324
1425
|
if (seen.has(name)) {
|
|
1325
1426
|
if (process.env.NODE_ENV !== 'production' && (0, warn_registry_js_1.shouldWarnOnce)(JUNCTION_WARN_NS, name)) {
|
|
1326
1427
|
console.warn(`[turbine] prisma-compat: the many-to-many junction table "${name}" collides with an ` +
|
|
1327
1428
|
'existing client member of the same name, so no junction accessor was created for it. ' +
|
|
1328
|
-
'Reach its rows through
|
|
1429
|
+
'Reach its rows through the owning model relation (a nested connect / disconnect / set on the ' +
|
|
1430
|
+
'related model), or through $queryRaw / $executeRaw.');
|
|
1329
1431
|
}
|
|
1330
1432
|
continue;
|
|
1331
1433
|
}
|
|
1332
|
-
|
|
1434
|
+
registered.add(name);
|
|
1333
1435
|
out.push([name, { table: name, accessor: name, fields: {}, relations: {}, compoundUniques: {} }]);
|
|
1334
1436
|
}
|
|
1335
1437
|
}
|
|
@@ -1392,6 +1494,27 @@ function createPrismaCompatClient(client, map, options = {}) {
|
|
|
1392
1494
|
throw decorate((0, errors_js_1.wrapPgError)(err), ctx.options.prismaErrorCodes);
|
|
1393
1495
|
}
|
|
1394
1496
|
};
|
|
1497
|
+
/**
|
|
1498
|
+
* The same executor bound to a transaction's OWN connection. A raw statement
|
|
1499
|
+
* that quietly ran on a pool connection would leave the caller unable to tell
|
|
1500
|
+
* that its writes were outside the transaction, so a transaction client that
|
|
1501
|
+
* cannot execute raw SQL refuses rather than falling back to the pool.
|
|
1502
|
+
* `wrapPgError` is idempotent (it returns an already-typed TurbineError
|
|
1503
|
+
* untouched), so the error shape matches the pool path exactly.
|
|
1504
|
+
*/
|
|
1505
|
+
const txRunRaw = (tx) => async (text, params) => {
|
|
1506
|
+
if (typeof tx.rawQuery !== 'function') {
|
|
1507
|
+
throw decorate(new errors_js_1.ValidationError('[turbine] prisma-compat: raw SQL inside $transaction needs a transaction client that can execute it ' +
|
|
1508
|
+
'(core TransactionClient.rawQuery). Refusing to run the statement on a pool connection, which would ' +
|
|
1509
|
+
'silently place it outside the transaction.'), ctx.options.prismaErrorCodes);
|
|
1510
|
+
}
|
|
1511
|
+
try {
|
|
1512
|
+
return await tx.rawQuery(text, params);
|
|
1513
|
+
}
|
|
1514
|
+
catch (err) {
|
|
1515
|
+
throw decorate((0, errors_js_1.wrapPgError)(err), ctx.options.prismaErrorCodes);
|
|
1516
|
+
}
|
|
1517
|
+
};
|
|
1395
1518
|
const base = {
|
|
1396
1519
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1397
1520
|
$transaction: ((arg, txOptions) => {
|
|
@@ -1440,23 +1563,16 @@ function createPrismaCompatClient(client, map, options = {}) {
|
|
|
1440
1563
|
txDelegates[alias] = txDelegates[prismaModel];
|
|
1441
1564
|
}
|
|
1442
1565
|
}
|
|
1443
|
-
|
|
1566
|
+
// Raw SQL on the transaction's own connection. Prisma's tx client
|
|
1567
|
+
// carries these four, and code that mixes `$transaction` with raw SQL is
|
|
1568
|
+
// the common case in a migrated codebase. No model can shadow them: a
|
|
1569
|
+
// Prisma model name cannot start with `$`, and junction accessors skip
|
|
1570
|
+
// every CLIENT_RESERVED_KEYS name.
|
|
1571
|
+
const txClient = { ...txDelegates, ...makeRawSurface(txRunRaw(tx), ph) };
|
|
1572
|
+
return fn(txClient);
|
|
1444
1573
|
}, txOptions);
|
|
1445
1574
|
}),
|
|
1446
|
-
|
|
1447
|
-
const { text, params } = flattenTemplate(strings, values, ph);
|
|
1448
|
-
return (await runRaw(text, params)).rows;
|
|
1449
|
-
},
|
|
1450
|
-
$queryRawUnsafe: async (sql, ...params) => {
|
|
1451
|
-
return (await runRaw(sql, params)).rows;
|
|
1452
|
-
},
|
|
1453
|
-
$executeRaw: async (strings, ...values) => {
|
|
1454
|
-
const { text, params } = flattenTemplate(strings, values, ph);
|
|
1455
|
-
return (await runRaw(text, params)).rowCount ?? 0;
|
|
1456
|
-
},
|
|
1457
|
-
$executeRawUnsafe: async (sql, ...params) => {
|
|
1458
|
-
return (await runRaw(sql, params)).rowCount ?? 0;
|
|
1459
|
-
},
|
|
1575
|
+
...makeRawSurface(runRaw, ph),
|
|
1460
1576
|
$connect: async () => { },
|
|
1461
1577
|
$disconnect: async () => { },
|
|
1462
1578
|
};
|
|
@@ -36,6 +36,13 @@
|
|
|
36
36
|
* (`[]` when empty), single-or-null for hasOne/belongsTo, with the same
|
|
37
37
|
* camelCase keys and Date coercion, because the child rows are parsed by the
|
|
38
38
|
* very same `parseRow`/`buildFindMany` machinery via a child QueryInterface.
|
|
39
|
+
* - **Identical KEY ORDER.** Object key order is observable output: callers
|
|
40
|
+
* `JSON.stringify` results into HTTP bodies, ETags and cache keys. The
|
|
41
|
+
* follow-up queries run concurrently, so the completion order of two sibling
|
|
42
|
+
* relations is a race; every relation key (and every `_count` entry) is
|
|
43
|
+
* therefore SEEDED up front in the order the join plan emits it, and the
|
|
44
|
+
* concurrent loads only overwrite already-existing keys. See
|
|
45
|
+
* {@link seedRelationKeys}.
|
|
39
46
|
* - **Stitch keys never leak.** To stitch, the follow-up query must select the
|
|
40
47
|
* FK/PK it joins on even when the caller's `select`/`omit` excluded it; the
|
|
41
48
|
* loader adds those columns for the query and strips them from the returned
|
|
@@ -37,6 +37,13 @@
|
|
|
37
37
|
* (`[]` when empty), single-or-null for hasOne/belongsTo, with the same
|
|
38
38
|
* camelCase keys and Date coercion, because the child rows are parsed by the
|
|
39
39
|
* very same `parseRow`/`buildFindMany` machinery via a child QueryInterface.
|
|
40
|
+
* - **Identical KEY ORDER.** Object key order is observable output: callers
|
|
41
|
+
* `JSON.stringify` results into HTTP bodies, ETags and cache keys. The
|
|
42
|
+
* follow-up queries run concurrently, so the completion order of two sibling
|
|
43
|
+
* relations is a race; every relation key (and every `_count` entry) is
|
|
44
|
+
* therefore SEEDED up front in the order the join plan emits it, and the
|
|
45
|
+
* concurrent loads only overwrite already-existing keys. See
|
|
46
|
+
* {@link seedRelationKeys}.
|
|
40
47
|
* - **Stitch keys never leak.** To stitch, the follow-up query must select the
|
|
41
48
|
* FK/PK it joins on even when the caller's `select`/`omit` excluded it; the
|
|
42
49
|
* loader adds those columns for the query and strips them from the returned
|
|
@@ -277,30 +284,96 @@ async function loadRelationsBatched(ctx, parents, withClause, timeout, depth = 0
|
|
|
277
284
|
rejectNestedPickOrder(withClause);
|
|
278
285
|
if (parents.length === 0)
|
|
279
286
|
return;
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
const
|
|
284
|
-
for (const [relName, spec] of
|
|
285
|
-
if (!spec)
|
|
286
|
-
continue;
|
|
287
|
-
// Reserved `_count` key, one grouped COUNT(*) follow-up per counted relation.
|
|
288
|
-
if (relName === '_count') {
|
|
289
|
-
loads.push(loadCounts(ctx, parents, spec));
|
|
287
|
+
// Resolve the relations to load in the SAME order the join plan emits their
|
|
288
|
+
// columns (`sortedEntries` in buildSelectWithRelations), with the reserved
|
|
289
|
+
// `_count` key last.
|
|
290
|
+
const resolved = [];
|
|
291
|
+
for (const [relName, spec] of (0, filters_js_1.sortedEntries)(withClause)) {
|
|
292
|
+
if (!spec || relName === '_count')
|
|
290
293
|
continue;
|
|
291
|
-
}
|
|
292
294
|
const rel = (0, utils_js_1.ownLookup)(ctx.parentMeta.relations, relName);
|
|
293
295
|
if (!rel) {
|
|
294
296
|
throw new errors_js_1.ValidationError(`[turbine] Unknown relation "${relName}" on table "${ctx.parentMeta.name}". ` +
|
|
295
297
|
`Available: ${Object.keys(ctx.parentMeta.relations).join(', ')}`);
|
|
296
298
|
}
|
|
297
|
-
|
|
299
|
+
resolved.push({ relName, rel, options: spec === true ? {} : spec });
|
|
300
|
+
}
|
|
301
|
+
// A falsy `_count` opts out, exactly like a falsy relation spec.
|
|
302
|
+
const countSpec = withClause._count;
|
|
303
|
+
const hasCount = Boolean(countSpec);
|
|
304
|
+
// Fix key order BEFORE anything is awaited: the loads below all write their
|
|
305
|
+
// key on completion, and completion order is a race between concurrent
|
|
306
|
+
// statements.
|
|
307
|
+
seedRelationKeys(parents, ctx.parentMeta, resolved, hasCount);
|
|
308
|
+
// Sibling relations are independent (each writes only its own parent[relName]
|
|
309
|
+
// and reads only parent keys), so load them concurrently, on a pool that's
|
|
310
|
+
// real parallelism, inside a transaction pg queues them on the one connection.
|
|
311
|
+
const loads = [];
|
|
312
|
+
for (const { relName, rel, options } of resolved) {
|
|
298
313
|
loads.push(rel.type === 'manyToMany'
|
|
299
314
|
? loadManyToMany(ctx, parents, rel, relName, options, timeout, depth, path)
|
|
300
315
|
: loadToOneOrMany(ctx, parents, rel, relName, options, timeout, depth, path));
|
|
301
316
|
}
|
|
317
|
+
// Reserved `_count` key, one grouped COUNT(*) follow-up per counted relation.
|
|
318
|
+
if (hasCount)
|
|
319
|
+
loads.push(loadCounts(ctx, parents, countSpec));
|
|
302
320
|
await Promise.all(loads);
|
|
303
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Give every relation key its final POSITION on each parent row before the
|
|
324
|
+
* concurrent follow-up queries start, so the stitched object serializes to the
|
|
325
|
+
* same bytes on every run and matches the join strategy.
|
|
326
|
+
*
|
|
327
|
+
* The reference order is the join plan's SELECT list: base columns, then the
|
|
328
|
+
* relation columns in sorted `with` order, then the `_count__<rel>` scalars
|
|
329
|
+
* (which `parseNestedRow` folds into a `_count` object appended last). A key
|
|
330
|
+
* that is already present is re-inserted rather than seeded: under the `'auto'`
|
|
331
|
+
* split some relations arrive resolved by the join plan and the rest are loaded
|
|
332
|
+
* here, and only a re-insert can interleave the two sets into one canonical
|
|
333
|
+
* order. Base (non-relation) columns are never touched.
|
|
334
|
+
*/
|
|
335
|
+
function seedRelationKeys(parents, parentMeta, resolved, hasCount) {
|
|
336
|
+
// Placeholder per relation: the value an empty load produces, so a seeded key
|
|
337
|
+
// is never a shape the caller could not otherwise see.
|
|
338
|
+
const seeds = new Map();
|
|
339
|
+
for (const { relName, rel } of resolved) {
|
|
340
|
+
seeds.set(relName, rel.type === 'belongsTo' || rel.type === 'hasOne' ? 'one' : 'many');
|
|
341
|
+
}
|
|
342
|
+
// Relations the join plan already resolved onto these rows (the `'auto'`
|
|
343
|
+
// split's residual join). Rows all come from one query, so one row's shape
|
|
344
|
+
// answers for the batch.
|
|
345
|
+
const sample = parents[0];
|
|
346
|
+
if (sample) {
|
|
347
|
+
for (const relName of Object.keys(parentMeta.relations)) {
|
|
348
|
+
// Already resolved by the join plan: it needs a position, never a
|
|
349
|
+
// placeholder, so it is re-inserted rather than overwritten.
|
|
350
|
+
if (!seeds.has(relName) && Object.hasOwn(sample, relName))
|
|
351
|
+
seeds.set(relName, 'present');
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const ordered = [...seeds.keys()].sort();
|
|
355
|
+
const countPresent = hasCount || (sample !== undefined && Object.hasOwn(sample, '_count'));
|
|
356
|
+
if (ordered.length === 0 && !countPresent)
|
|
357
|
+
return;
|
|
358
|
+
for (const parent of parents) {
|
|
359
|
+
for (const relName of ordered) {
|
|
360
|
+
if (Object.hasOwn(parent, relName)) {
|
|
361
|
+
// Re-insert so this key sits in canonical order, value untouched.
|
|
362
|
+
const existing = parent[relName];
|
|
363
|
+
delete parent[relName];
|
|
364
|
+
parent[relName] = existing;
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
parent[relName] = seeds.get(relName) === 'many' ? [] : null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (!countPresent)
|
|
371
|
+
continue;
|
|
372
|
+
const existingCount = Object.hasOwn(parent, '_count') ? parent._count : undefined;
|
|
373
|
+
delete parent._count;
|
|
374
|
+
parent._count = existingCount ?? {};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
304
377
|
/**
|
|
305
378
|
* hasMany / hasOne / belongsTo: one follow-up `SELECT ... WHERE childKey = ANY($1)`
|
|
306
379
|
* (chunked), grouped by the correlation key and attached (array vs single-or-null).
|
|
@@ -483,15 +556,24 @@ async function loadManyToMany(ctx, parents, rel, relName, options, timeout, dept
|
|
|
483
556
|
* Load correlated `_count` values for the counted relations. One grouped
|
|
484
557
|
* follow-up per relation (`SELECT key, COUNT(*) … WHERE key = ANY($1) GROUP BY
|
|
485
558
|
* key`), attached onto each parent's `_count` object (0 when a parent has no
|
|
486
|
-
* matching rows), byte-identical to the join strategy's `_count` output
|
|
559
|
+
* matching rows), byte-identical to the join strategy's `_count` output,
|
|
560
|
+
* INCLUDING key order: the join plan emits its `_count__<rel>` columns in
|
|
561
|
+
* `resolveCountRelations` order, so every key is seeded here in that same order
|
|
562
|
+
* before the concurrent counts run and can only be overwritten in place.
|
|
487
563
|
*/
|
|
488
564
|
async function loadCounts(ctx, parents, countSpec) {
|
|
489
565
|
const rels = resolveCountRelations(ctx.parentMeta, countSpec);
|
|
490
|
-
// Initialise every parent's `_count` up-front
|
|
491
|
-
//
|
|
566
|
+
// Initialise every parent's `_count` up-front, and seed every counted
|
|
567
|
+
// relation's key in `resolveCountRelations` order, so the concurrent
|
|
568
|
+
// per-relation loads below only ever overwrite a key that already exists.
|
|
569
|
+
// Without the seeded keys, insertion order is whichever COUNT statement
|
|
570
|
+
// finishes first and the same query serializes differently run to run.
|
|
492
571
|
for (const parent of parents) {
|
|
493
572
|
if (parent._count === undefined)
|
|
494
573
|
parent._count = {};
|
|
574
|
+
const counts = parent._count;
|
|
575
|
+
for (const rel of rels)
|
|
576
|
+
counts[rel.name] = 0;
|
|
495
577
|
}
|
|
496
578
|
await Promise.all(rels.map((rel) => loadOneCount(ctx, parents, rel)));
|
|
497
579
|
}
|
|
@@ -78,6 +78,82 @@ export declare const AUTO_TO_ONE_JOIN_MAX_ROWS: number;
|
|
|
78
78
|
*/
|
|
79
79
|
export declare const AUTO_TO_ONE_JOIN_ROWS_MIN = 100;
|
|
80
80
|
export declare const AUTO_TO_ONE_JOIN_ROWS_MAX = 100000;
|
|
81
|
+
/**
|
|
82
|
+
* WHY THIS IS A CONFIGURED LATENCY AND NOT A MEASURED ONE.
|
|
83
|
+
*
|
|
84
|
+
* The obvious next step from the formula above is to have the client measure
|
|
85
|
+
* its own round-trip time and derive the threshold at runtime. That was built
|
|
86
|
+
* and benchmarked, and it is NOT what ships, for a reason worth recording so it
|
|
87
|
+
* is not re-litigated blind:
|
|
88
|
+
*
|
|
89
|
+
* Every query's wall time is `roundTrip + serverWork`, and nothing in a
|
|
90
|
+
* duration distinguishes the two. An all-time MINIMUM reads a lucky packet
|
|
91
|
+
* (1.489ms on a link whose real per-statement cost was 2.862ms) and lands the
|
|
92
|
+
* threshold at half the true break-even. A MEDIAN over recent durations is
|
|
93
|
+
* accurate when the workload is cheap queries, but the workload being planned
|
|
94
|
+
* for here is precisely the expensive one: in the verification sweep the ring
|
|
95
|
+
* filled with 10-17ms relation queries, the estimate inflated, and `'auto'`
|
|
96
|
+
* held an 8,000-row query on the join plan, 1.30x slower than the better plan,
|
|
97
|
+
* WORSE than the fixed constant it replaced. Capping the median against a
|
|
98
|
+
* multiple of the floor mitigates it but turns the whole thing into a pair of
|
|
99
|
+
* magic numbers tuned against two synthetic links, which is the same mistake as
|
|
100
|
+
* a socket-tuned row count wearing a different hat.
|
|
101
|
+
*
|
|
102
|
+
* Round-trip time is a deployment fact, not a runtime discovery: it is fixed by
|
|
103
|
+
* where the app runs relative to the database, the operator knows it (or gets
|
|
104
|
+
* it from one `ping`), and it does not change between queries. So it is
|
|
105
|
+
* configuration. That also keeps plan selection deterministic, which matters
|
|
106
|
+
* for a library whose documented guarantee is that the strategy changes the
|
|
107
|
+
* plan and never the result.
|
|
108
|
+
*/
|
|
109
|
+
/**
|
|
110
|
+
* The smallest plan-time parent-row bound at which `'auto'` moves a relation
|
|
111
|
+
* `_count` on a PROVEN-UNINDEXED probe to the grouped follow-up. Deliberately
|
|
112
|
+
* 2, i.e. "everything except a parent set provably bounded at one row".
|
|
113
|
+
*
|
|
114
|
+
* `_count` does NOT share the to-one break-even formula above, because its two
|
|
115
|
+
* plans do not differ by a small per-row penalty. Writing S for one scan of the
|
|
116
|
+
* child table and RTT for a round trip:
|
|
117
|
+
*
|
|
118
|
+
* inline(N) = N x S (a correlated COUNT(*) per parent row; see
|
|
119
|
+
* buildRelationCountExpr in relations.ts, the
|
|
120
|
+
* inline form is NOT a grouped scan)
|
|
121
|
+
* batched(N) = S + RTT (one `COUNT(*) ... GROUP BY fk` follow-up)
|
|
122
|
+
*
|
|
123
|
+
* so the crossover sits at `N = 1 + RTT/S` and, decisively, the two regrets are
|
|
124
|
+
* not comparable in kind:
|
|
125
|
+
*
|
|
126
|
+
* - choosing batched when inline would have won costs at most RTT, once, and
|
|
127
|
+
* ONLY at N = 1 (at N = 1 the difference is exactly RTT, and it shrinks to
|
|
128
|
+
* zero immediately after);
|
|
129
|
+
* - choosing inline when batched would have won costs (N - 1) x S, which is
|
|
130
|
+
* unbounded in the parent count.
|
|
131
|
+
*
|
|
132
|
+
* Measured on an UNINDEXED FK (PostgreSQL 16, 200K-row child table, 10K-row
|
|
133
|
+
* parent table, median of 11 interleaved reps per point, loopback;
|
|
134
|
+
* benchmarks/bench-count-strategy.ts):
|
|
135
|
+
*
|
|
136
|
+
* parents 1 2 3 5 20 100 1000 10000
|
|
137
|
+
* inline 4.3ms 8.3ms 12.3ms 20.1ms 79.2ms 417.5ms 3.06s 31.06s
|
|
138
|
+
* batched 5.2ms 4.8ms 4.8ms 5.2ms 6.1ms 11.5ms 9.9ms 28.4ms
|
|
139
|
+
* winner inline batched batched batched batched batched batched batched
|
|
140
|
+
* ratio 1.22x 1.73x 2.54x 3.84x 13.05x 36.42x 310.92x 1093.35x
|
|
141
|
+
*
|
|
142
|
+
* Inline wins exactly one cell, by 0.9ms, then loses the next by 1.73x and the
|
|
143
|
+
* last by 1093x. A skewed child distribution (half the rows on ten parents)
|
|
144
|
+
* moves nothing: same crossover at 2, same 1179x at 10,000. So the useful
|
|
145
|
+
* threshold is not a tunable row count, it is the one row where inline provably
|
|
146
|
+
* cannot lose. There is deliberately no config knob: the entire regret this rule
|
|
147
|
+
* can produce is one round trip, which is less than any knob would be worth, and
|
|
148
|
+
* `relationLoadStrategy: 'join'` already forces the single-statement plan.
|
|
149
|
+
*
|
|
150
|
+
* This applies ONLY to a probe the introspected index metadata PROVES unindexed.
|
|
151
|
+
* An INDEXED `_count` stays inline at every size measured (inline wins 1.30x to
|
|
152
|
+
* 2.06x from 1 to 10,000 parents, because the per-parent subquery collapses to
|
|
153
|
+
* an index-only scan costing ~0.001ms), and the partition below never demotes
|
|
154
|
+
* it.
|
|
155
|
+
*/
|
|
156
|
+
export declare const AUTO_COUNT_BATCH_MIN_PARENT_ROWS = 2;
|
|
81
157
|
export type { DeferredQuery, MiddlewareFn, QueryEvent, QueryEventListener, QueryInterfaceOptions, ReselectExecutor, } from './deferred.js';
|
|
82
158
|
import type { DeferredQuery, MiddlewareFn, QueryInterfaceOptions } from './deferred.js';
|
|
83
159
|
export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
@@ -225,6 +301,46 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
225
301
|
*/
|
|
226
302
|
private readonly ctx;
|
|
227
303
|
constructor(pool: pg.Pool, table: string, schema: SchemaMetadata, middlewares?: MiddlewareFn[], options?: QueryInterfaceOptions);
|
|
304
|
+
/**
|
|
305
|
+
* Dev-only, once per table: the columns whose database type is absent from
|
|
306
|
+
* BOTH the column entry and the table-level type maps, the residual case
|
|
307
|
+
* after the two-source resolution above.
|
|
308
|
+
*
|
|
309
|
+
* The set is deliberately every untyped column, not the `dateColumns`
|
|
310
|
+
* members. An unresolved type is precisely the state in which Turbine cannot
|
|
311
|
+
* say WHICH kind of column it is, so restricting the scan to `dateColumns`
|
|
312
|
+
* got it wrong in both directions: that set carries `timestamptz` (whose
|
|
313
|
+
* bind was never affected, since binding the `Date` is the correct thing to
|
|
314
|
+
* do for it) and omits `time` / `timetz` entirely (deliberately, see
|
|
315
|
+
* `timeOfDayKind` in schema.ts), which is the one kind that fails LOUDLY
|
|
316
|
+
* rather than silently. The message therefore names the columns and states
|
|
317
|
+
* what each kind does, rather than asserting a kind it cannot know.
|
|
318
|
+
*
|
|
319
|
+
* What is actually at stake per kind, all of it `coerceWriteValue` returning
|
|
320
|
+
* the bound `Date` by identity for want of a type:
|
|
321
|
+
* - zone-less `date` / `timestamp`: the driver serializes with the
|
|
322
|
+
* PROCESS's offset, so the column stores local calendar fields. Nothing
|
|
323
|
+
* surfaces at runtime, and a turbine-only round trip reads the same value
|
|
324
|
+
* back (the read path shifts by the same offset), so only an outside
|
|
325
|
+
* reader sees the drift.
|
|
326
|
+
* - `time` / `timetz`: the driver serializes a full ISO timestamp, which
|
|
327
|
+
* Postgres rejects with `22007 invalid input syntax for type time`.
|
|
328
|
+
* - `timestamptz` and every non-temporal type: unaffected.
|
|
329
|
+
*
|
|
330
|
+
* PostgreSQL only: the UTC bind rewrite is Postgres-gated (see
|
|
331
|
+
* `utcDateTimeWrites` in writes.ts), so on the other engines a missing type
|
|
332
|
+
* changes nothing about how a `Date` is bound. Suppressed under
|
|
333
|
+
* `NODE_ENV=production` like the other dev diagnostics and deduped through
|
|
334
|
+
* the shared registry, so a hot table logs one line for the process.
|
|
335
|
+
*
|
|
336
|
+
* Cannot throw on odd metadata: it walks `tableMeta.columns`, the array the
|
|
337
|
+
* constructor loop above has already iterated (and that client.ts validates
|
|
338
|
+
* as an array), never `dateColumns`, which is a `Set` in every first-party
|
|
339
|
+
* metadata path but arrives as a plain object from JSON-round-tripped
|
|
340
|
+
* metadata. A dev-only diagnostic that crashes a shape production would serve
|
|
341
|
+
* is worse than the bug it reports.
|
|
342
|
+
*/
|
|
343
|
+
private warnUntypedColumns;
|
|
228
344
|
/** Quote an identifier through the active SQL dialect. */
|
|
229
345
|
private q;
|
|
230
346
|
/** Return the active dialect's placeholder for a 1-indexed parameter position. */
|
|
@@ -427,6 +543,16 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
427
543
|
* `findFirst` pass `false` explicitly (their parent set is one row).
|
|
428
544
|
*/
|
|
429
545
|
private autoParentSetLarge;
|
|
546
|
+
/**
|
|
547
|
+
* The plan-time UPPER BOUND on the parent-row count, or `undefined` when the
|
|
548
|
+
* query is unbounded. This is the raw number behind
|
|
549
|
+
* {@link autoParentSetLarge}; the `_count` rule needs the number itself
|
|
550
|
+
* because its threshold ({@link AUTO_COUNT_BATCH_MIN_PARENT_ROWS}) is two
|
|
551
|
+
* rows rather than the to-one break-even. `findUnique` / `findFirst` pass `1`
|
|
552
|
+
* directly: their parent set is one row as a matter of the statement's shape,
|
|
553
|
+
* not an estimate.
|
|
554
|
+
*/
|
|
555
|
+
private autoParentBound;
|
|
430
556
|
/**
|
|
431
557
|
* The parent-row count at which `'auto'` stops preferring the single-statement
|
|
432
558
|
* join for a to-one relation.
|
|
@@ -465,11 +591,11 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
465
591
|
*
|
|
466
592
|
* Everything else (indexed to-many, composite-key, unknown) stays in `joinWith`
|
|
467
593
|
* (byte-identical join). The reserved `_count` key falls back on rule 1 only,
|
|
468
|
-
* and
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
* the dev note.
|
|
594
|
+
* and on its OWN size rule: an inline `_count` is one correlated `COUNT(*)`
|
|
595
|
+
* per parent row over an unindexed child table, so the grouped follow-up wins
|
|
596
|
+
* from {@link AUTO_COUNT_BATCH_MIN_PARENT_ROWS} parent rows upward and inline
|
|
597
|
+
* is preferred only when the parent set is provably bounded below that. Also
|
|
598
|
+
* returns the engaged relations for the dev note.
|
|
473
599
|
*/
|
|
474
600
|
private partitionWithForAuto;
|
|
475
601
|
/**
|