litedbmodel 2.2.2 → 2.2.3

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 (105) 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 +99 -33
  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 +251 -629
  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 +251 -617
  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 +27 -69
  70. package/dist/scp/makesql/tx.d.ts.map +1 -1
  71. package/dist/scp/makesql/tx.js +26 -448
  72. package/dist/scp/makesql/tx.js.map +1 -1
  73. package/dist/scp/middleware.d.ts +4 -4
  74. package/dist/scp/middleware.d.ts.map +1 -1
  75. package/dist/scp/middleware.js +2 -2
  76. package/dist/scp/middleware.js.map +1 -1
  77. package/dist/scp/relation.d.ts +31 -12
  78. package/dist/scp/relation.d.ts.map +1 -1
  79. package/dist/scp/relation.js +16 -6
  80. package/dist/scp/relation.js.map +1 -1
  81. package/dist/scp/typed-object.d.ts +23 -19
  82. package/dist/scp/typed-object.d.ts.map +1 -1
  83. package/dist/scp/typed-object.js +24 -30
  84. package/dist/scp/typed-object.js.map +1 -1
  85. package/dist/scp/write-bundle.d.ts +21 -70
  86. package/dist/scp/write-bundle.d.ts.map +1 -1
  87. package/dist/scp/write-bundle.js +15 -79
  88. package/dist/scp/write-bundle.js.map +1 -1
  89. package/dist/types.d.ts +12 -0
  90. package/dist/types.d.ts.map +1 -1
  91. package/dist/types.js.map +1 -1
  92. package/docs/architecture.md +452 -0
  93. package/docs/design/phase-a-execution-context.md +2 -0
  94. package/package.json +14 -3
  95. package/dist/scp/writes.d.ts +0 -251
  96. package/dist/scp/writes.d.ts.map +0 -1
  97. package/dist/scp/writes.js +0 -176
  98. package/dist/scp/writes.js.map +0 -1
  99. package/docs/proposal/contracts-architecture-feasibility.md +0 -328
  100. package/docs/proposal/contracts-architecture.md +0 -579
  101. package/docs/proposal/litedbmodel-v2-scp-architecture.md +0 -433
  102. package/docs/proposal/sql-dynamic-expansion-spec.md +0 -175
  103. package/docs/proposal/v0-coverage-matrix.md +0 -157
  104. package/docs/proposal/v2-completeness-plan.md +0 -90
  105. 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,101 +4878,7 @@ 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}` };
5000
- }
4881
+ var ENTITY_ROOT = "__entity";
5001
4882
  function stringPort(ports, name) {
5002
4883
  const v = ports[name];
5003
4884
  if (v === void 0) return void 0;
@@ -5053,11 +4934,6 @@ function v1ConditionText(conditions) {
5053
4934
  const probe = [];
5054
4935
  return new DBConditions(conditions).compile(probe);
5055
4936
  }
5056
- function v1EqualityWhereText(columns) {
5057
- const conditions = {};
5058
- for (const c of columns) conditions[c] = PROBE;
5059
- return v1ConditionText(conditions);
5060
- }
5061
4937
  function lowerWhereMember(node, at, dialect) {
5062
4938
  const op = opKey(node);
5063
4939
  if (op === void 0) throw new Error(`compileWriteNode: ${at}: a where member must be a single-operator Expression node`);
@@ -5195,7 +5071,7 @@ function compileWriteNodeSql(node, dialect, resolveColumnType) {
5195
5071
  const sorted = [...cols].sort();
5196
5072
  if (stringPort(ports, "batch") === "true") {
5197
5073
  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).`);
5074
+ 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
5075
  return pgBatchInsert(table, sorted, values, ports, resolveColumnType, dialect);
5200
5076
  }
5201
5077
  const shapeOpts = { tableName: table, columns: sorted, records: [], ...onConflictJsonOpts(ports, sorted), ...stringPort(ports, "returning") !== void 0 ? { returning: stringPort(ports, "returning") } : {} };
