litedbmodel 2.1.0 → 2.2.1

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 (189) hide show
  1. package/README.md +8 -1
  2. package/dist/DBConditions.d.ts.map +1 -1
  3. package/dist/DBConditions.js +16 -1
  4. package/dist/DBConditions.js.map +1 -1
  5. package/dist/DBHandler.d.ts.map +1 -1
  6. package/dist/DBHandler.js +6 -0
  7. package/dist/DBHandler.js.map +1 -1
  8. package/dist/DBModel.d.ts +84 -2
  9. package/dist/DBModel.d.ts.map +1 -1
  10. package/dist/DBModel.js +312 -29
  11. package/dist/DBModel.js.map +1 -1
  12. package/dist/decorators.d.ts +52 -0
  13. package/dist/decorators.d.ts.map +1 -1
  14. package/dist/decorators.js +83 -0
  15. package/dist/decorators.js.map +1 -1
  16. package/dist/drivers/mysql.d.ts.map +1 -1
  17. package/dist/drivers/mysql.js +6 -2
  18. package/dist/drivers/mysql.js.map +1 -1
  19. package/dist/drivers/postgres.d.ts.map +1 -1
  20. package/dist/drivers/postgres.js +6 -2
  21. package/dist/drivers/postgres.js.map +1 -1
  22. package/dist/drivers/sqlite.d.ts.map +1 -1
  23. package/dist/drivers/sqlite.js +12 -2
  24. package/dist/drivers/sqlite.js.map +1 -1
  25. package/dist/index.cjs +10741 -0
  26. package/dist/index.d.cts +1 -0
  27. package/dist/scp/coltype.d.ts +26 -1
  28. package/dist/scp/coltype.d.ts.map +1 -1
  29. package/dist/scp/coltype.js +82 -15
  30. package/dist/scp/coltype.js.map +1 -1
  31. package/dist/scp/connection-routing.d.ts +312 -0
  32. package/dist/scp/connection-routing.d.ts.map +1 -0
  33. package/dist/scp/connection-routing.js +394 -0
  34. package/dist/scp/connection-routing.js.map +1 -0
  35. package/dist/scp/dbmodel-runtime.d.ts +101 -0
  36. package/dist/scp/dbmodel-runtime.d.ts.map +1 -0
  37. package/dist/scp/dbmodel-runtime.js +174 -0
  38. package/dist/scp/dbmodel-runtime.js.map +1 -0
  39. package/dist/scp/decorator-adapter.d.ts +196 -0
  40. package/dist/scp/decorator-adapter.d.ts.map +1 -0
  41. package/dist/scp/decorator-adapter.js +286 -0
  42. package/dist/scp/decorator-adapter.js.map +1 -0
  43. package/dist/scp/emit/emitter.d.ts +108 -0
  44. package/dist/scp/emit/emitter.d.ts.map +1 -0
  45. package/dist/scp/emit/emitter.js +782 -0
  46. package/dist/scp/emit/emitter.js.map +1 -0
  47. package/dist/scp/emit/endpoint.d.ts +248 -0
  48. package/dist/scp/emit/endpoint.d.ts.map +1 -0
  49. package/dist/scp/emit/endpoint.js +20 -0
  50. package/dist/scp/emit/endpoint.js.map +1 -0
  51. package/dist/scp/emit/index.d.ts +11 -0
  52. package/dist/scp/emit/index.d.ts.map +1 -0
  53. package/dist/scp/emit/index.js +13 -0
  54. package/dist/scp/emit/index.js.map +1 -0
  55. package/dist/scp/errors.d.ts +45 -1
  56. package/dist/scp/errors.d.ts.map +1 -1
  57. package/dist/scp/errors.js +47 -1
  58. package/dist/scp/errors.js.map +1 -1
  59. package/dist/scp/exec-context.d.ts +390 -0
  60. package/dist/scp/exec-context.d.ts.map +1 -0
  61. package/dist/scp/exec-context.js +540 -0
  62. package/dist/scp/exec-context.js.map +1 -0
  63. package/dist/scp/grouping.d.ts +32 -0
  64. package/dist/scp/grouping.d.ts.map +1 -0
  65. package/dist/scp/grouping.js +0 -0
  66. package/dist/scp/grouping.js.map +1 -0
  67. package/dist/scp/index.cjs +6814 -15731
  68. package/dist/scp/index.d.cts +1 -0
  69. package/dist/scp/index.d.mts +1 -0
  70. package/dist/scp/index.d.ts +47 -49
  71. package/dist/scp/index.d.ts.map +1 -1
  72. package/dist/scp/index.js +174 -152
  73. package/dist/scp/index.js.map +1 -1
  74. package/dist/scp/index.mjs +5561 -3704
  75. package/dist/scp/leaf-transport.d.ts +50 -0
  76. package/dist/scp/leaf-transport.d.ts.map +1 -0
  77. package/dist/scp/leaf-transport.js +92 -0
  78. package/dist/scp/leaf-transport.js.map +1 -0
  79. package/dist/scp/leaves.d.ts +174 -0
  80. package/dist/scp/leaves.d.ts.map +1 -0
  81. package/dist/scp/leaves.js +287 -0
  82. package/dist/scp/leaves.js.map +1 -0
  83. package/dist/scp/limit-config.d.ts +118 -0
  84. package/dist/scp/limit-config.d.ts.map +1 -0
  85. package/dist/scp/limit-config.js +126 -0
  86. package/dist/scp/limit-config.js.map +1 -0
  87. package/dist/scp/makesql/compile-relation.d.ts +49 -23
  88. package/dist/scp/makesql/compile-relation.d.ts.map +1 -1
  89. package/dist/scp/makesql/compile-relation.js +101 -76
  90. package/dist/scp/makesql/compile-relation.js.map +1 -1
  91. package/dist/scp/makesql/compile-select.d.ts +45 -10
  92. package/dist/scp/makesql/compile-select.d.ts.map +1 -1
  93. package/dist/scp/makesql/compile-select.js +50 -10
  94. package/dist/scp/makesql/compile-select.js.map +1 -1
  95. package/dist/scp/makesql/handler.d.ts +1 -1
  96. package/dist/scp/makesql/handler.d.ts.map +1 -1
  97. package/dist/scp/makesql/index.d.ts +7 -9
  98. package/dist/scp/makesql/index.d.ts.map +1 -1
  99. package/dist/scp/makesql/index.js +17 -28
  100. package/dist/scp/makesql/index.js.map +1 -1
  101. package/dist/scp/makesql/json-array.d.ts +65 -0
  102. package/dist/scp/makesql/json-array.d.ts.map +1 -1
  103. package/dist/scp/makesql/json-array.js +99 -10
  104. package/dist/scp/makesql/json-array.js.map +1 -1
  105. package/dist/scp/makesql/json-batch.d.ts.map +1 -1
  106. package/dist/scp/makesql/json-batch.js +15 -18
  107. package/dist/scp/makesql/json-batch.js.map +1 -1
  108. package/dist/scp/makesql/makesql.d.ts +1 -1
  109. package/dist/scp/makesql/makesql.d.ts.map +1 -1
  110. package/dist/scp/makesql/mysql-returning.d.ts +93 -0
  111. package/dist/scp/makesql/mysql-returning.d.ts.map +1 -0
  112. package/dist/scp/makesql/mysql-returning.js +215 -0
  113. package/dist/scp/makesql/mysql-returning.js.map +1 -0
  114. package/dist/scp/makesql/outtype.d.ts +40 -54
  115. package/dist/scp/makesql/outtype.d.ts.map +1 -1
  116. package/dist/scp/makesql/outtype.js +46 -204
  117. package/dist/scp/makesql/outtype.js.map +1 -1
  118. package/dist/scp/makesql/pool-executor.d.ts +86 -34
  119. package/dist/scp/makesql/pool-executor.d.ts.map +1 -1
  120. package/dist/scp/makesql/pool-executor.js +179 -39
  121. package/dist/scp/makesql/pool-executor.js.map +1 -1
  122. package/dist/scp/makesql/tx.d.ts +107 -22
  123. package/dist/scp/makesql/tx.d.ts.map +1 -1
  124. package/dist/scp/makesql/tx.js +451 -97
  125. package/dist/scp/makesql/tx.js.map +1 -1
  126. package/dist/scp/makesql/writeouttype.d.ts +4 -4
  127. package/dist/scp/makesql/writeouttype.d.ts.map +1 -1
  128. package/dist/scp/makesql/writeouttype.js +5 -3
  129. package/dist/scp/makesql/writeouttype.js.map +1 -1
  130. package/dist/scp/middleware.d.ts +287 -0
  131. package/dist/scp/middleware.d.ts.map +1 -0
  132. package/dist/scp/middleware.js +358 -0
  133. package/dist/scp/middleware.js.map +1 -0
  134. package/dist/scp/relation.d.ts +65 -8
  135. package/dist/scp/relation.d.ts.map +1 -1
  136. package/dist/scp/relation.js +154 -93
  137. package/dist/scp/relation.js.map +1 -1
  138. package/dist/scp/tx-options.d.ts +136 -0
  139. package/dist/scp/tx-options.d.ts.map +1 -0
  140. package/dist/scp/tx-options.js +184 -0
  141. package/dist/scp/tx-options.js.map +1 -0
  142. package/dist/scp/write-bundle.d.ts +131 -0
  143. package/dist/scp/write-bundle.d.ts.map +1 -0
  144. package/dist/scp/write-bundle.js +160 -0
  145. package/dist/scp/write-bundle.js.map +1 -0
  146. package/dist/types.d.ts +7 -0
  147. package/dist/types.d.ts.map +1 -1
  148. package/dist/types.js.map +1 -1
  149. package/docs/BENCHMARK.md +54 -97
  150. package/docs/COMPARISON.md +14 -8
  151. package/docs/benchmark-chart.svg +310 -248
  152. package/docs/design/phase-a-execution-context.md +139 -0
  153. package/package.json +14 -13
  154. package/dist/scp/authoring-sql.d.ts +0 -134
  155. package/dist/scp/authoring-sql.d.ts.map +0 -1
  156. package/dist/scp/authoring-sql.js +0 -191
  157. package/dist/scp/authoring-sql.js.map +0 -1
  158. package/dist/scp/authoring.d.ts +0 -218
  159. package/dist/scp/authoring.d.ts.map +0 -1
  160. package/dist/scp/authoring.js +0 -351
  161. package/dist/scp/authoring.js.map +0 -1
  162. package/dist/scp/catalog.d.ts +0 -95
  163. package/dist/scp/catalog.d.ts.map +0 -1
  164. package/dist/scp/catalog.js +0 -245
  165. package/dist/scp/catalog.js.map +0 -1
  166. package/dist/scp/codegen.d.ts +0 -201
  167. package/dist/scp/codegen.d.ts.map +0 -1
  168. package/dist/scp/codegen.js +0 -605
  169. package/dist/scp/codegen.js.map +0 -1
  170. package/dist/scp/find-filter-guard.d.ts +0 -68
  171. package/dist/scp/find-filter-guard.d.ts.map +0 -1
  172. package/dist/scp/find-filter-guard.js +0 -88
  173. package/dist/scp/find-filter-guard.js.map +0 -1
  174. package/dist/scp/guard.d.ts +0 -43
  175. package/dist/scp/guard.d.ts.map +0 -1
  176. package/dist/scp/guard.js +0 -140
  177. package/dist/scp/guard.js.map +0 -1
  178. package/dist/scp/makesql/authoring-compile.d.ts +0 -90
  179. package/dist/scp/makesql/authoring-compile.d.ts.map +0 -1
  180. package/dist/scp/makesql/authoring-compile.js +0 -505
  181. package/dist/scp/makesql/authoring-compile.js.map +0 -1
  182. package/dist/scp/makesql/static-bundle.d.ts +0 -196
  183. package/dist/scp/makesql/static-bundle.d.ts.map +0 -1
  184. package/dist/scp/makesql/static-bundle.js +0 -1244
  185. package/dist/scp/makesql/static-bundle.js.map +0 -1
  186. package/dist/scp/runtime.d.ts +0 -243
  187. package/dist/scp/runtime.d.ts.map +0 -1
  188. package/dist/scp/runtime.js +0 -422
  189. package/dist/scp/runtime.js.map +0 -1
