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/cjs/powdb.js
CHANGED
|
@@ -90,25 +90,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
90
90
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
91
91
|
};
|
|
92
92
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
93
|
-
exports.PowqlInterface = exports.introspectPowdbDatabase = exports.PowdbEmbeddedPool = exports.POWQL_LEXER_TESTED_CEILING = exports.PowdbPool = exports.DEFAULT_TX_QUEUE_TIMEOUT_MS = exports.
|
|
93
|
+
exports.PowqlInterface = exports.introspectPowdbDatabase = exports.PowdbEmbeddedPool = exports.POWQL_LEXER_TESTED_CEILING = exports.PowdbPool = exports.DEFAULT_TX_QUEUE_TIMEOUT_MS = exports.POWQL_MAX_NESTING_DEPTH = exports.MIN_POWDB_VERSION = exports.powdbDialect = exports.rowToEntity = exports.requireCapability = exports.quotePowqlIdent = exports.quotePowqlDotted = exports.powqlColumnType = exports.PowdbJsonParam = exports.PowdbFloatParam = exports.POWQL_KEYWORDS = exports.isStaleFramePowdbError = exports.isPowdbDatetimeColumn = exports.isJsonColumn = exports.coerceValue = exports.coerceNativeValue = exports.ALL_POWDB_CAPABILITIES = void 0;
|
|
94
94
|
exports.parsePowdbUrl = parsePowdbUrl;
|
|
95
95
|
exports.assertSupportedPowdbVersion = assertSupportedPowdbVersion;
|
|
96
96
|
exports.capabilitiesFromVersion = capabilitiesFromVersion;
|
|
97
|
-
exports.requireCapability = requireCapability;
|
|
98
|
-
exports.isJsonColumn = isJsonColumn;
|
|
99
|
-
exports.powqlColumnType = powqlColumnType;
|
|
100
|
-
exports.isPowdbDatetimeColumn = isPowdbDatetimeColumn;
|
|
101
|
-
exports.quotePowqlIdent = quotePowqlIdent;
|
|
102
|
-
exports.quotePowqlDotted = quotePowqlDotted;
|
|
103
97
|
exports.deriveDesiredLinks = deriveDesiredLinks;
|
|
104
98
|
exports.powdbLinkStatement = powdbLinkStatement;
|
|
105
99
|
exports.powqlSchemaDDL = powqlSchemaDDL;
|
|
106
100
|
exports.applyPowdbLinks = applyPowdbLinks;
|
|
107
|
-
exports.coerceValue = coerceValue;
|
|
108
|
-
exports.coerceNativeValue = coerceNativeValue;
|
|
109
|
-
exports.rowToEntity = rowToEntity;
|
|
110
101
|
exports.wrapPowdbError = wrapPowdbError;
|
|
111
|
-
exports.isStaleFramePowdbError = isStaleFramePowdbError;
|
|
112
102
|
exports.encodePowqlLiteral = encodePowqlLiteral;
|
|
113
103
|
exports.materializePowql = materializePowql;
|
|
114
104
|
exports.turbinePowDB = turbinePowDB;
|
|
@@ -117,8 +107,29 @@ const client_js_1 = require("./client.js");
|
|
|
117
107
|
const dialect_js_1 = require("./dialect.js");
|
|
118
108
|
const errors_js_1 = require("./errors.js");
|
|
119
109
|
const optional_peer_import_cjs_1 = __importDefault(require("./optional-peer-import.cjs"));
|
|
110
|
+
const powdb_shared_js_1 = require("./powdb-shared.js");
|
|
120
111
|
const warn_registry_js_1 = require("./query/warn-registry.js");
|
|
121
112
|
const schema_js_1 = require("./schema.js");
|
|
113
|
+
// The shared PowDB primitives live in a leaf module (see powdb-shared.ts): this
|
|
114
|
+
// file re-exports from powql.ts and powdb-introspect.ts, and both of those need
|
|
115
|
+
// the primitives, so keeping them here made the three a runtime cycle. Re-export
|
|
116
|
+
// every name that was public before the split, under its original name, so the
|
|
117
|
+
// `turbine-orm/powdb` surface is unchanged.
|
|
118
|
+
var powdb_shared_js_2 = require("./powdb-shared.js");
|
|
119
|
+
Object.defineProperty(exports, "ALL_POWDB_CAPABILITIES", { enumerable: true, get: function () { return powdb_shared_js_2.ALL_POWDB_CAPABILITIES; } });
|
|
120
|
+
Object.defineProperty(exports, "coerceNativeValue", { enumerable: true, get: function () { return powdb_shared_js_2.coerceNativeValue; } });
|
|
121
|
+
Object.defineProperty(exports, "coerceValue", { enumerable: true, get: function () { return powdb_shared_js_2.coerceValue; } });
|
|
122
|
+
Object.defineProperty(exports, "isJsonColumn", { enumerable: true, get: function () { return powdb_shared_js_2.isJsonColumn; } });
|
|
123
|
+
Object.defineProperty(exports, "isPowdbDatetimeColumn", { enumerable: true, get: function () { return powdb_shared_js_2.isPowdbDatetimeColumn; } });
|
|
124
|
+
Object.defineProperty(exports, "isStaleFramePowdbError", { enumerable: true, get: function () { return powdb_shared_js_2.isStaleFramePowdbError; } });
|
|
125
|
+
Object.defineProperty(exports, "POWQL_KEYWORDS", { enumerable: true, get: function () { return powdb_shared_js_2.POWQL_KEYWORDS; } });
|
|
126
|
+
Object.defineProperty(exports, "PowdbFloatParam", { enumerable: true, get: function () { return powdb_shared_js_2.PowdbFloatParam; } });
|
|
127
|
+
Object.defineProperty(exports, "PowdbJsonParam", { enumerable: true, get: function () { return powdb_shared_js_2.PowdbJsonParam; } });
|
|
128
|
+
Object.defineProperty(exports, "powqlColumnType", { enumerable: true, get: function () { return powdb_shared_js_2.powqlColumnType; } });
|
|
129
|
+
Object.defineProperty(exports, "quotePowqlDotted", { enumerable: true, get: function () { return powdb_shared_js_2.quotePowqlDotted; } });
|
|
130
|
+
Object.defineProperty(exports, "quotePowqlIdent", { enumerable: true, get: function () { return powdb_shared_js_2.quotePowqlIdent; } });
|
|
131
|
+
Object.defineProperty(exports, "requireCapability", { enumerable: true, get: function () { return powdb_shared_js_2.requireCapability; } });
|
|
132
|
+
Object.defineProperty(exports, "rowToEntity", { enumerable: true, get: function () { return powdb_shared_js_2.rowToEntity; } });
|
|
122
133
|
/**
|
|
123
134
|
* Capability descriptor for PowDB. PowQL generation is owned by
|
|
124
135
|
* {@link PowqlInterface} (not the SQL `Dialect`), so this dialect exists only to
|
|
@@ -177,40 +188,6 @@ function throwNoNestedTransaction() {
|
|
|
177
188
|
throw new errors_js_1.UnsupportedFeatureError('nested transactions', 'powdb', 'PowDB is single-writer, it has one global write lock and no savepoints. ' +
|
|
178
189
|
'Complete the open transaction before starting another; do not nest `$transaction` calls.');
|
|
179
190
|
}
|
|
180
|
-
/**
|
|
181
|
-
* Marker wrapper for a value bound to a `float` column. The networked driver
|
|
182
|
-
* unwraps it to the plain number (the wire param is unchanged), but the
|
|
183
|
-
* *embedded* literal encoder reads it to emit a float-form PowQL literal (`42`
|
|
184
|
-
* → `42.0`) so an integer-valued float column stays unambiguously a float.
|
|
185
|
-
* Constructed in {@link PowqlInterface.param}.
|
|
186
|
-
*/
|
|
187
|
-
class PowdbFloatParam {
|
|
188
|
-
value;
|
|
189
|
-
constructor(value) {
|
|
190
|
-
this.value = value;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
exports.PowdbFloatParam = PowdbFloatParam;
|
|
194
|
-
/**
|
|
195
|
-
* Marker wrapper for a JS object/array bound to a `json` document column. Both
|
|
196
|
-
* transports serialize `value` with `JSON.stringify` and send the text as a
|
|
197
|
-
* `str` param / string literal, exactly how the PowDB docs insert a json
|
|
198
|
-
* document (the engine validates it as JSON text and stores the canonical
|
|
199
|
-
* binary form). Constructed in {@link PowqlInterface.param} when the target
|
|
200
|
-
* column is `json` and the value is a non-null object/array; a JS string
|
|
201
|
-
* written to a json column passes through RAW (same contract as pg jsonb,
|
|
202
|
-
* pass `'"x"'` to store the JSON string `"x"`), and `null` stays `null`.
|
|
203
|
-
*/
|
|
204
|
-
class PowdbJsonParam {
|
|
205
|
-
value;
|
|
206
|
-
column;
|
|
207
|
-
/** `column` is diagnostic only: it names the target column when serialization fails. */
|
|
208
|
-
constructor(value, column) {
|
|
209
|
-
this.value = value;
|
|
210
|
-
this.column = column;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
exports.PowdbJsonParam = PowdbJsonParam;
|
|
214
191
|
/** Minimum PowDB server version the networked transport requires. */
|
|
215
192
|
exports.MIN_POWDB_VERSION = '0.7.0';
|
|
216
193
|
/**
|
|
@@ -228,7 +205,7 @@ function parsePowdbUrl(connectionString) {
|
|
|
228
205
|
throw new errors_js_1.ConnectionError((0, errors_js_1.malformedConnectionStringMessage)('PowDB', 'powdb://user:password@127.0.0.1:5433/app'));
|
|
229
206
|
}
|
|
230
207
|
if (u.protocol !== 'powdb:') {
|
|
231
|
-
throw new errors_js_1.ConnectionError(`
|
|
208
|
+
throw new errors_js_1.ConnectionError(`PowDB connection string must use the powdb:// scheme (got "${u.protocol}//…").`);
|
|
232
209
|
}
|
|
233
210
|
const opts = {
|
|
234
211
|
host: u.hostname || '127.0.0.1',
|
|
@@ -259,9 +236,12 @@ function assertSupportedPowdbVersion(version) {
|
|
|
259
236
|
// 0.7.0 is the floor; >= 0.7 (or any 1.x+) passes.
|
|
260
237
|
if (major > 0 || (major === 0 && minor >= 7))
|
|
261
238
|
return;
|
|
262
|
-
throw new errors_js_1.ConnectionError(`
|
|
239
|
+
throw new errors_js_1.ConnectionError(`turbine-orm/powdb requires PowDB >= ${exports.MIN_POWDB_VERSION}; the server reports "${version}". ` +
|
|
263
240
|
'Upgrade the PowDB server (0.7.0 added the `returning` keyword and the int->float coercion fix Turbine relies on).');
|
|
264
241
|
}
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// Capability gating: per-version / per-transport feature flags
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
265
245
|
/**
|
|
266
246
|
* PowQL's parser bounds the SHAPE of the AST it produces, not just its own
|
|
267
247
|
* recursion: an `and` / `or` chain is parsed iteratively but re-wraps its
|
|
@@ -290,66 +270,6 @@ function assertSupportedPowdbVersion(version) {
|
|
|
290
270
|
* powql.ts.
|
|
291
271
|
*/
|
|
292
272
|
exports.POWQL_MAX_NESTING_DEPTH = 64;
|
|
293
|
-
/**
|
|
294
|
-
* Minimum engine version each gated feature needs, for the E017 hint text.
|
|
295
|
-
* Most gates carry a `major.minor` floor (patch-insensitive); the two link
|
|
296
|
-
* lanes carry a `major.minor.patch` floor (`0.19.1`) because the listing
|
|
297
|
-
* statement and the safe traversal semantics landed in the PATCH release, not
|
|
298
|
-
* in 0.19.0. {@link atLeastVersion} compares all three components, so a
|
|
299
|
-
* `major.minor` floor still matches every patch of that minor.
|
|
300
|
-
*/
|
|
301
|
-
const POWDB_FEATURE_MIN_VERSION = {
|
|
302
|
-
introspection: '0.10',
|
|
303
|
-
jsonDocs: '0.12',
|
|
304
|
-
docFieldIndexes: '0.13',
|
|
305
|
-
serverJoins: '0.13',
|
|
306
|
-
nestedProjections: '0.18',
|
|
307
|
-
entityLinks: '0.19',
|
|
308
|
-
linkIntrospection: '0.19.1',
|
|
309
|
-
linkPaths: '0.19.1',
|
|
310
|
-
datetimeCompare: '0.20',
|
|
311
|
-
projectedCountNonNull: '0.20',
|
|
312
|
-
};
|
|
313
|
-
/**
|
|
314
|
-
* Trusted-caller default: every FEATURE gate on, engine version unknown. Used
|
|
315
|
-
* for a directly-constructed {@link PowdbPool} / {@link PowdbEmbeddedPool} that
|
|
316
|
-
* did not go through {@link turbinePowDB}'s version probe (e.g. an injected
|
|
317
|
-
* pool, or a unit-test pool). `nativeRaw` stays OFF here because it flips the
|
|
318
|
-
* actual wire path and must only be enabled after a real server-version probe,
|
|
319
|
-
* never inferred from a bare construction. `nestedProjections` stays OFF for
|
|
320
|
-
* the same reason: it changes the generated PowQL for every `with` query, and
|
|
321
|
-
* an unprobed engine below 0.18 would reject the syntax outright.
|
|
322
|
-
* `entityLinks` stays OFF for a stronger reason still: declaring a link
|
|
323
|
-
* one-way-upgrades the on-disk catalog to v7 and locks out pre-0.19 binaries,
|
|
324
|
-
* so it must only ever light up behind a real version probe.
|
|
325
|
-
* `linkIntrospection` / `linkPaths` stay OFF for the same probe-only discipline:
|
|
326
|
-
* `linkPaths` flips real query generation (a to-one `with` compiling to link
|
|
327
|
-
* projections), and `linkIntrospection` is only meaningful once genuinely
|
|
328
|
-
* probed, so both must come from a real version resolution, never a bare
|
|
329
|
-
* construction.
|
|
330
|
-
* `datetimeCompare` / `projectedCountNonNull` stay ON here for the same
|
|
331
|
-
* trusted-caller reason as `jsonDocs` and `serverJoins`. Neither is a fallback
|
|
332
|
-
* gate: with the flag OFF the affected query is REFUSED, not served by some
|
|
333
|
-
* other statement, so defaulting them off would break working queries rather
|
|
334
|
-
* than protect anything. Every path that can learn the engine version
|
|
335
|
-
* (`turbinePowDB`, embedded or networked) resolves them from a real probe; this
|
|
336
|
-
* fallback only covers a hand-constructed or injected pool, whose owner is
|
|
337
|
-
* asserting the engine is current.
|
|
338
|
-
*/
|
|
339
|
-
exports.ALL_POWDB_CAPABILITIES = {
|
|
340
|
-
engineVersion: null,
|
|
341
|
-
jsonDocs: true,
|
|
342
|
-
docFieldIndexes: true,
|
|
343
|
-
introspection: true,
|
|
344
|
-
serverJoins: true,
|
|
345
|
-
nestedProjections: false,
|
|
346
|
-
entityLinks: false,
|
|
347
|
-
linkIntrospection: false,
|
|
348
|
-
linkPaths: false,
|
|
349
|
-
datetimeCompare: true,
|
|
350
|
-
projectedCountNonNull: true,
|
|
351
|
-
nativeRaw: false,
|
|
352
|
-
};
|
|
353
273
|
/** Parse a PowDB semver prefix (`0.13.0`, `0.13`, `1.2.3-rc`) into components, or `null`. */
|
|
354
274
|
function parsePowdbSemver(version) {
|
|
355
275
|
const m = /^(\d+)\.(\d+)(?:\.(\d+))?/.exec(String(version ?? '').trim());
|
|
@@ -414,270 +334,6 @@ function capabilitiesFromVersion(version, opts = {}) {
|
|
|
414
334
|
nativeRaw: Boolean(opts.hasNativeRaw) && atLeastVersion(sem, 0, 13),
|
|
415
335
|
};
|
|
416
336
|
}
|
|
417
|
-
/**
|
|
418
|
-
* Throw a version-hinting {@link UnsupportedFeatureError} (E017) when a gated
|
|
419
|
-
* PowQL feature is used on an engine that does not support it. Keeps old engines
|
|
420
|
-
* getting clean typed errors instead of raw PowQL parse failures.
|
|
421
|
-
*
|
|
422
|
-
* The error's first sentence already names the feature (`<feature> is
|
|
423
|
-
* unsupported on "PowDB".`), so the hint says "Requires PowDB >= x" rather than
|
|
424
|
-
* repeating the label: a long feature description read twice in one message
|
|
425
|
-
* (`per-field \`_count\` … is unsupported … per-field \`_count\` … requires …`)
|
|
426
|
-
* buries the version floor that is the actionable part.
|
|
427
|
-
*
|
|
428
|
-
* `extra` appends one more sentence for gates that have a workaround worth
|
|
429
|
-
* naming (e.g. the read path that answers the same query without the gated
|
|
430
|
-
* comparison).
|
|
431
|
-
*/
|
|
432
|
-
function requireCapability(caps, key, feature, extra) {
|
|
433
|
-
if (caps[key])
|
|
434
|
-
return;
|
|
435
|
-
const min = POWDB_FEATURE_MIN_VERSION[key];
|
|
436
|
-
const reported = caps.engineVersion
|
|
437
|
-
? `this connection reports ${caps.engineVersion}`
|
|
438
|
-
: 'this connection could not report a version';
|
|
439
|
-
throw new errors_js_1.UnsupportedFeatureError(feature, 'PowDB', `Requires PowDB >= ${min}; ${reported}. Upgrade powdb-server / @zvndev/powdb-embedded ` +
|
|
440
|
-
`(or pass \`assumeEngineVersion\` if the version cannot be detected).${extra ? ` ${extra}` : ''}`);
|
|
441
|
-
}
|
|
442
|
-
/**
|
|
443
|
-
* Does this column map to PowDB's native `json` document type? A Postgres
|
|
444
|
-
* `json`/`jsonb` type (via `dialectType`/`pgType`) is authoritative; otherwise
|
|
445
|
-
* the tsType heuristic (`Record<…>`, `object`, `unknown`, an object/array
|
|
446
|
-
* literal) that the four scalar branches do not claim. Array columns never map
|
|
447
|
-
* to json, a PowDB array only exists INSIDE a json document, so a Postgres
|
|
448
|
-
* array column has no PowDB shape and still throws in {@link powqlColumnType}.
|
|
449
|
-
*/
|
|
450
|
-
function isJsonColumn(col) {
|
|
451
|
-
if (col.isArray)
|
|
452
|
-
return false;
|
|
453
|
-
const dbType = (col.dialectType ?? col.pgType ?? '').toLowerCase();
|
|
454
|
-
if (dbType === 'json' || dbType === 'jsonb')
|
|
455
|
-
return true;
|
|
456
|
-
const ts = col.tsType.replace(/\s*\|\s*null$/i, '').trim();
|
|
457
|
-
if (ts === 'Date' || ts === 'boolean' || ts === 'number' || ts === 'bigint' || ts === 'string')
|
|
458
|
-
return false;
|
|
459
|
-
if (ts === 'Buffer' || ts === 'Uint8Array')
|
|
460
|
-
return false;
|
|
461
|
-
return /Record<|object|unknown|\[\]|\{/.test(ts);
|
|
462
|
-
}
|
|
463
|
-
/**
|
|
464
|
-
* Map a Turbine column to the PowQL DDL type used in `defineSchema` →
|
|
465
|
-
* `type T { … }`. Turbine never emits PowDB's `uuid`/`datetime`/`bytes` types,
|
|
466
|
-
* which cannot hold client-supplied values on the wire (no literal, no cast):
|
|
467
|
-
* - `Date` → `int` (epoch micros) - `boolean` → `bool`
|
|
468
|
-
* - integral `number`/`bigint` → `int` - fractional `number` → `float`
|
|
469
|
-
* - JSON / object columns → `json` (native PowDB document type, ≥ 0.12)
|
|
470
|
-
* - everything else (incl. UUID/PK strings) → `str`
|
|
471
|
-
* Array (non-json) and bytes columns throw, they have no PowDB equivalent.
|
|
472
|
-
*/
|
|
473
|
-
function powqlColumnType(col) {
|
|
474
|
-
if (col.isArray) {
|
|
475
|
-
throw new errors_js_1.ValidationError(`[turbine] Column "${col.name}" is an array, PowDB has no array type. Arrays are unsupported on the PowDB backend.`);
|
|
476
|
-
}
|
|
477
|
-
if (isJsonColumn(col))
|
|
478
|
-
return 'json';
|
|
479
|
-
const ts = col.tsType.replace(/\s*\|\s*null$/i, '').trim();
|
|
480
|
-
if (ts === 'Date')
|
|
481
|
-
return 'int'; // epoch micros
|
|
482
|
-
if (ts === 'boolean')
|
|
483
|
-
return 'bool';
|
|
484
|
-
if (ts === 'number')
|
|
485
|
-
return isFloatColumn(col) ? 'float' : 'int';
|
|
486
|
-
if (ts === 'bigint')
|
|
487
|
-
return 'int';
|
|
488
|
-
if (ts === 'string')
|
|
489
|
-
return 'str';
|
|
490
|
-
if (ts === 'Buffer' || ts === 'Uint8Array') {
|
|
491
|
-
throw new errors_js_1.ValidationError(`[turbine] Column "${col.name}" is binary, PowDB cannot store client-supplied bytes on the wire. Use a string (e.g. base64) instead.`);
|
|
492
|
-
}
|
|
493
|
-
return 'str';
|
|
494
|
-
}
|
|
495
|
-
/** Heuristic: does this numeric column hold fractional values (→ PowQL `float`)? */
|
|
496
|
-
function isFloatColumn(col) {
|
|
497
|
-
const t = (col.dialectType ?? col.pgType ?? '').toLowerCase();
|
|
498
|
-
return /float|double|real|numeric|decimal|money/.test(t);
|
|
499
|
-
}
|
|
500
|
-
/** Is a column stored as `int` epoch micros but surfaced as a JS `Date`? */
|
|
501
|
-
function isDateColumn(col) {
|
|
502
|
-
return col.tsType.replace(/\s*\|\s*null$/i, '').trim() === 'Date';
|
|
503
|
-
}
|
|
504
|
-
/**
|
|
505
|
-
* Is this column stored in PowDB's NATIVE `datetime` type (as opposed to the
|
|
506
|
-
* `int` epoch micros Turbine's own DDL emits for a `Date` column)?
|
|
507
|
-
*
|
|
508
|
-
* Only the literal PowQL type name counts. `powqlColumnType` never returns
|
|
509
|
-
* `datetime`, so a Turbine-provisioned table can never have one; the shapes that
|
|
510
|
-
* do are a table created outside Turbine and read back through
|
|
511
|
-
* `introspectPowdbDatabase` (which maps `datetime` → `{ tsType: 'Date',
|
|
512
|
-
* dialectType: 'datetime' }`), or hand-written metadata declaring it. Deliberately
|
|
513
|
-
* strict: a Postgres-sourced `timestamptz` column is DDL'd as PowQL `int`, so it
|
|
514
|
-
* is NOT a PowDB datetime and must not be caught here.
|
|
515
|
-
*
|
|
516
|
-
* Matters because comparing a datetime column against the integer timestamp
|
|
517
|
-
* literal Turbine binds was silently wrong below engine 0.20 (see
|
|
518
|
-
* {@link PowdbCapabilities.datetimeCompare}).
|
|
519
|
-
*/
|
|
520
|
-
function isPowdbDatetimeColumn(col) {
|
|
521
|
-
return (col.dialectType ?? col.pgType ?? '').toLowerCase() === 'datetime';
|
|
522
|
-
}
|
|
523
|
-
/**
|
|
524
|
-
* Generate PowQL DDL (`type T { … }`) for every table in a schema. Used to
|
|
525
|
-
* provision a PowDB database from a code-first `defineSchema`/`SchemaMetadata`
|
|
526
|
-
* (PowDB has no migration runner yet). The primary key column is declared
|
|
527
|
-
* `required unique`; non-nullable columns are `required`. A server-generated
|
|
528
|
-
* column ({@link ColumnMetadata.isGenerated}) that maps to PowQL `int` gets the
|
|
529
|
-
* `auto` modifier, so PowDB assigns a monotonic id on insert and Turbine stops
|
|
530
|
-
* synthesizing a client-side value for it.
|
|
531
|
-
*/
|
|
532
|
-
/**
|
|
533
|
-
* PowQL reserved words, the v0.10 lexer keyword table from POWQL.md's
|
|
534
|
-
* "Reserved Words and Quoting" section, including the v0.10 additions
|
|
535
|
-
* `schema` and `describe`. Keyword matching is case-sensitive in the lexer,
|
|
536
|
-
* so only the exact lowercase form collides.
|
|
537
|
-
*/
|
|
538
|
-
exports.POWQL_KEYWORDS = new Set([
|
|
539
|
-
'abs',
|
|
540
|
-
'add',
|
|
541
|
-
'alter',
|
|
542
|
-
'and',
|
|
543
|
-
'as',
|
|
544
|
-
'asc',
|
|
545
|
-
'auto',
|
|
546
|
-
'avg',
|
|
547
|
-
'begin',
|
|
548
|
-
'between',
|
|
549
|
-
'case',
|
|
550
|
-
'cast',
|
|
551
|
-
'ceil',
|
|
552
|
-
'column',
|
|
553
|
-
'commit',
|
|
554
|
-
'concat',
|
|
555
|
-
'conflict',
|
|
556
|
-
'count',
|
|
557
|
-
'cross',
|
|
558
|
-
'date_add',
|
|
559
|
-
'date_diff',
|
|
560
|
-
'default',
|
|
561
|
-
'delete',
|
|
562
|
-
'dense_rank',
|
|
563
|
-
'desc',
|
|
564
|
-
'describe',
|
|
565
|
-
'distinct',
|
|
566
|
-
'drop',
|
|
567
|
-
'else',
|
|
568
|
-
'end',
|
|
569
|
-
'exists',
|
|
570
|
-
'explain',
|
|
571
|
-
'extract',
|
|
572
|
-
'false',
|
|
573
|
-
'filter',
|
|
574
|
-
'floor',
|
|
575
|
-
'group',
|
|
576
|
-
'having',
|
|
577
|
-
'in',
|
|
578
|
-
'index',
|
|
579
|
-
'inner',
|
|
580
|
-
'insert',
|
|
581
|
-
'is',
|
|
582
|
-
'join',
|
|
583
|
-
'left',
|
|
584
|
-
'length',
|
|
585
|
-
'let',
|
|
586
|
-
'like',
|
|
587
|
-
'limit',
|
|
588
|
-
'link',
|
|
589
|
-
'lower',
|
|
590
|
-
'match',
|
|
591
|
-
'materialize',
|
|
592
|
-
'materialized',
|
|
593
|
-
'max',
|
|
594
|
-
'min',
|
|
595
|
-
'multi',
|
|
596
|
-
'not',
|
|
597
|
-
'now',
|
|
598
|
-
'null',
|
|
599
|
-
'offset',
|
|
600
|
-
'on',
|
|
601
|
-
'or',
|
|
602
|
-
'order',
|
|
603
|
-
'outer',
|
|
604
|
-
'over',
|
|
605
|
-
'partition',
|
|
606
|
-
'pow',
|
|
607
|
-
'rank',
|
|
608
|
-
'refresh',
|
|
609
|
-
'required',
|
|
610
|
-
'returning',
|
|
611
|
-
'right',
|
|
612
|
-
'rollback',
|
|
613
|
-
'round',
|
|
614
|
-
'row_number',
|
|
615
|
-
'schema',
|
|
616
|
-
'select',
|
|
617
|
-
'sqrt',
|
|
618
|
-
'substring',
|
|
619
|
-
'sum',
|
|
620
|
-
'then',
|
|
621
|
-
'transaction',
|
|
622
|
-
'trim',
|
|
623
|
-
'true',
|
|
624
|
-
'type',
|
|
625
|
-
'union',
|
|
626
|
-
'unique',
|
|
627
|
-
'update',
|
|
628
|
-
'upper',
|
|
629
|
-
'upsert',
|
|
630
|
-
'view',
|
|
631
|
-
'when',
|
|
632
|
-
]);
|
|
633
|
-
const POWQL_BARE_IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
634
|
-
/**
|
|
635
|
-
* Backtick-quote an identifier when PowQL would otherwise lex it as a keyword
|
|
636
|
-
* (or when it contains characters outside the bare-identifier grammar).
|
|
637
|
-
* Applied only in bare-identifier positions, DDL type/field names, index DDL,
|
|
638
|
-
* and `insert`/`update`/`upsert` assignment targets. Dotted references
|
|
639
|
-
* (`.col` in filters/projections/ordering) bypass keyword lookup on every
|
|
640
|
-
* engine version and deliberately stay bare for ≤0.9 compatibility. Backticks
|
|
641
|
-
* parse on PowDB ≥ 0.10; on older engines these names were already parse
|
|
642
|
-
* errors when emitted bare, so quoting is strictly an improvement.
|
|
643
|
-
*/
|
|
644
|
-
function quotePowqlIdent(name) {
|
|
645
|
-
if (name.includes('`')) {
|
|
646
|
-
// The lexer has no backtick escape inside a quoted identifier.
|
|
647
|
-
throw new errors_js_1.ValidationError(`[turbine] Identifier "${name}" contains a backtick, which PowQL cannot represent.`);
|
|
648
|
-
}
|
|
649
|
-
return exports.POWQL_KEYWORDS.has(name) || !POWQL_BARE_IDENT.test(name) ? `\`${name}\`` : name;
|
|
650
|
-
}
|
|
651
|
-
/**
|
|
652
|
-
* The DOTTED-position spelling of {@link quotePowqlIdent}: quote a name that
|
|
653
|
-
* falls outside the bare-identifier grammar, and only that.
|
|
654
|
-
*
|
|
655
|
-
* A dotted reference (`.col` in a filter, projection, `order`, `group`, or an
|
|
656
|
-
* `upsert on`) bypasses keyword lookup, so `.order` parses on every engine
|
|
657
|
-
* version and stays bare here, which is the ≤0.9 compatibility decision
|
|
658
|
-
* {@link quotePowqlIdent} documents and which this must not undo.
|
|
659
|
-
*
|
|
660
|
-
* What it does NOT excuse is interpolating the name RAW, which is what these
|
|
661
|
-
* sites used to do. Keyword-ness is a parsing question; a name outside
|
|
662
|
-
* `POWQL_BARE_IDENT` is a statement-integrity one, and that name is the only
|
|
663
|
-
* thing that can carry PowQL syntax into a statement whose values are all bound
|
|
664
|
-
* as `$N` params. Reaching it needs a hostile column name (an introspected
|
|
665
|
-
* database, a generator, a migration authored elsewhere) since names come from
|
|
666
|
-
* schema metadata, but "the names are trusted" is not the invariant the rest of
|
|
667
|
-
* this engine is written to. So: bare when the grammar allows it (byte-identical
|
|
668
|
-
* output for every ordinary and every keyword name), quoted when it does not,
|
|
669
|
-
* where the bare form was a parse error anyway. Verified against the engine that
|
|
670
|
-
* a quoted dotted reference parses everywhere the bare one does and yields the
|
|
671
|
-
* same result-column name.
|
|
672
|
-
*/
|
|
673
|
-
function quotePowqlDotted(name) {
|
|
674
|
-
if (POWQL_BARE_IDENT.test(name))
|
|
675
|
-
return name;
|
|
676
|
-
if (name.includes('`')) {
|
|
677
|
-
throw new errors_js_1.ValidationError(`[turbine] Identifier "${name}" contains a backtick, which PowQL cannot represent.`);
|
|
678
|
-
}
|
|
679
|
-
return `\`${name}\``;
|
|
680
|
-
}
|
|
681
337
|
/**
|
|
682
338
|
* Derive the entity links Turbine would declare from a schema's relations. One
|
|
683
339
|
* link per single-column hasMany / hasOne / belongsTo relation, owned by the
|
|
@@ -717,9 +373,18 @@ function deriveDesiredLinks(schema, onCollision) {
|
|
|
717
373
|
}
|
|
718
374
|
/** Render one {@link PowdbDesiredLink} as its create-only `link ...` DDL statement. */
|
|
719
375
|
function powdbLinkStatement(link) {
|
|
720
|
-
return (`link ${quotePowqlIdent(link.owner)}.${quotePowqlIdent(link.name)} -> ` +
|
|
721
|
-
`${quotePowqlIdent(link.target)} on ${quotePowqlIdent(link.localKey)} = ${quotePowqlIdent(link.targetKey)}`);
|
|
376
|
+
return (`link ${(0, powdb_shared_js_1.quotePowqlIdent)(link.owner)}.${(0, powdb_shared_js_1.quotePowqlIdent)(link.name)} -> ` +
|
|
377
|
+
`${(0, powdb_shared_js_1.quotePowqlIdent)(link.target)} on ${(0, powdb_shared_js_1.quotePowqlIdent)(link.localKey)} = ${(0, powdb_shared_js_1.quotePowqlIdent)(link.targetKey)}`);
|
|
722
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* Generate PowQL DDL (`type T { … }`) for every table in a schema. Used to
|
|
381
|
+
* provision a PowDB database from a code-first `defineSchema`/`SchemaMetadata`
|
|
382
|
+
* (PowDB has no migration runner yet). The primary key column is declared
|
|
383
|
+
* `required unique`; non-nullable columns are `required`. A server-generated
|
|
384
|
+
* column ({@link ColumnMetadata.isGenerated}) that maps to PowQL `int` gets the
|
|
385
|
+
* `auto` modifier, so PowDB assigns a monotonic id on insert and Turbine stops
|
|
386
|
+
* synthesizing a client-side value for it.
|
|
387
|
+
*/
|
|
723
388
|
function powqlSchemaDDL(schema, opts = {}) {
|
|
724
389
|
const caps = opts.capabilities;
|
|
725
390
|
const stmts = [];
|
|
@@ -732,12 +397,12 @@ function powqlSchemaDDL(schema, opts = {}) {
|
|
|
732
397
|
// cannot enforce the tuple's uniqueness at the engine level.
|
|
733
398
|
const pkIsSingle = meta.primaryKey.length === 1;
|
|
734
399
|
const fields = meta.columns.map((col) => {
|
|
735
|
-
const powqlType = powqlColumnType(col);
|
|
400
|
+
const powqlType = (0, powdb_shared_js_1.powqlColumnType)(col);
|
|
736
401
|
// Gate `json` columns behind the engine's jsonDocs capability when a
|
|
737
402
|
// caller supplied one, an old engine has no `json` type and would reject
|
|
738
403
|
// the DDL. Pure-function callers (no opts) emit unconditionally.
|
|
739
404
|
if (powqlType === 'json' && caps)
|
|
740
|
-
requireCapability(caps, 'jsonDocs', 'JSON document columns');
|
|
405
|
+
(0, powdb_shared_js_1.requireCapability)(caps, 'jsonDocs', 'JSON document columns');
|
|
741
406
|
const mods = [];
|
|
742
407
|
if (!col.nullable || pkSet.has(col.name))
|
|
743
408
|
mods.push('required');
|
|
@@ -748,9 +413,9 @@ function powqlSchemaDDL(schema, opts = {}) {
|
|
|
748
413
|
// a plain typed column (Turbine assigns the value client-side instead).
|
|
749
414
|
if (col.isGenerated && powqlType === 'int')
|
|
750
415
|
mods.push('auto');
|
|
751
|
-
return ` ${mods.join(' ')}${mods.length ? ' ' : ''}${quotePowqlIdent(col.name)}: ${powqlType}`;
|
|
416
|
+
return ` ${mods.join(' ')}${mods.length ? ' ' : ''}${(0, powdb_shared_js_1.quotePowqlIdent)(col.name)}: ${powqlType}`;
|
|
752
417
|
});
|
|
753
|
-
stmts.push(`type ${quotePowqlIdent(meta.name)} {\n${fields.join(',\n')}\n}`);
|
|
418
|
+
stmts.push(`type ${(0, powdb_shared_js_1.quotePowqlIdent)(meta.name)} {\n${fields.join(',\n')}\n}`);
|
|
754
419
|
// Track which single columns already carry a unique constraint (the
|
|
755
420
|
// single-column PK is inlined `required unique` in the type body above) so
|
|
756
421
|
// a redundant `add unique .col` is never emitted twice.
|
|
@@ -760,7 +425,7 @@ function powqlSchemaDDL(schema, opts = {}) {
|
|
|
760
425
|
// Secondary unique constraints (beyond the PK) become unique indexes.
|
|
761
426
|
for (const uniq of meta.uniqueColumns) {
|
|
762
427
|
if (uniq.length === 1 && !pkSet.has(uniq[0])) {
|
|
763
|
-
stmts.push(`alter ${quotePowqlIdent(meta.name)} add unique .${quotePowqlIdent(uniq[0])}`);
|
|
428
|
+
stmts.push(`alter ${(0, powdb_shared_js_1.quotePowqlIdent)(meta.name)} add unique .${(0, powdb_shared_js_1.quotePowqlIdent)(uniq[0])}`);
|
|
764
429
|
emittedUnique.add(uniq[0]);
|
|
765
430
|
}
|
|
766
431
|
}
|
|
@@ -775,13 +440,13 @@ function powqlSchemaDDL(schema, opts = {}) {
|
|
|
775
440
|
const kind = idx.unique ? 'unique' : 'index';
|
|
776
441
|
if (idx.docPath) {
|
|
777
442
|
if (caps)
|
|
778
|
-
requireCapability(caps, 'docFieldIndexes', 'JSON doc-field expression indexes');
|
|
443
|
+
(0, powdb_shared_js_1.requireCapability)(caps, 'docFieldIndexes', 'JSON doc-field expression indexes');
|
|
779
444
|
const column = idx.columns[0];
|
|
780
445
|
if (column === undefined) {
|
|
781
|
-
throw new errors_js_1.ValidationError(`
|
|
446
|
+
throw new errors_js_1.ValidationError(`Doc-field index "${idx.name}" on ${meta.name} has no target json column.`);
|
|
782
447
|
}
|
|
783
448
|
const segs = idx.docPath.map((s) => (typeof s === 'number' ? `->${s}` : `->${encodePowqlString(s)}`)).join('');
|
|
784
|
-
stmts.push(`alter ${quotePowqlIdent(meta.name)} add ${kind} (.${quotePowqlIdent(column)}${segs})`);
|
|
449
|
+
stmts.push(`alter ${(0, powdb_shared_js_1.quotePowqlIdent)(meta.name)} add ${kind} (.${(0, powdb_shared_js_1.quotePowqlIdent)(column)}${segs})`);
|
|
785
450
|
}
|
|
786
451
|
else {
|
|
787
452
|
// Plain column index. PowDB has no composite index (`add index` takes a
|
|
@@ -795,7 +460,7 @@ function powqlSchemaDDL(schema, opts = {}) {
|
|
|
795
460
|
// PK, or a column-level unique) would be a redundant duplicate, so skip it.
|
|
796
461
|
if (idx.unique && emittedUnique.has(column))
|
|
797
462
|
continue;
|
|
798
|
-
stmts.push(`alter ${quotePowqlIdent(meta.name)} add ${kind} .${quotePowqlIdent(column)}`);
|
|
463
|
+
stmts.push(`alter ${(0, powdb_shared_js_1.quotePowqlIdent)(meta.name)} add ${kind} .${(0, powdb_shared_js_1.quotePowqlIdent)(column)}`);
|
|
799
464
|
if (idx.unique)
|
|
800
465
|
emittedUnique.add(column);
|
|
801
466
|
}
|
|
@@ -806,7 +471,7 @@ function powqlSchemaDDL(schema, opts = {}) {
|
|
|
806
471
|
// when a caller supplied one (an old engine has no `link` statement).
|
|
807
472
|
if (opts.emitLinks) {
|
|
808
473
|
if (caps)
|
|
809
|
-
requireCapability(caps, 'entityLinks', 'entity link DDL (`emitLinks`)');
|
|
474
|
+
(0, powdb_shared_js_1.requireCapability)(caps, 'entityLinks', 'entity link DDL (`emitLinks`)');
|
|
810
475
|
const links = deriveDesiredLinks(schema, (owner, name) => {
|
|
811
476
|
if ((0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.powdbLinks, `collide:${owner}.${name}`)) {
|
|
812
477
|
console.warn(`[turbine] powqlSchemaDDL(emitLinks): relation "${name}" on "${owner}" collides with a column of the ` +
|
|
@@ -839,8 +504,8 @@ function powqlSchemaDDL(schema, opts = {}) {
|
|
|
839
504
|
async function applyPowdbLinks(exec, schema, options = {}) {
|
|
840
505
|
const caps = options.capabilities;
|
|
841
506
|
if (caps) {
|
|
842
|
-
requireCapability(caps, 'entityLinks', 'entity link DDL (`applyPowdbLinks`)');
|
|
843
|
-
requireCapability(caps, 'linkIntrospection', 'PowDB `schema links` introspection');
|
|
507
|
+
(0, powdb_shared_js_1.requireCapability)(caps, 'entityLinks', 'entity link DDL (`applyPowdbLinks`)');
|
|
508
|
+
(0, powdb_shared_js_1.requireCapability)(caps, 'linkIntrospection', 'PowDB `schema links` introspection');
|
|
844
509
|
}
|
|
845
510
|
const existing = await listPowdbLinks(exec);
|
|
846
511
|
const byOwnerName = new Map(existing.map((l) => [`${l.owner}\u0000${l.name}`, l]));
|
|
@@ -903,10 +568,10 @@ function powdbJsonText(param, position) {
|
|
|
903
568
|
json = JSON.stringify(param.value);
|
|
904
569
|
}
|
|
905
570
|
catch (err) {
|
|
906
|
-
throw new errors_js_1.ValidationError(`
|
|
571
|
+
throw new errors_js_1.ValidationError(`The json value ${where} cannot be serialized for PowDB: ${err.message}`);
|
|
907
572
|
}
|
|
908
573
|
if (json === undefined) {
|
|
909
|
-
throw new errors_js_1.ValidationError(`
|
|
574
|
+
throw new errors_js_1.ValidationError(`The json value ${where} serializes to nothing (JSON.stringify returned undefined); ` +
|
|
910
575
|
'write `null` explicitly instead.');
|
|
911
576
|
}
|
|
912
577
|
return json;
|
|
@@ -915,23 +580,23 @@ function powdbJsonText(param, position) {
|
|
|
915
580
|
function powdbDateMicros(value, position) {
|
|
916
581
|
const ms = value.getTime();
|
|
917
582
|
if (!Number.isFinite(ms)) {
|
|
918
|
-
throw new errors_js_1.ValidationError(`
|
|
583
|
+
throw new errors_js_1.ValidationError(`Invalid Date${position ? ` (${position})` : ''} cannot be encoded as a PowDB timestamp.`);
|
|
919
584
|
}
|
|
920
585
|
return BigInt(ms) * 1000n;
|
|
921
586
|
}
|
|
922
587
|
/** Coerce a JS value into a PowDB positional param (the write side). */
|
|
923
588
|
function toPowdbParam(value, col) {
|
|
924
|
-
if (value instanceof PowdbFloatParam)
|
|
589
|
+
if (value instanceof powdb_shared_js_1.PowdbFloatParam)
|
|
925
590
|
return value.value; // wire-side: a float column takes the plain number
|
|
926
591
|
// json document: serialize to canonical JSON text and bind as a str param,
|
|
927
592
|
// the engine validates it as JSON and stores the canonical binary form.
|
|
928
|
-
if (value instanceof PowdbJsonParam)
|
|
593
|
+
if (value instanceof powdb_shared_js_1.PowdbJsonParam)
|
|
929
594
|
return powdbJsonText(value);
|
|
930
595
|
if (value === undefined || value === null)
|
|
931
596
|
return null;
|
|
932
597
|
if (value instanceof Date)
|
|
933
598
|
return powdbDateMicros(value); // ms → micros (int column)
|
|
934
|
-
if (col && isDateColumn(col) && typeof value === 'number')
|
|
599
|
+
if (col && (0, powdb_shared_js_1.isDateColumn)(col) && typeof value === 'number')
|
|
935
600
|
return BigInt(value) * 1000n;
|
|
936
601
|
if (typeof value === 'boolean' ||
|
|
937
602
|
typeof value === 'number' ||
|
|
@@ -940,116 +605,7 @@ function toPowdbParam(value, col) {
|
|
|
940
605
|
return value;
|
|
941
606
|
}
|
|
942
607
|
// Objects/arrays have no PowDB representation.
|
|
943
|
-
throw new errors_js_1.ValidationError(`
|
|
944
|
-
}
|
|
945
|
-
/**
|
|
946
|
-
* Coerce a single PowDB wire string into the JS value its column type implies.
|
|
947
|
-
* Every PowDB value arrives as a string; NULL arrives as the bareword `"null"`.
|
|
948
|
-
* Metadata resolves the `"null"` ambiguity for nullable non-string columns.
|
|
949
|
-
*/
|
|
950
|
-
function coerceValue(raw, col) {
|
|
951
|
-
const ts = col.tsType.replace(/\s*\|\s*null$/i, '').trim();
|
|
952
|
-
const json = isJsonColumn(col);
|
|
953
|
-
// NULL bareword: unambiguous for non-string columns; for `str` we cannot tell a
|
|
954
|
-
// literal "null" from SQL NULL, so a nullable str of value "null" reads as null.
|
|
955
|
-
// For a `json` column the bareword `null` (a legacy-wire rendering shared by an
|
|
956
|
-
// absent value AND a top-level JSON-null document, documented residual,
|
|
957
|
-
// resolved on the native transport by the WireValue path) maps to null; a JSON
|
|
958
|
-
// string document "null" renders WITH quotes (`"null"`) and parses distinctly.
|
|
959
|
-
if (raw === 'null' && (json || ts !== 'string' || col.nullable))
|
|
960
|
-
return null;
|
|
961
|
-
if (json) {
|
|
962
|
-
try {
|
|
963
|
-
return JSON.parse(raw);
|
|
964
|
-
}
|
|
965
|
-
catch {
|
|
966
|
-
return raw; // defensive: canonical JSON text always parses
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
if (ts === 'Date') {
|
|
970
|
-
const micros = Number(raw);
|
|
971
|
-
return Number.isFinite(micros) ? new Date(micros / 1000) : null;
|
|
972
|
-
}
|
|
973
|
-
if (ts === 'boolean')
|
|
974
|
-
return raw === 'true';
|
|
975
|
-
if (ts === 'number') {
|
|
976
|
-
const n = Number(raw);
|
|
977
|
-
// int8 policy: keep precision-losing big integers as strings.
|
|
978
|
-
return Number.isSafeInteger(n) || !Number.isInteger(n) ? n : raw;
|
|
979
|
-
}
|
|
980
|
-
if (ts === 'bigint')
|
|
981
|
-
return BigInt(raw);
|
|
982
|
-
return raw; // string / uuid-as-string
|
|
983
|
-
}
|
|
984
|
-
/**
|
|
985
|
-
* Coerce a single cell that arrived over the NATIVE typed wire (decoded from a
|
|
986
|
-
* {@link PowdbWireValue}, so already a JS `bigint`/`number`/`boolean`/`string`/
|
|
987
|
-
* `NativeJson`/`Uint8Array`/`null`, never a bare `"null"` string). Unlike
|
|
988
|
-
* {@link coerceValue} this NEVER collapses the string `"null"` to `null`: an
|
|
989
|
-
* absent value already decoded to `null` (from the `empty` cell), so a genuine
|
|
990
|
-
* str `"null"` stays the string `"null"` (fixes the legacy-wire wart on the
|
|
991
|
-
* native transport). `datetime`-shaped cells (int micros) become `Date`; a
|
|
992
|
-
* bigint on a `number` column follows the int8 safe-integer policy.
|
|
993
|
-
*
|
|
994
|
-
* A date cell can also arrive as a DIGIT STRING: a nested-projection block's
|
|
995
|
-
* children ride a JSON array, and micros exceed `Number.MAX_SAFE_INTEGER`'s
|
|
996
|
-
* decimal comfort, so the engine renders them as a JSON string. Before that
|
|
997
|
-
* string was parsed here, a nested `with` handed back the raw micros text while
|
|
998
|
-
* the batched loader and the native join both handed back a `Date` (the same
|
|
999
|
-
* relation, three answers). Only an all-digit string is parsed; any other text
|
|
1000
|
-
* on a date column passes through untouched.
|
|
1001
|
-
*/
|
|
1002
|
-
function coerceNativeValue(value, col) {
|
|
1003
|
-
if (value === undefined || value === null)
|
|
1004
|
-
return null;
|
|
1005
|
-
if (isDateColumn(col)) {
|
|
1006
|
-
if (typeof value === 'bigint')
|
|
1007
|
-
return new Date(Number(value) / 1000);
|
|
1008
|
-
if (typeof value === 'number')
|
|
1009
|
-
return new Date(value / 1000);
|
|
1010
|
-
if (typeof value === 'string' && /^-?\d+$/.test(value))
|
|
1011
|
-
return new Date(Number(value) / 1000);
|
|
1012
|
-
return value;
|
|
1013
|
-
}
|
|
1014
|
-
const ts = col.tsType.replace(/\s*\|\s*null$/i, '').trim();
|
|
1015
|
-
if (typeof value === 'bigint') {
|
|
1016
|
-
if (ts === 'bigint')
|
|
1017
|
-
return value;
|
|
1018
|
-
if (ts === 'number') {
|
|
1019
|
-
const n = Number(value);
|
|
1020
|
-
return Number.isSafeInteger(n) ? n : value.toString(); // int8 policy: keep big ints as strings
|
|
1021
|
-
}
|
|
1022
|
-
return value;
|
|
1023
|
-
}
|
|
1024
|
-
return value; // number / boolean / string / NativeJson document / Uint8Array
|
|
1025
|
-
}
|
|
1026
|
-
/**
|
|
1027
|
-
* Map one raw PowDB row into a typed entity (camelCase fields, coerced values).
|
|
1028
|
-
* Only the columns present in `raw` are emitted, so partial `select`
|
|
1029
|
-
* projections round-trip unchanged. `native` selects the coercion policy: the
|
|
1030
|
-
* default `false` handles the legacy string wire (every cell is a string, via
|
|
1031
|
-
* {@link coerceValue}); `true` handles the native typed wire, where non-string
|
|
1032
|
-
* cells arrive pre-typed and go through {@link coerceNativeValue} (see F3).
|
|
1033
|
-
* Callers on the native transport pass `this.pool.capabilities.nativeRaw`.
|
|
1034
|
-
*/
|
|
1035
|
-
function rowToEntity(raw, meta, native = false) {
|
|
1036
|
-
const byName = new Map(meta.columns.map((c) => [c.name, c]));
|
|
1037
|
-
const out = {};
|
|
1038
|
-
for (const snake of Object.keys(raw)) {
|
|
1039
|
-
const col = byName.get(snake);
|
|
1040
|
-
const field = meta.reverseColumnMap[snake] ?? snake;
|
|
1041
|
-
const value = raw[snake];
|
|
1042
|
-
if (!col) {
|
|
1043
|
-
out[field] = value;
|
|
1044
|
-
}
|
|
1045
|
-
else if (native) {
|
|
1046
|
-
out[field] = coerceNativeValue(value, col);
|
|
1047
|
-
}
|
|
1048
|
-
else {
|
|
1049
|
-
out[field] = typeof value === 'string' ? coerceValue(value, col) : value;
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
return out;
|
|
608
|
+
throw new errors_js_1.ValidationError(`Value of type ${typeof value} cannot be bound as a PowDB parameter.`);
|
|
1053
609
|
}
|
|
1054
610
|
// ---------------------------------------------------------------------------
|
|
1055
611
|
// Error translation
|
|
@@ -1072,7 +628,7 @@ function rowToEntity(raw, meta, native = false) {
|
|
|
1072
628
|
*/
|
|
1073
629
|
function wrapPowdbError(err) {
|
|
1074
630
|
if (!err || typeof err !== 'object')
|
|
1075
|
-
return new errors_js_1.ConnectionError(`
|
|
631
|
+
return new errors_js_1.ConnectionError(`PowDB error: ${String(err)}`);
|
|
1076
632
|
const e = err;
|
|
1077
633
|
const msg = e.message ?? 'unknown PowDB error';
|
|
1078
634
|
// Unique-constraint, message-based on both transports.
|
|
@@ -1090,7 +646,7 @@ function wrapPowdbError(err) {
|
|
|
1090
646
|
// statement reaching an already-closed embedded handle) carry no .code:
|
|
1091
647
|
// classify by message so both transports surface E004.
|
|
1092
648
|
if (/pool closed|pool acquire timeout|database is closed/i.test(msg)) {
|
|
1093
|
-
return new errors_js_1.ConnectionError(`
|
|
649
|
+
return new errors_js_1.ConnectionError(`PowDB connection unavailable: ${msg}`, { cause: err });
|
|
1094
650
|
}
|
|
1095
651
|
// Server-side transaction-gate wait bound (PowDB ≥ 0.10, default 5s): another
|
|
1096
652
|
// connection held the single global write lock past the server's
|
|
@@ -1109,7 +665,7 @@ function wrapPowdbError(err) {
|
|
|
1109
665
|
// callers (and the opt-in stale-read retry) can inspect the driver code.
|
|
1110
666
|
if (e.code === 'protocol_error' ||
|
|
1111
667
|
/received unexpected frame|unknown message type|truncated payload|bad framing/i.test(msg)) {
|
|
1112
|
-
return new errors_js_1.ConnectionError(`
|
|
668
|
+
return new errors_js_1.ConnectionError(`PowDB connection is in an invalid state: ${msg}`, { cause: err });
|
|
1113
669
|
}
|
|
1114
670
|
// Read-only refusal → ReadOnlyError (E018). Two engine shapes, both mapped by
|
|
1115
671
|
// substring (the networked transport prefixes the message with `query failed:
|
|
@@ -1135,7 +691,7 @@ function wrapPowdbError(err) {
|
|
|
1135
691
|
// not empty …`). It is a connection failure (E004), not a query defect, the
|
|
1136
692
|
// fix is to recover the directory with a writable open first.
|
|
1137
693
|
if (/cannot open read-only: the WAL is not empty/i.test(msg)) {
|
|
1138
|
-
return new errors_js_1.ConnectionError(`
|
|
694
|
+
return new errors_js_1.ConnectionError(`PowDB could not open the directory read-only: ${msg}. Open it once with a writable handle to ` +
|
|
1139
695
|
'flush the WAL (recover the directory), then reopen it read-only for snapshot serving.', { cause: err });
|
|
1140
696
|
}
|
|
1141
697
|
// Catalog-version mismatch → ConnectionError (E004). A pre-0.19 binary/addon
|
|
@@ -1144,7 +700,7 @@ function wrapPowdbError(err) {
|
|
|
1144
700
|
// not a query defect, so it runs among the connection families BEFORE the
|
|
1145
701
|
// generic validation regexes below.
|
|
1146
702
|
if (/unsupported catalog version/i.test(msg)) {
|
|
1147
|
-
return new errors_js_1.ConnectionError(`
|
|
703
|
+
return new errors_js_1.ConnectionError(`PowDB could not open the data directory: ${msg}. This directory uses a newer PowDB catalog ` +
|
|
1148
704
|
'format (a `link` declaration upgrades it to v7); upgrade the PowDB addon/server to a version that can read it.', { cause: err });
|
|
1149
705
|
}
|
|
1150
706
|
// Per-query deadline → TimeoutError (E002). Message-path so it fires on the
|
|
@@ -1153,20 +709,20 @@ function wrapPowdbError(err) {
|
|
|
1153
709
|
// transaction-gate timeout below) so the real "query timeout after <n>ms"
|
|
1154
710
|
// survives instead of rendering the placeholder "timed out after 0ms".
|
|
1155
711
|
if (/query timeout after/i.test(msg)) {
|
|
1156
|
-
return new errors_js_1.TimeoutError(0, 'PowDB query', { message: `
|
|
712
|
+
return new errors_js_1.TimeoutError(0, 'PowDB query', { message: `PowDB ${msg}`, cause: err });
|
|
1157
713
|
}
|
|
1158
714
|
// Client-initiated cancellation → ConnectionError (E004). This is FINAL: the
|
|
1159
715
|
// issuing client disconnected, so the query was a clean early return, never
|
|
1160
716
|
// auto-retry it (the opt-in stale-read retry only replays stale-FRAME reads).
|
|
1161
717
|
if (/query cancelled by client disconnect/i.test(msg)) {
|
|
1162
|
-
return new errors_js_1.ConnectionError(`
|
|
718
|
+
return new errors_js_1.ConnectionError(`PowDB query cancelled by client disconnect: ${msg}`, { cause: err });
|
|
1163
719
|
}
|
|
1164
720
|
// Bounded join rejection → ValidationError (E003). The engine rejects a pure
|
|
1165
721
|
// nested-loop join whose candidate-pair count (or result row count) exceeds
|
|
1166
722
|
// the safety bound BEFORE executing, and names the fix in the message, keep
|
|
1167
723
|
// that fix-hint intact so the caller knows how to make the join eligible.
|
|
1168
724
|
if (/nested-loop join would evaluate|join result exceeds row limit/i.test(msg)) {
|
|
1169
|
-
return new errors_js_1.ValidationError(`
|
|
725
|
+
return new errors_js_1.ValidationError(`PowDB join rejected: ${msg}`);
|
|
1170
726
|
}
|
|
1171
727
|
// Entity-link misuse hard errors (0.19.1 turned the two silent-wrong-results
|
|
1172
728
|
// 0.19.0 behaviors into hard errors) → ValidationError (E003), a query defect
|
|
@@ -1178,7 +734,7 @@ function wrapPowdbError(err) {
|
|
|
1178
734
|
// always aliases and never aggregates over a link), so these fire only for a
|
|
1179
735
|
// raw user PowQL string; mapping them keeps that path typed.
|
|
1180
736
|
if (/is ambiguous in a projection|aggregates over a nested or link projection/i.test(msg)) {
|
|
1181
|
-
return new errors_js_1.ValidationError(`
|
|
737
|
+
return new errors_js_1.ValidationError(`PowDB query rejected: ${msg}`);
|
|
1182
738
|
}
|
|
1183
739
|
// Corrupt storage → ConnectionError (E004). PowDB 0.20 verifies page checksums
|
|
1184
740
|
// at table-OPEN time and fails closed (previously the open scan skipped the bad
|
|
@@ -1188,7 +744,7 @@ function wrapPowdbError(err) {
|
|
|
1188
744
|
// token would otherwise class it E003). There is no salvage mode: restoring
|
|
1189
745
|
// from a backup is the documented recovery, so say so.
|
|
1190
746
|
if (/page corrupt|catalog corrupt|corrupt heap superblock|CRC32 mismatch/i.test(msg)) {
|
|
1191
|
-
return new errors_js_1.ConnectionError(`
|
|
747
|
+
return new errors_js_1.ConnectionError(`PowDB refused to open a corrupt data directory: ${msg}. PowDB verifies page checksums on open ` +
|
|
1192
748
|
'and fails closed rather than serving partial data; there is no skip-corrupt-pages mode, so recover by ' +
|
|
1193
749
|
'restoring the directory from a backup.', { cause: err });
|
|
1194
750
|
}
|
|
@@ -1202,7 +758,7 @@ function wrapPowdbError(err) {
|
|
|
1202
758
|
const m = /column '([^']+)' not found(?: in table '([^']+)')?/i.exec(msg);
|
|
1203
759
|
if (m) {
|
|
1204
760
|
const where = m[2] ? ` on table "${m[2]}"` : '';
|
|
1205
|
-
return new errors_js_1.ValidationError(`
|
|
761
|
+
return new errors_js_1.ValidationError(`PowDB rejected column "${m[1]}"${where}: it does not exist in the live catalog. ` +
|
|
1206
762
|
'The schema metadata Turbine is using has drifted from the database; re-derive it ' +
|
|
1207
763
|
'(`schemaDefToMetadata` / `introspectPowdbDatabase`) or apply the missing DDL. ' +
|
|
1208
764
|
`(engine: ${msg})`);
|
|
@@ -1215,7 +771,7 @@ function wrapPowdbError(err) {
|
|
|
1215
771
|
{
|
|
1216
772
|
const m = /type mismatch for column '([^']+)': expected ([^,]+), got (\w+)/i.exec(msg);
|
|
1217
773
|
if (m) {
|
|
1218
|
-
return new errors_js_1.ValidationError(`
|
|
774
|
+
return new errors_js_1.ValidationError(`PowDB rejected a comparison on column "${m[1]}": the column is ${m[2]} but the bound value is ` +
|
|
1219
775
|
`${m[3]}. PowQL never coerces across types in a comparison (before engine 0.20 this silently matched ` +
|
|
1220
776
|
"every row), so bind a value of the column's own type.");
|
|
1221
777
|
}
|
|
@@ -1225,7 +781,7 @@ function wrapPowdbError(err) {
|
|
|
1225
781
|
// nested parentheses, so a machine-built predicate with very many terms is now
|
|
1226
782
|
// rejected outright (see POWQL_MAX_NESTING_DEPTH).
|
|
1227
783
|
if (/nesting depth exceeds maximum/i.test(msg)) {
|
|
1228
|
-
return new errors_js_1.ValidationError(`
|
|
784
|
+
return new errors_js_1.ValidationError(`PowDB rejected the query: ${msg}. PowQL bounds the shape of the predicate tree, and a flat ` +
|
|
1229
785
|
`\`OR\` / \`AND\` array counts one level per term (roughly ${exports.POWQL_MAX_NESTING_DEPTH - 1} terms at the top ` +
|
|
1230
786
|
'level, fewer inside a nested `with` block). Split a large `OR` / `AND` array into several queries and ' +
|
|
1231
787
|
'merge the results, or express it as a single `in` list, which is one flat node and does not count ' +
|
|
@@ -1234,7 +790,7 @@ function wrapPowdbError(err) {
|
|
|
1234
790
|
// Negative limit / offset → ValidationError (E003). Turbine validates these
|
|
1235
791
|
// client-side before emitting, so this is the backstop for a raw PowQL string.
|
|
1236
792
|
if (/(limit|offset) must not be negative/i.test(msg)) {
|
|
1237
|
-
return new errors_js_1.ValidationError(`
|
|
793
|
+
return new errors_js_1.ValidationError(`PowDB rejected the query: ${msg}. Pass a non-negative \`limit\` / \`offset\` ` +
|
|
1238
794
|
'(before engine 0.20 a negative limit was ignored and returned every row).');
|
|
1239
795
|
}
|
|
1240
796
|
// Client-side result-frame cell cap (`@zvndev/powdb-client` >= 0.20 rejects a
|
|
@@ -1243,7 +799,7 @@ function wrapPowdbError(err) {
|
|
|
1243
799
|
// Error with no code, so match the message and give the caller the two real
|
|
1244
800
|
// remedies instead of letting it fall through untyped.
|
|
1245
801
|
if (/result too large: \d+ cells/i.test(msg)) {
|
|
1246
|
-
return new errors_js_1.ValidationError(`
|
|
802
|
+
return new errors_js_1.ValidationError(`PowDB result too large to decode: ${msg}. The client caps one result frame at 2,000,000 cells ` +
|
|
1247
803
|
'(rows x columns). Page the query with `limit` / `offset`, or narrow the row with `select` so each row ' +
|
|
1248
804
|
'carries fewer columns.');
|
|
1249
805
|
}
|
|
@@ -1261,28 +817,28 @@ function wrapPowdbError(err) {
|
|
|
1261
817
|
if (typeof wireClass === 'number') {
|
|
1262
818
|
switch (wireClass) {
|
|
1263
819
|
case 3: // timeout (per-query budget, gate wait, idle timeout), retryable
|
|
1264
|
-
return new errors_js_1.TimeoutError(0, 'PowDB query', { message: `
|
|
820
|
+
return new errors_js_1.TimeoutError(0, 'PowDB query', { message: `PowDB ${msg}`, cause: err });
|
|
1265
821
|
case 4: // limit_exceeded (memory / size budget), a query-shape defect
|
|
1266
|
-
return new errors_js_1.ValidationError(`
|
|
822
|
+
return new errors_js_1.ValidationError(`PowDB resource limit exceeded: ${msg}`);
|
|
1267
823
|
case 5: // readonly_refused, the snapshot-serving routing signal
|
|
1268
824
|
return new errors_js_1.ReadOnlyError(`PowDB refused a write on a read-only database: ${msg}.`, {
|
|
1269
825
|
cause: err,
|
|
1270
826
|
reason: 'snapshot',
|
|
1271
827
|
});
|
|
1272
828
|
case 6: // auth_failed at CONNECT
|
|
1273
|
-
return new errors_js_1.ConnectionError(`
|
|
829
|
+
return new errors_js_1.ConnectionError(`PowDB authentication failed: ${msg} (check the user / password / dbName for this connection).`, { cause: err });
|
|
1274
830
|
case 7: // rate_limited (repeated bad auth), connection-establishment class
|
|
1275
|
-
return new errors_js_1.ConnectionError(`
|
|
831
|
+
return new errors_js_1.ConnectionError(`PowDB rate-limited this address after repeated failed authentication: ${msg}. Wait before retrying.`, { cause: err });
|
|
1276
832
|
case 8: {
|
|
1277
833
|
// constraint_violation, today that is always a unique index
|
|
1278
834
|
const m = /on\s+\S+\.(\w+)/i.exec(msg);
|
|
1279
835
|
return new errors_js_1.UniqueConstraintError({ constraint: m?.[1], cause: err });
|
|
1280
836
|
}
|
|
1281
837
|
case 9: // cancelled (issuing client disconnected), final, never retry
|
|
1282
|
-
return new errors_js_1.ConnectionError(`
|
|
838
|
+
return new errors_js_1.ConnectionError(`PowDB query cancelled by client disconnect: ${msg}`, { cause: err });
|
|
1283
839
|
case 1: // parse
|
|
1284
840
|
case 2: // execution
|
|
1285
|
-
return new errors_js_1.ValidationError(`
|
|
841
|
+
return new errors_js_1.ValidationError(`PowDB query rejected: ${msg}`);
|
|
1286
842
|
default: // internal (0) or an unknown future class: fall through
|
|
1287
843
|
break;
|
|
1288
844
|
}
|
|
@@ -1292,45 +848,28 @@ function wrapPowdbError(err) {
|
|
|
1292
848
|
// signal we get (code is always 'GenericFailure'); on the networked path they
|
|
1293
849
|
// are a safety net before the .code switch.
|
|
1294
850
|
if (/type mismatch|\bParse\b|\bExecution\b|StorageError|unexpected|row too large/i.test(msg)) {
|
|
1295
|
-
return new errors_js_1.ValidationError(`
|
|
851
|
+
return new errors_js_1.ValidationError(`PowDB query rejected: ${msg}`);
|
|
1296
852
|
}
|
|
1297
853
|
switch (e.code) {
|
|
1298
854
|
case 'connect_failed':
|
|
1299
855
|
case 'closed':
|
|
1300
|
-
return new errors_js_1.ConnectionError(`
|
|
856
|
+
return new errors_js_1.ConnectionError(`PowDB connection failed: ${msg}`, { cause: err });
|
|
1301
857
|
case 'auth_failed':
|
|
1302
858
|
// Connection-establishment class, non-retryable: the handshake was
|
|
1303
859
|
// rejected. Surface E004 with a concrete remediation hint instead of
|
|
1304
860
|
// letting it fall through to the raw error.
|
|
1305
|
-
return new errors_js_1.ConnectionError(`
|
|
861
|
+
return new errors_js_1.ConnectionError(`PowDB authentication failed: ${msg} (check the user / password / dbName for this connection).`, { cause: err });
|
|
1306
862
|
case 'timeout':
|
|
1307
863
|
case 'aborted':
|
|
1308
864
|
return new errors_js_1.TimeoutError(0, 'PowDB query', { cause: err });
|
|
1309
865
|
case 'query_failed':
|
|
1310
866
|
case 'type_coercion_failed':
|
|
1311
867
|
case 'size_exceeded':
|
|
1312
|
-
return new errors_js_1.ValidationError(`
|
|
868
|
+
return new errors_js_1.ValidationError(`PowDB query rejected: ${msg}`);
|
|
1313
869
|
default:
|
|
1314
|
-
return err instanceof Error ? err : new errors_js_1.ConnectionError(`
|
|
870
|
+
return err instanceof Error ? err : new errors_js_1.ConnectionError(`PowDB error: ${msg}`, { cause: err });
|
|
1315
871
|
}
|
|
1316
872
|
}
|
|
1317
|
-
/**
|
|
1318
|
-
* True when `err` is the stale-wire-frame {@link ConnectionError} produced by
|
|
1319
|
-
* {@link wrapPowdbError} (its `.cause` is a `protocol_error` PowDBError, or the
|
|
1320
|
-
* message carries the invalid-state signature). The opt-in read retry
|
|
1321
|
-
* (`retryStaleReads`, evaluated in {@link PowqlInterface}'s exec seam) uses this
|
|
1322
|
-
* to decide whether a first-statement READ may be replayed once on a fresh
|
|
1323
|
-
* connection; writes are NEVER retried (an ambiguous mutation reply is unsafe
|
|
1324
|
-
* to replay, matching the client's own native-path policy).
|
|
1325
|
-
*/
|
|
1326
|
-
function isStaleFramePowdbError(err) {
|
|
1327
|
-
if (!(err instanceof errors_js_1.ConnectionError))
|
|
1328
|
-
return false;
|
|
1329
|
-
const cause = err.cause;
|
|
1330
|
-
if (cause && typeof cause === 'object' && cause.code === 'protocol_error')
|
|
1331
|
-
return true;
|
|
1332
|
-
return /PowDB connection is in an invalid state/.test(err.message);
|
|
1333
|
-
}
|
|
1334
873
|
function normalizeQueryArgs(arg, values) {
|
|
1335
874
|
if (typeof arg === 'string')
|
|
1336
875
|
return { text: arg, params: values ?? [] };
|
|
@@ -1638,7 +1177,7 @@ class PowdbPool {
|
|
|
1638
1177
|
this.pool = pool;
|
|
1639
1178
|
this.toParam = toParam;
|
|
1640
1179
|
this.txGate = new PowdbTxGate(options.transactionQueueTimeoutMs ?? exports.DEFAULT_TX_QUEUE_TIMEOUT_MS);
|
|
1641
|
-
this.capabilities = options.capabilities ??
|
|
1180
|
+
this.capabilities = options.capabilities ?? powdb_shared_js_1.ALL_POWDB_CAPABILITIES;
|
|
1642
1181
|
this.retryStaleReads = options.retryStaleReads ?? false;
|
|
1643
1182
|
this.readonly = options.readonly ?? false;
|
|
1644
1183
|
}
|
|
@@ -1697,7 +1236,7 @@ class PowdbPool {
|
|
|
1697
1236
|
*/
|
|
1698
1237
|
assertOpen() {
|
|
1699
1238
|
if (this.closed) {
|
|
1700
|
-
throw new errors_js_1.ConnectionError('
|
|
1239
|
+
throw new errors_js_1.ConnectionError('The PowDB pool is closed, disconnect() was already called on this client.');
|
|
1701
1240
|
}
|
|
1702
1241
|
}
|
|
1703
1242
|
async connect() {
|
|
@@ -1866,17 +1405,17 @@ function normalizeEmbeddedResult(r) {
|
|
|
1866
1405
|
*/
|
|
1867
1406
|
function encodePowqlLiteral(value, position) {
|
|
1868
1407
|
const at = position ? ` (${position})` : '';
|
|
1869
|
-
if (value instanceof PowdbFloatParam) {
|
|
1408
|
+
if (value instanceof powdb_shared_js_1.PowdbFloatParam) {
|
|
1870
1409
|
const n = value.value;
|
|
1871
1410
|
if (!Number.isFinite(n))
|
|
1872
|
-
throw new errors_js_1.ValidationError(`
|
|
1411
|
+
throw new errors_js_1.ValidationError(`Non-finite float cannot be encoded for PowDB${at}.`);
|
|
1873
1412
|
// Force a float-form literal so an integer-valued float column stays a float.
|
|
1874
1413
|
const text = powqlNumberText(n);
|
|
1875
1414
|
return text.includes('.') ? text : `${text}.0`;
|
|
1876
1415
|
}
|
|
1877
1416
|
// json document: emit the canonical JSON text as a PowQL string literal (the
|
|
1878
1417
|
// embedded engine validates and stores it as a json document).
|
|
1879
|
-
if (value instanceof PowdbJsonParam)
|
|
1418
|
+
if (value instanceof powdb_shared_js_1.PowdbJsonParam)
|
|
1880
1419
|
return encodePowqlString(powdbJsonText(value, position), position);
|
|
1881
1420
|
if (value === undefined || value === null)
|
|
1882
1421
|
return 'null';
|
|
@@ -1888,14 +1427,14 @@ function encodePowqlLiteral(value, position) {
|
|
|
1888
1427
|
return value.toString();
|
|
1889
1428
|
if (typeof value === 'number') {
|
|
1890
1429
|
if (!Number.isFinite(value))
|
|
1891
|
-
throw new errors_js_1.ValidationError(`
|
|
1430
|
+
throw new errors_js_1.ValidationError(`Non-finite number cannot be encoded for PowDB${at}.`);
|
|
1892
1431
|
// Renders an integer as an int literal (`42`) and a fractional number as a
|
|
1893
1432
|
// float literal (`4.2`); PowQL distinguishes them by the dot.
|
|
1894
1433
|
return powqlNumberText(value);
|
|
1895
1434
|
}
|
|
1896
1435
|
if (typeof value === 'string')
|
|
1897
1436
|
return encodePowqlString(value, position);
|
|
1898
|
-
throw new errors_js_1.ValidationError(`
|
|
1437
|
+
throw new errors_js_1.ValidationError(`Value of type ${typeof value} cannot be encoded as a PowDB literal${at}.`);
|
|
1899
1438
|
}
|
|
1900
1439
|
/**
|
|
1901
1440
|
* Render a finite JS number as PowQL numeric text.
|
|
@@ -1916,7 +1455,7 @@ function powqlNumberText(n) {
|
|
|
1916
1455
|
return BigInt(n).toString();
|
|
1917
1456
|
const m = /^(-?)(\d+)(?:\.(\d+))?[eE]([+-]?\d+)$/.exec(s);
|
|
1918
1457
|
if (!m) {
|
|
1919
|
-
throw new errors_js_1.ValidationError(`
|
|
1458
|
+
throw new errors_js_1.ValidationError(`Number ${s} cannot be rendered as a PowDB literal.`);
|
|
1920
1459
|
}
|
|
1921
1460
|
const sign = m[1] ?? '';
|
|
1922
1461
|
const intPart = m[2] ?? '';
|
|
@@ -1983,7 +1522,7 @@ exports.POWQL_LEXER_TESTED_CEILING = '0.20';
|
|
|
1983
1522
|
*/
|
|
1984
1523
|
function encodePowqlString(s, position) {
|
|
1985
1524
|
if (s.includes('\0')) {
|
|
1986
|
-
throw new errors_js_1.ValidationError(`
|
|
1525
|
+
throw new errors_js_1.ValidationError(`String value${position ? ` (${position})` : ''} contains a NUL byte (U+0000), which PowQL string ` +
|
|
1987
1526
|
'literals cannot represent. Strip it before writing.');
|
|
1988
1527
|
}
|
|
1989
1528
|
let out = '"';
|
|
@@ -2013,7 +1552,7 @@ function materializePowql(powql, params) {
|
|
|
2013
1552
|
return powql.replace(/\$(\d+)/g, (_m, n) => {
|
|
2014
1553
|
const idx = Number(n) - 1;
|
|
2015
1554
|
if (idx < 0 || idx >= params.length) {
|
|
2016
|
-
throw new errors_js_1.ValidationError(`
|
|
1555
|
+
throw new errors_js_1.ValidationError(`PowQL placeholder $${n} has no bound parameter (have ${params.length}).`);
|
|
2017
1556
|
}
|
|
2018
1557
|
return encodePowqlLiteral(params[idx], `parameter $${n}`);
|
|
2019
1558
|
});
|
|
@@ -2067,7 +1606,7 @@ class PowdbEmbeddedPool {
|
|
|
2067
1606
|
constructor(db, options = {}) {
|
|
2068
1607
|
this.db = db;
|
|
2069
1608
|
this.txGate = new PowdbTxGate(options.transactionQueueTimeoutMs ?? exports.DEFAULT_TX_QUEUE_TIMEOUT_MS);
|
|
2070
|
-
this.capabilities = options.capabilities ??
|
|
1609
|
+
this.capabilities = options.capabilities ?? powdb_shared_js_1.ALL_POWDB_CAPABILITIES;
|
|
2071
1610
|
this.retryStaleReads = options.retryStaleReads ?? false;
|
|
2072
1611
|
this.readonly = options.readonly ?? false;
|
|
2073
1612
|
}
|
|
@@ -2100,7 +1639,7 @@ class PowdbEmbeddedPool {
|
|
|
2100
1639
|
if (engineSem &&
|
|
2101
1640
|
ceiling &&
|
|
2102
1641
|
(engineSem.major > ceiling.major || (engineSem.major === ceiling.major && engineSem.minor > ceiling.minor))) {
|
|
2103
|
-
throw new errors_js_1.ValidationError(`
|
|
1642
|
+
throw new errors_js_1.ValidationError(`Refusing the PowDB legacy string wire: this embedded addon reports engine ` +
|
|
2104
1643
|
`${this.capabilities.engineVersion}, which is newer than the escaper's verified lexer range ` +
|
|
2105
1644
|
`(<= ${exports.POWQL_LEXER_TESTED_CEILING}) AND such an addon exposes the parameterized native API, so ` +
|
|
2106
1645
|
`reaching the legacy materialize path means the queryWithParams feature-detect failed. Upgrade ` +
|
|
@@ -2117,7 +1656,7 @@ class PowdbEmbeddedPool {
|
|
|
2117
1656
|
*/
|
|
2118
1657
|
async run(powql, params, holdRef) {
|
|
2119
1658
|
if (this.closed) {
|
|
2120
|
-
throw new errors_js_1.ConnectionError('
|
|
1659
|
+
throw new errors_js_1.ConnectionError('The PowDB embedded pool is closed: disconnect() was already called on this client.');
|
|
2121
1660
|
}
|
|
2122
1661
|
const ctl = txControl(powql);
|
|
2123
1662
|
if (ctl === 'begin') {
|
|
@@ -2132,7 +1671,7 @@ class PowdbEmbeddedPool {
|
|
|
2132
1671
|
if (this.closed) {
|
|
2133
1672
|
holdRef.hold.finish();
|
|
2134
1673
|
holdRef.hold = null;
|
|
2135
|
-
throw new errors_js_1.ConnectionError('
|
|
1674
|
+
throw new errors_js_1.ConnectionError('The PowDB embedded pool is closed: disconnect() was already called on this client.');
|
|
2136
1675
|
}
|
|
2137
1676
|
}
|
|
2138
1677
|
if ((ctl === 'commit' || ctl === 'rollback') && holdRef.hold === null) {
|
|
@@ -2241,7 +1780,7 @@ async function loadPowdb() {
|
|
|
2241
1780
|
return (await (0, optional_peer_import_cjs_1.default)('@zvndev/powdb-client'));
|
|
2242
1781
|
}
|
|
2243
1782
|
catch (err) {
|
|
2244
|
-
throw new errors_js_1.ConnectionError("
|
|
1783
|
+
throw new errors_js_1.ConnectionError("turbine-orm/powdb requires the optional peer dependency '@zvndev/powdb-client'. Install it: npm i @zvndev/powdb-client, " +
|
|
2245
1784
|
'or construct the PowDB pool yourself and inject it: turbinePowDB(pool, schema). ' +
|
|
2246
1785
|
`(${err.message})`);
|
|
2247
1786
|
}
|
|
@@ -2261,7 +1800,7 @@ async function loadPowdbEmbedded() {
|
|
|
2261
1800
|
mod = (await (0, optional_peer_import_cjs_1.default)('@zvndev/powdb-embedded'));
|
|
2262
1801
|
}
|
|
2263
1802
|
catch (err) {
|
|
2264
|
-
throw new errors_js_1.ConnectionError("
|
|
1803
|
+
throw new errors_js_1.ConnectionError("turbine-orm/powdb embedded mode requires the optional peer '@zvndev/powdb-embedded'. " +
|
|
2265
1804
|
'Install it: npm i @zvndev/powdb-embedded. If install succeeded but loading failed, your platform has no ' +
|
|
2266
1805
|
'prebuilt binary (prebuilts ship for macOS arm64/x64 and Linux glibc x64/arm64; Intel-mac/musl/Windows ' +
|
|
2267
1806
|
'build from source), build it with `npm run build` in the addon, then retry. You can also construct the ' +
|
|
@@ -2269,7 +1808,7 @@ async function loadPowdbEmbedded() {
|
|
|
2269
1808
|
`(${err.message})`);
|
|
2270
1809
|
}
|
|
2271
1810
|
if (!mod || typeof mod.Database?.open !== 'function') {
|
|
2272
|
-
throw new errors_js_1.ConnectionError("
|
|
1811
|
+
throw new errors_js_1.ConnectionError("'@zvndev/powdb-embedded' loaded but did not export Database.open, the installed version is " +
|
|
2273
1812
|
'likely incompatible (turbine-orm/powdb embedded requires @zvndev/powdb-embedded ^0.7.0).');
|
|
2274
1813
|
}
|
|
2275
1814
|
return mod;
|
|
@@ -2293,7 +1832,7 @@ async function openEmbeddedPool(target, poolOptions = {}, assumeEngineVersion, i
|
|
|
2293
1832
|
// A read-only engine never writes, so a durability selector is meaningless
|
|
2294
1833
|
// there, reject the combination loudly rather than silently ignoring one.
|
|
2295
1834
|
if (readonly && syncMode !== undefined) {
|
|
2296
|
-
throw new errors_js_1.ValidationError('
|
|
1835
|
+
throw new errors_js_1.ValidationError('embedded `syncMode` is meaningless with `readonly: true` (a read-only database never writes). Remove one.');
|
|
2297
1836
|
}
|
|
2298
1837
|
let db;
|
|
2299
1838
|
try {
|
|
@@ -2303,20 +1842,20 @@ async function openEmbeddedPool(target, poolOptions = {}, assumeEngineVersion, i
|
|
|
2303
1842
|
// installed addon predates them.
|
|
2304
1843
|
if (memoryLimit !== undefined) {
|
|
2305
1844
|
if (typeof mod.Database.openReadOnlyWithMemoryLimit !== 'function') {
|
|
2306
|
-
throw new errors_js_1.ConnectionError('
|
|
1845
|
+
throw new errors_js_1.ConnectionError('embedded `readonly` + `memoryLimit` requires @zvndev/powdb-embedded >= 0.14 (openReadOnlyWithMemoryLimit).');
|
|
2307
1846
|
}
|
|
2308
1847
|
db = mod.Database.openReadOnlyWithMemoryLimit(dir, memoryLimit);
|
|
2309
1848
|
}
|
|
2310
1849
|
else {
|
|
2311
1850
|
if (typeof mod.Database.openReadOnly !== 'function') {
|
|
2312
|
-
throw new errors_js_1.ConnectionError('
|
|
1851
|
+
throw new errors_js_1.ConnectionError('embedded `readonly: true` requires @zvndev/powdb-embedded >= 0.14 (the installed addon has no openReadOnly).');
|
|
2313
1852
|
}
|
|
2314
1853
|
db = mod.Database.openReadOnly(dir);
|
|
2315
1854
|
}
|
|
2316
1855
|
}
|
|
2317
1856
|
else if (memoryLimit !== undefined) {
|
|
2318
1857
|
if (typeof mod.Database.openWithMemoryLimit !== 'function') {
|
|
2319
|
-
throw new errors_js_1.ConnectionError('
|
|
1858
|
+
throw new errors_js_1.ConnectionError('embedded `memoryLimit` requires @zvndev/powdb-embedded ≥ 0.7.1.');
|
|
2320
1859
|
}
|
|
2321
1860
|
db = mod.Database.openWithMemoryLimit(dir, memoryLimit);
|
|
2322
1861
|
}
|
|
@@ -2327,11 +1866,11 @@ async function openEmbeddedPool(target, poolOptions = {}, assumeEngineVersion, i
|
|
|
2327
1866
|
catch (err) {
|
|
2328
1867
|
if (err instanceof errors_js_1.ConnectionError)
|
|
2329
1868
|
throw err;
|
|
2330
|
-
throw new errors_js_1.ConnectionError(`
|
|
1869
|
+
throw new errors_js_1.ConnectionError(`PowDB embedded could not open data dir "${dir}": ${err.message}`);
|
|
2331
1870
|
}
|
|
2332
1871
|
if (syncMode !== undefined) {
|
|
2333
1872
|
if (typeof db.setSyncMode !== 'function') {
|
|
2334
|
-
throw new errors_js_1.ConnectionError('
|
|
1873
|
+
throw new errors_js_1.ConnectionError('embedded `syncMode` requires @zvndev/powdb-embedded ≥ 0.7.1 (the installed addon has no setSyncMode).');
|
|
2335
1874
|
}
|
|
2336
1875
|
db.setSyncMode(syncMode);
|
|
2337
1876
|
}
|