turbine-orm 0.76.0 → 0.77.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 (87) hide show
  1. package/dist/cjs/cli/index.js +2 -2
  2. package/dist/cjs/cli/migrate.js +8 -8
  3. package/dist/cjs/cli/studio.js +6 -3
  4. package/dist/cjs/client.js +10 -10
  5. package/dist/cjs/dialect.js +1 -1
  6. package/dist/cjs/errors.d.ts +2 -2
  7. package/dist/cjs/errors.js +21 -25
  8. package/dist/cjs/generate.js +1 -1
  9. package/dist/cjs/introspect.js +5 -5
  10. package/dist/cjs/mssql.js +12 -12
  11. package/dist/cjs/mysql.js +5 -5
  12. package/dist/cjs/nested-write.js +31 -31
  13. package/dist/cjs/observe.js +1 -1
  14. package/dist/cjs/pipeline-submittable.js +5 -1
  15. package/dist/cjs/pipeline.js +8 -0
  16. package/dist/cjs/powdb-introspect.d.ts +1 -1
  17. package/dist/cjs/powdb-introspect.js +4 -4
  18. package/dist/cjs/powdb-shared.d.ts +348 -0
  19. package/dist/cjs/powdb-shared.js +587 -0
  20. package/dist/cjs/powdb.d.ts +12 -299
  21. package/dist/cjs/powdb.js +106 -567
  22. package/dist/cjs/powql.d.ts +1 -1
  23. package/dist/cjs/powql.js +89 -89
  24. package/dist/cjs/prisma-compat.js +16 -16
  25. package/dist/cjs/query/aggregates.js +19 -20
  26. package/dist/cjs/query/batched-loader.js +8 -8
  27. package/dist/cjs/query/builder.js +8 -8
  28. package/dist/cjs/query/compound-unique.js +2 -2
  29. package/dist/cjs/query/filters.js +1 -1
  30. package/dist/cjs/query/relations.js +60 -48
  31. package/dist/cjs/query/types.js +4 -4
  32. package/dist/cjs/query/utils.js +5 -5
  33. package/dist/cjs/query/warn-registry.d.ts +7 -0
  34. package/dist/cjs/query/warn-registry.js +7 -0
  35. package/dist/cjs/query/where-compile.js +1 -1
  36. package/dist/cjs/query/where.js +30 -31
  37. package/dist/cjs/query/writes.js +7 -7
  38. package/dist/cjs/realtime.js +4 -4
  39. package/dist/cjs/schema-metadata.js +1 -1
  40. package/dist/cjs/schema-sql.js +3 -3
  41. package/dist/cjs/seed.js +1 -1
  42. package/dist/cjs/sqlite.js +7 -7
  43. package/dist/cjs/typed-sql.js +1 -1
  44. package/dist/cli/index.js +2 -2
  45. package/dist/cli/migrate.js +8 -8
  46. package/dist/cli/studio.js +6 -3
  47. package/dist/client.js +10 -10
  48. package/dist/dialect.js +1 -1
  49. package/dist/errors.d.ts +2 -2
  50. package/dist/errors.js +21 -25
  51. package/dist/generate.js +1 -1
  52. package/dist/introspect.js +5 -5
  53. package/dist/mssql.js +12 -12
  54. package/dist/mysql.js +5 -5
  55. package/dist/nested-write.js +31 -31
  56. package/dist/observe.js +1 -1
  57. package/dist/pipeline-submittable.js +6 -2
  58. package/dist/pipeline.js +9 -1
  59. package/dist/powdb-introspect.d.ts +1 -1
  60. package/dist/powdb-introspect.js +2 -2
  61. package/dist/powdb-shared.d.ts +348 -0
  62. package/dist/powdb-shared.js +570 -0
  63. package/dist/powdb.d.ts +12 -299
  64. package/dist/powdb.js +71 -534
  65. package/dist/powql.d.ts +1 -1
  66. package/dist/powql.js +43 -43
  67. package/dist/prisma-compat.js +16 -16
  68. package/dist/query/aggregates.js +19 -20
  69. package/dist/query/batched-loader.js +8 -8
  70. package/dist/query/builder.js +8 -8
  71. package/dist/query/compound-unique.js +2 -2
  72. package/dist/query/filters.js +1 -1
  73. package/dist/query/relations.js +60 -48
  74. package/dist/query/types.js +4 -4
  75. package/dist/query/utils.js +5 -5
  76. package/dist/query/warn-registry.d.ts +7 -0
  77. package/dist/query/warn-registry.js +7 -0
  78. package/dist/query/where-compile.js +1 -1
  79. package/dist/query/where.js +30 -31
  80. package/dist/query/writes.js +7 -7
  81. package/dist/realtime.js +4 -4
  82. package/dist/schema-metadata.js +1 -1
  83. package/dist/schema-sql.js +3 -3
  84. package/dist/seed.js +1 -1
  85. package/dist/sqlite.js +7 -7
  86. package/dist/typed-sql.js +1 -1
  87. package/package.json +15 -7
package/dist/powdb.js CHANGED
@@ -57,8 +57,15 @@ import { TurbineClient, } from './client.js';
57
57
  import { postgresDialect } from './dialect.js';
58
58
  import { ConnectionError, malformedConnectionStringMessage, NotNullViolationError, ReadOnlyError, TimeoutError, UniqueConstraintError, UnsupportedFeatureError, ValidationError, } from './errors.js';
59
59
  import importOptionalPeer from './optional-peer-import.cjs';
60
+ import { ALL_POWDB_CAPABILITIES, isDateColumn, PowdbFloatParam, PowdbJsonParam, powqlColumnType, quotePowqlIdent, requireCapability, } from './powdb-shared.js';
60
61
  import { shouldWarnOnce, WARN_NS } from './query/warn-registry.js';
61
62
  import { normalizeKeyColumns } from './schema.js';
63
+ // The shared PowDB primitives live in a leaf module (see powdb-shared.ts): this
64
+ // file re-exports from powql.ts and powdb-introspect.ts, and both of those need
65
+ // the primitives, so keeping them here made the three a runtime cycle. Re-export
66
+ // every name that was public before the split, under its original name, so the
67
+ // `turbine-orm/powdb` surface is unchanged.
68
+ export { ALL_POWDB_CAPABILITIES, coerceNativeValue, coerceValue, isJsonColumn, isPowdbDatetimeColumn, isStaleFramePowdbError, POWQL_KEYWORDS, PowdbFloatParam, PowdbJsonParam, powqlColumnType, quotePowqlDotted, quotePowqlIdent, requireCapability, rowToEntity, } from './powdb-shared.js';
62
69
  /**
63
70
  * Capability descriptor for PowDB. PowQL generation is owned by
64
71
  * {@link PowqlInterface} (not the SQL `Dialect`), so this dialect exists only to
@@ -117,38 +124,6 @@ function throwNoNestedTransaction() {
117
124
  throw new UnsupportedFeatureError('nested transactions', 'powdb', 'PowDB is single-writer, it has one global write lock and no savepoints. ' +
118
125
  'Complete the open transaction before starting another; do not nest `$transaction` calls.');
119
126
  }
120
- /**
121
- * Marker wrapper for a value bound to a `float` column. The networked driver
122
- * unwraps it to the plain number (the wire param is unchanged), but the
123
- * *embedded* literal encoder reads it to emit a float-form PowQL literal (`42`
124
- * → `42.0`) so an integer-valued float column stays unambiguously a float.
125
- * Constructed in {@link PowqlInterface.param}.
126
- */
127
- export class PowdbFloatParam {
128
- value;
129
- constructor(value) {
130
- this.value = value;
131
- }
132
- }
133
- /**
134
- * Marker wrapper for a JS object/array bound to a `json` document column. Both
135
- * transports serialize `value` with `JSON.stringify` and send the text as a
136
- * `str` param / string literal, exactly how the PowDB docs insert a json
137
- * document (the engine validates it as JSON text and stores the canonical
138
- * binary form). Constructed in {@link PowqlInterface.param} when the target
139
- * column is `json` and the value is a non-null object/array; a JS string
140
- * written to a json column passes through RAW (same contract as pg jsonb,
141
- * pass `'"x"'` to store the JSON string `"x"`), and `null` stays `null`.
142
- */
143
- export class PowdbJsonParam {
144
- value;
145
- column;
146
- /** `column` is diagnostic only: it names the target column when serialization fails. */
147
- constructor(value, column) {
148
- this.value = value;
149
- this.column = column;
150
- }
151
- }
152
127
  /** Minimum PowDB server version the networked transport requires. */