package/dist/DBModel.js CHANGED
@@ -21,6 +21,15 @@ const drivers_2 = require("./drivers");
21
21
  const connection_errors_1 = require("./connection-errors");
22
22
  // Import LazyRelation module (static import for Vitest compatibility)
23
23
  const LazyRelation_1 = require("./LazyRelation");
24
+ // Phase F-2 (#105): the SCP runtime bridge — puts DBModel's pooled execution on the Phase A-E runtime
25
+ // (real tx / reader-writer routing / multi-DB / middleware). Wired for the live pg/mysql dialects;
26
+ // sqlite + an uninitialized handler keep the v1 in-proc path. Every statement — a v1-built SELECT as
27
+ // much as a compiled write bundle — reaches the DB through this ONE ctx seam.
28
+ const dbmodel_runtime_1 = require("./scp/dbmodel-runtime");
29
+ const exec_context_1 = require("./scp/exec-context");
30
+ // The row-lock tail SSoT (` FOR UPDATE` / ` FOR SHARE`) — shared with the SCP `compileSelect`, so the
31
+ // imperative builder and the compiled statement render byte-identical locking clauses.
32
+ const compile_select_1 = require("./scp/makesql/compile-select");
24
33
  // AsyncLocalStorage for transaction context
25
34
  const transactionContext = new async_hooks_1.AsyncLocalStorage();