@@ -5223,7 +5099,7 @@ function compileWriteNodeSql(node, dialect, resolveColumnType) {
5223
5099
  if (keyCols.length === 0) throw new Error(`compileWriteNode: batch Update requires at least one 'key.<field>' port`);
5224
5100
  const updateCols = [...setCols].sort();
5225
5101
  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).`);
5102
+ 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
5103
  return pgBatchUpdate(table, keyCols, updateCols, key, set, ports, resolveColumnType, dialect);
5228
5104
  }
5229
5105
  const shapeOpts = { tableName: table, keyColumns: keyCols, updateColumns: updateCols, records: [], ...stringPort(ports, "returning") !== void 0 ? { returning: stringPort(ports, "returning") } : {} };
@@ -5255,133 +5131,9 @@ function compileWriteNodeSql(node, dialect, resolveColumnType) {
5255
5131
  throw new Error(`compileWriteNode: catalog component '${component}' has no write compile (SQL writes: Insert/Update/Delete)`);
5256
5132
  }
5257
5133
  }
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" };
5134
+ function makeIdGen() {
5135
+ let n = 0;
5136
+ return (role) => `tx_${role}_${n++}`;
5385
5137
  }
5386
5138
  function deriveBatchPlan(phase, ops) {
5387
5139
  if (ops.length === 0) {
@@ -5396,55 +5148,6 @@ function deriveBatchPlan(phase, ops) {
5396
5148
  }));
5397
5149
  return { phase, entityFrom: null, statements, onIdempotentHit: "rollback" };
5398
5150
  }
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);
5447
- }
5448
5151
  function toDriverParam(v, dialect) {
5449
5152
  if (typeof v === "bigint") {
5450
5153
  if (v >= BigInt(Number.MIN_SAFE_INTEGER) && v <= BigInt(Number.MAX_SAFE_INTEGER)) return Number(v);
@@ -5462,12 +5165,6 @@ function renderStatement(op, scope, dialect) {
5462
5165
  const { sql: sql2, params } = evalAssemble(op, scope, dialect);
5463
5166
  return { sql: renderPlaceholders(sql2, dialect), params };
5464
5167
  }
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
5168
  function gateShortCircuit(gate, result) {
5472
5169
  switch (gate) {
5473
5170
  case "existsElseRollback":
@@ -5480,60 +5177,13 @@ function gateShortCircuit(gate, result) {
5480
5177
  throw new Error(`scp write: unknown gate rule '${String(gate)}'`);
5481
5178
  }
5482
5179
  }
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
5180
  function renderTxStatement(op, scope, dialect = "sqlite") {
5531
5181
  return renderStatement(op, scope, dialect);
5532
5182
  }
5533
5183
  async function execStatementAsync(ctx, op, scope, dialect) {
5534
5184
  const { sql: sql2, params } = evalAssemble(op, scope, dialect);
5535
5185
  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 });
5186
+ const out = await executeSQLAsync({ sql: sql2, params, write: { returning: hasReturn } }, { execAsync: ctx, dialect });
5537
5187
  return hasReturn ? { rows: out, changes: out.length } : { rows: [], changes: Number(out[0]?.changes ?? 0) };
5538
5188
  }
5539
5189
  function executeTransactionAsync(ctx, plan, input, dialect = "sqlite", options = {}) {
@@ -5743,8 +5393,8 @@ function declarationStub(leafName, args) {
5743
5393
  );
5744
5394
  }
5745
5395
  var Db = class {
5746
- static executeSQL(sql2, params, write, returning, bigint, whereDynamic, guard) {
5747
- return declarationStub("executeSQL", [sql2, params, write, returning, bigint, whereDynamic, guard]);
5396
+ static executeSQL(sql2, params, opts) {
5397
+ return declarationStub("executeSQL", [sql2, params, opts]);
5748
5398
  }
5749
5399
  static pluck(rows, col) {
5750
5400
  return declarationStub("pluck", [rows, col]);
@@ -5947,11 +5597,7 @@ function targetKeyCols(op) {
5947
5597
  }
5948
5598
  function relationGuard(op) {
5949
5599
  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
- };
5600
+ return { limit: op.hardLimit, model: op.targetTable, relation: op.name };
5955
5601
  }
5956
5602
  function bindKeys(op, tuples) {
5957
5603
  if (op.parentKeys !== void 0) return [encodeJsonParam(op.dialect, tuples.map((t) => [...t]))];
@@ -5969,7 +5615,10 @@ function runRelationOp(op, parents, db) {
5969
5615
  const tCols = targetKeyCols(op);
5970
5616
  const childCols = op.materializers;
5971
5617
  const boundParams = bindKeys(op, keys);
5972
- const rawRows = executeSafe(ctx, sql2, boundParams);
5618
+ const rawRows = executeSafe(ctx, sql2, boundParams, {
5619
+ write: false,
5620
+ ...op.connection !== void 0 ? { db: op.connection } : {}
5621
+ });
5973
5622
  assertRelationHardLimit(rawRows, relationGuard(op));
5974
5623
  const rows = materializeChildRows(rawRows, childCols);
5975
5624
  return { sql: sql2, keys, batch: groupByKey(rows, tCols) };
@@ -6089,27 +5738,6 @@ function annotateWriteBundleOutType(bundle, resolve) {
6089
5738
  }
6090
5739
 
6091
5740
  // 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
5741
  function flattenBatchOp(node, label) {
6114
5742
  const assembled = assembleMakeSQL(node);
6115
5743
  return { sql: assembled.sql, params: assembled.params, label };
@@ -6146,9 +5774,6 @@ function compileDeleteManyBundle(name, options, dialectName = "sqlite") {
6146
5774
  const plan = deriveBatchPlan("remove", ops);
6147
5775
  return { dialect: dialectName, name, statement: { sql: ops[0].sql, params: ops[0].params }, transaction: plan };
6148
5776
  }
6149
- function executeTransactionBundle(bundle, input, options) {
6150
- return executeTransaction(options.db, bundle.transaction, input, bundle.dialect);
6151
- }
6152
5777
 
6153
5778
  // src/decorators.ts
6154
5779
  var import_reflect_metadata = __toESM(require_Reflect());
@@ -6669,6 +6294,9 @@ function deriveModelColumns(modelClass, options = {}) {
6669
6294
  function tableNameOf(modelClass) {
6670
6295
  return modelClass.TABLE_NAME ?? modelClass.name.toLowerCase();
6671
6296
  }
6297
+ function connectionOf(modelClass) {
6298
+ return modelClass.CONNECTION;
6299
+ }
6672
6300
  function compileCreateBundle(modelClass, name, options, dialect = "sqlite", columnsOptions) {
6673
6301
  const resolveColumnType = modelColumnResolver(modelClass, columnsOptions);
6674
6302
  return compileCreateManyBundle(name, options, dialect, resolveColumnType);
@@ -6709,12 +6337,14 @@ function relationDeclOf(rel, resolveTargetModel, dialect = "sqlite") {
6709
6337
  const targetTable = tableNameOf(targetModel);
6710
6338
  const select = targetProjection(targetModel);
6711
6339
  const order = rel.options?.order ? orderToString(rel.options.order()) : void 0;
6340
+ const connection = connectionOf(targetModel);
6712
6341
  const base = {
6713
6342
  name: rel.propertyKey,
6714
6343
  kind: rel.type,
6715
6344
  targetTable,
6716
6345
  select,
6717
6346
  dialect,
6347
+ ...connection !== void 0 ? { connection } : {},
6718
6348
  ...order !== void 0 ? { order } : {},
6719
6349
  ...rel.options?.limit !== void 0 ? { limit: rel.options.limit } : {},
6720
6350
  ...rel.options?.hardLimit !== void 0 ? { hardLimit: rel.options.hardLimit } : {}
@@ -6806,6 +6436,20 @@ var EmitContext = class {
6806
6436
  bindTypeOf(model, column2) {
6807
6437
  return tsScalar(sqlTypeToBcScalar(this.sqlTypeOf(model, column2)));
6808
6438
  }
6439
+ /**
6440
+ * The NAMED connection every statement of an endpoint declared over `model` runs on — read from the
6441
+ * model, the ONE authority ({@link connectionOf}), and handed to {@link execOptions} as its `db`
6442
+ * field. `{}` ⇒ the DEFAULT connection, which leaves a bounded read's option record unspelled
6443
+ * entirely (native-clean).
6444
+ *
6445
+ * A RELATION's child fetch does NOT come through here: its connection is the TARGET model's, which
6446
+ * `relationDeclOf` already resolved onto the compiled op (`RelationOp.connection`) — the emitter
6447
+ * consumes that rather than re-deriving it from a second model handle.
6448
+ */
6449
+ dbOf(model) {
6450
+ const db = connectionOf(model);
6451
+ return db !== void 0 ? { db } : {};
6452
+ }
6809
6453
  // ── methods ────────────────────────────────────────────────────────────────────────────────
6810
6454
  method(name, endpoint) {
6811
6455
  assertIdentifier(name, `endpoint name '${name}'`);
@@ -6869,12 +6513,7 @@ var EmitContext = class {
6869
6513
  const readResolve = endpoint.view !== void 0 ? (t, c) => resolve(t === alias ? baseTable : t, c) : resolve;
6870
6514
  const cap = this.bakedFindCap(endpoint);
6871
6515
  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
- }
6516
+ const optional = where.filter(isOptional);
6878
6517
  const params = [];
6879
6518
  for (const p of where) this.declareWhereParams(model, p, params);
6880
6519
  const limit = this.page(endpoint.limit ?? (cap !== null ? cap + 1 : void 0), params);
@@ -6883,15 +6522,21 @@ var EmitContext = class {
6883
6522
  dialect: this.spec.dialect,
6884
6523
  tableName: table,
6885
6524
  select: projection.join(", "),
6886
- ...dynamic ? {} : { conditions: this.conditions(model, where) },
6525
+ // The BOUNDED predicates — this read's static WHERE, whatever else it declares. (An all-optional
6526
+ // read compiles an EMPTY condition set, which is the same no-WHERE statement it always was.)
6527
+ conditions: this.conditions(model, where.filter((p) => !isOptional(p))),
6887
6528
  ...endpoint.view !== void 0 ? { cte: cteOf(endpoint.view) } : {},
6888
6529
  ...endpoint.order !== void 0 ? { order: endpoint.order } : {},
6889
6530
  ...limit !== void 0 ? { limit } : {},
6890
6531
  ...offset !== void 0 ? { offset } : {},
6891
6532
  ...endpoint.lock === "update" ? { forUpdate: true } : endpoint.lock === "share" ? { forShare: true } : {}
6892
6533
  });
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)}` : ""}`;
6534
+ const plan = optional.length > 0 ? this.dynamicWherePlan(model, optional, compiled) : void 0;
6535
+ const [sql2, slots] = plan === void 0 ? [compiled.sql, compiled.params] : [compiled.head, compiled.headParams];
6536
+ const call = `${quote(sql2)}, [${slots.map(renderSlot).join(", ")}]${execOptions({
6537
+ ...this.dbOf(model),
6538
+ ...plan !== void 0 ? { whereDynamic: plan } : {}
6539
+ })}`;
6895
6540
  const rowObj = objOf(deriveReadRow(table, projection, readResolve, `endpoint '${name}'`).outType, name);
6896
6541
  const selections = normalizeSelections(endpoint.with ?? []);
6897
6542
  const fresh = freshNames(params.map((p) => p.name));
@@ -6908,28 +6553,35 @@ var EmitContext = class {
6908
6553
  const lines = [];
6909
6554
  const parentVar = fresh("rows");
6910
6555
  lines.push(`const ${parentVar}: WireValue[] = Db.executeSQL(${call});`);
6911
- const graph = this.relations(model, parentVar, rowObj, selections, lines, fresh, bigint, `endpoint '${name}'`);
6556
+ const graph = this.relations(model, parentVar, rowObj, selections, lines, fresh, `endpoint '${name}'`);
6912
6557
  const rowType = this.declareInterface(`${cap1(name)}Row`, graph.obj);
6913
6558
  lines[lines.length - 1] = `const ${graph.terminalVar}: ${rowType}[] = ${graph.terminalCall} as ${rowType}[];`;
6914
6559
  lines.push(`return ${graph.terminalVar};`);
6915
6560
  return { name, params, returnType: `${rowType}[]`, lines };
6916
6561
  }
6917
6562
  /**
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.
6563
+ * The DYNAMIC WHERE plan expression: ONE fragment per OPTIONAL predicate — never a bounded one, which
6564
+ * is already in the static statement (CLAUDE.md §2) — every fragment the SAME struct
6565
+ * `{skipped, sql, params}`, with `sql`/`params` from the SAME `compileWhere` that produces a bounded
6566
+ * WHERE and `skipped` the per-call SKIP decision carried as DATA (`param === null`). That is the whole
6567
+ * fragment vocabulary (SQL + params + a SKIP flag). The array is HOMOGENEOUS — never a `cond`-to-null
6568
+ * element — so the go/rust native-codegen emitters resolve it (a variant/nullable array element is the
6569
+ * one shape they reject); the `executeSQL` leaf drops the `skipped` fragments at run and continues the
6570
+ * static WHERE with the survivors.
6571
+ *
6572
+ * The plan also carries what FINISHES the statement, taken straight off the bundle this same read
6573
+ * compiled ({@link import('../makesql/compile-select').SelectBundle}): `lead` — the connector the first
6574
+ * survivor joins with, which is `AND` exactly when the builder emitted a static WHERE — and `tail` /
6575
+ * `tailParams`, the text and bound values that follow the WHERE region. The leaf then concatenates
6576
+ * `head` + clause + `tail`; nothing on the run side has to find the boundary again, which is what a
6577
+ * lexical scan got wrong on a nested statement (#198) and on a quoted `?` (#202).
6925
6578
  */
6926
- dynamicWherePlan(model, where) {
6927
- const frags = where.map((p) => {
6579
+ dynamicWherePlan(model, optional, compiled) {
6580
+ const frags = optional.map((p) => {
6928
6581
  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(", ")}] }`;
