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.
- package/dist/cjs/cli/index.js +2 -2
- package/dist/cjs/cli/migrate.js +8 -8
- package/dist/cjs/cli/studio.js +6 -3
- package/dist/cjs/client.js +10 -10
- package/dist/cjs/dialect.js +1 -1
- package/dist/cjs/errors.d.ts +2 -2
- package/dist/cjs/errors.js +21 -25
- package/dist/cjs/generate.js +1 -1
- package/dist/cjs/introspect.js +5 -5
- package/dist/cjs/mssql.js +12 -12
- package/dist/cjs/mysql.js +5 -5
- package/dist/cjs/nested-write.js +31 -31
- package/dist/cjs/observe.js +1 -1
- package/dist/cjs/pipeline-submittable.js +5 -1
- package/dist/cjs/pipeline.js +8 -0
- package/dist/cjs/powdb-introspect.d.ts +1 -1
- package/dist/cjs/powdb-introspect.js +4 -4
- package/dist/cjs/powdb-shared.d.ts +348 -0
- package/dist/cjs/powdb-shared.js +587 -0
- package/dist/cjs/powdb.d.ts +12 -299
- package/dist/cjs/powdb.js +106 -567
- package/dist/cjs/powql.d.ts +1 -1
- package/dist/cjs/powql.js +89 -89
- package/dist/cjs/prisma-compat.js +16 -16
- package/dist/cjs/query/aggregates.js +19 -20
- package/dist/cjs/query/batched-loader.js +8 -8
- package/dist/cjs/query/builder.js +8 -8
- package/dist/cjs/query/compound-unique.js +2 -2
- package/dist/cjs/query/filters.js +1 -1
- package/dist/cjs/query/relations.js +60 -48
- package/dist/cjs/query/types.js +4 -4
- package/dist/cjs/query/utils.js +5 -5
- package/dist/cjs/query/warn-registry.d.ts +7 -0
- package/dist/cjs/query/warn-registry.js +7 -0
- package/dist/cjs/query/where-compile.js +1 -1
- package/dist/cjs/query/where.js +30 -31
- package/dist/cjs/query/writes.js +7 -7
- package/dist/cjs/realtime.js +4 -4
- package/dist/cjs/schema-metadata.js +1 -1
- package/dist/cjs/schema-sql.js +3 -3
- package/dist/cjs/seed.js +1 -1
- package/dist/cjs/sqlite.js +7 -7
- package/dist/cjs/typed-sql.js +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/cli/migrate.js +8 -8
- package/dist/cli/studio.js +6 -3
- package/dist/client.js +10 -10
- package/dist/dialect.js +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +21 -25
- package/dist/generate.js +1 -1
- package/dist/introspect.js +5 -5
- package/dist/mssql.js +12 -12
- package/dist/mysql.js +5 -5
- package/dist/nested-write.js +31 -31
- package/dist/observe.js +1 -1
- package/dist/pipeline-submittable.js +6 -2
- package/dist/pipeline.js +9 -1
- package/dist/powdb-introspect.d.ts +1 -1
- package/dist/powdb-introspect.js +2 -2
- package/dist/powdb-shared.d.ts +348 -0
- package/dist/powdb-shared.js +570 -0
- package/dist/powdb.d.ts +12 -299
- package/dist/powdb.js +71 -534
- package/dist/powql.d.ts +1 -1
- package/dist/powql.js +43 -43
- package/dist/prisma-compat.js +16 -16
- package/dist/query/aggregates.js +19 -20
- package/dist/query/batched-loader.js +8 -8
- package/dist/query/builder.js +8 -8
- package/dist/query/compound-unique.js +2 -2
- package/dist/query/filters.js +1 -1
- package/dist/query/relations.js +60 -48
- package/dist/query/types.js +4 -4
- package/dist/query/utils.js +5 -5
- package/dist/query/warn-registry.d.ts +7 -0
- package/dist/query/warn-registry.js +7 -0
- package/dist/query/where-compile.js +1 -1
- package/dist/query/where.js +30 -31
- package/dist/query/writes.js +7 -7
- package/dist/realtime.js +4 -4
- package/dist/schema-metadata.js +1 -1
- package/dist/schema-sql.js +3 -3
- package/dist/seed.js +1 -1
- package/dist/sqlite.js +7 -7
- package/dist/typed-sql.js +1 -1
- 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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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: `
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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: `
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
804
|
+
return new ValidationError(`PowDB query rejected: ${msg}`);
|
|
1251
805
|
default:
|
|
1252
|
-
return err instanceof Error ? err : new ConnectionError(`
|
|
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('
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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('
|
|
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('
|
|
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("
|
|
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("
|
|
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("
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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(`
|
|
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('
|
|
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
|
}
|