turbine-orm 0.33.0 → 0.34.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 +2 -2
- package/dist/cjs/index-advisor.js +0 -0
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/optional-peer-import.cjs +28 -0
- package/dist/cjs/powdb-introspect.js +222 -0
- package/dist/cjs/powdb.js +443 -55
- package/dist/cjs/powql.js +566 -111
- package/dist/cjs/query/builder.js +12 -1
- package/dist/cjs/schema-builder.js +16 -0
- package/dist/cjs/schema-metadata.js +81 -10
- package/dist/index-advisor.d.ts +15 -1
- package/dist/index-advisor.js +0 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/optional-peer-import.cjs +28 -0
- package/dist/optional-peer-import.d.cts +19 -0
- package/dist/powdb-introspect.d.ts +84 -0
- package/dist/powdb-introspect.js +219 -0
- package/dist/powdb.d.ts +249 -13
- package/dist/powdb.js +435 -54
- package/dist/powql.d.ts +113 -6
- package/dist/powql.js +568 -113
- package/dist/query/builder.js +12 -1
- package/dist/query/types.d.ts +39 -6
- package/dist/schema-builder.d.ts +46 -1
- package/dist/schema-builder.js +15 -0
- package/dist/schema-metadata.d.ts +13 -7
- package/dist/schema-metadata.js +82 -11
- package/dist/schema.d.ts +25 -0
- package/package.json +3 -3
package/dist/cjs/powdb.js
CHANGED
|
@@ -90,15 +90,20 @@ 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.PowdbEmbeddedPool = exports.PowdbPool = exports.DEFAULT_TX_QUEUE_TIMEOUT_MS = exports.POWQL_KEYWORDS = exports.MIN_POWDB_VERSION = exports.PowdbFloatParam = exports.powdbDialect = void 0;
|
|
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;
|
|
94
94
|
exports.parsePowdbUrl = parsePowdbUrl;
|
|
95
95
|
exports.assertSupportedPowdbVersion = assertSupportedPowdbVersion;
|
|
96
|
+
exports.capabilitiesFromVersion = capabilitiesFromVersion;
|
|
97
|
+
exports.requireCapability = requireCapability;
|
|
98
|
+
exports.isJsonColumn = isJsonColumn;
|
|
96
99
|
exports.powqlColumnType = powqlColumnType;
|
|
97
100
|
exports.quotePowqlIdent = quotePowqlIdent;
|
|
98
101
|
exports.powqlSchemaDDL = powqlSchemaDDL;
|
|
99
102
|
exports.coerceValue = coerceValue;
|
|
103
|
+
exports.coerceNativeValue = coerceNativeValue;
|
|
100
104
|
exports.rowToEntity = rowToEntity;
|
|
101
105
|
exports.wrapPowdbError = wrapPowdbError;
|
|
106
|
+
exports.isStaleFramePowdbError = isStaleFramePowdbError;
|
|
102
107
|
exports.encodePowqlLiteral = encodePowqlLiteral;
|
|
103
108
|
exports.materializePowql = materializePowql;
|
|
104
109
|
exports.turbinePowDB = turbinePowDB;
|
|
@@ -175,6 +180,23 @@ class PowdbFloatParam {
|
|
|
175
180
|
}
|
|
176
181
|
}
|
|
177
182
|
exports.PowdbFloatParam = PowdbFloatParam;
|
|
183
|
+
/**
|
|
184
|
+
* Marker wrapper for a JS object/array bound to a `json` document column. Both
|
|
185
|
+
* transports serialize `value` with `JSON.stringify` and send the text as a
|
|
186
|
+
* `str` param / string literal, exactly how the PowDB docs insert a json
|
|
187
|
+
* document (the engine validates it as JSON text and stores the canonical
|
|
188
|
+
* binary form). Constructed in {@link PowqlInterface.param} when the target
|
|
189
|
+
* column is `json` and the value is a non-null object/array; a JS string
|
|
190
|
+
* written to a json column passes through RAW (same contract as pg jsonb,
|
|
191
|
+
* pass `'"x"'` to store the JSON string `"x"`), and `null` stays `null`.
|
|
192
|
+
*/
|
|
193
|
+
class PowdbJsonParam {
|
|
194
|
+
value;
|
|
195
|
+
constructor(value) {
|
|
196
|
+
this.value = value;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
exports.PowdbJsonParam = PowdbJsonParam;
|
|
178
200
|
/** Minimum PowDB server version the networked transport requires. */
|
|
179
201
|
exports.MIN_POWDB_VERSION = '0.7.0';
|
|
180
202
|
/**
|
|
@@ -225,19 +247,115 @@ function assertSupportedPowdbVersion(version) {
|
|
|
225
247
|
throw new errors_js_1.ConnectionError(`[turbine] turbine-orm/powdb requires PowDB >= ${exports.MIN_POWDB_VERSION}; the server reports "${version}". ` +
|
|
226
248
|
'Upgrade the PowDB server (0.7.0 added the `returning` keyword and the int->float coercion fix Turbine relies on).');
|
|
227
249
|
}
|
|
250
|
+
/** Minimum engine version each gated feature needs, for the E017 hint text. */
|
|
251
|
+
const POWDB_FEATURE_MIN_VERSION = {
|
|
252
|
+
introspection: '0.10',
|
|
253
|
+
jsonDocs: '0.12',
|
|
254
|
+
docFieldIndexes: '0.13',
|
|
255
|
+
};
|
|
256
|
+
/**
|
|
257
|
+
* Trusted-caller default: every FEATURE gate on, engine version unknown. Used
|
|
258
|
+
* for a directly-constructed {@link PowdbPool} / {@link PowdbEmbeddedPool} that
|
|
259
|
+
* did not go through {@link turbinePowDB}'s version probe (e.g. an injected
|
|
260
|
+
* pool, or a unit-test pool). `nativeRaw` stays OFF here because it flips the
|
|
261
|
+
* actual wire path and must only be enabled after a real server-version probe,
|
|
262
|
+
* never inferred from a bare construction.
|
|
263
|
+
*/
|
|
264
|
+
exports.ALL_POWDB_CAPABILITIES = {
|
|
265
|
+
engineVersion: null,
|
|
266
|
+
jsonDocs: true,
|
|
267
|
+
docFieldIndexes: true,
|
|
268
|
+
introspection: true,
|
|
269
|
+
nativeRaw: false,
|
|
270
|
+
};
|
|
271
|
+
/** Parse a PowDB semver prefix (`0.13.0`, `0.13`, `1.2.3-rc`) into components, or `null`. */
|
|
272
|
+
function parsePowdbSemver(version) {
|
|
273
|
+
const m = /^(\d+)\.(\d+)(?:\.(\d+))?/.exec(String(version ?? '').trim());
|
|
274
|
+
if (!m)
|
|
275
|
+
return null;
|
|
276
|
+
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3] ?? 0) };
|
|
277
|
+
}
|
|
278
|
+
/** Is `sem` at least `major.minor`? */
|
|
279
|
+
function atLeastVersion(sem, major, minor) {
|
|
280
|
+
return sem.major > major || (sem.major === major && sem.minor >= minor);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Derive {@link PowdbCapabilities} from an engine version string. A non-semver /
|
|
284
|
+
* unknown version turns every gate OFF (the E017 hint then tells the caller to
|
|
285
|
+
* upgrade or pass `assumeEngineVersion`). `nativeRaw` requires BOTH the client
|
|
286
|
+
* to expose `queryNativeRaw` (passed in) AND server ≥ 0.13.
|
|
287
|
+
*/
|
|
288
|
+
function capabilitiesFromVersion(version, opts = {}) {
|
|
289
|
+
const sem = parsePowdbSemver(version);
|
|
290
|
+
if (!sem) {
|
|
291
|
+
return {
|
|
292
|
+
engineVersion: version ?? null,
|
|
293
|
+
jsonDocs: false,
|
|
294
|
+
docFieldIndexes: false,
|
|
295
|
+
introspection: false,
|
|
296
|
+
nativeRaw: false,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
engineVersion: `${sem.major}.${sem.minor}.${sem.patch}`,
|
|
301
|
+
introspection: atLeastVersion(sem, 0, 10),
|
|
302
|
+
jsonDocs: atLeastVersion(sem, 0, 12),
|
|
303
|
+
docFieldIndexes: atLeastVersion(sem, 0, 13),
|
|
304
|
+
nativeRaw: Boolean(opts.hasNativeRaw) && atLeastVersion(sem, 0, 13),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Throw a version-hinting {@link UnsupportedFeatureError} (E017) when a gated
|
|
309
|
+
* PowQL feature is used on an engine that does not support it. Keeps old engines
|
|
310
|
+
* getting clean typed errors instead of raw PowQL parse failures.
|
|
311
|
+
*/
|
|
312
|
+
function requireCapability(caps, key, feature) {
|
|
313
|
+
if (caps[key])
|
|
314
|
+
return;
|
|
315
|
+
const min = POWDB_FEATURE_MIN_VERSION[key];
|
|
316
|
+
const reported = caps.engineVersion
|
|
317
|
+
? `this connection reports ${caps.engineVersion}`
|
|
318
|
+
: 'this connection could not report a version';
|
|
319
|
+
throw new errors_js_1.UnsupportedFeatureError(feature, 'PowDB', `${feature} requires PowDB >= ${min}; ${reported}. Upgrade powdb-server / @zvndev/powdb-embedded ` +
|
|
320
|
+
'(or pass `assumeEngineVersion` if the version cannot be detected).');
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Does this column map to PowDB's native `json` document type? A Postgres
|
|
324
|
+
* `json`/`jsonb` type (via `dialectType`/`pgType`) is authoritative; otherwise
|
|
325
|
+
* the tsType heuristic (`Record<…>`, `object`, `unknown`, an object/array
|
|
326
|
+
* literal) that the four scalar branches do not claim. Array columns never map
|
|
327
|
+
* to json, a PowDB array only exists INSIDE a json document, so a Postgres
|
|
328
|
+
* array column has no PowDB shape and still throws in {@link powqlColumnType}.
|
|
329
|
+
*/
|
|
330
|
+
function isJsonColumn(col) {
|
|
331
|
+
if (col.isArray)
|
|
332
|
+
return false;
|
|
333
|
+
const dbType = (col.dialectType ?? col.pgType ?? '').toLowerCase();
|
|
334
|
+
if (dbType === 'json' || dbType === 'jsonb')
|
|
335
|
+
return true;
|
|
336
|
+
const ts = col.tsType.replace(/\s*\|\s*null$/i, '').trim();
|
|
337
|
+
if (ts === 'Date' || ts === 'boolean' || ts === 'number' || ts === 'bigint' || ts === 'string')
|
|
338
|
+
return false;
|
|
339
|
+
if (ts === 'Buffer' || ts === 'Uint8Array')
|
|
340
|
+
return false;
|
|
341
|
+
return /Record<|object|unknown|\[\]|\{/.test(ts);
|
|
342
|
+
}
|
|
228
343
|
/**
|
|
229
344
|
* Map a Turbine column to the PowQL DDL type used in `defineSchema` →
|
|
230
345
|
* `type T { … }`. Turbine never emits PowDB's `uuid`/`datetime`/`bytes` types,
|
|
231
346
|
* which cannot hold client-supplied values on the wire (no literal, no cast):
|
|
232
347
|
* - `Date` → `int` (epoch micros) - `boolean` → `bool`
|
|
233
348
|
* - integral `number`/`bigint` → `int` - fractional `number` → `float`
|
|
349
|
+
* - JSON / object columns → `json` (native PowDB document type, ≥ 0.12)
|
|
234
350
|
* - everything else (incl. UUID/PK strings) → `str`
|
|
235
|
-
* Array
|
|
351
|
+
* Array (non-json) and bytes columns throw, they have no PowDB equivalent.
|
|
236
352
|
*/
|
|
237
353
|
function powqlColumnType(col) {
|
|
238
354
|
if (col.isArray) {
|
|
239
355
|
throw new errors_js_1.ValidationError(`[turbine] Column "${col.name}" is an array — PowDB has no array type. Arrays are unsupported on the PowDB backend.`);
|
|
240
356
|
}
|
|
357
|
+
if (isJsonColumn(col))
|
|
358
|
+
return 'json';
|
|
241
359
|
const ts = col.tsType.replace(/\s*\|\s*null$/i, '').trim();
|
|
242
360
|
if (ts === 'Date')
|
|
243
361
|
return 'int'; // epoch micros
|
|
@@ -252,9 +370,6 @@ function powqlColumnType(col) {
|
|
|
252
370
|
if (ts === 'Buffer' || ts === 'Uint8Array') {
|
|
253
371
|
throw new errors_js_1.ValidationError(`[turbine] Column "${col.name}" is binary — PowDB cannot store client-supplied bytes on the wire. Use a string (e.g. base64) instead.`);
|
|
254
372
|
}
|
|
255
|
-
if (/Record<|object|unknown|\[\]|\{/.test(ts)) {
|
|
256
|
-
throw new errors_js_1.ValidationError(`[turbine] Column "${col.name}" (${col.tsType}) maps to JSON/object, which PowDB has no type for. Flatten it or store a JSON string.`);
|
|
257
|
-
}
|
|
258
373
|
return 'str';
|
|
259
374
|
}
|
|
260
375
|
/** Heuristic: does this numeric column hold fractional values (→ PowQL `float`)? */
|
|
@@ -394,7 +509,8 @@ function quotePowqlIdent(name) {
|
|
|
394
509
|
}
|
|
395
510
|
return exports.POWQL_KEYWORDS.has(name) || !POWQL_BARE_IDENT.test(name) ? `\`${name}\`` : name;
|
|
396
511
|
}
|
|
397
|
-
function powqlSchemaDDL(schema) {
|
|
512
|
+
function powqlSchemaDDL(schema, opts = {}) {
|
|
513
|
+
const caps = opts.capabilities;
|
|
398
514
|
const stmts = [];
|
|
399
515
|
for (const meta of Object.values(schema.tables)) {
|
|
400
516
|
const pkSet = new Set(meta.primaryKey);
|
|
@@ -405,6 +521,12 @@ function powqlSchemaDDL(schema) {
|
|
|
405
521
|
// cannot enforce the tuple's uniqueness at the engine level.
|
|
406
522
|
const pkIsSingle = meta.primaryKey.length === 1;
|
|
407
523
|
const fields = meta.columns.map((col) => {
|
|
524
|
+
const powqlType = powqlColumnType(col);
|
|
525
|
+
// Gate `json` columns behind the engine's jsonDocs capability when a
|
|
526
|
+
// caller supplied one, an old engine has no `json` type and would reject
|
|
527
|
+
// the DDL. Pure-function callers (no opts) emit unconditionally.
|
|
528
|
+
if (powqlType === 'json' && caps)
|
|
529
|
+
requireCapability(caps, 'jsonDocs', 'JSON document columns');
|
|
408
530
|
const mods = [];
|
|
409
531
|
if (!col.nullable || pkSet.has(col.name))
|
|
410
532
|
mods.push('required');
|
|
@@ -413,15 +535,57 @@ function powqlSchemaDDL(schema) {
|
|
|
413
535
|
// `auto` = server-generated monotonic int. PowDB requires it be `int` and
|
|
414
536
|
// rejects it alongside a `default`; non-int generated columns fall back to
|
|
415
537
|
// a plain typed column (Turbine assigns the value client-side instead).
|
|
416
|
-
if (col.isGenerated &&
|
|
538
|
+
if (col.isGenerated && powqlType === 'int')
|
|
417
539
|
mods.push('auto');
|
|
418
|
-
return ` ${mods.join(' ')}${mods.length ? ' ' : ''}${quotePowqlIdent(col.name)}: ${
|
|
540
|
+
return ` ${mods.join(' ')}${mods.length ? ' ' : ''}${quotePowqlIdent(col.name)}: ${powqlType}`;
|
|
419
541
|
});
|
|
420
542
|
stmts.push(`type ${quotePowqlIdent(meta.name)} {\n${fields.join(',\n')}\n}`);
|
|
543
|
+
// Track which single columns already carry a unique constraint (the
|
|
544
|
+
// single-column PK is inlined `required unique` in the type body above) so
|
|
545
|
+
// a redundant `add unique .col` is never emitted twice.
|
|
546
|
+
const emittedUnique = new Set();
|
|
547
|
+
if (pkIsSingle && meta.primaryKey[0] !== undefined)
|
|
548
|
+
emittedUnique.add(meta.primaryKey[0]);
|
|
421
549
|
// Secondary unique constraints (beyond the PK) become unique indexes.
|
|
422
550
|
for (const uniq of meta.uniqueColumns) {
|
|
423
551
|
if (uniq.length === 1 && !pkSet.has(uniq[0])) {
|
|
424
552
|
stmts.push(`alter ${quotePowqlIdent(meta.name)} add unique .${quotePowqlIdent(uniq[0])}`);
|
|
553
|
+
emittedUnique.add(uniq[0]);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// Declared indexes: PowDB doc-field expression indexes (docPath) and plain
|
|
557
|
+
// single-column indexes. A doc-field index MUST be parenthesized (the engine
|
|
558
|
+
// rejects a bare JSON path); string path segments emit lexer-exact via the
|
|
559
|
+
// shared `encodePowqlString`, integer array indexes emit bare. A json
|
|
560
|
+
// document column reference stays dotted-bare (`.col`), which bypasses
|
|
561
|
+
// keyword lookup on every engine version exactly like a filter path.
|
|
562
|
+
for (const idx of meta.indexes) {
|
|
563
|
+
const kind = idx.unique ? 'unique' : 'index';
|
|
564
|
+
if (idx.docPath) {
|
|
565
|
+
if (caps)
|
|
566
|
+
requireCapability(caps, 'docFieldIndexes', 'JSON doc-field expression indexes');
|
|
567
|
+
const column = idx.columns[0];
|
|
568
|
+
if (column === undefined) {
|
|
569
|
+
throw new errors_js_1.ValidationError(`[turbine] Doc-field index "${idx.name}" on ${meta.name} has no target json column.`);
|
|
570
|
+
}
|
|
571
|
+
const segs = idx.docPath.map((s) => (typeof s === 'number' ? `->${s}` : `->${encodePowqlString(s)}`)).join('');
|
|
572
|
+
stmts.push(`alter ${quotePowqlIdent(meta.name)} add ${kind} (.${column}${segs})`);
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
// Plain column index. PowDB has no composite index (`add index` takes a
|
|
576
|
+
// single `.column`), so a multi-column entry is a typed E017.
|
|
577
|
+
if (idx.columns.length !== 1) {
|
|
578
|
+
throw new errors_js_1.UnsupportedFeatureError('composite indexes', 'PowDB', `PowDB has no composite index. Index "${idx.name}" on ${meta.name} lists ` +
|
|
579
|
+
`${idx.columns.length} columns; declare a single-column index (or a doc-field index) instead.`);
|
|
580
|
+
}
|
|
581
|
+
const column = idx.columns[0];
|
|
582
|
+
// A unique index whose column already carries a unique constraint (the
|
|
583
|
+
// PK, or a column-level unique) would be a redundant duplicate, so skip it.
|
|
584
|
+
if (idx.unique && emittedUnique.has(column))
|
|
585
|
+
continue;
|
|
586
|
+
stmts.push(`alter ${quotePowqlIdent(meta.name)} add ${kind} .${quotePowqlIdent(column)}`);
|
|
587
|
+
if (idx.unique)
|
|
588
|
+
emittedUnique.add(column);
|
|
425
589
|
}
|
|
426
590
|
}
|
|
427
591
|
}
|
|
@@ -431,6 +595,10 @@ function powqlSchemaDDL(schema) {
|
|
|
431
595
|
function toPowdbParam(value, col) {
|
|
432
596
|
if (value instanceof PowdbFloatParam)
|
|
433
597
|
return value.value; // wire-side: a float column takes the plain number
|
|
598
|
+
// json document: serialize to canonical JSON text and bind as a str param,
|
|
599
|
+
// the engine validates it as JSON and stores the canonical binary form.
|
|
600
|
+
if (value instanceof PowdbJsonParam)
|
|
601
|
+
return JSON.stringify(value.value);
|
|
434
602
|
if (value === undefined || value === null)
|
|
435
603
|
return null;
|
|
436
604
|
if (value instanceof Date)
|
|
@@ -453,10 +621,23 @@ function toPowdbParam(value, col) {
|
|
|
453
621
|
*/
|
|
454
622
|
function coerceValue(raw, col) {
|
|
455
623
|
const ts = col.tsType.replace(/\s*\|\s*null$/i, '').trim();
|
|
624
|
+
const json = isJsonColumn(col);
|
|
456
625
|
// NULL bareword: unambiguous for non-string columns; for `str` we cannot tell a
|
|
457
626
|
// literal "null" from SQL NULL, so a nullable str of value "null" reads as null.
|
|
458
|
-
|
|
627
|
+
// For a `json` column the bareword `null` (a legacy-wire rendering shared by an
|
|
628
|
+
// absent value AND a top-level JSON-null document, documented residual,
|
|
629
|
+
// resolved on the native transport by the WireValue path) maps to null; a JSON
|
|
630
|
+
// string document "null" renders WITH quotes (`"null"`) and parses distinctly.
|
|
631
|
+
if (raw === 'null' && (json || ts !== 'string' || col.nullable))
|
|
459
632
|
return null;
|
|
633
|
+
if (json) {
|
|
634
|
+
try {
|
|
635
|
+
return JSON.parse(raw);
|
|
636
|
+
}
|
|
637
|
+
catch {
|
|
638
|
+
return raw; // defensive: canonical JSON text always parses
|
|
639
|
+
}
|
|
640
|
+
}
|
|
460
641
|
if (ts === 'Date') {
|
|
461
642
|
const micros = Number(raw);
|
|
462
643
|
return Number.isFinite(micros) ? new Date(micros / 1000) : null;
|
|
@@ -473,19 +654,62 @@ function coerceValue(raw, col) {
|
|
|
473
654
|
return raw; // string / uuid-as-string
|
|
474
655
|
}
|
|
475
656
|
/**
|
|
476
|
-
*
|
|
477
|
-
* {@link
|
|
478
|
-
*
|
|
479
|
-
*
|
|
657
|
+
* Coerce a single cell that arrived over the NATIVE typed wire (decoded from a
|
|
658
|
+
* {@link PowdbWireValue}, so already a JS `bigint`/`number`/`boolean`/`string`/
|
|
659
|
+
* `NativeJson`/`Uint8Array`/`null`, never a bare `"null"` string). Unlike
|
|
660
|
+
* {@link coerceValue} this NEVER collapses the string `"null"` to `null`: an
|
|
661
|
+
* absent value already decoded to `null` (from the `empty` cell), so a genuine
|
|
662
|
+
* str `"null"` stays the string `"null"` (fixes the legacy-wire wart on the
|
|
663
|
+
* native transport). `datetime`-shaped cells (int micros) become `Date`; a
|
|
664
|
+
* bigint on a `number` column follows the int8 safe-integer policy.
|
|
665
|
+
*/
|
|
666
|
+
function coerceNativeValue(value, col) {
|
|
667
|
+
if (value === undefined || value === null)
|
|
668
|
+
return null;
|
|
669
|
+
if (isDateColumn(col)) {
|
|
670
|
+
if (typeof value === 'bigint')
|
|
671
|
+
return new Date(Number(value) / 1000);
|
|
672
|
+
if (typeof value === 'number')
|
|
673
|
+
return new Date(value / 1000);
|
|
674
|
+
return value;
|
|
675
|
+
}
|
|
676
|
+
const ts = col.tsType.replace(/\s*\|\s*null$/i, '').trim();
|
|
677
|
+
if (typeof value === 'bigint') {
|
|
678
|
+
if (ts === 'bigint')
|
|
679
|
+
return value;
|
|
680
|
+
if (ts === 'number') {
|
|
681
|
+
const n = Number(value);
|
|
682
|
+
return Number.isSafeInteger(n) ? n : value.toString(); // int8 policy: keep big ints as strings
|
|
683
|
+
}
|
|
684
|
+
return value;
|
|
685
|
+
}
|
|
686
|
+
return value; // number / boolean / string / NativeJson document / Uint8Array
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Map one raw PowDB row into a typed entity (camelCase fields, coerced values).
|
|
690
|
+
* Only the columns present in `raw` are emitted, so partial `select`
|
|
691
|
+
* projections round-trip unchanged. `native` selects the coercion policy: the
|
|
692
|
+
* default `false` handles the legacy string wire (every cell is a string, via
|
|
693
|
+
* {@link coerceValue}); `true` handles the native typed wire, where non-string
|
|
694
|
+
* cells arrive pre-typed and go through {@link coerceNativeValue} (see F3).
|
|
695
|
+
* Callers on the native transport pass `this.pool.capabilities.nativeRaw`.
|
|
480
696
|
*/
|
|
481
|
-
function rowToEntity(raw, meta) {
|
|
697
|
+
function rowToEntity(raw, meta, native = false) {
|
|
482
698
|
const byName = new Map(meta.columns.map((c) => [c.name, c]));
|
|
483
699
|
const out = {};
|
|
484
700
|
for (const snake of Object.keys(raw)) {
|
|
485
701
|
const col = byName.get(snake);
|
|
486
702
|
const field = meta.reverseColumnMap[snake] ?? snake;
|
|
487
703
|
const value = raw[snake];
|
|
488
|
-
|
|
704
|
+
if (!col) {
|
|
705
|
+
out[field] = value;
|
|
706
|
+
}
|
|
707
|
+
else if (native) {
|
|
708
|
+
out[field] = coerceNativeValue(value, col);
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
out[field] = typeof value === 'string' ? coerceValue(value, col) : value;
|
|
712
|
+
}
|
|
489
713
|
}
|
|
490
714
|
return out;
|
|
491
715
|
}
|
|
@@ -511,7 +735,7 @@ function wrapPowdbError(err) {
|
|
|
511
735
|
const e = err;
|
|
512
736
|
const msg = e.message ?? 'unknown PowDB error';
|
|
513
737
|
// Unique-constraint — message-based on both transports.
|
|
514
|
-
if (/unique constraint violation/i.test(msg)) {
|
|
738
|
+
if (/unique (constraint|expression index) violation/i.test(msg)) {
|
|
515
739
|
const m = /on\s+\S+\.(\w+)/i.exec(msg);
|
|
516
740
|
return new errors_js_1.UniqueConstraintError({ constraint: m?.[1], cause: err });
|
|
517
741
|
}
|
|
@@ -524,37 +748,71 @@ function wrapPowdbError(err) {
|
|
|
524
748
|
// Driver pool lifecycle errors (acquire after close, acquire timeout) carry
|
|
525
749
|
// no .code — classify by message so both transports surface E004.
|
|
526
750
|
if (/pool closed|pool acquire timeout/i.test(msg)) {
|
|
527
|
-
return new errors_js_1.ConnectionError(`[turbine] PowDB connection unavailable: ${msg}
|
|
751
|
+
return new errors_js_1.ConnectionError(`[turbine] PowDB connection unavailable: ${msg}`, { cause: err });
|
|
528
752
|
}
|
|
529
753
|
// Server-side transaction-gate wait bound (PowDB ≥ 0.10, default 5s): another
|
|
530
754
|
// connection held the single global write lock past the server's
|
|
531
755
|
// --tx-wait-timeout-ms. Retryable timeout, not a query defect.
|
|
532
756
|
if (/transaction gate timeout/i.test(msg)) {
|
|
533
|
-
return new errors_js_1.TimeoutError(0, 'PowDB transaction gate');
|
|
757
|
+
return new errors_js_1.TimeoutError(0, 'PowDB transaction gate', { cause: err });
|
|
758
|
+
}
|
|
759
|
+
// Stale / violated WIRE state → ConnectionError (E004), NOT a query defect.
|
|
760
|
+
// A `protocol_error`-class failure means the socket's framing state is gone
|
|
761
|
+
// (the client cannot safely reuse it and the pool must destroy it). The
|
|
762
|
+
// canonical trigger is the "received unexpected frame from server" that a
|
|
763
|
+
// fresh request hits after a multi-minute idle gap; sibling shapes are an
|
|
764
|
+
// unknown message type, a truncated payload, or bad framing. Runs BEFORE the
|
|
765
|
+
// validation regex below, whose `unexpected` token would otherwise misclass
|
|
766
|
+
// "received unexpected frame" as an E003 query defect. `.cause` preserved so
|
|
767
|
+
// callers (and the opt-in stale-read retry) can inspect the driver code.
|
|
768
|
+
if (e.code === 'protocol_error' ||
|
|
769
|
+
/received unexpected frame|unknown message type|truncated payload|bad framing/i.test(msg)) {
|
|
770
|
+
return new errors_js_1.ConnectionError(`[turbine] PowDB connection is in an invalid state: ${msg}`, { cause: err });
|
|
534
771
|
}
|
|
535
|
-
// Type mismatch / parse / execution / storage / unexpected
|
|
536
|
-
// (E003). On the embedded transport these are the only
|
|
537
|
-
// (code is always 'GenericFailure'); on the networked path they
|
|
538
|
-
// safety net before the .code switch.
|
|
772
|
+
// Type mismatch / parse / execution / storage / unexpected(token) / row too
|
|
773
|
+
// large → validation (E003). On the embedded transport these are the only
|
|
774
|
+
// signal we get (code is always 'GenericFailure'); on the networked path they
|
|
775
|
+
// are a safety net before the .code switch.
|
|
539
776
|
if (/type mismatch|\bParse\b|\bExecution\b|StorageError|unexpected|row too large/i.test(msg)) {
|
|
540
777
|
return new errors_js_1.ValidationError(`[turbine] PowDB query rejected: ${msg}`);
|
|
541
778
|
}
|
|
542
779
|
switch (e.code) {
|
|
543
780
|
case 'connect_failed':
|
|
544
781
|
case 'closed':
|
|
545
|
-
return new errors_js_1.ConnectionError(`[turbine] PowDB connection failed: ${msg}
|
|
782
|
+
return new errors_js_1.ConnectionError(`[turbine] PowDB connection failed: ${msg}`, { cause: err });
|
|
783
|
+
case 'auth_failed':
|
|
784
|
+
// Connection-establishment class, non-retryable: the handshake was
|
|
785
|
+
// rejected. Surface E004 with a concrete remediation hint instead of
|
|
786
|
+
// letting it fall through to the raw error.
|
|
787
|
+
return new errors_js_1.ConnectionError(`[turbine] PowDB authentication failed: ${msg} (check the user / password / dbName for this connection).`, { cause: err });
|
|
546
788
|
case 'timeout':
|
|
547
789
|
case 'aborted':
|
|
548
|
-
return new errors_js_1.TimeoutError(0, 'PowDB query');
|
|
790
|
+
return new errors_js_1.TimeoutError(0, 'PowDB query', { cause: err });
|
|
549
791
|
case 'query_failed':
|
|
550
792
|
case 'type_coercion_failed':
|
|
551
|
-
case 'protocol_error':
|
|
552
793
|
case 'size_exceeded':
|
|
553
794
|
return new errors_js_1.ValidationError(`[turbine] PowDB query rejected: ${msg}`);
|
|
554
795
|
default:
|
|
555
|
-
return err instanceof Error ? err : new errors_js_1.ConnectionError(`[turbine] PowDB error: ${msg}
|
|
796
|
+
return err instanceof Error ? err : new errors_js_1.ConnectionError(`[turbine] PowDB error: ${msg}`, { cause: err });
|
|
556
797
|
}
|
|
557
798
|
}
|
|
799
|
+
/**
|
|
800
|
+
* True when `err` is the stale-wire-frame {@link ConnectionError} produced by
|
|
801
|
+
* {@link wrapPowdbError} (its `.cause` is a `protocol_error` PowDBError, or the
|
|
802
|
+
* message carries the invalid-state signature). The opt-in read retry
|
|
803
|
+
* (`retryStaleReads`, evaluated in {@link PowqlInterface}'s exec seam) uses this
|
|
804
|
+
* to decide whether a first-statement READ may be replayed once on a fresh
|
|
805
|
+
* connection; writes are NEVER retried (an ambiguous mutation reply is unsafe
|
|
806
|
+
* to replay, matching the client's own native-path policy).
|
|
807
|
+
*/
|
|
808
|
+
function isStaleFramePowdbError(err) {
|
|
809
|
+
if (!(err instanceof errors_js_1.ConnectionError))
|
|
810
|
+
return false;
|
|
811
|
+
const cause = err.cause;
|
|
812
|
+
if (cause && typeof cause === 'object' && cause.code === 'protocol_error')
|
|
813
|
+
return true;
|
|
814
|
+
return /PowDB connection is in an invalid state/.test(err.message);
|
|
815
|
+
}
|
|
558
816
|
function normalizeQueryArgs(arg, values) {
|
|
559
817
|
if (typeof arg === 'string')
|
|
560
818
|
return { text: arg, params: values ?? [] };
|
|
@@ -733,7 +991,7 @@ class PowdbTxGate {
|
|
|
733
991
|
return hold;
|
|
734
992
|
}
|
|
735
993
|
}
|
|
736
|
-
/** Adapt a PowDB result into the pg-compat `{ rows, rowCount, fields }` shape. */
|
|
994
|
+
/** Adapt a PowDB (legacy string wire) result into the pg-compat `{ rows, rowCount, fields }` shape. */
|
|
737
995
|
function adaptResult(r) {
|
|
738
996
|
switch (r.kind) {
|
|
739
997
|
case 'rows': {
|
|
@@ -744,21 +1002,86 @@ function adaptResult(r) {
|
|
|
744
1002
|
});
|
|
745
1003
|
return o;
|
|
746
1004
|
});
|
|
747
|
-
return { rows, rowCount: rows.length, fields: r.columns.map((name) => ({ name, dataTypeID: 0 })) };
|
|
1005
|
+
return { rows, rowCount: rows.length, fields: r.columns.map((name) => ({ name, dataTypeID: 0 })), native: false };
|
|
748
1006
|
}
|
|
749
1007
|
case 'ok':
|
|
750
|
-
return { rows: [], rowCount: Number(r.affected), fields: [] };
|
|
1008
|
+
return { rows: [], rowCount: Number(r.affected), fields: [], native: false };
|
|
751
1009
|
case 'scalar':
|
|
752
|
-
return { rows: [{ value: r.value }], rowCount: 1, fields: [{ name: 'value', dataTypeID: 0 }] };
|
|
1010
|
+
return { rows: [{ value: r.value }], rowCount: 1, fields: [{ name: 'value', dataTypeID: 0 }], native: false };
|
|
753
1011
|
default:
|
|
754
|
-
return { rows: [], rowCount: 0, fields: [] };
|
|
1012
|
+
return { rows: [], rowCount: 0, fields: [], native: false };
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
/** Format 16 raw UUID bytes as a canonical `8-4-4-4-12` hex string. */
|
|
1016
|
+
function uuidBytesToHex(bytes) {
|
|
1017
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
1018
|
+
if (hex.length !== 32)
|
|
1019
|
+
return hex; // defensive: non-16B payloads pass through as raw hex
|
|
1020
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Decode one native {@link PowdbWireValue} cell into a JS value. `empty` →
|
|
1024
|
+
* `null` (an unset value; for a json column this cleanly distinguishes absent
|
|
1025
|
+
* from a JSON-null document, which arrives as `{ type: 'json', value: null }`).
|
|
1026
|
+
* `int`/`datetime` stay `bigint` so the row layer ({@link coerceNativeValue})
|
|
1027
|
+
* applies the int8 policy / Date conversion by column; `uuid` becomes canonical
|
|
1028
|
+
* hex; `bytes` stay `Uint8Array`; `json` passes the decoded document through
|
|
1029
|
+
* with no re-parse (its `pj1` raw bytes are dropped).
|
|
1030
|
+
*/
|
|
1031
|
+
function decodeWireValue(cell) {
|
|
1032
|
+
switch (cell.type) {
|
|
1033
|
+
case 'empty':
|
|
1034
|
+
return null;
|
|
1035
|
+
case 'int':
|
|
1036
|
+
case 'datetime':
|
|
1037
|
+
return cell.value; // bigint; row layer decides Date vs number vs bigint per column
|
|
1038
|
+
case 'float':
|
|
1039
|
+
case 'bool':
|
|
1040
|
+
case 'str':
|
|
1041
|
+
return cell.value;
|
|
1042
|
+
case 'uuid':
|
|
1043
|
+
return uuidBytesToHex(cell.value);
|
|
1044
|
+
case 'bytes':
|
|
1045
|
+
return cell.value; // Uint8Array
|
|
1046
|
+
case 'json':
|
|
1047
|
+
return cell.value; // NativeJson document, already recursive data
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
/** Adapt a native (typed-wire) PowDB result into the pg-compat shape, decoding every {@link PowdbWireValue} cell. */
|
|
1051
|
+
function adaptNativeResult(r) {
|
|
1052
|
+
switch (r.kind) {
|
|
1053
|
+
case 'rows': {
|
|
1054
|
+
const rows = r.rows.map((row) => {
|
|
1055
|
+
const o = {};
|
|
1056
|
+
r.columns.forEach((c, i) => {
|
|
1057
|
+
o[c] = decodeWireValue(row[i] ?? { type: 'empty' });
|
|
1058
|
+
});
|
|
1059
|
+
return o;
|
|
1060
|
+
});
|
|
1061
|
+
return { rows, rowCount: rows.length, fields: r.columns.map((name) => ({ name, dataTypeID: 0 })), native: true };
|
|
1062
|
+
}
|
|
1063
|
+
case 'ok':
|
|
1064
|
+
return { rows: [], rowCount: Number(r.affected), fields: [], native: true };
|
|
1065
|
+
case 'scalar':
|
|
1066
|
+
return {
|
|
1067
|
+
rows: [{ value: decodeWireValue(r.value) }],
|
|
1068
|
+
rowCount: 1,
|
|
1069
|
+
fields: [{ name: 'value', dataTypeID: 0 }],
|
|
1070
|
+
native: true,
|
|
1071
|
+
};
|
|
1072
|
+
default:
|
|
1073
|
+
return { rows: [], rowCount: 0, fields: [], native: true };
|
|
755
1074
|
}
|
|
756
1075
|
}
|
|
757
1076
|
/**
|
|
758
1077
|
* A {@link PgCompatPool} backed by a `@zvndev/powdb-client` `Pool`. The query
|
|
759
|
-
* `text` is **PowQL**, not SQL
|
|
760
|
-
*
|
|
761
|
-
* (
|
|
1078
|
+
* `text` is **PowQL**, not SQL ({@link PowqlInterface} generates it). On the
|
|
1079
|
+
* legacy string wire cells come back as strings; when `capabilities.nativeRaw`
|
|
1080
|
+
* is set (server ≥ 0.13 + a client exposing `queryNativeRaw`) this pool routes
|
|
1081
|
+
* through the typed native wire instead, so cells arrive pre-typed (a json int
|
|
1082
|
+
* as `bigint`, etc.) and each result is tagged with the wire that served it
|
|
1083
|
+
* ({@link PowdbTaggedResult}). Per-column JS coercion still happens in
|
|
1084
|
+
* `PowqlInterface` (it owns the schema metadata), keyed on that per-result tag.
|
|
762
1085
|
*/
|
|
763
1086
|
class PowdbPool {
|
|
764
1087
|
pool;
|
|
@@ -782,10 +1105,29 @@ class PowdbPool {
|
|
|
782
1105
|
* live socket holding the process open until the server's idle timeout.
|
|
783
1106
|
*/
|
|
784
1107
|
checkedOut = new Set();
|
|
1108
|
+
/** Feature capabilities of the bound server (probed version + native-wire feature-detect). */
|
|
1109
|
+
capabilities;
|
|
1110
|
+
/** Opt-in first-statement-read replay on a stale wire frame (read by {@link PowqlInterface}). */
|
|
1111
|
+
retryStaleReads;
|
|
785
1112
|
constructor(pool, toParam = (v) => toPowdbParam(v), options = {}) {
|
|
786
1113
|
this.pool = pool;
|
|
787
1114
|
this.toParam = toParam;
|
|
788
1115
|
this.txGate = new PowdbTxGate(options.transactionQueueTimeoutMs ?? exports.DEFAULT_TX_QUEUE_TIMEOUT_MS);
|
|
1116
|
+
this.capabilities = options.capabilities ?? exports.ALL_POWDB_CAPABILITIES;
|
|
1117
|
+
this.retryStaleReads = options.retryStaleReads ?? false;
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Run one statement on `c`, choosing the lossless native typed wire when the
|
|
1121
|
+
* server supports it (`capabilities.nativeRaw`) AND this client exposes
|
|
1122
|
+
* `queryNativeRaw` (a defensive per-call feature-detect, so a heterogeneous
|
|
1123
|
+
* injected pool cannot crash). Otherwise the legacy string wire, unchanged.
|
|
1124
|
+
*/
|
|
1125
|
+
async runOnClient(c, powql, params) {
|
|
1126
|
+
const bound = params.map(this.toParam);
|
|
1127
|
+
if (this.capabilities.nativeRaw && typeof c.queryNativeRaw === 'function') {
|
|
1128
|
+
return adaptNativeResult(await c.queryNativeRaw(powql, bound));
|
|
1129
|
+
}
|
|
1130
|
+
return adaptResult(await c.query(powql, bound));
|
|
789
1131
|
}
|
|
790
1132
|
// biome-ignore lint/suspicious/noExplicitAny: pg-compat query is generic over the row shape.
|
|
791
1133
|
async query(text, values) {
|
|
@@ -806,8 +1148,7 @@ class PowdbPool {
|
|
|
806
1148
|
return { rows: [], rowCount: 0, fields: [] };
|
|
807
1149
|
}
|
|
808
1150
|
try {
|
|
809
|
-
|
|
810
|
-
return adaptResult(result);
|
|
1151
|
+
return await this.pool.withClient((c) => this.runOnClient(c, powql, params));
|
|
811
1152
|
}
|
|
812
1153
|
catch (err) {
|
|
813
1154
|
if (ctl === 'begin') {
|
|
@@ -878,7 +1219,7 @@ class PowdbPool {
|
|
|
878
1219
|
return { rows: [], rowCount: 0, fields: [] };
|
|
879
1220
|
}
|
|
880
1221
|
try {
|
|
881
|
-
return
|
|
1222
|
+
return await this.runOnClient(client, powql, params);
|
|
882
1223
|
}
|
|
883
1224
|
catch (err) {
|
|
884
1225
|
broken = true;
|
|
@@ -1005,6 +1346,10 @@ function encodePowqlLiteral(value) {
|
|
|
1005
1346
|
// Force a float-form literal so an integer-valued float column stays a float.
|
|
1006
1347
|
return Number.isInteger(n) ? `${n}.0` : String(n);
|
|
1007
1348
|
}
|
|
1349
|
+
// json document: emit the canonical JSON text as a PowQL string literal (the
|
|
1350
|
+
// embedded engine validates and stores it as a json document).
|
|
1351
|
+
if (value instanceof PowdbJsonParam)
|
|
1352
|
+
return encodePowqlString(JSON.stringify(value.value));
|
|
1008
1353
|
if (value === undefined || value === null)
|
|
1009
1354
|
return 'null';
|
|
1010
1355
|
if (value instanceof Date)
|
|
@@ -1082,9 +1427,19 @@ class PowdbEmbeddedPool {
|
|
|
1082
1427
|
txGate;
|
|
1083
1428
|
/** Hold taken by a `begin` issued via `query()` directly (no checked-out client). */
|
|
1084
1429
|
poolHoldRef = { hold: null };
|
|
1430
|
+
/**
|
|
1431
|
+
* Feature capabilities of the embedded engine (resolved from the addon
|
|
1432
|
+
* package version). `nativeRaw` is always false: the embedded addon exposes
|
|
1433
|
+
* no native typed-wire surface (its rows are `string[][]`, the legacy wire).
|
|
1434
|
+
*/
|
|
1435
|
+
capabilities;
|
|
1436
|
+
/** Carried for surface uniformity with {@link PowdbPool}; inert on embedded (no protocol_error frames). */
|
|
1437
|
+
retryStaleReads;
|
|
1085
1438
|
constructor(db, options = {}) {
|
|
1086
1439
|
this.db = db;
|
|
1087
1440
|
this.txGate = new PowdbTxGate(options.transactionQueueTimeoutMs ?? exports.DEFAULT_TX_QUEUE_TIMEOUT_MS);
|
|
1441
|
+
this.capabilities = options.capabilities ?? exports.ALL_POWDB_CAPABILITIES;
|
|
1442
|
+
this.retryStaleReads = options.retryStaleReads ?? false;
|
|
1088
1443
|
}
|
|
1089
1444
|
/** Materialize `$N` params and hand the PowQL to the in-process engine. */
|
|
1090
1445
|
exec(powql, params) {
|
|
@@ -1195,6 +1550,9 @@ exports.PowdbEmbeddedPool = PowdbEmbeddedPool;
|
|
|
1195
1550
|
// ---------------------------------------------------------------------------
|
|
1196
1551
|
// PowqlInterface — the PowQL query generator (Phase A: flat CRUD via returning)
|
|
1197
1552
|
// ---------------------------------------------------------------------------
|
|
1553
|
+
// `describe`-based introspection (programmatic API; see powdb-introspect.ts).
|
|
1554
|
+
var powdb_introspect_js_1 = require("./powdb-introspect.js");
|
|
1555
|
+
Object.defineProperty(exports, "introspectPowdbDatabase", { enumerable: true, get: function () { return powdb_introspect_js_1.introspectPowdbDatabase; } });
|
|
1198
1556
|
var powql_js_1 = require("./powql.js");
|
|
1199
1557
|
Object.defineProperty(exports, "PowqlInterface", { enumerable: true, get: function () { return powql_js_1.PowqlInterface; } });
|
|
1200
1558
|
/**
|
|
@@ -1242,8 +1600,20 @@ async function loadPowdbEmbedded() {
|
|
|
1242
1600
|
}
|
|
1243
1601
|
return mod;
|
|
1244
1602
|
}
|
|
1603
|
+
/**
|
|
1604
|
+
* Resolve the embedded addon's engine version. The addon vendors the engine and
|
|
1605
|
+
* exports no version, but `@zvndev/powdb-embedded/package.json` has no `exports`
|
|
1606
|
+
* map, so a bare `require` of it resolves: the package version IS the engine
|
|
1607
|
+
* version. Delegated to the `.cts` optional-peer helper so the resolution uses a
|
|
1608
|
+
* real CommonJS `require` in BOTH build outputs; `import.meta.url` here would
|
|
1609
|
+
* fail `tsc` under `tsconfig.cjs.json` (module: CommonJS) and crash any CJS
|
|
1610
|
+
* consumer of `turbine-orm/powdb`. Returns `null` when it cannot be resolved.
|
|
1611
|
+
*/
|
|
1612
|
+
function resolveEmbeddedVersion() {
|
|
1613
|
+
return optional_peer_import_cjs_1.default.peerPackageVersion('@zvndev/powdb-embedded');
|
|
1614
|
+
}
|
|
1245
1615
|
/** Open an embedded database handle, wrapping engine open failures (corrupt dir, etc.). */
|
|
1246
|
-
async function openEmbeddedPool(target, poolOptions = {}) {
|
|
1616
|
+
async function openEmbeddedPool(target, poolOptions = {}, assumeEngineVersion) {
|
|
1247
1617
|
const mod = await loadPowdbEmbedded();
|
|
1248
1618
|
const { embedded: dir, syncMode, memoryLimit } = target;
|
|
1249
1619
|
let db;
|
|
@@ -1269,7 +1639,11 @@ async function openEmbeddedPool(target, poolOptions = {}) {
|
|
|
1269
1639
|
}
|
|
1270
1640
|
db.setSyncMode(syncMode);
|
|
1271
1641
|
}
|
|
1272
|
-
|
|
1642
|
+
// Embedded exposes no native typed-wire surface, so nativeRaw is always false.
|
|
1643
|
+
const capabilities = capabilitiesFromVersion(assumeEngineVersion ?? resolveEmbeddedVersion(), {
|
|
1644
|
+
hasNativeRaw: false,
|
|
1645
|
+
});
|
|
1646
|
+
return new PowdbEmbeddedPool(db, { ...poolOptions, capabilities });
|
|
1273
1647
|
}
|
|
1274
1648
|
/**
|
|
1275
1649
|
* Bind Turbine to PowDB. `target` is one of:
|
|
@@ -1292,30 +1666,38 @@ async function openEmbeddedPool(target, poolOptions = {}) {
|
|
|
1292
1666
|
async function turbinePowDB(target, schema, options = {}) {
|
|
1293
1667
|
let pool;
|
|
1294
1668
|
let owns = false;
|
|
1295
|
-
const poolOptions = {
|
|
1669
|
+
const poolOptions = {
|
|
1670
|
+
transactionQueueTimeoutMs: options.transactionQueueTimeoutMs,
|
|
1671
|
+
retryStaleReads: options.retryStaleReads,
|
|
1672
|
+
};
|
|
1673
|
+
const max = options.connectionLimit ?? 10;
|
|
1296
1674
|
if (typeof target === 'string') {
|
|
1297
1675
|
const mod = options.powdbClientModule ?? (await loadPowdb());
|
|
1298
|
-
const clientPool = new mod.Pool({ ...parsePowdbUrl(target), max
|
|
1299
|
-
await assertNetworkedVersion(clientPool);
|
|
1300
|
-
pool = new PowdbPool(clientPool, undefined, poolOptions);
|
|
1676
|
+
const clientPool = new mod.Pool({ ...parsePowdbUrl(target), max });
|
|
1677
|
+
const capabilities = await assertNetworkedVersion(clientPool, options.assumeEngineVersion);
|
|
1678
|
+
pool = new PowdbPool(clientPool, undefined, { ...poolOptions, capabilities });
|
|
1301
1679
|
owns = true;
|
|
1302
1680
|
}
|
|
1303
1681
|
else if (target instanceof PowdbPool) {
|
|
1304
|
-
// An injected PowdbPool carries its own PowdbPoolOptions.
|
|
1682
|
+
// An injected PowdbPool carries its own PowdbPoolOptions (incl. capabilities).
|
|
1305
1683
|
pool = target;
|
|
1306
1684
|
}
|
|
1307
1685
|
else if (isEmbeddedTarget(target)) {
|
|
1308
|
-
pool = await openEmbeddedPool(target, poolOptions);
|
|
1686
|
+
pool = await openEmbeddedPool(target, poolOptions, options.assumeEngineVersion);
|
|
1309
1687
|
owns = true;
|
|
1310
1688
|
}
|
|
1311
1689
|
else if (isPowdbClientPool(target)) {
|
|
1312
|
-
pool
|
|
1690
|
+
// Injected client pool: run the SAME probe as the URL / host+port paths so
|
|
1691
|
+
// it gets real capabilities AND the version-floor check (this branch used
|
|
1692
|
+
// to skip the probe entirely (an injected pool silently bypassed both).
|
|
1693
|
+
const capabilities = await assertNetworkedVersion(target, options.assumeEngineVersion);
|
|
1694
|
+
pool = new PowdbPool(target, undefined, { ...poolOptions, capabilities });
|
|
1313
1695
|
}
|
|
1314
1696
|
else {
|
|
1315
1697
|
const mod = options.powdbClientModule ?? (await loadPowdb());
|
|
1316
|
-
const clientPool = new mod.Pool({ ...target, max
|
|
1317
|
-
await assertNetworkedVersion(clientPool);
|
|
1318
|
-
pool = new PowdbPool(clientPool, undefined, poolOptions);
|
|
1698
|
+
const clientPool = new mod.Pool({ ...target, max });
|
|
1699
|
+
const capabilities = await assertNetworkedVersion(clientPool, options.assumeEngineVersion);
|
|
1700
|
+
pool = new PowdbPool(clientPool, undefined, { ...poolOptions, capabilities });
|
|
1319
1701
|
owns = true;
|
|
1320
1702
|
}
|
|
1321
1703
|
// The PowQL generator is loaded here to keep client.ts free of any PowDB import.
|
|
@@ -1353,14 +1735,20 @@ async function turbinePowDB(target, schema, options = {}) {
|
|
|
1353
1735
|
return client;
|
|
1354
1736
|
}
|
|
1355
1737
|
/**
|
|
1356
|
-
* Probe a networked pool's `serverVersion` (declared on {@link PowdbClient})
|
|
1357
|
-
* fail fast if the server is older than {@link MIN_POWDB_VERSION}
|
|
1358
|
-
*
|
|
1359
|
-
*
|
|
1738
|
+
* Probe a networked pool's `serverVersion` (declared on {@link PowdbClient}),
|
|
1739
|
+
* fail fast if the server is older than {@link MIN_POWDB_VERSION}, and derive
|
|
1740
|
+
* the {@link PowdbCapabilities} for it: the version gates PLUS `nativeRaw`
|
|
1741
|
+
* (server ≥ 0.13 AND the client exposes `queryNativeRaw`, feature-detected
|
|
1742
|
+
* here). `assumeEngineVersion` overrides the version used for capability
|
|
1743
|
+
* derivation (the floor check still runs against the real reported version).
|
|
1744
|
+
* Errors from the probe itself surface as the normal connect failure.
|
|
1360
1745
|
*/
|
|
1361
|
-
async function assertNetworkedVersion(clientPool) {
|
|
1362
|
-
|
|
1746
|
+
async function assertNetworkedVersion(clientPool, assumeEngineVersion) {
|
|
1747
|
+
return clientPool.withClient(async (c) => {
|
|
1363
1748
|
assertSupportedPowdbVersion(c.serverVersion);
|
|
1749
|
+
const version = assumeEngineVersion ?? c.serverVersion;
|
|
1750
|
+
const hasNativeRaw = typeof c.queryNativeRaw === 'function';
|
|
1751
|
+
return capabilitiesFromVersion(version, { hasNativeRaw });
|
|
1364
1752
|
});
|
|
1365
1753
|
}
|
|
1366
1754
|
function isPowdbClientPool(x) {
|