26
35
  // AsyncLocalStorage for writer context (for withWriter())
@@ -173,6 +182,132 @@ class DBModel {
173
182
  };
174
183
  /** Last transaction completion time (for writer sticky) */
175
184
  static _lastTransactionTime = 0;
185
+ /**
186
+ * The Phase A-E SCP runtime context (built lazily from `_dbConfig` for the live pg/mysql dialects).
187
+ * `null` when not yet built; `false` when the config is sqlite / uninitialized (⇒ v1 in-proc path,
188
+ * the backward-compat fallback). Rebuilt on `setConfig`. Phase F-2 (#105). @internal
189
+ */
190
+ static _scpRuntime = null;
191
+ /**
192
+ * The `scpRuntimeGeneration()` a cached `_scpRuntime` was built at. `closeAllPools()` bumps that
193
+ * generation and closes the pools; a cache from an older generation therefore points at a CLOSED pool
194
+ * and must be rebuilt on next use (#182). `-1` = never built. @internal
195
+ */
196
+ static _scpRuntimeGen = -1;
197
+ /**
198
+ * Get the SCP runtime context for this model's config (Phase F-2 / #105), lazily built from
199
+ * `_dbConfig`. Returns `null` when the model should use the v1 in-proc path (sqlite, or no live
200
+ * config, or the pg/mysql peer dep is unavailable) — the backward-compat fallback (empty middleware
201
+ * / single-DB / sqlite behavior unchanged). The routed reader/writer pools, writer-sticky, and the
202
+ * ctx seam (middleware + tx-pinning) all come from here. @internal
203
+ */
204
+ static _getScpRuntime() {
205
+ if (this._scpRuntime === false)
206
+ return null;
207
+ // A cached runtime is reusable only while its pools are still open — i.e. its generation still matches.
208
+ // After a `closeAllPools()` bumped the generation, the cache points at a closed pool; fall through to
209
+ // rebuild rather than hand back a runtime whose `acquire()` throws "pool after end" (#182).
210
+ if (this._scpRuntime !== null && this._scpRuntimeGen === (0, dbmodel_runtime_1.scpRuntimeGeneration)())
211
+ return this._scpRuntime;
212
+ const config = this._dbConfig;
213
+ const driver = config?.driver ?? (config ? 'postgres' : undefined);
214
+ if (config === null || driver === 'sqlite' || driver === undefined) {
215
+ this._scpRuntime = false;
216
+ return null;
217
+ }
218
+ try {
219
+ const rt = (0, dbmodel_runtime_1.buildContextFromConfig)({
220
+ ...(config.host !== undefined ? { host: config.host } : {}),
221
+ ...(config.port !== undefined ? { port: config.port } : {}),
222
+ ...(config.database !== undefined ? { database: config.database } : {}),
223
+ ...(config.user !== undefined ? { user: config.user } : {}),
224
+ ...(config.password !== undefined ? { password: config.password } : {}),
225
+ ...(config.max !== undefined ? { max: config.max } : {}),
226
+ ...(config.keepAlive !== undefined ? { keepAlive: config.keepAlive } : {}),
227
+ ...(config.keepAliveInitialDelayMillis !== undefined ? { keepAliveInitialDelayMillis: config.keepAliveInitialDelayMillis } : {}),
228
+ driver: driver,
229
+ }, {
230
+ ...(this._configOptions.writerConfig !== undefined ? { writerConfig: this._configOptions.writerConfig } : {}),
231
+ ...(this._configOptions.useWriterAfterTransaction !== undefined ? { useWriterAfterTransaction: this._configOptions.useWriterAfterTransaction } : {}),
232
+ ...(this._configOptions.writerStickyDuration !== undefined ? { writerStickyDuration: this._configOptions.writerStickyDuration } : {}),
233
+ });
234
+ this._scpRuntime = rt;
235
+ this._scpRuntimeGen = (0, dbmodel_runtime_1.scpRuntimeGeneration)();
236
+ return rt;
237
+ }
238
+ catch {
239
+ // The pg/mysql peer dep is unavailable (or pool construction failed) — fall back to the v1 path.
240
+ this._scpRuntime = false;
241
+ return null;
242
+ }
243
+ }
244
+ /**
245
+ * Reset the cached SCP runtime (called on `setConfig`). This clears the CACHE only; it does not close
246
+ * the displaced runtime's pools, because `setConfig` is synchronous and closing is not.
247
+ *
248
+ * It used to say the caller closed them. It did not — `setConfig` only called this — so every
249
+ * reconfigure leaked a routed pool set with no handle left to reach it. The displaced runtime stays in
250
+ * `dbmodel-runtime`'s own registry and is closed by `closeAllPools()`. @internal
251
+ */
252
+ static _resetScpRuntime() {
253
+ this._scpRuntime = null;
254
+ }
255
+ /** Cached §4.1 column-type overrides for the SCP read/write bundle (per class). @internal */
256
+ static _scpColumnTypesCache = new WeakMap();
257
+ /**
258
+ * The §4.1 column-type OVERRIDES for the SCP read/write bundle (the F1 `columnTypes` escape hatch).
259
+ * Phase F-2 (#105): a column whose SQL type cannot be derived from the decorator — no explicit
260
+ * `sqlCast` family AND no `baseSqlType` (its TS `design:type` is `Object`, e.g. a NULLABLE
261
+ * `string | null` / `number | null` field: TS emits `design:type = Object` for a union) — would
262
+ * otherwise take the blanket-INTEGER default and THROW `materialize int32` on a live string. Because
263
+ * the DBModel read path does its OWN v1 de-box (`_createInstance` → `typeCastFromDB`), the SCP typed
264
+ * read only needs a NON-THROWING, passthrough-safe SQL type for such columns: we pin them to `TEXT`
265
+ * (passthrough — the driver value flows through unchanged, then v1 casts it). Columns WITH a derivable
266
+ * type (explicit family / a non-union `design:type`) keep it. This preserves the v1 read contract
267
+ * exactly (int→number, string→string) while never throwing on an undeterminable nullable column.
268
+ * @internal
269
+ */
270
+ static _scpColumnTypes() {
271
+ const cached = DBModel._scpColumnTypesCache.get(this);
272
+ if (cached !== undefined)
273
+ return cached;
274
+ const meta = (0, decorators_1.getColumnMeta)(this);
275
+ const columnTypes = {};
276
+ if (meta !== undefined) {
277
+ for (const [propKey, m] of meta) {
278
+ const baseSqlType = m.baseSqlType;
279
+ // A column with an EXPLICIT `@column.*` family (`sqlCast`: boolean / bigint / uuid / jsonb /
280
+ // date / timestamp / *Array) keeps its derived SQL type — that family de-box matches v1's.
281
+ if (m.sqlCast !== undefined)
282
+ continue;
283
+ // A bare `@column()` whose TS `design:type` is String / Boolean / Date / BigInt has an
284
+ // UNAMBIGUOUS SQL type (option B: TEXT / BOOLEAN / TIMESTAMP / BIGINT) — keep it, so the SCP
285
+ // typed de-box fires exactly as the v1 contract (string→string, bool→boolean, …).
286
+ if (baseSqlType !== undefined && baseSqlType !== 'INTEGER')
287
+ continue;
288
+ // What remains is a bare `Number` (baseSqlType INTEGER) — AMBIGUOUS: it may back an INTEGER or a
289
+ // DECIMAL/NUMERIC column (pg returns DECIMAL as a string that `int32` materialize would REJECT)
290
+ // — OR a union field with NO `design:type` (string|null). Pin these to `TEXT` = passthrough:
291
+ // the DBModel path does its OWN v1 de-box (`_createInstance` → `typeCastFromDB`), so the SCP
292
+ // read only needs a NON-THROWING passthrough (int→number, decimal→number, string→string — the
293
+ // exact v1 read contract). This is F1's `columnTypes` escape hatch for the un-derivable number.
294
+ columnTypes[propKey] = 'TEXT';
295
+ }
296
+ }
297
+ const result = { columnTypes };
298
+ DBModel._scpColumnTypesCache.set(this, result);
299
+ return result;
300
+ }
301
+ /**
302
+ * Is the SCP execution path usable RIGHT NOW? The v1 method/SQL middleware system
303
+ * (`src/Middleware.ts` + `DBModel.use`) is a SEPARATE registry from the SCP middleware chain, so a
304
+ * registered v1 middleware must keep the v1 execute path (its `execute`/method hooks fire exactly as
305
+ * before — backward-compat). With NO v1 middleware, the SCP path (routing/tx via the ctx seam, plus
306
+ * the SCP middleware chain for `src/scp` `use()`) drives execution. Phase F-2 (#105). @internal
307
+ */
308
+ static _scpUsable() {
309
+ return DBModel._middlewares.length === 0;
310
+ }
176
311
  /**
177
312
  * Initialize DBModel with database config.
178
313
  * Call this once at application startup.
@@ -217,6 +352,11 @@ class DBModel {
217
352
  ...this._configOptions,
218
353
  ...options,
219
354
  };
355
+ // Phase F-2 (#105): drop the cached SCP runtime so the next op rebuilds it from the new config
356
+ // (new reader/writer pools + routing). The v1 DBHandler above still owns its own pools (used by
357
+ // the v1 fallback path + tx-context bridging); the SCP runtime owns the routed pools for the
358
+ // SCP execution path. Rebuilt lazily on first use.
359
+ this._resetScpRuntime();
220
360
  }
221
361
  /**
222
362
  * Get current limit configuration.
@@ -557,9 +697,9 @@ class DBModel {
557
697
  if (options.offset !== undefined) {
558
698
  sql += ` OFFSET ${options.offset}`;
559
699
  }
560
- if (options.forUpdate) {
561
- sql += ' FOR UPDATE';
562
- }
700
+ // Row lock (` FOR UPDATE` / ` FOR SHARE`) — rendered by the ONE `lockTail` aggregation point the
701
+ // SCP `compileSelect` also consumes, so the imperative and the compiled text cannot drift.
702
+ sql += (0, compile_select_1.lockTail)(options);
563
703
  if (options.append) {
564
704
  sql += ` ${options.append}`;
565
705
  }
@@ -640,10 +780,30 @@ class DBModel {
640
780
  * Build SELECT SQL and execute via query()
641
781
  * @internal
642
782
  */
