turbine-orm 0.32.2 → 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/dialect.js +1 -0
- package/dist/cjs/index-advisor.js +0 -0
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/mssql.js +3 -0
- package/dist/cjs/mysql.js +3 -0
- package/dist/cjs/optional-peer-import.cjs +28 -0
- package/dist/cjs/powdb-introspect.js +222 -0
- package/dist/cjs/powdb.js +446 -55
- package/dist/cjs/powql.js +566 -111
- package/dist/cjs/query/builder.js +136 -53
- package/dist/cjs/query/filters.js +4 -4
- package/dist/cjs/schema-builder.js +16 -0
- package/dist/cjs/schema-metadata.js +81 -10
- package/dist/cjs/sqlite.js +2 -0
- package/dist/dialect.d.ts +7 -0
- package/dist/dialect.js +1 -0
- 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/mssql.js +3 -0
- package/dist/mysql.js +3 -0
- 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 +438 -54
- package/dist/powql.d.ts +113 -6
- package/dist/powql.js +568 -113
- package/dist/query/builder.d.ts +11 -0
- package/dist/query/builder.js +136 -53
- package/dist/query/filters.d.ts +3 -3
- package/dist/query/filters.js +4 -4
- package/dist/query/types.d.ts +50 -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/dist/sqlite.js +2 -0
- package/package.json +3 -3
package/dist/powdb.js
CHANGED
|
@@ -92,6 +92,9 @@ export const powdbDialect = {
|
|
|
92
92
|
supportsRLS: false,
|
|
93
93
|
supportsAdvisoryLock: false,
|
|
94
94
|
supportsILike: false,
|
|
95
|
+
// PowQL has no LATERAL construct; PowqlInterface refuses pick ordering
|
|
96
|
+
// earlier, this override keeps the flag truthful if a future path consults it.
|
|
97
|
+
supportsLateralJoin: false,
|
|
95
98
|
beginStatement: () => 'begin',
|
|
96
99
|
commitStatement: () => 'commit',
|
|
97
100
|
rollbackStatement: () => 'rollback',
|
|
@@ -121,6 +124,22 @@ export class PowdbFloatParam {
|
|
|
121
124
|
this.value = value;
|
|
122
125
|
}
|
|
123
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
|
+
}
|
|
124
143
|
/** Minimum PowDB server version the networked transport requires. */
|
|
125
144
|
export const MIN_POWDB_VERSION = '0.7.0';
|
|
126
145
|
/**
|
|
@@ -171,19 +190,115 @@ export function assertSupportedPowdbVersion(version) {
|
|
|
171
190
|
throw new ConnectionError(`[turbine] turbine-orm/powdb requires PowDB >= ${MIN_POWDB_VERSION}; the server reports "${version}". ` +
|
|
172
191
|
'Upgrade the PowDB server (0.7.0 added the `returning` keyword and the int->float coercion fix Turbine relies on).');
|
|
173
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
|
+
}
|
|
174
286
|
/**
|
|
175
287
|
* Map a Turbine column to the PowQL DDL type used in `defineSchema` →
|
|
176
288
|
* `type T { … }`. Turbine never emits PowDB's `uuid`/`datetime`/`bytes` types,
|
|
177
289
|
* which cannot hold client-supplied values on the wire (no literal, no cast):
|
|
178
290
|
* - `Date` → `int` (epoch micros) - `boolean` → `bool`
|
|
179
291
|
* - integral `number`/`bigint` → `int` - fractional `number` → `float`
|
|
292
|
+
* - JSON / object columns → `json` (native PowDB document type, ≥ 0.12)
|
|
180
293
|
* - everything else (incl. UUID/PK strings) → `str`
|
|
181
|
-
* Array
|
|
294
|
+
* Array (non-json) and bytes columns throw, they have no PowDB equivalent.
|
|
182
295
|
*/
|
|
183
296
|
export function powqlColumnType(col) {
|
|
184
297
|
if (col.isArray) {
|
|
185
298
|
throw new ValidationError(`[turbine] Column "${col.name}" is an array — PowDB has no array type. Arrays are unsupported on the PowDB backend.`);
|
|
186
299
|
}
|
|
300
|
+
if (isJsonColumn(col))
|
|
301
|
+
return 'json';
|
|
187
302
|
const ts = col.tsType.replace(/\s*\|\s*null$/i, '').trim();
|
|
188
303
|
if (ts === 'Date')
|
|
189
304
|
return 'int'; // epoch micros
|
|
@@ -198,9 +313,6 @@ export function powqlColumnType(col) {
|
|
|
198
313
|
if (ts === 'Buffer' || ts === 'Uint8Array') {
|
|
199
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.`);
|
|
200
315
|
}
|
|
201
|
-
if (/Record<|object|unknown|\[\]|\{/.test(ts)) {
|
|
202
|
-
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.`);
|
|
203
|
-
}
|
|
204
316
|
return 'str';
|
|
205
317
|
}
|
|
206
318
|
/** Heuristic: does this numeric column hold fractional values (→ PowQL `float`)? */
|
|
@@ -340,7 +452,8 @@ export function quotePowqlIdent(name) {
|
|
|
340
452
|
}
|
|
341
453
|
return POWQL_KEYWORDS.has(name) || !POWQL_BARE_IDENT.test(name) ? `\`${name}\`` : name;
|
|
342
454
|
}
|
|
343
|
-
export function powqlSchemaDDL(schema) {
|
|
455
|
+
export function powqlSchemaDDL(schema, opts = {}) {
|
|
456
|
+
const caps = opts.capabilities;
|
|
344
457
|
const stmts = [];
|
|
345
458
|
for (const meta of Object.values(schema.tables)) {
|
|
346
459
|
const pkSet = new Set(meta.primaryKey);
|
|
@@ -351,6 +464,12 @@ export function powqlSchemaDDL(schema) {
|
|
|
351
464
|
// cannot enforce the tuple's uniqueness at the engine level.
|
|
352
465
|
const pkIsSingle = meta.primaryKey.length === 1;
|
|
353
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');
|
|
354
473
|
const mods = [];
|
|
355
474
|
if (!col.nullable || pkSet.has(col.name))
|
|
356
475
|
mods.push('required');
|
|
@@ -359,15 +478,57 @@ export function powqlSchemaDDL(schema) {
|
|
|
359
478
|
// `auto` = server-generated monotonic int. PowDB requires it be `int` and
|
|
360
479
|
// rejects it alongside a `default`; non-int generated columns fall back to
|
|
361
480
|
// a plain typed column (Turbine assigns the value client-side instead).
|
|
362
|
-
if (col.isGenerated &&
|
|
481
|
+
if (col.isGenerated && powqlType === 'int')
|
|
363
482
|
mods.push('auto');
|
|
364
|
-
return ` ${mods.join(' ')}${mods.length ? ' ' : ''}${quotePowqlIdent(col.name)}: ${
|
|
483
|
+
return ` ${mods.join(' ')}${mods.length ? ' ' : ''}${quotePowqlIdent(col.name)}: ${powqlType}`;
|
|
365
484
|
});
|
|
366
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]);
|
|
367
492
|
// Secondary unique constraints (beyond the PK) become unique indexes.
|
|
368
493
|
for (const uniq of meta.uniqueColumns) {
|
|
369
494
|
if (uniq.length === 1 && !pkSet.has(uniq[0])) {
|
|
370
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);
|
|
371
532
|
}
|
|
372
533
|
}
|
|
373
534
|
}
|
|
@@ -377,6 +538,10 @@ export function powqlSchemaDDL(schema) {
|
|
|
377
538
|
function toPowdbParam(value, col) {
|
|
378
539
|
if (value instanceof PowdbFloatParam)
|
|
379
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);
|
|
380
545
|
if (value === undefined || value === null)
|
|
381
546
|
return null;
|
|
382
547
|
if (value instanceof Date)
|
|
@@ -399,10 +564,23 @@ function toPowdbParam(value, col) {
|
|
|
399
564
|
*/
|
|
400
565
|
export function coerceValue(raw, col) {
|
|
401
566
|
const ts = col.tsType.replace(/\s*\|\s*null$/i, '').trim();
|
|
567
|
+
const json = isJsonColumn(col);
|
|
402
568
|
// NULL bareword: unambiguous for non-string columns; for `str` we cannot tell a
|
|
403
569
|
// literal "null" from SQL NULL, so a nullable str of value "null" reads as null.
|
|
404
|
-
|
|
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))
|
|
405
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
|
+
}
|
|
406
584
|
if (ts === 'Date') {
|
|
407
585
|
const micros = Number(raw);
|
|
408
586
|
return Number.isFinite(micros) ? new Date(micros / 1000) : null;
|
|
@@ -419,19 +597,62 @@ export function coerceValue(raw, col) {
|
|
|
419
597
|
return raw; // string / uuid-as-string
|
|
420
598
|
}
|
|
421
599
|
/**
|
|
422
|
-
*
|
|
423
|
-
* {@link
|
|
424
|
-
*
|
|
425
|
-
*
|
|
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`.
|
|
426
639
|
*/
|
|
427
|
-
export function rowToEntity(raw, meta) {
|
|
640
|
+
export function rowToEntity(raw, meta, native = false) {
|
|
428
641
|
const byName = new Map(meta.columns.map((c) => [c.name, c]));
|
|
429
642
|
const out = {};
|
|
430
643
|
for (const snake of Object.keys(raw)) {
|
|
431
644
|
const col = byName.get(snake);
|
|
432
645
|
const field = meta.reverseColumnMap[snake] ?? snake;
|
|
433
646
|
const value = raw[snake];
|
|
434
|
-
|
|
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
|
+
}
|
|
435
656
|
}
|
|
436
657
|
return out;
|
|
437
658
|
}
|
|
@@ -457,7 +678,7 @@ export function wrapPowdbError(err) {
|
|
|
457
678
|
const e = err;
|
|
458
679
|
const msg = e.message ?? 'unknown PowDB error';
|
|
459
680
|
// Unique-constraint — message-based on both transports.
|
|
460
|
-
if (/unique constraint violation/i.test(msg)) {
|
|
681
|
+
if (/unique (constraint|expression index) violation/i.test(msg)) {
|
|
461
682
|
const m = /on\s+\S+\.(\w+)/i.exec(msg);
|
|
462
683
|
return new UniqueConstraintError({ constraint: m?.[1], cause: err });
|
|
463
684
|
}
|
|
@@ -470,37 +691,71 @@ export function wrapPowdbError(err) {
|
|
|
470
691
|
// Driver pool lifecycle errors (acquire after close, acquire timeout) carry
|
|
471
692
|
// no .code — classify by message so both transports surface E004.
|
|
472
693
|
if (/pool closed|pool acquire timeout/i.test(msg)) {
|
|
473
|
-
return new ConnectionError(`[turbine] PowDB connection unavailable: ${msg}
|
|
694
|
+
return new ConnectionError(`[turbine] PowDB connection unavailable: ${msg}`, { cause: err });
|
|
474
695
|
}
|
|
475
696
|
// Server-side transaction-gate wait bound (PowDB ≥ 0.10, default 5s): another
|
|
476
697
|
// connection held the single global write lock past the server's
|
|
477
698
|
// --tx-wait-timeout-ms. Retryable timeout, not a query defect.
|
|
478
699
|
if (/transaction gate timeout/i.test(msg)) {
|
|
479
|
-
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 });
|
|
480
714
|
}
|
|
481
|
-
// Type mismatch / parse / execution / storage / unexpected
|
|
482
|
-
// (E003). On the embedded transport these are the only
|
|
483
|
-
// (code is always 'GenericFailure'); on the networked path they
|
|
484
|
-
// 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.
|
|
485
719
|
if (/type mismatch|\bParse\b|\bExecution\b|StorageError|unexpected|row too large/i.test(msg)) {
|
|
486
720
|
return new ValidationError(`[turbine] PowDB query rejected: ${msg}`);
|
|
487
721
|
}
|
|
488
722
|
switch (e.code) {
|
|
489
723
|
case 'connect_failed':
|
|
490
724
|
case 'closed':
|
|
491
|
-
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 });
|
|
492
731
|
case 'timeout':
|
|
493
732
|
case 'aborted':
|
|
494
|
-
return new TimeoutError(0, 'PowDB query');
|
|
733
|
+
return new TimeoutError(0, 'PowDB query', { cause: err });
|
|
495
734
|
case 'query_failed':
|
|
496
735
|
case 'type_coercion_failed':
|
|
497
|
-
case 'protocol_error':
|
|
498
736
|
case 'size_exceeded':
|
|
499
737
|
return new ValidationError(`[turbine] PowDB query rejected: ${msg}`);
|
|
500
738
|
default:
|
|
501
|
-
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 });
|
|
502
740
|
}
|
|
503
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
|
+
}
|
|
504
759
|
function normalizeQueryArgs(arg, values) {
|
|
505
760
|
if (typeof arg === 'string')
|
|
506
761
|
return { text: arg, params: values ?? [] };
|
|
@@ -679,7 +934,7 @@ class PowdbTxGate {
|
|
|
679
934
|
return hold;
|
|
680
935
|
}
|
|
681
936
|
}
|
|
682
|
-
/** 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. */
|
|
683
938
|
function adaptResult(r) {
|
|
684
939
|
switch (r.kind) {
|
|
685
940
|
case 'rows': {
|
|
@@ -690,21 +945,86 @@ function adaptResult(r) {
|
|
|
690
945
|
});
|
|
691
946
|
return o;
|
|
692
947
|
});
|
|
693
|
-
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 };
|
|
694
949
|
}
|
|
695
950
|
case 'ok':
|
|
696
|
-
return { rows: [], rowCount: Number(r.affected), fields: [] };
|
|
951
|
+
return { rows: [], rowCount: Number(r.affected), fields: [], native: false };
|
|
697
952
|
case 'scalar':
|
|
698
|
-
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 };
|
|
699
954
|
default:
|
|
700
|
-
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 };
|
|
701
1017
|
}
|
|
702
1018
|
}
|
|
703
1019
|
/**
|
|
704
1020
|
* A {@link PgCompatPool} backed by a `@zvndev/powdb-client` `Pool`. The query
|
|
705
|
-
* `text` is **PowQL**, not SQL
|
|
706
|
-
*
|
|
707
|
-
* (
|
|
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.
|
|
708
1028
|
*/
|
|
709
1029
|
export class PowdbPool {
|
|
710
1030
|
pool;
|
|
@@ -728,10 +1048,29 @@ export class PowdbPool {
|
|
|
728
1048
|
* live socket holding the process open until the server's idle timeout.
|
|
729
1049
|
*/
|
|
730
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;
|
|
731
1055
|
constructor(pool, toParam = (v) => toPowdbParam(v), options = {}) {
|
|
732
1056
|
this.pool = pool;
|
|
733
1057
|
this.toParam = toParam;
|
|
734
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));
|
|
735
1074
|
}
|
|
736
1075
|
// biome-ignore lint/suspicious/noExplicitAny: pg-compat query is generic over the row shape.
|
|
737
1076
|
async query(text, values) {
|
|
@@ -752,8 +1091,7 @@ export class PowdbPool {
|
|
|
752
1091
|
return { rows: [], rowCount: 0, fields: [] };
|
|
753
1092
|
}
|
|
754
1093
|
try {
|
|
755
|
-
|
|
756
|
-
return adaptResult(result);
|
|
1094
|
+
return await this.pool.withClient((c) => this.runOnClient(c, powql, params));
|
|
757
1095
|
}
|
|
758
1096
|
catch (err) {
|
|
759
1097
|
if (ctl === 'begin') {
|
|
@@ -824,7 +1162,7 @@ export class PowdbPool {
|
|
|
824
1162
|
return { rows: [], rowCount: 0, fields: [] };
|
|
825
1163
|
}
|
|
826
1164
|
try {
|
|
827
|
-
return
|
|
1165
|
+
return await this.runOnClient(client, powql, params);
|
|
828
1166
|
}
|
|
829
1167
|
catch (err) {
|
|
830
1168
|
broken = true;
|
|
@@ -950,6 +1288,10 @@ export function encodePowqlLiteral(value) {
|
|
|
950
1288
|
// Force a float-form literal so an integer-valued float column stays a float.
|
|
951
1289
|
return Number.isInteger(n) ? `${n}.0` : String(n);
|
|
952
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));
|
|
953
1295
|
if (value === undefined || value === null)
|
|
954
1296
|
return 'null';
|
|
955
1297
|
if (value instanceof Date)
|
|
@@ -1027,9 +1369,19 @@ export class PowdbEmbeddedPool {
|
|
|
1027
1369
|
txGate;
|
|
1028
1370
|
/** Hold taken by a `begin` issued via `query()` directly (no checked-out client). */
|
|
1029
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;
|
|
1030
1380
|
constructor(db, options = {}) {
|
|
1031
1381
|
this.db = db;
|
|
1032
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;
|
|
1033
1385
|
}
|
|
1034
1386
|
/** Materialize `$N` params and hand the PowQL to the in-process engine. */
|
|
1035
1387
|
exec(powql, params) {
|
|
@@ -1139,6 +1491,8 @@ export class PowdbEmbeddedPool {
|
|
|
1139
1491
|
// ---------------------------------------------------------------------------
|
|
1140
1492
|
// PowqlInterface — the PowQL query generator (Phase A: flat CRUD via returning)
|
|
1141
1493
|
// ---------------------------------------------------------------------------
|
|
1494
|
+
// `describe`-based introspection (programmatic API; see powdb-introspect.ts).
|
|
1495
|
+
export { introspectPowdbDatabase, } from './powdb-introspect.js';
|
|
1142
1496
|
export { PowqlInterface } from './powql.js';
|
|
1143
1497
|
/**
|
|
1144
1498
|
* Dynamically load `@zvndev/powdb-client`. Kept out of the static import graph so
|
|
@@ -1185,8 +1539,20 @@ async function loadPowdbEmbedded() {
|
|
|
1185
1539
|
}
|
|
1186
1540
|
return mod;
|
|
1187
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
|
+
}
|
|
1188
1554
|
/** Open an embedded database handle, wrapping engine open failures (corrupt dir, etc.). */
|
|
1189
|
-
async function openEmbeddedPool(target, poolOptions = {}) {
|
|
1555
|
+
async function openEmbeddedPool(target, poolOptions = {}, assumeEngineVersion) {
|
|
1190
1556
|
const mod = await loadPowdbEmbedded();
|
|
1191
1557
|
const { embedded: dir, syncMode, memoryLimit } = target;
|
|
1192
1558
|
let db;
|
|
@@ -1212,7 +1578,11 @@ async function openEmbeddedPool(target, poolOptions = {}) {
|
|
|
1212
1578
|
}
|
|
1213
1579
|
db.setSyncMode(syncMode);
|
|
1214
1580
|
}
|
|
1215
|
-
|
|
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 });
|
|
1216
1586
|
}
|
|
1217
1587
|
/**
|
|
1218
1588
|
* Bind Turbine to PowDB. `target` is one of:
|
|
@@ -1235,30 +1605,38 @@ async function openEmbeddedPool(target, poolOptions = {}) {
|
|
|
1235
1605
|
export async function turbinePowDB(target, schema, options = {}) {
|
|
1236
1606
|
let pool;
|
|
1237
1607
|
let owns = false;
|
|
1238
|
-
const poolOptions = {
|
|
1608
|
+
const poolOptions = {
|
|
1609
|
+
transactionQueueTimeoutMs: options.transactionQueueTimeoutMs,
|
|
1610
|
+
retryStaleReads: options.retryStaleReads,
|
|
1611
|
+
};
|
|
1612
|
+
const max = options.connectionLimit ?? 10;
|
|
1239
1613
|
if (typeof target === 'string') {
|
|
1240
1614
|
const mod = options.powdbClientModule ?? (await loadPowdb());
|
|
1241
|
-
const clientPool = new mod.Pool({ ...parsePowdbUrl(target), max
|
|
1242
|
-
await assertNetworkedVersion(clientPool);
|
|
1243
|
-
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 });
|
|
1244
1618
|
owns = true;
|
|
1245
1619
|
}
|
|
1246
1620
|
else if (target instanceof PowdbPool) {
|
|
1247
|
-
// An injected PowdbPool carries its own PowdbPoolOptions.
|
|
1621
|
+
// An injected PowdbPool carries its own PowdbPoolOptions (incl. capabilities).
|
|
1248
1622
|
pool = target;
|
|
1249
1623
|
}
|
|
1250
1624
|
else if (isEmbeddedTarget(target)) {
|
|
1251
|
-
pool = await openEmbeddedPool(target, poolOptions);
|
|
1625
|
+
pool = await openEmbeddedPool(target, poolOptions, options.assumeEngineVersion);
|
|
1252
1626
|
owns = true;
|
|
1253
1627
|
}
|
|
1254
1628
|
else if (isPowdbClientPool(target)) {
|
|
1255
|
-
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 });
|
|
1256
1634
|
}
|
|
1257
1635
|
else {
|
|
1258
1636
|
const mod = options.powdbClientModule ?? (await loadPowdb());
|
|
1259
|
-
const clientPool = new mod.Pool({ ...target, max
|
|
1260
|
-
await assertNetworkedVersion(clientPool);
|
|
1261
|
-
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 });
|
|
1262
1640
|
owns = true;
|
|
1263
1641
|
}
|
|
1264
1642
|
// The PowQL generator is loaded here to keep client.ts free of any PowDB import.
|
|
@@ -1296,14 +1674,20 @@ export async function turbinePowDB(target, schema, options = {}) {
|
|
|
1296
1674
|
return client;
|
|
1297
1675
|
}
|
|
1298
1676
|
/**
|
|
1299
|
-
* Probe a networked pool's `serverVersion` (declared on {@link PowdbClient})
|
|
1300
|
-
* fail fast if the server is older than {@link MIN_POWDB_VERSION}
|
|
1301
|
-
*
|
|
1302
|
-
*
|
|
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.
|
|
1303
1684
|
*/
|
|
1304
|
-
async function assertNetworkedVersion(clientPool) {
|
|
1305
|
-
|
|
1685
|
+
async function assertNetworkedVersion(clientPool, assumeEngineVersion) {
|
|
1686
|
+
return clientPool.withClient(async (c) => {
|
|
1306
1687
|
assertSupportedPowdbVersion(c.serverVersion);
|
|
1688
|
+
const version = assumeEngineVersion ?? c.serverVersion;
|
|
1689
|
+
const hasNativeRaw = typeof c.queryNativeRaw === 'function';
|
|
1690
|
+
return capabilitiesFromVersion(version, { hasNativeRaw });
|
|
1307
1691
|
});
|
|
1308
1692
|
}
|
|
1309
1693
|
function isPowdbClientPool(x) {
|