153
128
  export const MIN_POWDB_VERSION = '0.7.0';
154
129
  /**
@@ -166,7 +141,7 @@ export function parsePowdbUrl(connectionString) {
166
141
  throw new ConnectionError(malformedConnectionStringMessage('PowDB', 'powdb://user:password@127.0.0.1:5433/app'));
167
142
  }
168
143
  if (u.protocol !== 'powdb:') {
169
- throw new ConnectionError(`[turbine] PowDB connection string must use the powdb:// scheme (got "${u.protocol}//…").`);
144
+ throw new ConnectionError(`PowDB connection string must use the powdb:// scheme (got "${u.protocol}//…").`);
170
145
  }
171
146
  const opts = {
172
147
  host: u.hostname || '127.0.0.1',
@@ -197,9 +172,12 @@ export function assertSupportedPowdbVersion(version) {
197
172
  // 0.7.0 is the floor; >= 0.7 (or any 1.x+) passes.
198
173
  if (major > 0 || (major === 0 && minor >= 7))
199
174
  return;
200
- throw new ConnectionError(`[turbine] turbine-orm/powdb requires PowDB >= ${MIN_POWDB_VERSION}; the server reports "${version}". ` +
175
+ throw new ConnectionError(`turbine-orm/powdb requires PowDB >= ${MIN_POWDB_VERSION}; the server reports "${version}". ` +
201
176
  'Upgrade the PowDB server (0.7.0 added the `returning` keyword and the int->float coercion fix Turbine relies on).');
202
177
  }
178
+ // ---------------------------------------------------------------------------
179
+ // Capability gating: per-version / per-transport feature flags
180
+ // ---------------------------------------------------------------------------
203
181
  /**
204
182
  * PowQL's parser bounds the SHAPE of the AST it produces, not just its own
205
183
  * recursion: an `and` / `or` chain is parsed iteratively but re-wraps its
@@ -228,66 +206,6 @@ export function assertSupportedPowdbVersion(version) {
228
206
  * powql.ts.
229
207
  */
230
208
  export const POWQL_MAX_NESTING_DEPTH = 64;
231
- /**
232
- * Minimum engine version each gated feature needs, for the E017 hint text.
233
- * Most gates carry a `major.minor` floor (patch-insensitive); the two link
234
- * lanes carry a `major.minor.patch` floor (`0.19.1`) because the listing
235
- * statement and the safe traversal semantics landed in the PATCH release, not
236
- * in 0.19.0. {@link atLeastVersion} compares all three components, so a
237
- * `major.minor` floor still matches every patch of that minor.
238
- */
239
- const POWDB_FEATURE_MIN_VERSION = {
240
- introspection: '0.10',
241
- jsonDocs: '0.12',
242
- docFieldIndexes: '0.13',
243
- serverJoins: '0.13',
244
- nestedProjections: '0.18',
245
- entityLinks: '0.19',
246
- linkIntrospection: '0.19.1',
247
- linkPaths: '0.19.1',
248
- datetimeCompare: '0.20',
249
- projectedCountNonNull: '0.20',
250
- };
251
- /**
252
- * Trusted-caller default: every FEATURE gate on, engine version unknown. Used
253
- * for a directly-constructed {@link PowdbPool} / {@link PowdbEmbeddedPool} that
254
- * did not go through {@link turbinePowDB}'s version probe (e.g. an injected
255
- * pool, or a unit-test pool). `nativeRaw` stays OFF here because it flips the
256
- * actual wire path and must only be enabled after a real server-version probe,
257
- * never inferred from a bare construction. `nestedProjections` stays OFF for
258
- * the same reason: it changes the generated PowQL for every `with` query, and
259
- * an unprobed engine below 0.18 would reject the syntax outright.
260
- * `entityLinks` stays OFF for a stronger reason still: declaring a link
261
- * one-way-upgrades the on-disk catalog to v7 and locks out pre-0.19 binaries,
262
- * so it must only ever light up behind a real version probe.
263
- * `linkIntrospection` / `linkPaths` stay OFF for the same probe-only discipline:
264
- * `linkPaths` flips real query generation (a to-one `with` compiling to link
265
- * projections), and `linkIntrospection` is only meaningful once genuinely
266
- * probed, so both must come from a real version resolution, never a bare
267
- * construction.
268
- * `datetimeCompare` / `projectedCountNonNull` stay ON here for the same
269
- * trusted-caller reason as `jsonDocs` and `serverJoins`. Neither is a fallback
270
- * gate: with the flag OFF the affected query is REFUSED, not served by some
271
- * other statement, so defaulting them off would break working queries rather
272
- * than protect anything. Every path that can learn the engine version
273
- * (`turbinePowDB`, embedded or networked) resolves them from a real probe; this
274
- * fallback only covers a hand-constructed or injected pool, whose owner is
275
- * asserting the engine is current.
276
- */
277
- export const ALL_POWDB_CAPABILITIES = {
278
- engineVersion: null,
279
- jsonDocs: true,
280
- docFieldIndexes: true,
281
- introspection: true,
282
- serverJoins: true,
283
- nestedProjections: false,
284
- entityLinks: false,
285
- linkIntrospection: false,
286
- linkPaths: false,
287
- datetimeCompare: true,
288
- projectedCountNonNull: true,
289
- nativeRaw: false,
290
- };
291
209
  /** Parse a PowDB semver prefix (`0.13.0`, `0.13`, `1.2.3-rc`) into components, or `null`. */
292
210
  function parsePowdbSemver(version) {
293
211
  const m = /^(\d+)\.(\d+)(?:\.(\d+))?/.exec(String(version ?? '').trim());
@@ -352,270 +270,6 @@ export function capabilitiesFromVersion(version, opts = {}) {
352
270
  nativeRaw: Boolean(opts.hasNativeRaw) && atLeastVersion(sem, 0, 13),
353
271
  };
354
272
  }
