turbine-orm 0.29.0 → 0.30.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 +1 -1
- package/dist/cjs/cli/index.js +5 -0
- package/dist/cjs/cli/mcp.js +22 -92
- package/dist/cjs/client.js +33 -3
- package/dist/cjs/generate.js +71 -25
- package/dist/cjs/index.js +4 -1
- package/dist/cjs/introspect.js +350 -120
- package/dist/cjs/mssql.js +18 -133
- package/dist/cjs/mysql.js +16 -129
- package/dist/cjs/optional-peer-import.cjs +122 -0
- package/dist/cjs/powdb.js +424 -81
- package/dist/cjs/powql.js +49 -25
- package/dist/cjs/query/builder.js +290 -23
- package/dist/cjs/query/filters.js +32 -1
- package/dist/cjs/schema-metadata.js +316 -0
- package/dist/cjs/sqlite.js +8 -89
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +5 -0
- package/dist/cli/mcp.d.ts +18 -0
- package/dist/cli/mcp.js +22 -93
- package/dist/client.d.ts +6 -2
- package/dist/client.js +33 -3
- package/dist/generate.d.ts +16 -4
- package/dist/generate.js +71 -25
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/introspect.d.ts +94 -1
- package/dist/introspect.js +345 -120
- package/dist/mssql.js +16 -101
- package/dist/mysql.js +14 -97
- package/dist/optional-peer-import.cjs +89 -0
- package/dist/optional-peer-import.d.cts +53 -0
- package/dist/powdb.d.ts +87 -25
- package/dist/powdb.js +419 -80
- package/dist/powql.d.ts +6 -0
- package/dist/powql.js +51 -27
- package/dist/query/builder.d.ts +60 -3
- package/dist/query/builder.js +291 -24
- package/dist/query/deferred.d.ts +7 -2
- package/dist/query/filters.d.ts +18 -0
- package/dist/query/filters.js +30 -0
- package/dist/query/types.d.ts +19 -0
- package/dist/schema-metadata.d.ts +77 -0
- package/dist/schema-metadata.js +313 -0
- package/dist/schema.d.ts +10 -0
- package/dist/sqlite.js +9 -90
- package/package.json +3 -3
package/dist/cjs/powdb.js
CHANGED
|
@@ -25,6 +25,11 @@
|
|
|
25
25
|
* impossible → it degrades to batched N+1 loaders (Phase B).
|
|
26
26
|
* - **Single global write lock; no savepoints/isolation** — nested
|
|
27
27
|
* transactions / isolation / vector / LISTEN-NOTIFY / RLS throw.
|
|
28
|
+
* Independent concurrent `db.$transaction` calls do NOT throw: they queue
|
|
29
|
+
* FIFO on a pool-level gate and run one at a time (see {@link PowdbTxGate}).
|
|
30
|
+
* Only a *re-entrant* transaction — a `db.$transaction` opened from inside
|
|
31
|
+
* an active transaction callback's async context, which queueing would
|
|
32
|
+
* deadlock — fails fast with E017.
|
|
28
33
|
* - **The wire protocol pipelines** — `@zvndev/powdb-client` writes each
|
|
29
34
|
* request frame immediately and matches replies FIFO, so multiple queries
|
|
30
35
|
* may be in flight on one connection. {@link PowdbPool}'s checked-out
|
|
@@ -81,11 +86,15 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
81
86
|
return result;
|
|
82
87
|
};
|
|
83
88
|
})();
|
|
89
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
90
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
91
|
+
};
|
|
84
92
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
85
|
-
exports.PowqlInterface = exports.PowdbEmbeddedPool = exports.PowdbPool = exports.MIN_POWDB_VERSION = exports.PowdbFloatParam = exports.powdbDialect = void 0;
|
|
93
|
+
exports.PowqlInterface = exports.PowdbEmbeddedPool = exports.PowdbPool = exports.DEFAULT_TX_QUEUE_TIMEOUT_MS = exports.POWQL_KEYWORDS = exports.MIN_POWDB_VERSION = exports.PowdbFloatParam = exports.powdbDialect = void 0;
|
|
86
94
|
exports.parsePowdbUrl = parsePowdbUrl;
|
|
87
95
|
exports.assertSupportedPowdbVersion = assertSupportedPowdbVersion;
|
|
88
96
|
exports.powqlColumnType = powqlColumnType;
|
|
97
|
+
exports.quotePowqlIdent = quotePowqlIdent;
|
|
89
98
|
exports.powqlSchemaDDL = powqlSchemaDDL;
|
|
90
99
|
exports.coerceValue = coerceValue;
|
|
91
100
|
exports.rowToEntity = rowToEntity;
|
|
@@ -93,9 +102,11 @@ exports.wrapPowdbError = wrapPowdbError;
|
|
|
93
102
|
exports.encodePowqlLiteral = encodePowqlLiteral;
|
|
94
103
|
exports.materializePowql = materializePowql;
|
|
95
104
|
exports.turbinePowDB = turbinePowDB;
|
|
105
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
96
106
|
const client_js_1 = require("./client.js");
|
|
97
107
|
const dialect_js_1 = require("./dialect.js");
|
|
98
108
|
const errors_js_1 = require("./errors.js");
|
|
109
|
+
const optional_peer_import_cjs_1 = __importDefault(require("./optional-peer-import.cjs"));
|
|
99
110
|
/**
|
|
100
111
|
* Capability descriptor for PowDB. PowQL generation is owned by
|
|
101
112
|
* {@link PowqlInterface} (not the SQL `Dialect`), so this dialect exists only to
|
|
@@ -111,9 +122,11 @@ const errors_js_1 = require("./errors.js");
|
|
|
111
122
|
* {@link UnsupportedFeatureError} (E017): a nested `tx.$transaction` emits a
|
|
112
123
|
* savepoint synchronously (before any DB call) and so fails fast with a
|
|
113
124
|
* clear typed error instead of leaking PowDB's cryptic `Parse(... 'sp_1')`.
|
|
114
|
-
* The pool-level
|
|
115
|
-
* other
|
|
116
|
-
*
|
|
125
|
+
* The pool-level transaction gate (see {@link PowdbTxGate}) handles the
|
|
126
|
+
* other shapes: a fresh top-level `db.$transaction` opened inside an
|
|
127
|
+
* already-open one throws E017 before it can deadlock on the write lock,
|
|
128
|
+
* while INDEPENDENT concurrent `db.$transaction` calls queue FIFO and run
|
|
129
|
+
* one at a time instead of failing.
|
|
117
130
|
* Isolation levels remain Phase B.
|
|
118
131
|
*/
|
|
119
132
|
exports.powdbDialect = {
|
|
@@ -259,6 +272,125 @@ function isDateColumn(col) {
|
|
|
259
272
|
* `auto` modifier, so PowDB assigns a monotonic id on insert and Turbine stops
|
|
260
273
|
* synthesizing a client-side value for it.
|
|
261
274
|
*/
|
|
275
|
+
/**
|
|
276
|
+
* PowQL reserved words — the v0.10 lexer keyword table from POWQL.md's
|
|
277
|
+
* "Reserved Words and Quoting" section, including the v0.10 additions
|
|
278
|
+
* `schema` and `describe`. Keyword matching is case-sensitive in the lexer,
|
|
279
|
+
* so only the exact lowercase form collides.
|
|
280
|
+
*/
|
|
281
|
+
exports.POWQL_KEYWORDS = new Set([
|
|
282
|
+
'abs',
|
|
283
|
+
'add',
|
|
284
|
+
'alter',
|
|
285
|
+
'and',
|
|
286
|
+
'as',
|
|
287
|
+
'asc',
|
|
288
|
+
'auto',
|
|
289
|
+
'avg',
|
|
290
|
+
'begin',
|
|
291
|
+
'between',
|
|
292
|
+
'case',
|
|
293
|
+
'cast',
|
|
294
|
+
'ceil',
|
|
295
|
+
'column',
|
|
296
|
+
'commit',
|
|
297
|
+
'concat',
|
|
298
|
+
'conflict',
|
|
299
|
+
'count',
|
|
300
|
+
'cross',
|
|
301
|
+
'date_add',
|
|
302
|
+
'date_diff',
|
|
303
|
+
'default',
|
|
304
|
+
'delete',
|
|
305
|
+
'dense_rank',
|
|
306
|
+
'desc',
|
|
307
|
+
'describe',
|
|
308
|
+
'distinct',
|
|
309
|
+
'drop',
|
|
310
|
+
'else',
|
|
311
|
+
'end',
|
|
312
|
+
'exists',
|
|
313
|
+
'explain',
|
|
314
|
+
'extract',
|
|
315
|
+
'false',
|
|
316
|
+
'filter',
|
|
317
|
+
'floor',
|
|
318
|
+
'group',
|
|
319
|
+
'having',
|
|
320
|
+
'in',
|
|
321
|
+
'index',
|
|
322
|
+
'inner',
|
|
323
|
+
'insert',
|
|
324
|
+
'is',
|
|
325
|
+
'join',
|
|
326
|
+
'left',
|
|
327
|
+
'length',
|
|
328
|
+
'let',
|
|
329
|
+
'like',
|
|
330
|
+
'limit',
|
|
331
|
+
'link',
|
|
332
|
+
'lower',
|
|
333
|
+
'match',
|
|
334
|
+
'materialize',
|
|
335
|
+
'materialized',
|
|
336
|
+
'max',
|
|
337
|
+
'min',
|
|
338
|
+
'multi',
|
|
339
|
+
'not',
|
|
340
|
+
'now',
|
|
341
|
+
'null',
|
|
342
|
+
'offset',
|
|
343
|
+
'on',
|
|
344
|
+
'or',
|
|
345
|
+
'order',
|
|
346
|
+
'outer',
|
|
347
|
+
'over',
|
|
348
|
+
'partition',
|
|
349
|
+
'pow',
|
|
350
|
+
'rank',
|
|
351
|
+
'refresh',
|
|
352
|
+
'required',
|
|
353
|
+
'returning',
|
|
354
|
+
'right',
|
|
355
|
+
'rollback',
|
|
356
|
+
'round',
|
|
357
|
+
'row_number',
|
|
358
|
+
'schema',
|
|
359
|
+
'select',
|
|
360
|
+
'sqrt',
|
|
361
|
+
'substring',
|
|
362
|
+
'sum',
|
|
363
|
+
'then',
|
|
364
|
+
'transaction',
|
|
365
|
+
'trim',
|
|
366
|
+
'true',
|
|
367
|
+
'type',
|
|
368
|
+
'union',
|
|
369
|
+
'unique',
|
|
370
|
+
'update',
|
|
371
|
+
'upper',
|
|
372
|
+
'upsert',
|
|
373
|
+
'view',
|
|
374
|
+
'when',
|
|
375
|
+
]);
|
|
376
|
+
const POWQL_BARE_IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
377
|
+
/**
|
|
378
|
+
* Backtick-quote an identifier when PowQL would otherwise lex it as a keyword
|
|
379
|
+
* (or when it contains characters outside the bare-identifier grammar).
|
|
380
|
+
* Applied only in bare-identifier positions — DDL type/field names, index DDL,
|
|
381
|
+
* and `insert`/`update`/`upsert` assignment targets. Dotted references
|
|
382
|
+
* (`.col` in filters/projections/ordering) bypass keyword lookup on every
|
|
383
|
+
* engine version and deliberately stay bare for ≤0.9 compatibility. Backticks
|
|
384
|
+
* parse on PowDB ≥ 0.10; on older engines these names were already parse
|
|
385
|
+
* errors when emitted bare, so quoting is strictly an improvement.
|
|
386
|
+
*/
|
|
387
|
+
function quotePowqlIdent(name) {
|
|
388
|
+
if (name.includes('`')) {
|
|
389
|
+
// The lexer has no backtick escape inside a quoted identifier.
|
|
390
|
+
throw new errors_js_1.ValidationError(`[turbine] Identifier "${name}" contains a backtick, which PowQL cannot represent.`);
|
|
391
|
+
}
|
|
392
|
+
return exports.POWQL_KEYWORDS.has(name) || !POWQL_BARE_IDENT.test(name) ? `\`${name}\`` : name;
|
|
393
|
+
}
|
|
262
394
|
function powqlSchemaDDL(schema) {
|
|
263
395
|
const stmts = [];
|
|
264
396
|
for (const meta of Object.values(schema.tables)) {
|
|
@@ -280,13 +412,13 @@ function powqlSchemaDDL(schema) {
|
|
|
280
412
|
// a plain typed column (Turbine assigns the value client-side instead).
|
|
281
413
|
if (col.isGenerated && powqlColumnType(col) === 'int')
|
|
282
414
|
mods.push('auto');
|
|
283
|
-
return ` ${mods.join(' ')}${mods.length ? ' ' : ''}${col.name}: ${powqlColumnType(col)}`;
|
|
415
|
+
return ` ${mods.join(' ')}${mods.length ? ' ' : ''}${quotePowqlIdent(col.name)}: ${powqlColumnType(col)}`;
|
|
284
416
|
});
|
|
285
|
-
stmts.push(`type ${meta.name} {\n${fields.join(',\n')}\n}`);
|
|
417
|
+
stmts.push(`type ${quotePowqlIdent(meta.name)} {\n${fields.join(',\n')}\n}`);
|
|
286
418
|
// Secondary unique constraints (beyond the PK) become unique indexes.
|
|
287
419
|
for (const uniq of meta.uniqueColumns) {
|
|
288
420
|
if (uniq.length === 1 && !pkSet.has(uniq[0])) {
|
|
289
|
-
stmts.push(`alter ${meta.name} add unique .${uniq[0]}`);
|
|
421
|
+
stmts.push(`alter ${quotePowqlIdent(meta.name)} add unique .${quotePowqlIdent(uniq[0])}`);
|
|
290
422
|
}
|
|
291
423
|
}
|
|
292
424
|
}
|
|
@@ -386,6 +518,12 @@ function wrapPowdbError(err) {
|
|
|
386
518
|
const m = /column ['"]?(\w+)['"]?/i.exec(msg);
|
|
387
519
|
return new errors_js_1.NotNullViolationError({ column: m?.[1], cause: err });
|
|
388
520
|
}
|
|
521
|
+
// Server-side transaction-gate wait bound (PowDB ≥ 0.10, default 5s): another
|
|
522
|
+
// connection held the single global write lock past the server's
|
|
523
|
+
// --tx-wait-timeout-ms. Retryable timeout, not a query defect.
|
|
524
|
+
if (/transaction gate timeout/i.test(msg)) {
|
|
525
|
+
return new errors_js_1.TimeoutError(0, 'PowDB transaction gate');
|
|
526
|
+
}
|
|
389
527
|
// Type mismatch / parse / execution / storage / unexpected → validation
|
|
390
528
|
// (E003). On the embedded transport these are the only signal we get
|
|
391
529
|
// (code is always 'GenericFailure'); on the networked path they are a
|
|
@@ -431,15 +569,142 @@ function txControl(powql) {
|
|
|
431
569
|
return null;
|
|
432
570
|
}
|
|
433
571
|
/**
|
|
434
|
-
* The error a pool throws when a `begin` arrives
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
572
|
+
* The error a pool throws when a `begin` arrives from INSIDE an already-open
|
|
573
|
+
* transaction's async context (a re-entrant `db.$transaction`). PowDB has ONE
|
|
574
|
+
* global write lock and no savepoints: queueing a re-entrant transaction would
|
|
575
|
+
* deadlock (the outer callback awaits the inner transaction, which waits on
|
|
576
|
+
* the write lock the outer transaction holds), and on the networked transport
|
|
577
|
+
* it would block a fresh pooled connection on the lock forever. This guard
|
|
578
|
+
* converts that hang into a fast, typed error. Independent concurrent
|
|
579
|
+
* transactions do NOT hit this — they queue FIFO on {@link PowdbTxGate}.
|
|
439
580
|
*/
|
|
440
581
|
function reentrantTransactionError() {
|
|
441
|
-
return new errors_js_1.UnsupportedFeatureError('
|
|
442
|
-
'
|
|
582
|
+
return new errors_js_1.UnsupportedFeatureError('re-entrant transactions', 'powdb', 'PowDB is single-writer — a transaction opened from inside an active transaction callback would deadlock ' +
|
|
583
|
+
'on the write lock the open transaction holds. Use the `tx` client the callback receives, or start the ' +
|
|
584
|
+
'second transaction after the first completes. (Independent concurrent transactions queue automatically.)');
|
|
585
|
+
}
|
|
586
|
+
// ---------------------------------------------------------------------------
|
|
587
|
+
// Single-writer transaction gate — FIFO queueing + re-entrancy detection
|
|
588
|
+
// ---------------------------------------------------------------------------
|
|
589
|
+
/**
|
|
590
|
+
* Default cap (ms) on how long a `begin` may wait in the FIFO queue for
|
|
591
|
+
* PowDB's single global write lock before failing with a typed
|
|
592
|
+
* {@link TimeoutError} (E002). Prevents silent starvation behind a wedged
|
|
593
|
+
* transaction. Override via `transactionQueueTimeoutMs`
|
|
594
|
+
* ({@link TurbinePowdbOptions} / {@link PowdbPoolOptions}); `0` or `Infinity`
|
|
595
|
+
* waits without limit.
|
|
596
|
+
*/
|
|
597
|
+
exports.DEFAULT_TX_QUEUE_TIMEOUT_MS = 30_000;
|
|
598
|
+
const powdbTxStorage = new node_async_hooks_1.AsyncLocalStorage();
|
|
599
|
+
/**
|
|
600
|
+
* FIFO gate serializing transactions across a whole pool. PowDB holds one
|
|
601
|
+
* global write lock, so at most one transaction may be open per database:
|
|
602
|
+
* without this gate a second `begin` on the networked transport checks out a
|
|
603
|
+
* fresh connection and blocks forever on the lock the open transaction holds
|
|
604
|
+
* (and the embedded engine rejects it with a raw parse error).
|
|
605
|
+
*
|
|
606
|
+
* `acquire()` is called (synchronously, see below) for every `begin`:
|
|
607
|
+
* - a **re-entrant** `begin` — issued from inside an active transaction's
|
|
608
|
+
* async context, detected via {@link powdbTxStorage} — throws E017
|
|
609
|
+
* immediately. Queueing it can never succeed: the open transaction cannot
|
|
610
|
+
* commit while its callback awaits the queued one.
|
|
611
|
+
* - an **independent** `begin` waits its FIFO turn, bounded by the queue
|
|
612
|
+
* timeout, then returns a {@link PowdbTxHold} the caller finishes on
|
|
613
|
+
* commit / rollback / connection release.
|
|
614
|
+
*
|
|
615
|
+
* Context propagation relies on `AsyncLocalStorage.enterWith()` running in the
|
|
616
|
+
* caller's *synchronous* execution: the pools call `acquire()` from the
|
|
617
|
+
* synchronous prologue of `query('begin')`, so when `TurbineClient.$transaction`
|
|
618
|
+
* awaits that begin, its continuation — and therefore the user callback — runs
|
|
619
|
+
* with the marker set, while sibling contexts (concurrent transactions, the
|
|
620
|
+
* caller after `$transaction` resolves) captured their snapshots earlier and
|
|
621
|
+
* never see it. Markers form a chain ({@link PowdbTxContext.parent}) so
|
|
622
|
+
* transactions nested across DIFFERENT pools cannot shadow an outer marker on
|
|
623
|
+
* this gate — the re-entrancy check walks every live ancestor.
|
|
624
|
+
*
|
|
625
|
+
* **Residual limitation:** a re-entrant begin issued from an async context
|
|
626
|
+
* created BEFORE the transaction opened (e.g. a job-queue worker loop whose
|
|
627
|
+
* continuations captured their ALS snapshot up front) carries no marker, so it
|
|
628
|
+
* cannot be told apart from a legitimate independent concurrent transaction.
|
|
629
|
+
* It queues FIFO and — because the open transaction is awaiting it — times out
|
|
630
|
+
* after `transactionQueueTimeoutMs` with a typed {@link TimeoutError} rather
|
|
631
|
+
* than throwing E017 instantly. The 30s default is the backstop for exactly
|
|
632
|
+
* this case: do not set `transactionQueueTimeoutMs: 0` (wait forever) in code
|
|
633
|
+
* paths that may start transactions from pre-existing async contexts.
|
|
634
|
+
*/
|
|
635
|
+
class PowdbTxGate {
|
|
636
|
+
queueTimeoutMs;
|
|
637
|
+
/** Tail of the FIFO queue — resolves once every earlier transaction has finished. */
|
|
638
|
+
tail = Promise.resolve();
|
|
639
|
+
constructor(queueTimeoutMs) {
|
|
640
|
+
this.queueTimeoutMs = queueTimeoutMs;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Take a place in the transaction queue. MUST be invoked in the same
|
|
644
|
+
* synchronous execution as the caller's `begin` (no `await` before it) so
|
|
645
|
+
* the re-entrancy marker propagates into the transaction's async scope.
|
|
646
|
+
*/
|
|
647
|
+
async acquire() {
|
|
648
|
+
// --- synchronous section (runs before the caller's first await) ---
|
|
649
|
+
// Walk the WHOLE marker chain, not just the innermost marker: with two
|
|
650
|
+
// pools, dbA-tx → dbB-tx → dbA-begin leaves dbB's marker innermost, but
|
|
651
|
+
// the dbA ancestor is still open — queueing the inner dbA begin behind it
|
|
652
|
+
// would deadlock. Any live ancestor on this gate ⇒ re-entrant E017.
|
|
653
|
+
// Prune completed heads first (`done` never flips back) so sequential
|
|
654
|
+
// transactions issued from one long-lived context do not chain — and leak
|
|
655
|
+
// — unboundedly; what remains is bounded by real nesting depth.
|
|
656
|
+
let parent = powdbTxStorage.getStore();
|
|
657
|
+
while (parent?.done)
|
|
658
|
+
parent = parent.parent;
|
|
659
|
+
for (let c = parent; c !== undefined; c = c.parent) {
|
|
660
|
+
if (c.gate === this && !c.done) {
|
|
661
|
+
throw reentrantTransactionError();
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
const ctx = { gate: this, done: false, parent };
|
|
665
|
+
powdbTxStorage.enterWith(ctx);
|
|
666
|
+
let handOff;
|
|
667
|
+
const finished = new Promise((resolve) => {
|
|
668
|
+
handOff = resolve;
|
|
669
|
+
});
|
|
670
|
+
const ahead = this.tail;
|
|
671
|
+
this.tail = ahead.then(() => finished);
|
|
672
|
+
const hold = {
|
|
673
|
+
finish: () => {
|
|
674
|
+
if (ctx.done)
|
|
675
|
+
return;
|
|
676
|
+
ctx.done = true;
|
|
677
|
+
handOff();
|
|
678
|
+
},
|
|
679
|
+
};
|
|
680
|
+
// --- FIFO wait (optionally bounded) ---
|
|
681
|
+
const timeoutMs = this.queueTimeoutMs;
|
|
682
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
683
|
+
await ahead;
|
|
684
|
+
return hold;
|
|
685
|
+
}
|
|
686
|
+
await new Promise((resolve, reject) => {
|
|
687
|
+
let settled = false;
|
|
688
|
+
const timer = setTimeout(() => {
|
|
689
|
+
if (settled)
|
|
690
|
+
return;
|
|
691
|
+
settled = true;
|
|
692
|
+
// Give up the queue slot: finishing resolves our `finished` link, so
|
|
693
|
+
// once every transaction ahead completes, later waiters skip straight
|
|
694
|
+
// past us instead of stalling behind a slot nobody will release.
|
|
695
|
+
hold.finish();
|
|
696
|
+
reject(new errors_js_1.TimeoutError(timeoutMs, 'PowDB transaction (queued behind the single-writer lock)'));
|
|
697
|
+
}, timeoutMs);
|
|
698
|
+
void ahead.then(() => {
|
|
699
|
+
if (settled)
|
|
700
|
+
return;
|
|
701
|
+
settled = true;
|
|
702
|
+
clearTimeout(timer);
|
|
703
|
+
resolve();
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
return hold;
|
|
707
|
+
}
|
|
443
708
|
}
|
|
444
709
|
/** Adapt a PowDB result into the pg-compat `{ rows, rowCount, fields }` shape. */
|
|
445
710
|
function adaptResult(r) {
|
|
@@ -473,49 +738,60 @@ class PowdbPool {
|
|
|
473
738
|
toParam;
|
|
474
739
|
closed = false;
|
|
475
740
|
/**
|
|
476
|
-
* Pool-level single-writer
|
|
477
|
-
* most one transaction may be open across the whole pool.
|
|
478
|
-
*
|
|
479
|
-
* connection and
|
|
741
|
+
* Pool-level single-writer gate. PowDB holds one global write lock, so at
|
|
742
|
+
* most one transaction may be open across the whole pool. Concurrent
|
|
743
|
+
* `begin`s queue FIFO on the gate (instead of checking out a second
|
|
744
|
+
* connection and blocking on the lock forever — the networked hang);
|
|
745
|
+
* re-entrant `begin`s throw E017 (see {@link PowdbTxGate}).
|
|
480
746
|
*/
|
|
481
|
-
|
|
482
|
-
|
|
747
|
+
txGate;
|
|
748
|
+
/** Hold taken by a `begin` issued via `query()` directly (no checked-out client). */
|
|
749
|
+
poolHold = null;
|
|
750
|
+
constructor(pool, toParam = (v) => toPowdbParam(v), options = {}) {
|
|
483
751
|
this.pool = pool;
|
|
484
752
|
this.toParam = toParam;
|
|
753
|
+
this.txGate = new PowdbTxGate(options.transactionQueueTimeoutMs ?? exports.DEFAULT_TX_QUEUE_TIMEOUT_MS);
|
|
485
754
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
* otherwise flips the pool-level flag. Returns the control kind so the caller
|
|
490
|
-
* can decide whether it even needs to hit the engine.
|
|
491
|
-
*/
|
|
492
|
-
guardTxControl(powql) {
|
|
755
|
+
// biome-ignore lint/suspicious/noExplicitAny: pg-compat query is generic over the row shape.
|
|
756
|
+
async query(text, values) {
|
|
757
|
+
const { text: powql, params } = normalizeQueryArgs(text, values);
|
|
493
758
|
const ctl = txControl(powql);
|
|
494
759
|
if (ctl === 'begin') {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
760
|
+
// Gate BEFORE touching the engine: a re-entrant begin throws fast, an
|
|
761
|
+
// independent concurrent one waits its FIFO turn. (acquire()'s
|
|
762
|
+
// re-entrancy check + context mark run synchronously right here.)
|
|
763
|
+
this.poolHold = await this.txGate.acquire();
|
|
498
764
|
}
|
|
499
|
-
|
|
500
|
-
|
|
765
|
+
if ((ctl === 'commit' || ctl === 'rollback') && this.poolHold === null) {
|
|
766
|
+
// No gate hold → our `begin` never ran (gate timeout / re-entrant
|
|
767
|
+
// E017 / no begin at all). Never forward a stray commit/rollback to
|
|
768
|
+
// the engine — PowDB is single-writer, so it could only ever end a
|
|
769
|
+
// DIFFERENT caller's open transaction. Empty success instead.
|
|
770
|
+
return { rows: [], rowCount: 0, fields: [] };
|
|
501
771
|
}
|
|
502
|
-
return ctl;
|
|
503
|
-
}
|
|
504
|
-
// biome-ignore lint/suspicious/noExplicitAny: pg-compat query is generic over the row shape.
|
|
505
|
-
async query(text, values) {
|
|
506
|
-
const { text: powql, params } = normalizeQueryArgs(text, values);
|
|
507
|
-
this.guardTxControl(powql);
|
|
508
772
|
try {
|
|
509
773
|
const result = await this.pool.withClient((c) => c.query(powql, params.map(this.toParam)));
|
|
510
774
|
return adaptResult(result);
|
|
511
775
|
}
|
|
512
776
|
catch (err) {
|
|
777
|
+
if (ctl === 'begin') {
|
|
778
|
+
this.poolHold?.finish();
|
|
779
|
+
this.poolHold = null;
|
|
780
|
+
}
|
|
513
781
|
throw wrapPowdbError(err);
|
|
514
782
|
}
|
|
783
|
+
finally {
|
|
784
|
+
if (ctl === 'commit' || ctl === 'rollback') {
|
|
785
|
+
this.poolHold?.finish();
|
|
786
|
+
this.poolHold = null;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
515
789
|
}
|
|
516
790
|
async connect() {
|
|
517
791
|
const client = await this.pool.acquire();
|
|
518
792
|
let broken = false;
|
|
793
|
+
/** The gate hold of the transaction begun through THIS connection (if any). */
|
|
794
|
+
let hold = null;
|
|
519
795
|
return {
|
|
520
796
|
// The networked client's query() supports concurrent in-flight calls on
|
|
521
797
|
// one connection: every request frame is written to the socket
|
|
@@ -525,28 +801,54 @@ class PowdbPool {
|
|
|
525
801
|
// for the batch's rollback contract because a failed statement leaves
|
|
526
802
|
// the engine's transaction open (no aborted state, no auto-rollback) —
|
|
527
803
|
// later pipelined statements execute inside the same still-open
|
|
528
|
-
// transaction and the final `rollback` discards every effect.
|
|
804
|
+
// transaction and the final `rollback` discards every effect. (The
|
|
805
|
+
// batch path awaits `begin` before dispatching the burst, so the gate
|
|
806
|
+
// wait below never reorders statements around it.)
|
|
529
807
|
supportsPipelining: true,
|
|
530
808
|
// biome-ignore lint/suspicious/noExplicitAny: see query() above.
|
|
531
809
|
query: async (text, values) => {
|
|
532
810
|
const { text: powql, params } = normalizeQueryArgs(text, values);
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
811
|
+
const ctl = txControl(powql);
|
|
812
|
+
if (ctl === 'begin') {
|
|
813
|
+
// Gate BEFORE hitting the engine — a re-entrant begin throws fast
|
|
814
|
+
// instead of blocking on the global write lock the open tx holds;
|
|
815
|
+
// an independent concurrent begin queues FIFO.
|
|
816
|
+
hold = await this.txGate.acquire();
|
|
817
|
+
}
|
|
818
|
+
if ((ctl === 'commit' || ctl === 'rollback') && hold === null) {
|
|
819
|
+
// This connection never acquired the gate — its `begin` never ran
|
|
820
|
+
// (gate timeout / re-entrant E017). A stray commit/rollback must
|
|
821
|
+
// never reach the single-writer engine, where it could only end a
|
|
822
|
+
// DIFFERENT caller's open transaction. Empty success instead.
|
|
823
|
+
return { rows: [], rowCount: 0, fields: [] };
|
|
824
|
+
}
|
|
536
825
|
try {
|
|
537
826
|
return adaptResult(await client.query(powql, params.map(this.toParam)));
|
|
538
827
|
}
|
|
539
828
|
catch (err) {
|
|
540
829
|
broken = true;
|
|
830
|
+
if (ctl === 'begin') {
|
|
831
|
+
hold?.finish();
|
|
832
|
+
hold = null;
|
|
833
|
+
}
|
|
541
834
|
throw wrapPowdbError(err);
|
|
542
835
|
}
|
|
836
|
+
finally {
|
|
837
|
+
if (ctl === 'commit' || ctl === 'rollback') {
|
|
838
|
+
hold?.finish();
|
|
839
|
+
hold = null;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
543
842
|
},
|
|
544
843
|
release: () => {
|
|
545
|
-
// Releasing this connection ends its transaction scope.
|
|
546
|
-
// as a safety net so a tx torn down without an explicit
|
|
547
|
-
// (e.g. a timeout that destroys the connection)
|
|
548
|
-
//
|
|
549
|
-
|
|
844
|
+
// Releasing this connection ends its transaction scope. Finish the
|
|
845
|
+
// hold as a safety net so a tx torn down without an explicit
|
|
846
|
+
// commit/rollback (e.g. a timeout that destroys the connection) hands
|
|
847
|
+
// the gate to the next queued transaction instead of wedging the
|
|
848
|
+
// queue. Only THIS connection's hold — releasing an unrelated (read)
|
|
849
|
+
// connection never touches another transaction's slot.
|
|
850
|
+
hold?.finish();
|
|
851
|
+
hold = null;
|
|
550
852
|
return broken ? this.pool.destroy(client) : this.pool.release(client);
|
|
551
853
|
},
|
|
552
854
|
};
|
|
@@ -660,57 +962,89 @@ class PowdbEmbeddedPool {
|
|
|
660
962
|
db;
|
|
661
963
|
closed = false;
|
|
662
964
|
/**
|
|
663
|
-
* Single-writer
|
|
965
|
+
* Single-writer gate. The embedded engine is one handle with one global
|
|
664
966
|
* write lock — only one transaction may be open at a time. A re-entrant
|
|
665
|
-
* `begin` (a fresh top-level `db.$transaction` opened inside an open one
|
|
666
|
-
* would otherwise hit PowDB's raw "already in a transaction"
|
|
667
|
-
*
|
|
668
|
-
*
|
|
967
|
+
* `begin` (a fresh top-level `db.$transaction` opened inside an open one's
|
|
968
|
+
* callback) would otherwise hit PowDB's raw "already in a transaction"
|
|
969
|
+
* parse error; the gate surfaces a typed E017 instead, while INDEPENDENT
|
|
970
|
+
* concurrent transactions queue FIFO and run one at a time. (Nested
|
|
971
|
+
* `tx.$transaction` is caught earlier still, by the savepoint override in
|
|
972
|
+
* {@link powdbDialect}.)
|
|
669
973
|
*/
|
|
670
|
-
|
|
671
|
-
|
|
974
|
+
txGate;
|
|
975
|
+
/** Hold taken by a `begin` issued via `query()` directly (no checked-out client). */
|
|
976
|
+
poolHoldRef = { hold: null };
|
|
977
|
+
constructor(db, options = {}) {
|
|
672
978
|
this.db = db;
|
|
979
|
+
this.txGate = new PowdbTxGate(options.transactionQueueTimeoutMs ?? exports.DEFAULT_TX_QUEUE_TIMEOUT_MS);
|
|
980
|
+
}
|
|
981
|
+
/** Materialize `$N` params and hand the PowQL to the in-process engine. */
|
|
982
|
+
exec(powql, params) {
|
|
983
|
+
const materialized = materializePowql(powql, params);
|
|
984
|
+
return adaptResult(normalizeEmbeddedResult(this.db.query(materialized)));
|
|
673
985
|
}
|
|
674
|
-
/**
|
|
675
|
-
|
|
986
|
+
/**
|
|
987
|
+
* Run one statement, gating transaction control. `holdRef` scopes the gate
|
|
988
|
+
* hold to whoever issued the `begin` (the pool itself or one checked-out
|
|
989
|
+
* client), so finishing a transaction can never release a slot a different
|
|
990
|
+
* transaction is holding.
|
|
991
|
+
*/
|
|
992
|
+
async run(powql, params, holdRef) {
|
|
676
993
|
const ctl = txControl(powql);
|
|
677
994
|
if (ctl === 'begin') {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
995
|
+
// Gate BEFORE hitting the engine — re-entrant begins throw fast,
|
|
996
|
+
// independent concurrent ones wait their FIFO turn. (acquire()'s
|
|
997
|
+
// re-entrancy check + context mark run synchronously right here.)
|
|
998
|
+
holdRef.hold = await this.txGate.acquire();
|
|
681
999
|
}
|
|
682
|
-
|
|
683
|
-
|
|
1000
|
+
if ((ctl === 'commit' || ctl === 'rollback') && holdRef.hold === null) {
|
|
1001
|
+
// This context never acquired the gate — its `begin` never ran (the
|
|
1002
|
+
// gate timed out / threw re-entrant E017, or no begin was issued at
|
|
1003
|
+
// all). The engine is ONE shared handle: forwarding this stray
|
|
1004
|
+
// commit/rollback would hit whatever transaction ANOTHER caller has
|
|
1005
|
+
// open on it (live-reproduced: a best-effort ROLLBACK after a failed
|
|
1006
|
+
// begin silently discarded a concurrent transaction's writes). Swallow
|
|
1007
|
+
// it as an empty success instead — there is nothing of ours to end.
|
|
1008
|
+
return { rows: [], rowCount: 0, fields: [] };
|
|
684
1009
|
}
|
|
685
|
-
}
|
|
686
|
-
run(powql, params) {
|
|
687
|
-
this.guardTxControl(powql);
|
|
688
1010
|
try {
|
|
689
|
-
|
|
690
|
-
return adaptResult(normalizeEmbeddedResult(this.db.query(materialized)));
|
|
1011
|
+
return this.exec(powql, params);
|
|
691
1012
|
}
|
|
692
1013
|
catch (err) {
|
|
1014
|
+
if (ctl === 'begin') {
|
|
1015
|
+
holdRef.hold?.finish();
|
|
1016
|
+
holdRef.hold = null;
|
|
1017
|
+
}
|
|
693
1018
|
throw wrapPowdbError(err);
|
|
694
1019
|
}
|
|
1020
|
+
finally {
|
|
1021
|
+
if (ctl === 'commit' || ctl === 'rollback') {
|
|
1022
|
+
holdRef.hold?.finish();
|
|
1023
|
+
holdRef.hold = null;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
695
1026
|
}
|
|
696
1027
|
// biome-ignore lint/suspicious/noExplicitAny: pg-compat query is generic over the row shape.
|
|
697
1028
|
async query(text, values) {
|
|
698
1029
|
const { text: powql, params } = normalizeQueryArgs(text, values);
|
|
699
|
-
return this.run(powql, params);
|
|
1030
|
+
return this.run(powql, params, this.poolHoldRef);
|
|
700
1031
|
}
|
|
701
1032
|
async connect() {
|
|
702
1033
|
// Single in-process handle — the "client" shares the one Database; tx
|
|
703
|
-
// keywords run serially on it.
|
|
1034
|
+
// keywords run serially on it. Each checked-out client scopes its own
|
|
1035
|
+
// gate hold so release() only ever finishes ITS transaction.
|
|
1036
|
+
const holdRef = { hold: null };
|
|
704
1037
|
return {
|
|
705
1038
|
// biome-ignore lint/suspicious/noExplicitAny: see query() above.
|
|
706
1039
|
query: async (text, values) => {
|
|
707
1040
|
const { text: powql, params } = normalizeQueryArgs(text, values);
|
|
708
|
-
return this.run(powql, params);
|
|
1041
|
+
return this.run(powql, params, holdRef);
|
|
709
1042
|
},
|
|
710
1043
|
release: () => {
|
|
711
1044
|
// End-of-scope safety net (see PowdbPool.connect()): a tx torn down
|
|
712
|
-
// without an explicit commit/rollback must not wedge the
|
|
713
|
-
|
|
1045
|
+
// without an explicit commit/rollback must not wedge the queue.
|
|
1046
|
+
holdRef.hold?.finish();
|
|
1047
|
+
holdRef.hold = null;
|
|
714
1048
|
},
|
|
715
1049
|
};
|
|
716
1050
|
}
|
|
@@ -735,10 +1069,14 @@ Object.defineProperty(exports, "PowqlInterface", { enumerable: true, get: functi
|
|
|
735
1069
|
*/
|
|
736
1070
|
async function loadPowdb() {
|
|
737
1071
|
try {
|
|
738
|
-
|
|
1072
|
+
// Via the .cts helper so the CJS build keeps a path to a REAL dynamic
|
|
1073
|
+
// import() — @zvndev/powdb-client ≥ 0.9 is ESM-only, and the CommonJS
|
|
1074
|
+
// pass transpiles a plain `import()` here into an unusable `require()`.
|
|
1075
|
+
return (await (0, optional_peer_import_cjs_1.default)('@zvndev/powdb-client'));
|
|
739
1076
|
}
|
|
740
1077
|
catch (err) {
|
|
741
|
-
throw new errors_js_1.ConnectionError("[turbine] turbine-orm/powdb requires the optional peer dependency '@zvndev/powdb-client'. Install it: npm i @zvndev/powdb-client
|
|
1078
|
+
throw new errors_js_1.ConnectionError("[turbine] turbine-orm/powdb requires the optional peer dependency '@zvndev/powdb-client'. Install it: npm i @zvndev/powdb-client — " +
|
|
1079
|
+
'or construct the PowDB pool yourself and inject it: turbinePowDB(pool, schema). ' +
|
|
742
1080
|
`(${err.message})`);
|
|
743
1081
|
}
|
|
744
1082
|
}
|
|
@@ -752,13 +1090,16 @@ async function loadPowdb() {
|
|
|
752
1090
|
async function loadPowdbEmbedded() {
|
|
753
1091
|
let mod;
|
|
754
1092
|
try {
|
|
755
|
-
|
|
1093
|
+
// Via the .cts helper — keeps a real dynamic import() available to the
|
|
1094
|
+
// CJS build in case a future addon version ships ESM-only (see loadPowdb).
|
|
1095
|
+
mod = (await (0, optional_peer_import_cjs_1.default)('@zvndev/powdb-embedded'));
|
|
756
1096
|
}
|
|
757
1097
|
catch (err) {
|
|
758
1098
|
throw new errors_js_1.ConnectionError("[turbine] turbine-orm/powdb embedded mode requires the optional peer '@zvndev/powdb-embedded'. " +
|
|
759
1099
|
'Install it: npm i @zvndev/powdb-embedded. If install succeeded but loading failed, your platform has no ' +
|
|
760
1100
|
'prebuilt binary (prebuilts ship for macOS arm64/x64 and Linux glibc x64/arm64; Intel-mac/musl/Windows ' +
|
|
761
|
-
'build from source) — build it with `npm run build` in the addon, then retry. ' +
|
|
1101
|
+
'build from source) — build it with `npm run build` in the addon, then retry. You can also construct the ' +
|
|
1102
|
+
'pool yourself and inject it: turbinePowDB(pool, schema). ' +
|
|
762
1103
|
`(${err.message})`);
|
|
763
1104
|
}
|
|
764
1105
|
if (!mod || typeof mod.Database?.open !== 'function') {
|
|
@@ -768,7 +1109,7 @@ async function loadPowdbEmbedded() {
|
|
|
768
1109
|
return mod;
|
|
769
1110
|
}
|
|
770
1111
|
/** Open an embedded database handle, wrapping engine open failures (corrupt dir, etc.). */
|
|
771
|
-
async function openEmbeddedPool(target) {
|
|
1112
|
+
async function openEmbeddedPool(target, poolOptions = {}) {
|
|
772
1113
|
const mod = await loadPowdbEmbedded();
|
|
773
1114
|
const { embedded: dir, syncMode, memoryLimit } = target;
|
|
774
1115
|
let db;
|
|
@@ -794,7 +1135,7 @@ async function openEmbeddedPool(target) {
|
|
|
794
1135
|
}
|
|
795
1136
|
db.setSyncMode(syncMode);
|
|
796
1137
|
}
|
|
797
|
-
return new PowdbEmbeddedPool(db);
|
|
1138
|
+
return new PowdbEmbeddedPool(db, poolOptions);
|
|
798
1139
|
}
|
|
799
1140
|
/**
|
|
800
1141
|
* Bind Turbine to PowDB. `target` is one of:
|
|
@@ -817,28 +1158,30 @@ async function openEmbeddedPool(target) {
|
|
|
817
1158
|
async function turbinePowDB(target, schema, options = {}) {
|
|
818
1159
|
let pool;
|
|
819
1160
|
let owns = false;
|
|
1161
|
+
const poolOptions = { transactionQueueTimeoutMs: options.transactionQueueTimeoutMs };
|
|
820
1162
|
if (typeof target === 'string') {
|
|
821
1163
|
const mod = await loadPowdb();
|
|
822
1164
|
const clientPool = new mod.Pool({ ...parsePowdbUrl(target), max: options.connectionLimit ?? 10 });
|
|
823
1165
|
await assertNetworkedVersion(clientPool);
|
|
824
|
-
pool = new PowdbPool(clientPool);
|
|
1166
|
+
pool = new PowdbPool(clientPool, undefined, poolOptions);
|
|
825
1167
|
owns = true;
|
|
826
1168
|
}
|
|
827
1169
|
else if (target instanceof PowdbPool) {
|
|
1170
|
+
// An injected PowdbPool carries its own PowdbPoolOptions.
|
|
828
1171
|
pool = target;
|
|
829
1172
|
}
|
|
830
1173
|
else if (isEmbeddedTarget(target)) {
|
|
831
|
-
pool = await openEmbeddedPool(target);
|
|
1174
|
+
pool = await openEmbeddedPool(target, poolOptions);
|
|
832
1175
|
owns = true;
|
|
833
1176
|
}
|
|
834
1177
|
else if (isPowdbClientPool(target)) {
|
|
835
|
-
pool = new PowdbPool(target);
|
|
1178
|
+
pool = new PowdbPool(target, undefined, poolOptions);
|
|
836
1179
|
}
|
|
837
1180
|
else {
|
|
838
1181
|
const mod = await loadPowdb();
|
|
839
1182
|
const clientPool = new mod.Pool({ ...target, max: options.connectionLimit ?? 10 });
|
|
840
1183
|
await assertNetworkedVersion(clientPool);
|
|
841
|
-
pool = new PowdbPool(clientPool);
|
|
1184
|
+
pool = new PowdbPool(clientPool, undefined, poolOptions);
|
|
842
1185
|
owns = true;
|
|
843
1186
|
}
|
|
844
1187
|
// The PowQL generator is loaded here to keep client.ts free of any PowDB import.
|