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/cjs/client.js
CHANGED
|
@@ -111,15 +111,27 @@ class TransactionClient {
|
|
|
111
111
|
schema;
|
|
112
112
|
middlewares;
|
|
113
113
|
queryOptions;
|
|
114
|
+
sourcePool;
|
|
114
115
|
tableCache = new Map();
|
|
115
116
|
savepointCounter = 0;
|
|
116
117
|
/** Active SQL dialect — owns savepoint keywords and raw-SQL placeholders. */
|
|
117
118
|
dialect;
|
|
118
|
-
constructor(client, schema, middlewares, queryOptions
|
|
119
|
+
constructor(client, schema, middlewares, queryOptions,
|
|
120
|
+
/**
|
|
121
|
+
* The parent pool this transaction runs on. Only its `readonly` and
|
|
122
|
+
* `capabilities` are read (both PowDB-only flags), so the transaction-scoped
|
|
123
|
+
* proxy pool built by {@link createTxPool} carries them through: without this
|
|
124
|
+
* a read-only client's `$transaction` writes bypass the E018 guard, and an
|
|
125
|
+
* older-engine client falls back to ALL_POWDB_CAPABILITIES inside the tx
|
|
126
|
+
* (emitting join PowQL a pre-0.13 engine rejects). Undefined / absent flags
|
|
127
|
+
* for a plain pg pool leave the proxy unchanged.
|
|
128
|
+
*/
|
|
129
|
+
sourcePool) {
|
|
119
130
|
this.client = client;
|
|
120
131
|
this.schema = schema;
|
|
121
132
|
this.middlewares = middlewares;
|
|
122
133
|
this.queryOptions = queryOptions;
|
|
134
|
+
this.sourcePool = sourcePool;
|
|
123
135
|
this.dialect = queryOptions?.dialect ?? dialect_js_1.postgresDialect;
|
|
124
136
|
// Auto-create typed table accessors for all tables in the schema
|
|
125
137
|
for (const tableName of Object.keys(schema.tables)) {
|
|
@@ -199,7 +211,7 @@ class TransactionClient {
|
|
|
199
211
|
const client = this.client;
|
|
200
212
|
// Return a minimal pool-compatible object that routes queries
|
|
201
213
|
// through the transaction client
|
|
202
|
-
|
|
214
|
+
const txPool = {
|
|
203
215
|
query: async (textOrConfig, values) => {
|
|
204
216
|
try {
|
|
205
217
|
if (typeof textOrConfig === 'string') {
|
|
@@ -216,6 +228,14 @@ class TransactionClient {
|
|
|
216
228
|
},
|
|
217
229
|
connect: () => Promise.resolve(client),
|
|
218
230
|
};
|
|
231
|
+
// Carry the parent pool's PowDB-only flags through so a transaction-scoped
|
|
232
|
+
// PowqlInterface reads the same read-only guard and capabilities it would
|
|
233
|
+
// outside the transaction (a plain pg pool has neither, so nothing changes).
|
|
234
|
+
if (this.sourcePool?.readonly !== undefined)
|
|
235
|
+
txPool.readonly = this.sourcePool.readonly;
|
|
236
|
+
if (this.sourcePool?.capabilities !== undefined)
|
|
237
|
+
txPool.capabilities = this.sourcePool.capabilities;
|
|
238
|
+
return txPool;
|
|
219
239
|
}
|
|
220
240
|
}
|
|
221
241
|
exports.TransactionClient = TransactionClient;
|
|
@@ -884,8 +904,10 @@ class TurbineClient {
|
|
|
884
904
|
await client.query(cfg.sql, cfg.params);
|
|
885
905
|
}
|
|
886
906
|
}
|
|
887
|
-
// Create the transaction client with typed table accessors
|
|
888
|
-
|
|
907
|
+
// Create the transaction client with typed table accessors. Pass the
|
|
908
|
+
// parent pool so its read-only guard + PowDB capabilities flow into the
|
|
909
|
+
// transaction-scoped proxy pool (see TransactionClient.createTxPool).
|
|
910
|
+
const tx = new TransactionClient(client, this.schema, this.middlewares, this.queryOptions, this.pool);
|
|
889
911
|
// Dynamically attach table accessors to tx
|
|
890
912
|
for (const tableName of Object.keys(this.schema.tables)) {
|
|
891
913
|
const camelName = tableName.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
package/dist/cjs/dialect.js
CHANGED
package/dist/cjs/errors.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* All Turbine errors extend TurbineError which includes a `code` property.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.UnsupportedFeatureError = exports.OptimisticLockError = exports.PipelineError = exports.ExclusionConstraintError = exports.CheckConstraintError = exports.SerializationFailureError = exports.DeadlockError = exports.NotNullViolationError = exports.ForeignKeyError = exports.UniqueConstraintError = exports.CircularRelationError = exports.MigrationError = exports.RelationError = exports.ConnectionError = exports.ValidationError = exports.TimeoutError = exports.NotFoundError = exports.TurbineError = exports.TurbineErrorCode = void 0;
|
|
9
|
+
exports.ReadOnlyError = exports.UnsupportedFeatureError = exports.OptimisticLockError = exports.PipelineError = exports.ExclusionConstraintError = exports.CheckConstraintError = exports.SerializationFailureError = exports.DeadlockError = exports.NotNullViolationError = exports.ForeignKeyError = exports.UniqueConstraintError = exports.CircularRelationError = exports.MigrationError = exports.RelationError = exports.ConnectionError = exports.ValidationError = exports.TimeoutError = exports.NotFoundError = exports.TurbineError = exports.TurbineErrorCode = void 0;
|
|
10
10
|
exports.setErrorMessageMode = setErrorMessageMode;
|
|
11
11
|
exports.getErrorMessageMode = getErrorMessageMode;
|
|
12
12
|
exports.wrapPgError = wrapPgError;
|
|
@@ -29,6 +29,7 @@ exports.TurbineErrorCode = {
|
|
|
29
29
|
OPTIMISTIC_LOCK: 'TURBINE_E015',
|
|
30
30
|
EXCLUSION_VIOLATION: 'TURBINE_E016',
|
|
31
31
|
UNSUPPORTED_FEATURE: 'TURBINE_E017',
|
|
32
|
+
READ_ONLY: 'TURBINE_E018',
|
|
32
33
|
};
|
|
33
34
|
/**
|
|
34
35
|
* Prefix a human message with its stable error code so logs are greppable
|
|
@@ -503,6 +504,45 @@ class UnsupportedFeatureError extends TurbineError {
|
|
|
503
504
|
}
|
|
504
505
|
}
|
|
505
506
|
exports.UnsupportedFeatureError = UnsupportedFeatureError;
|
|
507
|
+
/**
|
|
508
|
+
* Thrown when a write or DDL statement is refused because the target is
|
|
509
|
+
* read-only. Two shapes reach here, both on PowDB:
|
|
510
|
+
* - an embedded database opened read-only for snapshot serving refuses a write
|
|
511
|
+
* with `readonly mode: statement requires a writer …`;
|
|
512
|
+
* - a networked read-only role refuses a write with `permission denied: role
|
|
513
|
+
* '<role>' cannot execute write statements` (translated by `wrapPowdbError`).
|
|
514
|
+
* It is also raised locally, before the wire, when a write is issued on a pool
|
|
515
|
+
* the caller marked read-only (fail-fast). The message carries the engine text
|
|
516
|
+
* plus a hint to route writes to a writable primary.
|
|
517
|
+
*
|
|
518
|
+
* NOT retryable: the same write against the same read-only target fails
|
|
519
|
+
* identically; route it to a writable primary instead.
|
|
520
|
+
*/
|
|
521
|
+
class ReadOnlyError extends TurbineError {
|
|
522
|
+
/**
|
|
523
|
+
* Why the write was refused. `'snapshot'`: the database itself is read-only
|
|
524
|
+
* (snapshot serving, an embedded `readonly: true` open, or the client-level
|
|
525
|
+
* fail-fast flag), so NOTHING can write here and writes must route to the
|
|
526
|
+
* primary. `'rbac'`: the database is writable but THIS connection's role may
|
|
527
|
+
* not write (per-connection permission), so re-authenticating may suffice.
|
|
528
|
+
*/
|
|
529
|
+
reason;
|
|
530
|
+
/**
|
|
531
|
+
* @param detail human-readable description of the refused write (the engine
|
|
532
|
+
* message, or a local fail-fast description). A "route writes to a writable
|
|
533
|
+
* primary" hint is always appended.
|
|
534
|
+
* @param options optional driver `cause` to preserve when wrapping a refusal,
|
|
535
|
+
* and the refusal `reason` (default `'snapshot'`).
|
|
536
|
+
*/
|
|
537
|
+
constructor(detail, options) {
|
|
538
|
+
super(exports.TurbineErrorCode.READ_ONLY, `[turbine] ${detail} Route writes to a writable primary.`, {
|
|
539
|
+
cause: options?.cause,
|
|
540
|
+
});
|
|
541
|
+
this.name = 'ReadOnlyError';
|
|
542
|
+
this.reason = options?.reason ?? 'snapshot';
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
exports.ReadOnlyError = ReadOnlyError;
|
|
506
546
|
/**
|
|
507
547
|
* Parse column names out of a pg `detail` string like:
|
|
508
548
|
* "Key (email)=(foo@bar) already exists."
|
package/dist/cjs/index.js
CHANGED
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
* ```
|
|
35
35
|
*/
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
-
exports.
|
|
38
|
-
exports.TypedSqlQuery = exports.buildTypedSql = exports.turbineHttp = exports.defineSeed = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.schemaDefToMetadata = exports.table = exports.isDocFieldIndexDef = exports.defineSchema = exports.column = void 0;
|
|
37
|
+
exports.applyManyToManyRelations = exports.snakeToPascal = exports.snakeToCamel = exports.singularize = exports.pgTypeToTs = exports.pgArrayType = exports.normalizeKeyColumns = exports.isDateType = exports.camelToSnake = exports.validateChannel = exports.QueryInterface = exports.pipelineSupported = exports.executePipeline = exports.hasRelationFields = exports.executeNestedUpdate = exports.executeNestedCreate = exports.introspect = exports.generate = exports.wrapPgError = exports.ValidationError = exports.UnsupportedFeatureError = exports.UniqueConstraintError = exports.TurbineErrorCode = exports.TurbineError = exports.TimeoutError = exports.setErrorMessageMode = exports.SerializationFailureError = exports.RelationError = exports.ReadOnlyError = exports.PipelineError = exports.OptimisticLockError = exports.NotNullViolationError = exports.NotFoundError = exports.MigrationError = exports.getErrorMessageMode = exports.ForeignKeyError = exports.ExclusionConstraintError = exports.DeadlockError = exports.ConnectionError = exports.CircularRelationError = exports.CheckConstraintError = exports.postgresDialect = exports.withRetry = exports.TurbineClient = exports.TransactionClient = exports.yugabytedb = exports.timescale = exports.postgresql = exports.cockroachdb = exports.alloydb = void 0;
|
|
38
|
+
exports.TypedSqlQuery = exports.buildTypedSql = exports.turbineHttp = exports.defineSeed = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.schemaDefToMetadata = exports.table = exports.isDocFieldIndexDef = exports.defineSchema = exports.column = exports.ColumnBuilder = void 0;
|
|
39
39
|
var index_js_1 = require("./adapters/index.js");
|
|
40
40
|
Object.defineProperty(exports, "alloydb", { enumerable: true, get: function () { return index_js_1.alloydb; } });
|
|
41
41
|
Object.defineProperty(exports, "cockroachdb", { enumerable: true, get: function () { return index_js_1.cockroachdb; } });
|
|
@@ -63,6 +63,7 @@ Object.defineProperty(exports, "NotFoundError", { enumerable: true, get: functio
|
|
|
63
63
|
Object.defineProperty(exports, "NotNullViolationError", { enumerable: true, get: function () { return errors_js_1.NotNullViolationError; } });
|
|
64
64
|
Object.defineProperty(exports, "OptimisticLockError", { enumerable: true, get: function () { return errors_js_1.OptimisticLockError; } });
|
|
65
65
|
Object.defineProperty(exports, "PipelineError", { enumerable: true, get: function () { return errors_js_1.PipelineError; } });
|
|
66
|
+
Object.defineProperty(exports, "ReadOnlyError", { enumerable: true, get: function () { return errors_js_1.ReadOnlyError; } });
|
|
66
67
|
Object.defineProperty(exports, "RelationError", { enumerable: true, get: function () { return errors_js_1.RelationError; } });
|
|
67
68
|
Object.defineProperty(exports, "SerializationFailureError", { enumerable: true, get: function () { return errors_js_1.SerializationFailureError; } });
|
|
68
69
|
Object.defineProperty(exports, "setErrorMessageMode", { enumerable: true, get: function () { return errors_js_1.setErrorMessageMode; } });
|
package/dist/cjs/mssql.js
CHANGED
|
@@ -483,6 +483,11 @@ exports.mssqlDialect = {
|
|
|
483
483
|
supportsLateralJoin: false,
|
|
484
484
|
// sp_getapplock / sp_releaseapplock exist (used by a future migrate adapter).
|
|
485
485
|
supportsAdvisoryLock: true,
|
|
486
|
+
// No in-band EXPLAIN: SQL Server's SHOWPLAN is a session toggle
|
|
487
|
+
// (SET SHOWPLAN_ALL ON), not a statement prefix, so a compiled query cannot
|
|
488
|
+
// be explained in one round-trip. Override the inherited Postgres `EXPLAIN`
|
|
489
|
+
// to absent → QueryInterface.explain() throws E017.
|
|
490
|
+
explainQuery: undefined,
|
|
486
491
|
// FOR JSON over zero rows is NULL → coalesced in the relation override.
|
|
487
492
|
aggSupportsInlineOrderBy: false,
|
|
488
493
|
jsonPathSupport: 'limited',
|
package/dist/cjs/mysql.js
CHANGED
|
@@ -384,6 +384,10 @@ exports.mysqlDialect = {
|
|
|
384
384
|
supportsLateralJoin: false,
|
|
385
385
|
// GET_LOCK / RELEASE_LOCK exist (used by a future migrate adapter).
|
|
386
386
|
supportsAdvisoryLock: true,
|
|
387
|
+
// Plain `EXPLAIN` (one row of tabular plan columns) works on every supported
|
|
388
|
+
// MySQL 8.0.x; the readable `FORMAT=TREE` variant only exists from 8.0.16 and
|
|
389
|
+
// the engine floor here is 8.0.0. Plan text is a diagnostic, not a contract.
|
|
390
|
+
explainQuery: { prefix: 'EXPLAIN' },
|
|
387
391
|
// JSON_ARRAYAGG has no inline ORDER BY argument → force the inner-subquery
|
|
388
392
|
// rewrite for every ordered to-many relation.
|
|
389
393
|
aggSupportsInlineOrderBy: false,
|
package/dist/cjs/powdb.js
CHANGED
|
@@ -252,6 +252,7 @@ const POWDB_FEATURE_MIN_VERSION = {
|
|
|
252
252
|
introspection: '0.10',
|
|
253
253
|
jsonDocs: '0.12',
|
|
254
254
|
docFieldIndexes: '0.13',
|
|
255
|
+
serverJoins: '0.13',
|
|
255
256
|
};
|
|
256
257
|
/**
|
|
257
258
|
* Trusted-caller default: every FEATURE gate on, engine version unknown. Used
|
|
@@ -266,6 +267,7 @@ exports.ALL_POWDB_CAPABILITIES = {
|
|
|
266
267
|
jsonDocs: true,
|
|
267
268
|
docFieldIndexes: true,
|
|
268
269
|
introspection: true,
|
|
270
|
+
serverJoins: true,
|
|
269
271
|
nativeRaw: false,
|
|
270
272
|
};
|
|
271
273
|
/** Parse a PowDB semver prefix (`0.13.0`, `0.13`, `1.2.3-rc`) into components, or `null`. */
|
|
@@ -293,6 +295,7 @@ function capabilitiesFromVersion(version, opts = {}) {
|
|
|
293
295
|
jsonDocs: false,
|
|
294
296
|
docFieldIndexes: false,
|
|
295
297
|
introspection: false,
|
|
298
|
+
serverJoins: false,
|
|
296
299
|
nativeRaw: false,
|
|
297
300
|
};
|
|
298
301
|
}
|
|
@@ -301,6 +304,7 @@ function capabilitiesFromVersion(version, opts = {}) {
|
|
|
301
304
|
introspection: atLeastVersion(sem, 0, 10),
|
|
302
305
|
jsonDocs: atLeastVersion(sem, 0, 12),
|
|
303
306
|
docFieldIndexes: atLeastVersion(sem, 0, 13),
|
|
307
|
+
serverJoins: atLeastVersion(sem, 0, 13),
|
|
304
308
|
nativeRaw: Boolean(opts.hasNativeRaw) && atLeastVersion(sem, 0, 13),
|
|
305
309
|
};
|
|
306
310
|
}
|
|
@@ -745,9 +749,10 @@ function wrapPowdbError(err) {
|
|
|
745
749
|
const m = /column ['"]?(\w+)['"]?/i.exec(msg);
|
|
746
750
|
return new errors_js_1.NotNullViolationError({ column: m?.[1], cause: err });
|
|
747
751
|
}
|
|
748
|
-
// Driver pool lifecycle errors (acquire after close, acquire timeout
|
|
749
|
-
//
|
|
750
|
-
|
|
752
|
+
// Driver pool lifecycle errors (acquire after close, acquire timeout, or a
|
|
753
|
+
// statement reaching an already-closed embedded handle) carry no .code:
|
|
754
|
+
// classify by message so both transports surface E004.
|
|
755
|
+
if (/pool closed|pool acquire timeout|database is closed/i.test(msg)) {
|
|
751
756
|
return new errors_js_1.ConnectionError(`[turbine] PowDB connection unavailable: ${msg}`, { cause: err });
|
|
752
757
|
}
|
|
753
758
|
// Server-side transaction-gate wait bound (PowDB ≥ 0.10, default 5s): another
|
|
@@ -769,6 +774,54 @@ function wrapPowdbError(err) {
|
|
|
769
774
|
/received unexpected frame|unknown message type|truncated payload|bad framing/i.test(msg)) {
|
|
770
775
|
return new errors_js_1.ConnectionError(`[turbine] PowDB connection is in an invalid state: ${msg}`, { cause: err });
|
|
771
776
|
}
|
|
777
|
+
// Read-only refusal → ReadOnlyError (E018). Two engine shapes, both mapped by
|
|
778
|
+
// substring (the networked transport prefixes the message with `query failed:
|
|
779
|
+
// `, so never anchor on the start): an embedded database opened read-only for
|
|
780
|
+
// snapshot serving (`readonly mode: statement requires a writer …`), and a
|
|
781
|
+
// networked read-only role (`permission denied: role '<role>' cannot execute
|
|
782
|
+
// write statements`). These run BEFORE the generic validation regex below so a
|
|
783
|
+
// read-only write is surfaced as the routing signal E018, not a query defect.
|
|
784
|
+
// The driver spec (0.15) distinguishes them via `reason`: snapshot mode
|
|
785
|
+
// means "nothing can write here; route writes to the primary", RBAC means
|
|
786
|
+
// "this connection's role may not write here".
|
|
787
|
+
if (/readonly mode: statement requires a writer/i.test(msg)) {
|
|
788
|
+
return new errors_js_1.ReadOnlyError(`PowDB refused a write on a read-only database: ${msg}.`, {
|
|
789
|
+
cause: err,
|
|
790
|
+
reason: 'snapshot',
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
if (/permission denied: role/i.test(msg)) {
|
|
794
|
+
return new errors_js_1.ReadOnlyError(`PowDB refused a write for a read-only role: ${msg}.`, { cause: err, reason: 'rbac' });
|
|
795
|
+
}
|
|
796
|
+
// Open-time read-only failure: a read-only handle over a directory whose WAL
|
|
797
|
+
// still has uncommitted frames is refused (`cannot open read-only: the WAL is
|
|
798
|
+
// not empty …`). It is a connection failure (E004), not a query defect, the
|
|
799
|
+
// fix is to recover the directory with a writable open first.
|
|
800
|
+
if (/cannot open read-only: the WAL is not empty/i.test(msg)) {
|
|
801
|
+
return new errors_js_1.ConnectionError(`[turbine] PowDB could not open the directory read-only: ${msg}. Open it once with a writable handle to ` +
|
|
802
|
+
'flush the WAL (recover the directory), then reopen it read-only for snapshot serving.', { cause: err });
|
|
803
|
+
}
|
|
804
|
+
// Per-query deadline → TimeoutError (E002). Message-path so it fires on the
|
|
805
|
+
// embedded transport too (code is always 'GenericFailure' there); retryable.
|
|
806
|
+
// Pass the engine prose through the message override (same pattern as the
|
|
807
|
+
// transaction-gate timeout below) so the real "query timeout after <n>ms"
|
|
808
|
+
// survives instead of rendering the placeholder "timed out after 0ms".
|
|
809
|
+
if (/query timeout after/i.test(msg)) {
|
|
810
|
+
return new errors_js_1.TimeoutError(0, 'PowDB query', { message: `[turbine] PowDB ${msg}`, cause: err });
|
|
811
|
+
}
|
|
812
|
+
// Client-initiated cancellation → ConnectionError (E004). This is FINAL: the
|
|
813
|
+
// issuing client disconnected, so the query was a clean early return, never
|
|
814
|
+
// auto-retry it (the opt-in stale-read retry only replays stale-FRAME reads).
|
|
815
|
+
if (/query cancelled by client disconnect/i.test(msg)) {
|
|
816
|
+
return new errors_js_1.ConnectionError(`[turbine] PowDB query cancelled by client disconnect: ${msg}`, { cause: err });
|
|
817
|
+
}
|
|
818
|
+
// Bounded join rejection → ValidationError (E003). The engine rejects a pure
|
|
819
|
+
// nested-loop join whose candidate-pair count (or result row count) exceeds
|
|
820
|
+
// the safety bound BEFORE executing, and names the fix in the message, keep
|
|
821
|
+
// that fix-hint intact so the caller knows how to make the join eligible.
|
|
822
|
+
if (/nested-loop join would evaluate|join result exceeds row limit/i.test(msg)) {
|
|
823
|
+
return new errors_js_1.ValidationError(`[turbine] PowDB join rejected: ${msg}`);
|
|
824
|
+
}
|
|
772
825
|
// Type mismatch / parse / execution / storage / unexpected(token) / row too
|
|
773
826
|
// large → validation (E003). On the embedded transport these are the only
|
|
774
827
|
// signal we get (code is always 'GenericFailure'); on the networked path they
|
|
@@ -1109,12 +1162,20 @@ class PowdbPool {
|
|
|
1109
1162
|
capabilities;
|
|
1110
1163
|
/** Opt-in first-statement-read replay on a stale wire frame (read by {@link PowqlInterface}). */
|
|
1111
1164
|
retryStaleReads;
|
|
1165
|
+
/**
|
|
1166
|
+
* True when the caller marked this pool read-only (`readonly: true`). Read by
|
|
1167
|
+
* {@link PowqlInterface}'s exec seam to fail writes fast with E018 before the
|
|
1168
|
+
* wire; the engine's own read-only-role refusal (mapped by
|
|
1169
|
+
* {@link wrapPowdbError}) is the backstop for raw / injected paths.
|
|
1170
|
+
*/
|
|
1171
|
+
readonly;
|
|
1112
1172
|
constructor(pool, toParam = (v) => toPowdbParam(v), options = {}) {
|
|
1113
1173
|
this.pool = pool;
|
|
1114
1174
|
this.toParam = toParam;
|
|
1115
1175
|
this.txGate = new PowdbTxGate(options.transactionQueueTimeoutMs ?? exports.DEFAULT_TX_QUEUE_TIMEOUT_MS);
|
|
1116
1176
|
this.capabilities = options.capabilities ?? exports.ALL_POWDB_CAPABILITIES;
|
|
1117
1177
|
this.retryStaleReads = options.retryStaleReads ?? false;
|
|
1178
|
+
this.readonly = options.readonly ?? false;
|
|
1118
1179
|
}
|
|
1119
1180
|
/**
|
|
1120
1181
|
* Run one statement on `c`, choosing the lossless native typed wire when the
|
|
@@ -1405,10 +1466,14 @@ function materializePowql(powql, params) {
|
|
|
1405
1466
|
}
|
|
1406
1467
|
/**
|
|
1407
1468
|
* A {@link PgCompatPool} backed by an in-process `@zvndev/powdb-embedded`
|
|
1408
|
-
* `Database`.
|
|
1409
|
-
*
|
|
1410
|
-
*
|
|
1411
|
-
*
|
|
1469
|
+
* `Database`. On the addon's typed native wire (≥ 0.14, when
|
|
1470
|
+
* `capabilities.nativeRaw` is set) this pool binds positional `$N` params via
|
|
1471
|
+
* `queryWithParams` and decodes the typed cells, exactly like the networked
|
|
1472
|
+
* transport. On an older addon (no `queryWithParams`) it falls back to the
|
|
1473
|
+
* legacy string wire, which takes **no params array** (its `query(powql)`
|
|
1474
|
+
* accepts only a string), so each positional `$N` is materialized into a PowQL
|
|
1475
|
+
* literal via {@link materializePowql} before the text is handed to the engine.
|
|
1476
|
+
* One handle, single connection: transaction keywords (`begin`/`commit`/
|
|
1412
1477
|
* `rollback`) are issued serially as ordinary queries.
|
|
1413
1478
|
*/
|
|
1414
1479
|
class PowdbEmbeddedPool {
|
|
@@ -1429,20 +1494,44 @@ class PowdbEmbeddedPool {
|
|
|
1429
1494
|
poolHoldRef = { hold: null };
|
|
1430
1495
|
/**
|
|
1431
1496
|
* Feature capabilities of the embedded engine (resolved from the addon
|
|
1432
|
-
* package version). `nativeRaw` is
|
|
1433
|
-
*
|
|
1497
|
+
* package version). `nativeRaw` is true when the addon is ≥ 0.14 and the
|
|
1498
|
+
* opened handle exposes `queryWithParams` (the typed native wire); an older
|
|
1499
|
+
* addon has no such method, so it stays false and the legacy string wire is
|
|
1500
|
+
* used.
|
|
1434
1501
|
*/
|
|
1435
1502
|
capabilities;
|
|
1436
1503
|
/** Carried for surface uniformity with {@link PowdbPool}; inert on embedded (no protocol_error frames). */
|
|
1437
1504
|
retryStaleReads;
|
|
1505
|
+
/**
|
|
1506
|
+
* True when this pool was opened read-only (an `{ embedded, readonly: true }`
|
|
1507
|
+
* target, or a directly-constructed pool passed `readonly: true`). Read by
|
|
1508
|
+
* {@link PowqlInterface}'s exec seam to fail writes fast with E018 before the
|
|
1509
|
+
* wire; the engine's own refusal (mapped by {@link wrapPowdbError}) is the
|
|
1510
|
+
* backstop for raw / injected paths.
|
|
1511
|
+
*/
|
|
1512
|
+
readonly;
|
|
1438
1513
|
constructor(db, options = {}) {
|
|
1439
1514
|
this.db = db;
|
|
1440
1515
|
this.txGate = new PowdbTxGate(options.transactionQueueTimeoutMs ?? exports.DEFAULT_TX_QUEUE_TIMEOUT_MS);
|
|
1441
1516
|
this.capabilities = options.capabilities ?? exports.ALL_POWDB_CAPABILITIES;
|
|
1442
1517
|
this.retryStaleReads = options.retryStaleReads ?? false;
|
|
1518
|
+
this.readonly = options.readonly ?? false;
|
|
1443
1519
|
}
|
|
1444
|
-
/**
|
|
1520
|
+
/** Run the PowQL on the in-process engine, choosing the native or legacy wire. */
|
|
1445
1521
|
exec(powql, params) {
|
|
1522
|
+
// Native typed wire (addon ≥ 0.14): bind positional params with the SAME
|
|
1523
|
+
// binder the networked transport uses ({@link toPowdbParam} yields exactly
|
|
1524
|
+
// the NativeParam union null|bigint|number|boolean|string) and decode the
|
|
1525
|
+
// typed cells: a genuine str "null" survives, a json-null document stays
|
|
1526
|
+
// distinct from an absent value. Gated on the resolved capability AND a
|
|
1527
|
+
// per-call feature-detect so a heterogeneous injected handle cannot crash.
|
|
1528
|
+
if (this.capabilities.nativeRaw && typeof this.db.queryWithParams === 'function') {
|
|
1529
|
+
const bound = params.map((v) => toPowdbParam(v));
|
|
1530
|
+
return adaptNativeResult(this.db.queryWithParams(powql, bound));
|
|
1531
|
+
}
|
|
1532
|
+
// Legacy string wire (addon < 0.14): the engine takes no params array, so
|
|
1533
|
+
// materialize each `$N` into a PowQL literal. Byte-for-byte unchanged, kept
|
|
1534
|
+
// live and tested as the pre-0.14 fallback.
|
|
1446
1535
|
const materialized = materializePowql(powql, params);
|
|
1447
1536
|
return adaptResult(normalizeEmbeddedResult(this.db.query(materialized)));
|
|
1448
1537
|
}
|
|
@@ -1462,6 +1551,15 @@ class PowdbEmbeddedPool {
|
|
|
1462
1551
|
// transaction callback throws re-entrant E017 fast; independent
|
|
1463
1552
|
// concurrent ones wait their FIFO turn.
|
|
1464
1553
|
holdRef.hold = await this.txGate.acquire();
|
|
1554
|
+
// The gate may have handed us the slot AFTER disconnect() closed the
|
|
1555
|
+
// handle (a transaction queued behind an in-flight one, released as the
|
|
1556
|
+
// pool shut down). Re-check before touching the now-closed engine, and
|
|
1557
|
+
// release the slot we just took so the queue keeps draining.
|
|
1558
|
+
if (this.closed) {
|
|
1559
|
+
holdRef.hold.finish();
|
|
1560
|
+
holdRef.hold = null;
|
|
1561
|
+
throw new errors_js_1.ConnectionError('[turbine] The PowDB embedded pool is closed: disconnect() was already called on this client.');
|
|
1562
|
+
}
|
|
1465
1563
|
}
|
|
1466
1564
|
if ((ctl === 'commit' || ctl === 'rollback') && holdRef.hold === null) {
|
|
1467
1565
|
// This context never acquired the gate — its `begin` never ran (the
|
|
@@ -1537,13 +1635,15 @@ class PowdbEmbeddedPool {
|
|
|
1537
1635
|
async end() {
|
|
1538
1636
|
if (this.closed)
|
|
1539
1637
|
return;
|
|
1540
|
-
// The addon exposes no explicit close — drop the reference and let GC /
|
|
1541
|
-
// the engine's checkpoint flush. Marking the pool closed makes later
|
|
1542
|
-
// queries fail with a typed ConnectionError instead of silently running
|
|
1543
|
-
// against a handle the caller believes is gone. Caveat: durability is
|
|
1544
|
-
// checkpoint-bound, so hold the process open long enough for the final
|
|
1545
|
-
// WAL flush in short scripts.
|
|
1546
1638
|
this.closed = true;
|
|
1639
|
+
// Addon ≥ 0.14 exposes an explicit checkpoint-flushing close(): call it so
|
|
1640
|
+
// the final WAL flush completes deterministically before the handle is
|
|
1641
|
+
// dropped. An older addon has no close, dropping the reference and letting
|
|
1642
|
+
// GC / the engine's checkpoint flush is the fallback (durability is then
|
|
1643
|
+
// checkpoint-bound, so a short script must hold the process open long enough
|
|
1644
|
+
// for the final flush). Marking the pool closed makes later queries fail
|
|
1645
|
+
// with a typed ConnectionError instead of running against a gone handle.
|
|
1646
|
+
this.db.close?.();
|
|
1547
1647
|
}
|
|
1548
1648
|
}
|
|
1549
1649
|
exports.PowdbEmbeddedPool = PowdbEmbeddedPool;
|
|
@@ -1613,12 +1713,34 @@ function resolveEmbeddedVersion() {
|
|
|
1613
1713
|
return optional_peer_import_cjs_1.default.peerPackageVersion('@zvndev/powdb-embedded');
|
|
1614
1714
|
}
|
|
1615
1715
|
/** Open an embedded database handle, wrapping engine open failures (corrupt dir, etc.). */
|
|
1616
|
-
async function openEmbeddedPool(target, poolOptions = {}, assumeEngineVersion) {
|
|
1617
|
-
const mod = await loadPowdbEmbedded();
|
|
1618
|
-
const { embedded: dir, syncMode, memoryLimit } = target;
|
|
1716
|
+
async function openEmbeddedPool(target, poolOptions = {}, assumeEngineVersion, injectedModule) {
|
|
1717
|
+
const mod = injectedModule ?? (await loadPowdbEmbedded());
|
|
1718
|
+
const { embedded: dir, syncMode, memoryLimit, readonly } = target;
|
|
1719
|
+
// A read-only engine never writes, so a durability selector is meaningless
|
|
1720
|
+
// there, reject the combination loudly rather than silently ignoring one.
|
|
1721
|
+
if (readonly && syncMode !== undefined) {
|
|
1722
|
+
throw new errors_js_1.ValidationError('[turbine] embedded `syncMode` is meaningless with `readonly: true` (a read-only database never writes). Remove one.');
|
|
1723
|
+
}
|
|
1619
1724
|
let db;
|
|
1620
1725
|
try {
|
|
1621
|
-
if (
|
|
1726
|
+
if (readonly) {
|
|
1727
|
+
// Read-only snapshot serving (addon ≥ 0.14): route to the openReadOnly*
|
|
1728
|
+
// constructors; feature-detect and fail with a clear version hint if the
|
|
1729
|
+
// installed addon predates them.
|
|
1730
|
+
if (memoryLimit !== undefined) {
|
|
1731
|
+
if (typeof mod.Database.openReadOnlyWithMemoryLimit !== 'function') {
|
|
1732
|
+
throw new errors_js_1.ConnectionError('[turbine] embedded `readonly` + `memoryLimit` requires @zvndev/powdb-embedded >= 0.14 (openReadOnlyWithMemoryLimit).');
|
|
1733
|
+
}
|
|
1734
|
+
db = mod.Database.openReadOnlyWithMemoryLimit(dir, memoryLimit);
|
|
1735
|
+
}
|
|
1736
|
+
else {
|
|
1737
|
+
if (typeof mod.Database.openReadOnly !== 'function') {
|
|
1738
|
+
throw new errors_js_1.ConnectionError('[turbine] embedded `readonly: true` requires @zvndev/powdb-embedded >= 0.14 (the installed addon has no openReadOnly).');
|
|
1739
|
+
}
|
|
1740
|
+
db = mod.Database.openReadOnly(dir);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
else if (memoryLimit !== undefined) {
|
|
1622
1744
|
if (typeof mod.Database.openWithMemoryLimit !== 'function') {
|
|
1623
1745
|
throw new errors_js_1.ConnectionError('[turbine] embedded `memoryLimit` requires @zvndev/powdb-embedded ≥ 0.7.1.');
|
|
1624
1746
|
}
|
|
@@ -1639,11 +1761,19 @@ async function openEmbeddedPool(target, poolOptions = {}, assumeEngineVersion) {
|
|
|
1639
1761
|
}
|
|
1640
1762
|
db.setSyncMode(syncMode);
|
|
1641
1763
|
}
|
|
1642
|
-
//
|
|
1764
|
+
// Native typed wire is feature-detected on the OPENED handle: an addon ≥ 0.14
|
|
1765
|
+
// exposes `queryWithParams`, so nativeRaw turns on (server-gate ≥ 0.13 still
|
|
1766
|
+
// applies via the version); an older addon has no such method → false.
|
|
1643
1767
|
const capabilities = capabilitiesFromVersion(assumeEngineVersion ?? resolveEmbeddedVersion(), {
|
|
1644
|
-
hasNativeRaw:
|
|
1768
|
+
hasNativeRaw: typeof db.queryWithParams === 'function',
|
|
1769
|
+
});
|
|
1770
|
+
// A read-only target forces the pool's readonly flag; otherwise honor whatever
|
|
1771
|
+
// `poolOptions` (threaded from `options.readonly`) carried.
|
|
1772
|
+
return new PowdbEmbeddedPool(db, {
|
|
1773
|
+
...poolOptions,
|
|
1774
|
+
capabilities,
|
|
1775
|
+
readonly: Boolean(readonly) || Boolean(poolOptions.readonly),
|
|
1645
1776
|
});
|
|
1646
|
-
return new PowdbEmbeddedPool(db, { ...poolOptions, capabilities });
|
|
1647
1777
|
}
|
|
1648
1778
|
/**
|
|
1649
1779
|
* Bind Turbine to PowDB. `target` is one of:
|
|
@@ -1669,6 +1799,7 @@ async function turbinePowDB(target, schema, options = {}) {
|
|
|
1669
1799
|
const poolOptions = {
|
|
1670
1800
|
transactionQueueTimeoutMs: options.transactionQueueTimeoutMs,
|
|
1671
1801
|
retryStaleReads: options.retryStaleReads,
|
|
1802
|
+
readonly: options.readonly,
|
|
1672
1803
|
};
|
|
1673
1804
|
const max = options.connectionLimit ?? 10;
|
|
1674
1805
|
if (typeof target === 'string') {
|
|
@@ -1683,7 +1814,7 @@ async function turbinePowDB(target, schema, options = {}) {
|
|
|
1683
1814
|
pool = target;
|
|
1684
1815
|
}
|
|
1685
1816
|
else if (isEmbeddedTarget(target)) {
|
|
1686
|
-
pool = await openEmbeddedPool(target, poolOptions, options.assumeEngineVersion);
|
|
1817
|
+
pool = await openEmbeddedPool(target, poolOptions, options.assumeEngineVersion, options.powdbEmbeddedModule);
|
|
1687
1818
|
owns = true;
|
|
1688
1819
|
}
|
|
1689
1820
|
else if (isPowdbClientPool(target)) {
|
|
@@ -1710,6 +1841,7 @@ async function turbinePowDB(target, schema, options = {}) {
|
|
|
1710
1841
|
logging: options.logging,
|
|
1711
1842
|
defaultLimit: options.defaultLimit,
|
|
1712
1843
|
warnOnUnlimited: options.warnOnUnlimited,
|
|
1844
|
+
relationLoadStrategy: options.relationLoadStrategy,
|
|
1713
1845
|
queryInterfaceFactory,
|
|
1714
1846
|
}, schema);
|
|
1715
1847
|
if (owns) {
|