litedbmodel 2.0.2 → 2.2.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 +7 -0
- package/dist/DBConditions.d.ts.map +1 -1
- package/dist/DBConditions.js +16 -1
- package/dist/DBConditions.js.map +1 -1
- package/dist/DBHandler.d.ts.map +1 -1
- package/dist/DBHandler.js +6 -0
- package/dist/DBHandler.js.map +1 -1
- package/dist/DBModel.d.ts +84 -2
- package/dist/DBModel.d.ts.map +1 -1
- package/dist/DBModel.js +312 -29
- package/dist/DBModel.js.map +1 -1
- package/dist/decorators.d.ts +52 -0
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +127 -24
- package/dist/decorators.js.map +1 -1
- package/dist/drivers/mysql.d.ts.map +1 -1
- package/dist/drivers/mysql.js +12 -2
- package/dist/drivers/mysql.js.map +1 -1
- package/dist/drivers/postgres.d.ts.map +1 -1
- package/dist/drivers/postgres.js +13 -2
- package/dist/drivers/postgres.js.map +1 -1
- package/dist/drivers/sqlite.d.ts +2 -0
- package/dist/drivers/sqlite.d.ts.map +1 -1
- package/dist/drivers/sqlite.js +49 -4
- package/dist/drivers/sqlite.js.map +1 -1
- package/dist/index.cjs +10741 -0
- package/dist/index.d.cts +1 -0
- package/dist/scp/coltype.d.ts +108 -0
- package/dist/scp/coltype.d.ts.map +1 -1
- package/dist/scp/coltype.js +293 -6
- package/dist/scp/coltype.js.map +1 -1
- package/dist/scp/connection-routing.d.ts +312 -0
- package/dist/scp/connection-routing.d.ts.map +1 -0
- package/dist/scp/connection-routing.js +394 -0
- package/dist/scp/connection-routing.js.map +1 -0
- package/dist/scp/dbmodel-runtime.d.ts +101 -0
- package/dist/scp/dbmodel-runtime.d.ts.map +1 -0
- package/dist/scp/dbmodel-runtime.js +174 -0
- package/dist/scp/dbmodel-runtime.js.map +1 -0
- package/dist/scp/decorator-adapter.d.ts +196 -0
- package/dist/scp/decorator-adapter.d.ts.map +1 -0
- package/dist/scp/decorator-adapter.js +286 -0
- package/dist/scp/decorator-adapter.js.map +1 -0
- package/dist/scp/emit/emitter.d.ts +108 -0
- package/dist/scp/emit/emitter.d.ts.map +1 -0
- package/dist/scp/emit/emitter.js +782 -0
- package/dist/scp/emit/emitter.js.map +1 -0
- package/dist/scp/emit/endpoint.d.ts +248 -0
- package/dist/scp/emit/endpoint.d.ts.map +1 -0
- package/dist/scp/emit/endpoint.js +20 -0
- package/dist/scp/emit/endpoint.js.map +1 -0
- package/dist/scp/emit/index.d.ts +11 -0
- package/dist/scp/emit/index.d.ts.map +1 -0
- package/dist/scp/emit/index.js +13 -0
- package/dist/scp/emit/index.js.map +1 -0
- package/dist/scp/errors.d.ts +45 -1
- package/dist/scp/errors.d.ts.map +1 -1
- package/dist/scp/errors.js +47 -1
- package/dist/scp/errors.js.map +1 -1
- package/dist/scp/exec-context.d.ts +390 -0
- package/dist/scp/exec-context.d.ts.map +1 -0
- package/dist/scp/exec-context.js +540 -0
- package/dist/scp/exec-context.js.map +1 -0
- package/dist/scp/grouping.d.ts +32 -0
- package/dist/scp/grouping.d.ts.map +1 -0
- package/dist/scp/grouping.js +0 -0
- package/dist/scp/grouping.js.map +1 -0
- package/dist/scp/index.cjs +6432 -17579
- package/dist/scp/index.d.cts +1 -0
- package/dist/scp/index.d.mts +1 -0
- package/dist/scp/index.d.ts +51 -52
- package/dist/scp/index.d.ts.map +1 -1
- package/dist/scp/index.js +189 -152
- package/dist/scp/index.js.map +1 -1
- package/dist/scp/index.mjs +6067 -3659
- package/dist/scp/leaf-transport.d.ts +50 -0
- package/dist/scp/leaf-transport.d.ts.map +1 -0
- package/dist/scp/leaf-transport.js +92 -0
- package/dist/scp/leaf-transport.js.map +1 -0
- package/dist/scp/leaves.d.ts +174 -0
- package/dist/scp/leaves.d.ts.map +1 -0
- package/dist/scp/leaves.js +287 -0
- package/dist/scp/leaves.js.map +1 -0
- package/dist/scp/limit-config.d.ts +118 -0
- package/dist/scp/limit-config.d.ts.map +1 -0
- package/dist/scp/limit-config.js +126 -0
- package/dist/scp/limit-config.js.map +1 -0
- package/dist/scp/makesql/compile-relation.d.ts +49 -23
- package/dist/scp/makesql/compile-relation.d.ts.map +1 -1
- package/dist/scp/makesql/compile-relation.js +101 -76
- package/dist/scp/makesql/compile-relation.js.map +1 -1
- package/dist/scp/makesql/compile-select.d.ts +45 -10
- package/dist/scp/makesql/compile-select.d.ts.map +1 -1
- package/dist/scp/makesql/compile-select.js +50 -10
- package/dist/scp/makesql/compile-select.js.map +1 -1
- package/dist/scp/makesql/handler.d.ts +1 -1
- package/dist/scp/makesql/handler.d.ts.map +1 -1
- package/dist/scp/makesql/index.d.ts +7 -10
- package/dist/scp/makesql/index.d.ts.map +1 -1
- package/dist/scp/makesql/index.js +19 -30
- package/dist/scp/makesql/index.js.map +1 -1
- package/dist/scp/makesql/json-array.d.ts +65 -0
- package/dist/scp/makesql/json-array.d.ts.map +1 -1
- package/dist/scp/makesql/json-array.js +99 -10
- package/dist/scp/makesql/json-array.js.map +1 -1
- package/dist/scp/makesql/json-batch.d.ts.map +1 -1
- package/dist/scp/makesql/json-batch.js +21 -19
- package/dist/scp/makesql/json-batch.js.map +1 -1
- package/dist/scp/makesql/makesql.d.ts +1 -1
- package/dist/scp/makesql/makesql.d.ts.map +1 -1
- package/dist/scp/makesql/mysql-returning.d.ts +93 -0
- package/dist/scp/makesql/mysql-returning.d.ts.map +1 -0
- package/dist/scp/makesql/mysql-returning.js +215 -0
- package/dist/scp/makesql/mysql-returning.js.map +1 -0
- package/dist/scp/makesql/outtype.d.ts +75 -34
- package/dist/scp/makesql/outtype.d.ts.map +1 -1
- package/dist/scp/makesql/outtype.js +91 -118
- package/dist/scp/makesql/outtype.js.map +1 -1
- package/dist/scp/makesql/pool-executor.d.ts +135 -13
- package/dist/scp/makesql/pool-executor.d.ts.map +1 -1
- package/dist/scp/makesql/pool-executor.js +229 -21
- package/dist/scp/makesql/pool-executor.js.map +1 -1
- package/dist/scp/makesql/tx.d.ts +107 -22
- package/dist/scp/makesql/tx.d.ts.map +1 -1
- package/dist/scp/makesql/tx.js +451 -97
- package/dist/scp/makesql/tx.js.map +1 -1
- package/dist/scp/makesql/writeouttype.d.ts +7 -8
- package/dist/scp/makesql/writeouttype.d.ts.map +1 -1
- package/dist/scp/makesql/writeouttype.js +8 -7
- package/dist/scp/makesql/writeouttype.js.map +1 -1
- package/dist/scp/middleware.d.ts +287 -0
- package/dist/scp/middleware.d.ts.map +1 -0
- package/dist/scp/middleware.js +358 -0
- package/dist/scp/middleware.js.map +1 -0
- package/dist/scp/relation.d.ts +80 -9
- package/dist/scp/relation.d.ts.map +1 -1
- package/dist/scp/relation.js +198 -78
- package/dist/scp/relation.js.map +1 -1
- package/dist/scp/tx-options.d.ts +136 -0
- package/dist/scp/tx-options.d.ts.map +1 -0
- package/dist/scp/tx-options.js +184 -0
- package/dist/scp/tx-options.js.map +1 -0
- package/dist/scp/write-bundle.d.ts +131 -0
- package/dist/scp/write-bundle.d.ts.map +1 -0
- package/dist/scp/write-bundle.js +160 -0
- package/dist/scp/write-bundle.js.map +1 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/docs/BENCHMARK.md +58 -56
- package/docs/benchmark-chart.svg +166 -166
- package/docs/design/phase-a-execution-context.md +139 -0
- package/package.json +16 -9
- package/dist/scp/authoring-sql.d.ts +0 -134
- package/dist/scp/authoring-sql.d.ts.map +0 -1
- package/dist/scp/authoring-sql.js +0 -191
- package/dist/scp/authoring-sql.js.map +0 -1
- package/dist/scp/authoring.d.ts +0 -155
- package/dist/scp/authoring.d.ts.map +0 -1
- package/dist/scp/authoring.js +0 -162
- package/dist/scp/authoring.js.map +0 -1
- package/dist/scp/catalog.d.ts +0 -94
- package/dist/scp/catalog.d.ts.map +0 -1
- package/dist/scp/catalog.js +0 -234
- package/dist/scp/catalog.js.map +0 -1
- package/dist/scp/codegen.d.ts +0 -189
- package/dist/scp/codegen.d.ts.map +0 -1
- package/dist/scp/codegen.js +0 -421
- package/dist/scp/codegen.js.map +0 -1
- package/dist/scp/find-filter-guard.d.ts +0 -68
- package/dist/scp/find-filter-guard.d.ts.map +0 -1
- package/dist/scp/find-filter-guard.js +0 -88
- package/dist/scp/find-filter-guard.js.map +0 -1
- package/dist/scp/guard.d.ts +0 -43
- package/dist/scp/guard.d.ts.map +0 -1
- package/dist/scp/guard.js +0 -134
- package/dist/scp/guard.js.map +0 -1
- package/dist/scp/makesql/authoring-compile.d.ts +0 -90
- package/dist/scp/makesql/authoring-compile.d.ts.map +0 -1
- package/dist/scp/makesql/authoring-compile.js +0 -494
- package/dist/scp/makesql/authoring-compile.js.map +0 -1
- package/dist/scp/makesql/ir.d.ts +0 -42
- package/dist/scp/makesql/ir.d.ts.map +0 -1
- package/dist/scp/makesql/ir.js +0 -66
- package/dist/scp/makesql/ir.js.map +0 -1
- package/dist/scp/makesql/static-bundle.d.ts +0 -187
- package/dist/scp/makesql/static-bundle.d.ts.map +0 -1
- package/dist/scp/makesql/static-bundle.js +0 -1113
- package/dist/scp/makesql/static-bundle.js.map +0 -1
- package/dist/scp/runtime.d.ts +0 -242
- package/dist/scp/runtime.d.ts.map +0 -1
- package/dist/scp/runtime.js +0 -400
- package/dist/scp/runtime.js.map +0 -1
package/dist/scp/makesql/tx.js
CHANGED
|
@@ -35,19 +35,31 @@
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.IN_SENTINEL = void 0;
|
|
37
37
|
exports.literalize = literalize;
|
|
38
|
-
exports.
|
|
39
|
-
exports.
|
|
38
|
+
exports.pgTypeSpecimen = pgTypeSpecimen;
|
|
39
|
+
exports.pkPort = pkPort;
|
|
40
40
|
exports.compileWriteNode = compileWriteNode;
|
|
41
41
|
exports.deriveTransactionPlan = deriveTransactionPlan;
|
|
42
42
|
exports.deriveBatchPlan = deriveBatchPlan;
|
|
43
43
|
exports.executeTransaction = executeTransaction;
|
|
44
44
|
exports.countingDriver = countingDriver;
|
|
45
45
|
exports.renderTxStatement = renderTxStatement;
|
|
46
|
-
|
|
46
|
+
exports.executeTransactionAsync = executeTransactionAsync;
|
|
47
|
+
const runtime_1 = require("behavior-contracts/runtime");
|
|
47
48
|
const makesql_1 = require("./makesql");
|
|
49
|
+
const json_batch_1 = require("./json-batch");
|
|
50
|
+
const json_array_1 = require("./json-array");
|
|
51
|
+
const PostgresSqlBuilder_1 = require("../../drivers/PostgresSqlBuilder");
|
|
52
|
+
const coltype_1 = require("../coltype");
|
|
48
53
|
const handler_1 = require("./handler");
|
|
49
54
|
const compile_1 = require("./compile");
|
|
50
55
|
const errors_1 = require("../errors");
|
|
56
|
+
// The mysql pk-hint writer. `mysql-returning` imports `TxOp` from here TYPE-only (erased), so this
|
|
57
|
+
// runtime import closes no cycle.
|
|
58
|
+
const mysql_returning_1 = require("./mysql-returning");
|
|
59
|
+
const exec_context_1 = require("../exec-context");
|
|
60
|
+
const leaves_1 = require("../leaves");
|
|
61
|
+
const tx_options_1 = require("../tx-options");
|
|
62
|
+
const connection_errors_1 = require("../../connection-errors");
|
|
51
63
|
const DBConditions_1 = require("../../DBConditions");
|
|
52
64
|
const writes_1 = require("../writes");
|
|
53
65
|
/**
|
|
@@ -73,6 +85,16 @@ function literalize(value) {
|
|
|
73
85
|
return null;
|
|
74
86
|
if (Array.isArray(value))
|
|
75
87
|
return { arr: value.map((e) => literalize(e)) };
|
|
88
|
+
// A bc `int` is a BigInt on the TS plane. `evaluateExpression` accepts an integer only as a plain
|
|
89
|
+
// JS number (safe range) or the `{int:"…"}` literal — a RAW BigInt node is rejected ("invalid node").
|
|
90
|
+
// A value read back from an integer column now arrives as a BigInt (bc's int model), and using it in a
|
|
91
|
+
// later write (e.g. the id from a `create({returning:true})` fed to a nested `create`) sent that BigInt
|
|
92
|
+
// straight into `evaluateExpression`, which threw. Encode it as bc's own canonical int literal
|
|
93
|
+
// (`{int: v.toString()}`, exactly what bc emits for an out-of-safe-range int) — exact, no rounding,
|
|
94
|
+
// and no value inference. MUST be before the `typeof === 'object'` branch would ever see it (a bigint
|
|
95
|
+
// is a primitive, so it would fall through to the bare `return value`).
|
|
96
|
+
if (typeof value === 'bigint')
|
|
97
|
+
return { int: value.toString() };
|
|
76
98
|
if (typeof value === 'object') {
|
|
77
99
|
const obj = {};
|
|
78
100
|
for (const [k, v] of Object.entries(value))
|
|
@@ -306,49 +328,175 @@ function v1EqualityWhereText(columns) {
|
|
|
306
328
|
conditions[c] = PROBE;
|
|
307
329
|
return v1ConditionText(conditions);
|
|
308
330
|
}
|
|
309
|
-
/**
|
|
310
|
-
|
|
331
|
+
/**
|
|
332
|
+
* Lower one where-member Expression node → a `<sql, params, columns>` WHERE fragment (deferred
|
|
333
|
+
* params). `columns[i]` is the table column the i-th emitted param binds — parallel to `params` — so
|
|
334
|
+
* the native-codegen chain types each WHERE-bound `?` from its column (see {@link TxOp.writeMeta}).
|
|
335
|
+
*/
|
|
336
|
+
function lowerWhereMember(node, at, dialect) {
|
|
311
337
|
const op = opKey(node);
|
|
312
338
|
if (op === undefined)
|
|
313
339
|
throw new Error(`compileWriteNode: ${at}: a where member must be a single-operator Expression node`);
|
|
340
|
+
if (op === 'in') {
|
|
341
|
+
// Key-set membership — the SAME static single-param predicate the read IN-list uses
|
|
342
|
+
// ({@link inListPredicate}: PG `= ANY(?)`, MySQL/SQLite a JSON subquery), so a `deleteMany`'s
|
|
343
|
+
// statement comes from THIS compiler rather than being assembled by its caller.
|
|
344
|
+
const [col, val] = binOperands(node, op, at);
|
|
345
|
+
const column = columnOf(col, at);
|
|
346
|
+
return { sql: v1ConditionText({ [(0, json_array_1.inListPredicate)(dialect, column)]: PROBE }), params: [val], columns: [column] };
|
|
347
|
+
}
|
|
314
348
|
if (op === 'eq') {
|
|
315
349
|
const [col, val] = binOperands(node, op, at);
|
|
316
350
|
const column = columnOf(col, at);
|
|
317
351
|
if (val === null)
|
|
318
|
-
return { sql: v1ConditionText({ [column]: null }), params: [] };
|
|
319
|
-
return { sql: v1ConditionText({ [column]: PROBE }), params: [val] };
|
|
352
|
+
return { sql: v1ConditionText({ [column]: null }), params: [], columns: [] };
|
|
353
|
+
return { sql: v1ConditionText({ [column]: PROBE }), params: [val], columns: [column] };
|
|
320
354
|
}
|
|
321
355
|
if (op in CMP_OPS) {
|
|
322
356
|
const [col, val] = binOperands(node, op, at);
|
|
323
357
|
const column = columnOf(col, at);
|
|
324
358
|
if (op === 'ne' && val === null)
|
|
325
|
-
return { sql: v1ConditionText({ [`${column} IS NOT NULL`]: true }), params: [] };
|
|
326
|
-
return { sql: v1ConditionText({ [`${column} ${CMP_OPS[op]} ?`]: PROBE }), params: [val] };
|
|
359
|
+
return { sql: v1ConditionText({ [`${column} IS NOT NULL`]: true }), params: [], columns: [] };
|
|
360
|
+
return { sql: v1ConditionText({ [`${column} ${CMP_OPS[op]} ?`]: PROBE }), params: [val], columns: [column] };
|
|
327
361
|
}
|
|
328
362
|
throw new Error(`compileWriteNode: ${at}: unsupported where operator '${op}' (write path supports eq/ne/lt/le/gt/ge)`);
|
|
329
363
|
}
|
|
330
|
-
function lowerWherePort(ports, at) {
|
|
364
|
+
function lowerWherePort(ports, at, dialect) {
|
|
331
365
|
const v = ports.where;
|
|
332
366
|
if (v === undefined)
|
|
333
|
-
return { sql: '', params: [] };
|
|
367
|
+
return { sql: '', params: [], columns: [] };
|
|
334
368
|
if (typeof v !== 'object' || v === null || !('arr' in v) || !Array.isArray(v.arr)) {
|
|
335
369
|
throw new Error(`compileWriteNode: ${at}: 'where' must be an {arr:[…]} literal`);
|
|
336
370
|
}
|
|
337
371
|
const members = v.arr;
|
|
338
372
|
const parts = [];
|
|
339
373
|
const params = [];
|
|
374
|
+
const columns = [];
|
|
340
375
|
members.forEach((m, i) => {
|
|
341
|
-
const f = lowerWhereMember(m, `${at}.where[${i}]
|
|
376
|
+
const f = lowerWhereMember(m, `${at}.where[${i}]`, dialect);
|
|
342
377
|
parts.push(f.sql);
|
|
343
378
|
params.push(...f.params);
|
|
379
|
+
columns.push(...f.columns);
|
|
344
380
|
});
|
|
345
381
|
// Join with the SAME ` AND ` connector `DBConditions.compile` uses (parts.join(' AND ')).
|
|
346
|
-
return { sql: parts.join(' AND '), params };
|
|
382
|
+
return { sql: parts.join(' AND '), params, columns };
|
|
383
|
+
}
|
|
384
|
+
/** The RETURNING column names (`['id','author_id']`), or `[]` when the op has no RETURNING clause. */
|
|
385
|
+
function returningColumns(ports) {
|
|
386
|
+
const r = stringPort(ports, 'returning');
|
|
387
|
+
return r === undefined ? [] : r.split(',').map((c) => c.trim()).filter((c) => c.length > 0);
|
|
347
388
|
}
|
|
348
389
|
function returningTail(ports) {
|
|
349
390
|
const r = stringPort(ports, 'returning');
|
|
350
391
|
return r === undefined ? '' : ` RETURNING ${r}`;
|
|
351
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* The upsert `ON CONFLICT` / `ON DUPLICATE KEY` tail of an `Insert`, from the `onConflict` (the
|
|
395
|
+
* conflict-target column list) + `onConflictAction` (`'update'` default / `'ignore'`) ports. Absent
|
|
396
|
+
* `onConflict` ⇒ a plain INSERT (no tail). Per-dialect verbs, byte-matching the JSON-batch upsert
|
|
397
|
+
* form (`sqliteInsertJson`): pg/sqlite `ON CONFLICT (k) DO UPDATE SET c = excluded.c` / `DO NOTHING`;
|
|
398
|
+
* mysql `ON DUPLICATE KEY UPDATE c = VALUES(c)` (mysql ignores the target list). The DO-UPDATE sets
|
|
399
|
+
* every inserted column to its excluded value (`onConflictUpdate:'all'` — the v1 builder fallback);
|
|
400
|
+
* setting the key column to itself is a harmless no-op. This shared compiler is what BOTH the runtime
|
|
401
|
+
* (`executeStaticWrite`) and codegen read, so an authored upsert executes AND bakes identically.
|
|
402
|
+
*/
|
|
403
|
+
/** Map the `onConflict`/`onConflictAction` ports to the {@link JsonInsertOptions} upsert fields (the
|
|
404
|
+
* batch json_each builder's own shape) — so a batch UPSERT (upsertMany) reuses the SAME conflict verbs
|
|
405
|
+
* as the single upsert (E2). Absent `onConflict` ⇒ a plain batch INSERT. */
|
|
406
|
+
function onConflictJsonOpts(ports, cols) {
|
|
407
|
+
const conflict = stringPort(ports, 'onConflict');
|
|
408
|
+
if (conflict === undefined)
|
|
409
|
+
return {};
|
|
410
|
+
const onConflict = conflict.split(',').map((c) => c.trim()).filter((c) => c.length > 0);
|
|
411
|
+
const action = stringPort(ports, 'onConflictAction') ?? 'update';
|
|
412
|
+
void cols;
|
|
413
|
+
return action === 'ignore' ? { onConflict, onConflictIgnore: true } : { onConflict, onConflictUpdate: 'all' };
|
|
414
|
+
}
|
|
415
|
+
function onConflictTail(dialect, ports, cols) {
|
|
416
|
+
const conflict = stringPort(ports, 'onConflict');
|
|
417
|
+
if (conflict === undefined)
|
|
418
|
+
return '';
|
|
419
|
+
const action = stringPort(ports, 'onConflictAction') ?? 'update';
|
|
420
|
+
const conflictCols = conflict.split(',').map((c) => c.trim()).filter((c) => c.length > 0);
|
|
421
|
+
if (action === 'ignore') {
|
|
422
|
+
return dialect === 'mysql'
|
|
423
|
+
? ` ON DUPLICATE KEY UPDATE ${conflictCols[0]} = ${conflictCols[0]}` // mysql no-op update = IGNORE-equivalent
|
|
424
|
+
: ` ON CONFLICT (${conflictCols.join(', ')}) DO NOTHING`;
|
|
425
|
+
}
|
|
426
|
+
if (dialect === 'mysql') {
|
|
427
|
+
return ` ON DUPLICATE KEY UPDATE ${cols.map((c) => `${c} = VALUES(${c})`).join(', ')}`;
|
|
428
|
+
}
|
|
429
|
+
return ` ON CONFLICT (${conflictCols.join(', ')}) DO UPDATE SET ${cols.map((c) => `${c} = excluded.${c}`).join(', ')}`;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* A per-column raw VALUE SPECIMEN whose {@link import('../../drivers/PostgresSqlBuilder').inferPgType}
|
|
433
|
+
* matches the column's SCHEMA SQL type, so the PG batch UNNEST cast (`?::<pgType>[]`) the original
|
|
434
|
+
* `buildInsert`/`buildUpdateMany` emits is derived from the schema SoT — NOT from the runtime values
|
|
435
|
+
* the symbolic codegen path never sees. int32→number / int64→bigint / bool→boolean / date→Date /
|
|
436
|
+
* float→non-integer number / string(+decimal/uuid/json)→string, reproducing each `inferPgType` branch.
|
|
437
|
+
* Ambiguous only for a REAL column whose live value is integer-valued (v1 would infer `int`, not
|
|
438
|
+
* `numeric`); the bench columns (text/int/bigint) are unambiguous. Unknown SQL types are a hard error
|
|
439
|
+
* (fail-closed) via the §4.1 classifiers.
|
|
440
|
+
*/
|
|
441
|
+
function pgTypeSpecimen(sqlType) {
|
|
442
|
+
const klass = (0, coltype_1.sqlTypeToMaterializeClass)(sqlType);
|
|
443
|
+
if (klass === 'int32')
|
|
444
|
+
return 0;
|
|
445
|
+
if (klass === 'int64')
|
|
446
|
+
return 0n;
|
|
447
|
+
if (klass === 'bool')
|
|
448
|
+
return false;
|
|
449
|
+
if (klass === 'date')
|
|
450
|
+
return new Date(0);
|
|
451
|
+
// passthrough: float / decimal(→string) / text / uuid / json — split by the bc scalar.
|
|
452
|
+
const scalar = (0, coltype_1.sqlTypeToBcScalar)(sqlType);
|
|
453
|
+
if (scalar === 'float')
|
|
454
|
+
return 0.5; // a non-integer number ⇒ inferPgType 'numeric'
|
|
455
|
+
return ''; // string family (text / varchar / uuid / decimal / json) ⇒ inferPgType 'text'
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Compile a PG BATCH Insert/Update to its byte-identical v1 UNNEST form for the NATIVE codegen path,
|
|
459
|
+
* by driving the ORIGINAL `postgresSqlBuilder` (never a re-roll) with schema-typed specimen records
|
|
460
|
+
* (so the emitted `?::<pgType>[]` casts come from the schema SoT). Each `?` binds a per-column
|
|
461
|
+
* {@link BatchArrayMarker} — the PG (v1) twin of the sqlite/mysql (v2) `{__batchRows}` JSON marker:
|
|
462
|
+
* `refs[i]` is the WHOLE array for column `columns[i]`. codegen types the SAME array-input head off
|
|
463
|
+
* both markers (one shared path); the per-driver seam binds each PG marker as a `<elem>[]` array.
|
|
464
|
+
* Length-independent (the UNNEST text depends only on columns + types + onConflict/returning), so
|
|
465
|
+
* FIXED and bakeable.
|
|
466
|
+
*/
|
|
467
|
+
function pgBatchArrayParams(cols, refFor, dialect) {
|
|
468
|
+
return cols.map((c) => ({ __batchArray: { column: c, ref: refFor(c), dialect } }));
|
|
469
|
+
}
|
|
470
|
+
function pgBatchInsert(table, sorted, values, ports, resolve, dialect) {
|
|
471
|
+
const specimen = Object.fromEntries(sorted.map((c) => [c, pgTypeSpecimen(resolve(table, c))]));
|
|
472
|
+
const records = [specimen, specimen]; // 2 rows ⇒ UNNEST branch (records.length > 1)
|
|
473
|
+
const { sql } = PostgresSqlBuilder_1.postgresSqlBuilder.buildInsert({
|
|
474
|
+
tableName: table, columns: sorted, records, rawRecords: records,
|
|
475
|
+
...onConflictJsonOpts(ports, sorted),
|
|
476
|
+
...(stringPort(ports, 'returning') !== undefined ? { returning: stringPort(ports, 'returning') } : {}),
|
|
477
|
+
});
|
|
478
|
+
return { sql, params: pgBatchArrayParams(sorted, (c) => values[c], dialect) };
|
|
479
|
+
}
|
|
480
|
+
function pgBatchUpdate(table, keyCols, updateCols, key, set, ports, resolve, dialect) {
|
|
481
|
+
const allCols = [...keyCols, ...updateCols];
|
|
482
|
+
const specimen = Object.fromEntries(allCols.map((c) => [c, pgTypeSpecimen(resolve(table, c))]));
|
|
483
|
+
const records = [specimen, specimen];
|
|
484
|
+
// The pg batch UPDATE aliases the table `AS t` and the value source `AS v(keyCols…)`, so a BARE
|
|
485
|
+
// RETURNING column that is also a key column (in `v`) is ambiguous. v1's `DBModel.updateMany`
|
|
486
|
+
// qualifies RETURNING with the `t` alias via `buildReturning(table, cols, 't')` — reuse the SAME
|
|
487
|
+
// builder so the qualified RETURNING is byte-identical to v1 (never a hand-roll).
|
|
488
|
+
const returningPort = stringPort(ports, 'returning');
|
|
489
|
+
const returning = returningPort === undefined
|
|
490
|
+
? undefined
|
|
491
|
+
: PostgresSqlBuilder_1.postgresSqlBuilder.buildReturning(table, returningPort.split(',').map((c) => c.trim()).filter((c) => c.length > 0), 't');
|
|
492
|
+
const { sql } = PostgresSqlBuilder_1.postgresSqlBuilder.buildUpdateMany({
|
|
493
|
+
tableName: table, keyColumns: keyCols, updateColumns: updateCols, records, rawRecords: records,
|
|
494
|
+
...(returning !== undefined ? { returning } : {}),
|
|
495
|
+
});
|
|
496
|
+
// One array param per UNNEST column in [keyCols…, updateCols…] order (matches buildUpdateMany).
|
|
497
|
+
const refFor = (c) => (keyCols.includes(c) ? key[c] : set[c]);
|
|
498
|
+
return { sql, params: pgBatchArrayParams(allCols, refFor, dialect) };
|
|
499
|
+
}
|
|
352
500
|
/**
|
|
353
501
|
* Read the optional PRIMARY KEY descriptor ports of an Insert node (for the MySQL RETURNING
|
|
354
502
|
* emulation). `pk` is a comma-separated column list (`'doc_id'` / `'order_id,line_no'`); `autoInc`
|
|
@@ -366,34 +514,22 @@ function pkPort(ports) {
|
|
|
366
514
|
const ai = stringPort(ports, 'autoInc');
|
|
367
515
|
return { columns, autoInc: ai ?? null };
|
|
368
516
|
}
|
|
369
|
-
/** The strip-before-execute PK-hint comment marker the MySQL RETURNING emulation reads. */
|
|
370
|
-
const MYSQL_PK_HINT_RE = /\s*\/\*scp:pk=[^*]*\*\//;
|
|
371
|
-
/**
|
|
372
|
-
* Serialize a {@link TxOp.pk} descriptor into a strip-before-execute SQL comment appended to an
|
|
373
|
-
* INSERT…RETURNING op, so the MySQL driver emulation can re-select by the REAL primary key. The
|
|
374
|
-
* comment is STRIPPED (with the RETURNING clause) before the INSERT executes, so the executed SQL
|
|
375
|
-
* stays byte-clean; it is emitted ONLY into the mysql-dialect bundle (PG/SQLite keep native
|
|
376
|
-
* RETURNING and never see it). Format: ` /*scp:pk=col1,col2;ai=<autoIncCol|>* /`.
|
|
377
|
-
*/
|
|
378
|
-
function mysqlPkHint(op) {
|
|
379
|
-
if (op.pk === undefined)
|
|
380
|
-
return op;
|
|
381
|
-
if (!/\breturning\b/i.test(op.sql))
|
|
382
|
-
return op;
|
|
383
|
-
const hint = ` /*scp:pk=${op.pk.columns.join(',')};ai=${op.pk.autoInc ?? ''}*/`;
|
|
384
|
-
return { ...op, sql: op.sql + hint };
|
|
385
|
-
}
|
|
386
|
-
/** Strip a trailing MySQL PK-hint comment from a rendered SQL (defensive; runtimes strip too). */
|
|
387
|
-
function stripMysqlPkHint(sql) {
|
|
388
|
-
return sql.replace(MYSQL_PK_HINT_RE, '');
|
|
389
|
-
}
|
|
390
517
|
/**
|
|
391
518
|
* Compile ONE authored catalog write node (`Insert`/`Update`/`Delete`) into a makeSQL {@link TxOp}
|
|
392
519
|
* — complete tuned SQL text + DEFERRED Expression-IR params. This is the makeSQL re-expression of
|
|
393
520
|
* the reduced bridge's `compileNode` for the write path (the tx-DAG base writes). INSERT columns
|
|
394
521
|
* are CANONICAL (alphabetical) sorted — the v2 write-path SSoT (matches `DBModel._insert`).
|
|
395
522
|
*/
|
|
396
|
-
function compileWriteNode(node, dialect = 'sqlite') {
|
|
523
|
+
function compileWriteNode(node, dialect = 'sqlite', resolveColumnType) {
|
|
524
|
+
const op = compileWriteNodeSql(node, dialect, resolveColumnType);
|
|
525
|
+
// MySQL parses no RETURNING: the strip-before-execute pk hint rides the compiled SQL so the mysql
|
|
526
|
+
// connection adapter can re-select the written rows by the REAL key. It is applied HERE, at the one
|
|
527
|
+
// write compiler, rather than by each producer — the producers that forgot the ritual are exactly
|
|
528
|
+
// how a UUID / client-supplied / composite PK came to return no rows at all.
|
|
529
|
+
return dialect === 'mysql' ? (0, mysql_returning_1.mysqlPkHint)(op) : op;
|
|
530
|
+
}
|
|
531
|
+
/** {@link compileWriteNode}'s SQL body — the dialect-neutral descriptor → statement compile. */
|
|
532
|
+
function compileWriteNodeSql(node, dialect, resolveColumnType) {
|
|
397
533
|
const { component, ports } = node;
|
|
398
534
|
const table = stringPort(ports, 'table');
|
|
399
535
|
if (table === undefined)
|
|
@@ -407,32 +543,103 @@ function compileWriteNode(node, dialect = 'sqlite') {
|
|
|
407
543
|
if (cols.length === 0)
|
|
408
544
|
throw new Error(`compileWriteNode: Insert requires at least one 'values.<field>' port`);
|
|
409
545
|
const sorted = [...cols].sort();
|
|
546
|
+
// E3 (#118) BATCH insert (createMany / upsertMany): a `batch:'true'` marker means each
|
|
547
|
+
// `values.<col>` port is a PARALLEL ARRAY of that column's values (bc has no Vec<struct>, so the
|
|
548
|
+
// records ride as one scalar array per column). Reuse the EXISTING json_each batch f_sql (its
|
|
549
|
+
// text depends only on the columns + onConflict/returning — value-length-independent, so FIXED
|
|
550
|
+
// and bakeable); the ONE JSON `?` binds a `{__batchRows}` marker the runtime/codegen build from
|
|
551
|
+
// the parallel arrays at execute time (NOT literalized). One statement for N records.
|
|
552
|
+
if (stringPort(ports, 'batch') === 'true') {
|
|
553
|
+
if (dialect === 'postgres') {
|
|
554
|
+
if (resolveColumnType === undefined)
|
|
555
|
+
throw new Error(`compileWriteNode: batch insert on postgres needs the column-type resolver (schema SoT) to derive the UNNEST element casts — pass it through compileCreateManyBundle/compileWriteBundle (the decorator-adapter write path).`);
|
|
556
|
+
return pgBatchInsert(table, sorted, values, ports, resolveColumnType, dialect);
|
|
557
|
+
}
|
|
558
|
+
const shapeOpts = { tableName: table, columns: sorted, records: [], ...onConflictJsonOpts(ports, sorted), ...(stringPort(ports, 'returning') !== undefined ? { returning: stringPort(ports, 'returning') } : {}) };
|
|
559
|
+
const shape = dialect === 'mysql' ? (0, json_batch_1.mysqlInsertJson)(shapeOpts) : (0, json_batch_1.sqliteInsertJson)(shapeOpts);
|
|
560
|
+
// The ONE json param = a deferred marker carrying the columns + their parallel array refs.
|
|
561
|
+
const marker = { __batchRows: { columns: sorted, refs: sorted.map((c) => values[c]), dialect } };
|
|
562
|
+
// Carry `pk` + `onConflict` (the SAME pkPort/onConflict SSoT the single INSERT path uses) so the
|
|
563
|
+
// mysql RETURNING re-select recovers ALL N written rows by the real key (auto-inc range, or the
|
|
564
|
+
// conflict key for upsertMany). `batch:true` keeps the native tx-chain lowering rejecting this op.
|
|
565
|
+
const pk = pkPort(ports);
|
|
566
|
+
const onConflictCols = stringPort(ports, 'onConflict');
|
|
567
|
+
const writeMeta = { table, bindColumns: sorted, returning: returningColumns(ports), batch: true, ...(onConflictCols !== undefined ? { onConflict: onConflictCols } : {}) };
|
|
568
|
+
return { sql: shape.sql, params: [marker], ...(pk !== undefined ? { pk } : {}), writeMeta };
|
|
569
|
+
}
|
|
410
570
|
// v1 `DBModel._insert` emits `?::<sqlCast>` PER COLUMN on Postgres (skipping timestamp/date);
|
|
411
571
|
// the placeholder list is thus per-column, NOT a uniform `?` join (the latent H1 divergence).
|
|
412
572
|
const placeholders = sorted.map((c) => castPlaceholder(dialect, sqlCastMap, c)).join(', ');
|
|
413
|
-
const sql = `INSERT INTO ${table} (${sorted.join(', ')}) VALUES (${placeholders})${returningTail(ports)}`;
|
|
573
|
+
const sql = `INSERT INTO ${table} (${sorted.join(', ')}) VALUES (${placeholders})${onConflictTail(dialect, ports, sorted)}${returningTail(ports)}`;
|
|
414
574
|
const pk = pkPort(ports);
|
|
415
|
-
|
|
575
|
+
// The `?`s bind the value columns in sorted order (the ON CONFLICT / RETURNING tails add no `?`).
|
|
576
|
+
// `onConflict` (the conflict-target column list) rides on writeMeta ADDITIVELY (no SQL change) so a
|
|
577
|
+
// downstream mysql RETURNING re-select can recover an upserted row by its conflict key (mysql does
|
|
578
|
+
// not report the conflicted-row id) — used by the tx-chain codegen lowering.
|
|
579
|
+
const onConflictCols = stringPort(ports, 'onConflict');
|
|
580
|
+
const writeMeta = { table, bindColumns: sorted, returning: returningColumns(ports), ...(onConflictCols !== undefined ? { onConflict: onConflictCols } : {}) };
|
|
581
|
+
return { sql, params: sorted.map((c) => values[c]), ...(pk !== undefined ? { pk } : {}), writeMeta };
|
|
416
582
|
}
|
|
417
583
|
case 'Update': {
|
|
418
584
|
const set = collectFamily(ports, 'set');
|
|
419
585
|
const setCols = Object.keys(set);
|
|
420
586
|
if (setCols.length === 0)
|
|
421
587
|
throw new Error(`compileWriteNode: Update requires at least one 'set.<field>' port`);
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
588
|
+
// E3 (#118) BATCH update (updateMany): `batch:'true'` — the `key.<col>` family names the match
|
|
589
|
+
// key(s) (parallel arrays), the `set.<col>` family the columns to set (parallel arrays). Reuse
|
|
590
|
+
// the EXISTING json_each/JSON_TABLE batch UPDATE (`sqliteUpdateManyJson`): its text depends only
|
|
591
|
+
// on the key + update columns, so it's FIXED and bakeable. It binds the ONE records-JSON to
|
|
592
|
+
// MULTIPLE `?` (one per SET clause + the WHERE) — each is the SAME `__batchRows` marker, so the
|
|
593
|
+
// runtime evalSpec (and the codegen seam) build the SAME JSON per `?`. ONE statement for N rows.
|
|
594
|
+
if (stringPort(ports, 'batch') === 'true') {
|
|
595
|
+
const key = collectFamily(ports, 'key');
|
|
596
|
+
const keyCols = Object.keys(key).sort();
|
|
597
|
+
if (keyCols.length === 0)
|
|
598
|
+
throw new Error(`compileWriteNode: batch Update requires at least one 'key.<field>' port`);
|
|
599
|
+
const updateCols = [...setCols].sort();
|
|
600
|
+
if (dialect === 'postgres') {
|
|
601
|
+
if (resolveColumnType === undefined)
|
|
602
|
+
throw new Error(`compileWriteNode: batch update on postgres needs the column-type resolver (schema SoT) to derive the UNNEST element casts — pass it through compileCreateManyBundle/compileWriteBundle (the decorator-adapter write path).`);
|
|
603
|
+
return pgBatchUpdate(table, keyCols, updateCols, key, set, ports, resolveColumnType, dialect);
|
|
604
|
+
}
|
|
605
|
+
const shapeOpts = { tableName: table, keyColumns: keyCols, updateColumns: updateCols, records: [], ...(stringPort(ports, 'returning') !== undefined ? { returning: stringPort(ports, 'returning') } : {}) };
|
|
606
|
+
const shape = dialect === 'mysql' ? (0, json_batch_1.mysqlUpdateManyJson)(shapeOpts) : (0, json_batch_1.sqliteUpdateManyJson)(shapeOpts);
|
|
607
|
+
// The JSON carries BOTH the key + update columns (in that order); one marker per `?`.
|
|
608
|
+
const columns = [...keyCols, ...updateCols];
|
|
609
|
+
const refs = [...keyCols.map((c) => key[c]), ...updateCols.map((c) => set[c])];
|
|
610
|
+
const nQ = (shape.sql.match(/\?/g) ?? []).length;
|
|
611
|
+
const marker = { __batchRows: { columns, refs, dialect } };
|
|
612
|
+
// Carry `pk` (the SAME pkPort SSoT) so the mysql RETURNING re-select orders the recovered rows by
|
|
613
|
+
// the real key (matching pg/sqlite RETURNING order). `batch:true` keeps the tx-chain rejecting it.
|
|
614
|
+
const pk = pkPort(ports);
|
|
615
|
+
const writeMeta = { table, bindColumns: columns, returning: returningColumns(ports), batch: true };
|
|
616
|
+
return { sql: shape.sql, params: Array.from({ length: nQ }, () => marker), ...(pk !== undefined ? { pk } : {}), writeMeta };
|
|
617
|
+
}
|
|
618
|
+
// An absent `where` port is an UNCONDITIONAL update — `UPDATE … SET …` with no tail. The port
|
|
619
|
+
// carries the structured `{arr:[…]}` member list and is lowered INLINE here; nothing appends a
|
|
620
|
+
// WHERE afterwards.
|
|
621
|
+
const where = lowerWherePort(ports, 'Update', dialect);
|
|
425
622
|
// v1 `DBModel._update` emits `<c> = ?::<sqlCast>` PER COLUMN on Postgres (skipping timestamp/date).
|
|
426
623
|
const setClauses = setCols.map((c) => `${c} = ${castPlaceholder(dialect, sqlCastMap, c)}`).join(', ');
|
|
427
|
-
const
|
|
428
|
-
|
|
624
|
+
const whereTail = where.sql === '' ? '' : ` WHERE ${where.sql}`;
|
|
625
|
+
const sql = `UPDATE ${table} SET ${setClauses}${whereTail}${returningTail(ports)}`;
|
|
626
|
+
// The `?`s bind the SET columns (in setCols order) then the WHERE columns (`where.columns`).
|
|
627
|
+
// `pk` rides along (the SAME pkPort SSoT the INSERT paths use) so a dialect without native
|
|
628
|
+
// RETURNING orders its recovered rows by the real key — without it a multi-row UPDATE…RETURNING
|
|
629
|
+
// comes back in the re-select's own order, which need not match PG's.
|
|
630
|
+
const writeMeta = { table, bindColumns: [...setCols, ...where.columns], returning: returningColumns(ports) };
|
|
631
|
+
const pk = pkPort(ports);
|
|
632
|
+
return { sql, params: [...setCols.map((c) => set[c]), ...where.params], ...(pk !== undefined ? { pk } : {}), writeMeta };
|
|
429
633
|
}
|
|
430
634
|
case 'Delete': {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
635
|
+
// As Update: the `where` port is lowered inline. Absent ⇒ an unconditional `DELETE FROM t`.
|
|
636
|
+
const where = lowerWherePort(ports, 'Delete', dialect);
|
|
637
|
+
const sql = `DELETE FROM ${table}${where.sql === '' ? '' : ` WHERE ${where.sql}`}${returningTail(ports)}`;
|
|
638
|
+
// The `?`s bind the WHERE columns; a DELETE has no SET/VALUES params. `pk` rides along so the
|
|
639
|
+
// pre-image re-select is ordered by the real key (as Update above).
|
|
640
|
+
const writeMeta = { table, bindColumns: where.columns, returning: returningColumns(ports) };
|
|
641
|
+
const pk = pkPort(ports);
|
|
642
|
+
return { sql, params: where.params, ...(pk !== undefined ? { pk } : {}), writeMeta };
|
|
436
643
|
}
|
|
437
644
|
default:
|
|
438
645
|
throw new Error(`compileWriteNode: catalog component '${component}' has no write compile (SQL writes: Insert/Update/Delete)`);
|
|
@@ -669,40 +876,57 @@ function collectRefHeads(node, heads) {
|
|
|
669
876
|
for (const k of keys)
|
|
670
877
|
collectRefHeads(node[k], heads);
|
|
671
878
|
}
|
|
879
|
+
/** Coerce a `SqliteDb | ExecutionContext` argument to a ctx (raw driver ⇒ backward-compat wrapper). */
|
|
880
|
+
function asContext(dbOrCtx) {
|
|
881
|
+
return 'connectionFor' in dbOrCtx ? dbOrCtx : (0, exec_context_1.contextForDriver)(dbOrCtx);
|
|
882
|
+
}
|
|
672
883
|
/** bc evaluates ints to bigint; convert a rendered param to a driver-bindable value. */
|
|
673
|
-
function toDriverParam(v) {
|
|
884
|
+
function toDriverParam(v, dialect) {
|
|
674
885
|
if (typeof v === 'bigint') {
|
|
675
886
|
if (v >= BigInt(Number.MIN_SAFE_INTEGER) && v <= BigInt(Number.MAX_SAFE_INTEGER))
|
|
676
887
|
return Number(v);
|
|
677
888
|
return v;
|
|
678
889
|
}
|
|
679
|
-
// An emit payload evaluates to a plain object (`{obj:{…}}`); serialize it to the outbox JSON text
|
|
890
|
+
// An emit payload evaluates to a plain object (`{obj:{…}}`); serialize it to the outbox JSON text
|
|
891
|
+
// through the ONE JSON-param encoder, so a field that came off a read (a bc `int`, i.e. a BigInt that
|
|
892
|
+
// `JSON.stringify` refuses) is handled the same way the batch and array params handle it.
|
|
680
893
|
if (v !== null && typeof v === 'object' && !Array.isArray(v))
|
|
681
|
-
return
|
|
894
|
+
return (0, json_array_1.encodeJsonParam)(dialect, v);
|
|
682
895
|
return v;
|
|
683
896
|
}
|
|
684
897
|
/**
|
|
685
|
-
*
|
|
686
|
-
* param to a concrete value (bc `evaluateExpression`),
|
|
687
|
-
*
|
|
688
|
-
*
|
|
898
|
+
* Evaluate a statement's makeSQL op against the tx scope to RAW `?` SQL + driver-coerced params:
|
|
899
|
+
* evaluate each deferred Expression-IR param to a concrete value (bc `evaluateExpression`), assemble
|
|
900
|
+
* the concrete `makeSQL` (splice nested makeSQL), and coerce each param to the driver form
|
|
901
|
+
* ({@link toDriverParam}). Placeholder render (`?`→`$N`) is NOT done here — it is the transport leaf's
|
|
902
|
+
* job (`executeSQL`) so a tx body statement rides the SAME op-independent transport as every read/write.
|
|
903
|
+
* This is the single eval/assemble/coerce for both the sync leaf exec ({@link execStatement}) and the
|
|
904
|
+
* placeholder-rendered {@link renderStatement} (async path + golden export).
|
|
905
|
+
*/
|
|
906
|
+
function evalAssemble(op, scope, dialect) {
|
|
907
|
+
const concrete = op.params.map((p) => (0, runtime_1.evaluateExpression)(p, scope));
|
|
908
|
+
const assembled = (0, makesql_1.assembleMakeSQL)({ sql: op.sql, params: concrete });
|
|
909
|
+
return { sql: assembled.sql, params: assembled.params.map((p) => toDriverParam(p, dialect)) };
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Render a statement's makeSQL op against the tx scope to the dialect placeholder form ({@link
|
|
913
|
+
* evalAssemble} + `?`→`$N`). Used by the ASYNC tx path ({@link execStatementAsync}) and the golden
|
|
914
|
+
* export {@link renderTxStatement}; the SYNC path renders via the transport leaf ({@link execStatement}).
|
|
689
915
|
*/
|
|
690
916
|
function renderStatement(op, scope, dialect) {
|
|
691
|
-
const
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
917
|
+
const { sql, params } = evalAssemble(op, scope, dialect);
|
|
918
|
+
return { sql: (0, handler_1.renderPlaceholders)(sql, dialect), params };
|
|
919
|
+
}
|
|
920
|
+
function execStatement(ctx, op, scope, dialect) {
|
|
921
|
+
// A tx body statement rides the SAME op-independent transport as every read/write: the `executeSQL`
|
|
922
|
+
// leaf (placeholder render + param encode + central seam). The tx scope-eval/assemble/coerce is done
|
|
923
|
+
// first (`evalAssemble`); the leaf receives raw `?` SQL + concrete params. `write:true` ⇒ the tx-owned
|
|
924
|
+
// connection resolves via `connectionFor` (§3); a SELECT/RETURNING statement reads rows (`returning`
|
|
925
|
+
// ⇒ the row-returning seam), a bare write runs (the `[{changes,…}]` summary).
|
|
926
|
+
const { sql, params } = evalAssemble(op, scope, dialect);
|
|
699
927
|
const hasReturn = /\bselect\b/i.test(sql.slice(0, 8)) || /\breturning\b/i.test(sql);
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
return { rows, changes: rows.length };
|
|
703
|
-
}
|
|
704
|
-
const info = stmt.run(...params);
|
|
705
|
-
return { rows: [], changes: info.changes };
|
|
928
|
+
const out = (0, leaves_1.executeSQL)({ sql, params, write: true, returning: hasReturn, bigint: false }, { exec: ctx, dialect });
|
|
929
|
+
return hasReturn ? { rows: out, changes: out.length } : { rows: [], changes: Number(out[0]?.changes ?? 0) };
|
|
706
930
|
}
|
|
707
931
|
function gateShortCircuit(gate, result) {
|
|
708
932
|
switch (gate) {
|
|
@@ -719,9 +943,17 @@ function gateShortCircuit(gate, result) {
|
|
|
719
943
|
throw new Error(`scp write: unknown gate rule '${String(gate)}'`);
|
|
720
944
|
}
|
|
721
945
|
}
|
|
722
|
-
/**
|
|
946
|
+
/**
|
|
947
|
+
* Execute a derived {@link TransactionPlan} as ONE real SQLite transaction with gate-first
|
|
948
|
+
* short-circuit. Accepts a raw {@link SqliteDb} (wrapped via {@link contextForDriver}) or a full
|
|
949
|
+
* {@link ExecutionContext}. The transaction derives a tx-scoped ctx (`withConnection(conn, true)`)
|
|
950
|
+
* that PINS one connection so BEGIN, every body statement, and COMMIT/ROLLBACK all run on the SAME
|
|
951
|
+
* connection (§3, per-execution ownership). For the single-DB SQLite driver the pinned connection
|
|
952
|
+
* is the sole connection; the ownership shows its teeth on the pooled async path
|
|
953
|
+
* ({@link import('../exec-context').withTransactionAsync}), which this mirrors.
|
|
954
|
+
*/
|
|
723
955
|
function executeTransaction(db, plan, input, dialect = 'sqlite') {
|
|
724
|
-
db
|
|
956
|
+
const outer = asContext(db);
|
|
725
957
|
const executed = [];
|
|
726
958
|
const scope = { ...input };
|
|
727
959
|
let entity = null;
|
|
@@ -732,38 +964,36 @@ function executeTransaction(db, plan, input, dialect = 'sqlite') {
|
|
|
732
964
|
const isBatch = plan.entityFrom === null &&
|
|
733
965
|
plan.statements.every((s) => s.gate === undefined && s.binds === undefined && s.role === 'body');
|
|
734
966
|
const returnedRows = [];
|
|
967
|
+
// The tx boundary is the sync `ctx.transaction` ({@link withTransactionSync}: pin conn → BEGIN →
|
|
968
|
+
// body → COMMIT/ROLLBACK, inside `runInTransactionScope`). The body runs the derived plan's
|
|
969
|
+
// statements in order via the `executeSQL` transport leaf ({@link execStatement}); a gate
|
|
970
|
+
// short-circuit signals `{commit:false}` so the boundary ROLLBACKs but still returns the reason.
|
|
735
971
|
try {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
972
|
+
return (0, exec_context_1.withTransactionSync)(outer, (ctx) => {
|
|
973
|
+
for (const stmt of plan.statements) {
|
|
974
|
+
const result = execStatement(ctx, stmt.op, scope, dialect);
|
|
975
|
+
executed.push(stmt.id);
|
|
976
|
+
if (stmt.gate !== undefined) {
|
|
977
|
+
const reason = gateShortCircuit(stmt.gate, result);
|
|
978
|
+
if (reason !== null) {
|
|
979
|
+
return { commit: false, value: { committed: false, shortCircuit: { statementId: stmt.id, reason }, entity: null, executed } };
|
|
980
|
+
}
|
|
744
981
|
}
|
|
982
|
+
if (stmt.id === plan.entityFrom) {
|
|
983
|
+
entity = result.rows.length > 0 ? result.rows[0] : null;
|
|
984
|
+
if (entity !== null)
|
|
985
|
+
scope[writes_1.ENTITY_ROOT] = entity;
|
|
986
|
+
}
|
|
987
|
+
if (stmt.binds !== undefined && result.rows.length > 0) {
|
|
988
|
+
scope[stmt.binds] = result.rows[0];
|
|
989
|
+
}
|
|
990
|
+
if (isBatch && stmt.role === 'body' && result.rows.length > 0)
|
|
991
|
+
returnedRows.push(result.rows);
|
|
745
992
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
if (entity !== null)
|
|
749
|
-
scope[writes_1.ENTITY_ROOT] = entity;
|
|
750
|
-
}
|
|
751
|
-
if (stmt.binds !== undefined && result.rows.length > 0) {
|
|
752
|
-
scope[stmt.binds] = result.rows[0];
|
|
753
|
-
}
|
|
754
|
-
if (isBatch && stmt.role === 'body' && result.rows.length > 0)
|
|
755
|
-
returnedRows.push(result.rows);
|
|
756
|
-
}
|
|
757
|
-
db.prepare('COMMIT').run();
|
|
758
|
-
return { committed: true, entity, executed, ...(returnedRows.length > 0 ? { returnedRows } : {}) };
|
|
993
|
+
return { commit: true, value: { committed: true, entity, executed, ...(returnedRows.length > 0 ? { returnedRows } : {}) } };
|
|
994
|
+
}, dialect);
|
|
759
995
|
}
|
|
760
996
|
catch (e) {
|
|
761
|
-
try {
|
|
762
|
-
db.prepare('ROLLBACK').run();
|
|
763
|
-
}
|
|
764
|
-
catch {
|
|
765
|
-
/* ROLLBACK best-effort; surface the original failure below */
|
|
766
|
-
}
|
|
767
997
|
throw (0, errors_1.mapSqliteError)(e);
|
|
768
998
|
}
|
|
769
999
|
}
|
|
@@ -785,4 +1015,128 @@ function countingDriver(db) {
|
|
|
785
1015
|
function renderTxStatement(op, scope, dialect = 'sqlite') {
|
|
786
1016
|
return renderStatement(op, scope, dialect);
|
|
787
1017
|
}
|
|
1018
|
+
// ============================================================================
|
|
1019
|
+
// Phase A (#75) — ASYNC transaction runtime (live PG / MySQL) with PER-EXECUTION
|
|
1020
|
+
// CONNECTION OWNERSHIP. The async twin of `executeTransaction`: it runs the derived
|
|
1021
|
+
// TransactionPlan through `withTransactionAsync`, which acquires ONE pooled connection,
|
|
1022
|
+
// pins it in the ALS ctx, and runs BEGIN…COMMIT on it. Concurrent transactions each own a
|
|
1023
|
+
// DISTINCT connection ⇒ isolated (no shared-slot cross-talk). This is the production async
|
|
1024
|
+
// write-tx path the concurrent-tx isolation test exercises.
|
|
1025
|
+
// ============================================================================
|
|
1026
|
+
/**
|
|
1027
|
+
* Run ONE rendered tx statement through the async seam — the SAME op-independent `executeSQL`
|
|
1028
|
+
* transport leaf every read/write rides. Returns `{ rows, changes }`.
|
|
1029
|
+
*
|
|
1030
|
+
* MySQL's missing RETURNING is NOT handled here: it is a property of the CONNECTION, and the mysql
|
|
1031
|
+
* connection adapter ({@link import('./pool-executor').mysqlConnectionPool}) owns it — so the mode-2
|
|
1032
|
+
* plan executor and a generated (codegen) write reach the identical write→re-select through one
|
|
1033
|
+
* seam, and cannot disagree about the rows a write returns.
|
|
1034
|
+
*/
|
|
1035
|
+
async function execStatementAsync(ctx, op, scope, dialect) {
|
|
1036
|
+
// Eval value-specs + assemble to RAW `?` SQL + coerced params (the SSoT `evalAssemble`, shared with
|
|
1037
|
+
// the sync path). The leaf renders `?`→`$N` per dialect after the final SQL is assembled.
|
|
1038
|
+
const { sql, params } = evalAssemble(op, scope, dialect);
|
|
1039
|
+
// A SELECT/RETURNING reads rows; a bare write returns the `[{changes,…}]` summary.
|
|
1040
|
+
const hasReturn = /\bselect\b/i.test(sql.slice(0, 8)) || /\breturning\b/i.test(sql);
|
|
1041
|
+
const out = await (0, leaves_1.executeSQLAsync)({ sql, params, write: true, returning: hasReturn, bigint: false }, { execAsync: ctx, dialect });
|
|
1042
|
+
return hasReturn ? { rows: out, changes: out.length } : { rows: [], changes: Number(out[0]?.changes ?? 0) };
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Execute a derived {@link TransactionPlan} on a live PG / MySQL connection with gate-first
|
|
1046
|
+
* short-circuit and **per-execution connection ownership** (§3). The live-DB WRITE entry (#86).
|
|
1047
|
+
*
|
|
1048
|
+
* ## Ambient-tx JOIN vs. its own envelope (the #86 core)
|
|
1049
|
+
*
|
|
1050
|
+
* `withTransactionAsync` (:495) decides the envelope:
|
|
1051
|
+
* - **inside a user `transaction(fn)`** (an outer connection is pinned in the ALS) → the write
|
|
1052
|
+
* JOINS the outer: its statements run on the outer's owned connection with NO new BEGIN/COMMIT,
|
|
1053
|
+
* so N writes in one boundary are ONE physical transaction (one BEGIN, one COMMIT, one conn);
|
|
1054
|
+
* - **outside any transaction** → it opens its OWN BEGIN…COMMIT on a freshly-acquired owned
|
|
1055
|
+
* connection (the per-execution auto-tx; concurrent calls each own a DISTINCT connection ⇒
|
|
1056
|
+
* isolated).
|
|
1057
|
+
*
|
|
1058
|
+
* ## write=tx guard (#86, wired here — fires at runtime, not a standalone helper)
|
|
1059
|
+
*
|
|
1060
|
+
* With `options.guard` (DEFAULT true), a write with NO ambient user tx is REJECTED via {@link
|
|
1061
|
+
* checkWriteAllowed} BEFORE any SQL: `WriteOutsideTransactionError` (no active tx) /
|
|
1062
|
+
* `WriteInReadOnlyContextError` (read-only scope). The check runs at ENTRY — before
|
|
1063
|
+
* `withTransactionAsync` would open the write's own envelope — so it sees the CALLER's scope, exactly
|
|
1064
|
+
* mirroring v1 `DBModel._checkWriteAllowed` (:886, called at the public write entry, not the plan
|
|
1065
|
+
* executor). Inside a `transaction(fn)` the ambient marker is set ⇒ the guard passes and the write
|
|
1066
|
+
* joins. The structured {@link TransactionResult} is identical to the sync {@link executeTransaction}.
|
|
1067
|
+
*/
|
|
1068
|
+
function executeTransactionAsync(ctx, plan, input, dialect = 'sqlite', options = {}) {
|
|
1069
|
+
// write=tx guard (#86), enforced at ENTRY so it sees the CALLER's scope — a write inside a user
|
|
1070
|
+
// `transaction(fn)` has the ambient "inside a tx" marker set (⇒ passes + JOINS the outer); a bare
|
|
1071
|
+
// write outside any boundary has no marker (⇒ WriteOutsideTransactionError). Reject as a REJECTED
|
|
1072
|
+
// promise (never a synchronous throw) since this entry is async. Tx-control statements the runtime
|
|
1073
|
+
// itself issues (BEGIN/COMMIT) never pass through here — only data-write plans do.
|
|
1074
|
+
if (options.guard !== false) {
|
|
1075
|
+
// Run the guard, then the plan, as a REJECTED promise on failure (never a synchronous throw).
|
|
1076
|
+
// `checkWriteAllowed` mirrors v1 ordering (:886): read-only is rejected FIRST
|
|
1077
|
+
// (`WriteInReadOnlyContextError`), then a missing active tx (`WriteOutsideTransactionError`).
|
|
1078
|
+
// Inside a user `transaction(fn)` the ambient marker is set ⇒ neither fires and the write JOINS
|
|
1079
|
+
// the outer; outside any boundary the no-active-tx branch throws.
|
|
1080
|
+
return Promise.resolve().then(() => {
|
|
1081
|
+
(0, tx_options_1.checkWriteAllowed)('WRITE', plan.statements[0]?.id);
|
|
1082
|
+
return runTransactionPlanAsync(ctx, plan, input, dialect, options);
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
return runTransactionPlanAsync(ctx, plan, input, dialect, options);
|
|
1086
|
+
}
|
|
1087
|
+
/** The plan-executor body of {@link executeTransactionAsync}, split so the guard runs at entry. */
|
|
1088
|
+
function runTransactionPlanAsync(ctx, plan, input, dialect, options) {
|
|
1089
|
+
const isBatch = plan.entityFrom === null &&
|
|
1090
|
+
plan.statements.every((s) => s.gate === undefined && s.binds === undefined && s.role === 'body');
|
|
1091
|
+
return (0, exec_context_1.withTransactionAsync)(ctx, async (txCtx) => {
|
|
1092
|
+
const executed = [];
|
|
1093
|
+
const scope = { ...input };
|
|
1094
|
+
let entity = null;
|
|
1095
|
+
const returnedRows = [];
|
|
1096
|
+
for (const stmt of plan.statements) {
|
|
1097
|
+
const result = await execStatementAsync(txCtx, stmt.op, scope, dialect);
|
|
1098
|
+
executed.push(stmt.id);
|
|
1099
|
+
if (stmt.gate !== undefined) {
|
|
1100
|
+
const reason = gateShortCircuit(stmt.gate, result);
|
|
1101
|
+
if (reason !== null) {
|
|
1102
|
+
// A gate short-circuit ROLLBACKs the whole tx (atomicity): throw the sentinel so
|
|
1103
|
+
// `withTransactionAsync` runs ROLLBACK on the owned connection, then translate to the
|
|
1104
|
+
// structured non-committed result at the boundary.
|
|
1105
|
+
throw new GateShortCircuit(stmt.id, reason, executed);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
if (stmt.id === plan.entityFrom) {
|
|
1109
|
+
entity = result.rows.length > 0 ? result.rows[0] : null;
|
|
1110
|
+
if (entity !== null)
|
|
1111
|
+
scope[writes_1.ENTITY_ROOT] = entity;
|
|
1112
|
+
}
|
|
1113
|
+
if (stmt.binds !== undefined && result.rows.length > 0) {
|
|
1114
|
+
scope[stmt.binds] = result.rows[0];
|
|
1115
|
+
}
|
|
1116
|
+
if (isBatch && stmt.role === 'body' && result.rows.length > 0)
|
|
1117
|
+
returnedRows.push(result.rows);
|
|
1118
|
+
}
|
|
1119
|
+
return { committed: true, entity, executed, ...(returnedRows.length > 0 ? { returnedRows } : {}) };
|
|
1120
|
+
}, options, dialect === 'sqlite' ? 'postgres' : dialect, connection_errors_1.isConnectionError).catch((e) => {
|
|
1121
|
+
// A gate short-circuit is NOT a failure — it ROLLBACKs and reports `committed:false`. Any other
|
|
1122
|
+
// error is a real driver failure (already rolled back by withTransactionAsync) → re-surface.
|
|
1123
|
+
if (e instanceof GateShortCircuit) {
|
|
1124
|
+
return { committed: false, shortCircuit: { statementId: e.statementId, reason: e.reason }, entity: null, executed: e.executed };
|
|
1125
|
+
}
|
|
1126
|
+
throw (0, errors_1.mapSqliteError)(e);
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
/** Internal sentinel: a gate short-circuit inside an async tx (ROLLBACK, then report non-committed). */
|
|
1130
|
+
class GateShortCircuit extends Error {
|
|
1131
|
+
statementId;
|
|
1132
|
+
reason;
|
|
1133
|
+
executed;
|
|
1134
|
+
constructor(statementId, reason, executed) {
|
|
1135
|
+
super(`scp write: gate short-circuit at '${statementId}' (${reason})`);
|
|
1136
|
+
this.statementId = statementId;
|
|
1137
|
+
this.reason = reason;
|
|
1138
|
+
this.executed = executed;
|
|
1139
|
+
this.name = 'GateShortCircuit';
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
788
1142
|
//# sourceMappingURL=tx.js.map
|