turbine-orm 0.76.0 → 0.77.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/cjs/cli/index.js +2 -2
  2. package/dist/cjs/cli/migrate.js +8 -8
  3. package/dist/cjs/cli/studio.js +6 -3
  4. package/dist/cjs/client.js +10 -10
  5. package/dist/cjs/dialect.js +1 -1
  6. package/dist/cjs/errors.d.ts +2 -2
  7. package/dist/cjs/errors.js +21 -25
  8. package/dist/cjs/generate.js +1 -1
  9. package/dist/cjs/introspect.js +5 -5
  10. package/dist/cjs/mssql.js +12 -12
  11. package/dist/cjs/mysql.js +5 -5
  12. package/dist/cjs/nested-write.js +31 -31
  13. package/dist/cjs/observe.js +1 -1
  14. package/dist/cjs/pipeline-submittable.js +5 -1
  15. package/dist/cjs/pipeline.js +8 -0
  16. package/dist/cjs/powdb-introspect.d.ts +1 -1
  17. package/dist/cjs/powdb-introspect.js +4 -4
  18. package/dist/cjs/powdb-shared.d.ts +348 -0
  19. package/dist/cjs/powdb-shared.js +587 -0
  20. package/dist/cjs/powdb.d.ts +12 -299
  21. package/dist/cjs/powdb.js +106 -567
  22. package/dist/cjs/powql.d.ts +1 -1
  23. package/dist/cjs/powql.js +89 -89
  24. package/dist/cjs/prisma-compat.js +16 -16
  25. package/dist/cjs/query/aggregates.js +19 -20
  26. package/dist/cjs/query/batched-loader.js +8 -8
  27. package/dist/cjs/query/builder.js +8 -8
  28. package/dist/cjs/query/compound-unique.js +2 -2
  29. package/dist/cjs/query/filters.js +1 -1
  30. package/dist/cjs/query/relations.js +60 -48
  31. package/dist/cjs/query/types.js +4 -4
  32. package/dist/cjs/query/utils.js +5 -5
  33. package/dist/cjs/query/warn-registry.d.ts +7 -0
  34. package/dist/cjs/query/warn-registry.js +7 -0
  35. package/dist/cjs/query/where-compile.js +1 -1
  36. package/dist/cjs/query/where.js +30 -31
  37. package/dist/cjs/query/writes.js +7 -7
  38. package/dist/cjs/realtime.js +4 -4
  39. package/dist/cjs/schema-metadata.js +1 -1
  40. package/dist/cjs/schema-sql.js +3 -3
  41. package/dist/cjs/seed.js +1 -1
  42. package/dist/cjs/sqlite.js +7 -7
  43. package/dist/cjs/typed-sql.js +1 -1
  44. package/dist/cli/index.js +2 -2
  45. package/dist/cli/migrate.js +8 -8
  46. package/dist/cli/studio.js +6 -3
  47. package/dist/client.js +10 -10
  48. package/dist/dialect.js +1 -1
  49. package/dist/errors.d.ts +2 -2
  50. package/dist/errors.js +21 -25
  51. package/dist/generate.js +1 -1
  52. package/dist/introspect.js +5 -5
  53. package/dist/mssql.js +12 -12
  54. package/dist/mysql.js +5 -5
  55. package/dist/nested-write.js +31 -31
  56. package/dist/observe.js +1 -1
  57. package/dist/pipeline-submittable.js +6 -2
  58. package/dist/pipeline.js +9 -1
  59. package/dist/powdb-introspect.d.ts +1 -1
  60. package/dist/powdb-introspect.js +2 -2
  61. package/dist/powdb-shared.d.ts +348 -0
  62. package/dist/powdb-shared.js +570 -0
  63. package/dist/powdb.d.ts +12 -299
  64. package/dist/powdb.js +71 -534
  65. package/dist/powql.d.ts +1 -1
  66. package/dist/powql.js +43 -43
  67. package/dist/prisma-compat.js +16 -16
  68. package/dist/query/aggregates.js +19 -20
  69. package/dist/query/batched-loader.js +8 -8
  70. package/dist/query/builder.js +8 -8
  71. package/dist/query/compound-unique.js +2 -2
  72. package/dist/query/filters.js +1 -1
  73. package/dist/query/relations.js +60 -48
  74. package/dist/query/types.js +4 -4
  75. package/dist/query/utils.js +5 -5
  76. package/dist/query/warn-registry.d.ts +7 -0
  77. package/dist/query/warn-registry.js +7 -0
  78. package/dist/query/where-compile.js +1 -1
  79. package/dist/query/where.js +30 -31
  80. package/dist/query/writes.js +7 -7
  81. package/dist/realtime.js +4 -4
  82. package/dist/schema-metadata.js +1 -1
  83. package/dist/schema-sql.js +3 -3
  84. package/dist/seed.js +1 -1
  85. package/dist/sqlite.js +7 -7
  86. package/dist/typed-sql.js +1 -1
  87. package/package.json +15 -7
