litedbmodel 2.0.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/DBConditions.d.ts.map +1 -1
- package/dist/DBConditions.js +16 -1
- package/dist/DBConditions.js.map +1 -1
- package/dist/DBHandler.d.ts.map +1 -1
- package/dist/DBHandler.js +6 -0
- package/dist/DBHandler.js.map +1 -1
- package/dist/DBModel.d.ts +84 -2
- package/dist/DBModel.d.ts.map +1 -1
- package/dist/DBModel.js +312 -29
- package/dist/DBModel.js.map +1 -1
- package/dist/decorators.d.ts +52 -0
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +127 -24
- package/dist/decorators.js.map +1 -1
- package/dist/drivers/mysql.d.ts.map +1 -1
- package/dist/drivers/mysql.js +12 -2
- package/dist/drivers/mysql.js.map +1 -1
- package/dist/drivers/postgres.d.ts.map +1 -1
- package/dist/drivers/postgres.js +13 -2
- package/dist/drivers/postgres.js.map +1 -1
- package/dist/drivers/sqlite.d.ts +2 -0
- package/dist/drivers/sqlite.d.ts.map +1 -1
- package/dist/drivers/sqlite.js +49 -4
- package/dist/drivers/sqlite.js.map +1 -1
- package/dist/index.cjs +10741 -0
- package/dist/index.d.cts +1 -0
- package/dist/scp/coltype.d.ts +108 -0
- package/dist/scp/coltype.d.ts.map +1 -1
- package/dist/scp/coltype.js +293 -6
- package/dist/scp/coltype.js.map +1 -1
- package/dist/scp/connection-routing.d.ts +312 -0
- package/dist/scp/connection-routing.d.ts.map +1 -0
- package/dist/scp/connection-routing.js +394 -0
- package/dist/scp/connection-routing.js.map +1 -0
- package/dist/scp/dbmodel-runtime.d.ts +101 -0
- package/dist/scp/dbmodel-runtime.d.ts.map +1 -0
- package/dist/scp/dbmodel-runtime.js +174 -0
- package/dist/scp/dbmodel-runtime.js.map +1 -0
- package/dist/scp/decorator-adapter.d.ts +196 -0
- package/dist/scp/decorator-adapter.d.ts.map +1 -0
- package/dist/scp/decorator-adapter.js +286 -0
- package/dist/scp/decorator-adapter.js.map +1 -0
- package/dist/scp/emit/emitter.d.ts +108 -0
- package/dist/scp/emit/emitter.d.ts.map +1 -0
- package/dist/scp/emit/emitter.js +782 -0
- package/dist/scp/emit/emitter.js.map +1 -0
- package/dist/scp/emit/endpoint.d.ts +248 -0
- package/dist/scp/emit/endpoint.d.ts.map +1 -0
- package/dist/scp/emit/endpoint.js +20 -0
- package/dist/scp/emit/endpoint.js.map +1 -0
- package/dist/scp/emit/index.d.ts +11 -0
- package/dist/scp/emit/index.d.ts.map +1 -0
- package/dist/scp/emit/index.js +13 -0
- package/dist/scp/emit/index.js.map +1 -0
- package/dist/scp/errors.d.ts +45 -1
- package/dist/scp/errors.d.ts.map +1 -1
- package/dist/scp/errors.js +47 -1
- package/dist/scp/errors.js.map +1 -1
- package/dist/scp/exec-context.d.ts +390 -0
- package/dist/scp/exec-context.d.ts.map +1 -0
- package/dist/scp/exec-context.js +540 -0
- package/dist/scp/exec-context.js.map +1 -0
- package/dist/scp/grouping.d.ts +32 -0
- package/dist/scp/grouping.d.ts.map +1 -0
- package/dist/scp/grouping.js +0 -0
- package/dist/scp/grouping.js.map +1 -0
- package/dist/scp/index.cjs +6432 -17579
- package/dist/scp/index.d.cts +1 -0
- package/dist/scp/index.d.mts +1 -0
- package/dist/scp/index.d.ts +51 -52
- package/dist/scp/index.d.ts.map +1 -1
- package/dist/scp/index.js +189 -152
- package/dist/scp/index.js.map +1 -1
- package/dist/scp/index.mjs +6067 -3659
- package/dist/scp/leaf-transport.d.ts +50 -0
- package/dist/scp/leaf-transport.d.ts.map +1 -0
- package/dist/scp/leaf-transport.js +92 -0
- package/dist/scp/leaf-transport.js.map +1 -0
- package/dist/scp/leaves.d.ts +174 -0
- package/dist/scp/leaves.d.ts.map +1 -0
- package/dist/scp/leaves.js +287 -0
- package/dist/scp/leaves.js.map +1 -0
- package/dist/scp/limit-config.d.ts +118 -0
- package/dist/scp/limit-config.d.ts.map +1 -0
- package/dist/scp/limit-config.js +126 -0
- package/dist/scp/limit-config.js.map +1 -0
- package/dist/scp/makesql/compile-relation.d.ts +49 -23
- package/dist/scp/makesql/compile-relation.d.ts.map +1 -1
- package/dist/scp/makesql/compile-relation.js +101 -76
- package/dist/scp/makesql/compile-relation.js.map +1 -1
- package/dist/scp/makesql/compile-select.d.ts +45 -10
- package/dist/scp/makesql/compile-select.d.ts.map +1 -1
- package/dist/scp/makesql/compile-select.js +50 -10
- package/dist/scp/makesql/compile-select.js.map +1 -1
- package/dist/scp/makesql/handler.d.ts +1 -1
- package/dist/scp/makesql/handler.d.ts.map +1 -1
- package/dist/scp/makesql/index.d.ts +7 -10
- package/dist/scp/makesql/index.d.ts.map +1 -1
- package/dist/scp/makesql/index.js +19 -30
- package/dist/scp/makesql/index.js.map +1 -1
- package/dist/scp/makesql/json-array.d.ts +65 -0
- package/dist/scp/makesql/json-array.d.ts.map +1 -1
- package/dist/scp/makesql/json-array.js +99 -10
- package/dist/scp/makesql/json-array.js.map +1 -1
- package/dist/scp/makesql/json-batch.d.ts.map +1 -1
- package/dist/scp/makesql/json-batch.js +21 -19
- package/dist/scp/makesql/json-batch.js.map +1 -1
- package/dist/scp/makesql/makesql.d.ts +1 -1
- package/dist/scp/makesql/makesql.d.ts.map +1 -1
- package/dist/scp/makesql/mysql-returning.d.ts +93 -0
- package/dist/scp/makesql/mysql-returning.d.ts.map +1 -0
- package/dist/scp/makesql/mysql-returning.js +215 -0
- package/dist/scp/makesql/mysql-returning.js.map +1 -0
- package/dist/scp/makesql/outtype.d.ts +75 -34
- package/dist/scp/makesql/outtype.d.ts.map +1 -1
- package/dist/scp/makesql/outtype.js +91 -118
- package/dist/scp/makesql/outtype.js.map +1 -1
- package/dist/scp/makesql/pool-executor.d.ts +135 -13
- package/dist/scp/makesql/pool-executor.d.ts.map +1 -1
- package/dist/scp/makesql/pool-executor.js +229 -21
- package/dist/scp/makesql/pool-executor.js.map +1 -1
- package/dist/scp/makesql/tx.d.ts +107 -22
- package/dist/scp/makesql/tx.d.ts.map +1 -1
- package/dist/scp/makesql/tx.js +451 -97
- package/dist/scp/makesql/tx.js.map +1 -1
- package/dist/scp/makesql/writeouttype.d.ts +7 -8
- package/dist/scp/makesql/writeouttype.d.ts.map +1 -1
- package/dist/scp/makesql/writeouttype.js +8 -7
- package/dist/scp/makesql/writeouttype.js.map +1 -1
- package/dist/scp/middleware.d.ts +287 -0
- package/dist/scp/middleware.d.ts.map +1 -0
- package/dist/scp/middleware.js +358 -0
- package/dist/scp/middleware.js.map +1 -0
- package/dist/scp/relation.d.ts +80 -9
- package/dist/scp/relation.d.ts.map +1 -1
- package/dist/scp/relation.js +198 -78
- package/dist/scp/relation.js.map +1 -1
- package/dist/scp/tx-options.d.ts +136 -0
- package/dist/scp/tx-options.d.ts.map +1 -0
- package/dist/scp/tx-options.js +184 -0
- package/dist/scp/tx-options.js.map +1 -0
- package/dist/scp/write-bundle.d.ts +131 -0
- package/dist/scp/write-bundle.d.ts.map +1 -0
- package/dist/scp/write-bundle.js +160 -0
- package/dist/scp/write-bundle.js.map +1 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/docs/BENCHMARK.md +58 -56
- package/docs/benchmark-chart.svg +166 -166
- package/docs/design/phase-a-execution-context.md +139 -0
- package/package.json +16 -9
- package/dist/scp/authoring-sql.d.ts +0 -134
- package/dist/scp/authoring-sql.d.ts.map +0 -1
- package/dist/scp/authoring-sql.js +0 -191
- package/dist/scp/authoring-sql.js.map +0 -1
- package/dist/scp/authoring.d.ts +0 -155
- package/dist/scp/authoring.d.ts.map +0 -1
- package/dist/scp/authoring.js +0 -162
- package/dist/scp/authoring.js.map +0 -1
- package/dist/scp/catalog.d.ts +0 -94
- package/dist/scp/catalog.d.ts.map +0 -1
- package/dist/scp/catalog.js +0 -234
- package/dist/scp/catalog.js.map +0 -1
- package/dist/scp/codegen.d.ts +0 -189
- package/dist/scp/codegen.d.ts.map +0 -1
- package/dist/scp/codegen.js +0 -421
- package/dist/scp/codegen.js.map +0 -1
- package/dist/scp/find-filter-guard.d.ts +0 -68
- package/dist/scp/find-filter-guard.d.ts.map +0 -1
- package/dist/scp/find-filter-guard.js +0 -88
- package/dist/scp/find-filter-guard.js.map +0 -1
- package/dist/scp/guard.d.ts +0 -43
- package/dist/scp/guard.d.ts.map +0 -1
- package/dist/scp/guard.js +0 -134
- package/dist/scp/guard.js.map +0 -1
- package/dist/scp/makesql/authoring-compile.d.ts +0 -90
- package/dist/scp/makesql/authoring-compile.d.ts.map +0 -1
- package/dist/scp/makesql/authoring-compile.js +0 -494
- package/dist/scp/makesql/authoring-compile.js.map +0 -1
- package/dist/scp/makesql/ir.d.ts +0 -42
- package/dist/scp/makesql/ir.d.ts.map +0 -1
- package/dist/scp/makesql/ir.js +0 -66
- package/dist/scp/makesql/ir.js.map +0 -1
- package/dist/scp/makesql/static-bundle.d.ts +0 -187
- package/dist/scp/makesql/static-bundle.d.ts.map +0 -1
- package/dist/scp/makesql/static-bundle.js +0 -1113
- package/dist/scp/makesql/static-bundle.js.map +0 -1
- package/dist/scp/runtime.d.ts +0 -242
- package/dist/scp/runtime.d.ts.map +0 -1
- package/dist/scp/runtime.js +0 -400
- package/dist/scp/runtime.js.map +0 -1
package/dist/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
|
-
|
|
561
|
-
|
|
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
|
-
//
|
|
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
|
-
*
|
|
1122
|
-
*
|
|
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
|
-
|
|
1136
|
-
|
|
1137
|
-
if (
|
|
1138
|
-
|
|
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
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
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
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
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;
|