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