litedbmodel 2.2.2 → 2.2.4
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 +2 -1
- package/dist/DBModel.d.ts.map +1 -1
- package/dist/DBModel.js +3 -0
- package/dist/DBModel.js.map +1 -1
- package/dist/Middleware.d.ts +19 -2
- package/dist/Middleware.d.ts.map +1 -1
- package/dist/Middleware.js +1 -0
- package/dist/Middleware.js.map +1 -1
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +8 -0
- package/dist/decorators.js.map +1 -1
- package/dist/drivers/MysqlHelper.d.ts +1 -1
- package/dist/drivers/MysqlHelper.d.ts.map +1 -1
- package/dist/drivers/MysqlHelper.js.map +1 -1
- package/dist/drivers/PostgresHelper.d.ts +1 -1
- package/dist/drivers/PostgresHelper.d.ts.map +1 -1
- package/dist/drivers/PostgresHelper.js.map +1 -1
- package/dist/drivers/SqliteHelper.d.ts +1 -1
- package/dist/drivers/SqliteHelper.d.ts.map +1 -1
- package/dist/drivers/SqliteHelper.js.map +1 -1
- package/dist/index.cjs +105 -74
- package/dist/scp/connection-routing.d.ts +38 -1
- package/dist/scp/connection-routing.d.ts.map +1 -1
- package/dist/scp/connection-routing.js +64 -5
- package/dist/scp/connection-routing.js.map +1 -1
- package/dist/scp/dbmodel-runtime.d.ts +2 -1
- package/dist/scp/dbmodel-runtime.d.ts.map +1 -1
- package/dist/scp/dbmodel-runtime.js +2 -1
- package/dist/scp/dbmodel-runtime.js.map +1 -1
- package/dist/scp/decorator-adapter.d.ts +22 -0
- package/dist/scp/decorator-adapter.d.ts.map +1 -1
- package/dist/scp/decorator-adapter.js +26 -0
- package/dist/scp/decorator-adapter.js.map +1 -1
- package/dist/scp/emit/emitter.d.ts +21 -10
- package/dist/scp/emit/emitter.d.ts.map +1 -1
- package/dist/scp/emit/emitter.js +119 -51
- package/dist/scp/emit/emitter.js.map +1 -1
- package/dist/scp/emit/endpoint.d.ts +3 -8
- package/dist/scp/emit/endpoint.d.ts.map +1 -1
- package/dist/scp/exec-context.d.ts +39 -17
- package/dist/scp/exec-context.d.ts.map +1 -1
- package/dist/scp/exec-context.js +69 -35
- package/dist/scp/exec-context.js.map +1 -1
- package/dist/scp/grouping.d.ts.map +1 -1
- package/dist/scp/grouping.js +0 -0
- package/dist/scp/grouping.js.map +1 -1
- package/dist/scp/index.cjs +257 -670
- package/dist/scp/index.d.ts +6 -11
- package/dist/scp/index.d.ts.map +1 -1
- package/dist/scp/index.js +10 -28
- package/dist/scp/index.js.map +1 -1
- package/dist/scp/index.mjs +257 -658
- package/dist/scp/leaf-transport.d.ts +127 -39
- package/dist/scp/leaf-transport.d.ts.map +1 -1
- package/dist/scp/leaf-transport.js +30 -24
- package/dist/scp/leaf-transport.js.map +1 -1
- package/dist/scp/leaves.d.ts +57 -20
- package/dist/scp/leaves.d.ts.map +1 -1
- package/dist/scp/leaves.js +224 -65
- package/dist/scp/leaves.js.map +1 -1
- package/dist/scp/makesql/compile-select.d.ts +28 -1
- package/dist/scp/makesql/compile-select.d.ts.map +1 -1
- package/dist/scp/makesql/compile-select.js +19 -17
- package/dist/scp/makesql/compile-select.js.map +1 -1
- package/dist/scp/makesql/index.d.ts +2 -2
- package/dist/scp/makesql/index.d.ts.map +1 -1
- package/dist/scp/makesql/index.js +4 -6
- package/dist/scp/makesql/index.js.map +1 -1
- package/dist/scp/makesql/tx.d.ts +43 -89
- package/dist/scp/makesql/tx.d.ts.map +1 -1
- package/dist/scp/makesql/tx.js +45 -496
- package/dist/scp/makesql/tx.js.map +1 -1
- package/dist/scp/makesql/writeouttype.d.ts +5 -7
- package/dist/scp/makesql/writeouttype.d.ts.map +1 -1
- package/dist/scp/makesql/writeouttype.js +13 -19
- package/dist/scp/makesql/writeouttype.js.map +1 -1
- package/dist/scp/middleware.d.ts +4 -4
- package/dist/scp/middleware.d.ts.map +1 -1
- package/dist/scp/middleware.js +2 -2
- package/dist/scp/middleware.js.map +1 -1
- package/dist/scp/relation.d.ts +31 -12
- package/dist/scp/relation.d.ts.map +1 -1
- package/dist/scp/relation.js +16 -6
- package/dist/scp/relation.js.map +1 -1
- package/dist/scp/typed-object.d.ts +23 -19
- package/dist/scp/typed-object.d.ts.map +1 -1
- package/dist/scp/typed-object.js +24 -30
- package/dist/scp/typed-object.js.map +1 -1
- package/dist/scp/write-bundle.d.ts +21 -70
- package/dist/scp/write-bundle.d.ts.map +1 -1
- package/dist/scp/write-bundle.js +16 -80
- package/dist/scp/write-bundle.js.map +1 -1
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/docs/BENCHMARK.md +26 -22
- package/docs/COMPARISON.md +8 -4
- package/docs/api/README.md +1 -1
- package/docs/api/classes/Conditions.md +10 -10
- package/docs/api/classes/DBHandler.md +17 -17
- package/docs/api/classes/DBModel.md +111 -104
- package/docs/api/classes/LimitExceededError.md +8 -8
- package/docs/api/classes/Middleware.md +18 -18
- package/docs/api/classes/SqlRaw.md +6 -6
- package/docs/api/classes/SqlRef.md +7 -7
- package/docs/api/classes/Values.md +6 -6
- package/docs/api/classes/WriteInReadOnlyContextError.md +5 -5
- package/docs/api/classes/WriteOutsideTransactionError.md +5 -5
- package/docs/api/functions/belongsTo.md +2 -2
- package/docs/api/functions/closeAllPools.md +2 -2
- package/docs/api/functions/condsToRecord.md +2 -2
- package/docs/api/functions/createMiddleware.md +2 -2
- package/docs/api/functions/createPostgresDriver.md +2 -2
- package/docs/api/functions/createSqliteDriver.md +2 -2
- package/docs/api/functions/dbCast.md +3 -3
- package/docs/api/functions/dbCastIn.md +2 -2
- package/docs/api/functions/dbDynamic.md +3 -3
- package/docs/api/functions/dbFalse.md +2 -2
- package/docs/api/functions/dbImmediate.md +2 -2
- package/docs/api/functions/dbIn.md +2 -2
- package/docs/api/functions/dbNotNull.md +2 -2
- package/docs/api/functions/dbNow.md +2 -2
- package/docs/api/functions/dbNull.md +2 -2
- package/docs/api/functions/dbRaw.md +2 -2
- package/docs/api/functions/dbTrue.md +2 -2
- package/docs/api/functions/dbUuid.md +3 -3
- package/docs/api/functions/dbUuidIn.md +2 -2
- package/docs/api/functions/formatLocalDate.md +25 -0
- package/docs/api/functions/formatUTCDate.md +25 -0
- package/docs/api/functions/getDBConfig.md +2 -2
- package/docs/api/functions/getDBHandler.md +2 -2
- package/docs/api/functions/getTransactionConnection.md +2 -2
- package/docs/api/functions/getTransactionContext.md +2 -2
- package/docs/api/functions/hasMany.md +2 -2
- package/docs/api/functions/hasOne.md +2 -2
- package/docs/api/functions/initDBHandler.md +2 -2
- package/docs/api/functions/isAnySqlFragment.md +2 -2
- package/docs/api/functions/isConnectionError.md +26 -0
- package/docs/api/functions/isSqlCondition.md +2 -2
- package/docs/api/functions/isSqlFragment.md +2 -2
- package/docs/api/functions/isSqlRaw.md +2 -2
- package/docs/api/functions/isSqlRef.md +2 -2
- package/docs/api/functions/isSqlTypedFragment.md +2 -2
- package/docs/api/functions/model.md +6 -42
- package/docs/api/functions/parentRef.md +2 -2
- package/docs/api/functions/sql.md +7 -7
- package/docs/api/globals.md +57 -22
- package/docs/api/interfaces/Column.md +31 -31
- package/docs/api/interfaces/ColumnOptions.md +5 -4
- package/docs/api/interfaces/CreatedMiddlewareClass.md +7 -7
- package/docs/api/interfaces/DBConfig.md +13 -11
- package/docs/api/interfaces/DBConfigOptions.md +8 -8
- package/docs/api/interfaces/DeleteOptions.md +4 -4
- package/docs/api/interfaces/InsertOptions.md +8 -8
- package/docs/api/interfaces/LimitConfig.md +4 -4
- package/docs/api/interfaces/MiddlewareConfig.md +15 -15
- package/docs/api/interfaces/ModelOptions.md +8 -7
- package/docs/api/interfaces/PkeyResult.md +4 -4
- package/docs/api/interfaces/SelectOptions.md +17 -16
- package/docs/api/interfaces/SqlCondition.md +6 -6
- package/docs/api/interfaces/SqlFragment.md +5 -5
- package/docs/api/interfaces/SqlTypedFragment.md +7 -7
- package/docs/api/interfaces/TransactionOptions.md +7 -7
- package/docs/api/interfaces/UpdateManyOptions.md +4 -4
- package/docs/api/interfaces/UpdateOptions.md +4 -4
- package/docs/architecture.md +452 -0
- package/docs/benchmark-chart.svg +225 -249
- package/docs/design/phase-a-execution-context.md +2 -0
- package/package.json +20 -5
- package/dist/scp/writes.d.ts +0 -251
- package/dist/scp/writes.d.ts.map +0 -1
- package/dist/scp/writes.js +0 -176
- package/dist/scp/writes.js.map +0 -1
- package/docs/proposal/contracts-architecture-feasibility.md +0 -328
- package/docs/proposal/contracts-architecture.md +0 -579
- package/docs/proposal/litedbmodel-v2-scp-architecture.md +0 -433
- package/docs/proposal/sql-dynamic-expansion-spec.md +0 -175
- package/docs/proposal/v0-coverage-matrix.md +0 -157
- package/docs/proposal/v2-completeness-plan.md +0 -90
- package/docs/proposal/v2-sql-parity-checklist.md +0 -162
package/dist/scp/index.mjs
CHANGED
|
@@ -1768,30 +1768,32 @@ function lockTail(opts) {
|
|
|
1768
1768
|
return "";
|
|
1769
1769
|
}
|
|
1770
1770
|
function compileSelect(desc) {
|
|
1771
|
-
const
|
|
1771
|
+
const headParams = [];
|
|
1772
1772
|
const selectCols = desc.select || (desc.selectColumn ?? "*");
|
|
1773
1773
|
const formatter = formatterFor(desc.dialect);
|
|
1774
|
-
if (desc.cte?.params && desc.cte.params.length > 0)
|
|
1775
|
-
if (desc.joinParams && desc.joinParams.length > 0)
|
|
1776
|
-
const whereClause2 = conditionsFor(desc.conditions ?? {}, desc.dialect).compile(
|
|
1777
|
-
let
|
|
1778
|
-
if (desc.cte)
|
|
1779
|
-
|
|
1780
|
-
if (desc.join)
|
|
1781
|
-
if (whereClause2)
|
|
1782
|
-
|
|
1774
|
+
if (desc.cte?.params && desc.cte.params.length > 0) headParams.push(...desc.cte.params);
|
|
1775
|
+
if (desc.joinParams && desc.joinParams.length > 0) headParams.push(...desc.joinParams);
|
|
1776
|
+
const whereClause2 = conditionsFor(desc.conditions ?? {}, desc.dialect).compile(headParams, formatter);
|
|
1777
|
+
let head = "";
|
|
1778
|
+
if (desc.cte) head = `WITH ${desc.cte.name} AS (${desc.cte.sql}) `;
|
|
1779
|
+
head += `SELECT ${selectCols} FROM ${desc.tableName}`;
|
|
1780
|
+
if (desc.join) head += ` ${desc.join}`;
|
|
1781
|
+
if (whereClause2) head += ` WHERE ${whereClause2}`;
|
|
1782
|
+
let tail = "";
|
|
1783
|
+
if (desc.group) tail += ` GROUP BY ${desc.group}`;
|
|
1783
1784
|
const orderClause = typeof desc.order === "string" ? desc.order : orderToString(desc.order);
|
|
1784
|
-
if (orderClause)
|
|
1785
|
+
if (orderClause) tail += ` ORDER BY ${orderClause}`;
|
|
1786
|
+
const tailParams = [];
|
|
1785
1787
|
const count = (c) => {
|
|
1786
1788
|
if (!isBoundCount(c)) return String(c);
|
|
1787
|
-
|
|
1789
|
+
tailParams.push(c.bind);
|
|
1788
1790
|
return "?";
|
|
1789
1791
|
};
|
|
1790
|
-
if (desc.limit !== void 0)
|
|
1791
|
-
if (desc.offset !== void 0)
|
|
1792
|
-
|
|
1793
|
-
if (desc.append)
|
|
1794
|
-
return { sql:
|
|
1792
|
+
if (desc.limit !== void 0) tail += ` LIMIT ${count(desc.limit)}`;
|
|
1793
|
+
if (desc.offset !== void 0) tail += ` OFFSET ${count(desc.offset)}`;
|
|
1794
|
+
tail += lockTail(desc);
|
|
1795
|
+
if (desc.append) tail += ` ${desc.append}`;
|
|
1796
|
+
return { sql: head + tail, params: [...headParams, ...tailParams], head, headParams, tail, tailParams, lead: whereClause2 ? "AND" : "WHERE" };
|
|
1795
1797
|
}
|
|
1796
1798
|
|
|
1797
1799
|
// src/scp/makesql/compile-relation.ts
|
|
@@ -3980,6 +3982,24 @@ function readerWriterPair(reader, writer) {
|
|
|
3980
3982
|
return { reader, writer };
|
|
3981
3983
|
}
|
|
3982
3984
|
var DEFAULT_CONNECTION = "default";
|
|
3985
|
+
function assertRoutableNamedDb(db, contextDescription) {
|
|
3986
|
+
if (effectiveConnection(db) === DEFAULT_CONNECTION) return;
|
|
3987
|
+
throw new Error(
|
|
3988
|
+
`scp connection routing: a statement names connection '${db}', but it is executing on ${contextDescription} \u2014 there is no connection registry to resolve the name against. Build the context from a RoutingConfig (setConfig/ConnectionRegistry), or drop the connection tag on the model.`
|
|
3989
|
+
);
|
|
3990
|
+
}
|
|
3991
|
+
function effectiveConnection(db) {
|
|
3992
|
+
return db === void 0 || db === null ? DEFAULT_CONNECTION : db;
|
|
3993
|
+
}
|
|
3994
|
+
function assertTxDbAgrees(db, txDb) {
|
|
3995
|
+
if (db === void 0 || db === null) return;
|
|
3996
|
+
const want = effectiveConnection(db);
|
|
3997
|
+
const open = effectiveConnection(txDb);
|
|
3998
|
+
if (want === open) return;
|
|
3999
|
+
throw new Error(
|
|
4000
|
+
`scp connection routing: a statement names connection '${want}', but it is executing inside a transaction opened on '${open}' \u2014 a transaction is ONE connection on ONE database, so the two cannot both be honored. Open the transaction on '${want}', or issue the statement outside it.`
|
|
4001
|
+
);
|
|
4002
|
+
}
|
|
3983
4003
|
var ConnectionRegistry = class _ConnectionRegistry {
|
|
3984
4004
|
/** name → reader/writer pools. */
|
|
3985
4005
|
connections;
|
|
@@ -4000,7 +4020,7 @@ var ConnectionRegistry = class _ConnectionRegistry {
|
|
|
4000
4020
|
}
|
|
4001
4021
|
/** The reader/writer pair for `name` (or {@link DEFAULT_CONNECTION} when `undefined`). Loud on a missing name. */
|
|
4002
4022
|
pairFor(name) {
|
|
4003
|
-
const key = name
|
|
4023
|
+
const key = effectiveConnection(name);
|
|
4004
4024
|
const pair = this.connections.get(key);
|
|
4005
4025
|
if (pair === void 0) {
|
|
4006
4026
|
const known = [...this.connections.keys()].map((k) => `'${k}'`).join(", ");
|
|
@@ -4048,7 +4068,13 @@ function withWriter(fn) {
|
|
|
4048
4068
|
return writerScopeStore.run(true, () => withReadOnlyAsync(fn));
|
|
4049
4069
|
}
|
|
4050
4070
|
var WriterStickyClock = class {
|
|
4051
|
-
|
|
4071
|
+
/**
|
|
4072
|
+
* `null` until the first `mark()` — absence, NOT a value sentinel. The injectable clocks legitimately
|
|
4073
|
+
* return 0 (a monotonic clock right after process start; rust's `SystemClock` does), so encoding
|
|
4074
|
+
* "never marked" as the value 0 would mis-classify a `mark()` at clock t=0 as unmarked and leak the
|
|
4075
|
+
* read-your-writes read to the reader replica. Absence must be distinct from the value 0.
|
|
4076
|
+
*/
|
|
4077
|
+
lastWriteAt = null;
|
|
4052
4078
|
enabled;
|
|
4053
4079
|
stickyDurationMs;
|
|
4054
4080
|
/** Injectable clock (tests advance it); defaults to `Date.now`. */
|
|
@@ -4064,12 +4090,12 @@ var WriterStickyClock = class {
|
|
|
4064
4090
|
}
|
|
4065
4091
|
/** Is a read currently sticky-to-writer (within `writerStickyDuration` of the last write)? */
|
|
4066
4092
|
isSticky() {
|
|
4067
|
-
if (!this.enabled || this.lastWriteAt ===
|
|
4093
|
+
if (!this.enabled || this.lastWriteAt === null) return false;
|
|
4068
4094
|
return this.now() - this.lastWriteAt < this.stickyDurationMs;
|
|
4069
4095
|
}
|
|
4070
4096
|
/** Reset the clock (e.g. between tests / on `closeAllPools`). */
|
|
4071
4097
|
reset() {
|
|
4072
|
-
this.lastWriteAt =
|
|
4098
|
+
this.lastWriteAt = null;
|
|
4073
4099
|
}
|
|
4074
4100
|
};
|
|
4075
4101
|
function resolvePool(intent, routing) {
|
|
@@ -4418,15 +4444,20 @@ function connectionForDriver(driver) {
|
|
|
4418
4444
|
var BasicContext = class _BasicContext {
|
|
4419
4445
|
middleware;
|
|
4420
4446
|
base;
|
|
4421
|
-
/** The pinned tx connection (present ⇒ this is a tx-scoped ctx; every statement uses it). */
|
|
4447
|
+
/** The pinned tx connection (present ⇒ this is a tx-scoped ctx; every statement of the tx's own database (an unnamed one, or one naming that database) uses it). */
|
|
4422
4448
|
pinned;
|
|
4423
4449
|
constructor(base, middleware, pinned) {
|
|
4424
4450
|
this.base = base;
|
|
4425
4451
|
this.middleware = middleware;
|
|
4426
4452
|
this.pinned = pinned;
|
|
4427
4453
|
}
|
|
4428
|
-
connectionFor(
|
|
4429
|
-
|
|
4454
|
+
connectionFor(intent) {
|
|
4455
|
+
if (this.pinned !== null) {
|
|
4456
|
+
assertTxDbAgrees(intent.db, null);
|
|
4457
|
+
return this.pinned;
|
|
4458
|
+
}
|
|
4459
|
+
assertRoutableNamedDb(intent.db, "a single-connection (non-routed) execution context");
|
|
4460
|
+
return this.base;
|
|
4430
4461
|
}
|
|
4431
4462
|
withConnection(conn, tx) {
|
|
4432
4463
|
return new _BasicContext(this.base, this.middleware, tx ? conn : null);
|
|
@@ -4464,7 +4495,10 @@ var PooledAsyncContext = class {
|
|
|
4464
4495
|
}
|
|
4465
4496
|
connectionFor(intent) {
|
|
4466
4497
|
const pinned = asyncCtxStore.getStore();
|
|
4467
|
-
if (pinned !== void 0)
|
|
4498
|
+
if (pinned !== void 0) {
|
|
4499
|
+
assertTxDbAgrees(intent.db, pinned.db);
|
|
4500
|
+
return pinned.conn;
|
|
4501
|
+
}
|
|
4468
4502
|
const pool = resolvePool(intent, this.routing);
|
|
4469
4503
|
return {
|
|
4470
4504
|
async execute(sql2, params) {
|
|
@@ -4508,8 +4542,8 @@ var PooledAsyncContext = class {
|
|
|
4508
4542
|
function isRoutingConfig(x) {
|
|
4509
4543
|
return typeof x === "object" && x !== null && "registry" in x && "sticky" in x;
|
|
4510
4544
|
}
|
|
4511
|
-
function runWithPinnedAsyncConnection(conn, fn) {
|
|
4512
|
-
return asyncCtxStore.run(conn, fn);
|
|
4545
|
+
function runWithPinnedAsyncConnection(conn, db, fn) {
|
|
4546
|
+
return asyncCtxStore.run({ conn, db }, fn);
|
|
4513
4547
|
}
|
|
4514
4548
|
function currentPinnedAsyncConnection() {
|
|
4515
4549
|
return asyncCtxStore.getStore();
|
|
@@ -4520,9 +4554,9 @@ function defaultIsConnectionError(err) {
|
|
|
4520
4554
|
return message.includes("Connection terminated") || message.includes("Client has encountered a connection error") || message.includes("ECONNRESET") || message.includes("ECONNREFUSED") || message.includes("Connection lost") || message.includes("This socket has been ended by the other party") || code === "ECONNRESET" || code === "ECONNREFUSED" || code === "EPIPE" || code === "PROTOCOL_CONNECTION_LOST";
|
|
4521
4555
|
}
|
|
4522
4556
|
async function withTransactionAsync(ctx, fn, opts = {}, dialect = "postgres", isConnectionError2 = defaultIsConnectionError, connection) {
|
|
4523
|
-
const
|
|
4524
|
-
if (
|
|
4525
|
-
return fn(ctx.withConnection(
|
|
4557
|
+
const outer = currentPinnedAsyncConnection();
|
|
4558
|
+
if (outer !== void 0) {
|
|
4559
|
+
return fn(ctx.withConnection(outer.conn, true));
|
|
4526
4560
|
}
|
|
4527
4561
|
const resolved = resolveTxOptions(opts);
|
|
4528
4562
|
const begins = beginStatements(dialect, resolved.isolation);
|
|
@@ -4533,7 +4567,7 @@ async function withTransactionAsync(ctx, fn, opts = {}, dialect = "postgres", is
|
|
|
4533
4567
|
const conn = await pool.acquire();
|
|
4534
4568
|
const txCtx = ctx.withConnection(conn, true);
|
|
4535
4569
|
let poisoned = false;
|
|
4536
|
-
const attemptResult = await runWithPinnedAsyncConnection(conn, async () => {
|
|
4570
|
+
const attemptResult = await runWithPinnedAsyncConnection(conn, connection ?? null, async () => {
|
|
4537
4571
|
for (const begin of begins) await runAsync(txCtx, begin, []);
|
|
4538
4572
|
try {
|
|
4539
4573
|
const r = await runInTransactionScope(() => fn(txCtx));
|
|
@@ -4564,21 +4598,6 @@ async function withTransactionAsync(ctx, fn, opts = {}, dialect = "postgres", is
|
|
|
4564
4598
|
function transaction(ctx, fn, options = {}, dialect = "postgres", isConnectionError2 = defaultIsConnectionError, connection) {
|
|
4565
4599
|
return withTransactionAsync(ctx, () => fn(), options, dialect, isConnectionError2, connection);
|
|
4566
4600
|
}
|
|
4567
|
-
function withTransactionSync(outer, fn, dialect = "sqlite", isolation) {
|
|
4568
|
-
const ctx = outer.withConnection(outer.connectionFor({ write: true }), true);
|
|
4569
|
-
for (const begin of beginStatements(dialect, isolation)) run(ctx, begin, []);
|
|
4570
|
-
try {
|
|
4571
|
-
const r = runInTransactionScope(() => fn(ctx));
|
|
4572
|
-
run(ctx, r.commit ? "COMMIT" : "ROLLBACK", []);
|
|
4573
|
-
return r.value;
|
|
4574
|
-
} catch (e) {
|
|
4575
|
-
try {
|
|
4576
|
-
run(ctx, "ROLLBACK", []);
|
|
4577
|
-
} catch {
|
|
4578
|
-
}
|
|
4579
|
-
throw e;
|
|
4580
|
-
}
|
|
4581
|
-
}
|
|
4582
4601
|
|
|
4583
4602
|
// src/scp/limit-config.ts
|
|
4584
4603
|
var globalLimitConfig = {};
|
|
@@ -4658,25 +4677,20 @@ function attachToParent(parent, pkCols, byKey, single) {
|
|
|
4658
4677
|
}
|
|
4659
4678
|
|
|
4660
4679
|
// src/scp/leaves.ts
|
|
4661
|
-
var WHERE_TAIL_RE = /\s+(GROUP BY|ORDER BY|LIMIT|OFFSET|FOR UPDATE|RETURNING)\b/i;
|
|
4662
|
-
function spliceWhere(baseSql, whereSql) {
|
|
4663
|
-
if (whereSql === "") return baseSql;
|
|
4664
|
-
const tail = WHERE_TAIL_RE.exec(baseSql);
|
|
4665
|
-
return tail === null ? baseSql + whereSql : baseSql.slice(0, tail.index) + whereSql + baseSql.slice(tail.index);
|
|
4666
|
-
}
|
|
4667
4680
|
function assembleDynamicWhere(p) {
|
|
4668
|
-
const
|
|
4669
|
-
|
|
4670
|
-
const
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
}
|
|
4675
|
-
return { sql: spliceWhere(p.sql, whereSql), params: [...whereParams, ...p.params] };
|
|
4681
|
+
const plan = dynamicWherePlan(p.whereDynamic);
|
|
4682
|
+
const frags = plan.frags.filter((f) => !f.skipped);
|
|
4683
|
+
const clause = frags.length === 0 ? "" : ` ${plan.lead} ${frags.map((f) => f.sql).join(" AND ")}`;
|
|
4684
|
+
return {
|
|
4685
|
+
sql: p.sql + clause + plan.tail,
|
|
4686
|
+
params: [...p.params, ...frags.flatMap((f) => f.params), ...plan.tailParams]
|
|
4687
|
+
};
|
|
4676
4688
|
}
|
|
4677
4689
|
function effectiveStatement(p) {
|
|
4678
|
-
|
|
4679
|
-
|
|
4690
|
+
const write = p.write !== null;
|
|
4691
|
+
const db = p.db ?? null;
|
|
4692
|
+
if (p.whereDynamic == null) return { sql: p.sql, params: p.params, write, db };
|
|
4693
|
+
return { ...assembleDynamicWhere({ sql: p.sql, params: p.params, whereDynamic: p.whereDynamic }), write, db };
|
|
4680
4694
|
}
|
|
4681
4695
|
function toBcInt(v) {
|
|
4682
4696
|
return typeof v === "bigint" ? v : BigInt(v);
|
|
@@ -4698,7 +4712,7 @@ function prepareSql(p, dialect) {
|
|
|
4698
4712
|
}
|
|
4699
4713
|
sql2 = renderPlaceholders(sql2, dialect);
|
|
4700
4714
|
const bound = encodeParams(p.params, dialect);
|
|
4701
|
-
const intent = { write: p.write
|
|
4715
|
+
const intent = { write: p.write, ...p.db != null ? { db: p.db } : {} };
|
|
4702
4716
|
return { sql: sql2, bound, intent };
|
|
4703
4717
|
}
|
|
4704
4718
|
function writeSummary(info) {
|
|
@@ -4706,14 +4720,14 @@ function writeSummary(info) {
|
|
|
4706
4720
|
}
|
|
4707
4721
|
function executeSQL(p, ctx) {
|
|
4708
4722
|
const prepared = prepareSql(effectiveStatement(p), ctx.dialect);
|
|
4709
|
-
if (p.write
|
|
4723
|
+
if (p.write !== null && !p.write.returning) return writeSummary(run(ctx.exec, prepared.sql, prepared.bound, prepared.intent));
|
|
4710
4724
|
const rows = executeSafe(ctx.exec, prepared.sql, prepared.bound, prepared.intent);
|
|
4711
4725
|
assertRelationHardLimit(rows, p.guard);
|
|
4712
4726
|
return rows;
|
|
4713
4727
|
}
|
|
4714
4728
|
async function executeSQLAsync(p, ctx) {
|
|
4715
4729
|
const prepared = prepareSql(effectiveStatement(p), ctx.dialect);
|
|
4716
|
-
if (p.write
|
|
4730
|
+
if (p.write !== null && !p.write.returning) return writeSummary(await runAsync(ctx.execAsync, prepared.sql, prepared.bound, prepared.intent));
|
|
4717
4731
|
const rows = await executeAsync(ctx.execAsync, prepared.sql, prepared.bound, prepared.intent);
|
|
4718
4732
|
assertRelationHardLimit(rows, p.guard);
|
|
4719
4733
|
return rows;
|
|
@@ -4724,40 +4738,106 @@ function pluck(p) {
|
|
|
4724
4738
|
}
|
|
4725
4739
|
function group(p) {
|
|
4726
4740
|
const byKey = groupByKey(p.children, p.fk);
|
|
4727
|
-
return p.parents.map((par) => ({ ...par, [p.into]: attachToParent(par, p.pk, byKey, p.single
|
|
4741
|
+
return p.parents.map((par) => ({ ...par, [p.into]: attachToParent(par, p.pk, byKey, p.single) }));
|
|
4742
|
+
}
|
|
4743
|
+
var PORT_TYPES = {
|
|
4744
|
+
bool: (v) => typeof v === "boolean",
|
|
4745
|
+
int: (v) => typeof v === "bigint",
|
|
4746
|
+
string: (v) => typeof v === "string",
|
|
4747
|
+
list: (v) => Array.isArray(v),
|
|
4748
|
+
"string[]": (v) => Array.isArray(v) && v.every((e) => typeof e === "string"),
|
|
4749
|
+
record: (v) => typeof v === "object" && v !== null && !Array.isArray(v)
|
|
4750
|
+
};
|
|
4751
|
+
function portTyped(value, what, declared) {
|
|
4752
|
+
const kind = declared.endsWith("|null") ? declared.slice(0, -"|null".length) : declared;
|
|
4753
|
+
if (kind !== declared && value === null) return null;
|
|
4754
|
+
if (!PORT_TYPES[kind](value)) {
|
|
4755
|
+
const got = JSON.stringify(value, (_k, v) => typeof v === "bigint" ? `${v}n` : v);
|
|
4756
|
+
throw new Error(`scp leaf: ${what} must be ${declared}, got ${got}`);
|
|
4757
|
+
}
|
|
4758
|
+
return value;
|
|
4728
4759
|
}
|
|
4729
|
-
function
|
|
4730
|
-
if (
|
|
4731
|
-
const g = port;
|
|
4732
|
-
const limit = typeof g.limit === "bigint" ? Number(g.limit) : g.limit;
|
|
4733
|
-
if (typeof limit !== "number" || !Number.isInteger(limit) || typeof g.relation !== "string") {
|
|
4760
|
+
function requiredField(record, name, at, declared) {
|
|
4761
|
+
if (!(name in record)) {
|
|
4734
4762
|
throw new Error(
|
|
4735
|
-
`scp leaf
|
|
4763
|
+
`scp leaf: ${at} is missing its '${name}' field \u2014 a generated module spells every field of every struct it wires, so an ABSENT key is an ABI break (a null VALUE is how an absent write mode / plan / cap is spelled)`
|
|
4736
4764
|
);
|
|
4737
4765
|
}
|
|
4738
|
-
return
|
|
4766
|
+
return portTyped(record[name], `${at}'s '${name}'`, declared);
|
|
4767
|
+
}
|
|
4768
|
+
var PLAN_AT = `the 'whereDynamic' plan`;
|
|
4769
|
+
function dynamicWherePlan(port) {
|
|
4770
|
+
const plan = port;
|
|
4771
|
+
const at = `a 'whereDynamic' fragment`;
|
|
4772
|
+
const frags = requiredField(plan, "frags", PLAN_AT, "list").map((frag) => {
|
|
4773
|
+
const f = portTyped(frag, at, "record");
|
|
4774
|
+
return {
|
|
4775
|
+
skipped: requiredField(f, "skipped", at, "bool"),
|
|
4776
|
+
sql: requiredField(f, "sql", at, "string"),
|
|
4777
|
+
params: requiredField(f, "params", at, "list")
|
|
4778
|
+
};
|
|
4779
|
+
});
|
|
4780
|
+
return {
|
|
4781
|
+
frags,
|
|
4782
|
+
lead: requiredField(plan, "lead", PLAN_AT, "string"),
|
|
4783
|
+
tail: requiredField(plan, "tail", PLAN_AT, "string"),
|
|
4784
|
+
tailParams: requiredField(plan, "tailParams", PLAN_AT, "list")
|
|
4785
|
+
};
|
|
4786
|
+
}
|
|
4787
|
+
function relationGuardPort(port) {
|
|
4788
|
+
if (port === null) return null;
|
|
4789
|
+
const g = port;
|
|
4790
|
+
const at = `the 'guard' cap`;
|
|
4791
|
+
const raw = requiredField(g, "limit", at, "int");
|
|
4792
|
+
const model = requiredField(g, "model", at, "string|null");
|
|
4793
|
+
return {
|
|
4794
|
+
limit: Number(raw),
|
|
4795
|
+
...model === null ? {} : { model },
|
|
4796
|
+
relation: requiredField(g, "relation", at, "string")
|
|
4797
|
+
};
|
|
4798
|
+
}
|
|
4799
|
+
var OPTS_AT = `the 'opts' control record`;
|
|
4800
|
+
function optsField(opts, name) {
|
|
4801
|
+
return requiredField(opts, name, OPTS_AT, "record|null");
|
|
4802
|
+
}
|
|
4803
|
+
function writeModePort(port) {
|
|
4804
|
+
if (port === null) return null;
|
|
4805
|
+
return { returning: requiredField(port, "returning", `the 'write' mode`, "bool") };
|
|
4739
4806
|
}
|
|
4740
4807
|
function executeSqlPorts(ports) {
|
|
4808
|
+
const at = "the executeSQL payload";
|
|
4809
|
+
const sql2 = requiredField(ports, "sql", at, "string");
|
|
4810
|
+
const params = requiredField(ports, "params", at, "list");
|
|
4811
|
+
const opts = "opts" in ports ? requiredField(ports, "opts", at, "record|null") : null;
|
|
4812
|
+
if (opts === null) return { sql: sql2, params, write: null };
|
|
4741
4813
|
return {
|
|
4742
|
-
sql:
|
|
4743
|
-
params
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4814
|
+
sql: sql2,
|
|
4815
|
+
params,
|
|
4816
|
+
// The NAMED database — the one control field that is a bare `string|null` rather than a struct, so
|
|
4817
|
+
// it is read against its own declared type instead of through `optsField`'s `record|null`.
|
|
4818
|
+
db: requiredField(opts, "db", OPTS_AT, "string|null"),
|
|
4819
|
+
write: writeModePort(optsField(opts, "write")),
|
|
4820
|
+
whereDynamic: optsField(opts, "whereDynamic"),
|
|
4821
|
+
guard: relationGuardPort(optsField(opts, "guard"))
|
|
4749
4822
|
};
|
|
4750
4823
|
}
|
|
4824
|
+
var PLUCK_AT = "the pluck payload";
|
|
4825
|
+
var GROUP_AT = "the group payload";
|
|
4751
4826
|
var shapingHandlers = {
|
|
4752
|
-
pluck: (ports) => ({
|
|
4827
|
+
pluck: (ports) => ({
|
|
4828
|
+
ok: pluck({
|
|
4829
|
+
rows: requiredField(ports, "rows", PLUCK_AT, "list"),
|
|
4830
|
+
col: requiredField(ports, "col", PLUCK_AT, "string[]")
|
|
4831
|
+
})
|
|
4832
|
+
}),
|
|
4753
4833
|
group: (ports) => ({
|
|
4754
4834
|
ok: group({
|
|
4755
|
-
parents: ports
|
|
4756
|
-
children: ports
|
|
4757
|
-
pk: ports
|
|
4758
|
-
fk: ports
|
|
4759
|
-
into: ports
|
|
4760
|
-
single: ports
|
|
4835
|
+
parents: requiredField(ports, "parents", GROUP_AT, "list"),
|
|
4836
|
+
children: requiredField(ports, "children", GROUP_AT, "list"),
|
|
4837
|
+
pk: requiredField(ports, "pk", GROUP_AT, "string[]"),
|
|
4838
|
+
fk: requiredField(ports, "fk", GROUP_AT, "string[]"),
|
|
4839
|
+
into: requiredField(ports, "into", GROUP_AT, "string"),
|
|
4840
|
+
single: requiredField(ports, "single", GROUP_AT, "bool")
|
|
4761
4841
|
})
|
|
4762
4842
|
})
|
|
4763
4843
|
};
|
|
@@ -4786,111 +4866,6 @@ function isConnectionError(error) {
|
|
|
4786
4866
|
return message.includes("Connection terminated unexpectedly") || message.includes("Connection terminated") || message.includes("Client has encountered a connection error") || message.includes("read ECONNRESET") || message.includes("connect ECONNREFUSED") || message.includes("Connection lost") || message.includes("This socket has been ended by the other party") || code === "ECONNRESET" || code === "ECONNREFUSED" || code === "EPIPE" || code === "EAI_AGAIN" || code === "PROTOCOL_CONNECTION_LOST";
|
|
4787
4867
|
}
|
|
4788
4868
|
|
|
4789
|
-
// src/scp/writes.ts
|
|
4790
|
-
var ENTITY_ROOT = "__entity";
|
|
4791
|
-
var PATH_RE = /^\$\.(input|entity)\.([A-Za-z_][A-Za-z0-9_]*)$/;
|
|
4792
|
-
var REF_PATH_RE = /^\$\.ref\.([A-Za-z_][A-Za-z0-9_]*)\.([A-Za-z_][A-Za-z0-9_]*)$/;
|
|
4793
|
-
function parseEffectPath(value) {
|
|
4794
|
-
const m = PATH_RE.exec(value);
|
|
4795
|
-
if (m !== null) return { root: m[1], field: m[2] };
|
|
4796
|
-
const r = REF_PATH_RE.exec(value);
|
|
4797
|
-
if (r !== null) return { root: "ref", writeName: r[1], field: r[2] };
|
|
4798
|
-
throw new Error(
|
|
4799
|
-
`write-time relations: '${value}' is not a valid path-rooted value; use '$.input.<field>' (the Command input), '$.entity.<field>' (the sole written row), or '$.ref.<writeName>.<field>' (a named upstream write's RETURNING row).`
|
|
4800
|
-
);
|
|
4801
|
-
}
|
|
4802
|
-
function assertPaths(map) {
|
|
4803
|
-
for (const v of Object.values(map)) parseEffectPath(v);
|
|
4804
|
-
}
|
|
4805
|
-
var recorder = {
|
|
4806
|
-
lifecycle(effects = {}) {
|
|
4807
|
-
for (const r of effects.requires ?? []) assertPaths(r.keys);
|
|
4808
|
-
if (effects.idempotency !== void 0) parseEffectPath(effects.idempotency.token);
|
|
4809
|
-
for (const u of effects.unique ?? []) {
|
|
4810
|
-
u.scope.forEach((p) => parseEffectPath(p));
|
|
4811
|
-
u.fields.forEach((p) => parseEffectPath(p));
|
|
4812
|
-
}
|
|
4813
|
-
for (const d of effects.derive ?? []) assertPaths(d.keys);
|
|
4814
|
-
for (const e of effects.edges ?? []) {
|
|
4815
|
-
assertPaths(e.columns);
|
|
4816
|
-
if (e.where !== void 0) assertPaths(e.where);
|
|
4817
|
-
}
|
|
4818
|
-
for (const em of effects.emits ?? []) assertPaths(em.payload);
|
|
4819
|
-
return { effects: freezeEffects(effects) };
|
|
4820
|
-
},
|
|
4821
|
-
exists(table, keys) {
|
|
4822
|
-
return { kind: "requires", table, keys };
|
|
4823
|
-
},
|
|
4824
|
-
unique(spec) {
|
|
4825
|
-
return { kind: "unique", name: spec.name, guardTable: spec.guardTable, scope: spec.scope, fields: spec.fields };
|
|
4826
|
-
},
|
|
4827
|
-
increment(table, keys, attribute, amount) {
|
|
4828
|
-
if (!Number.isFinite(amount)) {
|
|
4829
|
-
throw new Error(`entityWrites: increment amount for '${table}.${attribute}' must be a finite number (got ${String(amount)})`);
|
|
4830
|
-
}
|
|
4831
|
-
return { kind: "derive", table, keys, attribute, amount };
|
|
4832
|
-
},
|
|
4833
|
-
edge(spec) {
|
|
4834
|
-
if (spec.relation === "fk" && spec.where === void 0) {
|
|
4835
|
-
throw new Error(`entityWrites: a 'fk' edge on '${spec.table}' requires a 'where' key map (the matched row's identity)`);
|
|
4836
|
-
}
|
|
4837
|
-
return {
|
|
4838
|
-
kind: "edge",
|
|
4839
|
-
relation: spec.relation,
|
|
4840
|
-
action: spec.action,
|
|
4841
|
-
table: spec.table,
|
|
4842
|
-
columns: spec.columns,
|
|
4843
|
-
...spec.where !== void 0 ? { where: spec.where } : {}
|
|
4844
|
-
};
|
|
4845
|
-
},
|
|
4846
|
-
event(name, outboxTable, payload) {
|
|
4847
|
-
return { kind: "emit", name, outboxTable, payload };
|
|
4848
|
-
},
|
|
4849
|
-
idempotentBy(table, column2, token) {
|
|
4850
|
-
return { kind: "idempotency", table, column: column2, token };
|
|
4851
|
-
}
|
|
4852
|
-
};
|
|
4853
|
-
function freezeEffects(effects) {
|
|
4854
|
-
const out = {};
|
|
4855
|
-
if (effects.requires !== void 0) out.requires = effects.requires;
|
|
4856
|
-
if (effects.idempotency !== void 0) out.idempotency = effects.idempotency;
|
|
4857
|
-
if (effects.unique !== void 0) out.unique = effects.unique;
|
|
4858
|
-
if (effects.derive !== void 0) out.derive = effects.derive;
|
|
4859
|
-
if (effects.edges !== void 0) out.edges = effects.edges;
|
|
4860
|
-
if (effects.emits !== void 0) out.emits = effects.emits;
|
|
4861
|
-
return out;
|
|
4862
|
-
}
|
|
4863
|
-
function entityWrites(builder) {
|
|
4864
|
-
const shape = builder(recorder);
|
|
4865
|
-
if (shape.create === void 0 && shape.update === void 0 && shape.remove === void 0) {
|
|
4866
|
-
throw new Error(
|
|
4867
|
-
"entityWrites(...) must declare at least one lifecycle (`create` / `update` / `remove`)."
|
|
4868
|
-
);
|
|
4869
|
-
}
|
|
4870
|
-
return {
|
|
4871
|
-
...shape.create !== void 0 ? { create: shape.create } : {},
|
|
4872
|
-
...shape.update !== void 0 ? { update: shape.update } : {},
|
|
4873
|
-
...shape.remove !== void 0 ? { remove: shape.remove } : {}
|
|
4874
|
-
};
|
|
4875
|
-
}
|
|
4876
|
-
function edgeWrites(builder) {
|
|
4877
|
-
const def = entityWrites(builder);
|
|
4878
|
-
for (const phase of ["create", "update", "remove"]) {
|
|
4879
|
-
const c = def[phase];
|
|
4880
|
-
if (c === void 0) continue;
|
|
4881
|
-
const e = c.effects;
|
|
4882
|
-
if (e.requires !== void 0 || e.unique !== void 0 || e.derive !== void 0 || e.emits !== void 0 || e.idempotency !== void 0) {
|
|
4883
|
-
throw new Error(
|
|
4884
|
-
`edgeWrites: the '${phase}' lifecycle may declare ONLY 'edges' effects (an edge entity carries only edge writes; use entityWrites for a full save contract).`
|
|
4885
|
-
);
|
|
4886
|
-
}
|
|
4887
|
-
}
|
|
4888
|
-
return def;
|
|
4889
|
-
}
|
|
4890
|
-
function lifecycleFor(def, phase) {
|
|
4891
|
-
return def[phase];
|
|
4892
|
-
}
|
|
4893
|
-
|
|
4894
4869
|
// src/scp/makesql/tx.ts
|
|
4895
4870
|
function literalize(value) {
|
|
4896
4871
|
if (value === null || value === void 0) return null;
|
|
@@ -4903,100 +4878,9 @@ function literalize(value) {
|
|
|
4903
4878
|
}
|
|
4904
4879
|
return value;
|
|
4905
4880
|
}
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
if (p.root === "entity") return { ref: [ENTITY_ROOT, p.field] };
|
|
4910
|
-
return { ref: [p.writeName, p.field] };
|
|
4911
|
-
}
|
|
4912
|
-
function guardInsert(dialect, table, columns, placeholders2) {
|
|
4913
|
-
const cols = `(${columns.join(", ")})`;
|
|
4914
|
-
if (dialect === "mysql") return `INSERT IGNORE INTO ${table} ${cols} VALUES (${placeholders2})`;
|
|
4915
|
-
return `INSERT INTO ${table} ${cols} VALUES (${placeholders2}) ON CONFLICT DO NOTHING`;
|
|
4916
|
-
}
|
|
4917
|
-
function makeIdGen() {
|
|
4918
|
-
let n = 0;
|
|
4919
|
-
return (role) => `tx_${role}_${n++}`;
|
|
4920
|
-
}
|
|
4921
|
-
function compileRequires(e, nextId) {
|
|
4922
|
-
const cols = Object.keys(e.keys);
|
|
4923
|
-
if (cols.length === 0) throw new Error(`write-plan: requires on '${e.table}' declares no keys`);
|
|
4924
|
-
const whereSql = v1EqualityWhereText(cols);
|
|
4925
|
-
const op = {
|
|
4926
|
-
sql: `SELECT 1 FROM ${e.table} WHERE ${whereSql}`,
|
|
4927
|
-
params: cols.map((c) => pathToRef(e.keys[c]))
|
|
4928
|
-
};
|
|
4929
|
-
return { id: nextId("requires"), role: "gate:requires", op, gate: "existsElseRollback", label: `requires ${e.table}` };
|
|
4930
|
-
}
|
|
4931
|
-
function compileIdempotency(e, nextId, dialect) {
|
|
4932
|
-
const op = {
|
|
4933
|
-
sql: guardInsert(dialect, e.table, [e.column], "?"),
|
|
4934
|
-
params: [pathToRef(e.token)]
|
|
4935
|
-
};
|
|
4936
|
-
return { id: nextId("idem"), role: "gate:idempotency", op, gate: "insertedElseNoop", label: `idempotency ${e.table}` };
|
|
4937
|
-
}
|
|
4938
|
-
function compileUnique(e, nextId, dialect) {
|
|
4939
|
-
const scopeCols = e.scope.map((_, i) => `s${i}`);
|
|
4940
|
-
const fieldCols = e.fields.map((_, i) => `f${i}`);
|
|
4941
|
-
const cols = ["name", ...scopeCols, ...fieldCols];
|
|
4942
|
-
const placeholders2 = cols.map(() => "?").join(", ");
|
|
4943
|
-
const params = [
|
|
4944
|
-
e.name,
|
|
4945
|
-
// literal discriminator (a closed-set string literal)
|
|
4946
|
-
...e.scope.map(pathToRef),
|
|
4947
|
-
...e.fields.map(pathToRef)
|
|
4948
|
-
];
|
|
4949
|
-
const op = { sql: guardInsert(dialect, e.guardTable, cols, placeholders2), params };
|
|
4950
|
-
return { id: nextId("unique"), role: "gate:unique", op, gate: "insertedElseRollback", label: `unique ${e.name}` };
|
|
4951
|
-
}
|
|
4952
|
-
function compileDerive(e, nextId) {
|
|
4953
|
-
const keyCols = Object.keys(e.keys);
|
|
4954
|
-
if (keyCols.length === 0) throw new Error(`write-plan: derive on '${e.table}' declares no keys`);
|
|
4955
|
-
const whereSql = v1EqualityWhereText(keyCols);
|
|
4956
|
-
const op = {
|
|
4957
|
-
sql: `UPDATE ${e.table} SET ${e.attribute} = ${e.attribute} + ? WHERE ${whereSql}`,
|
|
4958
|
-
// SET amount is the first param (before the WHERE keys), matching the v1 param order.
|
|
4959
|
-
params: [e.amount, ...keyCols.map((c) => pathToRef(e.keys[c]))]
|
|
4960
|
-
};
|
|
4961
|
-
return { id: nextId("derive"), role: "derive", op, label: `derive ${e.table}.${e.attribute}` };
|
|
4962
|
-
}
|
|
4963
|
-
function compileEdge(e, nextId) {
|
|
4964
|
-
if (e.relation === "m2m") {
|
|
4965
|
-
const cols = Object.keys(e.columns);
|
|
4966
|
-
if (e.action === "set") {
|
|
4967
|
-
const placeholders2 = cols.map(() => "?").join(", ");
|
|
4968
|
-
const op3 = {
|
|
4969
|
-
sql: `INSERT INTO ${e.table} (${cols.join(", ")}) VALUES (${placeholders2})`,
|
|
4970
|
-
params: cols.map((c) => pathToRef(e.columns[c]))
|
|
4971
|
-
};
|
|
4972
|
-
return { id: nextId("edge"), role: "edge", op: op3, label: `edge m2m link ${e.table}` };
|
|
4973
|
-
}
|
|
4974
|
-
const whereSql2 = v1EqualityWhereText(cols);
|
|
4975
|
-
const op2 = {
|
|
4976
|
-
sql: `DELETE FROM ${e.table} WHERE ${whereSql2}`,
|
|
4977
|
-
params: cols.map((c) => pathToRef(e.columns[c]))
|
|
4978
|
-
};
|
|
4979
|
-
return { id: nextId("edge"), role: "edge", op: op2, label: `edge m2m unlink ${e.table}` };
|
|
4980
|
-
}
|
|
4981
|
-
const setCols = Object.keys(e.columns);
|
|
4982
|
-
const whereCols = Object.keys(e.where);
|
|
4983
|
-
const setClauses = setCols.map((c) => e.action === "set" ? `${c} = ?` : `${c} = NULL`);
|
|
4984
|
-
const setParams = e.action === "set" ? setCols.map((c) => pathToRef(e.columns[c])) : [];
|
|
4985
|
-
const whereSql = v1EqualityWhereText(whereCols);
|
|
4986
|
-
const op = {
|
|
4987
|
-
sql: `UPDATE ${e.table} SET ${setClauses.join(", ")} WHERE ${whereSql}`,
|
|
4988
|
-
params: [...setParams, ...whereCols.map((c) => pathToRef(e.where[c]))]
|
|
4989
|
-
};
|
|
4990
|
-
return { id: nextId("edge"), role: "edge", op, label: `edge fk ${e.action} ${e.table}` };
|
|
4991
|
-
}
|
|
4992
|
-
function compileEmit(e, nextId) {
|
|
4993
|
-
const payloadObj = {};
|
|
4994
|
-
for (const [k, v] of Object.entries(e.payload)) payloadObj[k] = pathToRef(v);
|
|
4995
|
-
const op = {
|
|
4996
|
-
sql: `INSERT INTO ${e.outboxTable} (type, payload) VALUES (?, ?)`,
|
|
4997
|
-
params: [e.name, { obj: payloadObj }]
|
|
4998
|
-
};
|
|
4999
|
-
return { id: nextId("emit"), role: "emit", op, label: `emit ${e.name}` };
|
|
4881
|
+
var ENTITY_ROOT = "__entity";
|
|
4882
|
+
function isBatchPlan(plan) {
|
|
4883
|
+
return plan.entityFrom === null && plan.statements.every((s) => s.binds === void 0 && s.role === "body");
|
|
5000
4884
|
}
|
|
5001
4885
|
function stringPort(ports, name) {
|
|
5002
4886
|
const v = ports[name];
|
|
@@ -5053,11 +4937,6 @@ function v1ConditionText(conditions) {
|
|
|
5053
4937
|
const probe = [];
|
|
5054
4938
|
return new DBConditions(conditions).compile(probe);
|
|
5055
4939
|
}
|
|
5056
|
-
function v1EqualityWhereText(columns) {
|
|
5057
|
-
const conditions = {};
|
|
5058
|
-
for (const c of columns) conditions[c] = PROBE;
|
|
5059
|
-
return v1ConditionText(conditions);
|
|
5060
|
-
}
|
|
5061
4940
|
function lowerWhereMember(node, at, dialect) {
|
|
5062
4941
|
const op = opKey(node);
|
|
5063
4942
|
if (op === void 0) throw new Error(`compileWriteNode: ${at}: a where member must be a single-operator Expression node`);
|
|
@@ -5195,7 +5074,7 @@ function compileWriteNodeSql(node, dialect, resolveColumnType) {
|
|
|
5195
5074
|
const sorted = [...cols].sort();
|
|
5196
5075
|
if (stringPort(ports, "batch") === "true") {
|
|
5197
5076
|
if (dialect === "postgres") {
|
|
5198
|
-
if (resolveColumnType === void 0) throw new Error(`compileWriteNode: batch insert on postgres needs the column-type resolver (schema SoT) to derive the UNNEST element casts \u2014 pass it through compileCreateManyBundle
|
|
5077
|
+
if (resolveColumnType === void 0) throw new Error(`compileWriteNode: batch insert on postgres needs the column-type resolver (schema SoT) to derive the UNNEST element casts \u2014 pass it through compileCreateManyBundle (the decorator-adapter batch write path).`);
|
|
5199
5078
|
return pgBatchInsert(table, sorted, values, ports, resolveColumnType, dialect);
|
|
5200
5079
|
}
|
|
5201
5080
|
const shapeOpts = { tableName: table, columns: sorted, records: [], ...onConflictJsonOpts(ports, sorted), ...stringPort(ports, "returning") !== void 0 ? { returning: stringPort(ports, "returning") } : {} };
|
|
@@ -5223,7 +5102,7 @@ function compileWriteNodeSql(node, dialect, resolveColumnType) {
|
|
|
5223
5102
|
if (keyCols.length === 0) throw new Error(`compileWriteNode: batch Update requires at least one 'key.<field>' port`);
|
|
5224
5103
|
const updateCols = [...setCols].sort();
|
|
5225
5104
|
if (dialect === "postgres") {
|
|
5226
|
-
if (resolveColumnType === void 0) throw new Error(`compileWriteNode: batch update on postgres needs the column-type resolver (schema SoT) to derive the UNNEST element casts \u2014 pass it through compileCreateManyBundle
|
|
5105
|
+
if (resolveColumnType === void 0) throw new Error(`compileWriteNode: batch update on postgres needs the column-type resolver (schema SoT) to derive the UNNEST element casts \u2014 pass it through compileCreateManyBundle (the decorator-adapter batch write path).`);
|
|
5227
5106
|
return pgBatchUpdate(table, keyCols, updateCols, key, set, ports, resolveColumnType, dialect);
|
|
5228
5107
|
}
|
|
5229
5108
|
const shapeOpts = { tableName: table, keyColumns: keyCols, updateColumns: updateCols, records: [], ...stringPort(ports, "returning") !== void 0 ? { returning: stringPort(ports, "returning") } : {} };
|
|
@@ -5255,133 +5134,9 @@ function compileWriteNodeSql(node, dialect, resolveColumnType) {
|
|
|
5255
5134
|
throw new Error(`compileWriteNode: catalog component '${component}' has no write compile (SQL writes: Insert/Update/Delete)`);
|
|
5256
5135
|
}
|
|
5257
5136
|
}
|
|
5258
|
-
function
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
const mk = (stmt, isGate) => {
|
|
5262
|
-
const consumes = refHeadsOf(stmt).filter((h) => h !== ENTITY_ROOT && !isInputHead(h, stmt));
|
|
5263
|
-
return { stmt, seq: seqRef.n++, consumes, produces: stmt.binds ?? null, isGate };
|
|
5264
|
-
};
|
|
5265
|
-
for (const r of e.requires ?? []) nodes.push(mk(compileRequires(r, nextId), true));
|
|
5266
|
-
if (e.idempotency !== void 0) nodes.push(mk(compileIdempotency(e.idempotency, nextId, dialect), true));
|
|
5267
|
-
for (const u of e.unique ?? []) nodes.push(mk(compileUnique(u, nextId, dialect), true));
|
|
5268
|
-
const bodyId = nextId("body");
|
|
5269
|
-
const bodyStmt = {
|
|
5270
|
-
id: bodyId,
|
|
5271
|
-
role: "body",
|
|
5272
|
-
op: base.op,
|
|
5273
|
-
label: base.label,
|
|
5274
|
-
...base.name !== void 0 ? { binds: base.name } : {}
|
|
5275
|
-
};
|
|
5276
|
-
nodes.push(mk(bodyStmt, false));
|
|
5277
|
-
for (const d of e.derive ?? []) nodes.push(mk(compileDerive(d, nextId), false));
|
|
5278
|
-
for (const ed of e.edges ?? []) nodes.push(mk(compileEdge(ed, nextId), false));
|
|
5279
|
-
for (const em of e.emits ?? []) nodes.push(mk(compileEmit(em, nextId), false));
|
|
5280
|
-
return { nodes, bodyId };
|
|
5281
|
-
}
|
|
5282
|
-
function topoOrder(nodes) {
|
|
5283
|
-
const byName = /* @__PURE__ */ new Map();
|
|
5284
|
-
for (const n of nodes) {
|
|
5285
|
-
if (n.produces !== null) {
|
|
5286
|
-
if (byName.has(n.produces)) {
|
|
5287
|
-
throw new Error(`write-plan: two writes both bind the name '${n.produces}' \u2014 write names must be unique in a composite Command.`);
|
|
5288
|
-
}
|
|
5289
|
-
byName.set(n.produces, n);
|
|
5290
|
-
}
|
|
5291
|
-
}
|
|
5292
|
-
const succ = /* @__PURE__ */ new Map();
|
|
5293
|
-
const indeg = /* @__PURE__ */ new Map();
|
|
5294
|
-
const nodeById = /* @__PURE__ */ new Map();
|
|
5295
|
-
for (const n of nodes) {
|
|
5296
|
-
nodeById.set(n.stmt.id, n);
|
|
5297
|
-
succ.set(n.stmt.id, /* @__PURE__ */ new Set());
|
|
5298
|
-
indeg.set(n.stmt.id, 0);
|
|
5299
|
-
}
|
|
5300
|
-
const addEdge = (fromId, toId) => {
|
|
5301
|
-
if (fromId === toId) return;
|
|
5302
|
-
const s = succ.get(fromId);
|
|
5303
|
-
if (!s.has(toId)) {
|
|
5304
|
-
s.add(toId);
|
|
5305
|
-
indeg.set(toId, indeg.get(toId) + 1);
|
|
5306
|
-
}
|
|
5307
|
-
};
|
|
5308
|
-
for (const n of nodes) {
|
|
5309
|
-
for (const dep of n.consumes) {
|
|
5310
|
-
const producer = byName.get(dep);
|
|
5311
|
-
if (producer === void 0) {
|
|
5312
|
-
throw new Error(
|
|
5313
|
-
`write-plan: statement '${n.stmt.label}' references '$.ref.${dep}.*' but no write in this Command binds the name '${dep}' \u2014 a dangling write reference (fail-closed; no silent skip).`
|
|
5314
|
-
);
|
|
5315
|
-
}
|
|
5316
|
-
addEdge(producer.stmt.id, n.stmt.id);
|
|
5317
|
-
}
|
|
5318
|
-
}
|
|
5319
|
-
const gates = nodes.filter((n) => n.isGate);
|
|
5320
|
-
const nonGates = nodes.filter((n) => !n.isGate);
|
|
5321
|
-
for (const g of gates) for (const b of nonGates) addEdge(g.stmt.id, b.stmt.id);
|
|
5322
|
-
const ordered = [];
|
|
5323
|
-
const ready = nodes.filter((n) => indeg.get(n.stmt.id) === 0).sort((a, b) => a.seq - b.seq);
|
|
5324
|
-
while (ready.length > 0) {
|
|
5325
|
-
const n = ready.shift();
|
|
5326
|
-
ordered.push(n.stmt);
|
|
5327
|
-
for (const depId of succ.get(n.stmt.id)) {
|
|
5328
|
-
indeg.set(depId, indeg.get(depId) - 1);
|
|
5329
|
-
if (indeg.get(depId) === 0) {
|
|
5330
|
-
const dn = nodeById.get(depId);
|
|
5331
|
-
let lo = 0;
|
|
5332
|
-
while (lo < ready.length && ready[lo].seq < dn.seq) lo++;
|
|
5333
|
-
ready.splice(lo, 0, dn);
|
|
5334
|
-
}
|
|
5335
|
-
}
|
|
5336
|
-
}
|
|
5337
|
-
if (ordered.length !== nodes.length) {
|
|
5338
|
-
const stuck = nodes.filter((n) => !ordered.includes(n.stmt)).map((n) => n.stmt.label);
|
|
5339
|
-
throw new Error(
|
|
5340
|
-
`write-plan: the write-time transaction DAG has a CYCLE (unresolvable ordering among [${stuck.join(", ")}]). This is either a mutual '$.ref' data dependency or a gate that depends on a body row (a gate cannot depend on a not-yet-written row). ESCALATE: the Command's write dependencies are contradictory \u2014 no order satisfies both the data dependencies and gate-first. (Fail-closed; the derivation never guesses an order.)`
|
|
5341
|
-
);
|
|
5342
|
-
}
|
|
5343
|
-
return ordered;
|
|
5344
|
-
}
|
|
5345
|
-
function deriveTransactionPlan(phase, bases, lifecycle, dialect = "sqlite") {
|
|
5346
|
-
if (bases.length === 0) {
|
|
5347
|
-
throw new Error("write-plan: a Command must declare at least one base write (Insert/Update/Delete).");
|
|
5348
|
-
}
|
|
5349
|
-
const nextId = makeIdGen();
|
|
5350
|
-
const seqRef = { n: 0 };
|
|
5351
|
-
const composite = bases.length > 1;
|
|
5352
|
-
const allNodes = [];
|
|
5353
|
-
const bodyIds = [];
|
|
5354
|
-
bases.forEach((base, i) => {
|
|
5355
|
-
const lc = base.effects !== void 0 ? { effects: base.effects } : i === 0 ? lifecycle : { effects: {} };
|
|
5356
|
-
const { nodes, bodyId } = compileWriteGroup(base, lc, nextId, dialect, seqRef);
|
|
5357
|
-
allNodes.push(...nodes);
|
|
5358
|
-
bodyIds.push(bodyId);
|
|
5359
|
-
});
|
|
5360
|
-
const statements = topoOrder(allNodes);
|
|
5361
|
-
const soleBody = bases[0];
|
|
5362
|
-
const soleBodyId = bodyIds[0];
|
|
5363
|
-
const usesEntity = referencesHead(statements, ENTITY_ROOT);
|
|
5364
|
-
const soleBodyReturns = /\breturning\b/i.test(soleBody.op.sql);
|
|
5365
|
-
if (usesEntity && composite) {
|
|
5366
|
-
throw new Error(
|
|
5367
|
-
`write-plan: a composite (multi-write) Command uses '$.entity.*' \u2014 ambiguous which write it denotes. Address each write's row by name via '$.ref.<writeName>.<field>' (fail-closed).`
|
|
5368
|
-
);
|
|
5369
|
-
}
|
|
5370
|
-
if (usesEntity && !soleBodyReturns) {
|
|
5371
|
-
throw new Error(
|
|
5372
|
-
`write-plan: a derive/edges/emits stage references '$.entity.*' but the body write ('${soleBody.label}') has no RETURNING clause \u2014 the written row cannot be exposed to later stages. Add a RETURNING to the base write (fail-closed; no absent-entity default).`
|
|
5373
|
-
);
|
|
5374
|
-
}
|
|
5375
|
-
const entityFrom = usesEntity || !composite && soleBodyReturns ? soleBodyId : null;
|
|
5376
|
-
for (const base of bases) {
|
|
5377
|
-
if (base.name === void 0) continue;
|
|
5378
|
-
if (referencesHead(statements, base.name) && !/\breturning\b/i.test(base.op.sql)) {
|
|
5379
|
-
throw new Error(
|
|
5380
|
-
`write-plan: write '${base.name}' is referenced as '$.ref.${base.name}.*' by a later statement but its op has no RETURNING clause \u2014 its row cannot be bound for downstream writes. Add a RETURNING to write '${base.name}' (fail-closed; no absent-row default).`
|
|
5381
|
-
);
|
|
5382
|
-
}
|
|
5383
|
-
}
|
|
5384
|
-
return { phase, entityFrom, statements, onIdempotentHit: "rollback" };
|
|
5137
|
+
function makeIdGen() {
|
|
5138
|
+
let n = 0;
|
|
5139
|
+
return (role) => `tx_${role}_${n++}`;
|
|
5385
5140
|
}
|
|
5386
5141
|
function deriveBatchPlan(phase, ops) {
|
|
5387
5142
|
if (ops.length === 0) {
|
|
@@ -5394,56 +5149,7 @@ function deriveBatchPlan(phase, ops) {
|
|
|
5394
5149
|
op: { sql: op.sql, params: op.params.map((p) => literalize(p)) },
|
|
5395
5150
|
label: op.label ?? `batch[${i}]`
|
|
5396
5151
|
}));
|
|
5397
|
-
return { phase, entityFrom: null, statements
|
|
5398
|
-
}
|
|
5399
|
-
function refHeadsOf(stmt) {
|
|
5400
|
-
const heads = /* @__PURE__ */ new Set();
|
|
5401
|
-
collectRefHeads(stmt.op.params, heads);
|
|
5402
|
-
return [...heads];
|
|
5403
|
-
}
|
|
5404
|
-
function isInputHead(head, stmt) {
|
|
5405
|
-
return refPathLengthFor(stmt.op.params, head) === 1;
|
|
5406
|
-
}
|
|
5407
|
-
function refPathLengthFor(node, head) {
|
|
5408
|
-
let maxLen = 0;
|
|
5409
|
-
const walk = (n) => {
|
|
5410
|
-
if (n === null || typeof n !== "object") return;
|
|
5411
|
-
if (Array.isArray(n)) {
|
|
5412
|
-
for (const el of n) walk(el);
|
|
5413
|
-
return;
|
|
5414
|
-
}
|
|
5415
|
-
const keys = Object.keys(n);
|
|
5416
|
-
if (keys.length === 1 && keys[0] === "ref") {
|
|
5417
|
-
const path = n.ref;
|
|
5418
|
-
if (Array.isArray(path) && path[0] === head) maxLen = Math.max(maxLen, path.length);
|
|
5419
|
-
return;
|
|
5420
|
-
}
|
|
5421
|
-
for (const k of keys) walk(n[k]);
|
|
5422
|
-
};
|
|
5423
|
-
walk(node);
|
|
5424
|
-
return maxLen;
|
|
5425
|
-
}
|
|
5426
|
-
function referencesHead(statements, head) {
|
|
5427
|
-
const heads = /* @__PURE__ */ new Set();
|
|
5428
|
-
for (const s of statements) collectRefHeads(s.op.params, heads);
|
|
5429
|
-
return heads.has(head);
|
|
5430
|
-
}
|
|
5431
|
-
function collectRefHeads(node, heads) {
|
|
5432
|
-
if (node === null || typeof node !== "object") return;
|
|
5433
|
-
if (Array.isArray(node)) {
|
|
5434
|
-
for (const el of node) collectRefHeads(el, heads);
|
|
5435
|
-
return;
|
|
5436
|
-
}
|
|
5437
|
-
const keys = Object.keys(node);
|
|
5438
|
-
if (keys.length === 1 && keys[0] === "ref") {
|
|
5439
|
-
const path = node.ref;
|
|
5440
|
-
if (Array.isArray(path) && typeof path[0] === "string") heads.add(path[0]);
|
|
5441
|
-
return;
|
|
5442
|
-
}
|
|
5443
|
-
for (const k of keys) collectRefHeads(node[k], heads);
|
|
5444
|
-
}
|
|
5445
|
-
function asContext(dbOrCtx) {
|
|
5446
|
-
return "connectionFor" in dbOrCtx ? dbOrCtx : contextForDriver(dbOrCtx);
|
|
5152
|
+
return { phase, entityFrom: null, statements };
|
|
5447
5153
|
}
|
|
5448
5154
|
function toDriverParam(v, dialect) {
|
|
5449
5155
|
if (typeof v === "bigint") {
|
|
@@ -5462,78 +5168,13 @@ function renderStatement(op, scope, dialect) {
|
|
|
5462
5168
|
const { sql: sql2, params } = evalAssemble(op, scope, dialect);
|
|
5463
5169
|
return { sql: renderPlaceholders(sql2, dialect), params };
|
|
5464
5170
|
}
|
|
5465
|
-
function execStatement(ctx, op, scope, dialect) {
|
|
5466
|
-
const { sql: sql2, params } = evalAssemble(op, scope, dialect);
|
|
5467
|
-
const hasReturn = /\bselect\b/i.test(sql2.slice(0, 8)) || /\breturning\b/i.test(sql2);
|
|
5468
|
-
const out = executeSQL({ sql: sql2, params, write: true, returning: hasReturn, bigint: false }, { exec: ctx, dialect });
|
|
5469
|
-
return hasReturn ? { rows: out, changes: out.length } : { rows: [], changes: Number(out[0]?.changes ?? 0) };
|
|
5470
|
-
}
|
|
5471
|
-
function gateShortCircuit(gate, result) {
|
|
5472
|
-
switch (gate) {
|
|
5473
|
-
case "existsElseRollback":
|
|
5474
|
-
return result.rows.length === 0 ? "requires_absent" : null;
|
|
5475
|
-
case "insertedElseRollback":
|
|
5476
|
-
return result.changes === 0 ? "unique_collision" : null;
|
|
5477
|
-
case "insertedElseNoop":
|
|
5478
|
-
return result.changes === 0 ? "idempotent_duplicate" : null;
|
|
5479
|
-
default:
|
|
5480
|
-
throw new Error(`scp write: unknown gate rule '${String(gate)}'`);
|
|
5481
|
-
}
|
|
5482
|
-
}
|
|
5483
|
-
function executeTransaction(db, plan, input, dialect = "sqlite") {
|
|
5484
|
-
const outer = asContext(db);
|
|
5485
|
-
const executed = [];
|
|
5486
|
-
const scope = { ...input };
|
|
5487
|
-
let entity = null;
|
|
5488
|
-
const isBatch = plan.entityFrom === null && plan.statements.every((s) => s.gate === void 0 && s.binds === void 0 && s.role === "body");
|
|
5489
|
-
const returnedRows = [];
|
|
5490
|
-
try {
|
|
5491
|
-
return withTransactionSync(
|
|
5492
|
-
outer,
|
|
5493
|
-
(ctx) => {
|
|
5494
|
-
for (const stmt of plan.statements) {
|
|
5495
|
-
const result = execStatement(ctx, stmt.op, scope, dialect);
|
|
5496
|
-
executed.push(stmt.id);
|
|
5497
|
-
if (stmt.gate !== void 0) {
|
|
5498
|
-
const reason = gateShortCircuit(stmt.gate, result);
|
|
5499
|
-
if (reason !== null) {
|
|
5500
|
-
return { commit: false, value: { committed: false, shortCircuit: { statementId: stmt.id, reason }, entity: null, executed } };
|
|
5501
|
-
}
|
|
5502
|
-
}
|
|
5503
|
-
if (stmt.id === plan.entityFrom) {
|
|
5504
|
-
entity = result.rows.length > 0 ? result.rows[0] : null;
|
|
5505
|
-
if (entity !== null) scope[ENTITY_ROOT] = entity;
|
|
5506
|
-
}
|
|
5507
|
-
if (stmt.binds !== void 0 && result.rows.length > 0) {
|
|
5508
|
-
scope[stmt.binds] = result.rows[0];
|
|
5509
|
-
}
|
|
5510
|
-
if (isBatch && stmt.role === "body" && result.rows.length > 0) returnedRows.push(result.rows);
|
|
5511
|
-
}
|
|
5512
|
-
return { commit: true, value: { committed: true, entity, executed, ...returnedRows.length > 0 ? { returnedRows } : {} } };
|
|
5513
|
-
},
|
|
5514
|
-
dialect
|
|
5515
|
-
);
|
|
5516
|
-
} catch (e) {
|
|
5517
|
-
throw mapSqliteError(e);
|
|
5518
|
-
}
|
|
5519
|
-
}
|
|
5520
|
-
function countingDriver(db) {
|
|
5521
|
-
const prepared = [];
|
|
5522
|
-
const wrapped = {
|
|
5523
|
-
prepare(sql2) {
|
|
5524
|
-
prepared.push(sql2);
|
|
5525
|
-
return db.prepare(sql2);
|
|
5526
|
-
}
|
|
5527
|
-
};
|
|
5528
|
-
return { db: wrapped, prepared };
|
|
5529
|
-
}
|
|
5530
5171
|
function renderTxStatement(op, scope, dialect = "sqlite") {
|
|
5531
5172
|
return renderStatement(op, scope, dialect);
|
|
5532
5173
|
}
|
|
5533
5174
|
async function execStatementAsync(ctx, op, scope, dialect) {
|
|
5534
5175
|
const { sql: sql2, params } = evalAssemble(op, scope, dialect);
|
|
5535
5176
|
const hasReturn = /\bselect\b/i.test(sql2.slice(0, 8)) || /\breturning\b/i.test(sql2);
|
|
5536
|
-
const out = await executeSQLAsync({ sql: sql2, params, write:
|
|
5177
|
+
const out = await executeSQLAsync({ sql: sql2, params, write: { returning: hasReturn } }, { execAsync: ctx, dialect });
|
|
5537
5178
|
return hasReturn ? { rows: out, changes: out.length } : { rows: [], changes: Number(out[0]?.changes ?? 0) };
|
|
5538
5179
|
}
|
|
5539
5180
|
function executeTransactionAsync(ctx, plan, input, dialect = "sqlite", options = {}) {
|
|
@@ -5546,7 +5187,7 @@ function executeTransactionAsync(ctx, plan, input, dialect = "sqlite", options =
|
|
|
5546
5187
|
return runTransactionPlanAsync(ctx, plan, input, dialect, options);
|
|
5547
5188
|
}
|
|
5548
5189
|
function runTransactionPlanAsync(ctx, plan, input, dialect, options) {
|
|
5549
|
-
const
|
|
5190
|
+
const batch = isBatchPlan(plan);
|
|
5550
5191
|
return withTransactionAsync(ctx, async (txCtx) => {
|
|
5551
5192
|
const executed = [];
|
|
5552
5193
|
const scope = { ...input };
|
|
@@ -5555,12 +5196,6 @@ function runTransactionPlanAsync(ctx, plan, input, dialect, options) {
|
|
|
5555
5196
|
for (const stmt of plan.statements) {
|
|
5556
5197
|
const result = await execStatementAsync(txCtx, stmt.op, scope, dialect);
|
|
5557
5198
|
executed.push(stmt.id);
|
|
5558
|
-
if (stmt.gate !== void 0) {
|
|
5559
|
-
const reason = gateShortCircuit(stmt.gate, result);
|
|
5560
|
-
if (reason !== null) {
|
|
5561
|
-
throw new GateShortCircuit(stmt.id, reason, executed);
|
|
5562
|
-
}
|
|
5563
|
-
}
|
|
5564
5199
|
if (stmt.id === plan.entityFrom) {
|
|
5565
5200
|
entity = result.rows.length > 0 ? result.rows[0] : null;
|
|
5566
5201
|
if (entity !== null) scope[ENTITY_ROOT] = entity;
|
|
@@ -5568,28 +5203,13 @@ function runTransactionPlanAsync(ctx, plan, input, dialect, options) {
|
|
|
5568
5203
|
if (stmt.binds !== void 0 && result.rows.length > 0) {
|
|
5569
5204
|
scope[stmt.binds] = result.rows[0];
|
|
5570
5205
|
}
|
|
5571
|
-
if (
|
|
5206
|
+
if (batch && stmt.role === "body" && result.rows.length > 0) returnedRows.push(result.rows);
|
|
5572
5207
|
}
|
|
5573
5208
|
return { committed: true, entity, executed, ...returnedRows.length > 0 ? { returnedRows } : {} };
|
|
5574
5209
|
}, options, dialect === "sqlite" ? "postgres" : dialect, isConnectionError).catch((e) => {
|
|
5575
|
-
if (e instanceof GateShortCircuit) {
|
|
5576
|
-
return { committed: false, shortCircuit: { statementId: e.statementId, reason: e.reason }, entity: null, executed: e.executed };
|
|
5577
|
-
}
|
|
5578
5210
|
throw mapSqliteError(e);
|
|
5579
5211
|
});
|
|
5580
5212
|
}
|
|
5581
|
-
var GateShortCircuit = class extends Error {
|
|
5582
|
-
constructor(statementId, reason, executed) {
|
|
5583
|
-
super(`scp write: gate short-circuit at '${statementId}' (${reason})`);
|
|
5584
|
-
this.statementId = statementId;
|
|
5585
|
-
this.reason = reason;
|
|
5586
|
-
this.executed = executed;
|
|
5587
|
-
this.name = "GateShortCircuit";
|
|
5588
|
-
}
|
|
5589
|
-
statementId;
|
|
5590
|
-
reason;
|
|
5591
|
-
executed;
|
|
5592
|
-
};
|
|
5593
5213
|
|
|
5594
5214
|
// src/scp/makesql/pool-executor.ts
|
|
5595
5215
|
var PG_OID = { INT2: 21, INT4: 23, INT8: 20, DATE: 1082, TIMESTAMP: 1114, TIMESTAMPTZ: 1184, TIME: 1083, TIMETZ: 1266 };
|
|
@@ -5743,8 +5363,8 @@ function declarationStub(leafName, args) {
|
|
|
5743
5363
|
);
|
|
5744
5364
|
}
|
|
5745
5365
|
var Db = class {
|
|
5746
|
-
static executeSQL(sql2, params,
|
|
5747
|
-
return declarationStub("executeSQL", [sql2, params,
|
|
5366
|
+
static executeSQL(sql2, params, opts) {
|
|
5367
|
+
return declarationStub("executeSQL", [sql2, params, opts]);
|
|
5748
5368
|
}
|
|
5749
5369
|
static pluck(rows, col) {
|
|
5750
5370
|
return declarationStub("pluck", [rows, col]);
|
|
@@ -5947,11 +5567,7 @@ function targetKeyCols(op) {
|
|
|
5947
5567
|
}
|
|
5948
5568
|
function relationGuard(op) {
|
|
5949
5569
|
if (op.hardLimit === void 0) return null;
|
|
5950
|
-
return {
|
|
5951
|
-
limit: op.hardLimit,
|
|
5952
|
-
...op.targetTable !== void 0 ? { model: op.targetTable } : {},
|
|
5953
|
-
relation: op.name
|
|
5954
|
-
};
|
|
5570
|
+
return { limit: op.hardLimit, model: op.targetTable, relation: op.name };
|
|
5955
5571
|
}
|
|
5956
5572
|
function bindKeys(op, tuples) {
|
|
5957
5573
|
if (op.parentKeys !== void 0) return [encodeJsonParam(op.dialect, tuples.map((t) => [...t]))];
|
|
@@ -5969,7 +5585,10 @@ function runRelationOp(op, parents, db) {
|
|
|
5969
5585
|
const tCols = targetKeyCols(op);
|
|
5970
5586
|
const childCols = op.materializers;
|
|
5971
5587
|
const boundParams = bindKeys(op, keys);
|
|
5972
|
-
const rawRows = executeSafe(ctx, sql2, boundParams
|
|
5588
|
+
const rawRows = executeSafe(ctx, sql2, boundParams, {
|
|
5589
|
+
write: false,
|
|
5590
|
+
...op.connection !== void 0 ? { db: op.connection } : {}
|
|
5591
|
+
});
|
|
5973
5592
|
assertRelationHardLimit(rawRows, relationGuard(op));
|
|
5974
5593
|
const rows = materializeChildRows(rawRows, childCols);
|
|
5975
5594
|
return { sql: sql2, keys, batch: groupByKey(rows, tCols) };
|
|
@@ -5998,7 +5617,6 @@ function distributeToParent(op, parent, batch) {
|
|
|
5998
5617
|
function toPortableScalar(scalar) {
|
|
5999
5618
|
return scalar;
|
|
6000
5619
|
}
|
|
6001
|
-
var SHORT_CIRCUIT_TYPE = { obj: { statementId: "string", reason: "string" } };
|
|
6002
5620
|
function targetAndReturning(sql2) {
|
|
6003
5621
|
const s = sql2.trim();
|
|
6004
5622
|
const ins = /^INSERT\s+(?:IGNORE\s+|OR\s+IGNORE\s+)?INTO\s+([A-Za-z_][A-Za-z0-9_]*)/i.exec(s);
|
|
@@ -6033,9 +5651,6 @@ function rowObjType(table, returning, resolve) {
|
|
|
6033
5651
|
function bodyStatements(plan) {
|
|
6034
5652
|
return plan.statements.filter((s) => s.role === "body");
|
|
6035
5653
|
}
|
|
6036
|
-
function isBatchPlan(plan) {
|
|
6037
|
-
return plan.entityFrom === null && plan.statements.length > 0 && plan.statements.every((s) => s.gate === void 0 && s.binds === void 0 && s.role === "body");
|
|
6038
|
-
}
|
|
6039
5654
|
function writeTargetReturning(plan) {
|
|
6040
5655
|
const bodies = bodyStatements(plan);
|
|
6041
5656
|
if (bodies.length === 0) {
|
|
@@ -6072,7 +5687,6 @@ function deriveWriteOutputType(plan, resolve) {
|
|
|
6072
5687
|
const fields = {
|
|
6073
5688
|
committed: "bool",
|
|
6074
5689
|
executed: { arr: "string" },
|
|
6075
|
-
shortCircuit: { opt: SHORT_CIRCUIT_TYPE },
|
|
6076
5690
|
entity: { opt: row }
|
|
6077
5691
|
};
|
|
6078
5692
|
if (isBatchPlan(plan) && hasReturning) fields.returnedRows = { opt: { arr: { arr: row } } };
|
|
@@ -6089,27 +5703,6 @@ function annotateWriteBundleOutType(bundle, resolve) {
|
|
|
6089
5703
|
}
|
|
6090
5704
|
|
|
6091
5705
|
// src/scp/write-bundle.ts
|
|
6092
|
-
function writeLabelOf(sql2) {
|
|
6093
|
-
const m = /^\s*(INSERT|UPDATE|DELETE)/i.exec(sql2);
|
|
6094
|
-
return m === null ? "Write" : m[1][0].toUpperCase() + m[1].slice(1).toLowerCase();
|
|
6095
|
-
}
|
|
6096
|
-
function compileWriteBundle(name, base, writes, phase, dialectName = "sqlite", resolveColumnType) {
|
|
6097
|
-
const lifecycle = lifecycleFor(writes, phase);
|
|
6098
|
-
if (lifecycle === void 0) {
|
|
6099
|
-
throw new Error(`scp write: the '${phase}' lifecycle is not declared in the entityWrites save contract`);
|
|
6100
|
-
}
|
|
6101
|
-
const plan = deriveTransactionPlan(phase, [{ op: base, label: writeLabelOf(base.sql) }], lifecycle, dialectName);
|
|
6102
|
-
const bundle = { dialect: dialectName, name, statement: base, transaction: plan };
|
|
6103
|
-
return resolveColumnType === void 0 ? bundle : annotateWriteBundleOutType(bundle, resolveColumnType);
|
|
6104
|
-
}
|
|
6105
|
-
function compileCompositeWriteBundle(entries, phase, dialectName = "sqlite") {
|
|
6106
|
-
if (entries.length < 2) {
|
|
6107
|
-
throw new Error("scp write: a composite write bundle needs at least 2 named write members (use compileWriteBundle for a single write).");
|
|
6108
|
-
}
|
|
6109
|
-
const bases = entries.map((e) => ({ op: e.base, label: `${writeLabelOf(e.base.sql)} ${e.name}`, name: e.name, effects: e.lifecycle.effects }));
|
|
6110
|
-
const plan = deriveTransactionPlan(phase, bases, { effects: {} }, dialectName);
|
|
6111
|
-
return { dialect: dialectName, name: entries[0].name, statement: entries[0].base, transaction: plan };
|
|
6112
|
-
}
|
|
6113
5706
|
function flattenBatchOp(node, label) {
|
|
6114
5707
|
const assembled = assembleMakeSQL(node);
|
|
6115
5708
|
return { sql: assembled.sql, params: assembled.params, label };
|
|
@@ -6141,14 +5734,11 @@ function compileDeleteManyBundle(name, options, dialectName = "sqlite") {
|
|
|
6141
5734
|
const components = compileDeleteMany({ dialect: dialectName, ...options });
|
|
6142
5735
|
const ops = components.map((c, i) => flattenBatchOp(c, `deleteMany group ${i}`));
|
|
6143
5736
|
if (ops.length === 0) {
|
|
6144
|
-
return { dialect: dialectName, name, statement: { sql: "", params: [] }, transaction: { phase: "remove", entityFrom: null, statements: []
|
|
5737
|
+
return { dialect: dialectName, name, statement: { sql: "", params: [] }, transaction: { phase: "remove", entityFrom: null, statements: [] } };
|
|
6145
5738
|
}
|
|
6146
5739
|
const plan = deriveBatchPlan("remove", ops);
|
|
6147
5740
|
return { dialect: dialectName, name, statement: { sql: ops[0].sql, params: ops[0].params }, transaction: plan };
|
|
6148
5741
|
}
|
|
6149
|
-
function executeTransactionBundle(bundle, input, options) {
|
|
6150
|
-
return executeTransaction(options.db, bundle.transaction, input, bundle.dialect);
|
|
6151
|
-
}
|
|
6152
5742
|
|
|
6153
5743
|
// src/decorators.ts
|
|
6154
5744
|
var import_reflect_metadata = __toESM(require_Reflect());
|
|
@@ -6669,6 +6259,9 @@ function deriveModelColumns(modelClass, options = {}) {
|
|
|
6669
6259
|
function tableNameOf(modelClass) {
|
|
6670
6260
|
return modelClass.TABLE_NAME ?? modelClass.name.toLowerCase();
|
|
6671
6261
|
}
|
|
6262
|
+
function connectionOf(modelClass) {
|
|
6263
|
+
return modelClass.CONNECTION;
|
|
6264
|
+
}
|
|
6672
6265
|
function compileCreateBundle(modelClass, name, options, dialect = "sqlite", columnsOptions) {
|
|
6673
6266
|
const resolveColumnType = modelColumnResolver(modelClass, columnsOptions);
|
|
6674
6267
|
return compileCreateManyBundle(name, options, dialect, resolveColumnType);
|
|
@@ -6709,12 +6302,14 @@ function relationDeclOf(rel, resolveTargetModel, dialect = "sqlite") {
|
|
|
6709
6302
|
const targetTable = tableNameOf(targetModel);
|
|
6710
6303
|
const select = targetProjection(targetModel);
|
|
6711
6304
|
const order = rel.options?.order ? orderToString(rel.options.order()) : void 0;
|
|
6305
|
+
const connection = connectionOf(targetModel);
|
|
6712
6306
|
const base = {
|
|
6713
6307
|
name: rel.propertyKey,
|
|
6714
6308
|
kind: rel.type,
|
|
6715
6309
|
targetTable,
|
|
6716
6310
|
select,
|
|
6717
6311
|
dialect,
|
|
6312
|
+
...connection !== void 0 ? { connection } : {},
|
|
6718
6313
|
...order !== void 0 ? { order } : {},
|
|
6719
6314
|
...rel.options?.limit !== void 0 ? { limit: rel.options.limit } : {},
|
|
6720
6315
|
...rel.options?.hardLimit !== void 0 ? { hardLimit: rel.options.hardLimit } : {}
|
|
@@ -6806,6 +6401,20 @@ var EmitContext = class {
|
|
|
6806
6401
|
bindTypeOf(model, column2) {
|
|
6807
6402
|
return tsScalar(sqlTypeToBcScalar(this.sqlTypeOf(model, column2)));
|
|
6808
6403
|
}
|
|
6404
|
+
/**
|
|
6405
|
+
* The NAMED connection every statement of an endpoint declared over `model` runs on — read from the
|
|
6406
|
+
* model, the ONE authority ({@link connectionOf}), and handed to {@link execOptions} as its `db`
|
|
6407
|
+
* field. `{}` ⇒ the DEFAULT connection, which leaves a bounded read's option record unspelled
|
|
6408
|
+
* entirely (native-clean).
|
|
6409
|
+
*
|
|
6410
|
+
* A RELATION's child fetch does NOT come through here: its connection is the TARGET model's, which
|
|
6411
|
+
* `relationDeclOf` already resolved onto the compiled op (`RelationOp.connection`) — the emitter
|
|
6412
|
+
* consumes that rather than re-deriving it from a second model handle.
|
|
6413
|
+
*/
|
|
6414
|
+
dbOf(model) {
|
|
6415
|
+
const db = connectionOf(model);
|
|
6416
|
+
return db !== void 0 ? { db } : {};
|
|
6417
|
+
}
|
|
6809
6418
|
// ── methods ────────────────────────────────────────────────────────────────────────────────
|
|
6810
6419
|
method(name, endpoint) {
|
|
6811
6420
|
assertIdentifier(name, `endpoint name '${name}'`);
|
|
@@ -6869,12 +6478,7 @@ var EmitContext = class {
|
|
|
6869
6478
|
const readResolve = endpoint.view !== void 0 ? (t, c) => resolve(t === alias ? baseTable : t, c) : resolve;
|
|
6870
6479
|
const cap = this.bakedFindCap(endpoint);
|
|
6871
6480
|
const where = endpoint.where ?? [];
|
|
6872
|
-
const
|
|
6873
|
-
if (dynamic && endpoint.view !== void 0) {
|
|
6874
|
-
throw new Error(
|
|
6875
|
-
`emit: endpoint '${name}': a QUERY view-model binds its own CTE params BEFORE the WHERE, and a dynamic (SKIP) WHERE is assembled at execution time \u2014 the two cannot share one param order. Declare the view's own predicate inside the view query, or drop the optional predicate.`
|
|
6876
|
-
);
|
|
6877
|
-
}
|
|
6481
|
+
const optional = where.filter(isOptional);
|
|
6878
6482
|
const params = [];
|
|
6879
6483
|
for (const p of where) this.declareWhereParams(model, p, params);
|
|
6880
6484
|
const limit = this.page(endpoint.limit ?? (cap !== null ? cap + 1 : void 0), params);
|
|
@@ -6883,15 +6487,21 @@ var EmitContext = class {
|
|
|
6883
6487
|
dialect: this.spec.dialect,
|
|
6884
6488
|
tableName: table,
|
|
6885
6489
|
select: projection.join(", "),
|
|
6886
|
-
|
|
6490
|
+
// The BOUNDED predicates — this read's static WHERE, whatever else it declares. (An all-optional
|
|
6491
|
+
// read compiles an EMPTY condition set, which is the same no-WHERE statement it always was.)
|
|
6492
|
+
conditions: this.conditions(model, where.filter((p) => !isOptional(p))),
|
|
6887
6493
|
...endpoint.view !== void 0 ? { cte: cteOf(endpoint.view) } : {},
|
|
6888
6494
|
...endpoint.order !== void 0 ? { order: endpoint.order } : {},
|
|
6889
6495
|
...limit !== void 0 ? { limit } : {},
|
|
6890
6496
|
...offset !== void 0 ? { offset } : {},
|
|
6891
6497
|
...endpoint.lock === "update" ? { forUpdate: true } : endpoint.lock === "share" ? { forShare: true } : {}
|
|
6892
6498
|
});
|
|
6893
|
-
const
|
|
6894
|
-
const
|
|
6499
|
+
const plan = optional.length > 0 ? this.dynamicWherePlan(model, optional, compiled) : void 0;
|
|
6500
|
+
const [sql2, slots] = plan === void 0 ? [compiled.sql, compiled.params] : [compiled.head, compiled.headParams];
|
|
6501
|
+
const call = `${quote(sql2)}, [${slots.map(renderSlot).join(", ")}]${execOptions({
|
|
6502
|
+
...this.dbOf(model),
|
|
6503
|
+
...plan !== void 0 ? { whereDynamic: plan } : {}
|
|
6504
|
+
})}`;
|
|
6895
6505
|
const rowObj = objOf(deriveReadRow(table, projection, readResolve, `endpoint '${name}'`).outType, name);
|
|
6896
6506
|
const selections = normalizeSelections(endpoint.with ?? []);
|
|
6897
6507
|
const fresh = freshNames(params.map((p) => p.name));
|
|
@@ -6908,28 +6518,35 @@ var EmitContext = class {
|
|
|
6908
6518
|
const lines = [];
|
|
6909
6519
|
const parentVar = fresh("rows");
|
|
6910
6520
|
lines.push(`const ${parentVar}: WireValue[] = Db.executeSQL(${call});`);
|
|
6911
|
-
const graph = this.relations(model, parentVar, rowObj, selections, lines, fresh,
|
|
6521
|
+
const graph = this.relations(model, parentVar, rowObj, selections, lines, fresh, `endpoint '${name}'`);
|
|
6912
6522
|
const rowType = this.declareInterface(`${cap1(name)}Row`, graph.obj);
|
|
6913
6523
|
lines[lines.length - 1] = `const ${graph.terminalVar}: ${rowType}[] = ${graph.terminalCall} as ${rowType}[];`;
|
|
6914
6524
|
lines.push(`return ${graph.terminalVar};`);
|
|
6915
6525
|
return { name, params, returnType: `${rowType}[]`, lines };
|
|
6916
6526
|
}
|
|
6917
6527
|
/**
|
|
6918
|
-
* The DYNAMIC WHERE plan expression: ONE fragment per
|
|
6919
|
-
*
|
|
6920
|
-
*
|
|
6921
|
-
* `
|
|
6922
|
-
* SQL + params + a SKIP flag). The array is HOMOGENEOUS — never a `cond`-to-null
|
|
6923
|
-
* go/rust native-codegen emitters resolve it (a variant/nullable array element is the
|
|
6924
|
-
* reject); the `executeSQL` leaf drops the `skipped` fragments at run
|
|
6528
|
+
* The DYNAMIC WHERE plan expression: ONE fragment per OPTIONAL predicate — never a bounded one, which
|
|
6529
|
+
* is already in the static statement (CLAUDE.md §2) — every fragment the SAME struct
|
|
6530
|
+
* `{skipped, sql, params}`, with `sql`/`params` from the SAME `compileWhere` that produces a bounded
|
|
6531
|
+
* WHERE and `skipped` the per-call SKIP decision carried as DATA (`param === null`). That is the whole
|
|
6532
|
+
* fragment vocabulary (SQL + params + a SKIP flag). The array is HOMOGENEOUS — never a `cond`-to-null
|
|
6533
|
+
* element — so the go/rust native-codegen emitters resolve it (a variant/nullable array element is the
|
|
6534
|
+
* one shape they reject); the `executeSQL` leaf drops the `skipped` fragments at run and continues the
|
|
6535
|
+
* static WHERE with the survivors.
|
|
6536
|
+
*
|
|
6537
|
+
* The plan also carries what FINISHES the statement, taken straight off the bundle this same read
|
|
6538
|
+
* compiled ({@link import('../makesql/compile-select').SelectBundle}): `lead` — the connector the first
|
|
6539
|
+
* survivor joins with, which is `AND` exactly when the builder emitted a static WHERE — and `tail` /
|
|
6540
|
+
* `tailParams`, the text and bound values that follow the WHERE region. The leaf then concatenates
|
|
6541
|
+
* `head` + clause + `tail`; nothing on the run side has to find the boundary again, which is what a
|
|
6542
|
+
* lexical scan got wrong on a nested statement (#198) and on a quoted `?` (#202).
|
|
6925
6543
|
*/
|
|
6926
|
-
dynamicWherePlan(model,
|
|
6927
|
-
const frags =
|
|
6544
|
+
dynamicWherePlan(model, optional, compiled) {
|
|
6545
|
+
const frags = optional.map((p) => {
|
|
6928
6546
|
const w = compileWhere(this.conditions(model, [p]), this.spec.dialect);
|
|
6929
|
-
|
|
6930
|
-
return `{ skipped: ${skipped}, sql: ${quote(w.sql)}, params: [${w.params.map(renderSlot).join(", ")}] }`;
|
|
6547
|
+
return `{ skipped: ${p.param} === null, sql: ${quote(w.sql)}, params: [${w.params.map(renderSlot).join(", ")}] }`;
|
|
6931
6548
|
});
|
|
6932
|
-
return `{ frags: [${frags.join(", ")}] }`;
|
|
6549
|
+
return `{ frags: [${frags.join(", ")}], lead: ${quote(compiled.lead)}, tail: ${quote(compiled.tail)}, tailParams: [${compiled.tailParams.map(renderSlot).join(", ")}] }`;
|
|
6933
6550
|
}
|
|
6934
6551
|
/**
|
|
6935
6552
|
* Emit ONE relation LEVEL: for each selected relation, pluck the parent key set, run the ONE batched
|
|
@@ -6937,7 +6554,7 @@ var EmitContext = class {
|
|
|
6937
6554
|
* selection recurses FIRST, so a grandchild level is grouped into its own parent before that parent
|
|
6938
6555
|
* is grouped into ours (one query per depth — never one per parent).
|
|
6939
6556
|
*/
|
|
6940
|
-
relations(model, parentVar, parentObj, selections, lines, fresh,
|
|
6557
|
+
relations(model, parentVar, parentObj, selections, lines, fresh, at) {
|
|
6941
6558
|
let acc = parentVar;
|
|
6942
6559
|
const obj = { ...parentObj };
|
|
6943
6560
|
let terminalCall = "";
|
|
@@ -6949,16 +6566,20 @@ var EmitContext = class {
|
|
|
6949
6566
|
lines.push(`const ${keysVar}: WireValue[] = Db.pluck(${acc}, ${jsonArray(parentKeyCols(op))});`);
|
|
6950
6567
|
const childVar = fresh(sel.name);
|
|
6951
6568
|
const guard = relationGuard(op);
|
|
6952
|
-
const guardExpr = guard !== null ? `{ limit: ${guard.limit} as Int,
|
|
6953
|
-
|
|
6954
|
-
|
|
6569
|
+
const guardExpr = guard !== null ? `{ limit: ${guard.limit} as Int, model: ${quote(guard.model)}, relation: ${quote(guard.relation)} }` : void 0;
|
|
6570
|
+
lines.push(
|
|
6571
|
+
`const ${childVar}: WireValue[] = Db.executeSQL(${quote(op.sql)}, [${keysVar}]${execOptions({
|
|
6572
|
+
...op.connection !== void 0 ? { db: op.connection } : {},
|
|
6573
|
+
...guardExpr !== void 0 ? { guard: guardExpr } : {}
|
|
6574
|
+
})});`
|
|
6575
|
+
);
|
|
6955
6576
|
let childRows = childVar;
|
|
6956
6577
|
let childObj = objOf(
|
|
6957
6578
|
deriveReadRow(decl.targetTable, decl.select, this.resolver(targetModel), `${at} relation '${sel.name}'`).outType,
|
|
6958
6579
|
`${at} relation '${sel.name}'`
|
|
6959
6580
|
);
|
|
6960
6581
|
if (sel.with.length > 0) {
|
|
6961
|
-
const nested = this.relations(targetModel, childVar, childObj, normalizeSelections(sel.with), lines, fresh,
|
|
6582
|
+
const nested = this.relations(targetModel, childVar, childObj, normalizeSelections(sel.with), lines, fresh, `${at}.${sel.name}`);
|
|
6962
6583
|
childObj = nested.obj;
|
|
6963
6584
|
childRows = nested.terminalVar;
|
|
6964
6585
|
}
|
|
@@ -7015,7 +6636,7 @@ var EmitContext = class {
|
|
|
7015
6636
|
params,
|
|
7016
6637
|
returnType: `${rowType}[]`,
|
|
7017
6638
|
lines: [
|
|
7018
|
-
`const ${out}: ${rowType}[] = Db.executeSQL(${quote(op.sql)}, [${op.params.map(renderSlot).join(", ")}]
|
|
6639
|
+
`const ${out}: ${rowType}[] = Db.executeSQL(${quote(op.sql)}, [${op.params.map(renderSlot).join(", ")}]${execOptions({ ...this.dbOf(model), write: { returning: returning !== void 0 } })}) as ${rowType}[];`,
|
|
7019
6640
|
`return ${out};`
|
|
7020
6641
|
]
|
|
7021
6642
|
};
|
|
@@ -7092,7 +6713,7 @@ var EmitContext = class {
|
|
|
7092
6713
|
params,
|
|
7093
6714
|
returnType: `${rowType}[]`,
|
|
7094
6715
|
lines: [
|
|
7095
|
-
`const ${out}: ${rowType}[] = Db.executeSQL(${quote(op.sql)}, [${slots.join(", ")}]
|
|
6716
|
+
`const ${out}: ${rowType}[] = Db.executeSQL(${quote(op.sql)}, [${slots.join(", ")}]${execOptions({ ...this.dbOf(model), write: { returning: endpoint.returning !== void 0 } })}) as ${rowType}[];`,
|
|
7096
6717
|
`return ${out};`
|
|
7097
6718
|
]
|
|
7098
6719
|
};
|
|
@@ -7120,7 +6741,7 @@ var EmitContext = class {
|
|
|
7120
6741
|
params,
|
|
7121
6742
|
returnType: `${rowType}[]`,
|
|
7122
6743
|
lines: [
|
|
7123
|
-
`const ${out}: ${rowType}[] = Db.executeSQL(${quote(op.sql)}, [${op.params.map(renderSlot).join(", ")}]
|
|
6744
|
+
`const ${out}: ${rowType}[] = Db.executeSQL(${quote(op.sql)}, [${op.params.map(renderSlot).join(", ")}]${execOptions({ ...this.dbOf(model), write: { returning: endpoint.returning !== void 0 } })}) as ${rowType}[];`,
|
|
7124
6745
|
`return ${out};`
|
|
7125
6746
|
]
|
|
7126
6747
|
};
|
|
@@ -7338,6 +6959,11 @@ function isParameterised(p) {
|
|
|
7338
6959
|
function isOptional(p) {
|
|
7339
6960
|
return isParameterised(p) && p.optional === true;
|
|
7340
6961
|
}
|
|
6962
|
+
function execOptions(o) {
|
|
6963
|
+
if (o.db === void 0 && o.write === void 0 && o.whereDynamic === void 0 && o.guard === void 0) return "";
|
|
6964
|
+
const write = o.write === void 0 ? "null" : `{ returning: ${o.write.returning} }`;
|
|
6965
|
+
return `, { db: ${o.db === void 0 ? "null" : quote(o.db)}, write: ${write}, whereDynamic: ${o.whereDynamic ?? "null"}, guard: ${o.guard ?? "null"} }`;
|
|
6966
|
+
}
|
|
7341
6967
|
function renderSlot(slot) {
|
|
7342
6968
|
if (slot instanceof ParamRef) return slot.expr;
|
|
7343
6969
|
const ref = slot?.__batchArray?.ref;
|
|
@@ -7512,20 +7138,13 @@ var RelationContext = class {
|
|
|
7512
7138
|
ops;
|
|
7513
7139
|
db;
|
|
7514
7140
|
hydrate;
|
|
7515
|
-
/** CROSS-DB (V0 R1): name → driver registry; a tagged relation routes to `connections[tag]`. */
|
|
7516
|
-
connections;
|
|
7517
7141
|
/** Per-relation resolved batch (name → grouping), memoized: one batch query per relation. */
|
|
7518
7142
|
resolved = /* @__PURE__ */ new Map();
|
|
7519
|
-
constructor(siblings, ops, db, hydrate
|
|
7143
|
+
constructor(siblings, ops, db, hydrate) {
|
|
7520
7144
|
this.siblings = siblings;
|
|
7521
7145
|
this.ops = ops;
|
|
7522
7146
|
this.db = db;
|
|
7523
7147
|
this.hydrate = hydrate;
|
|
7524
|
-
this.connections = connections;
|
|
7525
|
-
}
|
|
7526
|
-
/** The driver a relation runs against: its tagged cross-DB connection, else the primary `db`. */
|
|
7527
|
-
driverFor(op) {
|
|
7528
|
-
return op.connection === void 0 ? this.db : driverForOp(op, this.connections);
|
|
7529
7148
|
}
|
|
7530
7149
|
/**
|
|
7531
7150
|
* Resolve `relationName` for `parent` (lazy access). The relation is batch-loaded across
|
|
@@ -7539,19 +7158,12 @@ var RelationContext = class {
|
|
|
7539
7158
|
}
|
|
7540
7159
|
let batch = this.resolved.get(relationName);
|
|
7541
7160
|
if (batch === void 0) {
|
|
7542
|
-
batch = runRelationOp(op, this.siblings, this.
|
|
7161
|
+
batch = runRelationOp(op, this.siblings, this.db).batch;
|
|
7543
7162
|
this.resolved.set(relationName, batch);
|
|
7544
7163
|
}
|
|
7545
7164
|
return applyHydrate(distributeToParent(op, parent, batch), this.hydrate);
|
|
7546
7165
|
}
|
|
7547
7166
|
};
|
|
7548
|
-
function driverForOp(op, connections) {
|
|
7549
|
-
const d = connections?.[op.connection];
|
|
7550
|
-
if (d === void 0) {
|
|
7551
|
-
throw new Error(`cross-DB relation '${op.name}': no driver registered for connection '${op.connection}' (pass it in ReadOptions.connections)`);
|
|
7552
|
-
}
|
|
7553
|
-
return d;
|
|
7554
|
-
}
|
|
7555
7167
|
function readRelationContext(o) {
|
|
7556
7168
|
const v = o[RELATION_CONTEXT];
|
|
7557
7169
|
return v instanceof RelationContext ? v : void 0;
|
|
@@ -7589,7 +7201,7 @@ function buildResultSet(rawRows, ops, db, options = {}) {
|
|
|
7589
7201
|
for (const k of Object.keys(raw)) o[k] = raw[k];
|
|
7590
7202
|
return o;
|
|
7591
7203
|
});
|
|
7592
|
-
const ctx = new RelationContext(rows, ops, db, hydrate
|
|
7204
|
+
const ctx = new RelationContext(rows, ops, db, hydrate);
|
|
7593
7205
|
for (const o of rows) {
|
|
7594
7206
|
Object.defineProperty(o, RELATION_CONTEXT, {
|
|
7595
7207
|
value: ctx,
|
|
@@ -7601,8 +7213,7 @@ function buildResultSet(rawRows, ops, db, options = {}) {
|
|
|
7601
7213
|
for (const name of Object.keys(selected)) {
|
|
7602
7214
|
const op = ops[name];
|
|
7603
7215
|
if (op === void 0) throw new Error(`declarative select: relation '${name}' is not declared on this model`);
|
|
7604
|
-
const
|
|
7605
|
-
const { batch } = runRelationOp(op, rows, relDb);
|
|
7216
|
+
const { batch } = runRelationOp(op, rows, db);
|
|
7606
7217
|
for (const o of rows) {
|
|
7607
7218
|
o[name] = applyHydrate(distributeToParent(op, o, batch), hydrate);
|
|
7608
7219
|
}
|
|
@@ -7621,7 +7232,6 @@ export {
|
|
|
7621
7232
|
DEFAULT_CONNECTION,
|
|
7622
7233
|
DEFAULT_UNCAST_SQL_TYPE,
|
|
7623
7234
|
Db,
|
|
7624
|
-
ENTITY_ROOT,
|
|
7625
7235
|
LEAF_TRANSPORT_SYMBOLS,
|
|
7626
7236
|
LITEDBMODEL_MAKESQL_CATALOG,
|
|
7627
7237
|
LimitExceededError,
|
|
@@ -7658,7 +7268,6 @@ export {
|
|
|
7658
7268
|
compileCompositeKeyLimited,
|
|
7659
7269
|
compileCompositeKeyStaticUnlimited,
|
|
7660
7270
|
compileCompositeKeyUnlimited,
|
|
7661
|
-
compileCompositeWriteBundle,
|
|
7662
7271
|
compileCreateBundle,
|
|
7663
7272
|
compileCreateManyBundle,
|
|
7664
7273
|
compileDeleteBundle,
|
|
@@ -7677,7 +7286,6 @@ export {
|
|
|
7677
7286
|
compileUpdateManyBundle,
|
|
7678
7287
|
compileUpdateSingle,
|
|
7679
7288
|
compileWhere,
|
|
7680
|
-
compileWriteBundle,
|
|
7681
7289
|
compileWriteNode,
|
|
7682
7290
|
composeMakeSQL,
|
|
7683
7291
|
configurePgDeboxTypeParsers,
|
|
@@ -7685,7 +7293,6 @@ export {
|
|
|
7685
7293
|
connectionForDriver,
|
|
7686
7294
|
contextForConnection,
|
|
7687
7295
|
contextForDriver,
|
|
7688
|
-
countingDriver,
|
|
7689
7296
|
createMiddleware,
|
|
7690
7297
|
currentPinnedAsyncConnection,
|
|
7691
7298
|
currentRegistry,
|
|
@@ -7693,20 +7300,15 @@ export {
|
|
|
7693
7300
|
dbCastIn,
|
|
7694
7301
|
deriveModelColumns,
|
|
7695
7302
|
deriveRelationDecls,
|
|
7696
|
-
deriveTransactionPlan,
|
|
7697
7303
|
dialectFor,
|
|
7698
7304
|
distributeToParent,
|
|
7699
|
-
edgeWrites,
|
|
7700
7305
|
emitBehaviorModule,
|
|
7701
|
-
entityWrites,
|
|
7702
7306
|
execute,
|
|
7703
7307
|
executeAsync,
|
|
7704
7308
|
executeSQL,
|
|
7705
7309
|
executeSQLAsync,
|
|
7706
7310
|
executeSafe,
|
|
7707
|
-
executeTransaction,
|
|
7708
7311
|
executeTransactionAsync,
|
|
7709
|
-
executeTransactionBundle,
|
|
7710
7312
|
failClosedMaterializeResolverFromColumnMap,
|
|
7711
7313
|
formatterFor,
|
|
7712
7314
|
getLimitConfig,
|
|
@@ -7721,7 +7323,6 @@ export {
|
|
|
7721
7323
|
leaf2 as leaf,
|
|
7722
7324
|
leafHandlers,
|
|
7723
7325
|
leafHandlersAsync,
|
|
7724
|
-
lifecycleFor,
|
|
7725
7326
|
makeSqlCatalogEntry,
|
|
7726
7327
|
makeSqlHandler,
|
|
7727
7328
|
makeSqlHandlerSync,
|
|
@@ -7736,7 +7337,6 @@ export {
|
|
|
7736
7337
|
mysqlPoolFactory,
|
|
7737
7338
|
noCastFormatter,
|
|
7738
7339
|
opt,
|
|
7739
|
-
parseEffectPath,
|
|
7740
7340
|
parseSchemaColumnTypes,
|
|
7741
7341
|
pgCastFormatter,
|
|
7742
7342
|
pgConnectionPool,
|
|
@@ -7777,7 +7377,6 @@ export {
|
|
|
7777
7377
|
setLimitConfig,
|
|
7778
7378
|
singlePoolPair,
|
|
7779
7379
|
sleep,
|
|
7780
|
-
spliceWhere,
|
|
7781
7380
|
sqlTypeToBcScalar,
|
|
7782
7381
|
sqlTypeToMaterializeClass,
|
|
7783
7382
|
stripMysqlPkHint,
|