643
- static _select(conditions, options = {}) {
783
+ static async _select(conditions, options = {}) {
784
+ // A `find` shape is only known PER REQUEST (`{where:{age:{gt:x}}}`), so it is built imperatively —
785
+ // compile-free — by the v1 builder. It still reaches the DB through the SCP ctx seam: `query` →
786
+ // `execute` → `_scpRawExecute`, so routing, middleware and an ambient transaction pin all apply.
644
787
  const { sql, params } = this._buildSelectSQL(conditions, options);
645
788
  return this.query(sql, params);
646
789
  }
790
+ /** Merge FIND_FILTER (the model's default filter) into a condition object (v1 `_buildSelectSQL` parity). */
791
+ static _mergeFindFilter(conditions) {
792
+ if (!this.FIND_FILTER)
793
+ return conditions;
794
+ const filter = (0, Column_1.condsToRecord)(this.FIND_FILTER);
795
+ return { ...conditions, ...filter };
796
+ }
797
+ /** Build model instances from raw rows, wiring the deferred relation context (v1 `query` parity). @internal */
798
+ static _instancesFromRows(rows) {
799
+ const instances = rows.map((row) => this._createInstance(row));
800
+ if (instances.length > 1) {
801
+ const deferredRef = [this, instances];
802
+ for (const inst of instances)
803
+ inst._deferredContext = deferredRef;
804
+ }
805
+ return instances;
806
+ }
647
807
  /**
648
808
  * Select for lazy relation loading.
649
809
  * Applies hasManyHardLimit check and supports CTE/raw conditions.
@@ -812,7 +972,48 @@ class DBModel {
812
972
  const onConflictUpdateCols = options.onConflictUpdate === 'all'
813
973
  ? 'all'
814
974
  : options.onConflictUpdate?.map(toColName);
815
- // Execute INSERT for each group
975
+ // Phase F-2 (#105): route the INSERT through the SCP batch-write bundle (compileCreateBundle →
976
+ // executeTransactionAsync on the writer, write=tx guard). `compileInsertMany` reproduces the v1
977
+ // `buildInsert` byte-for-byte (per the parity rule), so the executed SQL is identical; onConflict*
978
+ // ride the build options unchanged. The legacy raw `conflict` string (no structured onConflict) is
979
+ // NOT modeled in the SCP builder — that rare path keeps the v1 execute loop below. Requires the
980
+ // rows to be a SINGLE column-pattern group (the common case); a heterogeneous multi-pattern insert
981
+ // and the legacy conflict path fall through to the v1 loop.
982
+ const rt = this._getScpRuntime();
983
+ const singleGroup = groupedByPattern.size === 1 ? [...groupedByPattern.values()][0] : undefined;
984
+ if (rt !== null &&
985
+ this._scpUsable() &&
986
+ singleGroup !== undefined &&
987
+ !(options.conflict && !options.onConflict)) {
988
+ // The SCP MySQL batch INSERT wraps values in a JSON array (`json_each`), so a Date value would
989
+ // be JSON-serialized to an ISO string (`…T…Z`) that MySQL's `CAST(… AS DATETIME)` rejects. The v1
990
+ // direct-param path passes a Date object (mysql2 formats it). For the batch/JSON path, pre-format
991
+ // any Date to the MySQL `YYYY-MM-DD HH:MM:SS.mmm` string the CAST accepts (PG/SQLite pass Date
992
+ // straight through their own binding, so this is mysql-only). Phase F-2 (#105).
993
+ const scpRecords = rt.dialect === 'mysql' ? singleGroup.records.map((r) => this._mysqlDateStrings(r)) : singleGroup.records;
994
+ const bundle = (0, dbmodel_runtime_1.compileCreateBundle)(this, 'create', {
995
+ tableName,
996
+ records: scpRecords,
997
+ rawRecords: singleGroup.rawRecords,
998
+ sqlCastMap,
999
+ ...(onConflictCols !== undefined ? { onConflict: onConflictCols } : {}),
1000
+ ...(options.onConflictIgnore !== undefined ? { onConflictIgnore: options.onConflictIgnore } : {}),
1001
+ ...(onConflictUpdateCols !== undefined ? { onConflictUpdate: onConflictUpdateCols } : {}),
1002
+ ...(options.returning !== undefined ? { returning: options.returning } : {}),
1003
+ // The model's PK travels with the write so a dialect without native RETURNING can recover
1004
+ // the rows it wrote by the REAL key — the auto-increment range, or the values the write
1005
+ // itself bound (UUID / client-supplied / composite). Omitting it is what made those
1006
+ // models silently return no rows.
1007
+ pk: (() => {
1008
+ const k = (0, decorators_1.getPrimaryKey)(this);
1009
+ return { columns: k.columns, autoInc: k.autoInc };
1010
+ })(),
1011
+ }, rt.dialect, this._scpColumnTypes());
1012
+ const result = await (0, dbmodel_runtime_1.executeWriteAsync)(bundle, {}, rt);
1013
+ const rows = this._writeResultRows(result);
1014
+ return rows.map((row) => this._createInstance(row));
1015
+ }
1016
+ // Execute INSERT for each group (v1 path — heterogeneous patterns / legacy conflict / v1 fallback).
816
1017
  const allResults = [];
817
1018
  for (const { columns, records: groupRecords, rawRecords: groupRawRecords } of groupedByPattern.values()) {
818
1019
  // Use SqlBuilder to build the INSERT SQL
@@ -925,6 +1126,56 @@ class DBModel {
925
1126
  instance.typeCastFromDB();
926
1127
  return instance;
927
1128
  }
1129
+ /**
1130
+ * Route a raw `(sql, params)` through the SCP ctx seam (the escape-hatch / Fragment path). A
1131
+ * SELECT / …RETURNING / WITH / VALUES / SHOW / PRAGMA runs the READ seam (reader routing / an
1132
+ * ambient tx pin); any other statement the WRITE seam (writer routing / the tx pin). The seam
1133
+ * applies the SCP middleware chain + connection routing; inside a SCP `transaction(fn)` the ALS pin
1134
+ * resolves the tx connection so the statement joins the transaction. Phase F-2 (#105). @internal
1135
+ */
1136
+ static async _scpRawExecute(sql, params, rt) {
1137
+ const ctx = rt.ctx;
1138
+ // v1-built SQL uses `?` placeholders; the SCP seam passes SQL verbatim, so renumber `?`→`$N` for
1139
+ // postgres (mysql keeps `?`) — the conversion v1's DBHandler did on the imperative path.
1140
+ const rendered = (0, dbmodel_runtime_1.renderRawSql)(sql, rt.dialect);
1141
+ const returnsRows = /^\s*(select|with|show|pragma|values|explain|table)\b/i.test(rendered) || /\breturning\b/i.test(rendered);
1142
+ if (returnsRows) {
1143
+ return (0, exec_context_1.executeAsync)(ctx, rendered, params, { write: false });
1144
+ }
1145
+ await (0, exec_context_1.runAsync)(ctx, rendered, params, { write: true });
1146
+ return [];
1147
+ }
1148
+ /**
1149
+ * Convert any `Date` value in a record to the MySQL `YYYY-MM-DD HH:MM:SS.mmm` string (server-local
1150
+ * wall-clock, matching mysql2's own Date→DATETIME formatting) so it survives the SCP MySQL batch
1151
+ * INSERT's JSON-array round-trip + `CAST(… AS DATETIME)`. Non-Date values pass through. @internal
1152
+ */
1153
+ static _mysqlDateStrings(record) {
1154
+ let out;
1155
+ for (const [k, v] of Object.entries(record)) {
1156
+ if (v instanceof Date) {
1157
+ if (out === undefined)
1158
+ out = { ...record };
1159
+ const p = (n, w = 2) => String(n).padStart(w, '0');
1160
+ out[k] = `${v.getFullYear()}-${p(v.getMonth() + 1)}-${p(v.getDate())} ${p(v.getHours())}:${p(v.getMinutes())}:${p(v.getSeconds())}.${p(v.getMilliseconds(), 3)}`;
1161
+ }
1162
+ }
1163
+ return out ?? record;
1164
+ }
1165
+ /**
1166
+ * Flatten a SCP {@link executeWriteAsync} result (a `TransactionResult`) into the RETURNING rows the
1167
+ * v1 write path returns: a batch write exposes every group's rows via `returnedRows`; a single-write
1168
+ * Command exposes its one row via `entity`. A non-returning write yields `[]`. Phase F-2 (#105). @internal
1169
+ */
1170
+ static _writeResultRows(result) {
1171
+ if (result.returnedRows !== undefined) {
1172
+ const out = [];
1173
+ for (const group of result.returnedRows)
1174
+ out.push(...group);
1175
+ return out;
1176
+ }
1177
+ return result.entity !== null ? [result.entity] : [];
1178
+ }
928
1179
  /**
929
1180
  * Build PkeyResult from rows containing primary key values
930
1181
  * @internal
@@ -1118,44 +1369,38 @@ class DBModel {
1118
1369
  /** Cache for primary key columns detected from @column({ primaryKey: true }) */
1119
1370
  static _pkeyColumnsCache = new WeakMap();
1120
1371
  /**
1121
- * Get primary key columns with fallback to ['id']
1122
- * Priority: 1. PKEY_COLUMNS getter 2. @column({ primaryKey: true }) 3. 'id' default
1372
+ * The primary key as v1 `Column` objects. WHICH columns those are is resolved by the ONE
1373
+ * derivation ({@link getPrimaryKey}, in the layer that owns column metadata); this method only
1374
+ * maps that answer onto the `Column` statics the imperative builders bind against.
1123
1375
  * @internal
1124
1376
  */
1125
1377
  static _getPkeyColumnsWithDefault() {
1126
- // 1. Explicit PKEY_COLUMNS takes precedence
1378
+ // 1. Explicit PKEY_COLUMNS takes precedence — already Column objects.
1127
1379
  if (this.PKEY_COLUMNS) {
1128
1380
  return this.PKEY_COLUMNS;
1129
1381
  }
1130
- // 2. Check cache for decorator-detected primary keys
1131
1382
  const cached = DBModel._pkeyColumnsCache.get(this);
1132
1383
  if (cached) {
1133
1384
  return cached;
1134
1385
  }
1135
- // 3. Detect from @column({ primaryKey: true }) decorator
1136
- const meta = (0, decorators_1.getColumnMeta)(this);
1137
- if (meta) {
1138
- const pkeyColumns = [];
1386
+ const pkey = (0, decorators_1.getPrimaryKey)(this);
1387
+ let pkeyColumns = [];
1388
+ if (pkey.declared) {
1389
+ // The declared PK: take each property's static Column (a callable carrying `columnName`).
1139
1390
  const thisClass = this;
1140
- for (const [propKey, colMeta] of meta) {
1141
- if (colMeta.primaryKey) {
1142
- // Get the static Column property from the class
1143
- // Note: Column is a callable function, so typeof is 'function', not 'object'
1144
- const col = thisClass[propKey];
1145
- if (col && typeof col === 'function' && 'columnName' in col) {
1146
- pkeyColumns.push(col);
1147
- }
1391
+ for (const propKey of pkey.propKeys) {
1392
+ const col = thisClass[propKey];
1393
+ if (col && typeof col === 'function' && 'columnName' in col) {
1394
+ pkeyColumns.push(col);
1148
1395
  }
1149
1396
  }
1150
- if (pkeyColumns.length > 0) {
1151
- DBModel._pkeyColumnsCache.set(this, pkeyColumns);
1152
- return pkeyColumns;
1153
- }
1154
1397
  }
1155
- // 4. Default: create a Column for 'id'
1156
- const defaultPkey = [(0, Column_1.createColumn)('id', this.TABLE_NAME, this.name)];
1157
- DBModel._pkeyColumnsCache.set(this, defaultPkey);
1158
- return defaultPkey;
1398
+ if (pkeyColumns.length === 0) {
1399
+ // The legacy default (or a declared PK with no matching Column static): synthesize it.
1400
+ pkeyColumns = pkey.columns.map((c) => (0, Column_1.createColumn)(c, this.TABLE_NAME, this.name));
1401
+ }
1402
+ DBModel._pkeyColumnsCache.set(this, pkeyColumns);
1403
+ return pkeyColumns;
1159
1404
  }
1160
1405
  /**
1161
1406
  * Get GROUP BY clause as string
@@ -2168,6 +2413,17 @@ class DBModel {
2168
2413
  sql = sqlOrFragment;
2169
2414
  }
2170
2415
  const core = async (s, p) => {
2416
+ // Phase F-2 (#105): the ESCAPE-HATCH / Fragment path through the SCP ctx seam. When the SCP
2417
+ // runtime is active (and no v1 middleware), route raw execute/query THROUGH the seam so an
2418
+ // ambient SCP transaction pin (the tx connection), reader/writer routing, and the SCP middleware
2419
+ // chain all apply — the SAME central point every SCP-generated statement uses. A SELECT/RETURNING
2420
+ // runs the read seam; any other statement the write seam (writer routing). This is what lets a v1
2421
+ // -fallback op (CTE / QUERY view / updateMany / findById) join a `transaction(fn)` correctly.
2422
+ const rt = this._getScpRuntime();
2423
+ if (rt !== null && this._scpUsable()) {
2424
+ const rows = await this._scpRawExecute(s, p || [], rt);
2425
+ return { rows, rowCount: rows.length };
2426
+ }
2171
2427
  const handler = this.getHandler();
2172
2428
  // Use writer pool if in withWriter context or writer sticky period
2173
2429
  const useWriter = this._getWriterContext().getStore() !== undefined || this._shouldUseWriterSticky();
@@ -2271,6 +2527,29 @@ class DBModel {
2271
2527
  // Already in a transaction, just execute the function
2272
2528
  return func();
2273
2529
  }
2530
+ // Phase F-2 (#105): when the SCP runtime is active (and no v1 middleware), open the transaction on
2531
+ // the SCP tx runtime (`scpTransaction` — one owned pooled connection pinned in the SCP ALS, the
2532
+ // isolation-aware BEGIN/COMMIT + #81 retry). Every op inside `func` joins it: an SCP write via
2533
+ // `executeWriteAsync` (its `executeTransactionAsync` sees the ambient pin → no new BEGIN), an SCP
2534
+ // read / a v1-fallback `execute()` via the SCP seam (the ALS pin resolves the tx connection). We
2535
+ // ALSO set the v1 `transactionContext` marker (a sentinel connection) so v1 code that checks
2536
+ // `inTransaction()` / `_checkWriteAllowed` / `getCurrentConnection()` still sees an active tx.
2537
+ const rt = this._getScpRuntime();
2538
+ if (rt !== null && this._scpUsable()) {
2539
+ const useWriterAfterTx = options.useWriterAfterTransaction ?? this._configOptions.useWriterAfterTransaction ?? true;
2540
+ const result = await (0, exec_context_1.transaction)(rt.ctx, () => {
2541
+ // Set the v1 tx marker for the duration of the body (same async scope as the SCP pin). The
2542
+ // sentinel connection is never used for I/O — v1-fallback ops route through the SCP seam.
2543
+ const sentinel = {
2544
+ query: async () => ({ rows: [], rowCount: 0 }),
2545
+ release: () => undefined,
2546
+ };
2547
+ return txContext.run({ connection: sentinel }, func);
2548
+ }, options, rt.dialect);
2549
+ if (useWriterAfterTx && !(options.rollbackOnly ?? false))
2550
+ this._lastTransactionTime = Date.now();
2551
+ return result;
2552
+ }
2274
2553
  // Get handler
2275
2554
  const handler = this.getHandler();
2276
2555
  const retryOnError = options.retryOnError ?? true;
@@ -2477,6 +2756,10 @@ class DBModel {
2477
2756
  ...options,
2478
2757
  };
2479
2758
  static _lastTransactionTime = 0;
2759
+ // Phase F-2 (#105): this base class gets its OWN SCP runtime slot (built lazily from ITS config),
2760
+ // isolated from DBModel's default-config runtime and from other base classes — otherwise a
2761
+ // sqlite/mysql base would inherit DBModel's shared pg runtime and route its SQL to the wrong DB.
2762
+ static _scpRuntime = null;
2480
2763
  // Override to use this base class's transaction context
2481
2764
  static _getTransactionContext() {
2482
2765
  return baseTransactionContext;