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.
Files changed (180) hide show
  1. package/README.md +2 -1
  2. package/dist/DBModel.d.ts.map +1 -1
  3. package/dist/DBModel.js +3 -0
  4. package/dist/DBModel.js.map +1 -1
  5. package/dist/Middleware.d.ts +19 -2
  6. package/dist/Middleware.d.ts.map +1 -1
  7. package/dist/Middleware.js +1 -0
  8. package/dist/Middleware.js.map +1 -1
  9. package/dist/decorators.d.ts.map +1 -1
  10. package/dist/decorators.js +8 -0
  11. package/dist/decorators.js.map +1 -1
  12. package/dist/drivers/MysqlHelper.d.ts +1 -1
  13. package/dist/drivers/MysqlHelper.d.ts.map +1 -1
  14. package/dist/drivers/MysqlHelper.js.map +1 -1
  15. package/dist/drivers/PostgresHelper.d.ts +1 -1
  16. package/dist/drivers/PostgresHelper.d.ts.map +1 -1
  17. package/dist/drivers/PostgresHelper.js.map +1 -1
  18. package/dist/drivers/SqliteHelper.d.ts +1 -1
  19. package/dist/drivers/SqliteHelper.d.ts.map +1 -1
  20. package/dist/drivers/SqliteHelper.js.map +1 -1
  21. package/dist/index.cjs +105 -74
  22. package/dist/scp/connection-routing.d.ts +38 -1
  23. package/dist/scp/connection-routing.d.ts.map +1 -1
  24. package/dist/scp/connection-routing.js +64 -5
  25. package/dist/scp/connection-routing.js.map +1 -1
  26. package/dist/scp/dbmodel-runtime.d.ts +2 -1
  27. package/dist/scp/dbmodel-runtime.d.ts.map +1 -1
  28. package/dist/scp/dbmodel-runtime.js +2 -1
  29. package/dist/scp/dbmodel-runtime.js.map +1 -1
  30. package/dist/scp/decorator-adapter.d.ts +22 -0
  31. package/dist/scp/decorator-adapter.d.ts.map +1 -1
  32. package/dist/scp/decorator-adapter.js +26 -0
  33. package/dist/scp/decorator-adapter.js.map +1 -1
  34. package/dist/scp/emit/emitter.d.ts +21 -10
  35. package/dist/scp/emit/emitter.d.ts.map +1 -1
  36. package/dist/scp/emit/emitter.js +119 -51
  37. package/dist/scp/emit/emitter.js.map +1 -1
  38. package/dist/scp/emit/endpoint.d.ts +3 -8
  39. package/dist/scp/emit/endpoint.d.ts.map +1 -1
  40. package/dist/scp/exec-context.d.ts +39 -17
  41. package/dist/scp/exec-context.d.ts.map +1 -1
  42. package/dist/scp/exec-context.js +69 -35
  43. package/dist/scp/exec-context.js.map +1 -1
  44. package/dist/scp/grouping.d.ts.map +1 -1
  45. package/dist/scp/grouping.js +0 -0
  46. package/dist/scp/grouping.js.map +1 -1
  47. package/dist/scp/index.cjs +257 -670
  48. package/dist/scp/index.d.ts +6 -11
  49. package/dist/scp/index.d.ts.map +1 -1
  50. package/dist/scp/index.js +10 -28
  51. package/dist/scp/index.js.map +1 -1
  52. package/dist/scp/index.mjs +257 -658
  53. package/dist/scp/leaf-transport.d.ts +127 -39
  54. package/dist/scp/leaf-transport.d.ts.map +1 -1
  55. package/dist/scp/leaf-transport.js +30 -24
  56. package/dist/scp/leaf-transport.js.map +1 -1
  57. package/dist/scp/leaves.d.ts +57 -20
  58. package/dist/scp/leaves.d.ts.map +1 -1
  59. package/dist/scp/leaves.js +224 -65
  60. package/dist/scp/leaves.js.map +1 -1
  61. package/dist/scp/makesql/compile-select.d.ts +28 -1
  62. package/dist/scp/makesql/compile-select.d.ts.map +1 -1
  63. package/dist/scp/makesql/compile-select.js +19 -17
  64. package/dist/scp/makesql/compile-select.js.map +1 -1
  65. package/dist/scp/makesql/index.d.ts +2 -2
  66. package/dist/scp/makesql/index.d.ts.map +1 -1
  67. package/dist/scp/makesql/index.js +4 -6
  68. package/dist/scp/makesql/index.js.map +1 -1
  69. package/dist/scp/makesql/tx.d.ts +43 -89
  70. package/dist/scp/makesql/tx.d.ts.map +1 -1
  71. package/dist/scp/makesql/tx.js +45 -496
  72. package/dist/scp/makesql/tx.js.map +1 -1
  73. package/dist/scp/makesql/writeouttype.d.ts +5 -7
  74. package/dist/scp/makesql/writeouttype.d.ts.map +1 -1
  75. package/dist/scp/makesql/writeouttype.js +13 -19
  76. package/dist/scp/makesql/writeouttype.js.map +1 -1
  77. package/dist/scp/middleware.d.ts +4 -4
  78. package/dist/scp/middleware.d.ts.map +1 -1
  79. package/dist/scp/middleware.js +2 -2
  80. package/dist/scp/middleware.js.map +1 -1
  81. package/dist/scp/relation.d.ts +31 -12
  82. package/dist/scp/relation.d.ts.map +1 -1
  83. package/dist/scp/relation.js +16 -6
  84. package/dist/scp/relation.js.map +1 -1
  85. package/dist/scp/typed-object.d.ts +23 -19
  86. package/dist/scp/typed-object.d.ts.map +1 -1
  87. package/dist/scp/typed-object.js +24 -30
  88. package/dist/scp/typed-object.js.map +1 -1
  89. package/dist/scp/write-bundle.d.ts +21 -70
  90. package/dist/scp/write-bundle.d.ts.map +1 -1
  91. package/dist/scp/write-bundle.js +16 -80
  92. package/dist/scp/write-bundle.js.map +1 -1
  93. package/dist/types.d.ts +12 -0
  94. package/dist/types.d.ts.map +1 -1
  95. package/dist/types.js.map +1 -1
  96. package/docs/BENCHMARK.md +26 -22
  97. package/docs/COMPARISON.md +8 -4
  98. package/docs/api/README.md +1 -1
  99. package/docs/api/classes/Conditions.md +10 -10
  100. package/docs/api/classes/DBHandler.md +17 -17
  101. package/docs/api/classes/DBModel.md +111 -104
  102. package/docs/api/classes/LimitExceededError.md +8 -8
  103. package/docs/api/classes/Middleware.md +18 -18
  104. package/docs/api/classes/SqlRaw.md +6 -6
  105. package/docs/api/classes/SqlRef.md +7 -7
  106. package/docs/api/classes/Values.md +6 -6
  107. package/docs/api/classes/WriteInReadOnlyContextError.md +5 -5
  108. package/docs/api/classes/WriteOutsideTransactionError.md +5 -5
  109. package/docs/api/functions/belongsTo.md +2 -2
  110. package/docs/api/functions/closeAllPools.md +2 -2
  111. package/docs/api/functions/condsToRecord.md +2 -2
  112. package/docs/api/functions/createMiddleware.md +2 -2
  113. package/docs/api/functions/createPostgresDriver.md +2 -2
  114. package/docs/api/functions/createSqliteDriver.md +2 -2
  115. package/docs/api/functions/dbCast.md +3 -3
  116. package/docs/api/functions/dbCastIn.md +2 -2
  117. package/docs/api/functions/dbDynamic.md +3 -3
  118. package/docs/api/functions/dbFalse.md +2 -2
  119. package/docs/api/functions/dbImmediate.md +2 -2
  120. package/docs/api/functions/dbIn.md +2 -2
  121. package/docs/api/functions/dbNotNull.md +2 -2
  122. package/docs/api/functions/dbNow.md +2 -2
  123. package/docs/api/functions/dbNull.md +2 -2
  124. package/docs/api/functions/dbRaw.md +2 -2
  125. package/docs/api/functions/dbTrue.md +2 -2
  126. package/docs/api/functions/dbUuid.md +3 -3
  127. package/docs/api/functions/dbUuidIn.md +2 -2
  128. package/docs/api/functions/formatLocalDate.md +25 -0
  129. package/docs/api/functions/formatUTCDate.md +25 -0
  130. package/docs/api/functions/getDBConfig.md +2 -2
  131. package/docs/api/functions/getDBHandler.md +2 -2
  132. package/docs/api/functions/getTransactionConnection.md +2 -2
  133. package/docs/api/functions/getTransactionContext.md +2 -2
  134. package/docs/api/functions/hasMany.md +2 -2
  135. package/docs/api/functions/hasOne.md +2 -2
  136. package/docs/api/functions/initDBHandler.md +2 -2
  137. package/docs/api/functions/isAnySqlFragment.md +2 -2
  138. package/docs/api/functions/isConnectionError.md +26 -0
  139. package/docs/api/functions/isSqlCondition.md +2 -2
  140. package/docs/api/functions/isSqlFragment.md +2 -2
  141. package/docs/api/functions/isSqlRaw.md +2 -2
  142. package/docs/api/functions/isSqlRef.md +2 -2
  143. package/docs/api/functions/isSqlTypedFragment.md +2 -2
  144. package/docs/api/functions/model.md +6 -42
  145. package/docs/api/functions/parentRef.md +2 -2
  146. package/docs/api/functions/sql.md +7 -7
  147. package/docs/api/globals.md +57 -22
  148. package/docs/api/interfaces/Column.md +31 -31
  149. package/docs/api/interfaces/ColumnOptions.md +5 -4
  150. package/docs/api/interfaces/CreatedMiddlewareClass.md +7 -7
  151. package/docs/api/interfaces/DBConfig.md +13 -11
  152. package/docs/api/interfaces/DBConfigOptions.md +8 -8
  153. package/docs/api/interfaces/DeleteOptions.md +4 -4
  154. package/docs/api/interfaces/InsertOptions.md +8 -8
  155. package/docs/api/interfaces/LimitConfig.md +4 -4
  156. package/docs/api/interfaces/MiddlewareConfig.md +15 -15
  157. package/docs/api/interfaces/ModelOptions.md +8 -7
  158. package/docs/api/interfaces/PkeyResult.md +4 -4
  159. package/docs/api/interfaces/SelectOptions.md +17 -16
  160. package/docs/api/interfaces/SqlCondition.md +6 -6
  161. package/docs/api/interfaces/SqlFragment.md +5 -5
  162. package/docs/api/interfaces/SqlTypedFragment.md +7 -7
  163. package/docs/api/interfaces/TransactionOptions.md +7 -7
  164. package/docs/api/interfaces/UpdateManyOptions.md +4 -4
  165. package/docs/api/interfaces/UpdateOptions.md +4 -4
  166. package/docs/architecture.md +452 -0
  167. package/docs/benchmark-chart.svg +225 -249
  168. package/docs/design/phase-a-execution-context.md +2 -0
  169. package/package.json +20 -5
  170. package/dist/scp/writes.d.ts +0 -251
  171. package/dist/scp/writes.d.ts.map +0 -1
  172. package/dist/scp/writes.js +0 -176
  173. package/dist/scp/writes.js.map +0 -1
  174. package/docs/proposal/contracts-architecture-feasibility.md +0 -328
  175. package/docs/proposal/contracts-architecture.md +0 -579
  176. package/docs/proposal/litedbmodel-v2-scp-architecture.md +0 -433
  177. package/docs/proposal/sql-dynamic-expansion-spec.md +0 -175
  178. package/docs/proposal/v0-coverage-matrix.md +0 -157
  179. package/docs/proposal/v2-completeness-plan.md +0 -90
  180. package/docs/proposal/v2-sql-parity-checklist.md +0 -162