6582
+ return `{ skipped: ${p.param} === null, sql: ${quote(w.sql)}, params: [${w.params.map(renderSlot).join(", ")}] }`;
6931
6583
  });
6932
- return `{ frags: [${frags.join(", ")}] }`;
6584
+ return `{ frags: [${frags.join(", ")}], lead: ${quote(compiled.lead)}, tail: ${quote(compiled.tail)}, tailParams: [${compiled.tailParams.map(renderSlot).join(", ")}] }`;
6933
6585
  }
6934
6586
  /**
6935
6587
  * Emit ONE relation LEVEL: for each selected relation, pluck the parent key set, run the ONE batched
@@ -6937,7 +6589,7 @@ var EmitContext = class {
6937
6589
  * selection recurses FIRST, so a grandchild level is grouped into its own parent before that parent
6938
6590
  * is grouped into ours (one query per depth — never one per parent).
6939
6591
  */
6940
- relations(model, parentVar, parentObj, selections, lines, fresh, bigint, at) {
6592
+ relations(model, parentVar, parentObj, selections, lines, fresh, at) {
6941
6593
  let acc = parentVar;
6942
6594
  const obj = { ...parentObj };
6943
6595
  let terminalCall = "";
@@ -6949,16 +6601,20 @@ var EmitContext = class {
6949
6601
  lines.push(`const ${keysVar}: WireValue[] = Db.pluck(${acc}, ${jsonArray(parentKeyCols(op))});`);
6950
6602
  const childVar = fresh(sel.name);
6951
6603
  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});`);
6604
+ const guardExpr = guard !== null ? `{ limit: ${guard.limit} as Int, model: ${quote(guard.model)}, relation: ${quote(guard.relation)} }` : void 0;
6605
+ lines.push(
6606
+ `const ${childVar}: WireValue[] = Db.executeSQL(${quote(op.sql)}, [${keysVar}]${execOptions({
6607
+ ...op.connection !== void 0 ? { db: op.connection } : {},
6608
+ ...guardExpr !== void 0 ? { guard: guardExpr } : {}
6609
+ })});`
6610
+ );
6955
6611
  let childRows = childVar;
6956
6612
  let childObj = objOf(
6957
6613
  deriveReadRow(decl.targetTable, decl.select, this.resolver(targetModel), `${at} relation '${sel.name}'`).outType,
6958
6614
  `${at} relation '${sel.name}'`
6959
6615
  );
6960
6616
  if (sel.with.length > 0) {
6961
- const nested = this.relations(targetModel, childVar, childObj, normalizeSelections(sel.with), lines, fresh, bigint, `${at}.${sel.name}`);
6617
+ const nested = this.relations(targetModel, childVar, childObj, normalizeSelections(sel.with), lines, fresh, `${at}.${sel.name}`);
6962
6618
  childObj = nested.obj;
6963
6619
  childRows = nested.terminalVar;
6964
6620
  }
@@ -7015,7 +6671,7 @@ var EmitContext = class {
7015
6671
  params,
7016
6672
  returnType: `${rowType}[]`,
7017
6673
  lines: [
7018
- `const ${out}: ${rowType}[] = Db.executeSQL(${quote(op.sql)}, [${op.params.map(renderSlot).join(", ")}], true, ${returning !== void 0}, false) as ${rowType}[];`,
6674
+ `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
6675
  `return ${out};`
7020
6676
  ]
