turbine-orm 0.30.0 → 0.32.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/dist/cjs/client.js +14 -3
- package/dist/cjs/mssql.js +24 -3
- package/dist/cjs/powdb.js +197 -50
- package/dist/cjs/powql.js +45 -2
- package/dist/cjs/query/batched-loader.js +34 -0
- package/dist/cjs/query/builder.js +758 -139
- package/dist/cjs/query/filters.js +77 -2
- package/dist/client.d.ts +13 -0
- package/dist/client.js +14 -3
- package/dist/index.d.ts +1 -1
- package/dist/mssql.js +24 -3
- package/dist/powdb.d.ts +33 -0
- package/dist/powdb.js +197 -50
- package/dist/powql.js +45 -2
- package/dist/query/batched-loader.d.ts +12 -0
- package/dist/query/batched-loader.js +33 -0
- package/dist/query/builder.d.ts +171 -5
- package/dist/query/builder.js +760 -141
- package/dist/query/filters.d.ts +40 -1
- package/dist/query/filters.js +73 -1
- package/dist/query/index.d.ts +1 -1
- package/dist/query/types.d.ts +213 -22
- package/package.json +1 -1
package/dist/cjs/client.js
CHANGED
|
@@ -783,7 +783,12 @@ class TurbineClient {
|
|
|
783
783
|
try {
|
|
784
784
|
await client.query(this.dialect.beginStatement());
|
|
785
785
|
began = true;
|
|
786
|
-
|
|
786
|
+
// Engine seam: single-writer engines scope their transaction re-entrancy
|
|
787
|
+
// marker to the callback's async subtree (see
|
|
788
|
+
// PgCompatPoolClient.wrapTransactionCallback). Absent everywhere else.
|
|
789
|
+
const wrap = client.wrapTransactionCallback;
|
|
790
|
+
// `.call` erases the generic, so the callback's Promise<T> is re-asserted.
|
|
791
|
+
const result = wrap ? (await wrap.call(client, () => fn(client))) : await fn(client);
|
|
787
792
|
await client.query(this.dialect.commitStatement());
|
|
788
793
|
return result;
|
|
789
794
|
}
|
|
@@ -876,6 +881,12 @@ class TurbineClient {
|
|
|
876
881
|
}
|
|
877
882
|
}
|
|
878
883
|
let result;
|
|
884
|
+
// Engine seam: when the checked-out connection exposes
|
|
885
|
+
// wrapTransactionCallback (single-writer engines such as PowDB), run the user
|
|
886
|
+
// callback through it so the engine can scope its re-entrancy marker to
|
|
887
|
+
// the callback's async subtree. All other drivers: plain fn(tx).
|
|
888
|
+
const wrap = client.wrapTransactionCallback;
|
|
889
|
+
const runCallback = () => (wrap ? wrap.call(client, () => fn(tx)) : fn(tx));
|
|
879
890
|
if (timeout) {
|
|
880
891
|
// Race between the function and a timeout. If the timeout fires we
|
|
881
892
|
// need to actually abort the in-flight query — otherwise the backend
|
|
@@ -897,14 +908,14 @@ class TurbineClient {
|
|
|
897
908
|
}, timeout);
|
|
898
909
|
});
|
|
899
910
|
try {
|
|
900
|
-
result = await Promise.race([
|
|
911
|
+
result = await Promise.race([runCallback(), timeoutPromise]);
|
|
901
912
|
}
|
|
902
913
|
finally {
|
|
903
914
|
clearTimeout(timer);
|
|
904
915
|
}
|
|
905
916
|
}
|
|
906
917
|
else {
|
|
907
|
-
result = await
|
|
918
|
+
result = await runCallback();
|
|
908
919
|
}
|
|
909
920
|
await client.query(this.dialect.commitStatement());
|
|
910
921
|
if (this.logging) {
|
package/dist/cjs/mssql.js
CHANGED
|
@@ -756,11 +756,32 @@ function buildForJsonSubquery(dialect, ctx) {
|
|
|
756
756
|
if (hasOrder) {
|
|
757
757
|
const orderBy = orderEntries
|
|
758
758
|
.map(([k, dir]) => {
|
|
759
|
-
|
|
759
|
+
// FOR JSON nested ordering supports plain directions and { sort }
|
|
760
|
+
// specs only. Object shapes the core builder compiles with params
|
|
761
|
+
// (JSON-path / vector / relation ordering) must throw here: the
|
|
762
|
+
// shared param-collect mirror is gated on the native path, so a
|
|
763
|
+
// silently-ignored object would desync SQL text from params.
|
|
764
|
+
let rawDir = dir;
|
|
765
|
+
if (typeof dir === 'object' && dir !== null) {
|
|
766
|
+
const sortValue = dir.sort;
|
|
767
|
+
if (typeof sortValue !== 'string') {
|
|
768
|
+
throw new errors_js_1.ValidationError(`[turbine] Nested orderBy on "${k}" (table "${targetTable}"): only plain directions and ` +
|
|
769
|
+
`{ sort } specs are supported inside a relation orderBy on SQL Server.`);
|
|
770
|
+
}
|
|
771
|
+
if (dir.nulls !== undefined) {
|
|
772
|
+
throw new errors_js_1.UnsupportedFeatureError('NULLS FIRST/LAST ordering', 'sqlserver', 'Explicit nulls placement in orderBy is only available on PostgreSQL and SQLite.');
|
|
773
|
+
}
|
|
774
|
+
rawDir = sortValue;
|
|
775
|
+
}
|
|
776
|
+
// columnMap-first resolution (camelToSnake fallback): matches the
|
|
777
|
+
// core builder's nested orderBy path so camelCase-named DB columns
|
|
778
|
+
// resolve on SQL Server too.
|
|
779
|
+
const col = targetMeta.columnMap[k] ?? (0, schema_js_1.camelToSnake)(k);
|
|
760
780
|
if (!targetMeta.allColumns.includes(col)) {
|
|
761
|
-
throw new errors_js_1.ValidationError(`[turbine] Unknown
|
|
781
|
+
throw new errors_js_1.ValidationError(`[turbine] Unknown field "${k}" in orderBy on table "${targetTable}". ` +
|
|
782
|
+
`Known fields: ${Object.keys(targetMeta.columnMap).join(', ') || '(none)'}.`);
|
|
762
783
|
}
|
|
763
|
-
const safeDir = String(
|
|
784
|
+
const safeDir = String(rawDir).toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
|
764
785
|
return `${a}.${q(col)} ${safeDir}`;
|
|
765
786
|
})
|
|
766
787
|
.join(', ');
|
package/dist/cjs/powdb.js
CHANGED
|
@@ -518,6 +518,11 @@ function wrapPowdbError(err) {
|
|
|
518
518
|
const m = /column ['"]?(\w+)['"]?/i.exec(msg);
|
|
519
519
|
return new errors_js_1.NotNullViolationError({ column: m?.[1], cause: err });
|
|
520
520
|
}
|
|
521
|
+
// Driver pool lifecycle errors (acquire after close, acquire timeout) carry
|
|
522
|
+
// no .code — classify by message so both transports surface E004.
|
|
523
|
+
if (/pool closed|pool acquire timeout/i.test(msg)) {
|
|
524
|
+
return new errors_js_1.ConnectionError(`[turbine] PowDB connection unavailable: ${msg}`);
|
|
525
|
+
}
|
|
521
526
|
// Server-side transaction-gate wait bound (PowDB ≥ 0.10, default 5s): another
|
|
522
527
|
// connection held the single global write lock past the server's
|
|
523
528
|
// --tx-wait-timeout-ms. Retryable timeout, not a query defect.
|
|
@@ -595,6 +600,13 @@ function reentrantTransactionError() {
|
|
|
595
600
|
* waits without limit.
|
|
596
601
|
*/
|
|
597
602
|
exports.DEFAULT_TX_QUEUE_TIMEOUT_MS = 30_000;
|
|
603
|
+
/**
|
|
604
|
+
* Upper bound on the best-effort `rollback` a release-with-open-hold fires
|
|
605
|
+
* before handing the gate to the next queued transaction. Keeps a dead socket
|
|
606
|
+
* from wedging the FIFO queue while still letting the engine drop its global
|
|
607
|
+
* write lock cleanly in the normal case.
|
|
608
|
+
*/
|
|
609
|
+
const RELEASE_ROLLBACK_TIMEOUT_MS = 2_000;
|
|
598
610
|
const powdbTxStorage = new node_async_hooks_1.AsyncLocalStorage();
|
|
599
611
|
/**
|
|
600
612
|
* FIFO gate serializing transactions across a whole pool. PowDB holds one
|
|
@@ -612,25 +624,35 @@ const powdbTxStorage = new node_async_hooks_1.AsyncLocalStorage();
|
|
|
612
624
|
* timeout, then returns a {@link PowdbTxHold} the caller finishes on
|
|
613
625
|
* commit / rollback / connection release.
|
|
614
626
|
*
|
|
615
|
-
* Context propagation
|
|
616
|
-
*
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
622
|
-
*
|
|
623
|
-
*
|
|
627
|
+
* Context propagation: `acquire()` only READS the async context (the
|
|
628
|
+
* chain-walking check in its prologue); it never writes it. The marker is
|
|
629
|
+
* planted by the pool's `wrapTransactionCallback` (`TurbineClient` invokes
|
|
630
|
+
* the user callback as `powdbTxStorage.run(hold.ctx, fn)`), so it exists
|
|
631
|
+
* exclusively inside the transaction CALLBACK's async subtree. Everything
|
|
632
|
+
* launched from inside the callback (table ops on the `tx` client, a
|
|
633
|
+
* fire-and-forget `db.$transaction`, nested-write implicit transactions)
|
|
634
|
+
* inherits it; the CALLER's context stays unmarked. This is load-bearing: the
|
|
635
|
+
* pre-0.31 implementation used `enterWith()` in acquire's prologue, which
|
|
636
|
+
* mutates the caller's shared context. On a cold client the FIRST same-tick
|
|
637
|
+
* burst of `db.$transaction` calls saw call #1's live marker from every
|
|
638
|
+
* sibling and falsely threw re-entrant E017 (9/10 rejected in production;
|
|
639
|
+
* one warm-up transaction masked it because its pruned `done` marker changed
|
|
640
|
+
* the propagation shape). With `run()` the sibling contexts are unmarked by
|
|
641
|
+
* construction, so they queue FIFO as intended. Markers form a chain
|
|
642
|
+
* ({@link PowdbTxContext.parent}) so transactions nested across DIFFERENT
|
|
643
|
+
* pools cannot shadow an outer marker on this gate; the re-entrancy check
|
|
644
|
+
* walks every live ancestor.
|
|
624
645
|
*
|
|
625
|
-
* **Residual limitation:** a
|
|
626
|
-
*
|
|
627
|
-
* continuations captured their
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
646
|
+
* **Residual limitation:** a transaction begun OUTSIDE a `$transaction`
|
|
647
|
+
* callback plants no marker (a manual raw `begin` span, or a worker loop
|
|
648
|
+
* whose continuations captured their context before the transaction opened).
|
|
649
|
+
* A deadlocking re-entrant begin from such a context cannot be told apart
|
|
650
|
+
* from a legitimate independent concurrent transaction: it queues FIFO and,
|
|
651
|
+
* because the open transaction is awaiting it, times out after
|
|
652
|
+
* `transactionQueueTimeoutMs` with a typed {@link TimeoutError} rather than
|
|
653
|
+
* throwing E017 instantly. The 30s default is the backstop for exactly this
|
|
654
|
+
* case: do not set `transactionQueueTimeoutMs: 0` (wait forever) in code
|
|
655
|
+
* paths that may start transactions from unmarked contexts.
|
|
634
656
|
*/
|
|
635
657
|
class PowdbTxGate {
|
|
636
658
|
queueTimeoutMs;
|
|
@@ -640,12 +662,14 @@ class PowdbTxGate {
|
|
|
640
662
|
this.queueTimeoutMs = queueTimeoutMs;
|
|
641
663
|
}
|
|
642
664
|
/**
|
|
643
|
-
* Take a place in the transaction queue.
|
|
644
|
-
*
|
|
645
|
-
*
|
|
665
|
+
* Take a place in the transaction queue. The prologue walks the caller's
|
|
666
|
+
* marker chain (planted around transaction callbacks by the pool's
|
|
667
|
+
* `wrapTransactionCallback`) and throws re-entrant E017 when any live
|
|
668
|
+
* ancestor holds THIS gate. It never marks the caller's context itself;
|
|
669
|
+
* the returned hold carries the fresh marker (`hold.ctx`) for
|
|
670
|
+
* `wrapTransactionCallback` to scope around the user callback.
|
|
646
671
|
*/
|
|
647
672
|
async acquire() {
|
|
648
|
-
// --- synchronous section (runs before the caller's first await) ---
|
|
649
673
|
// Walk the WHOLE marker chain, not just the innermost marker: with two
|
|
650
674
|
// pools, dbA-tx → dbB-tx → dbA-begin leaves dbB's marker innermost, but
|
|
651
675
|
// the dbA ancestor is still open — queueing the inner dbA begin behind it
|
|
@@ -662,7 +686,6 @@ class PowdbTxGate {
|
|
|
662
686
|
}
|
|
663
687
|
}
|
|
664
688
|
const ctx = { gate: this, done: false, parent };
|
|
665
|
-
powdbTxStorage.enterWith(ctx);
|
|
666
689
|
let handOff;
|
|
667
690
|
const finished = new Promise((resolve) => {
|
|
668
691
|
handOff = resolve;
|
|
@@ -670,6 +693,7 @@ class PowdbTxGate {
|
|
|
670
693
|
const ahead = this.tail;
|
|
671
694
|
this.tail = ahead.then(() => finished);
|
|
672
695
|
const hold = {
|
|
696
|
+
ctx,
|
|
673
697
|
finish: () => {
|
|
674
698
|
if (ctx.done)
|
|
675
699
|
return;
|
|
@@ -747,6 +771,14 @@ class PowdbPool {
|
|
|
747
771
|
txGate;
|
|
748
772
|
/** Hold taken by a `begin` issued via `query()` directly (no checked-out client). */
|
|
749
773
|
poolHold = null;
|
|
774
|
+
/**
|
|
775
|
+
* Clients currently checked out via {@link connect}. The driver pool's
|
|
776
|
+
* `close()` only closes IDLE clients (checked-out ones are documented as the
|
|
777
|
+
* caller's responsibility), so {@link end} destroys these explicitly;
|
|
778
|
+
* otherwise a `disconnect()` racing an unreleased connection would leave a
|
|
779
|
+
* live socket holding the process open until the server's idle timeout.
|
|
780
|
+
*/
|
|
781
|
+
checkedOut = new Set();
|
|
750
782
|
constructor(pool, toParam = (v) => toPowdbParam(v), options = {}) {
|
|
751
783
|
this.pool = pool;
|
|
752
784
|
this.toParam = toParam;
|
|
@@ -754,12 +786,13 @@ class PowdbPool {
|
|
|
754
786
|
}
|
|
755
787
|
// biome-ignore lint/suspicious/noExplicitAny: pg-compat query is generic over the row shape.
|
|
756
788
|
async query(text, values) {
|
|
789
|
+
this.assertOpen();
|
|
757
790
|
const { text: powql, params } = normalizeQueryArgs(text, values);
|
|
758
791
|
const ctl = txControl(powql);
|
|
759
792
|
if (ctl === 'begin') {
|
|
760
|
-
// Gate BEFORE touching the engine: a
|
|
761
|
-
//
|
|
762
|
-
//
|
|
793
|
+
// Gate BEFORE touching the engine: a begin from inside an active
|
|
794
|
+
// transaction callback throws re-entrant E017 fast; an independent
|
|
795
|
+
// concurrent one waits its FIFO turn.
|
|
763
796
|
this.poolHold = await this.txGate.acquire();
|
|
764
797
|
}
|
|
765
798
|
if ((ctl === 'commit' || ctl === 'rollback') && this.poolHold === null) {
|
|
@@ -787,8 +820,26 @@ class PowdbPool {
|
|
|
787
820
|
}
|
|
788
821
|
}
|
|
789
822
|
}
|
|
823
|
+
/**
|
|
824
|
+
* Typed guard mirroring {@link PowdbEmbeddedPool}: after `end()` the driver
|
|
825
|
+
* pool throws a raw `Error('pool closed')` that {@link wrapPowdbError}
|
|
826
|
+
* cannot classify — surface the same ConnectionError on both transports.
|
|
827
|
+
*/
|
|
828
|
+
assertOpen() {
|
|
829
|
+
if (this.closed) {
|
|
830
|
+
throw new errors_js_1.ConnectionError('[turbine] The PowDB pool is closed — disconnect() was already called on this client.');
|
|
831
|
+
}
|
|
832
|
+
}
|
|
790
833
|
async connect() {
|
|
791
|
-
|
|
834
|
+
this.assertOpen();
|
|
835
|
+
let client;
|
|
836
|
+
try {
|
|
837
|
+
client = await this.pool.acquire();
|
|
838
|
+
}
|
|
839
|
+
catch (err) {
|
|
840
|
+
throw wrapPowdbError(err);
|
|
841
|
+
}
|
|
842
|
+
this.checkedOut.add(client);
|
|
792
843
|
let broken = false;
|
|
793
844
|
/** The gate hold of the transaction begun through THIS connection (if any). */
|
|
794
845
|
let hold = null;
|
|
@@ -810,9 +861,10 @@ class PowdbPool {
|
|
|
810
861
|
const { text: powql, params } = normalizeQueryArgs(text, values);
|
|
811
862
|
const ctl = txControl(powql);
|
|
812
863
|
if (ctl === 'begin') {
|
|
813
|
-
// Gate BEFORE hitting the engine
|
|
814
|
-
//
|
|
815
|
-
//
|
|
864
|
+
// Gate BEFORE hitting the engine: a begin from inside an active
|
|
865
|
+
// transaction callback throws re-entrant E017 fast instead of
|
|
866
|
+
// blocking on the global write lock the open tx holds; an
|
|
867
|
+
// independent concurrent begin queues FIFO.
|
|
816
868
|
hold = await this.txGate.acquire();
|
|
817
869
|
}
|
|
818
870
|
if ((ctl === 'commit' || ctl === 'rollback') && hold === null) {
|
|
@@ -840,16 +892,60 @@ class PowdbPool {
|
|
|
840
892
|
}
|
|
841
893
|
}
|
|
842
894
|
},
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
hold?.
|
|
895
|
+
// Single-writer re-entrancy scoping: TurbineClient runs the user's
|
|
896
|
+
// transaction callback through this, so the gate's marker lives ONLY
|
|
897
|
+
// in the callback's async subtree (everything inside it, tx table ops
|
|
898
|
+
// and fire-and-forget db.$transaction alike, inherits it; the caller's
|
|
899
|
+
// context stays unmarked). `hold.ctx` is the same object `finish()` flips, so
|
|
900
|
+
// done-pruning keeps working for contexts that outlive the callback.
|
|
901
|
+
wrapTransactionCallback: (fn) => {
|
|
902
|
+
const ctx = hold?.ctx;
|
|
903
|
+
return ctx ? powdbTxStorage.run(ctx, fn) : fn();
|
|
904
|
+
},
|
|
905
|
+
release: (err) => {
|
|
906
|
+
// Releasing this connection ends its transaction scope. pg semantics:
|
|
907
|
+
// a truthy `err` means "destroy, don't re-idle" — client.ts's
|
|
908
|
+
// $transaction timeout path relies on that to keep an abandoned
|
|
909
|
+
// callback's connection out of the pool. Additionally, an OPEN hold
|
|
910
|
+
// here means the tx begun on this connection never saw commit/rollback
|
|
911
|
+
// (timeout teardown or caller bug): fire a best-effort bounded
|
|
912
|
+
// `rollback` FIRST so the engine drops its global write lock and the
|
|
913
|
+
// server-side transaction ends (destroying the socket alone leaves it
|
|
914
|
+
// open until the server's idle timeout), THEN hand the gate to the
|
|
915
|
+
// next queued transaction. If the rollback fails or times out the
|
|
916
|
+
// connection is treated as broken and destroyed. Never throws — a
|
|
917
|
+
// teardown error must not mask the transaction's real outcome.
|
|
918
|
+
const openHold = hold;
|
|
851
919
|
hold = null;
|
|
852
|
-
|
|
920
|
+
this.checkedOut.delete(client);
|
|
921
|
+
const teardown = async () => {
|
|
922
|
+
let rolledBack = false;
|
|
923
|
+
if (openHold) {
|
|
924
|
+
try {
|
|
925
|
+
await Promise.race([
|
|
926
|
+
client.query('rollback', []).then(() => {
|
|
927
|
+
rolledBack = true;
|
|
928
|
+
}),
|
|
929
|
+
new Promise((resolve) => {
|
|
930
|
+
const t = setTimeout(resolve, RELEASE_ROLLBACK_TIMEOUT_MS);
|
|
931
|
+
t.unref?.();
|
|
932
|
+
}),
|
|
933
|
+
]);
|
|
934
|
+
}
|
|
935
|
+
catch {
|
|
936
|
+
/* best-effort */
|
|
937
|
+
}
|
|
938
|
+
openHold.finish();
|
|
939
|
+
}
|
|
940
|
+
const mustDestroy = broken || Boolean(err) || (openHold !== null && !rolledBack);
|
|
941
|
+
try {
|
|
942
|
+
await (mustDestroy ? this.pool.destroy(client) : this.pool.release(client));
|
|
943
|
+
}
|
|
944
|
+
catch {
|
|
945
|
+
/* the pool may already be closed */
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
void teardown();
|
|
853
949
|
},
|
|
854
950
|
};
|
|
855
951
|
}
|
|
@@ -857,7 +953,16 @@ class PowdbPool {
|
|
|
857
953
|
if (this.closed)
|
|
858
954
|
return;
|
|
859
955
|
this.closed = true;
|
|
956
|
+
// close() rejects pending waiters and closes every IDLE client…
|
|
860
957
|
await this.pool.close();
|
|
958
|
+
// …but NOT checked-out ones (documented in @zvndev/powdb-client: "callers
|
|
959
|
+
// that still hold one when close() is called are responsible for closing
|
|
960
|
+
// it themselves"). Destroy any stragglers so end() never leaves a live
|
|
961
|
+
// socket keeping the process alive until the server's idle timeout.
|
|
962
|
+
for (const client of this.checkedOut) {
|
|
963
|
+
this.pool.destroy(client);
|
|
964
|
+
}
|
|
965
|
+
this.checkedOut.clear();
|
|
861
966
|
}
|
|
862
967
|
}
|
|
863
968
|
exports.PowdbPool = PowdbPool;
|
|
@@ -990,11 +1095,14 @@ class PowdbEmbeddedPool {
|
|
|
990
1095
|
* transaction is holding.
|
|
991
1096
|
*/
|
|
992
1097
|
async run(powql, params, holdRef) {
|
|
1098
|
+
if (this.closed) {
|
|
1099
|
+
throw new errors_js_1.ConnectionError('[turbine] The PowDB embedded pool is closed: disconnect() was already called on this client.');
|
|
1100
|
+
}
|
|
993
1101
|
const ctl = txControl(powql);
|
|
994
1102
|
if (ctl === 'begin') {
|
|
995
|
-
// Gate BEFORE hitting the engine
|
|
996
|
-
//
|
|
997
|
-
//
|
|
1103
|
+
// Gate BEFORE hitting the engine: a begin from inside an active
|
|
1104
|
+
// transaction callback throws re-entrant E017 fast; independent
|
|
1105
|
+
// concurrent ones wait their FIFO turn.
|
|
998
1106
|
holdRef.hold = await this.txGate.acquire();
|
|
999
1107
|
}
|
|
1000
1108
|
if ((ctl === 'commit' || ctl === 'rollback') && holdRef.hold === null) {
|
|
@@ -1040,11 +1148,31 @@ class PowdbEmbeddedPool {
|
|
|
1040
1148
|
const { text: powql, params } = normalizeQueryArgs(text, values);
|
|
1041
1149
|
return this.run(powql, params, holdRef);
|
|
1042
1150
|
},
|
|
1151
|
+
// Scope the gate's re-entrancy marker to the user callback's async
|
|
1152
|
+
// subtree (see PowdbPool.connect(), identical contract).
|
|
1153
|
+
wrapTransactionCallback: (fn) => {
|
|
1154
|
+
const ctx = holdRef.hold?.ctx;
|
|
1155
|
+
return ctx ? powdbTxStorage.run(ctx, fn) : fn();
|
|
1156
|
+
},
|
|
1043
1157
|
release: () => {
|
|
1044
1158
|
// End-of-scope safety net (see PowdbPool.connect()): a tx torn down
|
|
1045
|
-
// without an explicit commit/rollback must not wedge the queue
|
|
1046
|
-
|
|
1047
|
-
|
|
1159
|
+
// without an explicit commit/rollback must not wedge the queue — and
|
|
1160
|
+
// on the ONE shared embedded handle its open engine transaction must
|
|
1161
|
+
// actually be rolled back before the gate moves on, or the next
|
|
1162
|
+
// transaction's work interleaves into it. run() owns the
|
|
1163
|
+
// finish/null-out in its commit/rollback finally; the .finally here
|
|
1164
|
+
// is the fallback when run() itself rejects.
|
|
1165
|
+
const h = holdRef.hold;
|
|
1166
|
+
if (!h)
|
|
1167
|
+
return;
|
|
1168
|
+
void this.run('rollback', [], holdRef)
|
|
1169
|
+
.catch(() => {
|
|
1170
|
+
/* best-effort */
|
|
1171
|
+
})
|
|
1172
|
+
.finally(() => {
|
|
1173
|
+
h.finish();
|
|
1174
|
+
holdRef.hold = null;
|
|
1175
|
+
});
|
|
1048
1176
|
},
|
|
1049
1177
|
};
|
|
1050
1178
|
}
|
|
@@ -1052,8 +1180,11 @@ class PowdbEmbeddedPool {
|
|
|
1052
1180
|
if (this.closed)
|
|
1053
1181
|
return;
|
|
1054
1182
|
// The addon exposes no explicit close — drop the reference and let GC /
|
|
1055
|
-
// the engine's checkpoint flush.
|
|
1056
|
-
//
|
|
1183
|
+
// the engine's checkpoint flush. Marking the pool closed makes later
|
|
1184
|
+
// queries fail with a typed ConnectionError instead of silently running
|
|
1185
|
+
// against a handle the caller believes is gone. Caveat: durability is
|
|
1186
|
+
// checkpoint-bound, so hold the process open long enough for the final
|
|
1187
|
+
// WAL flush in short scripts.
|
|
1057
1188
|
this.closed = true;
|
|
1058
1189
|
}
|
|
1059
1190
|
}
|
|
@@ -1160,7 +1291,7 @@ async function turbinePowDB(target, schema, options = {}) {
|
|
|
1160
1291
|
let owns = false;
|
|
1161
1292
|
const poolOptions = { transactionQueueTimeoutMs: options.transactionQueueTimeoutMs };
|
|
1162
1293
|
if (typeof target === 'string') {
|
|
1163
|
-
const mod = await loadPowdb();
|
|
1294
|
+
const mod = options.powdbClientModule ?? (await loadPowdb());
|
|
1164
1295
|
const clientPool = new mod.Pool({ ...parsePowdbUrl(target), max: options.connectionLimit ?? 10 });
|
|
1165
1296
|
await assertNetworkedVersion(clientPool);
|
|
1166
1297
|
pool = new PowdbPool(clientPool, undefined, poolOptions);
|
|
@@ -1178,7 +1309,7 @@ async function turbinePowDB(target, schema, options = {}) {
|
|
|
1178
1309
|
pool = new PowdbPool(target, undefined, poolOptions);
|
|
1179
1310
|
}
|
|
1180
1311
|
else {
|
|
1181
|
-
const mod = await loadPowdb();
|
|
1312
|
+
const mod = options.powdbClientModule ?? (await loadPowdb());
|
|
1182
1313
|
const clientPool = new mod.Pool({ ...target, max: options.connectionLimit ?? 10 });
|
|
1183
1314
|
await assertNetworkedVersion(clientPool);
|
|
1184
1315
|
pool = new PowdbPool(clientPool, undefined, poolOptions);
|
|
@@ -1196,7 +1327,23 @@ async function turbinePowDB(target, schema, options = {}) {
|
|
|
1196
1327
|
warnOnUnlimited: options.warnOnUnlimited,
|
|
1197
1328
|
queryInterfaceFactory,
|
|
1198
1329
|
}, schema);
|
|
1199
|
-
if (
|
|
1330
|
+
if (owns) {
|
|
1331
|
+
// Turbine built this pool / embedded handle, so disconnect()/end() must
|
|
1332
|
+
// close it. client.ts sees TurbineConfig.pool as EXTERNAL (ownsPool =
|
|
1333
|
+
// false) and skips pool.end(); before this patch an owned networked
|
|
1334
|
+
// client leaked its live socket(s) on disconnect(), holding the process
|
|
1335
|
+
// open until powdb-server's idle timeout (~300s) closed them. Consistent
|
|
1336
|
+
// with turbineMssql's owned-pool patch.
|
|
1337
|
+
const baseDisconnect = client.disconnect.bind(client);
|
|
1338
|
+
const close = async () => {
|
|
1339
|
+
await baseDisconnect();
|
|
1340
|
+
await pool.end();
|
|
1341
|
+
};
|
|
1342
|
+
const patch = client;
|
|
1343
|
+
patch.disconnect = close;
|
|
1344
|
+
patch.end = close;
|
|
1345
|
+
}
|
|
1346
|
+
else {
|
|
1200
1347
|
// Injected pool — the caller owns its lifecycle.
|
|
1201
1348
|
client.disconnect = async () => { };
|
|
1202
1349
|
}
|
package/dist/cjs/powql.js
CHANGED
|
@@ -74,6 +74,7 @@ const node_crypto_1 = require("node:crypto");
|
|
|
74
74
|
const errors_js_1 = require("./errors.js");
|
|
75
75
|
const nested_write_js_1 = require("./nested-write.js");
|
|
76
76
|
const powdb_js_1 = require("./powdb.js");
|
|
77
|
+
const filters_js_1 = require("./query/filters.js");
|
|
77
78
|
const utils_js_1 = require("./query/utils.js");
|
|
78
79
|
const schema_js_1 = require("./schema.js");
|
|
79
80
|
/**
|
|
@@ -511,7 +512,22 @@ class PowqlInterface {
|
|
|
511
512
|
return '';
|
|
512
513
|
const parts = keys.map(([field, dir]) => {
|
|
513
514
|
if (dir && typeof dir === 'object') {
|
|
514
|
-
|
|
515
|
+
// Name the actual feature in the refusal — a pick-row ordering
|
|
516
|
+
// reported as "vector / distance ordering" sends users hunting for
|
|
517
|
+
// pgvector docs. All object-valued orderings stay E017 on PowDB.
|
|
518
|
+
const o = dir;
|
|
519
|
+
const feature = (0, filters_js_1.isRelationPickOrderBy)(dir)
|
|
520
|
+
? 'relation pick-row ordering'
|
|
521
|
+
: 'distance' in o
|
|
522
|
+
? 'vector / distance ordering'
|
|
523
|
+
: Array.isArray(o.path)
|
|
524
|
+
? 'JSON-path ordering'
|
|
525
|
+
: '_count' in o
|
|
526
|
+
? 'relation _count ordering'
|
|
527
|
+
: 'sort' in o || 'nulls' in o
|
|
528
|
+
? 'NULLS placement / sort-spec ordering'
|
|
529
|
+
: 'object-valued ordering';
|
|
530
|
+
throw new errors_js_1.UnsupportedFeatureError(feature, 'PowDB', `field "${field}"`);
|
|
515
531
|
}
|
|
516
532
|
return `${this.ref(field)} ${dir === 'desc' ? 'desc' : 'asc'}`;
|
|
517
533
|
});
|
|
@@ -985,7 +1001,13 @@ class PowqlInterface {
|
|
|
985
1001
|
const { TransactionClient } = await Promise.resolve().then(() => __importStar(require('./client.js')));
|
|
986
1002
|
const tx = new TransactionClient(client, this.schema, this.middlewares, this.options);
|
|
987
1003
|
const ctx = { schema: this.schema, tx: tx };
|
|
988
|
-
|
|
1004
|
+
// Plant the single-writer re-entrancy marker for the implicit tx's
|
|
1005
|
+
// subtree (same seam TurbineClient.$transaction uses) — user code that
|
|
1006
|
+
// fires db.$transaction from inside (e.g. $use middleware around a
|
|
1007
|
+
// nested-write child op) must fast-fail E017, not queue into deadlock.
|
|
1008
|
+
const wrap = client
|
|
1009
|
+
.wrapTransactionCallback;
|
|
1010
|
+
const result = await (wrap ? wrap(() => fn(ctx)) : fn(ctx));
|
|
989
1011
|
await client.query(d?.commitStatement?.() ?? 'commit');
|
|
990
1012
|
return result;
|
|
991
1013
|
}
|
|
@@ -1149,6 +1171,27 @@ class PowqlInterface {
|
|
|
1149
1171
|
}
|
|
1150
1172
|
async groupBy(args) {
|
|
1151
1173
|
return this.withMiddleware('groupBy', args, async () => {
|
|
1174
|
+
// The SQL-only groupBy extensions (DISTINCT ON row source, JSON-path
|
|
1175
|
+
// group keys / aggregate targets) have no PowQL equivalent: refuse
|
|
1176
|
+
// clearly instead of emitting broken PowQL.
|
|
1177
|
+
if (args.distinctOn) {
|
|
1178
|
+
throw new errors_js_1.UnsupportedFeatureError('groupBy distinctOn row source', 'PowDB');
|
|
1179
|
+
}
|
|
1180
|
+
for (const entry of args.by) {
|
|
1181
|
+
if (typeof entry !== 'string') {
|
|
1182
|
+
throw new errors_js_1.UnsupportedFeatureError('JSON-path groupBy keys', 'PowDB');
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
for (const fn of ['_sum', '_avg', '_min', '_max']) {
|
|
1186
|
+
const spec = args[fn];
|
|
1187
|
+
if (!spec)
|
|
1188
|
+
continue;
|
|
1189
|
+
for (const value of Object.values(spec)) {
|
|
1190
|
+
if (value !== undefined && typeof value !== 'boolean') {
|
|
1191
|
+
throw new errors_js_1.UnsupportedFeatureError(`JSON-path ${fn} aggregate targets`, 'PowDB');
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1152
1195
|
const params = [];
|
|
1153
1196
|
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
1154
1197
|
const where = this.buildWhere(resolvedWhere, params);
|
|
@@ -52,9 +52,11 @@ exports.includeKeysForBatching = includeKeysForBatching;
|
|
|
52
52
|
exports.stripFields = stripFields;
|
|
53
53
|
exports.neededParentKeyFields = neededParentKeyFields;
|
|
54
54
|
exports.resolveCountRelations = resolveCountRelations;
|
|
55
|
+
exports.rejectNestedPickOrder = rejectNestedPickOrder;
|
|
55
56
|
exports.loadRelationsBatched = loadRelationsBatched;
|
|
56
57
|
const errors_js_1 = require("../errors.js");
|
|
57
58
|
const schema_js_1 = require("../schema.js");
|
|
59
|
+
const filters_js_1 = require("./filters.js");
|
|
58
60
|
/**
|
|
59
61
|
* Max parent keys per follow-up query. On Postgres the whole key set travels as
|
|
60
62
|
* ONE array parameter (`= ANY($1)`), so this is not a bind-parameter limit — it
|
|
@@ -184,6 +186,34 @@ function resolveCountRelations(parentMeta, countSpec) {
|
|
|
184
186
|
function keyOf(value) {
|
|
185
187
|
return String(value);
|
|
186
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Reject pick-row relation ordering anywhere inside a `with` tree's orderBy —
|
|
191
|
+
* strategy parity with the join path, which throws this exact E003 at SQL
|
|
192
|
+
* build time (`pickOrderNestedError` in builder.ts). Without this guard the
|
|
193
|
+
* loaders would forward `options.orderBy` as the child reader's TOP-LEVEL
|
|
194
|
+
* findMany orderBy, where the pick shape compiles fine — so the same query
|
|
195
|
+
* would execute on 'batched' but throw on 'join'. Walks the whole tree up
|
|
196
|
+
* front so acceptance never depends on which levels have rows — the batched
|
|
197
|
+
* runners in builder.ts call this BEFORE the base query (a zero-row base
|
|
198
|
+
* result must still reject, exactly like the join strategy's build-time throw).
|
|
199
|
+
*/
|
|
200
|
+
function rejectNestedPickOrder(withClause) {
|
|
201
|
+
for (const spec of Object.values(withClause)) {
|
|
202
|
+
if (!spec || spec === true)
|
|
203
|
+
continue;
|
|
204
|
+
const options = spec;
|
|
205
|
+
if (options.orderBy) {
|
|
206
|
+
for (const [key, value] of Object.entries(options.orderBy)) {
|
|
207
|
+
if ((0, filters_js_1.isRelationPickOrderBy)(value)) {
|
|
208
|
+
throw new errors_js_1.ValidationError(`[turbine] Pick-row ordering on relation "${key}" is only supported in a top-level ` +
|
|
209
|
+
'findMany orderBy: nested `with` orderBy does not support it.');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (options.with)
|
|
214
|
+
rejectNestedPickOrder(options.with);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
187
217
|
/**
|
|
188
218
|
* Load every relation in `withClause` for `parents` and attach it onto each row
|
|
189
219
|
* in place. Mirrors the join strategy's output shape exactly. Recurses for nested
|
|
@@ -192,6 +222,10 @@ function keyOf(value) {
|
|
|
192
222
|
async function loadRelationsBatched(ctx, parents, withClause, timeout, depth = 0, path = [ctx.parentMeta.name]) {
|
|
193
223
|
if (depth >= MAX_DEPTH)
|
|
194
224
|
throw new errors_js_1.CircularRelationError([...path, '…']);
|
|
225
|
+
// Scope-rule parity with the join strategy: validate the whole tree BEFORE
|
|
226
|
+
// the empty-parents early return, so accept/reject never depends on data.
|
|
227
|
+
if (depth === 0)
|
|
228
|
+
rejectNestedPickOrder(withClause);
|
|
195
229
|
if (parents.length === 0)
|
|
196
230
|
return;
|
|
197
231
|
// Sibling relations are independent (each writes only its own parent[relName]
|