@@ -0,0 +1,587 @@
1
+ "use strict";
2
+ /**
3
+ * Shared PowDB primitives: identifier quoting, capability gating, type mapping
4
+ * and value coercion.
5
+ *
6
+ * KEEP THIS FILE FREE OF IMPORTS FROM powdb.ts / powql.ts / powdb-introspect.ts.
7
+ * That is the whole point: those three formed a runtime cycle because powdb.ts
8
+ * re-exported from the other two while both imported values back from it. This
9
+ * is the same shape pg-types.ts and connection-url.ts already use to keep
10
+ * client.ts and query/ acyclic.
11
+ *
12
+ * A cycle is not a style complaint. `dist/powdb.js` is the largest engine
13
+ * entry, and in a cycle one of its three modules always executes while another
14
+ * is half-initialized: whichever module the loader reaches first decides, so
15
+ * the same code can work under ESM and fail under CJS, or work until an import
16
+ * is reordered. The failure surfaces far from its cause, as an undefined
17
+ * binding at call time. `scripts/check-import-cycles.mjs` now fails the build
18
+ * on any such cycle under `src/`, so this file is what keeps that gate green
19
+ * for the PowDB engine.
20
+ *
21
+ * Everything here is re-exported by powdb.ts under its original name, so the
22
+ * public `turbine-orm/powdb` surface is byte-identical to before the split.
23
+ * The few helpers powdb.ts consumes but never published (`isDateColumn`) are
24
+ * exported from this module and NOT re-exported from powdb.ts.
25
+ *
26
+ * @module
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.POWQL_KEYWORDS = exports.ALL_POWDB_CAPABILITIES = exports.PowdbJsonParam = exports.PowdbFloatParam = void 0;
30
+ exports.requireCapability = requireCapability;
31
+ exports.baseTsType = baseTsType;
32
+ exports.isJsonColumn = isJsonColumn;
33
+ exports.powqlColumnType = powqlColumnType;
34
+ exports.isDateColumn = isDateColumn;
35
+ exports.isPowdbDatetimeColumn = isPowdbDatetimeColumn;
36
+ exports.quotePowqlIdent = quotePowqlIdent;
37
+ exports.quotePowqlDotted = quotePowqlDotted;
38
+ exports.coerceValue = coerceValue;
39
+ exports.coerceNativeValue = coerceNativeValue;
40
+ exports.rowToEntity = rowToEntity;
41
+ exports.isStaleFramePowdbError = isStaleFramePowdbError;
42
+ const errors_js_1 = require("./errors.js");
43
+ // ---------------------------------------------------------------------------
44
+ // Bound-parameter markers
45
+ // ---------------------------------------------------------------------------
46
+ /**
47
+ * Marker wrapper for a value bound to a `float` column. The networked driver
48
+ * unwraps it to the plain number (the wire param is unchanged), but the
49
+ * *embedded* literal encoder reads it to emit a float-form PowQL literal (`42`
50
+ * → `42.0`) so an integer-valued float column stays unambiguously a float.
51
+ * Constructed in {@link PowqlInterface.param}.
52
+ */
53
+ class PowdbFloatParam {
54
+ value;
55
+ constructor(value) {
56
+ this.value = value;
57
+ }
58
+ }
59
+ exports.PowdbFloatParam = PowdbFloatParam;
60
+ /**
61
+ * Marker wrapper for a JS object/array bound to a `json` document column. Both
62
+ * transports serialize `value` with `JSON.stringify` and send the text as a
63
+ * `str` param / string literal, exactly how the PowDB docs insert a json
64
+ * document (the engine validates it as JSON text and stores the canonical
65
+ * binary form). Constructed in {@link PowqlInterface.param} when the target
66
+ * column is `json` and the value is a non-null object/array; a JS string
67
+ * written to a json column passes through RAW (same contract as pg jsonb,
68
+ * pass `'"x"'` to store the JSON string `"x"`), and `null` stays `null`.
69
+ */
70
+ class PowdbJsonParam {
71
+ value;
72
+ column;
73
+ /** `column` is diagnostic only: it names the target column when serialization fails. */
74
+ constructor(value, column) {
75
+ this.value = value;
76
+ this.column = column;
77
+ }
78
+ }
79
+ exports.PowdbJsonParam = PowdbJsonParam;
80
+ /**
81
+ * Minimum engine version each gated feature needs, for the E017 hint text.
82
+ * Most gates carry a `major.minor` floor (patch-insensitive); the two link
83
+ * lanes carry a `major.minor.patch` floor (`0.19.1`) because the listing
84
+ * statement and the safe traversal semantics landed in the PATCH release, not
85
+ * in 0.19.0. {@link atLeastVersion} compares all three components, so a
86
+ * `major.minor` floor still matches every patch of that minor.
87
+ */
88
+ const POWDB_FEATURE_MIN_VERSION = {
89
+ introspection: '0.10',
90
+ jsonDocs: '0.12',
91
+ docFieldIndexes: '0.13',
92
+ serverJoins: '0.13',
93
+ nestedProjections: '0.18',
94
+ entityLinks: '0.19',
95
+ linkIntrospection: '0.19.1',
96
+ linkPaths: '0.19.1',
97
+ datetimeCompare: '0.20',
98
+ projectedCountNonNull: '0.20',
99
+ };
100
+ /**
101
+ * Trusted-caller default: every FEATURE gate on, engine version unknown. Used
102
+ * for a directly-constructed {@link PowdbPool} / {@link PowdbEmbeddedPool} that
103
+ * did not go through {@link turbinePowDB}'s version probe (e.g. an injected
104
+ * pool, or a unit-test pool). `nativeRaw` stays OFF here because it flips the
105
+ * actual wire path and must only be enabled after a real server-version probe,
106
+ * never inferred from a bare construction. `nestedProjections` stays OFF for
107
+ * the same reason: it changes the generated PowQL for every `with` query, and
108
+ * an unprobed engine below 0.18 would reject the syntax outright.
109
+ * `entityLinks` stays OFF for a stronger reason still: declaring a link
110
+ * one-way-upgrades the on-disk catalog to v7 and locks out pre-0.19 binaries,
111
+ * so it must only ever light up behind a real version probe.
112
+ * `linkIntrospection` / `linkPaths` stay OFF for the same probe-only discipline:
113
+ * `linkPaths` flips real query generation (a to-one `with` compiling to link
114
+ * projections), and `linkIntrospection` is only meaningful once genuinely
115
+ * probed, so both must come from a real version resolution, never a bare
116
+ * construction.
117
+ * `datetimeCompare` / `projectedCountNonNull` stay ON here for the same
118
+ * trusted-caller reason as `jsonDocs` and `serverJoins`. Neither is a fallback
119
+ * gate: with the flag OFF the affected query is REFUSED, not served by some
120
+ * other statement, so defaulting them off would break working queries rather
121
+ * than protect anything. Every path that can learn the engine version
122
+ * (`turbinePowDB`, embedded or networked) resolves them from a real probe; this
123
+ * fallback only covers a hand-constructed or injected pool, whose owner is
124
+ * asserting the engine is current.
125
+ */
126
+ exports.ALL_POWDB_CAPABILITIES = {
127
+ engineVersion: null,
128
+ jsonDocs: true,
129
+ docFieldIndexes: true,
130
+ introspection: true,
131
+ serverJoins: true,
132
+ nestedProjections: false,
133
+ entityLinks: false,
134
+ linkIntrospection: false,
135
+ linkPaths: false,
136
+ datetimeCompare: true,
137
+ projectedCountNonNull: true,
138
+ nativeRaw: false,
139
+ };
140
+ /**
141
+ * Throw a version-hinting {@link UnsupportedFeatureError} (E017) when a gated
142
+ * PowQL feature is used on an engine that does not support it. Keeps old engines
143
+ * getting clean typed errors instead of raw PowQL parse failures.
144
+ *
145
+ * The error's first sentence already names the feature (`<feature> is
146
+ * unsupported on "PowDB".`), so the hint says "Requires PowDB >= x" rather than
147
+ * repeating the label: a long feature description read twice in one message
148
+ * (`per-field \`_count\` … is unsupported … per-field \`_count\` … requires …`)
149
+ * buries the version floor that is the actionable part.
150
+ *
151
+ * `extra` appends one more sentence for gates that have a workaround worth
152
+ * naming (e.g. the read path that answers the same query without the gated
153
+ * comparison).
154
+ */
155
+ function requireCapability(caps, key, feature, extra) {
156
+ if (caps[key])
157
+ return;
158
+ const min = POWDB_FEATURE_MIN_VERSION[key];
159
+ const reported = caps.engineVersion
160
+ ? `this connection reports ${caps.engineVersion}`
161
+ : 'this connection could not report a version';
162
+ throw new errors_js_1.UnsupportedFeatureError(feature, 'PowDB', `Requires PowDB >= ${min}; ${reported}. Upgrade powdb-server / @zvndev/powdb-embedded ` +
163
+ `(or pass \`assumeEngineVersion\` if the version cannot be detected).${extra ? ` ${extra}` : ''}`);
164
+ }
165
+ /**
166
+ * Strip a trailing `| null` union from a generated tsType and trim.
167
+ *
168
+ * THE ONE AUTHORITY for that question, and deliberately not a regular
169
+ * expression. The obvious spelling, `tsType.replace(/\s*\|\s*null$/i, '')`,
170
+ * is POLYNOMIAL: `\s*` can start at every position, so an input of N spaces
171
+ * with no `|` costs O(N^2). Measured on Node 24 before this change: 10,000
172
+ * spaces took 39.6 ms, 20,000 took 150.3 ms and 40,000 took 617.5 ms, the
173
+ * quadratic signature. It was written eight times across powdb.ts and powql.ts.
174
+ *
175
+ * `tsType` comes from a generated `metadata.ts` or from `defineSchema`, so it
176
+ * is not request input and this was never remotely reachable. It is fixed
177
+ * anyway, in one place, because eight hand-copied spellings of a hot predicate
178
+ * is the drift shape this codebase keeps paying for, and because the linear
179
+ * version is not harder to read.
180
+ *
181
+ * Semantics are byte-for-byte those of the regex it replaces, including the
182
+ * `$` anchor: `"string | null "` has trailing space so the union does NOT
183
+ * match, and the value only gets trimmed. `src/test/tstype-null-union.test.ts`
184
+ * asserts the equivalence over a corpus rather than asserting my reading of it.
185
+ */
186
+ function baseTsType(tsType) {
187
+ const NULL = 'null';
188
+ const end = tsType.length;
189
+ if (end >= NULL.length && tsType.slice(end - NULL.length).toLowerCase() === NULL) {
190
+ let i = end - NULL.length;
191
+ while (i > 0 && isTsSpace(tsType.charCodeAt(i - 1)))
192
+ i--;
193
+ if (i > 0 && tsType.charCodeAt(i - 1) === 0x7c /* | */) {
194
+ i--;
195
+ while (i > 0 && isTsSpace(tsType.charCodeAt(i - 1)))
196
+ i--;
197
+ return tsType.slice(0, i).trim();
198
+ }
199
+ }
200
+ return tsType.trim();
201
+ }
202
+ /** `\s` for {@link baseTsType}: one character, O(1), no backtracking. */
203
+ function isTsSpace(code) {
204
+ return (code === 0x20 || // space
205
+ (code >= 0x09 && code <= 0x0d) || // tab, LF, VT, FF, CR
206
+ code === 0xa0 ||
207
+ code === 0x1680 ||
208
+ (code >= 0x2000 && code <= 0x200a) ||
209
+ code === 0x2028 ||
210
+ code === 0x2029 ||
211
+ code === 0x202f ||
212
+ code === 0x205f ||
213
+ code === 0x3000 ||
214
+ code === 0xfeff);
215
+ }
216
+ /**
217
+ * Does this column map to PowDB's native `json` document type? A Postgres
218
+ * `json`/`jsonb` type (via `dialectType`/`pgType`) is authoritative; otherwise
219
+ * the tsType heuristic (`Record<…>`, `object`, `unknown`, an object/array
220
+ * literal) that the four scalar branches do not claim. Array columns never map
221
+ * to json, a PowDB array only exists INSIDE a json document, so a Postgres
222
+ * array column has no PowDB shape and still throws in {@link powqlColumnType}.
223
+ */
224
+ function isJsonColumn(col) {
225
+ if (col.isArray)
226
+ return false;
227
+ const dbType = (col.dialectType ?? col.pgType ?? '').toLowerCase();
228
+ if (dbType === 'json' || dbType === 'jsonb')
229
+ return true;
230
+ const ts = baseTsType(col.tsType);
231
+ if (ts === 'Date' || ts === 'boolean' || ts === 'number' || ts === 'bigint' || ts === 'string')
232
+ return false;
233
+ if (ts === 'Buffer' || ts === 'Uint8Array')
234
+ return false;
235
+ return /Record<|object|unknown|\[\]|\{/.test(ts);
236
+ }
237
+ /**
238
+ * Map a Turbine column to the PowQL DDL type used in `defineSchema` →
239
+ * `type T { … }`. Turbine never emits PowDB's `uuid`/`datetime`/`bytes` types,
240
+ * which cannot hold client-supplied values on the wire (no literal, no cast):
241
+ * - `Date` → `int` (epoch micros) - `boolean` → `bool`
242
+ * - integral `number`/`bigint` → `int` - fractional `number` → `float`
243
+ * - JSON / object columns → `json` (native PowDB document type, ≥ 0.12)
244
+ * - everything else (incl. UUID/PK strings) → `str`
245
+ * Array (non-json) and bytes columns throw, they have no PowDB equivalent.
246
+ */
247
+ function powqlColumnType(col) {
248
+ if (col.isArray) {
249
+ throw new errors_js_1.ValidationError(`Column "${col.name}" is an array, PowDB has no array type. Arrays are unsupported on the PowDB backend.`);
250
+ }
251
+ if (isJsonColumn(col))
252
+ return 'json';
253
+ const ts = baseTsType(col.tsType);
254
+ if (ts === 'Date')
255
+ return 'int'; // epoch micros
256
+ if (ts === 'boolean')
257
+ return 'bool';
258
+ if (ts === 'number')
259
+ return isFloatColumn(col) ? 'float' : 'int';
260
+ if (ts === 'bigint')
261
+ return 'int';
262
+ if (ts === 'string')
263
+ return 'str';
264
+ if (ts === 'Buffer' || ts === 'Uint8Array') {
265
+ throw new errors_js_1.ValidationError(`Column "${col.name}" is binary, PowDB cannot store client-supplied bytes on the wire. Use a string (e.g. base64) instead.`);
266
+ }
267
+ return 'str';
268
+ }
269
+ /** Heuristic: does this numeric column hold fractional values (→ PowQL `float`)? */
270
+ function isFloatColumn(col) {
271
+ const t = (col.dialectType ?? col.pgType ?? '').toLowerCase();
272
+ return /float|double|real|numeric|decimal|money/.test(t);
273
+ }
274
+ /**
275
+ * Is a column stored as `int` epoch micros but surfaced as a JS `Date`?
276
+ *
277
+ * Exported because powdb.ts's parameter encoder asks the same question on the
278
+ * write side, and one definition of "this column is a Date" is the point of a
279
+ * shared leaf. Deliberately NOT re-exported from powdb.ts: the
280
+ * `turbine-orm/powdb` surface is unchanged by this split.
281
+ */
282
+ function isDateColumn(col) {
283
+ return baseTsType(col.tsType) === 'Date';
284
+ }
285
+ /**
286
+ * Is this column stored in PowDB's NATIVE `datetime` type (as opposed to the
287
+ * `int` epoch micros Turbine's own DDL emits for a `Date` column)?
288
+ *
289
+ * Only the literal PowQL type name counts. `powqlColumnType` never returns
290
+ * `datetime`, so a Turbine-provisioned table can never have one; the shapes that
291
+ * do are a table created outside Turbine and read back through
292
+ * `introspectPowdbDatabase` (which maps `datetime` → `{ tsType: 'Date',
293
+ * dialectType: 'datetime' }`), or hand-written metadata declaring it. Deliberately
294
+ * strict: a Postgres-sourced `timestamptz` column is DDL'd as PowQL `int`, so it
295
+ * is NOT a PowDB datetime and must not be caught here.
296
+ *
297
+ * Matters because comparing a datetime column against the integer timestamp
298
+ * literal Turbine binds was silently wrong below engine 0.20 (see
299
+ * {@link PowdbCapabilities.datetimeCompare}).
300
+ */
301
+ function isPowdbDatetimeColumn(col) {
302
+ return (col.dialectType ?? col.pgType ?? '').toLowerCase() === 'datetime';
303
+ }
304
+ // ---------------------------------------------------------------------------
305
+ // Identifier quoting
306
+ // ---------------------------------------------------------------------------
307
+ /**
308
+ * PowQL reserved words, the v0.10 lexer keyword table from POWQL.md's
309
+ * "Reserved Words and Quoting" section, including the v0.10 additions
310
+ * `schema` and `describe`. Keyword matching is case-sensitive in the lexer,
311
+ * so only the exact lowercase form collides.
312
+ */
313
+ exports.POWQL_KEYWORDS = new Set([
314
+ 'abs',
315
+ 'add',
316
+ 'alter',
317
+ 'and',
318
+ 'as',
319
+ 'asc',
320
+ 'auto',
321
+ 'avg',
322
+ 'begin',
323
+ 'between',
324
+ 'case',
325
+ 'cast',
326
+ 'ceil',
327
+ 'column',
328
+ 'commit',
329
+ 'concat',
330
+ 'conflict',
331
+ 'count',
332
+ 'cross',
333
+ 'date_add',
334
+ 'date_diff',
335
+ 'default',
336
+ 'delete',
337
+ 'dense_rank',
338
+ 'desc',
339
+ 'describe',
340
+ 'distinct',
341
+ 'drop',
342
+ 'else',
343
+ 'end',
344
+ 'exists',
345
+ 'explain',
346
+ 'extract',
347
+ 'false',
348
+ 'filter',
349
+ 'floor',
350
+ 'group',
351
+ 'having',
352
+ 'in',
353
+ 'index',
354
+ 'inner',
355
+ 'insert',
356
+ 'is',
357
+ 'join',
358
+ 'left',
359
+ 'length',
360
+ 'let',
361
+ 'like',
362
+ 'limit',
363
+ 'link',
364
+ 'lower',
365
+ 'match',
366
+ 'materialize',
367
+ 'materialized',
368
+ 'max',
369
+ 'min',
370
+ 'multi',
371
+ 'not',
372
+ 'now',
373
+ 'null',
374
+ 'offset',
375
+ 'on',
376
+ 'or',
377
+ 'order',
378
+ 'outer',
379
+ 'over',
380
+ 'partition',
381
+ 'pow',
382
+ 'rank',
383
+ 'refresh',
384
+ 'required',
385
+ 'returning',
386
+ 'right',
387
+ 'rollback',
388
+ 'round',
389
+ 'row_number',
390
+ 'schema',
391
+ 'select',
392
+ 'sqrt',
393
+ 'substring',
394
+ 'sum',
395
+ 'then',
396
+ 'transaction',
397
+ 'trim',
398
+ 'true',
399
+ 'type',
400
+ 'union',
401
+ 'unique',
402
+ 'update',
403
+ 'upper',
404
+ 'upsert',
405
+ 'view',
406
+ 'when',
407
+ ]);
408
+ const POWQL_BARE_IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
409
+ /**
410
+ * Backtick-quote an identifier when PowQL would otherwise lex it as a keyword
411
+ * (or when it contains characters outside the bare-identifier grammar).
412
+ * Applied only in bare-identifier positions, DDL type/field names, index DDL,
413
+ * and `insert`/`update`/`upsert` assignment targets. Dotted references
414
+ * (`.col` in filters/projections/ordering) bypass keyword lookup on every
415
+ * engine version and deliberately stay bare for ≤0.9 compatibility. Backticks
416
+ * parse on PowDB ≥ 0.10; on older engines these names were already parse
417
+ * errors when emitted bare, so quoting is strictly an improvement.
418
+ */
419
+ function quotePowqlIdent(name) {
420
+ if (name.includes('`')) {
421
+ // The lexer has no backtick escape inside a quoted identifier.
422
+ throw new errors_js_1.ValidationError(`Identifier "${name}" contains a backtick, which PowQL cannot represent.`);
423
+ }
424
+ return exports.POWQL_KEYWORDS.has(name) || !POWQL_BARE_IDENT.test(name) ? `\`${name}\`` : name;
425
+ }
426
+ /**
427
+ * The DOTTED-position spelling of {@link quotePowqlIdent}: quote a name that
428
+ * falls outside the bare-identifier grammar, and only that.
429
+ *
430
+ * A dotted reference (`.col` in a filter, projection, `order`, `group`, or an
431
+ * `upsert on`) bypasses keyword lookup, so `.order` parses on every engine
432
+ * version and stays bare here, which is the ≤0.9 compatibility decision
433
+ * {@link quotePowqlIdent} documents and which this must not undo.
434
+ *
435
+ * What it does NOT excuse is interpolating the name RAW, which is what these
436
+ * sites used to do. Keyword-ness is a parsing question; a name outside
437
+ * `POWQL_BARE_IDENT` is a statement-integrity one, and that name is the only
438
+ * thing that can carry PowQL syntax into a statement whose values are all bound
439
+ * as `$N` params. Reaching it needs a hostile column name (an introspected
440
+ * database, a generator, a migration authored elsewhere) since names come from
441
+ * schema metadata, but "the names are trusted" is not the invariant the rest of
442
+ * this engine is written to. So: bare when the grammar allows it (byte-identical
443
+ * output for every ordinary and every keyword name), quoted when it does not,
444
+ * where the bare form was a parse error anyway. Verified against the engine that
445
+ * a quoted dotted reference parses everywhere the bare one does and yields the
446
+ * same result-column name.
447
+ */
448
+ function quotePowqlDotted(name) {
449
+ if (POWQL_BARE_IDENT.test(name))
450
+ return name;
451
+ if (name.includes('`')) {
452
+ throw new errors_js_1.ValidationError(`Identifier "${name}" contains a backtick, which PowQL cannot represent.`);
453
+ }
454
+ return `\`${name}\``;
455
+ }
456
+ // ---------------------------------------------------------------------------
457
+ // Value coercion, PowDB wire cell -> JS value
458
+ // ---------------------------------------------------------------------------
459
+ /**
460
+ * Coerce a single PowDB wire string into the JS value its column type implies.
461
+ * Every PowDB value arrives as a string; NULL arrives as the bareword `"null"`.
462
+ * Metadata resolves the `"null"` ambiguity for nullable non-string columns.
463
+ */
464
+ function coerceValue(raw, col) {
465
+ const ts = baseTsType(col.tsType);
466
+ const json = isJsonColumn(col);
467
+ // NULL bareword: unambiguous for non-string columns; for `str` we cannot tell a
468
+ // literal "null" from SQL NULL, so a nullable str of value "null" reads as null.
469
+ // For a `json` column the bareword `null` (a legacy-wire rendering shared by an
470
+ // absent value AND a top-level JSON-null document, documented residual,
471
+ // resolved on the native transport by the WireValue path) maps to null; a JSON
472
+ // string document "null" renders WITH quotes (`"null"`) and parses distinctly.
473
+ if (raw === 'null' && (json || ts !== 'string' || col.nullable))
474
+ return null;
475
+ if (json) {
476
+ try {
477
+ return JSON.parse(raw);
478
+ }
479
+ catch {
480
+ return raw; // defensive: canonical JSON text always parses
481
+ }
482
+ }
483
+ if (ts === 'Date') {
484
+ const micros = Number(raw);
485
+ return Number.isFinite(micros) ? new Date(micros / 1000) : null;
486
+ }
487
+ if (ts === 'boolean')
488
+ return raw === 'true';
489
+ if (ts === 'number') {
490
+ const n = Number(raw);
491
+ // int8 policy: keep precision-losing big integers as strings.
492
+ return Number.isSafeInteger(n) || !Number.isInteger(n) ? n : raw;
493
+ }
494
+ if (ts === 'bigint')
495
+ return BigInt(raw);
496
+ return raw; // string / uuid-as-string
497
+ }
498
+ /**
499
+ * Coerce a single cell that arrived over the NATIVE typed wire (decoded from a
500
+ * {@link PowdbWireValue}, so already a JS `bigint`/`number`/`boolean`/`string`/
501
+ * `NativeJson`/`Uint8Array`/`null`, never a bare `"null"` string). Unlike
502
+ * {@link coerceValue} this NEVER collapses the string `"null"` to `null`: an
503
+ * absent value already decoded to `null` (from the `empty` cell), so a genuine
504
+ * str `"null"` stays the string `"null"` (fixes the legacy-wire wart on the
505
+ * native transport). `datetime`-shaped cells (int micros) become `Date`; a
506
+ * bigint on a `number` column follows the int8 safe-integer policy.
507
+ *
508
+ * A date cell can also arrive as a DIGIT STRING: a nested-projection block's
509
+ * children ride a JSON array, and micros exceed `Number.MAX_SAFE_INTEGER`'s
510
+ * decimal comfort, so the engine renders them as a JSON string. Before that
511
+ * string was parsed here, a nested `with` handed back the raw micros text while
512
+ * the batched loader and the native join both handed back a `Date` (the same
513
+ * relation, three answers). Only an all-digit string is parsed; any other text
514
+ * on a date column passes through untouched.
515
+ */
516
+ function coerceNativeValue(value, col) {
517
+ if (value === undefined || value === null)
518
+ return null;
519
+ if (isDateColumn(col)) {
520
+ if (typeof value === 'bigint')
521
+ return new Date(Number(value) / 1000);
522
+ if (typeof value === 'number')
523
+ return new Date(value / 1000);
524
+ if (typeof value === 'string' && /^-?\d+$/.test(value))
525
+ return new Date(Number(value) / 1000);
526
+ return value;
527
+ }
528
+ const ts = baseTsType(col.tsType);
529
+ if (typeof value === 'bigint') {
530
+ if (ts === 'bigint')
531
+ return value;
532
+ if (ts === 'number') {
533
+ const n = Number(value);
534
+ return Number.isSafeInteger(n) ? n : value.toString(); // int8 policy: keep big ints as strings
535
+ }
536
+ return value;
537
+ }
538
+ return value; // number / boolean / string / NativeJson document / Uint8Array
539
+ }
540
+ /**
541
+ * Map one raw PowDB row into a typed entity (camelCase fields, coerced values).
542
+ * Only the columns present in `raw` are emitted, so partial `select`
543
+ * projections round-trip unchanged. `native` selects the coercion policy: the
544
+ * default `false` handles the legacy string wire (every cell is a string, via
545
+ * {@link coerceValue}); `true` handles the native typed wire, where non-string
546
+ * cells arrive pre-typed and go through {@link coerceNativeValue} (see F3).
547
+ * Callers on the native transport pass `this.pool.capabilities.nativeRaw`.
548
+ */
549
+ function rowToEntity(raw, meta, native = false) {
550
+ const byName = new Map(meta.columns.map((c) => [c.name, c]));
551
+ const out = {};
552
+ for (const snake of Object.keys(raw)) {
553
+ const col = byName.get(snake);
554
+ const field = meta.reverseColumnMap[snake] ?? snake;
555
+ const value = raw[snake];
556
+ if (!col) {
557
+ out[field] = value;
558
+ }
559
+ else if (native) {
560
+ out[field] = coerceNativeValue(value, col);
561
+ }
562
+ else {
563
+ out[field] = typeof value === 'string' ? coerceValue(value, col) : value;
564
+ }
565
+ }
566
+ return out;
567
+ }
568
+ // ---------------------------------------------------------------------------
569
+ // Error classification shared with the read-retry path
570
+ // ---------------------------------------------------------------------------
571
+ /**
572
+ * True when `err` is the stale-wire-frame {@link ConnectionError} produced by
573
+ * {@link wrapPowdbError} (its `.cause` is a `protocol_error` PowDBError, or the
574
+ * message carries the invalid-state signature). The opt-in read retry
575
+ * (`retryStaleReads`, evaluated in {@link PowqlInterface}'s exec seam) uses this
576
+ * to decide whether a first-statement READ may be replayed once on a fresh
577
+ * connection; writes are NEVER retried (an ambiguous mutation reply is unsafe
578
+ * to replay, matching the client's own native-path policy).
579
+ */
580
+ function isStaleFramePowdbError(err) {
581
+ if (!(err instanceof errors_js_1.ConnectionError))
582
+ return false;
583
+ const cause = err.cause;
584
+ if (cause && typeof cause === 'object' && cause.code === 'protocol_error')
585
+ return true;
586
+ return /PowDB connection is in an invalid state/.test(err.message);
587
+ }