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.
Files changed (43) hide show
  1. package/README.md +2 -2
  2. package/dist/cjs/dialect.js +1 -0
  3. package/dist/cjs/index-advisor.js +0 -0
  4. package/dist/cjs/index.js +2 -1
  5. package/dist/cjs/mssql.js +3 -0
  6. package/dist/cjs/mysql.js +3 -0
  7. package/dist/cjs/optional-peer-import.cjs +28 -0
  8. package/dist/cjs/powdb-introspect.js +222 -0
  9. package/dist/cjs/powdb.js +446 -55
  10. package/dist/cjs/powql.js +566 -111
  11. package/dist/cjs/query/builder.js +136 -53
  12. package/dist/cjs/query/filters.js +4 -4
  13. package/dist/cjs/schema-builder.js +16 -0
  14. package/dist/cjs/schema-metadata.js +81 -10
  15. package/dist/cjs/sqlite.js +2 -0
  16. package/dist/dialect.d.ts +7 -0
  17. package/dist/dialect.js +1 -0
  18. package/dist/index-advisor.d.ts +15 -1
  19. package/dist/index-advisor.js +0 -0
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +1 -1
  22. package/dist/mssql.js +3 -0
  23. package/dist/mysql.js +3 -0
  24. package/dist/optional-peer-import.cjs +28 -0
  25. package/dist/optional-peer-import.d.cts +19 -0
  26. package/dist/powdb-introspect.d.ts +84 -0
  27. package/dist/powdb-introspect.js +219 -0
  28. package/dist/powdb.d.ts +249 -13
  29. package/dist/powdb.js +438 -54
  30. package/dist/powql.d.ts +113 -6
  31. package/dist/powql.js +568 -113
  32. package/dist/query/builder.d.ts +11 -0
  33. package/dist/query/builder.js +136 -53
  34. package/dist/query/filters.d.ts +3 -3
  35. package/dist/query/filters.js +4 -4
  36. package/dist/query/types.d.ts +50 -6
  37. package/dist/schema-builder.d.ts +46 -1
  38. package/dist/schema-builder.js +15 -0
  39. package/dist/schema-metadata.d.ts +13 -7
  40. package/dist/schema-metadata.js +82 -11
  41. package/dist/schema.d.ts +25 -0
  42. package/dist/sqlite.js +2 -0
  43. 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 / JSON / bytes columns throw they have no PowDB equivalent.
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 && powqlColumnType(col) === 'int')
538
+ if (col.isGenerated && powqlType === 'int')
414
539
  mods.push('auto');
415
- return ` ${mods.join(' ')}${mods.length ? ' ' : ''}${quotePowqlIdent(col.name)}: ${powqlColumnType(col)}`;
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
- if (raw === 'null' && (ts !== 'string' || col.nullable))
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
- * Map one raw PowDB row (snake-cased columns raw wire strings, as produced by
474
- * {@link PowdbPool}) into a typed entity (camelCase fields, coerced values).
475
- * Only the columns present in `raw` are emitted, so partial `select` projections
476
- * round-trip unchanged.
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
- out[field] = col && typeof value === 'string' ? coerceValue(value, col) : value;
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 validation
533
- // (E003). On the embedded transport these are the only signal we get
534
- // (code is always 'GenericFailure'); on the networked path they are a
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 {@link PowqlInterface} generates it. Rows come
757
- * back as raw strings here; per-column JS coercion happens in `PowqlInterface`
758
- * (it owns the schema metadata).
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
- const result = await this.pool.withClient((c) => c.query(powql, params.map(this.toParam)));
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 adaptResult(await client.query(powql, params.map(this.toParam)));
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
- return new PowdbEmbeddedPool(db, poolOptions);
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 = { transactionQueueTimeoutMs: options.transactionQueueTimeoutMs };
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: options.connectionLimit ?? 10 });
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 = new PowdbPool(target, undefined, poolOptions);
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: options.connectionLimit ?? 10 });
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}) and
1354
- * fail fast if the server is older than {@link MIN_POWDB_VERSION}. Best-effort:
1355
- * a driver that does not surface a version is left untouched (we cannot prove it
1356
- * too old). Errors from the probe itself surface as the normal connect failure.
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
- await clientPool.withClient(async (c) => {
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) {