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/powdb.js
CHANGED
|
@@ -124,6 +124,22 @@ export class PowdbFloatParam {
|
|
|
124
124
|
this.value = value;
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Marker wrapper for a JS object/array bound to a `json` document column. Both
|
|
129
|
+
* transports serialize `value` with `JSON.stringify` and send the text as a
|
|
130
|
+
* `str` param / string literal, exactly how the PowDB docs insert a json
|
|
131
|
+
* document (the engine validates it as JSON text and stores the canonical
|
|
132
|
+
* binary form). Constructed in {@link PowqlInterface.param} when the target
|
|
133
|
+
* column is `json` and the value is a non-null object/array; a JS string
|
|
134
|
+
* written to a json column passes through RAW (same contract as pg jsonb,
|
|
135
|
+
* pass `'"x"'` to store the JSON string `"x"`), and `null` stays `null`.
|
|
136
|
+
*/
|
|
137
|
+
export class PowdbJsonParam {
|
|
138
|
+
value;
|
|
139
|
+
constructor(value) {
|
|
140
|
+
this.value = value;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
127
143
|
/** Minimum PowDB server version the networked transport requires. */
|
|
128
144
|
export const MIN_POWDB_VERSION = '0.7.0';
|
|
129
145
|
/**
|
|
@@ -174,19 +190,115 @@ export function assertSupportedPowdbVersion(version) {
|
|
|
174
190
|
throw new ConnectionError(`[turbine] turbine-orm/powdb requires PowDB >= ${MIN_POWDB_VERSION}; the server reports "${version}". ` +
|
|
175
191
|
'Upgrade the PowDB server (0.7.0 added the `returning` keyword and the int->float coercion fix Turbine relies on).');
|
|
176
192
|
}
|
|
193
|
+
/** Minimum engine version each gated feature needs, for the E017 hint text. */
|
|
194
|
+
const POWDB_FEATURE_MIN_VERSION = {
|
|
195
|
+
introspection: '0.10',
|
|
196
|
+
jsonDocs: '0.12',
|
|
197
|
+
docFieldIndexes: '0.13',
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* Trusted-caller default: every FEATURE gate on, engine version unknown. Used
|
|
201
|
+
* for a directly-constructed {@link PowdbPool} / {@link PowdbEmbeddedPool} that
|
|
202
|
+
* did not go through {@link turbinePowDB}'s version probe (e.g. an injected
|
|
203
|
+
* pool, or a unit-test pool). `nativeRaw` stays OFF here because it flips the
|
|
204
|
+
* actual wire path and must only be enabled after a real server-version probe,
|
|
205
|
+
* never inferred from a bare construction.
|
|
206
|
+
*/
|
|
207
|
+
export const ALL_POWDB_CAPABILITIES = {
|
|
208
|
+
engineVersion: null,
|
|
209
|
+
jsonDocs: true,
|
|
210
|
+
docFieldIndexes: true,
|
|
211
|
+
introspection: true,
|
|
212
|
+
nativeRaw: false,
|
|
213
|
+
};
|
|
214
|
+
/** Parse a PowDB semver prefix (`0.13.0`, `0.13`, `1.2.3-rc`) into components, or `null`. */
|
|
215
|
+
function parsePowdbSemver(version) {
|
|
216
|
+
const m = /^(\d+)\.(\d+)(?:\.(\d+))?/.exec(String(version ?? '').trim());
|
|
217
|
+
if (!m)
|
|
218
|
+
return null;
|
|
219
|
+
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3] ?? 0) };
|
|
220
|
+
}
|
|
221
|
+
/** Is `sem` at least `major.minor`? */
|
|
222
|
+
function atLeastVersion(sem, major, minor) {
|
|
223
|
+
return sem.major > major || (sem.major === major && sem.minor >= minor);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Derive {@link PowdbCapabilities} from an engine version string. A non-semver /
|
|
227
|
+
* unknown version turns every gate OFF (the E017 hint then tells the caller to
|
|
228
|
+
* upgrade or pass `assumeEngineVersion`). `nativeRaw` requires BOTH the client
|
|
229
|
+
* to expose `queryNativeRaw` (passed in) AND server ≥ 0.13.
|
|
230
|
+
*/
|
|
231
|
+
export function capabilitiesFromVersion(version, opts = {}) {
|
|
232
|
+
const sem = parsePowdbSemver(version);
|
|
233
|
+
if (!sem) {
|
|
234
|
+
return {
|
|
235
|
+
engineVersion: version ?? null,
|
|
236
|
+
jsonDocs: false,
|
|
237
|
+
docFieldIndexes: false,
|
|
238
|
+
introspection: false,
|
|
239
|
+
nativeRaw: false,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
engineVersion: `${sem.major}.${sem.minor}.${sem.patch}`,
|
|
244
|
+
introspection: atLeastVersion(sem, 0, 10),
|
|
245
|
+
jsonDocs: atLeastVersion(sem, 0, 12),
|
|
246
|
+
docFieldIndexes: atLeastVersion(sem, 0, 13),
|
|
247
|
+
nativeRaw: Boolean(opts.hasNativeRaw) && atLeastVersion(sem, 0, 13),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Throw a version-hinting {@link UnsupportedFeatureError} (E017) when a gated
|
|
252
|
+
* PowQL feature is used on an engine that does not support it. Keeps old engines
|
|
253
|
+
* getting clean typed errors instead of raw PowQL parse failures.
|
|
254
|
+
*/
|
|
255
|
+
export function requireCapability(caps, key, feature) {
|
|
256
|
+
if (caps[key])
|
|
257
|
+
return;
|
|
258
|
+
const min = POWDB_FEATURE_MIN_VERSION[key];
|
|
259
|
+
const reported = caps.engineVersion
|
|
260
|
+
? `this connection reports ${caps.engineVersion}`
|
|
261
|
+
: 'this connection could not report a version';
|
|
262
|
+
throw new UnsupportedFeatureError(feature, 'PowDB', `${feature} requires PowDB >= ${min}; ${reported}. Upgrade powdb-server / @zvndev/powdb-embedded ` +
|
|
263
|
+
'(or pass `assumeEngineVersion` if the version cannot be detected).');
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Does this column map to PowDB's native `json` document type? A Postgres
|
|
267
|
+
* `json`/`jsonb` type (via `dialectType`/`pgType`) is authoritative; otherwise
|
|
268
|
+
* the tsType heuristic (`Record<…>`, `object`, `unknown`, an object/array
|
|
269
|
+
* literal) that the four scalar branches do not claim. Array columns never map
|
|
270
|
+
* to json, a PowDB array only exists INSIDE a json document, so a Postgres
|
|
271
|
+
* array column has no PowDB shape and still throws in {@link powqlColumnType}.
|
|
272
|
+
*/
|
|
273
|
+
export function isJsonColumn(col) {
|
|
274
|
+
if (col.isArray)
|
|
275
|
+
return false;
|
|
276
|
+
const dbType = (col.dialectType ?? col.pgType ?? '').toLowerCase();
|
|
277
|
+
if (dbType === 'json' || dbType === 'jsonb')
|
|
278
|
+
return true;
|
|
279
|
+
const ts = col.tsType.replace(/\s*\|\s*null$/i, '').trim();
|
|
280
|
+
if (ts === 'Date' || ts === 'boolean' || ts === 'number' || ts === 'bigint' || ts === 'string')
|
|
281
|
+
return false;
|
|
282
|
+
if (ts === 'Buffer' || ts === 'Uint8Array')
|
|
283
|
+
return false;
|
|
284
|
+
return /Record<|object|unknown|\[\]|\{/.test(ts);
|
|
285
|
+
}
|
|
177
286
|
/**
|
|
178
287
|
* Map a Turbine column to the PowQL DDL type used in `defineSchema` →
|
|
179
288
|
* `type T { … }`. Turbine never emits PowDB's `uuid`/`datetime`/`bytes` types,
|
|
180
289
|
* which cannot hold client-supplied values on the wire (no literal, no cast):
|
|
181
290
|
* - `Date` → `int` (epoch micros) - `boolean` → `bool`
|
|
182
291
|
* - integral `number`/`bigint` → `int` - fractional `number` → `float`
|
|
292
|
+
* - JSON / object columns → `json` (native PowDB document type, ≥ 0.12)
|
|
183
293
|
* - everything else (incl. UUID/PK strings) → `str`
|
|
184
|
-
* Array
|
|
294
|
+
* Array (non-json) and bytes columns throw, they have no PowDB equivalent.
|
|
185
295
|
*/
|
|
186
296
|
export function powqlColumnType(col) {
|
|
187
297
|
if (col.isArray) {
|
|
188
298
|
throw new ValidationError(`[turbine] Column "${col.name}" is an array — PowDB has no array type. Arrays are unsupported on the PowDB backend.`);
|
|
189
299
|
}
|
|
300
|
+
if (isJsonColumn(col))
|
|
301
|
+
return 'json';
|
|
190
302
|
const ts = col.tsType.replace(/\s*\|\s*null$/i, '').trim();
|
|
191
303
|
if (ts === 'Date')
|
|
192
304
|
return 'int'; // epoch micros
|
|
@@ -201,9 +313,6 @@ export function powqlColumnType(col) {
|
|
|
201
313
|
if (ts === 'Buffer' || ts === 'Uint8Array') {
|
|
202
314
|
throw new ValidationError(`[turbine] Column "${col.name}" is binary — PowDB cannot store client-supplied bytes on the wire. Use a string (e.g. base64) instead.`);
|
|
203
315
|
}
|
|
204
|
-
if (/Record<|object|unknown|\[\]|\{/.test(ts)) {
|
|
205
|
-
throw new ValidationError(`[turbine] Column "${col.name}" (${col.tsType}) maps to JSON/object, which PowDB has no type for. Flatten it or store a JSON string.`);
|
|
206
|
-
}
|
|
207
316
|
return 'str';
|
|
208
317
|
}
|
|
209
318
|
/** Heuristic: does this numeric column hold fractional values (→ PowQL `float`)? */
|
|
@@ -343,7 +452,8 @@ export function quotePowqlIdent(name) {
|
|
|
343
452
|
}
|
|
344
453
|
return POWQL_KEYWORDS.has(name) || !POWQL_BARE_IDENT.test(name) ? `\`${name}\`` : name;
|
|
345
454
|
}
|
|
346
|
-
export function powqlSchemaDDL(schema) {
|
|
455
|
+
export function powqlSchemaDDL(schema, opts = {}) {
|
|
456
|
+
const caps = opts.capabilities;
|
|
347
457
|
const stmts = [];
|
|
348
458
|
for (const meta of Object.values(schema.tables)) {
|
|
349
459
|
const pkSet = new Set(meta.primaryKey);
|
|
@@ -354,6 +464,12 @@ export function powqlSchemaDDL(schema) {
|
|
|
354
464
|
// cannot enforce the tuple's uniqueness at the engine level.
|
|
355
465
|
const pkIsSingle = meta.primaryKey.length === 1;
|
|
356
466
|
const fields = meta.columns.map((col) => {
|
|
467
|
+
const powqlType = powqlColumnType(col);
|
|
468
|
+
// Gate `json` columns behind the engine's jsonDocs capability when a
|
|
469
|
+
// caller supplied one, an old engine has no `json` type and would reject
|
|
470
|
+
// the DDL. Pure-function callers (no opts) emit unconditionally.
|
|
471
|
+
if (powqlType === 'json' && caps)
|
|
472
|
+
requireCapability(caps, 'jsonDocs', 'JSON document columns');
|
|
357
473
|
const mods = [];
|
|
358
474
|
if (!col.nullable || pkSet.has(col.name))
|
|
359
475
|
mods.push('required');
|
|
@@ -362,15 +478,57 @@ export function powqlSchemaDDL(schema) {
|
|
|
362
478
|
// `auto` = server-generated monotonic int. PowDB requires it be `int` and
|
|
363
479
|
// rejects it alongside a `default`; non-int generated columns fall back to
|
|
364
480
|
// a plain typed column (Turbine assigns the value client-side instead).
|
|
365
|
-
if (col.isGenerated &&
|
|
481
|
+
if (col.isGenerated && powqlType === 'int')
|
|
366
482
|
mods.push('auto');
|
|
367
|
-
return ` ${mods.join(' ')}${mods.length ? ' ' : ''}${quotePowqlIdent(col.name)}: ${
|
|
483
|
+
return ` ${mods.join(' ')}${mods.length ? ' ' : ''}${quotePowqlIdent(col.name)}: ${powqlType}`;
|
|
368
484
|
});
|
|
369
485
|
stmts.push(`type ${quotePowqlIdent(meta.name)} {\n${fields.join(',\n')}\n}`);
|
|
486
|
+
// Track which single columns already carry a unique constraint (the
|
|
487
|
+
// single-column PK is inlined `required unique` in the type body above) so
|
|
488
|
+
// a redundant `add unique .col` is never emitted twice.
|
|
489
|
+
const emittedUnique = new Set();
|
|
490
|
+
if (pkIsSingle && meta.primaryKey[0] !== undefined)
|
|
491
|
+
emittedUnique.add(meta.primaryKey[0]);
|
|
370
492
|
// Secondary unique constraints (beyond the PK) become unique indexes.
|
|
371
493
|
for (const uniq of meta.uniqueColumns) {
|
|
372
494
|
if (uniq.length === 1 && !pkSet.has(uniq[0])) {
|
|
373
495
|
stmts.push(`alter ${quotePowqlIdent(meta.name)} add unique .${quotePowqlIdent(uniq[0])}`);
|
|
496
|
+
emittedUnique.add(uniq[0]);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
// Declared indexes: PowDB doc-field expression indexes (docPath) and plain
|
|
500
|
+
// single-column indexes. A doc-field index MUST be parenthesized (the engine
|
|
501
|
+
// rejects a bare JSON path); string path segments emit lexer-exact via the
|
|
502
|
+
// shared `encodePowqlString`, integer array indexes emit bare. A json
|
|
503
|
+
// document column reference stays dotted-bare (`.col`), which bypasses
|
|
504
|
+
// keyword lookup on every engine version exactly like a filter path.
|
|
505
|
+
for (const idx of meta.indexes) {
|
|
506
|
+
const kind = idx.unique ? 'unique' : 'index';
|
|
507
|
+
if (idx.docPath) {
|
|
508
|
+
if (caps)
|
|
509
|
+
requireCapability(caps, 'docFieldIndexes', 'JSON doc-field expression indexes');
|
|
510
|
+
const column = idx.columns[0];
|
|
511
|
+
if (column === undefined) {
|
|
512
|
+
throw new ValidationError(`[turbine] Doc-field index "${idx.name}" on ${meta.name} has no target json column.`);
|
|
513
|
+
}
|
|
514
|
+
const segs = idx.docPath.map((s) => (typeof s === 'number' ? `->${s}` : `->${encodePowqlString(s)}`)).join('');
|
|
515
|
+
stmts.push(`alter ${quotePowqlIdent(meta.name)} add ${kind} (.${column}${segs})`);
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
// Plain column index. PowDB has no composite index (`add index` takes a
|
|
519
|
+
// single `.column`), so a multi-column entry is a typed E017.
|
|
520
|
+
if (idx.columns.length !== 1) {
|
|
521
|
+
throw new UnsupportedFeatureError('composite indexes', 'PowDB', `PowDB has no composite index. Index "${idx.name}" on ${meta.name} lists ` +
|
|
522
|
+
`${idx.columns.length} columns; declare a single-column index (or a doc-field index) instead.`);
|
|
523
|
+
}
|
|
524
|
+
const column = idx.columns[0];
|
|
525
|
+
// A unique index whose column already carries a unique constraint (the
|
|
526
|
+
// PK, or a column-level unique) would be a redundant duplicate, so skip it.
|
|
527
|
+
if (idx.unique && emittedUnique.has(column))
|
|
528
|
+
continue;
|
|
529
|
+
stmts.push(`alter ${quotePowqlIdent(meta.name)} add ${kind} .${quotePowqlIdent(column)}`);
|
|
530
|
+
if (idx.unique)
|
|
531
|
+
emittedUnique.add(column);
|
|
374
532
|
}
|
|
375
533
|
}
|
|
376
534
|
}
|
|
@@ -380,6 +538,10 @@ export function powqlSchemaDDL(schema) {
|
|
|
380
538
|
function toPowdbParam(value, col) {
|
|
381
539
|
if (value instanceof PowdbFloatParam)
|
|
382
540
|
return value.value; // wire-side: a float column takes the plain number
|
|
541
|
+
// json document: serialize to canonical JSON text and bind as a str param,
|
|
542
|
+
// the engine validates it as JSON and stores the canonical binary form.
|
|
543
|
+
if (value instanceof PowdbJsonParam)
|
|
544
|
+
return JSON.stringify(value.value);
|
|
383
545
|
if (value === undefined || value === null)
|
|
384
546
|
return null;
|
|
385
547
|
if (value instanceof Date)
|
|
@@ -402,10 +564,23 @@ function toPowdbParam(value, col) {
|
|
|
402
564
|
*/
|
|
403
565
|
export function coerceValue(raw, col) {
|
|
404
566
|
const ts = col.tsType.replace(/\s*\|\s*null$/i, '').trim();
|
|
567
|
+
const json = isJsonColumn(col);
|
|
405
568
|
// NULL bareword: unambiguous for non-string columns; for `str` we cannot tell a
|
|
406
569
|
// literal "null" from SQL NULL, so a nullable str of value "null" reads as null.
|
|
407
|
-
|
|
570
|
+
// For a `json` column the bareword `null` (a legacy-wire rendering shared by an
|
|
571
|
+
// absent value AND a top-level JSON-null document, documented residual,
|
|
572
|
+
// resolved on the native transport by the WireValue path) maps to null; a JSON
|
|
573
|
+
// string document "null" renders WITH quotes (`"null"`) and parses distinctly.
|
|
574
|
+
if (raw === 'null' && (json || ts !== 'string' || col.nullable))
|
|
408
575
|
return null;
|
|
576
|
+
if (json) {
|
|
577
|
+
try {
|
|
578
|
+
return JSON.parse(raw);
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
return raw; // defensive: canonical JSON text always parses
|
|
582
|
+
}
|
|
583
|
+
}
|
|
409
584
|
if (ts === 'Date') {
|
|
410
585
|
const micros = Number(raw);
|
|
411
586
|
return Number.isFinite(micros) ? new Date(micros / 1000) : null;
|
|
@@ -422,19 +597,62 @@ export function coerceValue(raw, col) {
|
|
|
422
597
|
return raw; // string / uuid-as-string
|
|
423
598
|
}
|
|
424
599
|
/**
|
|
425
|
-
*
|
|
426
|
-
* {@link
|
|
427
|
-
*
|
|
428
|
-
*
|
|
600
|
+
* Coerce a single cell that arrived over the NATIVE typed wire (decoded from a
|
|
601
|
+
* {@link PowdbWireValue}, so already a JS `bigint`/`number`/`boolean`/`string`/
|
|
602
|
+
* `NativeJson`/`Uint8Array`/`null`, never a bare `"null"` string). Unlike
|
|
603
|
+
* {@link coerceValue} this NEVER collapses the string `"null"` to `null`: an
|
|
604
|
+
* absent value already decoded to `null` (from the `empty` cell), so a genuine
|
|
605
|
+
* str `"null"` stays the string `"null"` (fixes the legacy-wire wart on the
|
|
606
|
+
* native transport). `datetime`-shaped cells (int micros) become `Date`; a
|
|
607
|
+
* bigint on a `number` column follows the int8 safe-integer policy.
|
|
608
|
+
*/
|
|
609
|
+
export function coerceNativeValue(value, col) {
|
|
610
|
+
if (value === undefined || value === null)
|
|
611
|
+
return null;
|
|
612
|
+
if (isDateColumn(col)) {
|
|
613
|
+
if (typeof value === 'bigint')
|
|
614
|
+
return new Date(Number(value) / 1000);
|
|
615
|
+
if (typeof value === 'number')
|
|
616
|
+
return new Date(value / 1000);
|
|
617
|
+
return value;
|
|
618
|
+
}
|
|
619
|
+
const ts = col.tsType.replace(/\s*\|\s*null$/i, '').trim();
|
|
620
|
+
if (typeof value === 'bigint') {
|
|
621
|
+
if (ts === 'bigint')
|
|
622
|
+
return value;
|
|
623
|
+
if (ts === 'number') {
|
|
624
|
+
const n = Number(value);
|
|
625
|
+
return Number.isSafeInteger(n) ? n : value.toString(); // int8 policy: keep big ints as strings
|
|
626
|
+
}
|
|
627
|
+
return value;
|
|
628
|
+
}
|
|
629
|
+
return value; // number / boolean / string / NativeJson document / Uint8Array
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Map one raw PowDB row into a typed entity (camelCase fields, coerced values).
|
|
633
|
+
* Only the columns present in `raw` are emitted, so partial `select`
|
|
634
|
+
* projections round-trip unchanged. `native` selects the coercion policy: the
|
|
635
|
+
* default `false` handles the legacy string wire (every cell is a string, via
|
|
636
|
+
* {@link coerceValue}); `true` handles the native typed wire, where non-string
|
|
637
|
+
* cells arrive pre-typed and go through {@link coerceNativeValue} (see F3).
|
|
638
|
+
* Callers on the native transport pass `this.pool.capabilities.nativeRaw`.
|
|
429
639
|
*/
|
|
430
|
-
export function rowToEntity(raw, meta) {
|
|
640
|
+
export function rowToEntity(raw, meta, native = false) {
|
|
431
641
|
const byName = new Map(meta.columns.map((c) => [c.name, c]));
|
|
432
642
|
const out = {};
|
|
433
643
|
for (const snake of Object.keys(raw)) {
|
|
434
644
|
const col = byName.get(snake);
|
|
435
645
|
const field = meta.reverseColumnMap[snake] ?? snake;
|
|
436
646
|
const value = raw[snake];
|
|
437
|
-
|
|
647
|
+
if (!col) {
|
|
648
|
+
out[field] = value;
|
|
649
|
+
}
|
|
650
|
+
else if (native) {
|
|
651
|
+
out[field] = coerceNativeValue(value, col);
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
out[field] = typeof value === 'string' ? coerceValue(value, col) : value;
|
|
655
|
+
}
|
|
438
656
|
}
|
|
439
657
|
return out;
|
|
440
658
|
}
|
|
@@ -460,7 +678,7 @@ export function wrapPowdbError(err) {
|
|
|
460
678
|
const e = err;
|
|
461
679
|
const msg = e.message ?? 'unknown PowDB error';
|
|
462
680
|
// Unique-constraint — message-based on both transports.
|
|
463
|
-
if (/unique constraint violation/i.test(msg)) {
|
|
681
|
+
if (/unique (constraint|expression index) violation/i.test(msg)) {
|
|
464
682
|
const m = /on\s+\S+\.(\w+)/i.exec(msg);
|
|
465
683
|
return new UniqueConstraintError({ constraint: m?.[1], cause: err });
|
|
466
684
|
}
|
|
@@ -473,37 +691,71 @@ export function wrapPowdbError(err) {
|
|
|
473
691
|
// Driver pool lifecycle errors (acquire after close, acquire timeout) carry
|
|
474
692
|
// no .code — classify by message so both transports surface E004.
|
|
475
693
|
if (/pool closed|pool acquire timeout/i.test(msg)) {
|
|
476
|
-
return new ConnectionError(`[turbine] PowDB connection unavailable: ${msg}
|
|
694
|
+
return new ConnectionError(`[turbine] PowDB connection unavailable: ${msg}`, { cause: err });
|
|
477
695
|
}
|
|
478
696
|
// Server-side transaction-gate wait bound (PowDB ≥ 0.10, default 5s): another
|
|
479
697
|
// connection held the single global write lock past the server's
|
|
480
698
|
// --tx-wait-timeout-ms. Retryable timeout, not a query defect.
|
|
481
699
|
if (/transaction gate timeout/i.test(msg)) {
|
|
482
|
-
return new TimeoutError(0, 'PowDB transaction gate');
|
|
700
|
+
return new TimeoutError(0, 'PowDB transaction gate', { cause: err });
|
|
701
|
+
}
|
|
702
|
+
// Stale / violated WIRE state → ConnectionError (E004), NOT a query defect.
|
|
703
|
+
// A `protocol_error`-class failure means the socket's framing state is gone
|
|
704
|
+
// (the client cannot safely reuse it and the pool must destroy it). The
|
|
705
|
+
// canonical trigger is the "received unexpected frame from server" that a
|
|
706
|
+
// fresh request hits after a multi-minute idle gap; sibling shapes are an
|
|
707
|
+
// unknown message type, a truncated payload, or bad framing. Runs BEFORE the
|
|
708
|
+
// validation regex below, whose `unexpected` token would otherwise misclass
|
|
709
|
+
// "received unexpected frame" as an E003 query defect. `.cause` preserved so
|
|
710
|
+
// callers (and the opt-in stale-read retry) can inspect the driver code.
|
|
711
|
+
if (e.code === 'protocol_error' ||
|
|
712
|
+
/received unexpected frame|unknown message type|truncated payload|bad framing/i.test(msg)) {
|
|
713
|
+
return new ConnectionError(`[turbine] PowDB connection is in an invalid state: ${msg}`, { cause: err });
|
|
483
714
|
}
|
|
484
|
-
// Type mismatch / parse / execution / storage / unexpected
|
|
485
|
-
// (E003). On the embedded transport these are the only
|
|
486
|
-
// (code is always 'GenericFailure'); on the networked path they
|
|
487
|
-
// safety net before the .code switch.
|
|
715
|
+
// Type mismatch / parse / execution / storage / unexpected(token) / row too
|
|
716
|
+
// large → validation (E003). On the embedded transport these are the only
|
|
717
|
+
// signal we get (code is always 'GenericFailure'); on the networked path they
|
|
718
|
+
// are a safety net before the .code switch.
|
|
488
719
|
if (/type mismatch|\bParse\b|\bExecution\b|StorageError|unexpected|row too large/i.test(msg)) {
|
|
489
720
|
return new ValidationError(`[turbine] PowDB query rejected: ${msg}`);
|
|
490
721
|
}
|
|
491
722
|
switch (e.code) {
|
|
492
723
|
case 'connect_failed':
|
|
493
724
|
case 'closed':
|
|
494
|
-
return new ConnectionError(`[turbine] PowDB connection failed: ${msg}
|
|
725
|
+
return new ConnectionError(`[turbine] PowDB connection failed: ${msg}`, { cause: err });
|
|
726
|
+
case 'auth_failed':
|
|
727
|
+
// Connection-establishment class, non-retryable: the handshake was
|
|
728
|
+
// rejected. Surface E004 with a concrete remediation hint instead of
|
|
729
|
+
// letting it fall through to the raw error.
|
|
730
|
+
return new ConnectionError(`[turbine] PowDB authentication failed: ${msg} (check the user / password / dbName for this connection).`, { cause: err });
|
|
495
731
|
case 'timeout':
|
|
496
732
|
case 'aborted':
|
|
497
|
-
return new TimeoutError(0, 'PowDB query');
|
|
733
|
+
return new TimeoutError(0, 'PowDB query', { cause: err });
|
|
498
734
|
case 'query_failed':
|
|
499
735
|
case 'type_coercion_failed':
|
|
500
|
-
case 'protocol_error':
|
|
501
736
|
case 'size_exceeded':
|
|
502
737
|
return new ValidationError(`[turbine] PowDB query rejected: ${msg}`);
|
|
503
738
|
default:
|
|
504
|
-
return err instanceof Error ? err : new ConnectionError(`[turbine] PowDB error: ${msg}
|
|
739
|
+
return err instanceof Error ? err : new ConnectionError(`[turbine] PowDB error: ${msg}`, { cause: err });
|
|
505
740
|
}
|
|
506
741
|
}
|
|
742
|
+
/**
|
|
743
|
+
* True when `err` is the stale-wire-frame {@link ConnectionError} produced by
|
|
744
|
+
* {@link wrapPowdbError} (its `.cause` is a `protocol_error` PowDBError, or the
|
|
745
|
+
* message carries the invalid-state signature). The opt-in read retry
|
|
746
|
+
* (`retryStaleReads`, evaluated in {@link PowqlInterface}'s exec seam) uses this
|
|
747
|
+
* to decide whether a first-statement READ may be replayed once on a fresh
|
|
748
|
+
* connection; writes are NEVER retried (an ambiguous mutation reply is unsafe
|
|
749
|
+
* to replay, matching the client's own native-path policy).
|
|
750
|
+
*/
|
|
751
|
+
export function isStaleFramePowdbError(err) {
|
|
752
|
+
if (!(err instanceof ConnectionError))
|
|
753
|
+
return false;
|
|
754
|
+
const cause = err.cause;
|
|
755
|
+
if (cause && typeof cause === 'object' && cause.code === 'protocol_error')
|
|
756
|
+
return true;
|
|
757
|
+
return /PowDB connection is in an invalid state/.test(err.message);
|
|
758
|
+
}
|
|
507
759
|
function normalizeQueryArgs(arg, values) {
|
|
508
760
|
if (typeof arg === 'string')
|
|
509
761
|
return { text: arg, params: values ?? [] };
|
|
@@ -682,7 +934,7 @@ class PowdbTxGate {
|
|
|
682
934
|
return hold;
|
|
683
935
|
}
|
|
684
936
|
}
|
|
685
|
-
/** Adapt a PowDB result into the pg-compat `{ rows, rowCount, fields }` shape. */
|
|
937
|
+
/** Adapt a PowDB (legacy string wire) result into the pg-compat `{ rows, rowCount, fields }` shape. */
|
|
686
938
|
function adaptResult(r) {
|
|
687
939
|
switch (r.kind) {
|
|
688
940
|
case 'rows': {
|
|
@@ -693,21 +945,86 @@ function adaptResult(r) {
|
|
|
693
945
|
});
|
|
694
946
|
return o;
|
|
695
947
|
});
|
|
696
|
-
return { rows, rowCount: rows.length, fields: r.columns.map((name) => ({ name, dataTypeID: 0 })) };
|
|
948
|
+
return { rows, rowCount: rows.length, fields: r.columns.map((name) => ({ name, dataTypeID: 0 })), native: false };
|
|
697
949
|
}
|
|
698
950
|
case 'ok':
|
|
699
|
-
return { rows: [], rowCount: Number(r.affected), fields: [] };
|
|
951
|
+
return { rows: [], rowCount: Number(r.affected), fields: [], native: false };
|
|
700
952
|
case 'scalar':
|
|
701
|
-
return { rows: [{ value: r.value }], rowCount: 1, fields: [{ name: 'value', dataTypeID: 0 }] };
|
|
953
|
+
return { rows: [{ value: r.value }], rowCount: 1, fields: [{ name: 'value', dataTypeID: 0 }], native: false };
|
|
702
954
|
default:
|
|
703
|
-
return { rows: [], rowCount: 0, fields: [] };
|
|
955
|
+
return { rows: [], rowCount: 0, fields: [], native: false };
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
/** Format 16 raw UUID bytes as a canonical `8-4-4-4-12` hex string. */
|
|
959
|
+
function uuidBytesToHex(bytes) {
|
|
960
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
961
|
+
if (hex.length !== 32)
|
|
962
|
+
return hex; // defensive: non-16B payloads pass through as raw hex
|
|
963
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Decode one native {@link PowdbWireValue} cell into a JS value. `empty` →
|
|
967
|
+
* `null` (an unset value; for a json column this cleanly distinguishes absent
|
|
968
|
+
* from a JSON-null document, which arrives as `{ type: 'json', value: null }`).
|
|
969
|
+
* `int`/`datetime` stay `bigint` so the row layer ({@link coerceNativeValue})
|
|
970
|
+
* applies the int8 policy / Date conversion by column; `uuid` becomes canonical
|
|
971
|
+
* hex; `bytes` stay `Uint8Array`; `json` passes the decoded document through
|
|
972
|
+
* with no re-parse (its `pj1` raw bytes are dropped).
|
|
973
|
+
*/
|
|
974
|
+
function decodeWireValue(cell) {
|
|
975
|
+
switch (cell.type) {
|
|
976
|
+
case 'empty':
|
|
977
|
+
return null;
|
|
978
|
+
case 'int':
|
|
979
|
+
case 'datetime':
|
|
980
|
+
return cell.value; // bigint; row layer decides Date vs number vs bigint per column
|
|
981
|
+
case 'float':
|
|
982
|
+
case 'bool':
|
|
983
|
+
case 'str':
|
|
984
|
+
return cell.value;
|
|
985
|
+
case 'uuid':
|
|
986
|
+
return uuidBytesToHex(cell.value);
|
|
987
|
+
case 'bytes':
|
|
988
|
+
return cell.value; // Uint8Array
|
|
989
|
+
case 'json':
|
|
990
|
+
return cell.value; // NativeJson document, already recursive data
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
/** Adapt a native (typed-wire) PowDB result into the pg-compat shape, decoding every {@link PowdbWireValue} cell. */
|
|
994
|
+
function adaptNativeResult(r) {
|
|
995
|
+
switch (r.kind) {
|
|
996
|
+
case 'rows': {
|
|
997
|
+
const rows = r.rows.map((row) => {
|
|
998
|
+
const o = {};
|
|
999
|
+
r.columns.forEach((c, i) => {
|
|
1000
|
+
o[c] = decodeWireValue(row[i] ?? { type: 'empty' });
|
|
1001
|
+
});
|
|
1002
|
+
return o;
|
|
1003
|
+
});
|
|
1004
|
+
return { rows, rowCount: rows.length, fields: r.columns.map((name) => ({ name, dataTypeID: 0 })), native: true };
|
|
1005
|
+
}
|
|
1006
|
+
case 'ok':
|
|
1007
|
+
return { rows: [], rowCount: Number(r.affected), fields: [], native: true };
|
|
1008
|
+
case 'scalar':
|
|
1009
|
+
return {
|
|
1010
|
+
rows: [{ value: decodeWireValue(r.value) }],
|
|
1011
|
+
rowCount: 1,
|
|
1012
|
+
fields: [{ name: 'value', dataTypeID: 0 }],
|
|
1013
|
+
native: true,
|
|
1014
|
+
};
|
|
1015
|
+
default:
|
|
1016
|
+
return { rows: [], rowCount: 0, fields: [], native: true };
|
|
704
1017
|
}
|
|
705
1018
|
}
|
|
706
1019
|
/**
|
|
707
1020
|
* A {@link PgCompatPool} backed by a `@zvndev/powdb-client` `Pool`. The query
|
|
708
|
-
* `text` is **PowQL**, not SQL
|
|
709
|
-
*
|
|
710
|
-
* (
|
|
1021
|
+
* `text` is **PowQL**, not SQL ({@link PowqlInterface} generates it). On the
|
|
1022
|
+
* legacy string wire cells come back as strings; when `capabilities.nativeRaw`
|
|
1023
|
+
* is set (server ≥ 0.13 + a client exposing `queryNativeRaw`) this pool routes
|
|
1024
|
+
* through the typed native wire instead, so cells arrive pre-typed (a json int
|
|
1025
|
+
* as `bigint`, etc.) and each result is tagged with the wire that served it
|
|
1026
|
+
* ({@link PowdbTaggedResult}). Per-column JS coercion still happens in
|
|
1027
|
+
* `PowqlInterface` (it owns the schema metadata), keyed on that per-result tag.
|
|
711
1028
|
*/
|
|
712
1029
|
export class PowdbPool {
|
|
713
1030
|
pool;
|
|
@@ -731,10 +1048,29 @@ export class PowdbPool {
|
|
|
731
1048
|
* live socket holding the process open until the server's idle timeout.
|
|
732
1049
|
*/
|
|
733
1050
|
checkedOut = new Set();
|
|
1051
|
+
/** Feature capabilities of the bound server (probed version + native-wire feature-detect). */
|
|
1052
|
+
capabilities;
|
|
1053
|
+
/** Opt-in first-statement-read replay on a stale wire frame (read by {@link PowqlInterface}). */
|
|
1054
|
+
retryStaleReads;
|
|
734
1055
|
constructor(pool, toParam = (v) => toPowdbParam(v), options = {}) {
|
|
735
1056
|
this.pool = pool;
|
|
736
1057
|
this.toParam = toParam;
|
|
737
1058
|
this.txGate = new PowdbTxGate(options.transactionQueueTimeoutMs ?? DEFAULT_TX_QUEUE_TIMEOUT_MS);
|
|
1059
|
+
this.capabilities = options.capabilities ?? ALL_POWDB_CAPABILITIES;
|
|
1060
|
+
this.retryStaleReads = options.retryStaleReads ?? false;
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Run one statement on `c`, choosing the lossless native typed wire when the
|
|
1064
|
+
* server supports it (`capabilities.nativeRaw`) AND this client exposes
|
|
1065
|
+
* `queryNativeRaw` (a defensive per-call feature-detect, so a heterogeneous
|
|
1066
|
+
* injected pool cannot crash). Otherwise the legacy string wire, unchanged.
|
|
1067
|
+
*/
|
|
1068
|
+
async runOnClient(c, powql, params) {
|
|
1069
|
+
const bound = params.map(this.toParam);
|
|
1070
|
+
if (this.capabilities.nativeRaw && typeof c.queryNativeRaw === 'function') {
|
|
1071
|
+
return adaptNativeResult(await c.queryNativeRaw(powql, bound));
|
|
1072
|
+
}
|
|
1073
|
+
return adaptResult(await c.query(powql, bound));
|
|
738
1074
|
}
|
|
739
1075
|
// biome-ignore lint/suspicious/noExplicitAny: pg-compat query is generic over the row shape.
|
|
740
1076
|
async query(text, values) {
|
|
@@ -755,8 +1091,7 @@ export class PowdbPool {
|
|
|
755
1091
|
return { rows: [], rowCount: 0, fields: [] };
|
|
756
1092
|
}
|
|
757
1093
|
try {
|
|
758
|
-
|
|
759
|
-
return adaptResult(result);
|
|
1094
|
+
return await this.pool.withClient((c) => this.runOnClient(c, powql, params));
|
|
760
1095
|
}
|
|
761
1096
|
catch (err) {
|
|
762
1097
|
if (ctl === 'begin') {
|
|
@@ -827,7 +1162,7 @@ export class PowdbPool {
|
|
|
827
1162
|
return { rows: [], rowCount: 0, fields: [] };
|
|
828
1163
|
}
|
|
829
1164
|
try {
|
|
830
|
-
return
|
|
1165
|
+
return await this.runOnClient(client, powql, params);
|
|
831
1166
|
}
|
|
832
1167
|
catch (err) {
|
|
833
1168
|
broken = true;
|
|
@@ -953,6 +1288,10 @@ export function encodePowqlLiteral(value) {
|
|
|
953
1288
|
// Force a float-form literal so an integer-valued float column stays a float.
|
|
954
1289
|
return Number.isInteger(n) ? `${n}.0` : String(n);
|
|
955
1290
|
}
|
|
1291
|
+
// json document: emit the canonical JSON text as a PowQL string literal (the
|
|
1292
|
+
// embedded engine validates and stores it as a json document).
|
|
1293
|
+
if (value instanceof PowdbJsonParam)
|
|
1294
|
+
return encodePowqlString(JSON.stringify(value.value));
|
|
956
1295
|
if (value === undefined || value === null)
|
|
957
1296
|
return 'null';
|
|
958
1297
|
if (value instanceof Date)
|
|
@@ -1030,9 +1369,19 @@ export class PowdbEmbeddedPool {
|
|
|
1030
1369
|
txGate;
|
|
1031
1370
|
/** Hold taken by a `begin` issued via `query()` directly (no checked-out client). */
|
|
1032
1371
|
poolHoldRef = { hold: null };
|
|
1372
|
+
/**
|
|
1373
|
+
* Feature capabilities of the embedded engine (resolved from the addon
|
|
1374
|
+
* package version). `nativeRaw` is always false: the embedded addon exposes
|
|
1375
|
+
* no native typed-wire surface (its rows are `string[][]`, the legacy wire).
|
|
1376
|
+
*/
|
|
1377
|
+
capabilities;
|
|
1378
|
+
/** Carried for surface uniformity with {@link PowdbPool}; inert on embedded (no protocol_error frames). */
|
|
1379
|
+
retryStaleReads;
|
|
1033
1380
|
constructor(db, options = {}) {
|
|
1034
1381
|
this.db = db;
|
|
1035
1382
|
this.txGate = new PowdbTxGate(options.transactionQueueTimeoutMs ?? DEFAULT_TX_QUEUE_TIMEOUT_MS);
|
|
1383
|
+
this.capabilities = options.capabilities ?? ALL_POWDB_CAPABILITIES;
|
|
1384
|
+
this.retryStaleReads = options.retryStaleReads ?? false;
|
|
1036
1385
|
}
|
|
1037
1386
|
/** Materialize `$N` params and hand the PowQL to the in-process engine. */
|
|
1038
1387
|
exec(powql, params) {
|
|
@@ -1142,6 +1491,8 @@ export class PowdbEmbeddedPool {
|
|
|
1142
1491
|
// ---------------------------------------------------------------------------
|
|
1143
1492
|
// PowqlInterface — the PowQL query generator (Phase A: flat CRUD via returning)
|
|
1144
1493
|
// ---------------------------------------------------------------------------
|
|
1494
|
+
// `describe`-based introspection (programmatic API; see powdb-introspect.ts).
|
|
1495
|
+
export { introspectPowdbDatabase, } from './powdb-introspect.js';
|
|
1145
1496
|
export { PowqlInterface } from './powql.js';
|
|
1146
1497
|
/**
|
|
1147
1498
|
* Dynamically load `@zvndev/powdb-client`. Kept out of the static import graph so
|
|
@@ -1188,8 +1539,20 @@ async function loadPowdbEmbedded() {
|
|
|
1188
1539
|
}
|
|
1189
1540
|
return mod;
|
|
1190
1541
|
}
|
|
1542
|
+
/**
|
|
1543
|
+
* Resolve the embedded addon's engine version. The addon vendors the engine and
|
|
1544
|
+
* exports no version, but `@zvndev/powdb-embedded/package.json` has no `exports`
|
|
1545
|
+
* map, so a bare `require` of it resolves: the package version IS the engine
|
|
1546
|
+
* version. Delegated to the `.cts` optional-peer helper so the resolution uses a
|
|
1547
|
+
* real CommonJS `require` in BOTH build outputs; `import.meta.url` here would
|
|
1548
|
+
* fail `tsc` under `tsconfig.cjs.json` (module: CommonJS) and crash any CJS
|
|
1549
|
+
* consumer of `turbine-orm/powdb`. Returns `null` when it cannot be resolved.
|
|
1550
|
+
*/
|
|
1551
|
+
function resolveEmbeddedVersion() {
|
|
1552
|
+
return importOptionalPeer.peerPackageVersion('@zvndev/powdb-embedded');
|
|
1553
|
+
}
|
|
1191
1554
|
/** Open an embedded database handle, wrapping engine open failures (corrupt dir, etc.). */
|
|
1192
|
-
async function openEmbeddedPool(target, poolOptions = {}) {
|
|
1555
|
+
async function openEmbeddedPool(target, poolOptions = {}, assumeEngineVersion) {
|
|
1193
1556
|
const mod = await loadPowdbEmbedded();
|
|
1194
1557
|
const { embedded: dir, syncMode, memoryLimit } = target;
|
|
1195
1558
|
let db;
|
|
@@ -1215,7 +1578,11 @@ async function openEmbeddedPool(target, poolOptions = {}) {
|
|
|
1215
1578
|
}
|
|
1216
1579
|
db.setSyncMode(syncMode);
|
|
1217
1580
|
}
|
|
1218
|
-
|
|
1581
|
+
// Embedded exposes no native typed-wire surface, so nativeRaw is always false.
|
|
1582
|
+
const capabilities = capabilitiesFromVersion(assumeEngineVersion ?? resolveEmbeddedVersion(), {
|
|
1583
|
+
hasNativeRaw: false,
|
|
1584
|
+
});
|
|
1585
|
+
return new PowdbEmbeddedPool(db, { ...poolOptions, capabilities });
|
|
1219
1586
|
}
|
|
1220
1587
|
/**
|
|
1221
1588
|
* Bind Turbine to PowDB. `target` is one of:
|
|
@@ -1238,30 +1605,38 @@ async function openEmbeddedPool(target, poolOptions = {}) {
|
|
|
1238
1605
|
export async function turbinePowDB(target, schema, options = {}) {
|
|
1239
1606
|
let pool;
|
|
1240
1607
|
let owns = false;
|
|
1241
|
-
const poolOptions = {
|
|
1608
|
+
const poolOptions = {
|
|
1609
|
+
transactionQueueTimeoutMs: options.transactionQueueTimeoutMs,
|
|
1610
|
+
retryStaleReads: options.retryStaleReads,
|
|
1611
|
+
};
|
|
1612
|
+
const max = options.connectionLimit ?? 10;
|
|
1242
1613
|
if (typeof target === 'string') {
|
|
1243
1614
|
const mod = options.powdbClientModule ?? (await loadPowdb());
|
|
1244
|
-
const clientPool = new mod.Pool({ ...parsePowdbUrl(target), max
|
|
1245
|
-
await assertNetworkedVersion(clientPool);
|
|
1246
|
-
pool = new PowdbPool(clientPool, undefined, poolOptions);
|
|
1615
|
+
const clientPool = new mod.Pool({ ...parsePowdbUrl(target), max });
|
|
1616
|
+
const capabilities = await assertNetworkedVersion(clientPool, options.assumeEngineVersion);
|
|
1617
|
+
pool = new PowdbPool(clientPool, undefined, { ...poolOptions, capabilities });
|
|
1247
1618
|
owns = true;
|
|
1248
1619
|
}
|
|
1249
1620
|
else if (target instanceof PowdbPool) {
|
|
1250
|
-
// An injected PowdbPool carries its own PowdbPoolOptions.
|
|
1621
|
+
// An injected PowdbPool carries its own PowdbPoolOptions (incl. capabilities).
|
|
1251
1622
|
pool = target;
|
|
1252
1623
|
}
|
|
1253
1624
|
else if (isEmbeddedTarget(target)) {
|
|
1254
|
-
pool = await openEmbeddedPool(target, poolOptions);
|
|
1625
|
+
pool = await openEmbeddedPool(target, poolOptions, options.assumeEngineVersion);
|
|
1255
1626
|
owns = true;
|
|
1256
1627
|
}
|
|
1257
1628
|
else if (isPowdbClientPool(target)) {
|
|
1258
|
-
pool
|
|
1629
|
+
// Injected client pool: run the SAME probe as the URL / host+port paths so
|
|
1630
|
+
// it gets real capabilities AND the version-floor check (this branch used
|
|
1631
|
+
// to skip the probe entirely (an injected pool silently bypassed both).
|
|
1632
|
+
const capabilities = await assertNetworkedVersion(target, options.assumeEngineVersion);
|
|
1633
|
+
pool = new PowdbPool(target, undefined, { ...poolOptions, capabilities });
|
|
1259
1634
|
}
|
|
1260
1635
|
else {
|
|
1261
1636
|
const mod = options.powdbClientModule ?? (await loadPowdb());
|
|
1262
|
-
const clientPool = new mod.Pool({ ...target, max
|
|
1263
|
-
await assertNetworkedVersion(clientPool);
|
|
1264
|
-
pool = new PowdbPool(clientPool, undefined, poolOptions);
|
|
1637
|
+
const clientPool = new mod.Pool({ ...target, max });
|
|
1638
|
+
const capabilities = await assertNetworkedVersion(clientPool, options.assumeEngineVersion);
|
|
1639
|
+
pool = new PowdbPool(clientPool, undefined, { ...poolOptions, capabilities });
|
|
1265
1640
|
owns = true;
|
|
1266
1641
|
}
|
|
1267
1642
|
// The PowQL generator is loaded here to keep client.ts free of any PowDB import.
|
|
@@ -1299,14 +1674,20 @@ export async function turbinePowDB(target, schema, options = {}) {
|
|
|
1299
1674
|
return client;
|
|
1300
1675
|
}
|
|
1301
1676
|
/**
|
|
1302
|
-
* Probe a networked pool's `serverVersion` (declared on {@link PowdbClient})
|
|
1303
|
-
* fail fast if the server is older than {@link MIN_POWDB_VERSION}
|
|
1304
|
-
*
|
|
1305
|
-
*
|
|
1677
|
+
* Probe a networked pool's `serverVersion` (declared on {@link PowdbClient}),
|
|
1678
|
+
* fail fast if the server is older than {@link MIN_POWDB_VERSION}, and derive
|
|
1679
|
+
* the {@link PowdbCapabilities} for it: the version gates PLUS `nativeRaw`
|
|
1680
|
+
* (server ≥ 0.13 AND the client exposes `queryNativeRaw`, feature-detected
|
|
1681
|
+
* here). `assumeEngineVersion` overrides the version used for capability
|
|
1682
|
+
* derivation (the floor check still runs against the real reported version).
|
|
1683
|
+
* Errors from the probe itself surface as the normal connect failure.
|
|
1306
1684
|
*/
|
|
1307
|
-
async function assertNetworkedVersion(clientPool) {
|
|
1308
|
-
|
|
1685
|
+
async function assertNetworkedVersion(clientPool, assumeEngineVersion) {
|
|
1686
|
+
return clientPool.withClient(async (c) => {
|
|
1309
1687
|
assertSupportedPowdbVersion(c.serverVersion);
|
|
1688
|
+
const version = assumeEngineVersion ?? c.serverVersion;
|
|
1689
|
+
const hasNativeRaw = typeof c.queryNativeRaw === 'function';
|
|
1690
|
+
return capabilitiesFromVersion(version, { hasNativeRaw });
|
|
1310
1691
|
});
|
|
1311
1692
|
}
|
|
1312
1693
|
function isPowdbClientPool(x) {
|