turbine-orm 0.34.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -16
- package/dist/cjs/cli/index.js +109 -16
- package/dist/cjs/cli/migrate.js +78 -3
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.js +333 -22
- package/dist/cjs/cli/ui.js +7 -1
- package/dist/cjs/client.js +26 -4
- package/dist/cjs/dialect.js +2 -1
- package/dist/cjs/errors.js +41 -1
- package/dist/cjs/generate.js +23 -2
- package/dist/cjs/index.js +4 -2
- package/dist/cjs/mssql.js +27 -5
- package/dist/cjs/mysql.js +4 -0
- package/dist/cjs/powdb.js +197 -25
- package/dist/cjs/powql.js +515 -51
- package/dist/cjs/query/aggregates.js +683 -0
- package/dist/cjs/query/batched-loader.js +2 -0
- package/dist/cjs/query/builder.js +361 -4508
- package/dist/cjs/query/filters.js +12 -0
- package/dist/cjs/query/relations.js +1698 -0
- package/dist/cjs/query/where-compile.js +180 -0
- package/dist/cjs/query/where.js +1491 -0
- package/dist/cjs/query/writes.js +680 -0
- package/dist/cjs/schema-builder.js +6 -0
- package/dist/cjs/schema-metadata.js +4 -0
- package/dist/cjs/schema-sql.js +265 -3
- package/dist/cjs/sqlite.js +4 -1
- package/dist/cli/index.d.ts +8 -2
- package/dist/cli/index.js +111 -18
- package/dist/cli/migrate.d.ts +24 -1
- package/dist/cli/migrate.js +77 -3
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +46 -13
- package/dist/cli/studio.js +331 -23
- package/dist/cli/ui.js +7 -1
- package/dist/client.d.ts +32 -5
- package/dist/client.js +26 -4
- package/dist/dialect.d.ts +28 -6
- package/dist/dialect.js +2 -1
- package/dist/errors.d.ts +36 -0
- package/dist/errors.js +39 -0
- package/dist/generate.js +23 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/mssql.js +27 -5
- package/dist/mysql.js +4 -0
- package/dist/powdb.d.ts +135 -9
- package/dist/powdb.js +197 -25
- package/dist/powql.d.ts +166 -4
- package/dist/powql.js +516 -52
- package/dist/query/aggregates.d.ts +74 -0
- package/dist/query/aggregates.js +641 -0
- package/dist/query/batched-loader.d.ts +6 -0
- package/dist/query/batched-loader.js +2 -0
- package/dist/query/builder.d.ts +98 -830
- package/dist/query/builder.js +366 -4513
- package/dist/query/deferred.d.ts +13 -2
- package/dist/query/filters.d.ts +7 -0
- package/dist/query/filters.js +11 -0
- package/dist/query/relations.d.ts +441 -0
- package/dist/query/relations.js +1627 -0
- package/dist/query/types.d.ts +25 -6
- package/dist/query/where-compile.d.ts +139 -0
- package/dist/query/where-compile.js +175 -0
- package/dist/query/where.d.ts +494 -0
- package/dist/query/where.js +1431 -0
- package/dist/query/writes.d.ts +131 -0
- package/dist/query/writes.js +626 -0
- package/dist/schema-builder.d.ts +18 -3
- package/dist/schema-builder.js +6 -0
- package/dist/schema-metadata.js +4 -0
- package/dist/schema-sql.d.ts +60 -3
- package/dist/schema-sql.js +261 -4
- package/dist/schema.d.ts +10 -0
- package/dist/sqlite.js +4 -1
- package/package.json +4 -4
package/dist/cjs/generate.js
CHANGED
|
@@ -168,8 +168,13 @@ function generateTypes(schema, options) {
|
|
|
168
168
|
for (const col of table.columns) {
|
|
169
169
|
const pkNote = table.primaryKey.includes(col.name) ? ' (primary key)' : '';
|
|
170
170
|
const nullNote = col.nullable ? ' (nullable)' : '';
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
// PII columns are excluded from default projections, so the field is
|
|
172
|
+
// absent unless the query names it in `select` or passes `includePii`.
|
|
173
|
+
// The emitted type marks it optional so it tells the truth about absence.
|
|
174
|
+
const piiNote = col.pii ? ' (PII: absent unless selected or includePii)' : '';
|
|
175
|
+
const optional = col.pii ? '?' : '';
|
|
176
|
+
lines.push(` /** Column: ${col.name}, ${col.pgType}${pkNote}${nullNote}${piiNote} */`);
|
|
177
|
+
lines.push(` ${col.field}${optional}: ${columnTsType(col, schema.enums)};`);
|
|
173
178
|
}
|
|
174
179
|
lines.push('}');
|
|
175
180
|
lines.push('');
|
|
@@ -531,6 +536,17 @@ function generateMetadata(schema, options) {
|
|
|
531
536
|
lines.push(` { name: '${escSQ(idx.name)}', columns: [${idx.columns.map((c) => `'${escSQ(c)}'`).join(', ')}], unique: ${idx.unique}, definition: ${JSON.stringify(idx.definition)} },`);
|
|
532
537
|
}
|
|
533
538
|
lines.push(' ],');
|
|
539
|
+
// checks: introspected named CHECK constraints. Emitted only when present
|
|
540
|
+
// (byte-stable for check-less tables) and sorted by name so `--no-timestamp`
|
|
541
|
+
// output is deterministic regardless of catalog row order.
|
|
542
|
+
if (table.checks && table.checks.length > 0) {
|
|
543
|
+
const sortedChecks = [...table.checks].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
544
|
+
lines.push(' checks: [');
|
|
545
|
+
for (const chk of sortedChecks) {
|
|
546
|
+
lines.push(` { name: '${escSQ(chk.name)}', expression: ${JSON.stringify(chk.expression)} },`);
|
|
547
|
+
}
|
|
548
|
+
lines.push(' ],');
|
|
549
|
+
}
|
|
534
550
|
// isView — read-only marker; the runtime write guard reads it.
|
|
535
551
|
if (table.isView)
|
|
536
552
|
lines.push(' isView: true,');
|
|
@@ -734,6 +750,11 @@ function serializeColumn(col) {
|
|
|
734
750
|
if (col.generationExpression !== undefined) {
|
|
735
751
|
parts.push(`generationExpression: '${escSQ(col.generationExpression)}'`);
|
|
736
752
|
}
|
|
753
|
+
// PII marker: emitted only when set, so untagged schemas stay byte-identical.
|
|
754
|
+
// Introspection never sets this (code-first declaration), but a metadata
|
|
755
|
+
// object built from `defineSchema` (pii: true) carries it through codegen.
|
|
756
|
+
if (col.pii)
|
|
757
|
+
parts.push(`pii: true`);
|
|
737
758
|
if (col.maxLength !== undefined)
|
|
738
759
|
parts.push(`maxLength: ${col.maxLength}`);
|
|
739
760
|
return `{ ${parts.join(', ')} }`;
|
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.DestructivePushRefusal = 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; } });
|
|
@@ -118,6 +119,7 @@ var schema_metadata_js_1 = require("./schema-metadata.js");
|
|
|
118
119
|
Object.defineProperty(exports, "schemaDefToMetadata", { enumerable: true, get: function () { return schema_metadata_js_1.schemaDefToMetadata; } });
|
|
119
120
|
// Schema SQL — generate DDL, diff, and push
|
|
120
121
|
var schema_sql_js_1 = require("./schema-sql.js");
|
|
122
|
+
Object.defineProperty(exports, "DestructivePushRefusal", { enumerable: true, get: function () { return schema_sql_js_1.DestructivePushRefusal; } });
|
|
121
123
|
Object.defineProperty(exports, "schemaDiff", { enumerable: true, get: function () { return schema_sql_js_1.schemaDiff; } });
|
|
122
124
|
Object.defineProperty(exports, "schemaPush", { enumerable: true, get: function () { return schema_sql_js_1.schemaPush; } });
|
|
123
125
|
Object.defineProperty(exports, "schemaToSQL", { enumerable: true, get: function () { return schema_sql_js_1.schemaToSQL; } });
|
package/dist/cjs/mssql.js
CHANGED
|
@@ -459,6 +459,23 @@ function mssqlColumnType(type, maxLength) {
|
|
|
459
459
|
// ---------------------------------------------------------------------------
|
|
460
460
|
// mssqlDialect — the full Dialect contract for SQL Server 2016+
|
|
461
461
|
// ---------------------------------------------------------------------------
|
|
462
|
+
/**
|
|
463
|
+
* Render the SQL Server `OUTPUT` clause for a write's returning selection.
|
|
464
|
+
* `'*'` → ` OUTPUT INSERTED.*` (byte-identical to the historical default); a
|
|
465
|
+
* quoted column list → ` OUTPUT INSERTED.[c1], INSERTED.[c2]`; each column
|
|
466
|
+
* carries its own `INSERTED.`/`DELETED.` prefix (a bare comma list is invalid
|
|
467
|
+
* T-SQL). Used to exclude PII columns from a write's returned row. Empty
|
|
468
|
+
* selection → no clause.
|
|
469
|
+
*/
|
|
470
|
+
function mssqlOutput(returning, alias) {
|
|
471
|
+
if (!returning)
|
|
472
|
+
return '';
|
|
473
|
+
if (returning === '*')
|
|
474
|
+
return ` OUTPUT ${alias}.*`;
|
|
475
|
+
if (returning.length === 0)
|
|
476
|
+
return '';
|
|
477
|
+
return ` OUTPUT ${returning.map((col) => `${alias}.${col}`).join(', ')}`;
|
|
478
|
+
}
|
|
462
479
|
/**
|
|
463
480
|
* SQL Server 2016+ implementation of the {@link Dialect} contract. Bracket
|
|
464
481
|
* identifier quoting (`[…]`), named `@pN` placeholders, the `FOR JSON PATH`
|
|
@@ -483,6 +500,11 @@ exports.mssqlDialect = {
|
|
|
483
500
|
supportsLateralJoin: false,
|
|
484
501
|
// sp_getapplock / sp_releaseapplock exist (used by a future migrate adapter).
|
|
485
502
|
supportsAdvisoryLock: true,
|
|
503
|
+
// No in-band EXPLAIN: SQL Server's SHOWPLAN is a session toggle
|
|
504
|
+
// (SET SHOWPLAN_ALL ON), not a statement prefix, so a compiled query cannot
|
|
505
|
+
// be explained in one round-trip. Override the inherited Postgres `EXPLAIN`
|
|
506
|
+
// to absent → QueryInterface.explain() throws E017.
|
|
507
|
+
explainQuery: undefined,
|
|
486
508
|
// FOR JSON over zero rows is NULL → coalesced in the relation override.
|
|
487
509
|
aggSupportsInlineOrderBy: false,
|
|
488
510
|
jsonPathSupport: 'limited',
|
|
@@ -517,7 +539,7 @@ exports.mssqlDialect = {
|
|
|
517
539
|
return '';
|
|
518
540
|
},
|
|
519
541
|
buildInsertStatement(input) {
|
|
520
|
-
const out = input.returning
|
|
542
|
+
const out = mssqlOutput(input.returning, 'INSERTED');
|
|
521
543
|
return `INSERT INTO ${input.table} (${input.columns.join(', ')})${out} VALUES (${input.valuePlaceholders.join(', ')})`;
|
|
522
544
|
},
|
|
523
545
|
buildBulkInsertStatement(input) {
|
|
@@ -537,7 +559,7 @@ exports.mssqlDialect = {
|
|
|
537
559
|
const placeholders = input.rowValues
|
|
538
560
|
.map((row) => `(${row.map(() => this.paramPlaceholder(++n)).join(', ')})`)
|
|
539
561
|
.join(', ');
|
|
540
|
-
const out = input.returning
|
|
562
|
+
const out = mssqlOutput(input.returning, 'INSERTED');
|
|
541
563
|
// skipDuplicates has no single-statement equivalent here; ignored (documented).
|
|
542
564
|
return {
|
|
543
565
|
sql: `INSERT INTO ${input.table} (${input.columns.join(', ')})${out} VALUES ${placeholders}`,
|
|
@@ -552,7 +574,7 @@ exports.mssqlDialect = {
|
|
|
552
574
|
const on = input.conflictColumns.map((c) => `T.${c} = S.${c}`).join(' AND ');
|
|
553
575
|
const insertCols = input.insertColumns.join(', ');
|
|
554
576
|
const sourceVals = input.insertColumns.map((c) => `S.${c}`).join(', ');
|
|
555
|
-
const out = input.returning
|
|
577
|
+
const out = mssqlOutput(input.returning, 'INSERTED');
|
|
556
578
|
return (`MERGE INTO ${input.table} AS T ` +
|
|
557
579
|
`USING (VALUES (${input.valuePlaceholders.join(', ')})) AS S (${insertCols}) ` +
|
|
558
580
|
`ON (${on}) ` +
|
|
@@ -563,11 +585,11 @@ exports.mssqlDialect = {
|
|
|
563
585
|
// UPDATE/DELETE inject OUTPUT mid-statement (between SET and WHERE / FROM and
|
|
564
586
|
// WHERE) — a trailing clause would be invalid T-SQL.
|
|
565
587
|
buildUpdateStatement(input) {
|
|
566
|
-
const out = input.returning
|
|
588
|
+
const out = mssqlOutput(input.returning, 'INSERTED');
|
|
567
589
|
return `UPDATE ${input.table} SET ${input.setClauses.join(', ')}${out}${input.whereSql}`;
|
|
568
590
|
},
|
|
569
591
|
buildDeleteStatement(input) {
|
|
570
|
-
const out = input.returning
|
|
592
|
+
const out = mssqlOutput(input.returning, 'DELETED');
|
|
571
593
|
return `DELETE FROM ${input.table}${out}${input.whereSql}`;
|
|
572
594
|
},
|
|
573
595
|
// SQL Server has no LIMIT — emit OFFSET/FETCH, injecting a stable ORDER BY when
|
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
|
@@ -90,7 +90,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
90
90
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
91
91
|
};
|
|
92
92
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
93
|
-
exports.PowqlInterface = exports.introspectPowdbDatabase = exports.PowdbEmbeddedPool = exports.PowdbPool = exports.DEFAULT_TX_QUEUE_TIMEOUT_MS = exports.POWQL_KEYWORDS = exports.ALL_POWDB_CAPABILITIES = exports.MIN_POWDB_VERSION = exports.PowdbJsonParam = exports.PowdbFloatParam = exports.powdbDialect = void 0;
|
|
93
|
+
exports.PowqlInterface = exports.introspectPowdbDatabase = exports.PowdbEmbeddedPool = exports.POWQL_LEXER_TESTED_CEILING = exports.PowdbPool = exports.DEFAULT_TX_QUEUE_TIMEOUT_MS = exports.POWQL_KEYWORDS = exports.ALL_POWDB_CAPABILITIES = exports.MIN_POWDB_VERSION = exports.PowdbJsonParam = exports.PowdbFloatParam = exports.powdbDialect = void 0;
|
|
94
94
|
exports.parsePowdbUrl = parsePowdbUrl;
|
|
95
95
|
exports.assertSupportedPowdbVersion = assertSupportedPowdbVersion;
|
|
96
96
|
exports.capabilitiesFromVersion = capabilitiesFromVersion;
|
|
@@ -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
|
|
@@ -1369,6 +1430,26 @@ function encodePowqlLiteral(value) {
|
|
|
1369
1430
|
return encodePowqlString(value);
|
|
1370
1431
|
throw new errors_js_1.ValidationError(`[turbine] Value of type ${typeof value} cannot be encoded as a PowDB literal.`);
|
|
1371
1432
|
}
|
|
1433
|
+
/**
|
|
1434
|
+
* The newest PowDB engine LINE (major.minor) whose lexer escape handling
|
|
1435
|
+
* {@link encodePowqlString} is VERIFIED against by reading
|
|
1436
|
+
* `crates/query/src/lexer.rs`. The legacy materialize path
|
|
1437
|
+
* ({@link materializePowql}) inlines encoded string literals directly into query
|
|
1438
|
+
* text, so it is only injection-safe while the lexer recognizes exactly the
|
|
1439
|
+
* escape set the escaper emits (`\"`, `\\`, `\n`, `\t`, everything else raw). If
|
|
1440
|
+
* a future engine line teaches the lexer new escapes (e.g. `\u`, `\x`), a string
|
|
1441
|
+
* that the escaper leaves raw could be re-interpreted by the lexer and break out
|
|
1442
|
+
* of the literal, turning the fallback into an injection primitive.
|
|
1443
|
+
*
|
|
1444
|
+
* CONTRACT: bump this ceiling ONLY after re-verifying the escape handling in
|
|
1445
|
+
* `crates/query/src/lexer.rs` for the newer line AND confirming
|
|
1446
|
+
* {@link encodePowqlString} still escapes every breakout vector the lexer
|
|
1447
|
+
* recognizes. The legacy path guards on this value (see
|
|
1448
|
+
* {@link PowdbEmbeddedPool.exec}): an embedded addon whose engine line exceeds
|
|
1449
|
+
* the ceiling yet still routes through the string wire is refused rather than
|
|
1450
|
+
* materialized.
|
|
1451
|
+
*/
|
|
1452
|
+
exports.POWQL_LEXER_TESTED_CEILING = '0.15';
|
|
1372
1453
|
/** Escape a string into a PowQL `"…"` literal, matching the engine lexer's escape rules. */
|
|
1373
1454
|
function encodePowqlString(s) {
|
|
1374
1455
|
let out = '"';
|
|
@@ -1405,10 +1486,14 @@ function materializePowql(powql, params) {
|
|
|
1405
1486
|
}
|
|
1406
1487
|
/**
|
|
1407
1488
|
* A {@link PgCompatPool} backed by an in-process `@zvndev/powdb-embedded`
|
|
1408
|
-
* `Database`.
|
|
1409
|
-
*
|
|
1410
|
-
*
|
|
1411
|
-
*
|
|
1489
|
+
* `Database`. On the addon's typed native wire (≥ 0.14, when
|
|
1490
|
+
* `capabilities.nativeRaw` is set) this pool binds positional `$N` params via
|
|
1491
|
+
* `queryWithParams` and decodes the typed cells, exactly like the networked
|
|
1492
|
+
* transport. On an older addon (no `queryWithParams`) it falls back to the
|
|
1493
|
+
* legacy string wire, which takes **no params array** (its `query(powql)`
|
|
1494
|
+
* accepts only a string), so each positional `$N` is materialized into a PowQL
|
|
1495
|
+
* literal via {@link materializePowql} before the text is handed to the engine.
|
|
1496
|
+
* One handle, single connection: transaction keywords (`begin`/`commit`/
|
|
1412
1497
|
* `rollback`) are issued serially as ordinary queries.
|
|
1413
1498
|
*/
|
|
1414
1499
|
class PowdbEmbeddedPool {
|
|
@@ -1429,20 +1514,64 @@ class PowdbEmbeddedPool {
|
|
|
1429
1514
|
poolHoldRef = { hold: null };
|
|
1430
1515
|
/**
|
|
1431
1516
|
* Feature capabilities of the embedded engine (resolved from the addon
|
|
1432
|
-
* package version). `nativeRaw` is
|
|
1433
|
-
*
|
|
1517
|
+
* package version). `nativeRaw` is true when the addon is ≥ 0.14 and the
|
|
1518
|
+
* opened handle exposes `queryWithParams` (the typed native wire); an older
|
|
1519
|
+
* addon has no such method, so it stays false and the legacy string wire is
|
|
1520
|
+
* used.
|
|
1434
1521
|
*/
|
|
1435
1522
|
capabilities;
|
|
1436
1523
|
/** Carried for surface uniformity with {@link PowdbPool}; inert on embedded (no protocol_error frames). */
|
|
1437
1524
|
retryStaleReads;
|
|
1525
|
+
/**
|
|
1526
|
+
* True when this pool was opened read-only (an `{ embedded, readonly: true }`
|
|
1527
|
+
* target, or a directly-constructed pool passed `readonly: true`). Read by
|
|
1528
|
+
* {@link PowqlInterface}'s exec seam to fail writes fast with E018 before the
|
|
1529
|
+
* wire; the engine's own refusal (mapped by {@link wrapPowdbError}) is the
|
|
1530
|
+
* backstop for raw / injected paths.
|
|
1531
|
+
*/
|
|
1532
|
+
readonly;
|
|
1438
1533
|
constructor(db, options = {}) {
|
|
1439
1534
|
this.db = db;
|
|
1440
1535
|
this.txGate = new PowdbTxGate(options.transactionQueueTimeoutMs ?? exports.DEFAULT_TX_QUEUE_TIMEOUT_MS);
|
|
1441
1536
|
this.capabilities = options.capabilities ?? exports.ALL_POWDB_CAPABILITIES;
|
|
1442
1537
|
this.retryStaleReads = options.retryStaleReads ?? false;
|
|
1538
|
+
this.readonly = options.readonly ?? false;
|
|
1443
1539
|
}
|
|
1444
|
-
/**
|
|
1540
|
+
/** Run the PowQL on the in-process engine, choosing the native or legacy wire. */
|
|
1445
1541
|
exec(powql, params) {
|
|
1542
|
+
// Native typed wire (addon ≥ 0.14): bind positional params with the SAME
|
|
1543
|
+
// binder the networked transport uses ({@link toPowdbParam} yields exactly
|
|
1544
|
+
// the NativeParam union null|bigint|number|boolean|string) and decode the
|
|
1545
|
+
// typed cells: a genuine str "null" survives, a json-null document stays
|
|
1546
|
+
// distinct from an absent value. Gated on the resolved capability AND a
|
|
1547
|
+
// per-call feature-detect so a heterogeneous injected handle cannot crash.
|
|
1548
|
+
if (this.capabilities.nativeRaw && typeof this.db.queryWithParams === 'function') {
|
|
1549
|
+
const bound = params.map((v) => toPowdbParam(v));
|
|
1550
|
+
return adaptNativeResult(this.db.queryWithParams(powql, bound));
|
|
1551
|
+
}
|
|
1552
|
+
// Legacy string wire (addon < 0.14): the engine takes no params array, so
|
|
1553
|
+
// materialize each `$N` into a PowQL literal. Byte-for-byte unchanged, kept
|
|
1554
|
+
// live and tested as the pre-0.14 fallback.
|
|
1555
|
+
//
|
|
1556
|
+
// Safety assertion (see {@link POWQL_LEXER_TESTED_CEILING}): an addon whose
|
|
1557
|
+
// engine line EXCEEDS the escaper's verified lexer ceiling must never be
|
|
1558
|
+
// materialized: a newer lexer may recognize escapes the escaper leaves raw,
|
|
1559
|
+
// making the inline path an injection primitive. Reachability: addons >= 0.14
|
|
1560
|
+
// expose `queryWithParams` and take the native path above, so the ONLY way to
|
|
1561
|
+
// reach this branch with a newer-than-ceiling engine is the anomalous
|
|
1562
|
+
// newer-addon-without-native case (feature-detect failed). Refuse it rather
|
|
1563
|
+
// than inline-encode against an unverified lexer.
|
|
1564
|
+
const engineSem = parsePowdbSemver(this.capabilities.engineVersion);
|
|
1565
|
+
const ceiling = parsePowdbSemver(exports.POWQL_LEXER_TESTED_CEILING);
|
|
1566
|
+
if (engineSem &&
|
|
1567
|
+
ceiling &&
|
|
1568
|
+
(engineSem.major > ceiling.major || (engineSem.major === ceiling.major && engineSem.minor > ceiling.minor))) {
|
|
1569
|
+
throw new errors_js_1.ValidationError(`[turbine] Refusing the PowDB legacy string wire: this embedded addon reports engine ` +
|
|
1570
|
+
`${this.capabilities.engineVersion}, which is newer than the escaper's verified lexer range ` +
|
|
1571
|
+
`(<= ${exports.POWQL_LEXER_TESTED_CEILING}) AND such an addon exposes the parameterized native API, so ` +
|
|
1572
|
+
`reaching the legacy materialize path means the queryWithParams feature-detect failed. Upgrade ` +
|
|
1573
|
+
`turbine-orm so its PowQL escaper is verified against this engine line before running on it.`);
|
|
1574
|
+
}
|
|
1446
1575
|
const materialized = materializePowql(powql, params);
|
|
1447
1576
|
return adaptResult(normalizeEmbeddedResult(this.db.query(materialized)));
|
|
1448
1577
|
}
|
|
@@ -1462,6 +1591,15 @@ class PowdbEmbeddedPool {
|
|
|
1462
1591
|
// transaction callback throws re-entrant E017 fast; independent
|
|
1463
1592
|
// concurrent ones wait their FIFO turn.
|
|
1464
1593
|
holdRef.hold = await this.txGate.acquire();
|
|
1594
|
+
// The gate may have handed us the slot AFTER disconnect() closed the
|
|
1595
|
+
// handle (a transaction queued behind an in-flight one, released as the
|
|
1596
|
+
// pool shut down). Re-check before touching the now-closed engine, and
|
|
1597
|
+
// release the slot we just took so the queue keeps draining.
|
|
1598
|
+
if (this.closed) {
|
|
1599
|
+
holdRef.hold.finish();
|
|
1600
|
+
holdRef.hold = null;
|
|
1601
|
+
throw new errors_js_1.ConnectionError('[turbine] The PowDB embedded pool is closed: disconnect() was already called on this client.');
|
|
1602
|
+
}
|
|
1465
1603
|
}
|
|
1466
1604
|
if ((ctl === 'commit' || ctl === 'rollback') && holdRef.hold === null) {
|
|
1467
1605
|
// This context never acquired the gate — its `begin` never ran (the
|
|
@@ -1537,13 +1675,15 @@ class PowdbEmbeddedPool {
|
|
|
1537
1675
|
async end() {
|
|
1538
1676
|
if (this.closed)
|
|
1539
1677
|
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
1678
|
this.closed = true;
|
|
1679
|
+
// Addon ≥ 0.14 exposes an explicit checkpoint-flushing close(): call it so
|
|
1680
|
+
// the final WAL flush completes deterministically before the handle is
|
|
1681
|
+
// dropped. An older addon has no close, dropping the reference and letting
|
|
1682
|
+
// GC / the engine's checkpoint flush is the fallback (durability is then
|
|
1683
|
+
// checkpoint-bound, so a short script must hold the process open long enough
|
|
1684
|
+
// for the final flush). Marking the pool closed makes later queries fail
|
|
1685
|
+
// with a typed ConnectionError instead of running against a gone handle.
|
|
1686
|
+
this.db.close?.();
|
|
1547
1687
|
}
|
|
1548
1688
|
}
|
|
1549
1689
|
exports.PowdbEmbeddedPool = PowdbEmbeddedPool;
|
|
@@ -1613,12 +1753,34 @@ function resolveEmbeddedVersion() {
|
|
|
1613
1753
|
return optional_peer_import_cjs_1.default.peerPackageVersion('@zvndev/powdb-embedded');
|
|
1614
1754
|
}
|
|
1615
1755
|
/** 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;
|
|
1756
|
+
async function openEmbeddedPool(target, poolOptions = {}, assumeEngineVersion, injectedModule) {
|
|
1757
|
+
const mod = injectedModule ?? (await loadPowdbEmbedded());
|
|
1758
|
+
const { embedded: dir, syncMode, memoryLimit, readonly } = target;
|
|
1759
|
+
// A read-only engine never writes, so a durability selector is meaningless
|
|
1760
|
+
// there, reject the combination loudly rather than silently ignoring one.
|
|
1761
|
+
if (readonly && syncMode !== undefined) {
|
|
1762
|
+
throw new errors_js_1.ValidationError('[turbine] embedded `syncMode` is meaningless with `readonly: true` (a read-only database never writes). Remove one.');
|
|
1763
|
+
}
|
|
1619
1764
|
let db;
|
|
1620
1765
|
try {
|
|
1621
|
-
if (
|
|
1766
|
+
if (readonly) {
|
|
1767
|
+
// Read-only snapshot serving (addon ≥ 0.14): route to the openReadOnly*
|
|
1768
|
+
// constructors; feature-detect and fail with a clear version hint if the
|
|
1769
|
+
// installed addon predates them.
|
|
1770
|
+
if (memoryLimit !== undefined) {
|
|
1771
|
+
if (typeof mod.Database.openReadOnlyWithMemoryLimit !== 'function') {
|
|
1772
|
+
throw new errors_js_1.ConnectionError('[turbine] embedded `readonly` + `memoryLimit` requires @zvndev/powdb-embedded >= 0.14 (openReadOnlyWithMemoryLimit).');
|
|
1773
|
+
}
|
|
1774
|
+
db = mod.Database.openReadOnlyWithMemoryLimit(dir, memoryLimit);
|
|
1775
|
+
}
|
|
1776
|
+
else {
|
|
1777
|
+
if (typeof mod.Database.openReadOnly !== 'function') {
|
|
1778
|
+
throw new errors_js_1.ConnectionError('[turbine] embedded `readonly: true` requires @zvndev/powdb-embedded >= 0.14 (the installed addon has no openReadOnly).');
|
|
1779
|
+
}
|
|
1780
|
+
db = mod.Database.openReadOnly(dir);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
else if (memoryLimit !== undefined) {
|
|
1622
1784
|
if (typeof mod.Database.openWithMemoryLimit !== 'function') {
|
|
1623
1785
|
throw new errors_js_1.ConnectionError('[turbine] embedded `memoryLimit` requires @zvndev/powdb-embedded ≥ 0.7.1.');
|
|
1624
1786
|
}
|
|
@@ -1639,11 +1801,19 @@ async function openEmbeddedPool(target, poolOptions = {}, assumeEngineVersion) {
|
|
|
1639
1801
|
}
|
|
1640
1802
|
db.setSyncMode(syncMode);
|
|
1641
1803
|
}
|
|
1642
|
-
//
|
|
1804
|
+
// Native typed wire is feature-detected on the OPENED handle: an addon ≥ 0.14
|
|
1805
|
+
// exposes `queryWithParams`, so nativeRaw turns on (server-gate ≥ 0.13 still
|
|
1806
|
+
// applies via the version); an older addon has no such method → false.
|
|
1643
1807
|
const capabilities = capabilitiesFromVersion(assumeEngineVersion ?? resolveEmbeddedVersion(), {
|
|
1644
|
-
hasNativeRaw:
|
|
1808
|
+
hasNativeRaw: typeof db.queryWithParams === 'function',
|
|
1809
|
+
});
|
|
1810
|
+
// A read-only target forces the pool's readonly flag; otherwise honor whatever
|
|
1811
|
+
// `poolOptions` (threaded from `options.readonly`) carried.
|
|
1812
|
+
return new PowdbEmbeddedPool(db, {
|
|
1813
|
+
...poolOptions,
|
|
1814
|
+
capabilities,
|
|
1815
|
+
readonly: Boolean(readonly) || Boolean(poolOptions.readonly),
|
|
1645
1816
|
});
|
|
1646
|
-
return new PowdbEmbeddedPool(db, { ...poolOptions, capabilities });
|
|
1647
1817
|
}
|
|
1648
1818
|
/**
|
|
1649
1819
|
* Bind Turbine to PowDB. `target` is one of:
|
|
@@ -1669,6 +1839,7 @@ async function turbinePowDB(target, schema, options = {}) {
|
|
|
1669
1839
|
const poolOptions = {
|
|
1670
1840
|
transactionQueueTimeoutMs: options.transactionQueueTimeoutMs,
|
|
1671
1841
|
retryStaleReads: options.retryStaleReads,
|
|
1842
|
+
readonly: options.readonly,
|
|
1672
1843
|
};
|
|
1673
1844
|
const max = options.connectionLimit ?? 10;
|
|
1674
1845
|
if (typeof target === 'string') {
|
|
@@ -1683,7 +1854,7 @@ async function turbinePowDB(target, schema, options = {}) {
|
|
|
1683
1854
|
pool = target;
|
|
1684
1855
|
}
|
|
1685
1856
|
else if (isEmbeddedTarget(target)) {
|
|
1686
|
-
pool = await openEmbeddedPool(target, poolOptions, options.assumeEngineVersion);
|
|
1857
|
+
pool = await openEmbeddedPool(target, poolOptions, options.assumeEngineVersion, options.powdbEmbeddedModule);
|
|
1687
1858
|
owns = true;
|
|
1688
1859
|
}
|
|
1689
1860
|
else if (isPowdbClientPool(target)) {
|
|
@@ -1710,6 +1881,7 @@ async function turbinePowDB(target, schema, options = {}) {
|
|
|
1710
1881
|
logging: options.logging,
|
|
1711
1882
|
defaultLimit: options.defaultLimit,
|
|
1712
1883
|
warnOnUnlimited: options.warnOnUnlimited,
|
|
1884
|
+
relationLoadStrategy: options.relationLoadStrategy,
|
|
1713
1885
|
queryInterfaceFactory,
|
|
1714
1886
|
}, schema);
|
|
1715
1887
|
if (owns) {
|