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/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;
|
|
@@ -142,6 +147,9 @@ exports.powdbDialect = {
|
|
|
142
147
|
supportsRLS: false,
|
|
143
148
|
supportsAdvisoryLock: false,
|
|
144
149
|
supportsILike: false,
|
|
150
|
+
// PowQL has no LATERAL construct; PowqlInterface refuses pick ordering
|
|
151
|
+
// earlier, this override keeps the flag truthful if a future path consults it.
|
|
152
|
+
supportsLateralJoin: false,
|
|
145
153
|
beginStatement: () => 'begin',
|
|
146
154
|
commitStatement: () => 'commit',
|
|
147
155
|
rollbackStatement: () => 'rollback',
|
|
@@ -172,6 +180,23 @@ class PowdbFloatParam {
|
|
|
172
180
|
}
|
|
173
181
|
}
|
|
174
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;
|
|
175
200
|
/** Minimum PowDB server version the networked transport requires. */
|
|
176
201
|
exports.MIN_POWDB_VERSION = '0.7.0';
|
|
177
202
|
/**
|
|
@@ -222,19 +247,115 @@ function assertSupportedPowdbVersion(version) {
|
|
|
222
247
|
throw new errors_js_1.ConnectionError(`[turbine] turbine-orm/powdb requires PowDB >= ${exports.MIN_POWDB_VERSION}; the server reports "${version}". ` +
|
|
223
248
|
'Upgrade the PowDB server (0.7.0 added the `returning` keyword and the int->float coercion fix Turbine relies on).');
|
|
224
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
|
+
}
|
|
225
343
|
/**
|
|
226
344
|
* Map a Turbine column to the PowQL DDL type used in `defineSchema` →
|
|
227
345
|
* `type T { … }`. Turbine never emits PowDB's `uuid`/`datetime`/`bytes` types,
|
|
228
346
|
* which cannot hold client-supplied values on the wire (no literal, no cast):
|
|
229
347
|
* - `Date` → `int` (epoch micros) - `boolean` → `bool`
|
|
230
348
|
* - integral `number`/`bigint` → `int` - fractional `number` → `float`
|
|
349
|
+
* - JSON / object columns → `json` (native PowDB document type, ≥ 0.12)
|
|
231
350
|
* - everything else (incl. UUID/PK strings) → `str`
|
|
232
|
-
* Array
|
|
351
|
+
* Array (non-json) and bytes columns throw, they have no PowDB equivalent.
|
|
233
352
|
*/
|
|
234
353
|
function powqlColumnType(col) {
|
|
235
354
|
if (col.isArray) {
|
|
236
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.`);
|
|
237
356
|
}
|
|
357
|
+
if (isJsonColumn(col))
|
|
358
|
+
return 'json';
|
|
238
359
|
const ts = col.tsType.replace(/\s*\|\s*null$/i, '').trim();
|
|
239
360
|
if (ts === 'Date')
|
|
240
361
|
return 'int'; // epoch micros
|
|
@@ -249,9 +370,6 @@ function powqlColumnType(col) {
|
|
|
249
370
|
if (ts === 'Buffer' || ts === 'Uint8Array') {
|
|
250
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.`);
|
|
251
372
|
}
|
|
252
|
-
if (/Record<|object|unknown|\[\]|\{/.test(ts)) {
|
|
253
|
-
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.`);
|
|
254
|
-
}
|
|
255
373
|
return 'str';
|
|
256
374
|
}
|
|
257
375
|
/** Heuristic: does this numeric column hold fractional values (→ PowQL `float`)? */
|
|
@@ -391,7 +509,8 @@ function quotePowqlIdent(name) {
|
|
|
391
509
|
}
|
|
392
510
|
return exports.POWQL_KEYWORDS.has(name) || !POWQL_BARE_IDENT.test(name) ? `\`${name}\`` : name;
|
|
393
511
|
}
|
|
394
|
-
function powqlSchemaDDL(schema) {
|
|
512
|
+
function powqlSchemaDDL(schema, opts = {}) {
|
|
513
|
+
const caps = opts.capabilities;
|
|
395
514
|
const stmts = [];
|
|
396
515
|
for (const meta of Object.values(schema.tables)) {
|
|
397
516
|
const pkSet = new Set(meta.primaryKey);
|
|
@@ -402,6 +521,12 @@ function powqlSchemaDDL(schema) {
|
|
|
402
521
|
// cannot enforce the tuple's uniqueness at the engine level.
|
|
403
522
|
const pkIsSingle = meta.primaryKey.length === 1;
|
|
404
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');
|
|
405
530
|
const mods = [];
|
|
406
531
|
if (!col.nullable || pkSet.has(col.name))
|
|
407
532
|
mods.push('required');
|
|
@@ -410,15 +535,57 @@ function powqlSchemaDDL(schema) {
|
|
|
410
535
|
// `auto` = server-generated monotonic int. PowDB requires it be `int` and
|
|
411
536
|
// rejects it alongside a `default`; non-int generated columns fall back to
|
|
412
537
|
// a plain typed column (Turbine assigns the value client-side instead).
|
|
413
|
-
if (col.isGenerated &&
|
|
538
|
+
if (col.isGenerated && powqlType === 'int')
|
|
414
539
|
mods.push('auto');
|
|
415
|
-
return ` ${mods.join(' ')}${mods.length ? ' ' : ''}${quotePowqlIdent(col.name)}: ${
|
|
540
|
+
return ` ${mods.join(' ')}${mods.length ? ' ' : ''}${quotePowqlIdent(col.name)}: ${powqlType}`;
|
|
416
541
|
});
|
|
417
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]);
|
|
418
549
|
// Secondary unique constraints (beyond the PK) become unique indexes.
|
|
419
550
|
for (const uniq of meta.uniqueColumns) {
|
|
420
551
|
if (uniq.length === 1 && !pkSet.has(uniq[0])) {
|
|
421
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);
|
|
422
589
|
}
|
|
423
590
|
}
|
|
424
591
|
}
|
|
@@ -428,6 +595,10 @@ function powqlSchemaDDL(schema) {
|
|
|
428
595
|
function toPowdbParam(value, col) {
|
|
429
596
|
if (value instanceof PowdbFloatParam)
|
|
430
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);
|
|
431
602
|
if (value === undefined || value === null)
|
|
432
603
|
return null;
|
|
433
604
|
if (value instanceof Date)
|
|
@@ -450,10 +621,23 @@ function toPowdbParam(value, col) {
|
|
|
450
621
|
*/
|
|
451
622
|
function coerceValue(raw, col) {
|
|
452
623
|
const ts = col.tsType.replace(/\s*\|\s*null$/i, '').trim();
|
|
624
|
+
const json = isJsonColumn(col);
|
|
453
625
|
// NULL bareword: unambiguous for non-string columns; for `str` we cannot tell a
|
|
454
626
|
// literal "null" from SQL NULL, so a nullable str of value "null" reads as null.
|
|
455
|
-
|
|
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))
|
|
456
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
|
+
}
|
|
457
641
|
if (ts === 'Date') {
|
|
458
642
|
const micros = Number(raw);
|
|
459
643
|
return Number.isFinite(micros) ? new Date(micros / 1000) : null;
|
|
@@ -470,19 +654,62 @@ function coerceValue(raw, col) {
|
|
|
470
654
|
return raw; // string / uuid-as-string
|
|
471
655
|
}
|
|
472
656
|
/**
|
|
473
|
-
*
|
|
474
|
-
* {@link
|
|
475
|
-
*
|
|
476
|
-
*
|
|
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`.
|
|
477
696
|
*/
|
|
478
|
-
function rowToEntity(raw, meta) {
|
|
697
|
+
function rowToEntity(raw, meta, native = false) {
|
|
479
698
|
const byName = new Map(meta.columns.map((c) => [c.name, c]));
|
|
480
699
|
const out = {};
|
|
481
700
|
for (const snake of Object.keys(raw)) {
|
|
482
701
|
const col = byName.get(snake);
|
|
483
702
|
const field = meta.reverseColumnMap[snake] ?? snake;
|
|
484
703
|
const value = raw[snake];
|
|
485
|
-
|
|
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
|
+
}
|
|
486
713
|
}
|
|
487
714
|
return out;
|
|
488
715
|
}
|
|
@@ -508,7 +735,7 @@ function wrapPowdbError(err) {
|
|
|
508
735
|
const e = err;
|
|
509
736
|
const msg = e.message ?? 'unknown PowDB error';
|
|
510
737
|
// Unique-constraint — message-based on both transports.
|
|
511
|
-
if (/unique constraint violation/i.test(msg)) {
|
|
738
|
+
if (/unique (constraint|expression index) violation/i.test(msg)) {
|
|
512
739
|
const m = /on\s+\S+\.(\w+)/i.exec(msg);
|
|
513
740
|
return new errors_js_1.UniqueConstraintError({ constraint: m?.[1], cause: err });
|
|
514
741
|
}
|
|
@@ -521,37 +748,71 @@ function wrapPowdbError(err) {
|
|
|
521
748
|
// Driver pool lifecycle errors (acquire after close, acquire timeout) carry
|
|
522
749
|
// no .code — classify by message so both transports surface E004.
|
|
523
750
|
if (/pool closed|pool acquire timeout/i.test(msg)) {
|
|
524
|
-
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 });
|
|
525
752
|
}
|
|
526
753
|
// Server-side transaction-gate wait bound (PowDB ≥ 0.10, default 5s): another
|
|
527
754
|
// connection held the single global write lock past the server's
|
|
528
755
|
// --tx-wait-timeout-ms. Retryable timeout, not a query defect.
|
|
529
756
|
if (/transaction gate timeout/i.test(msg)) {
|
|
530
|
-
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 });
|
|
531
771
|
}
|
|
532
|
-
// Type mismatch / parse / execution / storage / unexpected
|
|
533
|
-
// (E003). On the embedded transport these are the only
|
|
534
|
-
// (code is always 'GenericFailure'); on the networked path they
|
|
535
|
-
// 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.
|
|
536
776
|
if (/type mismatch|\bParse\b|\bExecution\b|StorageError|unexpected|row too large/i.test(msg)) {
|
|
537
777
|
return new errors_js_1.ValidationError(`[turbine] PowDB query rejected: ${msg}`);
|
|
538
778
|
}
|
|
539
779
|
switch (e.code) {
|
|
540
780
|
case 'connect_failed':
|
|
541
781
|
case 'closed':
|
|
542
|
-
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 });
|
|
543
788
|
case 'timeout':
|
|
544
789
|
case 'aborted':
|
|
545
|
-
return new errors_js_1.TimeoutError(0, 'PowDB query');
|
|
790
|
+
return new errors_js_1.TimeoutError(0, 'PowDB query', { cause: err });
|
|
546
791
|
case 'query_failed':
|
|
547
792
|
case 'type_coercion_failed':
|
|
548
|
-
case 'protocol_error':
|
|
549
793
|
case 'size_exceeded':
|
|
550
794
|
return new errors_js_1.ValidationError(`[turbine] PowDB query rejected: ${msg}`);
|
|
551
795
|
default:
|
|
552
|
-
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 });
|
|
553
797
|
}
|
|
554
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
|
+
}
|
|
555
816
|
function normalizeQueryArgs(arg, values) {
|
|
556
817
|
if (typeof arg === 'string')
|
|
557
818
|
return { text: arg, params: values ?? [] };
|
|
@@ -730,7 +991,7 @@ class PowdbTxGate {
|
|
|
730
991
|
return hold;
|
|
731
992
|
}
|
|
732
993
|
}
|
|
733
|
-
/** 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. */
|
|
734
995
|
function adaptResult(r) {
|
|
735
996
|
switch (r.kind) {
|
|
736
997
|
case 'rows': {
|
|
@@ -741,21 +1002,86 @@ function adaptResult(r) {
|
|
|
741
1002
|
});
|
|
742
1003
|
return o;
|
|
743
1004
|
});
|
|
744
|
-
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 };
|
|
745
1006
|
}
|
|
746
1007
|
case 'ok':
|
|
747
|
-
return { rows: [], rowCount: Number(r.affected), fields: [] };
|
|
1008
|
+
return { rows: [], rowCount: Number(r.affected), fields: [], native: false };
|
|
748
1009
|
case 'scalar':
|
|
749
|
-
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 };
|
|
750
1011
|
default:
|
|
751
|
-
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 };
|
|
752
1074
|
}
|
|
753
1075
|
}
|
|
754
1076
|
/**
|
|
755
1077
|
* A {@link PgCompatPool} backed by a `@zvndev/powdb-client` `Pool`. The query
|
|
756
|
-
* `text` is **PowQL**, not SQL
|
|
757
|
-
*
|
|
758
|
-
* (
|
|
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.
|
|
759
1085
|
*/
|
|
760
1086
|
class PowdbPool {
|
|
761
1087
|
pool;
|
|
@@ -779,10 +1105,29 @@ class PowdbPool {
|
|
|
779
1105
|
* live socket holding the process open until the server's idle timeout.
|
|
780
1106
|
*/
|
|
781
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;
|
|
782
1112
|
constructor(pool, toParam = (v) => toPowdbParam(v), options = {}) {
|
|
783
1113
|
this.pool = pool;
|
|
784
1114
|
this.toParam = toParam;
|
|
785
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));
|
|
786
1131
|
}
|
|
787
1132
|
// biome-ignore lint/suspicious/noExplicitAny: pg-compat query is generic over the row shape.
|
|
788
1133
|
async query(text, values) {
|
|
@@ -803,8 +1148,7 @@ class PowdbPool {
|
|
|
803
1148
|
return { rows: [], rowCount: 0, fields: [] };
|
|
804
1149
|
}
|
|
805
1150
|
try {
|
|
806
|
-
|
|
807
|
-
return adaptResult(result);
|
|
1151
|
+
return await this.pool.withClient((c) => this.runOnClient(c, powql, params));
|
|
808
1152
|
}
|
|
809
1153
|
catch (err) {
|
|
810
1154
|
if (ctl === 'begin') {
|
|
@@ -875,7 +1219,7 @@ class PowdbPool {
|
|
|
875
1219
|
return { rows: [], rowCount: 0, fields: [] };
|
|
876
1220
|
}
|
|
877
1221
|
try {
|
|
878
|
-
return
|
|
1222
|
+
return await this.runOnClient(client, powql, params);
|
|
879
1223
|
}
|
|
880
1224
|
catch (err) {
|
|
881
1225
|
broken = true;
|
|
@@ -1002,6 +1346,10 @@ function encodePowqlLiteral(value) {
|
|
|
1002
1346
|
// Force a float-form literal so an integer-valued float column stays a float.
|
|
1003
1347
|
return Number.isInteger(n) ? `${n}.0` : String(n);
|
|
1004
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));
|
|
1005
1353
|
if (value === undefined || value === null)
|
|
1006
1354
|
return 'null';
|
|
1007
1355
|
if (value instanceof Date)
|
|
@@ -1079,9 +1427,19 @@ class PowdbEmbeddedPool {
|
|
|
1079
1427
|
txGate;
|
|
1080
1428
|
/** Hold taken by a `begin` issued via `query()` directly (no checked-out client). */
|
|
1081
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;
|
|
1082
1438
|
constructor(db, options = {}) {
|
|
1083
1439
|
this.db = db;
|
|
1084
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;
|
|
1085
1443
|
}
|
|
1086
1444
|
/** Materialize `$N` params and hand the PowQL to the in-process engine. */
|
|
1087
1445
|
exec(powql, params) {
|
|
@@ -1192,6 +1550,9 @@ exports.PowdbEmbeddedPool = PowdbEmbeddedPool;
|
|
|
1192
1550
|
// ---------------------------------------------------------------------------
|
|
1193
1551
|
// PowqlInterface — the PowQL query generator (Phase A: flat CRUD via returning)
|
|
1194
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; } });
|
|
1195
1556
|
var powql_js_1 = require("./powql.js");
|
|
1196
1557
|
Object.defineProperty(exports, "PowqlInterface", { enumerable: true, get: function () { return powql_js_1.PowqlInterface; } });
|
|
1197
1558
|
/**
|
|
@@ -1239,8 +1600,20 @@ async function loadPowdbEmbedded() {
|
|
|
1239
1600
|
}
|
|
1240
1601
|
return mod;
|
|
1241
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
|
+
}
|
|
1242
1615
|
/** Open an embedded database handle, wrapping engine open failures (corrupt dir, etc.). */
|
|
1243
|
-
async function openEmbeddedPool(target, poolOptions = {}) {
|
|
1616
|
+
async function openEmbeddedPool(target, poolOptions = {}, assumeEngineVersion) {
|
|
1244
1617
|
const mod = await loadPowdbEmbedded();
|
|
1245
1618
|
const { embedded: dir, syncMode, memoryLimit } = target;
|
|
1246
1619
|
let db;
|
|
@@ -1266,7 +1639,11 @@ async function openEmbeddedPool(target, poolOptions = {}) {
|
|
|
1266
1639
|
}
|
|
1267
1640
|
db.setSyncMode(syncMode);
|
|
1268
1641
|
}
|
|
1269
|
-
|
|
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 });
|
|
1270
1647
|
}
|
|
1271
1648
|
/**
|
|
1272
1649
|
* Bind Turbine to PowDB. `target` is one of:
|
|
@@ -1289,30 +1666,38 @@ async function openEmbeddedPool(target, poolOptions = {}) {
|
|
|
1289
1666
|
async function turbinePowDB(target, schema, options = {}) {
|
|
1290
1667
|
let pool;
|
|
1291
1668
|
let owns = false;
|
|
1292
|
-
const poolOptions = {
|
|
1669
|
+
const poolOptions = {
|
|
1670
|
+
transactionQueueTimeoutMs: options.transactionQueueTimeoutMs,
|
|
1671
|
+
retryStaleReads: options.retryStaleReads,
|
|
1672
|
+
};
|
|
1673
|
+
const max = options.connectionLimit ?? 10;
|
|
1293
1674
|
if (typeof target === 'string') {
|
|
1294
1675
|
const mod = options.powdbClientModule ?? (await loadPowdb());
|
|
1295
|
-
const clientPool = new mod.Pool({ ...parsePowdbUrl(target), max
|
|
1296
|
-
await assertNetworkedVersion(clientPool);
|
|
1297
|
-
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 });
|
|
1298
1679
|
owns = true;
|
|
1299
1680
|
}
|
|
1300
1681
|
else if (target instanceof PowdbPool) {
|
|
1301
|
-
// An injected PowdbPool carries its own PowdbPoolOptions.
|
|
1682
|
+
// An injected PowdbPool carries its own PowdbPoolOptions (incl. capabilities).
|
|
1302
1683
|
pool = target;
|
|
1303
1684
|
}
|
|
1304
1685
|
else if (isEmbeddedTarget(target)) {
|
|
1305
|
-
pool = await openEmbeddedPool(target, poolOptions);
|
|
1686
|
+
pool = await openEmbeddedPool(target, poolOptions, options.assumeEngineVersion);
|
|
1306
1687
|
owns = true;
|
|
1307
1688
|
}
|
|
1308
1689
|
else if (isPowdbClientPool(target)) {
|
|
1309
|
-
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 });
|
|
1310
1695
|
}
|
|
1311
1696
|
else {
|
|
1312
1697
|
const mod = options.powdbClientModule ?? (await loadPowdb());
|
|
1313
|
-
const clientPool = new mod.Pool({ ...target, max
|
|
1314
|
-
await assertNetworkedVersion(clientPool);
|
|
1315
|
-
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 });
|
|
1316
1701
|
owns = true;
|
|
1317
1702
|
}
|
|
1318
1703
|
// The PowQL generator is loaded here to keep client.ts free of any PowDB import.
|
|
@@ -1350,14 +1735,20 @@ async function turbinePowDB(target, schema, options = {}) {
|
|
|
1350
1735
|
return client;
|
|
1351
1736
|
}
|
|
1352
1737
|
/**
|
|
1353
|
-
* Probe a networked pool's `serverVersion` (declared on {@link PowdbClient})
|
|
1354
|
-
* fail fast if the server is older than {@link MIN_POWDB_VERSION}
|
|
1355
|
-
*
|
|
1356
|
-
*
|
|
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.
|
|
1357
1745
|
*/
|
|
1358
|
-
async function assertNetworkedVersion(clientPool) {
|
|
1359
|
-
|
|
1746
|
+
async function assertNetworkedVersion(clientPool, assumeEngineVersion) {
|
|
1747
|
+
return clientPool.withClient(async (c) => {
|
|
1360
1748
|
assertSupportedPowdbVersion(c.serverVersion);
|
|
1749
|
+
const version = assumeEngineVersion ?? c.serverVersion;
|
|
1750
|
+
const hasNativeRaw = typeof c.queryNativeRaw === 'function';
|
|
1751
|
+
return capabilitiesFromVersion(version, { hasNativeRaw });
|
|
1361
1752
|
});
|
|
1362
1753
|
}
|
|
1363
1754
|
function isPowdbClientPool(x) {
|