@@ -1768,30 +1768,32 @@ function lockTail(opts) {
1768
1768
  return "";
1769
1769
  }
1770
1770
  function compileSelect(desc) {
1771
- const params = [];
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) params.push(...desc.cte.params);
1775
- if (desc.joinParams && desc.joinParams.length > 0) params.push(...desc.joinParams);
1776
- const whereClause2 = conditionsFor(desc.conditions ?? {}, desc.dialect).compile(params, formatter);
1777
- let sql2 = "";
1778
- if (desc.cte) sql2 = `WITH ${desc.cte.name} AS (${desc.cte.sql}) `;
1779
- sql2 += `SELECT ${selectCols} FROM ${desc.tableName}`;
1780
- if (desc.join) sql2 += ` ${desc.join}`;
1781
- if (whereClause2) sql2 += ` WHERE ${whereClause2}`;
1782
- if (desc.group) sql2 += ` GROUP BY ${desc.group}`;
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) sql2 += ` ORDER BY ${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
- params.push(c.bind);
1789
+ tailParams.push(c.bind);
1788
1790
  return "?";
1789
1791
  };
1790
- if (desc.limit !== void 0) sql2 += ` LIMIT ${count(desc.limit)}`;
1791
- if (desc.offset !== void 0) sql2 += ` OFFSET ${count(desc.offset)}`;
1792
- sql2 += lockTail(desc);
1793
- if (desc.append) sql2 += ` ${desc.append}`;
1794
- return { sql: sql2, params };
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 ?? DEFAULT_CONNECTION;
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
- lastWriteAt = 0;
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 === 0) return false;
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 = 0;
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(_intent) {
4429
- return this.pinned ?? this.base;
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) return pinned;
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 outerConn = currentPinnedAsyncConnection();
4524
- if (outerConn !== void 0) {
4525
- return fn(ctx.withConnection(outerConn, true));
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 frags = p.whereDynamic.frags.filter((f) => !f.skipped);
4669
- let whereSql = "";
4670
- const whereParams = [];
4671
- frags.forEach((f, i) => {
4672
- whereSql += (i === 0 ? " WHERE " : " AND ") + f.sql;
4673
- whereParams.push(...f.params);
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
- if (p.whereDynamic == null) return p;
4679
- return { ...assembleDynamicWhere({ sql: p.sql, params: p.params, whereDynamic: p.whereDynamic }), write: p.write };
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 === true };
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 === true && p.returning !== true) return writeSummary(run(ctx.exec, prepared.sql, prepared.bound, prepared.intent));
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 === true && p.returning !== true) return writeSummary(await runAsync(ctx.execAsync, prepared.sql, prepared.bound, prepared.intent));
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 === true) }));
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 relationGuardPort(port) {
4730
- if (port === void 0 || port === null) return null;
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 executeSQL: the 'guard' port must be a {limit:int, model?:string, relation:string} relation cap, got ${JSON.stringify(port)}`
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 { limit, ...typeof g.model === "string" ? { model: g.model } : {}, relation: g.relation };
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: ports.sql,
4743
- params: ports.params,
4744
- write: ports.write === true,
4745
- returning: ports.returning === true,
4746
- bigint: ports.bigint === true,
4747
- whereDynamic: ports.whereDynamic ?? null,
4748
- guard: relationGuardPort(ports.guard)
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) => ({ ok: pluck({ rows: ports.rows, col: ports.col }) }),
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.parents,
4756
- children: ports.children,
4757
- pk: ports.pk,
4758
- fk: ports.fk,
4759
- into: ports.into,
4760
- single: ports.single === true
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
- function pathToRef(value) {
4907
- const p = parseEffectPath(value);
4908
- if (p.root === "input") return { ref: [p.field] };
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/compileWriteBundle (the decorator-adapter write path).`);
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/compileWriteBundle (the decorator-adapter write path).`);
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 compileWriteGroup(base, lifecycle, nextId, dialect, seqRef) {
5259
- const e = lifecycle.effects;
5260
- const nodes = [];
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, onIdempotentHit: "rollback" };
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: true, returning: hasReturn, bigint: false }, { execAsync: ctx, dialect });
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 isBatch = plan.entityFrom === null && plan.statements.every((s) => s.gate === void 0 && s.binds === void 0 && s.role === "body");
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 (isBatch && stmt.role === "body" && result.rows.length > 0) returnedRows.push(result.rows);
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, write, returning, bigint, whereDynamic, guard) {
5747
- return declarationStub("executeSQL", [sql2, params, write, returning, bigint, whereDynamic, guard]);
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: [], onIdempotentHit: "rollback" } };
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 dynamic = where.some(isOptional);
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
- ...dynamic ? {} : { conditions: this.conditions(model, where) },
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 bigint = endpoint.bigint === true;
6894
- const call = `${quote(compiled.sql)}, [${compiled.params.map(renderSlot).join(", ")}], false, false, ${bigint}${dynamic ? `, ${this.dynamicWherePlan(model, where)}` : ""}`;
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, bigint, `endpoint '${name}'`);
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 declared predicate, every one the SAME struct
6919
- * `{skipped, sql, params}` — `sql`/`params` from the SAME `compileWhere` that produces a bounded
6920
- * WHERE, and `skipped` the per-call SKIP decision carried as DATA (a bounded predicate is a literal
6921
- * `false`; an optional one is `param === null`). That is the whole fragment vocabulary (CLAUDE.md §2:
6922
- * SQL + params + a SKIP flag). The array is HOMOGENEOUS — never a `cond`-to-null element — so the
6923
- * go/rust native-codegen emitters resolve it (a variant/nullable array element is the one shape they
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, where) {
6927
- const frags = where.map((p) => {
6544
+ dynamicWherePlan(model, optional, compiled) {
6545
+ const frags = optional.map((p) => {
6928
6546
  const w = compileWhere(this.conditions(model, [p]), this.spec.dialect);
6929
- const skipped = isParameterised(p) && p.optional === true ? `${p.param} === null` : "false";
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, bigint, at) {
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, ${guard.model !== void 0 ? `model: ${quote(guard.model)}, ` : ""}relation: ${quote(guard.relation)} }` : null;
6953
- const guardPorts = guardExpr !== null ? `, { frags: [] }, ${guardExpr}` : "";
6954
- lines.push(`const ${childVar}: WireValue[] = Db.executeSQL(${quote(op.sql)}, [${keysVar}], false, false, ${bigint}${guardPorts});`);
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, bigint, `${at}.${sel.name}`);
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(", ")}], true, ${returning !== void 0}, false) as ${rowType}[];`,
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(", ")}], true, ${endpoint.returning !== void 0}, false) as ${rowType}[];`,
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(", ")}], true, ${endpoint.returning !== void 0}, false) as ${rowType}[];`,
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, connections) {
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.driverFor(op)).batch;
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, options.connections);
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 relDb = op.connection === void 0 ? db : driverForOp(op, options.connections);
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,