turbine-orm 0.47.0 → 0.48.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.
@@ -128,7 +128,16 @@ async function introspectPowdbDatabase(exec, options = {}) {
128
128
  // `describe` needs the table name in bare-identifier position → quote it so
129
129
  // a reserved-word / non-bare table name (`order`) does not become a parse
130
130
  // error.
131
- const describeRows = (await exec(`describe ${(0, powdb_js_1.quotePowqlIdent)(tableName)}`)).rows.map((r) => ({
131
+ const describeRows = (await exec(`describe ${(0, powdb_js_1.quotePowqlIdent)(tableName)}`)).rows
132
+ // PowDB >= 0.19.1 appends entity-LINK rows after the 4 column rows in
133
+ // `describe <T>` (a `type` cell of literally `"link"`, an Empty nullable
134
+ // slot, and an arrow description in the index slot). They are NOT columns,
135
+ // so drop them before column parsing, unconditionally (no capability check):
136
+ // the rows simply never appear on a pre-0.19.1 engine, and a linked database
137
+ // introspected without this filter would produce garbage `link`-typed
138
+ // columns. Declared links are read separately via `schema links` below.
139
+ .filter((r) => asString(r.type) !== 'link')
140
+ .map((r) => ({
132
141
  column: asString(r.column),
133
142
  type: asString(r.type),
134
143
  nullable: asBool(r.nullable),
@@ -208,10 +217,120 @@ async function introspectPowdbDatabase(exec, options = {}) {
208
217
  allColumns,
209
218
  primaryKey,
210
219
  uniqueColumns,
211
- // PowDB has no declared foreign keys no relations from introspection.
220
+ // Populated below from `schema links` when the engine supports link
221
+ // introspection (>= 0.19.1); stays `{}` otherwise (PowDB has no declared
222
+ // foreign keys, so pre-link introspection reports no relations).
212
223
  relations: {},
213
224
  indexes,
214
225
  };
215
226
  }
227
+ // Link introspection (>= 0.19.1): populate relations from declared entity links.
228
+ // Behaves exactly as before (relations stay `{}`) when the capability is absent
229
+ // or not supplied.
230
+ if (options.capabilities?.linkIntrospection) {
231
+ await introspectPowdbLinks(exec, tables);
232
+ }
216
233
  return { tables, enums: {} };
217
234
  }
235
+ /**
236
+ * Read `schema links` and populate {@link TableMetadata.relations}: the first
237
+ * time PowDB introspection can report relations. Each declared link becomes a
238
+ * relation on its OWNER, and its natural REVERSE is synthesized on the target
239
+ * (mirroring the SQL introspector, which always emits both sides of a FK):
240
+ *
241
+ * - `"to-one"` link `Order.user -> User`: owner gets a `belongsTo` (localKey =
242
+ * the FK on the owner, targetKey = the referenced key on target); the target
243
+ * gets a reverse `hasMany` named for the pluralized owner table.
244
+ * - `"to-many"` link `User.orders -> Order`: owner gets a `hasMany` (localKey =
245
+ * the referenced key on the owner, targetKey = the FK on the child); the
246
+ * target gets a reverse `belongsTo` named for the singularized owner table.
247
+ *
248
+ * Reverse synthesis is best-effort and collision-guarded: a synthesized name that
249
+ * would shadow a column field or an already-present relation on the target is
250
+ * skipped (never a hard error), so introspection can only ADD relations, never
251
+ * clobber. m2m junctions cannot be inferred from links and stay undetected;
252
+ * `defineSchema` remains the relation-complete path. Column names arrive as
253
+ * PowDB's emitted (snake) names and stay snake in `foreignKey`/`referenceKey`
254
+ * (the SQL introspector's convention); the relation NAME is camelCased to match
255
+ * the query field surface. Naming edge: `schema links` is the contextual listing
256
+ * keyword: a table literally named `links` is still read via `describe links`.
257
+ */
258
+ async function introspectPowdbLinks(exec, tables) {
259
+ const linkRows = (await exec('schema links')).rows.map((r) => ({
260
+ owner: asString(r.owner),
261
+ name: asString(r.name),
262
+ target: asString(r.target),
263
+ localKey: asString(r.local_key),
264
+ targetKey: asString(r.target_key),
265
+ cardinality: asString(r.cardinality),
266
+ }));
267
+ for (const link of linkRows) {
268
+ const owner = tables[link.owner];
269
+ const target = tables[link.target];
270
+ // A link referencing a filtered-out (include/exclude) table is skipped: we
271
+ // cannot describe the target, so the relation would be unusable.
272
+ if (!owner || !target)
273
+ continue;
274
+ const toOne = link.cardinality === 'to-one';
275
+ const relName = (0, schema_js_1.snakeToCamel)(link.name);
276
+ // Forward relation on the owner (the declared direction).
277
+ if (toOne) {
278
+ addRelation(owner, {
279
+ type: 'belongsTo',
280
+ name: relName,
281
+ from: link.owner,
282
+ to: link.target,
283
+ foreignKey: link.localKey,
284
+ referenceKey: link.targetKey,
285
+ });
286
+ }
287
+ else {
288
+ addRelation(owner, {
289
+ type: 'hasMany',
290
+ name: relName,
291
+ from: link.owner,
292
+ to: link.target,
293
+ foreignKey: link.targetKey, // FK on the child (target) side
294
+ referenceKey: link.localKey, // referenced key on the owner (parent) side
295
+ });
296
+ }
297
+ // Reverse relation on the target (synthesized, collision-guarded).
298
+ const reverseName = toOne ? pluralize((0, schema_js_1.snakeToCamel)(link.owner)) : (0, schema_js_1.singularize)((0, schema_js_1.snakeToCamel)(link.owner));
299
+ if (!relationNameTaken(target, reverseName)) {
300
+ addRelation(target, {
301
+ type: toOne ? 'hasMany' : 'belongsTo',
302
+ name: reverseName,
303
+ from: link.target,
304
+ to: link.owner,
305
+ // Same two columns, roles swapped for the opposite direction.
306
+ foreignKey: toOne ? link.localKey : link.targetKey,
307
+ referenceKey: toOne ? link.targetKey : link.localKey,
308
+ });
309
+ }
310
+ }
311
+ }
312
+ /** True when `name` already names a relation OR a column field/name on `meta`. */
313
+ function relationNameTaken(meta, name) {
314
+ if (meta.relations[name])
315
+ return true;
316
+ return meta.columns.some((c) => c.field === name || c.name === name);
317
+ }
318
+ /** Add a relation to a table's relations map (keyed by its camelCase name). */
319
+ function addRelation(meta, rel) {
320
+ if (meta.relations[rel.name])
321
+ return; // never clobber an existing relation
322
+ meta.relations[rel.name] = rel;
323
+ }
324
+ /**
325
+ * Minimal English pluralizer for reverse-relation names (`user` → `users`,
326
+ * `company` → `companies`, `box` → `boxes`). Only ever produces a candidate name
327
+ * that {@link relationNameTaken} then vets, so an imperfect plural can at worst be
328
+ * skipped, never break a query.
329
+ */
330
+ function pluralize(word) {
331
+ if (/[^aeiou]y$/i.test(word))
332
+ return `${word.slice(0, -1)}ies`;
333
+ if (/(s|x|z|ch|sh)$/i.test(word))
334
+ return `${word}es`;
335
+ return `${word}s`;
336
+ }
package/dist/cjs/powdb.js CHANGED
@@ -98,7 +98,10 @@ exports.requireCapability = requireCapability;
98
98
  exports.isJsonColumn = isJsonColumn;
99
99
  exports.powqlColumnType = powqlColumnType;
100
100
  exports.quotePowqlIdent = quotePowqlIdent;
101
+ exports.deriveDesiredLinks = deriveDesiredLinks;
102
+ exports.powdbLinkStatement = powdbLinkStatement;
101
103
  exports.powqlSchemaDDL = powqlSchemaDDL;
104
+ exports.applyPowdbLinks = applyPowdbLinks;
102
105
  exports.coerceValue = coerceValue;
103
106
  exports.coerceNativeValue = coerceNativeValue;
104
107
  exports.rowToEntity = rowToEntity;
@@ -112,6 +115,8 @@ const client_js_1 = require("./client.js");
112
115
  const dialect_js_1 = require("./dialect.js");
113
116
  const errors_js_1 = require("./errors.js");
114
117
  const optional_peer_import_cjs_1 = __importDefault(require("./optional-peer-import.cjs"));
118
+ const warn_registry_js_1 = require("./query/warn-registry.js");
119
+ const schema_js_1 = require("./schema.js");
115
120
  /**
116
121
  * Capability descriptor for PowDB. PowQL generation is owned by
117
122
  * {@link PowqlInterface} (not the SQL `Dialect`), so this dialect exists only to
@@ -247,7 +252,14 @@ function assertSupportedPowdbVersion(version) {
247
252
  throw new errors_js_1.ConnectionError(`[turbine] turbine-orm/powdb requires PowDB >= ${exports.MIN_POWDB_VERSION}; the server reports "${version}". ` +
248
253
  'Upgrade the PowDB server (0.7.0 added the `returning` keyword and the int->float coercion fix Turbine relies on).');
249
254
  }
250
- /** Minimum engine version each gated feature needs, for the E017 hint text. */
255
+ /**
256
+ * Minimum engine version each gated feature needs, for the E017 hint text.
257
+ * Most gates carry a `major.minor` floor (patch-insensitive); the two link
258
+ * lanes carry a `major.minor.patch` floor (`0.19.1`) because the listing
259
+ * statement and the safe traversal semantics landed in the PATCH release, not
260
+ * in 0.19.0. {@link atLeastVersion} compares all three components, so a
261
+ * `major.minor` floor still matches every patch of that minor.
262
+ */
251
263
  const POWDB_FEATURE_MIN_VERSION = {
252
264
  introspection: '0.10',
253
265
  jsonDocs: '0.12',
@@ -255,6 +267,8 @@ const POWDB_FEATURE_MIN_VERSION = {
255
267
  serverJoins: '0.13',
256
268
  nestedProjections: '0.18',
257
269
  entityLinks: '0.19',
270
+ linkIntrospection: '0.19.1',
271
+ linkPaths: '0.19.1',
258
272
  };
259
273
  /**
260
274
  * Trusted-caller default: every FEATURE gate on, engine version unknown. Used
@@ -268,6 +282,11 @@ const POWDB_FEATURE_MIN_VERSION = {
268
282
  * `entityLinks` stays OFF for a stronger reason still: declaring a link
269
283
  * one-way-upgrades the on-disk catalog to v7 and locks out pre-0.19 binaries,
270
284
  * so it must only ever light up behind a real version probe.
285
+ * `linkIntrospection` / `linkPaths` stay OFF for the same probe-only discipline:
286
+ * `linkPaths` flips real query generation (a to-one `with` compiling to link
287
+ * projections), and `linkIntrospection` is only meaningful once genuinely
288
+ * probed, so both must come from a real version resolution, never a bare
289
+ * construction.
271
290
  */
272
291
  exports.ALL_POWDB_CAPABILITIES = {
273
292
  engineVersion: null,
@@ -277,6 +296,8 @@ exports.ALL_POWDB_CAPABILITIES = {
277
296
  serverJoins: true,
278
297
  nestedProjections: false,
279
298
  entityLinks: false,
299
+ linkIntrospection: false,
300
+ linkPaths: false,
280
301
  nativeRaw: false,
281
302
  };
282
303
  /** Parse a PowDB semver prefix (`0.13.0`, `0.13`, `1.2.3-rc`) into components, or `null`. */
@@ -286,9 +307,20 @@ function parsePowdbSemver(version) {
286
307
  return null;
287
308
  return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3] ?? 0) };
288
309
  }
289
- /** Is `sem` at least `major.minor`? */
290
- function atLeastVersion(sem, major, minor) {
291
- return sem.major > major || (sem.major === major && sem.minor >= minor);
310
+ /**
311
+ * Is `sem` at least `major.minor.patch`? PATCH-AWARE: `patch` defaults to `0`,
312
+ * so a two-component floor (`atLeastVersion(sem, 0, 19)`) behaves exactly as the
313
+ * old major/minor comparison did (matches every patch of 0.19), while a
314
+ * three-component floor (`atLeastVersion(sem, 0, 19, 1)`) additionally requires
315
+ * the patch, the distinction the link lanes need (0.19.1, never 0.19.0). Every
316
+ * existing two-argument call keeps its prior semantics unchanged.
317
+ */
318
+ function atLeastVersion(sem, major, minor, patch = 0) {
319
+ if (sem.major !== major)
320
+ return sem.major > major;
321
+ if (sem.minor !== minor)
322
+ return sem.minor > minor;
323
+ return sem.patch >= patch;
292
324
  }
293
325
  /**
294
326
  * Derive {@link PowdbCapabilities} from an engine version string. A non-semver /
@@ -307,6 +339,8 @@ function capabilitiesFromVersion(version, opts = {}) {
307
339
  serverJoins: false,
308
340
  nestedProjections: false,
309
341
  entityLinks: false,
342
+ linkIntrospection: false,
343
+ linkPaths: false,
310
344
  nativeRaw: false,
311
345
  };
312
346
  }
@@ -318,6 +352,11 @@ function capabilitiesFromVersion(version, opts = {}) {
318
352
  serverJoins: atLeastVersion(sem, 0, 13),
319
353
  nestedProjections: atLeastVersion(sem, 0, 18),
320
354
  entityLinks: atLeastVersion(sem, 0, 19),
355
+ // PATCH-floored at 0.19.1: the `schema links` listing + `describe` link rows
356
+ // and the safe (hard-erroring) scalar-path traversal both landed in 0.19.1,
357
+ // never 0.19.0.
358
+ linkIntrospection: atLeastVersion(sem, 0, 19, 1),
359
+ linkPaths: atLeastVersion(sem, 0, 19, 1),
321
360
  nativeRaw: Boolean(opts.hasNativeRaw) && atLeastVersion(sem, 0, 13),
322
361
  };
323
362
  }
@@ -526,6 +565,48 @@ function quotePowqlIdent(name) {
526
565
  }
527
566
  return exports.POWQL_KEYWORDS.has(name) || !POWQL_BARE_IDENT.test(name) ? `\`${name}\`` : name;
528
567
  }
568
+ /**
569
+ * Derive the entity links Turbine would declare from a schema's relations. One
570
+ * link per single-column hasMany / hasOne / belongsTo relation, owned by the
571
+ * relation's `from` table:
572
+ * - belongsTo Order->User: `Order.user -> User on user_id = id`
573
+ * (localKey = the FK on the owner, targetKey = the referenced key on target).
574
+ * - hasMany User->Order: `User.orders -> Order on id = user_id`
575
+ * (localKey = the referenced key on the owner, targetKey = the FK on the child).
576
+ * Composite-key relations and m2m junctions are skipped (links are single-column;
577
+ * a junction cannot be inferred from links). A relation whose name collides with
578
+ * a column on the owner is skipped and reported through `onCollision` (the caller
579
+ * decides whether to warn). Pure: no capability gate, no side effects.
580
+ */
581
+ function deriveDesiredLinks(schema, onCollision) {
582
+ const links = [];
583
+ for (const meta of Object.values(schema.tables)) {
584
+ for (const rel of Object.values(meta.relations)) {
585
+ if (rel.type === 'manyToMany')
586
+ continue;
587
+ const fk = (0, schema_js_1.normalizeKeyColumns)(rel.foreignKey);
588
+ const rk = (0, schema_js_1.normalizeKeyColumns)(rel.referenceKey);
589
+ if (fk.length !== 1 || rk.length !== 1)
590
+ continue; // links are single-column
591
+ // A link name that collides with a column on the owner is a hard engine
592
+ // error at declare time, so skip it (and let the caller warn).
593
+ const collides = meta.columns.some((c) => c.name === rel.name || c.field === rel.name);
594
+ if (collides) {
595
+ onCollision?.(meta.name, rel.name);
596
+ continue;
597
+ }
598
+ const localKey = rel.type === 'belongsTo' ? fk[0] : rk[0];
599
+ const targetKey = rel.type === 'belongsTo' ? rk[0] : fk[0];
600
+ links.push({ owner: meta.name, name: rel.name, target: rel.to, localKey, targetKey });
601
+ }
602
+ }
603
+ return links;
604
+ }
605
+ /** Render one {@link PowdbDesiredLink} as its create-only `link ...` DDL statement. */
606
+ function powdbLinkStatement(link) {
607
+ return (`link ${quotePowqlIdent(link.owner)}.${quotePowqlIdent(link.name)} -> ` +
608
+ `${quotePowqlIdent(link.target)} on ${quotePowqlIdent(link.localKey)} = ${quotePowqlIdent(link.targetKey)}`);
609
+ }
529
610
  function powqlSchemaDDL(schema, opts = {}) {
530
611
  const caps = opts.capabilities;
531
612
  const stmts = [];
@@ -606,8 +687,94 @@ function powqlSchemaDDL(schema, opts = {}) {
606
687
  }
607
688
  }
608
689
  }
690
+ // Entity-link DDL (opt-in). Appended after every type so the referenced types
691
+ // already exist when the links declare. Gated behind the entityLinks capability
692
+ // when a caller supplied one (an old engine has no `link` statement).
693
+ if (opts.emitLinks) {
694
+ if (caps)
695
+ requireCapability(caps, 'entityLinks', 'entity link DDL (`emitLinks`)');
696
+ const links = deriveDesiredLinks(schema, (owner, name) => {
697
+ if ((0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.powdbLinks, `collide:${owner}.${name}`)) {
698
+ console.warn(`[turbine] powqlSchemaDDL(emitLinks): relation "${name}" on "${owner}" collides with a column of the ` +
699
+ 'same name; skipping its link declaration (PowDB hard-errors on a link that shadows a column).');
700
+ }
701
+ });
702
+ for (const link of links)
703
+ stmts.push(powdbLinkStatement(link));
704
+ }
609
705
  return stmts;
610
706
  }
707
+ /**
708
+ * Existence-checked apply of entity-link DDL against a LIVE PowDB database.
709
+ * Because link DDL is create-only (no `if not exists`, redeclaring is an error,
710
+ * and there is no drop spelling), an apply layer must diff against the live
711
+ * catalog first: this reads the `schema links` listing, then executes only the
712
+ * links that are genuinely missing.
713
+ *
714
+ * - a desired link already declared with the SAME endpoints → skipped (idempotent);
715
+ * - a link declared with the same owner + name but DIFFERENT endpoints → skipped
716
+ * with a one-time warning (never dropped/replaced: there is no drop DDL, and a
717
+ * silent replace would be a destructive schema change);
718
+ * - a name/column collision on the owner → skipped (deriveDesiredLinks warns).
719
+ *
720
+ * Requires the `entityLinks` + `linkIntrospection` capabilities when `capabilities`
721
+ * is supplied (the listing statement is 0.19.1+). Returns the statements executed.
722
+ * `exec` runs one PowQL statement (embedded `db.raw` / a networked query shim); it
723
+ * must return rows keyed by column name for the `schema links` read.
724
+ */
725
+ async function applyPowdbLinks(exec, schema, options = {}) {
726
+ const caps = options.capabilities;
727
+ if (caps) {
728
+ requireCapability(caps, 'entityLinks', 'entity link DDL (`applyPowdbLinks`)');
729
+ requireCapability(caps, 'linkIntrospection', 'PowDB `schema links` introspection');
730
+ }
731
+ const existing = await listPowdbLinks(exec);
732
+ const byOwnerName = new Map(existing.map((l) => [`${l.owner}${l.name}`, l]));
733
+ const desired = deriveDesiredLinks(schema, (owner, name) => {
734
+ if ((0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.powdbLinks, `collide:${owner}.${name}`)) {
735
+ console.warn(`[turbine] applyPowdbLinks: relation "${name}" on "${owner}" collides with a column of the same name; ` +
736
+ 'skipping its link (PowDB hard-errors on a link that shadows a column).');
737
+ }
738
+ });
739
+ const executed = [];
740
+ for (const link of desired) {
741
+ const found = byOwnerName.get(`${link.owner}${link.name}`);
742
+ if (found) {
743
+ const same = found.target === link.target && found.localKey === link.localKey && found.targetKey === link.targetKey;
744
+ if (!same && (0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.powdbLinks, `drift:${link.owner}.${link.name}`)) {
745
+ console.warn(`[turbine] applyPowdbLinks: link "${link.name}" on "${link.owner}" is already declared with different ` +
746
+ `endpoints (have ${found.target} on ${found.localKey} = ${found.targetKey}, want ${link.target} on ` +
747
+ `${link.localKey} = ${link.targetKey}); leaving it untouched (PowDB has no drop-link DDL).`);
748
+ }
749
+ continue; // identical → idempotent skip; drift → warned skip
750
+ }
751
+ const stmt = powdbLinkStatement(link);
752
+ await exec(stmt);
753
+ executed.push(stmt);
754
+ }
755
+ return executed;
756
+ }
757
+ /**
758
+ * Read the live `schema links` listing into {@link PowdbDesiredLink} rows (owner,
759
+ * name, target, localKey, targetKey; cardinality is dropped — it is derived, not
760
+ * a DDL input). An empty catalog returns `[]`, never an error. Naming edge: a
761
+ * table literally named `links` is described with `describe links`, but the
762
+ * link LISTING is the contextual keyword form `schema links`.
763
+ */
764
+ async function listPowdbLinks(exec) {
765
+ const { rows } = await exec('schema links');
766
+ return rows.map((r) => ({
767
+ owner: powdbCell(r.owner),
768
+ name: powdbCell(r.name),
769
+ target: powdbCell(r.target),
770
+ localKey: powdbCell(r.local_key),
771
+ targetKey: powdbCell(r.target_key),
772
+ }));
773
+ }
774
+ /** Coerce a wire cell (string on the legacy wire, typed on the native wire) to string. */
775
+ function powdbCell(v) {
776
+ return v === null || v === undefined ? '' : String(v);
777
+ }
611
778
  /** Coerce a JS value into a PowDB positional param (the write side). */
612
779
  function toPowdbParam(value, col) {
613
780
  if (value instanceof PowdbFloatParam)
@@ -847,6 +1014,18 @@ function wrapPowdbError(err) {
847
1014
  if (/nested-loop join would evaluate|join result exceeds row limit/i.test(msg)) {
848
1015
  return new errors_js_1.ValidationError(`[turbine] PowDB join rejected: ${msg}`);
849
1016
  }
1017
+ // Entity-link misuse hard errors (0.19.1 turned the two silent-wrong-results
1018
+ // 0.19.0 behaviors into hard errors) → ValidationError (E003), a query defect
1019
+ // to fix. Matched explicitly on the pinned message text so both transports
1020
+ // classify identically regardless of how the wire wraps them (embedded tags
1021
+ // every error `GenericFailure`; networked carries a wire class): a bare dotted
1022
+ // projection path that was not alias-qualified, and an aggregate over a nested
1023
+ // or link projection. Turbine's own generator never emits either shape (it
1024
+ // always aliases and never aggregates over a link), so these fire only for a
1025
+ // raw user PowQL string; mapping them keeps that path typed.
1026
+ if (/is ambiguous in a projection|aggregates over a nested or link projection/i.test(msg)) {
1027
+ return new errors_js_1.ValidationError(`[turbine] PowDB query rejected: ${msg}`);
1028
+ }
850
1029
  // Typed wire error class (networked, server >= 0.17): the client surfaces the
851
1030
  // stable one-byte class from the error frame as `.wireErrorClass`. Classify by
852
1031
  // it BEFORE the generic message regexes: the server sanitizes non-allowlisted
@@ -1518,7 +1697,11 @@ function encodePowqlLiteral(value) {
1518
1697
  * between v0.18.0 and v0.19.0 (empty git diff), and `link` was already a lexer
1519
1698
  * keyword at 0.18.0. The 0.19 entity-links surface adds new STATEMENTS built from
1520
1699
  * pre-existing tokens, so the tokenization / escape surface this ceiling guards is
1521
- * unmoved.
1700
+ * unmoved. The 0.19.1 link-introspection / link-path round is likewise lexer-neutral:
1701
+ * `git diff v0.19.0 v0.19.1 -- crates/query/src/lexer.rs` is empty (the bare-dotted-path
1702
+ * hard error is parser-level, not tokenization), so this ceiling stays `'0.19'`. The
1703
+ * guard in {@link PowdbEmbeddedPool.exec} compares major.minor only, so `'0.19'`
1704
+ * already covers every 0.19.x patch — no bump is needed for 0.19.1.
1522
1705
  */
1523
1706
  exports.POWQL_LEXER_TESTED_CEILING = '0.19';
1524
1707
  /** Escape a string into a PowQL `"…"` literal, matching the engine lexer's escape rules. */