355
- /**
356
- * Throw a version-hinting {@link UnsupportedFeatureError} (E017) when a gated
357
- * PowQL feature is used on an engine that does not support it. Keeps old engines
358
- * getting clean typed errors instead of raw PowQL parse failures.
359
- *
360
- * The error's first sentence already names the feature (`<feature> is
361
- * unsupported on "PowDB".`), so the hint says "Requires PowDB >= x" rather than
362
- * repeating the label: a long feature description read twice in one message
363
- * (`per-field \`_count\` … is unsupported … per-field \`_count\` … requires …`)
364
- * buries the version floor that is the actionable part.
365
- *
366
- * `extra` appends one more sentence for gates that have a workaround worth
367
- * naming (e.g. the read path that answers the same query without the gated
368
- * comparison).
369
- */
370
- export function requireCapability(caps, key, feature, extra) {
371
- if (caps[key])
372
- return;
373
- const min = POWDB_FEATURE_MIN_VERSION[key];
374
- const reported = caps.engineVersion
375
- ? `this connection reports ${caps.engineVersion}`
376
- : 'this connection could not report a version';
377
- throw new UnsupportedFeatureError(feature, 'PowDB', `Requires PowDB >= ${min}; ${reported}. Upgrade powdb-server / @zvndev/powdb-embedded ` +
378
- `(or pass \`assumeEngineVersion\` if the version cannot be detected).${extra ? ` ${extra}` : ''}`);
379
- }
380
- /**
381
- * Does this column map to PowDB's native `json` document type? A Postgres
382
- * `json`/`jsonb` type (via `dialectType`/`pgType`) is authoritative; otherwise
383
- * the tsType heuristic (`Record<…>`, `object`, `unknown`, an object/array
384
- * literal) that the four scalar branches do not claim. Array columns never map
385
- * to json, a PowDB array only exists INSIDE a json document, so a Postgres
386
- * array column has no PowDB shape and still throws in {@link powqlColumnType}.
387
- */
388
- export function isJsonColumn(col) {
389
- if (col.isArray)
390
- return false;
391
- const dbType = (col.dialectType ?? col.pgType ?? '').toLowerCase();
392
- if (dbType === 'json' || dbType === 'jsonb')
393
- return true;
394
- const ts = col.tsType.replace(/\s*\|\s*null$/i, '').trim();
395
- if (ts === 'Date' || ts === 'boolean' || ts === 'number' || ts === 'bigint' || ts === 'string')
396
- return false;
397
- if (ts === 'Buffer' || ts === 'Uint8Array')
398
- return false;
399
- return /Record<|object|unknown|\[\]|\{/.test(ts);
400
- }
401
- /**
402
- * Map a Turbine column to the PowQL DDL type used in `defineSchema` →
403
- * `type T { … }`. Turbine never emits PowDB's `uuid`/`datetime`/`bytes` types,
404
- * which cannot hold client-supplied values on the wire (no literal, no cast):
405
- * - `Date` → `int` (epoch micros) - `boolean` → `bool`
406
- * - integral `number`/`bigint` → `int` - fractional `number` → `float`
407
- * - JSON / object columns → `json` (native PowDB document type, ≥ 0.12)
408
- * - everything else (incl. UUID/PK strings) → `str`
409
- * Array (non-json) and bytes columns throw, they have no PowDB equivalent.
410
- */
411
- export function powqlColumnType(col) {
412
- if (col.isArray) {
413
- throw new ValidationError(`[turbine] Column "${col.name}" is an array, PowDB has no array type. Arrays are unsupported on the PowDB backend.`);
414
- }
415
- if (isJsonColumn(col))
416
- return 'json';
417
- const ts = col.tsType.replace(/\s*\|\s*null$/i, '').trim();
418
- if (ts === 'Date')
419
- return 'int'; // epoch micros
420
- if (ts === 'boolean')
421
- return 'bool';
422
- if (ts === 'number')
423
- return isFloatColumn(col) ? 'float' : 'int';
424
- if (ts === 'bigint')
425
- return 'int';
426
- if (ts === 'string')
427
- return 'str';
428
- if (ts === 'Buffer' || ts === 'Uint8Array') {
429
- 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.`);
430
- }
431
- return 'str';
432
- }
433
- /** Heuristic: does this numeric column hold fractional values (→ PowQL `float`)? */
434
- function isFloatColumn(col) {
435
- const t = (col.dialectType ?? col.pgType ?? '').toLowerCase();
436
- return /float|double|real|numeric|decimal|money/.test(t);
437
- }
438
- /** Is a column stored as `int` epoch micros but surfaced as a JS `Date`? */
439
- function isDateColumn(col) {
440
- return col.tsType.replace(/\s*\|\s*null$/i, '').trim() === 'Date';
441
- }
442
- /**
443
- * Is this column stored in PowDB's NATIVE `datetime` type (as opposed to the
444
- * `int` epoch micros Turbine's own DDL emits for a `Date` column)?
445
- *
446
- * Only the literal PowQL type name counts. `powqlColumnType` never returns
447
- * `datetime`, so a Turbine-provisioned table can never have one; the shapes that
448
- * do are a table created outside Turbine and read back through
449
- * `introspectPowdbDatabase` (which maps `datetime` → `{ tsType: 'Date',
450
- * dialectType: 'datetime' }`), or hand-written metadata declaring it. Deliberately
451
- * strict: a Postgres-sourced `timestamptz` column is DDL'd as PowQL `int`, so it
452
- * is NOT a PowDB datetime and must not be caught here.
453
- *
454
- * Matters because comparing a datetime column against the integer timestamp
455
- * literal Turbine binds was silently wrong below engine 0.20 (see
456
- * {@link PowdbCapabilities.datetimeCompare}).
457
- */
458
- export function isPowdbDatetimeColumn(col) {
459
- return (col.dialectType ?? col.pgType ?? '').toLowerCase() === 'datetime';
460
- }
461
- /**
462
- * Generate PowQL DDL (`type T { … }`) for every table in a schema. Used to
463
- * provision a PowDB database from a code-first `defineSchema`/`SchemaMetadata`
464
- * (PowDB has no migration runner yet). The primary key column is declared
465
- * `required unique`; non-nullable columns are `required`. A server-generated
466
- * column ({@link ColumnMetadata.isGenerated}) that maps to PowQL `int` gets the
467
- * `auto` modifier, so PowDB assigns a monotonic id on insert and Turbine stops
468
- * synthesizing a client-side value for it.
469
- */
470
- /**
471
- * PowQL reserved words, the v0.10 lexer keyword table from POWQL.md's
472
- * "Reserved Words and Quoting" section, including the v0.10 additions
473
- * `schema` and `describe`. Keyword matching is case-sensitive in the lexer,
474
- * so only the exact lowercase form collides.
475
- */
476
- export const POWQL_KEYWORDS = new Set([
477
- 'abs',
478
- 'add',
479
- 'alter',
480
- 'and',
481
- 'as',
482
- 'asc',
483
- 'auto',
484
- 'avg',
485
- 'begin',
486
- 'between',
487
- 'case',
488
- 'cast',
489
- 'ceil',
490
- 'column',
491
- 'commit',
492
- 'concat',
493
- 'conflict',
494
- 'count',
495
- 'cross',
496
- 'date_add',
497
- 'date_diff',
498
- 'default',
499
- 'delete',
500
- 'dense_rank',
501
- 'desc',
502
- 'describe',
503
- 'distinct',
504
- 'drop',
505
- 'else',
506
- 'end',
507
- 'exists',
508
- 'explain',
509
- 'extract',
510
- 'false',
511
- 'filter',
512
- 'floor',
513
- 'group',
514
- 'having',
515
- 'in',
516
- 'index',
517
- 'inner',
518
- 'insert',
519
- 'is',
520
- 'join',
521
- 'left',
522
- 'length',
523
- 'let',
524
- 'like',
525
- 'limit',
526
- 'link',
527
- 'lower',
528
- 'match',
529
- 'materialize',
530
- 'materialized',
531
- 'max',
532
- 'min',
533
- 'multi',
534
- 'not',
535
- 'now',
536
- 'null',
537
- 'offset',
538
- 'on',
539
- 'or',
540
- 'order',
541
- 'outer',
542
- 'over',
543
- 'partition',
544
- 'pow',
545
- 'rank',
546
- 'refresh',
547
- 'required',
548
- 'returning',
549
- 'right',
550
- 'rollback',
551
- 'round',
552
- 'row_number',
553
- 'schema',
554
- 'select',
555
- 'sqrt',
556
- 'substring',
557
- 'sum',
558
- 'then',
559
- 'transaction',
560
- 'trim',
561
- 'true',
562
- 'type',
563
- 'union',
564
- 'unique',
565
- 'update',
566
- 'upper',
567
- 'upsert',
568
- 'view',
569
- 'when',
570
- ]);
571
- const POWQL_BARE_IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
572
- /**
573
- * Backtick-quote an identifier when PowQL would otherwise lex it as a keyword
574
- * (or when it contains characters outside the bare-identifier grammar).
575
- * Applied only in bare-identifier positions, DDL type/field names, index DDL,
576
- * and `insert`/`update`/`upsert` assignment targets. Dotted references
577
- * (`.col` in filters/projections/ordering) bypass keyword lookup on every
578
- * engine version and deliberately stay bare for ≤0.9 compatibility. Backticks
579
- * parse on PowDB ≥ 0.10; on older engines these names were already parse
580
- * errors when emitted bare, so quoting is strictly an improvement.
581
- */
582
- export function quotePowqlIdent(name) {
583
- if (name.includes('`')) {
584
- // The lexer has no backtick escape inside a quoted identifier.
585
- throw new ValidationError(`[turbine] Identifier "${name}" contains a backtick, which PowQL cannot represent.`);
586
- }
587
- return POWQL_KEYWORDS.has(name) || !POWQL_BARE_IDENT.test(name) ? `\`${name}\`` : name;
588
- }
589
- /**
590
- * The DOTTED-position spelling of {@link quotePowqlIdent}: quote a name that
591
- * falls outside the bare-identifier grammar, and only that.
592
- *
593
- * A dotted reference (`.col` in a filter, projection, `order`, `group`, or an
594
- * `upsert on`) bypasses keyword lookup, so `.order` parses on every engine
595
- * version and stays bare here, which is the ≤0.9 compatibility decision
596
- * {@link quotePowqlIdent} documents and which this must not undo.
597
- *
598
- * What it does NOT excuse is interpolating the name RAW, which is what these
599
- * sites used to do. Keyword-ness is a parsing question; a name outside
600
- * `POWQL_BARE_IDENT` is a statement-integrity one, and that name is the only
601
- * thing that can carry PowQL syntax into a statement whose values are all bound
602
- * as `$N` params. Reaching it needs a hostile column name (an introspected
603
- * database, a generator, a migration authored elsewhere) since names come from
604
- * schema metadata, but "the names are trusted" is not the invariant the rest of
605
- * this engine is written to. So: bare when the grammar allows it (byte-identical
606
- * output for every ordinary and every keyword name), quoted when it does not,
607
- * where the bare form was a parse error anyway. Verified against the engine that
608
- * a quoted dotted reference parses everywhere the bare one does and yields the
609
- * same result-column name.
610
- */
611
- export function quotePowqlDotted(name) {
612
- if (POWQL_BARE_IDENT.test(name))
613
- return name;
614
- if (name.includes('`')) {
615
- throw new ValidationError(`[turbine] Identifier "${name}" contains a backtick, which PowQL cannot represent.`);
616
- }
617
- return `\`${name}\``;
618
- }
619
273
  /**
620
274
  * Derive the entity links Turbine would declare from a schema's relations. One
621
275
  * link per single-column hasMany / hasOne / belongsTo relation, owned by the
@@ -658,6 +312,15 @@ export function powdbLinkStatement(link) {
658
312
  return (`link ${quotePowqlIdent(link.owner)}.${quotePowqlIdent(link.name)} -> ` +
659
313
  `${quotePowqlIdent(link.target)} on ${quotePowqlIdent(link.localKey)} = ${quotePowqlIdent(link.targetKey)}`);
660
314
  }
315
+ /**
316
+ * Generate PowQL DDL (`type T { … }`) for every table in a schema. Used to
317
+ * provision a PowDB database from a code-first `defineSchema`/`SchemaMetadata`
318
+ * (PowDB has no migration runner yet). The primary key column is declared
319
+ * `required unique`; non-nullable columns are `required`. A server-generated
320
+ * column ({@link ColumnMetadata.isGenerated}) that maps to PowQL `int` gets the
321
+ * `auto` modifier, so PowDB assigns a monotonic id on insert and Turbine stops
322
+ * synthesizing a client-side value for it.
323
+ */
661
324
  export function powqlSchemaDDL(schema, opts = {}) {
662
325
  const caps = opts.capabilities;
663
326
  const stmts = [];
@@ -716,7 +379,7 @@ export function powqlSchemaDDL(schema, opts = {}) {
716
379
  requireCapability(caps, 'docFieldIndexes', 'JSON doc-field expression indexes');
717
380
  const column = idx.columns[0];
718
381
  if (column === undefined) {
719
- throw new ValidationError(`[turbine] Doc-field index "${idx.name}" on ${meta.name} has no target json column.`);
382
+ throw new ValidationError(`Doc-field index "${idx.name}" on ${meta.name} has no target json column.`);
720
383
  }
721
384
  const segs = idx.docPath.map((s) => (typeof s === 'number' ? `->${s}` : `->${encodePowqlString(s)}`)).join('');
722
385
  stmts.push(`alter ${quotePowqlIdent(meta.name)} add ${kind} (.${quotePowqlIdent(column)}${segs})`);
@@ -841,10 +504,10 @@ function powdbJsonText(param, position) {
841
504
  json = JSON.stringify(param.value);
842
505
  }
843
506
  catch (err) {
844
- throw new ValidationError(`[turbine] The json value ${where} cannot be serialized for PowDB: ${err.message}`);
507
+ throw new ValidationError(`The json value ${where} cannot be serialized for PowDB: ${err.message}`);
845
508
  }
846
509
  if (json === undefined) {
847
- throw new ValidationError(`[turbine] The json value ${where} serializes to nothing (JSON.stringify returned undefined); ` +
510
+ throw new ValidationError(`The json value ${where} serializes to nothing (JSON.stringify returned undefined); ` +
848
511
  'write `null` explicitly instead.');
849
512
  }
850
513
  return json;
@@ -853,7 +516,7 @@ function powdbJsonText(param, position) {
853
516
  function powdbDateMicros(value, position) {
854
517
  const ms = value.getTime();
855
518
  if (!Number.isFinite(ms)) {
856
- throw new ValidationError(`[turbine] Invalid Date${position ? ` (${position})` : ''} cannot be encoded as a PowDB timestamp.`);
519
+ throw new ValidationError(`Invalid Date${position ? ` (${position})` : ''} cannot be encoded as a PowDB timestamp.`);
857
520
  }
858
521
  return BigInt(ms) * 1000n;
859
522
  }
@@ -878,116 +541,7 @@ function toPowdbParam(value, col) {
878
541
  return value;
879
542
  }
880
543
  // Objects/arrays have no PowDB representation.
881
- throw new ValidationError(`[turbine] Value of type ${typeof value} cannot be bound as a PowDB parameter.`);
882
- }
883
- /**
884
- * Coerce a single PowDB wire string into the JS value its column type implies.
885
- * Every PowDB value arrives as a string; NULL arrives as the bareword `"null"`.
886
- * Metadata resolves the `"null"` ambiguity for nullable non-string columns.
887
- */
888
- export function coerceValue(raw, col) {
889
- const ts = col.tsType.replace(/\s*\|\s*null$/i, '').trim();
890
- const json = isJsonColumn(col);
891
- // NULL bareword: unambiguous for non-string columns; for `str` we cannot tell a
892
- // literal "null" from SQL NULL, so a nullable str of value "null" reads as null.
893
- // For a `json` column the bareword `null` (a legacy-wire rendering shared by an
894
- // absent value AND a top-level JSON-null document, documented residual,
895
- // resolved on the native transport by the WireValue path) maps to null; a JSON
896
- // string document "null" renders WITH quotes (`"null"`) and parses distinctly.
897
- if (raw === 'null' && (json || ts !== 'string' || col.nullable))
898
- return null;
899
- if (json) {
900
- try {
901
- return JSON.parse(raw);
902
- }
903
- catch {
904
- return raw; // defensive: canonical JSON text always parses
905
- }
906
- }
907
- if (ts === 'Date') {
908
- const micros = Number(raw);
909
- return Number.isFinite(micros) ? new Date(micros / 1000) : null;
910
- }
911
- if (ts === 'boolean')
912
- return raw === 'true';
913
- if (ts === 'number') {
914
- const n = Number(raw);
915
- // int8 policy: keep precision-losing big integers as strings.
916
- return Number.isSafeInteger(n) || !Number.isInteger(n) ? n : raw;
917
- }
918
- if (ts === 'bigint')
919
- return BigInt(raw);
920
- return raw; // string / uuid-as-string
921
- }
922
- /**
923
- * Coerce a single cell that arrived over the NATIVE typed wire (decoded from a
924
- * {@link PowdbWireValue}, so already a JS `bigint`/`number`/`boolean`/`string`/
925
- * `NativeJson`/`Uint8Array`/`null`, never a bare `"null"` string). Unlike
926
- * {@link coerceValue} this NEVER collapses the string `"null"` to `null`: an
927
- * absent value already decoded to `null` (from the `empty` cell), so a genuine
928
- * str `"null"` stays the string `"null"` (fixes the legacy-wire wart on the
929
- * native transport). `datetime`-shaped cells (int micros) become `Date`; a
930
- * bigint on a `number` column follows the int8 safe-integer policy.
931
- *
932
- * A date cell can also arrive as a DIGIT STRING: a nested-projection block's
933
- * children ride a JSON array, and micros exceed `Number.MAX_SAFE_INTEGER`'s
934
- * decimal comfort, so the engine renders them as a JSON string. Before that
935
- * string was parsed here, a nested `with` handed back the raw micros text while
936
- * the batched loader and the native join both handed back a `Date` (the same
937
- * relation, three answers). Only an all-digit string is parsed; any other text
938
- * on a date column passes through untouched.
939
- */
940
- export function coerceNativeValue(value, col) {
941
- if (value === undefined || value === null)
942
- return null;
943
- if (isDateColumn(col)) {
944
- if (typeof value === 'bigint')
945
- return new Date(Number(value) / 1000);
946
- if (typeof value === 'number')
947
- return new Date(value / 1000);
948
- if (typeof value === 'string' && /^-?\d+$/.test(value))
949
- return new Date(Number(value) / 1000);
950
- return value;
951
- }
952
- const ts = col.tsType.replace(/\s*\|\s*null$/i, '').trim();
953
- if (typeof value === 'bigint') {
954
- if (ts === 'bigint')
955
- return value;
956
- if (ts === 'number') {
957
- const n = Number(value);
958
- return Number.isSafeInteger(n) ? n : value.toString(); // int8 policy: keep big ints as strings
959
- }
960
- return value;
961
- }
962
- return value; // number / boolean / string / NativeJson document / Uint8Array
963
- }
964
- /**
965
- * Map one raw PowDB row into a typed entity (camelCase fields, coerced values).
966
- * Only the columns present in `raw` are emitted, so partial `select`
967
- * projections round-trip unchanged. `native` selects the coercion policy: the
968
- * default `false` handles the legacy string wire (every cell is a string, via
969
- * {@link coerceValue}); `true` handles the native typed wire, where non-string
970
- * cells arrive pre-typed and go through {@link coerceNativeValue} (see F3).
971
- * Callers on the native transport pass `this.pool.capabilities.nativeRaw`.
972
- */
973
- export function rowToEntity(raw, meta, native = false) {
974
- const byName = new Map(meta.columns.map((c) => [c.name, c]));
975
- const out = {};
976
- for (const snake of Object.keys(raw)) {
977
- const col = byName.get(snake);
978
- const field = meta.reverseColumnMap[snake] ?? snake;
979
- const value = raw[snake];
980
- if (!col) {
981
- out[field] = value;
982
- }
983
- else if (native) {
984
- out[field] = coerceNativeValue(value, col);
985
- }
986
- else {
987
- out[field] = typeof value === 'string' ? coerceValue(value, col) : value;
988
- }
989
- }
990
- return out;
544
+ throw new ValidationError(`Value of type ${typeof value} cannot be bound as a PowDB parameter.`);
991
545
  }
992
546
  // ---------------------------------------------------------------------------
993
547
  // Error translation
@@ -1010,7 +564,7 @@ export function rowToEntity(raw, meta, native = false) {
1010
564
  */
1011
565
  export function wrapPowdbError(err) {
1012
566
  if (!err || typeof err !== 'object')
1013
- return new ConnectionError(`[turbine] PowDB error: ${String(err)}`);
567
+ return new ConnectionError(`PowDB error: ${String(err)}`);
1014
568
  const e = err;
1015
569
  const msg = e.message ?? 'unknown PowDB error';
1016
570
  // Unique-constraint, message-based on both transports.
@@ -1028,7 +582,7 @@ export function wrapPowdbError(err) {
1028
582
  // statement reaching an already-closed embedded handle) carry no .code:
1029
583
  // classify by message so both transports surface E004.
1030
584
  if (/pool closed|pool acquire timeout|database is closed/i.test(msg)) {
1031
- return new ConnectionError(`[turbine] PowDB connection unavailable: ${msg}`, { cause: err });
585
+ return new ConnectionError(`PowDB connection unavailable: ${msg}`, { cause: err });
1032
586
  }
1033
587
  // Server-side transaction-gate wait bound (PowDB ≥ 0.10, default 5s): another
1034
588
  // connection held the single global write lock past the server's
@@ -1047,7 +601,7 @@ export function wrapPowdbError(err) {
1047
601
  // callers (and the opt-in stale-read retry) can inspect the driver code.
1048
602
  if (e.code === 'protocol_error' ||
1049
603
  /received unexpected frame|unknown message type|truncated payload|bad framing/i.test(msg)) {
1050
- return new ConnectionError(`[turbine] PowDB connection is in an invalid state: ${msg}`, { cause: err });
604
+ return new ConnectionError(`PowDB connection is in an invalid state: ${msg}`, { cause: err });
1051
605
  }
1052
606
  // Read-only refusal → ReadOnlyError (E018). Two engine shapes, both mapped by
1053
607
  // substring (the networked transport prefixes the message with `query failed:
@@ -1073,7 +627,7 @@ export function wrapPowdbError(err) {
1073
627
  // not empty …`). It is a connection failure (E004), not a query defect, the
1074
628
  // fix is to recover the directory with a writable open first.
1075
629
  if (/cannot open read-only: the WAL is not empty/i.test(msg)) {
1076
- return new ConnectionError(`[turbine] PowDB could not open the directory read-only: ${msg}. Open it once with a writable handle to ` +
630
+ return new ConnectionError(`PowDB could not open the directory read-only: ${msg}. Open it once with a writable handle to ` +
1077
631
  'flush the WAL (recover the directory), then reopen it read-only for snapshot serving.', { cause: err });
1078
632
  }
1079
633
  // Catalog-version mismatch → ConnectionError (E004). A pre-0.19 binary/addon
@@ -1082,7 +636,7 @@ export function wrapPowdbError(err) {
1082
636
  // not a query defect, so it runs among the connection families BEFORE the
1083
637
  // generic validation regexes below.
1084
638
  if (/unsupported catalog version/i.test(msg)) {
1085
- return new ConnectionError(`[turbine] PowDB could not open the data directory: ${msg}. This directory uses a newer PowDB catalog ` +
639
+ return new ConnectionError(`PowDB could not open the data directory: ${msg}. This directory uses a newer PowDB catalog ` +
1086
640
  'format (a `link` declaration upgrades it to v7); upgrade the PowDB addon/server to a version that can read it.', { cause: err });
1087
641
  }
1088
642
  // Per-query deadline → TimeoutError (E002). Message-path so it fires on the
@@ -1091,20 +645,20 @@ export function wrapPowdbError(err) {
1091
645
  // transaction-gate timeout below) so the real "query timeout after <n>ms"
1092
646
  // survives instead of rendering the placeholder "timed out after 0ms".
1093
647
  if (/query timeout after/i.test(msg)) {
1094
- return new TimeoutError(0, 'PowDB query', { message: `[turbine] PowDB ${msg}`, cause: err });
648
+ return new TimeoutError(0, 'PowDB query', { message: `PowDB ${msg}`, cause: err });
1095
649
  }
1096
650
  // Client-initiated cancellation → ConnectionError (E004). This is FINAL: the
1097
651
  // issuing client disconnected, so the query was a clean early return, never
1098
652
  // auto-retry it (the opt-in stale-read retry only replays stale-FRAME reads).
1099
653
  if (/query cancelled by client disconnect/i.test(msg)) {
1100
- return new ConnectionError(`[turbine] PowDB query cancelled by client disconnect: ${msg}`, { cause: err });
654
+ return new ConnectionError(`PowDB query cancelled by client disconnect: ${msg}`, { cause: err });
1101
655
  }
1102
656
  // Bounded join rejection → ValidationError (E003). The engine rejects a pure
1103
657
  // nested-loop join whose candidate-pair count (or result row count) exceeds
1104
658
  // the safety bound BEFORE executing, and names the fix in the message, keep
1105
659
  // that fix-hint intact so the caller knows how to make the join eligible.
1106
660
  if (/nested-loop join would evaluate|join result exceeds row limit/i.test(msg)) {
1107
- return new ValidationError(`[turbine] PowDB join rejected: ${msg}`);
661
+ return new ValidationError(`PowDB join rejected: ${msg}`);
1108
662
  }
1109
663
  // Entity-link misuse hard errors (0.19.1 turned the two silent-wrong-results
1110
664
  // 0.19.0 behaviors into hard errors) → ValidationError (E003), a query defect
@@ -1116,7 +670,7 @@ export function wrapPowdbError(err) {
1116
670
  // always aliases and never aggregates over a link), so these fire only for a
1117
671
  // raw user PowQL string; mapping them keeps that path typed.
1118
672
  if (/is ambiguous in a projection|aggregates over a nested or link projection/i.test(msg)) {
1119
- return new ValidationError(`[turbine] PowDB query rejected: ${msg}`);
673
+ return new ValidationError(`PowDB query rejected: ${msg}`);
1120
674
  }
1121
675
  // Corrupt storage → ConnectionError (E004). PowDB 0.20 verifies page checksums
1122
676
  // at table-OPEN time and fails closed (previously the open scan skipped the bad
@@ -1126,7 +680,7 @@ export function wrapPowdbError(err) {
1126
680
  // token would otherwise class it E003). There is no salvage mode: restoring
1127
681
  // from a backup is the documented recovery, so say so.
1128
682
  if (/page corrupt|catalog corrupt|corrupt heap superblock|CRC32 mismatch/i.test(msg)) {
1129
- return new ConnectionError(`[turbine] PowDB refused to open a corrupt data directory: ${msg}. PowDB verifies page checksums on open ` +
683
+ return new ConnectionError(`PowDB refused to open a corrupt data directory: ${msg}. PowDB verifies page checksums on open ` +
1130
684
  'and fails closed rather than serving partial data; there is no skip-corrupt-pages mode, so recover by ' +
1131
685
  'restoring the directory from a backup.', { cause: err });
1132
686
  }
@@ -1140,7 +694,7 @@ export function wrapPowdbError(err) {
1140
694
  const m = /column '([^']+)' not found(?: in table '([^']+)')?/i.exec(msg);
1141
695
  if (m) {
1142
696
  const where = m[2] ? ` on table "${m[2]}"` : '';
1143
- return new ValidationError(`[turbine] PowDB rejected column "${m[1]}"${where}: it does not exist in the live catalog. ` +
697
+ return new ValidationError(`PowDB rejected column "${m[1]}"${where}: it does not exist in the live catalog. ` +
1144
698
  'The schema metadata Turbine is using has drifted from the database; re-derive it ' +
1145
699
  '(`schemaDefToMetadata` / `introspectPowdbDatabase`) or apply the missing DDL. ' +
1146
700
  `(engine: ${msg})`);
@@ -1153,7 +707,7 @@ export function wrapPowdbError(err) {
1153
707
  {
1154
708
  const m = /type mismatch for column '([^']+)': expected ([^,]+), got (\w+)/i.exec(msg);
1155
709
  if (m) {
1156
- return new ValidationError(`[turbine] PowDB rejected a comparison on column "${m[1]}": the column is ${m[2]} but the bound value is ` +
710
+ return new ValidationError(`PowDB rejected a comparison on column "${m[1]}": the column is ${m[2]} but the bound value is ` +
1157
711
  `${m[3]}. PowQL never coerces across types in a comparison (before engine 0.20 this silently matched ` +
1158
712
  "every row), so bind a value of the column's own type.");
1159
713
  }
@@ -1163,7 +717,7 @@ export function wrapPowdbError(err) {
1163
717
  // nested parentheses, so a machine-built predicate with very many terms is now
1164
718
  // rejected outright (see POWQL_MAX_NESTING_DEPTH).
1165
719
  if (/nesting depth exceeds maximum/i.test(msg)) {
1166
- return new ValidationError(`[turbine] PowDB rejected the query: ${msg}. PowQL bounds the shape of the predicate tree, and a flat ` +
720
+ return new ValidationError(`PowDB rejected the query: ${msg}. PowQL bounds the shape of the predicate tree, and a flat ` +
1167
721
  `\`OR\` / \`AND\` array counts one level per term (roughly ${POWQL_MAX_NESTING_DEPTH - 1} terms at the top ` +
1168
722
  'level, fewer inside a nested `with` block). Split a large `OR` / `AND` array into several queries and ' +
1169
723
  'merge the results, or express it as a single `in` list, which is one flat node and does not count ' +
@@ -1172,7 +726,7 @@ export function wrapPowdbError(err) {
1172
726
  // Negative limit / offset → ValidationError (E003). Turbine validates these
1173
727
  // client-side before emitting, so this is the backstop for a raw PowQL string.
1174
728
  if (/(limit|offset) must not be negative/i.test(msg)) {
1175
- return new ValidationError(`[turbine] PowDB rejected the query: ${msg}. Pass a non-negative \`limit\` / \`offset\` ` +
729
+ return new ValidationError(`PowDB rejected the query: ${msg}. Pass a non-negative \`limit\` / \`offset\` ` +
1176
730
  '(before engine 0.20 a negative limit was ignored and returned every row).');
1177
731
  }
1178
732
  // Client-side result-frame cell cap (`@zvndev/powdb-client` >= 0.20 rejects a
@@ -1181,7 +735,7 @@ export function wrapPowdbError(err) {
1181
735
  // Error with no code, so match the message and give the caller the two real
1182
736
  // remedies instead of letting it fall through untyped.
1183
737
  if (/result too large: \d+ cells/i.test(msg)) {
1184
- return new ValidationError(`[turbine] PowDB result too large to decode: ${msg}. The client caps one result frame at 2,000,000 cells ` +
738
+ return new ValidationError(`PowDB result too large to decode: ${msg}. The client caps one result frame at 2,000,000 cells ` +
1185
739
  '(rows x columns). Page the query with `limit` / `offset`, or narrow the row with `select` so each row ' +
1186
740
  'carries fewer columns.');
1187
741
  }
@@ -1199,28 +753,28 @@ export function wrapPowdbError(err) {
1199
753
  if (typeof wireClass === 'number') {
1200
754
  switch (wireClass) {
1201
755
  case 3: // timeout (per-query budget, gate wait, idle timeout), retryable
1202
- return new TimeoutError(0, 'PowDB query', { message: `[turbine] PowDB ${msg}`, cause: err });
756
+ return new TimeoutError(0, 'PowDB query', { message: `PowDB ${msg}`, cause: err });
1203
757
  case 4: // limit_exceeded (memory / size budget), a query-shape defect
1204
- return new ValidationError(`[turbine] PowDB resource limit exceeded: ${msg}`);
758
+ return new ValidationError(`PowDB resource limit exceeded: ${msg}`);
1205
759
  case 5: // readonly_refused, the snapshot-serving routing signal
1206
760
  return new ReadOnlyError(`PowDB refused a write on a read-only database: ${msg}.`, {
1207
761
  cause: err,
1208
762
  reason: 'snapshot',
1209
763
  });
1210
764
  case 6: // auth_failed at CONNECT
1211
- return new ConnectionError(`[turbine] PowDB authentication failed: ${msg} (check the user / password / dbName for this connection).`, { cause: err });
765
+ return new ConnectionError(`PowDB authentication failed: ${msg} (check the user / password / dbName for this connection).`, { cause: err });
1212
766
  case 7: // rate_limited (repeated bad auth), connection-establishment class
1213
- return new ConnectionError(`[turbine] PowDB rate-limited this address after repeated failed authentication: ${msg}. Wait before retrying.`, { cause: err });
767
+ return new ConnectionError(`PowDB rate-limited this address after repeated failed authentication: ${msg}. Wait before retrying.`, { cause: err });
1214
768
  case 8: {
1215
769
  // constraint_violation, today that is always a unique index
1216
770
  const m = /on\s+\S+\.(\w+)/i.exec(msg);
1217
771
  return new UniqueConstraintError({ constraint: m?.[1], cause: err });
1218
772
  }
1219
773
  case 9: // cancelled (issuing client disconnected), final, never retry
1220
- return new ConnectionError(`[turbine] PowDB query cancelled by client disconnect: ${msg}`, { cause: err });
774
+ return new ConnectionError(`PowDB query cancelled by client disconnect: ${msg}`, { cause: err });
1221
775
  case 1: // parse
1222
776
  case 2: // execution
1223
- return new ValidationError(`[turbine] PowDB query rejected: ${msg}`);
777
+ return new ValidationError(`PowDB query rejected: ${msg}`);
1224
778
  default: // internal (0) or an unknown future class: fall through
1225
779
  break;
1226
780
  }
@@ -1230,45 +784,28 @@ export function wrapPowdbError(err) {
1230
784
  // signal we get (code is always 'GenericFailure'); on the networked path they
1231
785
  // are a safety net before the .code switch.
1232
786
  if (/type mismatch|\bParse\b|\bExecution\b|StorageError|unexpected|row too large/i.test(msg)) {
1233
- return new ValidationError(`[turbine] PowDB query rejected: ${msg}`);
787
+ return new ValidationError(`PowDB query rejected: ${msg}`);
1234
788
  }
1235
789
  switch (e.code) {
1236
790
  case 'connect_failed':
1237
791
  case 'closed':
1238
- return new ConnectionError(`[turbine] PowDB connection failed: ${msg}`, { cause: err });
792
+ return new ConnectionError(`PowDB connection failed: ${msg}`, { cause: err });
1239
793
  case 'auth_failed':
1240
794
  // Connection-establishment class, non-retryable: the handshake was
1241
795
  // rejected. Surface E004 with a concrete remediation hint instead of
1242
796
  // letting it fall through to the raw error.
1243
- return new ConnectionError(`[turbine] PowDB authentication failed: ${msg} (check the user / password / dbName for this connection).`, { cause: err });
797
+ return new ConnectionError(`PowDB authentication failed: ${msg} (check the user / password / dbName for this connection).`, { cause: err });
1244
798
  case 'timeout':
1245
799
  case 'aborted':
1246
800
  return new TimeoutError(0, 'PowDB query', { cause: err });
1247
801
  case 'query_failed':
1248
802
  case 'type_coercion_failed':
1249
803
  case 'size_exceeded':
1250
- return new ValidationError(`[turbine] PowDB query rejected: ${msg}`);
804
+ return new ValidationError(`PowDB query rejected: ${msg}`);
1251
805
  default:
1252
- return err instanceof Error ? err : new ConnectionError(`[turbine] PowDB error: ${msg}`, { cause: err });
806
+ return err instanceof Error ? err : new ConnectionError(`PowDB error: ${msg}`, { cause: err });
1253
807
  }
1254
808
  }
1255
- /**
1256
- * True when `err` is the stale-wire-frame {@link ConnectionError} produced by
1257
- * {@link wrapPowdbError} (its `.cause` is a `protocol_error` PowDBError, or the
1258
- * message carries the invalid-state signature). The opt-in read retry
1259
- * (`retryStaleReads`, evaluated in {@link PowqlInterface}'s exec seam) uses this
1260
- * to decide whether a first-statement READ may be replayed once on a fresh
1261
- * connection; writes are NEVER retried (an ambiguous mutation reply is unsafe
1262
- * to replay, matching the client's own native-path policy).
1263
- */
1264
- export function isStaleFramePowdbError(err) {
1265
- if (!(err instanceof ConnectionError))
1266
- return false;
1267
- const cause = err.cause;
1268
- if (cause && typeof cause === 'object' && cause.code === 'protocol_error')
1269
- return true;
1270
- return /PowDB connection is in an invalid state/.test(err.message);
1271
- }
1272
809
  function normalizeQueryArgs(arg, values) {
1273
810
  if (typeof arg === 'string')
1274
811
  return { text: arg, params: values ?? [] };
@@ -1635,7 +1172,7 @@ export class PowdbPool {
1635
1172
  */
1636
1173
  assertOpen() {
1637
1174
  if (this.closed) {
1638
- throw new ConnectionError('[turbine] The PowDB pool is closed, disconnect() was already called on this client.');
1175
+ throw new ConnectionError('The PowDB pool is closed, disconnect() was already called on this client.');
1639
1176
  }
1640
1177
  }
1641
1178
  async connect() {
@@ -1806,7 +1343,7 @@ export function encodePowqlLiteral(value, position) {
1806
1343
  if (value instanceof PowdbFloatParam) {
1807
1344
  const n = value.value;
1808
1345
  if (!Number.isFinite(n))
1809
- throw new ValidationError(`[turbine] Non-finite float cannot be encoded for PowDB${at}.`);
1346
+ throw new ValidationError(`Non-finite float cannot be encoded for PowDB${at}.`);
1810
1347
  // Force a float-form literal so an integer-valued float column stays a float.
1811
1348
  const text = powqlNumberText(n);
1812
1349
  return text.includes('.') ? text : `${text}.0`;
@@ -1825,14 +1362,14 @@ export function encodePowqlLiteral(value, position) {
1825
1362
  return value.toString();
1826
1363
  if (typeof value === 'number') {
1827
1364
  if (!Number.isFinite(value))
1828
- throw new ValidationError(`[turbine] Non-finite number cannot be encoded for PowDB${at}.`);
1365
+ throw new ValidationError(`Non-finite number cannot be encoded for PowDB${at}.`);
1829
1366
  // Renders an integer as an int literal (`42`) and a fractional number as a
1830
1367
  // float literal (`4.2`); PowQL distinguishes them by the dot.
1831
1368
  return powqlNumberText(value);
1832
1369
  }
1833
1370
  if (typeof value === 'string')
1834
1371
  return encodePowqlString(value, position);
1835
- throw new ValidationError(`[turbine] Value of type ${typeof value} cannot be encoded as a PowDB literal${at}.`);
1372
+ throw new ValidationError(`Value of type ${typeof value} cannot be encoded as a PowDB literal${at}.`);
1836
1373
  }
1837
1374
  /**
1838
1375
  * Render a finite JS number as PowQL numeric text.
@@ -1853,7 +1390,7 @@ function powqlNumberText(n) {
1853
1390
  return BigInt(n).toString();
1854
1391
  const m = /^(-?)(\d+)(?:\.(\d+))?[eE]([+-]?\d+)$/.exec(s);
1855
1392
  if (!m) {
1856
- throw new ValidationError(`[turbine] Number ${s} cannot be rendered as a PowDB literal.`);
1393
+ throw new ValidationError(`Number ${s} cannot be rendered as a PowDB literal.`);
1857
1394
  }
1858
1395
  const sign = m[1] ?? '';
1859
1396
  const intPart = m[2] ?? '';
@@ -1920,7 +1457,7 @@ export const POWQL_LEXER_TESTED_CEILING = '0.20';
1920
1457
  */
1921
1458
  function encodePowqlString(s, position) {
1922
1459
  if (s.includes('\0')) {
1923
- throw new ValidationError(`[turbine] String value${position ? ` (${position})` : ''} contains a NUL byte (U+0000), which PowQL string ` +
1460
+ throw new ValidationError(`String value${position ? ` (${position})` : ''} contains a NUL byte (U+0000), which PowQL string ` +
1924
1461
  'literals cannot represent. Strip it before writing.');
1925
1462
  }
1926
1463
  let out = '"';
@@ -1950,7 +1487,7 @@ export function materializePowql(powql, params) {
1950
1487
  return powql.replace(/\$(\d+)/g, (_m, n) => {
1951
1488
  const idx = Number(n) - 1;
1952
1489
  if (idx < 0 || idx >= params.length) {
1953
- throw new ValidationError(`[turbine] PowQL placeholder $${n} has no bound parameter (have ${params.length}).`);
1490
+ throw new ValidationError(`PowQL placeholder $${n} has no bound parameter (have ${params.length}).`);
1954
1491
  }
1955
1492
  return encodePowqlLiteral(params[idx], `parameter $${n}`);
1956
1493
  });
@@ -2037,7 +1574,7 @@ export class PowdbEmbeddedPool {
2037
1574
  if (engineSem &&
2038
1575
  ceiling &&
2039
1576
  (engineSem.major > ceiling.major || (engineSem.major === ceiling.major && engineSem.minor > ceiling.minor))) {
2040
- throw new ValidationError(`[turbine] Refusing the PowDB legacy string wire: this embedded addon reports engine ` +
1577
+ throw new ValidationError(`Refusing the PowDB legacy string wire: this embedded addon reports engine ` +
2041
1578
  `${this.capabilities.engineVersion}, which is newer than the escaper's verified lexer range ` +
2042
1579
  `(<= ${POWQL_LEXER_TESTED_CEILING}) AND such an addon exposes the parameterized native API, so ` +
2043
1580
  `reaching the legacy materialize path means the queryWithParams feature-detect failed. Upgrade ` +
@@ -2054,7 +1591,7 @@ export class PowdbEmbeddedPool {
2054
1591
  */
2055
1592
  async run(powql, params, holdRef) {
2056
1593
  if (this.closed) {
2057
- throw new ConnectionError('[turbine] The PowDB embedded pool is closed: disconnect() was already called on this client.');
1594
+ throw new ConnectionError('The PowDB embedded pool is closed: disconnect() was already called on this client.');
2058
1595
  }
2059
1596
  const ctl = txControl(powql);
2060
1597
  if (ctl === 'begin') {
@@ -2069,7 +1606,7 @@ export class PowdbEmbeddedPool {
2069
1606
  if (this.closed) {
2070
1607
  holdRef.hold.finish();
2071
1608
  holdRef.hold = null;
2072
- throw new ConnectionError('[turbine] The PowDB embedded pool is closed: disconnect() was already called on this client.');
1609
+ throw new ConnectionError('The PowDB embedded pool is closed: disconnect() was already called on this client.');
2073
1610
  }
2074
1611
  }
2075
1612
  if ((ctl === 'commit' || ctl === 'rollback') && holdRef.hold === null) {
@@ -2175,7 +1712,7 @@ async function loadPowdb() {
2175
1712
  return (await importOptionalPeer('@zvndev/powdb-client'));
2176
1713
  }
2177
1714
  catch (err) {
2178
- throw new ConnectionError("[turbine] turbine-orm/powdb requires the optional peer dependency '@zvndev/powdb-client'. Install it: npm i @zvndev/powdb-client, " +
1715
+ throw new ConnectionError("turbine-orm/powdb requires the optional peer dependency '@zvndev/powdb-client'. Install it: npm i @zvndev/powdb-client, " +
2179
1716
  'or construct the PowDB pool yourself and inject it: turbinePowDB(pool, schema). ' +
2180
1717
  `(${err.message})`);
2181
1718
  }
@@ -2195,7 +1732,7 @@ async function loadPowdbEmbedded() {
2195
1732
  mod = (await importOptionalPeer('@zvndev/powdb-embedded'));
2196
1733
  }
2197
1734
  catch (err) {
2198
- throw new ConnectionError("[turbine] turbine-orm/powdb embedded mode requires the optional peer '@zvndev/powdb-embedded'. " +
1735
+ throw new ConnectionError("turbine-orm/powdb embedded mode requires the optional peer '@zvndev/powdb-embedded'. " +
2199
1736
  'Install it: npm i @zvndev/powdb-embedded. If install succeeded but loading failed, your platform has no ' +
2200
1737
  'prebuilt binary (prebuilts ship for macOS arm64/x64 and Linux glibc x64/arm64; Intel-mac/musl/Windows ' +
2201
1738
  'build from source), build it with `npm run build` in the addon, then retry. You can also construct the ' +
@@ -2203,7 +1740,7 @@ async function loadPowdbEmbedded() {
2203
1740
  `(${err.message})`);
2204
1741
  }
2205
1742
  if (!mod || typeof mod.Database?.open !== 'function') {
2206
- throw new ConnectionError("[turbine] '@zvndev/powdb-embedded' loaded but did not export Database.open, the installed version is " +
1743
+ throw new ConnectionError("'@zvndev/powdb-embedded' loaded but did not export Database.open, the installed version is " +
2207
1744
  'likely incompatible (turbine-orm/powdb embedded requires @zvndev/powdb-embedded ^0.7.0).');
2208
1745
  }
2209
1746
  return mod;
@@ -2227,7 +1764,7 @@ async function openEmbeddedPool(target, poolOptions = {}, assumeEngineVersion, i
2227
1764
  // A read-only engine never writes, so a durability selector is meaningless
2228
1765
  // there, reject the combination loudly rather than silently ignoring one.
2229
1766
  if (readonly && syncMode !== undefined) {
2230
- throw new ValidationError('[turbine] embedded `syncMode` is meaningless with `readonly: true` (a read-only database never writes). Remove one.');
1767
+ throw new ValidationError('embedded `syncMode` is meaningless with `readonly: true` (a read-only database never writes). Remove one.');
2231
1768
  }
2232
1769
  let db;
2233
1770
  try {
@@ -2237,20 +1774,20 @@ async function openEmbeddedPool(target, poolOptions = {}, assumeEngineVersion, i
2237
1774
  // installed addon predates them.
2238
1775
  if (memoryLimit !== undefined) {
2239
1776
  if (typeof mod.Database.openReadOnlyWithMemoryLimit !== 'function') {
2240
- throw new ConnectionError('[turbine] embedded `readonly` + `memoryLimit` requires @zvndev/powdb-embedded >= 0.14 (openReadOnlyWithMemoryLimit).');
1777
+ throw new ConnectionError('embedded `readonly` + `memoryLimit` requires @zvndev/powdb-embedded >= 0.14 (openReadOnlyWithMemoryLimit).');
2241
1778
  }
2242
1779
  db = mod.Database.openReadOnlyWithMemoryLimit(dir, memoryLimit);
2243
1780
  }
2244
1781
  else {
2245
1782
  if (typeof mod.Database.openReadOnly !== 'function') {
2246
- throw new ConnectionError('[turbine] embedded `readonly: true` requires @zvndev/powdb-embedded >= 0.14 (the installed addon has no openReadOnly).');
1783
+ throw new ConnectionError('embedded `readonly: true` requires @zvndev/powdb-embedded >= 0.14 (the installed addon has no openReadOnly).');
2247
1784
  }
2248
1785
  db = mod.Database.openReadOnly(dir);
2249
1786
  }
2250
1787
  }
2251
1788
  else if (memoryLimit !== undefined) {
2252
1789
  if (typeof mod.Database.openWithMemoryLimit !== 'function') {
2253
- throw new ConnectionError('[turbine] embedded `memoryLimit` requires @zvndev/powdb-embedded ≥ 0.7.1.');
1790
+ throw new ConnectionError('embedded `memoryLimit` requires @zvndev/powdb-embedded ≥ 0.7.1.');
2254
1791
  }
2255
1792
  db = mod.Database.openWithMemoryLimit(dir, memoryLimit);
2256
1793
  }
@@ -2261,11 +1798,11 @@ async function openEmbeddedPool(target, poolOptions = {}, assumeEngineVersion, i
2261
1798
  catch (err) {
2262
1799
  if (err instanceof ConnectionError)
2263
1800
  throw err;
2264
- throw new ConnectionError(`[turbine] PowDB embedded could not open data dir "${dir}": ${err.message}`);
1801
+ throw new ConnectionError(`PowDB embedded could not open data dir "${dir}": ${err.message}`);
2265
1802
  }
2266
1803
  if (syncMode !== undefined) {
2267
1804
  if (typeof db.setSyncMode !== 'function') {
2268
- throw new ConnectionError('[turbine] embedded `syncMode` requires @zvndev/powdb-embedded ≥ 0.7.1 (the installed addon has no setSyncMode).');
1805
+ throw new ConnectionError('embedded `syncMode` requires @zvndev/powdb-embedded ≥ 0.7.1 (the installed addon has no setSyncMode).');
2269
1806
  }
2270
1807
  db.setSyncMode(syncMode);
2271
1808
  }