7021
6677
  };
@@ -7092,7 +6748,7 @@ var EmitContext = class {
7092
6748
  params,
7093
6749
  returnType: `${rowType}[]`,
7094
6750
  lines: [
7095
- `const ${out}: ${rowType}[] = Db.executeSQL(${quote(op.sql)}, [${slots.join(", ")}], true, ${endpoint.returning !== void 0}, false) as ${rowType}[];`,
6751
+ `const ${out}: ${rowType}[] = Db.executeSQL(${quote(op.sql)}, [${slots.join(", ")}]${execOptions({ ...this.dbOf(model), write: { returning: endpoint.returning !== void 0 } })}) as ${rowType}[];`,
7096
6752
  `return ${out};`
7097
6753
  ]
7098
6754
  };
@@ -7120,7 +6776,7 @@ var EmitContext = class {
7120
6776
  params,
7121
6777
  returnType: `${rowType}[]`,
7122
6778
  lines: [
7123
- `const ${out}: ${rowType}[] = Db.executeSQL(${quote(op.sql)}, [${op.params.map(renderSlot).join(", ")}], true, ${endpoint.returning !== void 0}, false) as ${rowType}[];`,
6779
+ `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
6780
  `return ${out};`
7125
6781
  ]
7126
6782
  };
@@ -7338,6 +6994,11 @@ function isParameterised(p) {
7338
6994
  function isOptional(p) {
7339
6995
  return isParameterised(p) && p.optional === true;
7340
6996
  }
6997
+ function execOptions(o) {
6998
+ if (o.db === void 0 && o.write === void 0 && o.whereDynamic === void 0 && o.guard === void 0) return "";
6999
+ const write = o.write === void 0 ? "null" : `{ returning: ${o.write.returning} }`;
7000
+ return `, { db: ${o.db === void 0 ? "null" : quote(o.db)}, write: ${write}, whereDynamic: ${o.whereDynamic ?? "null"}, guard: ${o.guard ?? "null"} }`;
7001
+ }
7341
7002
  function renderSlot(slot) {
7342
7003
  if (slot instanceof ParamRef) return slot.expr;
7343
7004
  const ref = slot?.__batchArray?.ref;
@@ -7512,20 +7173,13 @@ var RelationContext = class {
7512
7173
  ops;
7513
7174
  db;
7514
7175
  hydrate;
7515
- /** CROSS-DB (V0 R1): name → driver registry; a tagged relation routes to `connections[tag]`. */
7516
- connections;
7517
7176
  /** Per-relation resolved batch (name → grouping), memoized: one batch query per relation. */
7518
7177
  resolved = /* @__PURE__ */ new Map();
7519
- constructor(siblings, ops, db, hydrate, connections) {
7178
+ constructor(siblings, ops, db, hydrate) {
7520
7179
  this.siblings = siblings;
7521
7180
  this.ops = ops;
7522
7181
  this.db = db;
7523
7182
  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
7183
  }
7530
7184
  /**
7531
7185
  * Resolve `relationName` for `parent` (lazy access). The relation is batch-loaded across
@@ -7539,19 +7193,12 @@ var RelationContext = class {
7539
7193
  }
7540
7194
  let batch = this.resolved.get(relationName);
7541
7195
  if (batch === void 0) {
7542
- batch = runRelationOp(op, this.siblings, this.driverFor(op)).batch;
7196
+ batch = runRelationOp(op, this.siblings, this.db).batch;
7543
7197
  this.resolved.set(relationName, batch);
7544
7198
  }
7545
7199
  return applyHydrate(distributeToParent(op, parent, batch), this.hydrate);
7546
7200
  }
7547
7201
  };
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
7202
  function readRelationContext(o) {
7556
7203
  const v = o[RELATION_CONTEXT];
7557
7204
  return v instanceof RelationContext ? v : void 0;
@@ -7589,7 +7236,7 @@ function buildResultSet(rawRows, ops, db, options = {}) {
7589
7236
  for (const k of Object.keys(raw)) o[k] = raw[k];
7590
7237
  return o;
7591
7238
  });
7592
- const ctx = new RelationContext(rows, ops, db, hydrate, options.connections);
7239
+ const ctx = new RelationContext(rows, ops, db, hydrate);
7593
7240
  for (const o of rows) {
7594
7241
  Object.defineProperty(o, RELATION_CONTEXT, {
7595
7242
  value: ctx,
@@ -7601,8 +7248,7 @@ function buildResultSet(rawRows, ops, db, options = {}) {
7601
7248
  for (const name of Object.keys(selected)) {
7602
7249
  const op = ops[name];
7603
7250
  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);
7251
+ const { batch } = runRelationOp(op, rows, db);
7606
7252
  for (const o of rows) {
7607
7253
  o[name] = applyHydrate(distributeToParent(op, o, batch), hydrate);
7608
7254
  }
@@ -7621,7 +7267,6 @@ export {
7621
7267
  DEFAULT_CONNECTION,
7622
7268
  DEFAULT_UNCAST_SQL_TYPE,
7623
7269
  Db,
7624
- ENTITY_ROOT,
7625
7270
  LEAF_TRANSPORT_SYMBOLS,
7626
7271
  LITEDBMODEL_MAKESQL_CATALOG,
7627
7272
  LimitExceededError,
@@ -7658,7 +7303,6 @@ export {
7658
7303
  compileCompositeKeyLimited,
7659
7304
  compileCompositeKeyStaticUnlimited,
7660
7305
  compileCompositeKeyUnlimited,
7661
- compileCompositeWriteBundle,
7662
7306
  compileCreateBundle,
7663
7307
  compileCreateManyBundle,
7664
7308
  compileDeleteBundle,
@@ -7677,7 +7321,6 @@ export {
7677
7321
  compileUpdateManyBundle,
7678
7322
  compileUpdateSingle,
7679
7323
  compileWhere,
7680
- compileWriteBundle,
7681
7324
  compileWriteNode,
7682
7325
  composeMakeSQL,
7683
7326
  configurePgDeboxTypeParsers,
@@ -7685,7 +7328,6 @@ export {
7685
7328
  connectionForDriver,
7686
7329
  contextForConnection,
7687
7330
  contextForDriver,
7688
- countingDriver,
7689
7331
  createMiddleware,
7690
7332
  currentPinnedAsyncConnection,
7691
7333
  currentRegistry,
@@ -7693,20 +7335,15 @@ export {
7693
7335
  dbCastIn,
7694
7336
  deriveModelColumns,
7695
7337
  deriveRelationDecls,
7696
- deriveTransactionPlan,
7697
7338
  dialectFor,
7698
7339
  distributeToParent,
7699
- edgeWrites,
7700
7340
  emitBehaviorModule,
7701
- entityWrites,
7702
7341
  execute,
7703
7342
  executeAsync,
7704
7343
  executeSQL,
7705
7344
  executeSQLAsync,
7706
7345
  executeSafe,
7707
- executeTransaction,
7708
7346
  executeTransactionAsync,
7709
- executeTransactionBundle,
7710
7347
  failClosedMaterializeResolverFromColumnMap,
7711
7348
  formatterFor,
7712
7349
  getLimitConfig,
@@ -7721,7 +7358,6 @@ export {
7721
7358
  leaf2 as leaf,
7722
7359
  leafHandlers,
7723
7360
  leafHandlersAsync,
7724
- lifecycleFor,
7725
7361
  makeSqlCatalogEntry,
7726
7362
  makeSqlHandler,
7727
7363
  makeSqlHandlerSync,
@@ -7736,7 +7372,6 @@ export {
7736
7372
  mysqlPoolFactory,
7737
7373
  noCastFormatter,
7738
7374
  opt,
7739
- parseEffectPath,
7740
7375
  parseSchemaColumnTypes,
7741
7376
  pgCastFormatter,
7742
7377
  pgConnectionPool,
@@ -7777,7 +7412,6 @@ export {
7777
7412
  setLimitConfig,
7778
7413
  singlePoolPair,
7779
7414
  sleep,
7780
- spliceWhere,
7781
7415
  sqlTypeToBcScalar,
7782
7416
  sqlTypeToMaterializeClass,
7783
7417
  stripMysqlPkHint,