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
|
@@ -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
|
+
}
|