turbine-orm 0.34.0 → 0.35.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 +26 -4
- package/dist/cjs/dialect.js +1 -0
- package/dist/cjs/errors.js +41 -1
- package/dist/cjs/index.js +3 -2
- package/dist/cjs/mssql.js +5 -0
- package/dist/cjs/mysql.js +4 -0
- package/dist/cjs/powdb.js +156 -24
- package/dist/cjs/powql.js +448 -39
- package/dist/cjs/query/builder.js +60 -0
- package/dist/cjs/sqlite.js +3 -0
- package/dist/client.d.ts +32 -5
- package/dist/client.js +26 -4
- package/dist/dialect.d.ts +13 -0
- package/dist/dialect.js +1 -0
- package/dist/errors.d.ts +36 -0
- package/dist/errors.js +39 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/mssql.js +5 -0
- package/dist/mysql.js +4 -0
- package/dist/powdb.d.ts +115 -9
- package/dist/powdb.js +157 -25
- package/dist/powql.d.ts +133 -3
- package/dist/powql.js +449 -40
- package/dist/query/builder.d.ts +36 -1
- package/dist/query/builder.js +60 -0
- package/dist/query/deferred.d.ts +6 -2
- package/dist/query/types.d.ts +10 -6
- package/dist/sqlite.js +3 -0
- package/package.json +3 -3
package/dist/powdb.js
CHANGED
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
56
56
|
import { TurbineClient, } from './client.js';
|
|
57
57
|
import { postgresDialect } from './dialect.js';
|
|
58
|
-
import { ConnectionError, NotNullViolationError, TimeoutError, UniqueConstraintError, UnsupportedFeatureError, ValidationError, } from './errors.js';
|
|
58
|
+
import { ConnectionError, NotNullViolationError, ReadOnlyError, TimeoutError, UniqueConstraintError, UnsupportedFeatureError, ValidationError, } from './errors.js';
|
|
59
59
|
import importOptionalPeer from './optional-peer-import.cjs';
|
|
60
60
|
/**
|
|
61
61
|
* Capability descriptor for PowDB. PowQL generation is owned by
|
|
@@ -195,6 +195,7 @@ const POWDB_FEATURE_MIN_VERSION = {
|
|
|
195
195
|
introspection: '0.10',
|
|
196
196
|
jsonDocs: '0.12',
|
|
197
197
|
docFieldIndexes: '0.13',
|
|
198
|
+
serverJoins: '0.13',
|
|
198
199
|
};
|
|
199
200
|
/**
|
|
200
201
|
* Trusted-caller default: every FEATURE gate on, engine version unknown. Used
|
|
@@ -209,6 +210,7 @@ export const ALL_POWDB_CAPABILITIES = {
|
|
|
209
210
|
jsonDocs: true,
|
|
210
211
|
docFieldIndexes: true,
|
|
211
212
|
introspection: true,
|
|
213
|
+
serverJoins: true,
|
|
212
214
|
nativeRaw: false,
|
|
213
215
|
};
|
|
214
216
|
/** Parse a PowDB semver prefix (`0.13.0`, `0.13`, `1.2.3-rc`) into components, or `null`. */
|
|
@@ -236,6 +238,7 @@ export function capabilitiesFromVersion(version, opts = {}) {
|
|
|
236
238
|
jsonDocs: false,
|
|
237
239
|
docFieldIndexes: false,
|
|
238
240
|
introspection: false,
|
|
241
|
+
serverJoins: false,
|
|
239
242
|
nativeRaw: false,
|
|
240
243
|
};
|
|
241
244
|
}
|
|
@@ -244,6 +247,7 @@ export function capabilitiesFromVersion(version, opts = {}) {
|
|
|
244
247
|
introspection: atLeastVersion(sem, 0, 10),
|
|
245
248
|
jsonDocs: atLeastVersion(sem, 0, 12),
|
|
246
249
|
docFieldIndexes: atLeastVersion(sem, 0, 13),
|
|
250
|
+
serverJoins: atLeastVersion(sem, 0, 13),
|
|
247
251
|
nativeRaw: Boolean(opts.hasNativeRaw) && atLeastVersion(sem, 0, 13),
|
|
248
252
|
};
|
|
249
253
|
}
|
|
@@ -688,9 +692,10 @@ export function wrapPowdbError(err) {
|
|
|
688
692
|
const m = /column ['"]?(\w+)['"]?/i.exec(msg);
|
|
689
693
|
return new NotNullViolationError({ column: m?.[1], cause: err });
|
|
690
694
|
}
|
|
691
|
-
// Driver pool lifecycle errors (acquire after close, acquire timeout
|
|
692
|
-
//
|
|
693
|
-
|
|
695
|
+
// Driver pool lifecycle errors (acquire after close, acquire timeout, or a
|
|
696
|
+
// statement reaching an already-closed embedded handle) carry no .code:
|
|
697
|
+
// classify by message so both transports surface E004.
|
|
698
|
+
if (/pool closed|pool acquire timeout|database is closed/i.test(msg)) {
|
|
694
699
|
return new ConnectionError(`[turbine] PowDB connection unavailable: ${msg}`, { cause: err });
|
|
695
700
|
}
|
|
696
701
|
// Server-side transaction-gate wait bound (PowDB ≥ 0.10, default 5s): another
|
|
@@ -712,6 +717,54 @@ export function wrapPowdbError(err) {
|
|
|
712
717
|
/received unexpected frame|unknown message type|truncated payload|bad framing/i.test(msg)) {
|
|
713
718
|
return new ConnectionError(`[turbine] PowDB connection is in an invalid state: ${msg}`, { cause: err });
|
|
714
719
|
}
|
|
720
|
+
// Read-only refusal → ReadOnlyError (E018). Two engine shapes, both mapped by
|
|
721
|
+
// substring (the networked transport prefixes the message with `query failed:
|
|
722
|
+
// `, so never anchor on the start): an embedded database opened read-only for
|
|
723
|
+
// snapshot serving (`readonly mode: statement requires a writer …`), and a
|
|
724
|
+
// networked read-only role (`permission denied: role '<role>' cannot execute
|
|
725
|
+
// write statements`). These run BEFORE the generic validation regex below so a
|
|
726
|
+
// read-only write is surfaced as the routing signal E018, not a query defect.
|
|
727
|
+
// The driver spec (0.15) distinguishes them via `reason`: snapshot mode
|
|
728
|
+
// means "nothing can write here; route writes to the primary", RBAC means
|
|
729
|
+
// "this connection's role may not write here".
|
|
730
|
+
if (/readonly mode: statement requires a writer/i.test(msg)) {
|
|
731
|
+
return new ReadOnlyError(`PowDB refused a write on a read-only database: ${msg}.`, {
|
|
732
|
+
cause: err,
|
|
733
|
+
reason: 'snapshot',
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
if (/permission denied: role/i.test(msg)) {
|
|
737
|
+
return new ReadOnlyError(`PowDB refused a write for a read-only role: ${msg}.`, { cause: err, reason: 'rbac' });
|
|
738
|
+
}
|
|
739
|
+
// Open-time read-only failure: a read-only handle over a directory whose WAL
|
|
740
|
+
// still has uncommitted frames is refused (`cannot open read-only: the WAL is
|
|
741
|
+
// not empty …`). It is a connection failure (E004), not a query defect, the
|
|
742
|
+
// fix is to recover the directory with a writable open first.
|
|
743
|
+
if (/cannot open read-only: the WAL is not empty/i.test(msg)) {
|
|
744
|
+
return new ConnectionError(`[turbine] PowDB could not open the directory read-only: ${msg}. Open it once with a writable handle to ` +
|
|
745
|
+
'flush the WAL (recover the directory), then reopen it read-only for snapshot serving.', { cause: err });
|
|
746
|
+
}
|
|
747
|
+
// Per-query deadline → TimeoutError (E002). Message-path so it fires on the
|
|
748
|
+
// embedded transport too (code is always 'GenericFailure' there); retryable.
|
|
749
|
+
// Pass the engine prose through the message override (same pattern as the
|
|
750
|
+
// transaction-gate timeout below) so the real "query timeout after <n>ms"
|
|
751
|
+
// survives instead of rendering the placeholder "timed out after 0ms".
|
|
752
|
+
if (/query timeout after/i.test(msg)) {
|
|
753
|
+
return new TimeoutError(0, 'PowDB query', { message: `[turbine] PowDB ${msg}`, cause: err });
|
|
754
|
+
}
|
|
755
|
+
// Client-initiated cancellation → ConnectionError (E004). This is FINAL: the
|
|
756
|
+
// issuing client disconnected, so the query was a clean early return, never
|
|
757
|
+
// auto-retry it (the opt-in stale-read retry only replays stale-FRAME reads).
|
|
758
|
+
if (/query cancelled by client disconnect/i.test(msg)) {
|
|
759
|
+
return new ConnectionError(`[turbine] PowDB query cancelled by client disconnect: ${msg}`, { cause: err });
|
|
760
|
+
}
|
|
761
|
+
// Bounded join rejection → ValidationError (E003). The engine rejects a pure
|
|
762
|
+
// nested-loop join whose candidate-pair count (or result row count) exceeds
|
|
763
|
+
// the safety bound BEFORE executing, and names the fix in the message, keep
|
|
764
|
+
// that fix-hint intact so the caller knows how to make the join eligible.
|
|
765
|
+
if (/nested-loop join would evaluate|join result exceeds row limit/i.test(msg)) {
|
|
766
|
+
return new ValidationError(`[turbine] PowDB join rejected: ${msg}`);
|
|
767
|
+
}
|
|
715
768
|
// Type mismatch / parse / execution / storage / unexpected(token) / row too
|
|
716
769
|
// large → validation (E003). On the embedded transport these are the only
|
|
717
770
|
// signal we get (code is always 'GenericFailure'); on the networked path they
|
|
@@ -1052,12 +1105,20 @@ export class PowdbPool {
|
|
|
1052
1105
|
capabilities;
|
|
1053
1106
|
/** Opt-in first-statement-read replay on a stale wire frame (read by {@link PowqlInterface}). */
|
|
1054
1107
|
retryStaleReads;
|
|
1108
|
+
/**
|
|
1109
|
+
* True when the caller marked this pool read-only (`readonly: true`). Read by
|
|
1110
|
+
* {@link PowqlInterface}'s exec seam to fail writes fast with E018 before the
|
|
1111
|
+
* wire; the engine's own read-only-role refusal (mapped by
|
|
1112
|
+
* {@link wrapPowdbError}) is the backstop for raw / injected paths.
|
|
1113
|
+
*/
|
|
1114
|
+
readonly;
|
|
1055
1115
|
constructor(pool, toParam = (v) => toPowdbParam(v), options = {}) {
|
|
1056
1116
|
this.pool = pool;
|
|
1057
1117
|
this.toParam = toParam;
|
|
1058
1118
|
this.txGate = new PowdbTxGate(options.transactionQueueTimeoutMs ?? DEFAULT_TX_QUEUE_TIMEOUT_MS);
|
|
1059
1119
|
this.capabilities = options.capabilities ?? ALL_POWDB_CAPABILITIES;
|
|
1060
1120
|
this.retryStaleReads = options.retryStaleReads ?? false;
|
|
1121
|
+
this.readonly = options.readonly ?? false;
|
|
1061
1122
|
}
|
|
1062
1123
|
/**
|
|
1063
1124
|
* Run one statement on `c`, choosing the lossless native typed wire when the
|
|
@@ -1347,10 +1408,14 @@ export function materializePowql(powql, params) {
|
|
|
1347
1408
|
}
|
|
1348
1409
|
/**
|
|
1349
1410
|
* A {@link PgCompatPool} backed by an in-process `@zvndev/powdb-embedded`
|
|
1350
|
-
* `Database`.
|
|
1351
|
-
*
|
|
1352
|
-
*
|
|
1353
|
-
*
|
|
1411
|
+
* `Database`. On the addon's typed native wire (≥ 0.14, when
|
|
1412
|
+
* `capabilities.nativeRaw` is set) this pool binds positional `$N` params via
|
|
1413
|
+
* `queryWithParams` and decodes the typed cells, exactly like the networked
|
|
1414
|
+
* transport. On an older addon (no `queryWithParams`) it falls back to the
|
|
1415
|
+
* legacy string wire, which takes **no params array** (its `query(powql)`
|
|
1416
|
+
* accepts only a string), so each positional `$N` is materialized into a PowQL
|
|
1417
|
+
* literal via {@link materializePowql} before the text is handed to the engine.
|
|
1418
|
+
* One handle, single connection: transaction keywords (`begin`/`commit`/
|
|
1354
1419
|
* `rollback`) are issued serially as ordinary queries.
|
|
1355
1420
|
*/
|
|
1356
1421
|
export class PowdbEmbeddedPool {
|
|
@@ -1371,20 +1436,44 @@ export class PowdbEmbeddedPool {
|
|
|
1371
1436
|
poolHoldRef = { hold: null };
|
|
1372
1437
|
/**
|
|
1373
1438
|
* Feature capabilities of the embedded engine (resolved from the addon
|
|
1374
|
-
* package version). `nativeRaw` is
|
|
1375
|
-
*
|
|
1439
|
+
* package version). `nativeRaw` is true when the addon is ≥ 0.14 and the
|
|
1440
|
+
* opened handle exposes `queryWithParams` (the typed native wire); an older
|
|
1441
|
+
* addon has no such method, so it stays false and the legacy string wire is
|
|
1442
|
+
* used.
|
|
1376
1443
|
*/
|
|
1377
1444
|
capabilities;
|
|
1378
1445
|
/** Carried for surface uniformity with {@link PowdbPool}; inert on embedded (no protocol_error frames). */
|
|
1379
1446
|
retryStaleReads;
|
|
1447
|
+
/**
|
|
1448
|
+
* True when this pool was opened read-only (an `{ embedded, readonly: true }`
|
|
1449
|
+
* target, or a directly-constructed pool passed `readonly: true`). Read by
|
|
1450
|
+
* {@link PowqlInterface}'s exec seam to fail writes fast with E018 before the
|
|
1451
|
+
* wire; the engine's own refusal (mapped by {@link wrapPowdbError}) is the
|
|
1452
|
+
* backstop for raw / injected paths.
|
|
1453
|
+
*/
|
|
1454
|
+
readonly;
|
|
1380
1455
|
constructor(db, options = {}) {
|
|
1381
1456
|
this.db = db;
|
|
1382
1457
|
this.txGate = new PowdbTxGate(options.transactionQueueTimeoutMs ?? DEFAULT_TX_QUEUE_TIMEOUT_MS);
|
|
1383
1458
|
this.capabilities = options.capabilities ?? ALL_POWDB_CAPABILITIES;
|
|
1384
1459
|
this.retryStaleReads = options.retryStaleReads ?? false;
|
|
1460
|
+
this.readonly = options.readonly ?? false;
|
|
1385
1461
|
}
|
|
1386
|
-
/**
|
|
1462
|
+
/** Run the PowQL on the in-process engine, choosing the native or legacy wire. */
|
|
1387
1463
|
exec(powql, params) {
|
|
1464
|
+
// Native typed wire (addon ≥ 0.14): bind positional params with the SAME
|
|
1465
|
+
// binder the networked transport uses ({@link toPowdbParam} yields exactly
|
|
1466
|
+
// the NativeParam union null|bigint|number|boolean|string) and decode the
|
|
1467
|
+
// typed cells: a genuine str "null" survives, a json-null document stays
|
|
1468
|
+
// distinct from an absent value. Gated on the resolved capability AND a
|
|
1469
|
+
// per-call feature-detect so a heterogeneous injected handle cannot crash.
|
|
1470
|
+
if (this.capabilities.nativeRaw && typeof this.db.queryWithParams === 'function') {
|
|
1471
|
+
const bound = params.map((v) => toPowdbParam(v));
|
|
1472
|
+
return adaptNativeResult(this.db.queryWithParams(powql, bound));
|
|
1473
|
+
}
|
|
1474
|
+
// Legacy string wire (addon < 0.14): the engine takes no params array, so
|
|
1475
|
+
// materialize each `$N` into a PowQL literal. Byte-for-byte unchanged, kept
|
|
1476
|
+
// live and tested as the pre-0.14 fallback.
|
|
1388
1477
|
const materialized = materializePowql(powql, params);
|
|
1389
1478
|
return adaptResult(normalizeEmbeddedResult(this.db.query(materialized)));
|
|
1390
1479
|
}
|
|
@@ -1404,6 +1493,15 @@ export class PowdbEmbeddedPool {
|
|
|
1404
1493
|
// transaction callback throws re-entrant E017 fast; independent
|
|
1405
1494
|
// concurrent ones wait their FIFO turn.
|
|
1406
1495
|
holdRef.hold = await this.txGate.acquire();
|
|
1496
|
+
// The gate may have handed us the slot AFTER disconnect() closed the
|
|
1497
|
+
// handle (a transaction queued behind an in-flight one, released as the
|
|
1498
|
+
// pool shut down). Re-check before touching the now-closed engine, and
|
|
1499
|
+
// release the slot we just took so the queue keeps draining.
|
|
1500
|
+
if (this.closed) {
|
|
1501
|
+
holdRef.hold.finish();
|
|
1502
|
+
holdRef.hold = null;
|
|
1503
|
+
throw new ConnectionError('[turbine] The PowDB embedded pool is closed: disconnect() was already called on this client.');
|
|
1504
|
+
}
|
|
1407
1505
|
}
|
|
1408
1506
|
if ((ctl === 'commit' || ctl === 'rollback') && holdRef.hold === null) {
|
|
1409
1507
|
// This context never acquired the gate — its `begin` never ran (the
|
|
@@ -1479,13 +1577,15 @@ export class PowdbEmbeddedPool {
|
|
|
1479
1577
|
async end() {
|
|
1480
1578
|
if (this.closed)
|
|
1481
1579
|
return;
|
|
1482
|
-
// The addon exposes no explicit close — drop the reference and let GC /
|
|
1483
|
-
// the engine's checkpoint flush. Marking the pool closed makes later
|
|
1484
|
-
// queries fail with a typed ConnectionError instead of silently running
|
|
1485
|
-
// against a handle the caller believes is gone. Caveat: durability is
|
|
1486
|
-
// checkpoint-bound, so hold the process open long enough for the final
|
|
1487
|
-
// WAL flush in short scripts.
|
|
1488
1580
|
this.closed = true;
|
|
1581
|
+
// Addon ≥ 0.14 exposes an explicit checkpoint-flushing close(): call it so
|
|
1582
|
+
// the final WAL flush completes deterministically before the handle is
|
|
1583
|
+
// dropped. An older addon has no close, dropping the reference and letting
|
|
1584
|
+
// GC / the engine's checkpoint flush is the fallback (durability is then
|
|
1585
|
+
// checkpoint-bound, so a short script must hold the process open long enough
|
|
1586
|
+
// for the final flush). Marking the pool closed makes later queries fail
|
|
1587
|
+
// with a typed ConnectionError instead of running against a gone handle.
|
|
1588
|
+
this.db.close?.();
|
|
1489
1589
|
}
|
|
1490
1590
|
}
|
|
1491
1591
|
// ---------------------------------------------------------------------------
|
|
@@ -1552,12 +1652,34 @@ function resolveEmbeddedVersion() {
|
|
|
1552
1652
|
return importOptionalPeer.peerPackageVersion('@zvndev/powdb-embedded');
|
|
1553
1653
|
}
|
|
1554
1654
|
/** Open an embedded database handle, wrapping engine open failures (corrupt dir, etc.). */
|
|
1555
|
-
async function openEmbeddedPool(target, poolOptions = {}, assumeEngineVersion) {
|
|
1556
|
-
const mod = await loadPowdbEmbedded();
|
|
1557
|
-
const { embedded: dir, syncMode, memoryLimit } = target;
|
|
1655
|
+
async function openEmbeddedPool(target, poolOptions = {}, assumeEngineVersion, injectedModule) {
|
|
1656
|
+
const mod = injectedModule ?? (await loadPowdbEmbedded());
|
|
1657
|
+
const { embedded: dir, syncMode, memoryLimit, readonly } = target;
|
|
1658
|
+
// A read-only engine never writes, so a durability selector is meaningless
|
|
1659
|
+
// there, reject the combination loudly rather than silently ignoring one.
|
|
1660
|
+
if (readonly && syncMode !== undefined) {
|
|
1661
|
+
throw new ValidationError('[turbine] embedded `syncMode` is meaningless with `readonly: true` (a read-only database never writes). Remove one.');
|
|
1662
|
+
}
|
|
1558
1663
|
let db;
|
|
1559
1664
|
try {
|
|
1560
|
-
if (
|
|
1665
|
+
if (readonly) {
|
|
1666
|
+
// Read-only snapshot serving (addon ≥ 0.14): route to the openReadOnly*
|
|
1667
|
+
// constructors; feature-detect and fail with a clear version hint if the
|
|
1668
|
+
// installed addon predates them.
|
|
1669
|
+
if (memoryLimit !== undefined) {
|
|
1670
|
+
if (typeof mod.Database.openReadOnlyWithMemoryLimit !== 'function') {
|
|
1671
|
+
throw new ConnectionError('[turbine] embedded `readonly` + `memoryLimit` requires @zvndev/powdb-embedded >= 0.14 (openReadOnlyWithMemoryLimit).');
|
|
1672
|
+
}
|
|
1673
|
+
db = mod.Database.openReadOnlyWithMemoryLimit(dir, memoryLimit);
|
|
1674
|
+
}
|
|
1675
|
+
else {
|
|
1676
|
+
if (typeof mod.Database.openReadOnly !== 'function') {
|
|
1677
|
+
throw new ConnectionError('[turbine] embedded `readonly: true` requires @zvndev/powdb-embedded >= 0.14 (the installed addon has no openReadOnly).');
|
|
1678
|
+
}
|
|
1679
|
+
db = mod.Database.openReadOnly(dir);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
else if (memoryLimit !== undefined) {
|
|
1561
1683
|
if (typeof mod.Database.openWithMemoryLimit !== 'function') {
|
|
1562
1684
|
throw new ConnectionError('[turbine] embedded `memoryLimit` requires @zvndev/powdb-embedded ≥ 0.7.1.');
|
|
1563
1685
|
}
|
|
@@ -1578,11 +1700,19 @@ async function openEmbeddedPool(target, poolOptions = {}, assumeEngineVersion) {
|
|
|
1578
1700
|
}
|
|
1579
1701
|
db.setSyncMode(syncMode);
|
|
1580
1702
|
}
|
|
1581
|
-
//
|
|
1703
|
+
// Native typed wire is feature-detected on the OPENED handle: an addon ≥ 0.14
|
|
1704
|
+
// exposes `queryWithParams`, so nativeRaw turns on (server-gate ≥ 0.13 still
|
|
1705
|
+
// applies via the version); an older addon has no such method → false.
|
|
1582
1706
|
const capabilities = capabilitiesFromVersion(assumeEngineVersion ?? resolveEmbeddedVersion(), {
|
|
1583
|
-
hasNativeRaw:
|
|
1707
|
+
hasNativeRaw: typeof db.queryWithParams === 'function',
|
|
1708
|
+
});
|
|
1709
|
+
// A read-only target forces the pool's readonly flag; otherwise honor whatever
|
|
1710
|
+
// `poolOptions` (threaded from `options.readonly`) carried.
|
|
1711
|
+
return new PowdbEmbeddedPool(db, {
|
|
1712
|
+
...poolOptions,
|
|
1713
|
+
capabilities,
|
|
1714
|
+
readonly: Boolean(readonly) || Boolean(poolOptions.readonly),
|
|
1584
1715
|
});
|
|
1585
|
-
return new PowdbEmbeddedPool(db, { ...poolOptions, capabilities });
|
|
1586
1716
|
}
|
|
1587
1717
|
/**
|
|
1588
1718
|
* Bind Turbine to PowDB. `target` is one of:
|
|
@@ -1608,6 +1738,7 @@ export async function turbinePowDB(target, schema, options = {}) {
|
|
|
1608
1738
|
const poolOptions = {
|
|
1609
1739
|
transactionQueueTimeoutMs: options.transactionQueueTimeoutMs,
|
|
1610
1740
|
retryStaleReads: options.retryStaleReads,
|
|
1741
|
+
readonly: options.readonly,
|
|
1611
1742
|
};
|
|
1612
1743
|
const max = options.connectionLimit ?? 10;
|
|
1613
1744
|
if (typeof target === 'string') {
|
|
@@ -1622,7 +1753,7 @@ export async function turbinePowDB(target, schema, options = {}) {
|
|
|
1622
1753
|
pool = target;
|
|
1623
1754
|
}
|
|
1624
1755
|
else if (isEmbeddedTarget(target)) {
|
|
1625
|
-
pool = await openEmbeddedPool(target, poolOptions, options.assumeEngineVersion);
|
|
1756
|
+
pool = await openEmbeddedPool(target, poolOptions, options.assumeEngineVersion, options.powdbEmbeddedModule);
|
|
1626
1757
|
owns = true;
|
|
1627
1758
|
}
|
|
1628
1759
|
else if (isPowdbClientPool(target)) {
|
|
@@ -1649,6 +1780,7 @@ export async function turbinePowDB(target, schema, options = {}) {
|
|
|
1649
1780
|
logging: options.logging,
|
|
1650
1781
|
defaultLimit: options.defaultLimit,
|
|
1651
1782
|
warnOnUnlimited: options.warnOnUnlimited,
|
|
1783
|
+
relationLoadStrategy: options.relationLoadStrategy,
|
|
1652
1784
|
queryInterfaceFactory,
|
|
1653
1785
|
}, schema);
|
|
1654
1786
|
if (owns) {
|
package/dist/powql.d.ts
CHANGED
|
@@ -57,8 +57,16 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
57
57
|
constructor(pool: PowdbPool, table: string, schema: SchemaMetadata, middlewares?: MiddlewareFn[], options?: QueryInterfaceOptions);
|
|
58
58
|
/** Resolve a camelCase field name (or raw snake) to its column metadata. */
|
|
59
59
|
private column;
|
|
60
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* PowQL column reference for a field. Unqualified it is a dotted field
|
|
62
|
+
* reference (`.snake_name`), which bypasses keyword lookup. When an `alias`
|
|
63
|
+
* is supplied (the F2 join path) it is qualified (`alias.snake_name`) and the
|
|
64
|
+
* column name is backtick-quoted if it is a reserved word (a qualified
|
|
65
|
+
* `p.order` does NOT bypass keyword lookup, unlike the dotted `.order`).
|
|
66
|
+
*/
|
|
61
67
|
private ref;
|
|
68
|
+
/** Render a raw column name as a PowQL reference, qualified with `alias` when given. */
|
|
69
|
+
private colRefName;
|
|
62
70
|
/**
|
|
63
71
|
* Push a value into the param array and return its `$N` placeholder. When the
|
|
64
72
|
* value targets a `float` column it is wrapped in {@link PowdbFloatParam} so
|
|
@@ -92,6 +100,13 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
92
100
|
/**
|
|
93
101
|
* Compile a {@link WhereClause} into a PowQL filter expression, pushing every
|
|
94
102
|
* value as a positional `$N` param. Returns `''` when there are no conditions.
|
|
103
|
+
*
|
|
104
|
+
* When `alias` is supplied (the F2 native-join path) every field reference is
|
|
105
|
+
* qualified with it (`.col` → `alias.col`, JSON path bases too); params bind
|
|
106
|
+
* exactly as in the unqualified path. The caller only ever passes an alias for
|
|
107
|
+
* an already-RESOLVED where (relation filters pre-resolved to literal in-lists
|
|
108
|
+
* by {@link resolveRelationFilters}): the relation-key branch below still
|
|
109
|
+
* throws, so an unresolved relation filter can never leak into a join.
|
|
95
110
|
*/
|
|
96
111
|
private buildWhere;
|
|
97
112
|
/** Build a single `field: value | operator` condition. */
|
|
@@ -189,6 +204,8 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
189
204
|
* read. Read statements pass a read-shaped action from {@link POWQL_READ_ACTIONS}.
|
|
190
205
|
*/
|
|
191
206
|
private exec;
|
|
207
|
+
/** Build the E018 refusal for a write / `begin` on a read-only pool. */
|
|
208
|
+
private readOnlyError;
|
|
192
209
|
/**
|
|
193
210
|
* Execute one statement, with the opt-in single stale-frame READ replay. When
|
|
194
211
|
* `retryStaleReads` is on and a first-statement READ fails with the stale-wire
|
|
@@ -217,13 +234,44 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
217
234
|
* rare caller with no per-result flag (hand-built test pools). */
|
|
218
235
|
private shape;
|
|
219
236
|
findMany(args?: FindManyArgs<T>): Promise<T[]>;
|
|
220
|
-
/**
|
|
237
|
+
/**
|
|
238
|
+
* Compile the flat findMany select into PowQL (no execution), pushing values
|
|
239
|
+
* into `params`. Returns the query plus the RESOLVED where (relation filters
|
|
240
|
+
* already collapsed to literal in-lists) so the F2 join path can re-emit the
|
|
241
|
+
* exact parent predicate alias-qualified, and so {@link explain} can wrap it.
|
|
242
|
+
*/
|
|
243
|
+
private buildFind;
|
|
244
|
+
/** Build + run the flat findMany select; returns raw rows, the serving wire, and the resolved where. */
|
|
221
245
|
private runFind;
|
|
246
|
+
/**
|
|
247
|
+
* Diagnostic surface: compile the same PowQL {@link findMany} would run for
|
|
248
|
+
* `args` (no cache) and return the engine's plan as one string per line.
|
|
249
|
+
*
|
|
250
|
+
* Runs as a READ (`explain <query>`), so it is safe on a read-only pool and
|
|
251
|
+
* eligible for the stale-read replay. The line content is engine-owned and is
|
|
252
|
+
* NOT covered by semver (match plan node names / tree shape, never exact
|
|
253
|
+
* bytes; mirrors PowDB's own `explain` contract).
|
|
254
|
+
*
|
|
255
|
+
* Does NOT run through the middleware chain: plan text is a diagnostic, not
|
|
256
|
+
* entity rows, and `QueryInterface.explain` deliberately bypasses middleware
|
|
257
|
+
* too, so both engines agree.
|
|
258
|
+
*/
|
|
259
|
+
explain(args?: FindManyArgs<T>): Promise<string[]>;
|
|
222
260
|
findUnique(args: FindUniqueArgs<T>): Promise<T | null>;
|
|
223
261
|
findFirst(args?: FindManyArgs<T>): Promise<T | null>;
|
|
224
262
|
findUniqueOrThrow(args: FindUniqueArgs<T>): Promise<T>;
|
|
225
263
|
findFirstOrThrow(args?: FindManyArgs<T>): Promise<T>;
|
|
226
|
-
/**
|
|
264
|
+
/**
|
|
265
|
+
* Load each requested relation for `parents` and attach it onto each row.
|
|
266
|
+
*
|
|
267
|
+
* `parent` is supplied ONLY by the top-level {@link findMany} (its args +
|
|
268
|
+
* resolved where). When the effective `relationLoadStrategy` resolves to an
|
|
269
|
+
* explicit `'join'` and the pool advertises `serverJoins`, an eligible
|
|
270
|
+
* top-level relation is loaded with a native PowQL join instead of the keyed
|
|
271
|
+
* loaders (F2); everything else (nested `with` levels, ineligible shapes, and
|
|
272
|
+
* the default `'batched'` strategy) keeps the loaders. Output is byte-equal
|
|
273
|
+
* either way (the join reuses the same stitch / shape helpers).
|
|
274
|
+
*/
|
|
227
275
|
private loadRelations;
|
|
228
276
|
/**
|
|
229
277
|
* manyToMany nested read — a three-hop batched loader (no `json_agg`/join
|
|
@@ -234,6 +282,88 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
234
282
|
* (composite junction keys would need PowQL tuple-`in`, which it lacks).
|
|
235
283
|
*/
|
|
236
284
|
private loadManyToMany;
|
|
285
|
+
/**
|
|
286
|
+
* Resolve the effective relation-load strategy: the per-query arg wins, then
|
|
287
|
+
* the client config, then the PowDB default of `'batched'` (the keyed
|
|
288
|
+
* loaders). PowDB deliberately does NOT inherit the SQL-side implicit `'join'`
|
|
289
|
+
* default (that would silently flip every existing PowDB user onto brand-new
|
|
290
|
+
* join generation). Only a value the user actually set to `'join'` activates it.
|
|
291
|
+
*/
|
|
292
|
+
private resolveStrategy;
|
|
293
|
+
/**
|
|
294
|
+
* Per-relation eligibility for the join path (checked before the serverJoins
|
|
295
|
+
* capability). Any `false` here is a SILENT fallback to the keyed loaders (it
|
|
296
|
+
* is never an error), so an off-page or nested-`with` shape still returns
|
|
297
|
+
* correct rows:
|
|
298
|
+
* - the parent query must not be paged (`limit`/`offset`/`take`, including the
|
|
299
|
+
* configured `defaultLimit`): a parent-filter join under a page would scan
|
|
300
|
+
* children of off-page parents, where the loaders are strictly better;
|
|
301
|
+
* - the relation must not request a nested `with` (its subtree stays on the
|
|
302
|
+
* loaders this round) or a `distinct`;
|
|
303
|
+
* - single-column relation keys only (a composite key falls to the loader,
|
|
304
|
+
* which throws the same E017 as today);
|
|
305
|
+
* - the PARENT-SIDE correlation column must be a single-column PK or unique
|
|
306
|
+
* column, or the INNER join would re-emit one child copy per matching
|
|
307
|
+
* parent row (a non-unique correlation key produces duplicate children the
|
|
308
|
+
* loader never would). For hasMany/hasOne/m2m that column is the relation's
|
|
309
|
+
* `referenceKey` on THIS (fetched) table; for belongsTo it is the
|
|
310
|
+
* `referenceKey` on the TARGET table (the join's non-fetched side);
|
|
311
|
+
* - m2m keeps any `orderBy`/`limit`/`offset` on the loader (the junction-order
|
|
312
|
+
* stitch can't be reproduced by the 3-table join deterministically);
|
|
313
|
+
* - a to-one relation `limit`/`offset` (meaningless) stays on the loader, as
|
|
314
|
+
* does a to-many relation `limit`/`offset` when the parent set spills past
|
|
315
|
+
* one loader chunk (the loader limits per chunk, the join once globally).
|
|
316
|
+
*/
|
|
317
|
+
private joinEligible;
|
|
318
|
+
/**
|
|
319
|
+
* True when `col` is a single-column unique key of `tableMeta`: the sole
|
|
320
|
+
* primary-key column, a single-column entry in `uniqueColumns` (where a
|
|
321
|
+
* per-column `unique: true` and an introspected single-column unique constraint
|
|
322
|
+
* both land), or a single-column unique index. Used by {@link joinEligible} to
|
|
323
|
+
* keep the INNER-join path off relations whose parent-side correlation column
|
|
324
|
+
* can repeat (which would duplicate children).
|
|
325
|
+
*/
|
|
326
|
+
private isSingleColumnUnique;
|
|
327
|
+
/** Dispatch one eligible relation to the correct native-join loader. */
|
|
328
|
+
private loadRelationViaJoin;
|
|
329
|
+
/**
|
|
330
|
+
* manyToMany via chained joins: the target (alias `t`) → junction (alias `j`)
|
|
331
|
+
* → the already-fetched side (alias `p`), correlating `__tpk` from the
|
|
332
|
+
* junction's source key. Always a list, stitched exactly like the loader.
|
|
333
|
+
*/
|
|
334
|
+
private loadManyToManyViaJoin;
|
|
335
|
+
/**
|
|
336
|
+
* The target column list to project through the join (honouring select/omit),
|
|
337
|
+
* with a loud guard: a real column named `__tpk` would collide with the
|
|
338
|
+
* reserved correlation alias, so refuse rather than silently mis-stitch.
|
|
339
|
+
*/
|
|
340
|
+
private joinChildCols;
|
|
341
|
+
/**
|
|
342
|
+
* `{ __tpk: <tpkExpr>, <col>: <childAlias>.<col>, … }`. Each child column is
|
|
343
|
+
* ALIASED to its bare name (a bare qualified ref `c.col` would come back named
|
|
344
|
+
* `c.col`, not `col`) so the stitched rows shape identically to a flat select.
|
|
345
|
+
*/
|
|
346
|
+
private joinProjection;
|
|
347
|
+
/**
|
|
348
|
+
* `filter <parentWhere qualified p> [and <relationWhere qualified childAlias>]`.
|
|
349
|
+
* The parent where is the ALREADY-RESOLVED predicate (relation filters collapsed
|
|
350
|
+
* to literal in-lists before the base query ran); the relation where is resolved
|
|
351
|
+
* on the target the same way before qualifying, so a nested relation filter in
|
|
352
|
+
* the relation `where` never reaches the join unresolved. Params bind in order.
|
|
353
|
+
*/
|
|
354
|
+
private joinFilter;
|
|
355
|
+
/** Group join rows by their (normalized) `__tpk`, stripping it and shaping each child. */
|
|
356
|
+
private bucketByTpk;
|
|
357
|
+
/**
|
|
358
|
+
* Normalize a correlation key to a stable string map key so a parent's key
|
|
359
|
+
* value (a shaped entity field) and a child row's `__tpk` cell match across
|
|
360
|
+
* wires and column types. A `Date` maps to microseconds
|
|
361
|
+
* (`getTime()` ms times 1000), because a datetime correlation cell arrives as
|
|
362
|
+
* raw micros (bigint on the native wire, a micros string on the legacy wire),
|
|
363
|
+
* never as ms. bigint / number / string all stringify to the same digits, so
|
|
364
|
+
* an int key matches whether it came back typed or as text.
|
|
365
|
+
*/
|
|
366
|
+
private joinKey;
|
|
237
367
|
/** Split `data` into scalar assignments; reject relation (nested-write) keys. */
|
|
238
368
|
private scalarData;
|
|
239
369
|
/**
|