turbine-orm 0.51.0 → 0.52.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/README.md +33 -5
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/client.d.ts +106 -2
- package/dist/cjs/client.js +111 -5
- package/dist/cjs/dialect.d.ts +33 -0
- package/dist/cjs/dialect.js +14 -0
- package/dist/cjs/engine-config.d.ts +49 -0
- package/dist/cjs/engine-config.js +19 -0
- package/dist/cjs/index-advisor.js +0 -0
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +3 -2
- package/dist/cjs/mssql.d.ts +8 -3
- package/dist/cjs/mssql.js +22 -3
- package/dist/cjs/mysql.d.ts +7 -3
- package/dist/cjs/mysql.js +20 -3
- package/dist/cjs/nested-write.d.ts +31 -0
- package/dist/cjs/nested-write.js +80 -2
- package/dist/cjs/powdb-introspect.d.ts +10 -1
- package/dist/cjs/powdb-introspect.js +10 -1
- package/dist/cjs/powdb.d.ts +116 -6
- package/dist/cjs/powdb.js +169 -10
- package/dist/cjs/powql.d.ts +161 -1
- package/dist/cjs/powql.js +299 -19
- package/dist/cjs/prisma-compat.d.ts +54 -8
- package/dist/cjs/prisma-compat.js +136 -20
- package/dist/cjs/query/batched-loader.d.ts +7 -0
- package/dist/cjs/query/batched-loader.js +97 -15
- package/dist/cjs/query/builder.d.ts +131 -5
- package/dist/cjs/query/builder.js +223 -19
- package/dist/cjs/query/compound-unique.js +0 -0
- package/dist/cjs/query/index.d.ts +1 -1
- package/dist/cjs/query/index.js +2 -1
- package/dist/cjs/query/warn-registry.d.ts +10 -0
- package/dist/cjs/query/warn-registry.js +10 -0
- package/dist/cjs/query/writes.js +115 -7
- package/dist/cjs/sqlite.d.ts +10 -4
- package/dist/cjs/sqlite.js +18 -4
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/client.d.ts +106 -2
- package/dist/client.js +111 -5
- package/dist/dialect.d.ts +33 -0
- package/dist/dialect.js +14 -0
- package/dist/engine-config.d.ts +49 -0
- package/dist/engine-config.js +18 -0
- package/dist/index-advisor.js +0 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mssql.d.ts +8 -3
- package/dist/mssql.js +22 -3
- package/dist/mysql.d.ts +7 -3
- package/dist/mysql.js +20 -3
- package/dist/nested-write.d.ts +31 -0
- package/dist/nested-write.js +79 -2
- package/dist/powdb-introspect.d.ts +10 -1
- package/dist/powdb-introspect.js +10 -1
- package/dist/powdb.d.ts +116 -6
- package/dist/powdb.js +167 -9
- package/dist/powql.d.ts +161 -1
- package/dist/powql.js +299 -19
- package/dist/prisma-compat.d.ts +54 -8
- package/dist/prisma-compat.js +136 -20
- package/dist/query/batched-loader.d.ts +7 -0
- package/dist/query/batched-loader.js +98 -16
- package/dist/query/builder.d.ts +131 -5
- package/dist/query/builder.js +222 -18
- package/dist/query/compound-unique.js +0 -0
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.js +1 -1
- package/dist/query/warn-registry.d.ts +10 -0
- package/dist/query/warn-registry.js +10 -0
- package/dist/query/writes.js +116 -8
- package/dist/sqlite.d.ts +10 -4
- package/dist/sqlite.js +19 -5
- package/package.json +3 -3
|
@@ -101,6 +101,37 @@ export declare function hasRelationFields(data: Record<string, unknown>, tableMe
|
|
|
101
101
|
* Handles composite keys. Returns a new object (does not mutate input).
|
|
102
102
|
*/
|
|
103
103
|
export declare function injectForeignKey(childData: Record<string, unknown>, relation: RelationDef, parentRow: Record<string, unknown>, schema: SchemaMetadata): Record<string, unknown>;
|
|
104
|
+
/**
|
|
105
|
+
* Split rows destined for `createMany` into CONTIGUOUS runs that each name the
|
|
106
|
+
* same fields.
|
|
107
|
+
*
|
|
108
|
+
* `createMany` compiles ONE statement whose column list comes from the first
|
|
109
|
+
* row, so it refuses a batch whose rows disagree about which fields they name
|
|
110
|
+
* (`assertUniformCreateManyRows` in query/writes.ts): a field a later row omits
|
|
111
|
+
* would be bound as NULL over that column's default, and a field only a later
|
|
112
|
+
* row names would be dropped. Rows that arrive as ordinary user input, a nested
|
|
113
|
+
* `create: [...]` array or a Prisma-shaped `createMany` on the compat layer, are
|
|
114
|
+
* allowed to mix shapes, so the caller splits the batch instead of refusing it
|
|
115
|
+
* and issues one `createMany` per run. Every run is internally uniform, so the
|
|
116
|
+
* refusal still stands where it matters and none of the corruption it exists to
|
|
117
|
+
* stop becomes reachable.
|
|
118
|
+
*
|
|
119
|
+
* Runs are CONTIGUOUS rather than grouped by shape across the whole array: one
|
|
120
|
+
* statement inserted the rows in array order, and running contiguous runs in
|
|
121
|
+
* order keeps that, so generated keys stay ascending with the caller's array.
|
|
122
|
+
* Grouping non-adjacent rows would batch harder (`A B A B` in two statements
|
|
123
|
+
* rather than four) at the cost of reordering rows, which is observable through
|
|
124
|
+
* any server-assigned sequence.
|
|
125
|
+
*
|
|
126
|
+
* A uniform array, the overwhelmingly common shape, yields exactly one run
|
|
127
|
+
* holding the ORIGINAL array, so the caller makes the one call it always made.
|
|
128
|
+
*
|
|
129
|
+
* A key whose value is `undefined` is not named, matching `definedKeys` in
|
|
130
|
+
* query/writes.ts (and single-row `create`).
|
|
131
|
+
*
|
|
132
|
+
* @internal shared by the nested-write batch path and turbine-orm/prisma-compat.
|
|
133
|
+
*/
|
|
134
|
+
export declare function createManyShapeRuns<T extends Record<string, unknown>>(rows: T[]): T[][];
|
|
104
135
|
/**
|
|
105
136
|
* Tree-walking create: inserts the parent row, then processes each relation
|
|
106
137
|
* operation (create, connect, connectOrCreate), and finally reads back the
|
package/dist/cjs/nested-write.js
CHANGED
|
@@ -16,6 +16,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
16
16
|
exports.extractRelationFields = extractRelationFields;
|
|
17
17
|
exports.hasRelationFields = hasRelationFields;
|
|
18
18
|
exports.injectForeignKey = injectForeignKey;
|
|
19
|
+
exports.createManyShapeRuns = createManyShapeRuns;
|
|
19
20
|
exports.executeNestedCreate = executeNestedCreate;
|
|
20
21
|
exports.executeNestedUpdate = executeNestedUpdate;
|
|
21
22
|
const errors_js_1 = require("./errors.js");
|
|
@@ -86,6 +87,72 @@ function injectForeignKey(childData, relation, parentRow, schema) {
|
|
|
86
87
|
}
|
|
87
88
|
return result;
|
|
88
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Split rows destined for `createMany` into CONTIGUOUS runs that each name the
|
|
92
|
+
* same fields.
|
|
93
|
+
*
|
|
94
|
+
* `createMany` compiles ONE statement whose column list comes from the first
|
|
95
|
+
* row, so it refuses a batch whose rows disagree about which fields they name
|
|
96
|
+
* (`assertUniformCreateManyRows` in query/writes.ts): a field a later row omits
|
|
97
|
+
* would be bound as NULL over that column's default, and a field only a later
|
|
98
|
+
* row names would be dropped. Rows that arrive as ordinary user input, a nested
|
|
99
|
+
* `create: [...]` array or a Prisma-shaped `createMany` on the compat layer, are
|
|
100
|
+
* allowed to mix shapes, so the caller splits the batch instead of refusing it
|
|
101
|
+
* and issues one `createMany` per run. Every run is internally uniform, so the
|
|
102
|
+
* refusal still stands where it matters and none of the corruption it exists to
|
|
103
|
+
* stop becomes reachable.
|
|
104
|
+
*
|
|
105
|
+
* Runs are CONTIGUOUS rather than grouped by shape across the whole array: one
|
|
106
|
+
* statement inserted the rows in array order, and running contiguous runs in
|
|
107
|
+
* order keeps that, so generated keys stay ascending with the caller's array.
|
|
108
|
+
* Grouping non-adjacent rows would batch harder (`A B A B` in two statements
|
|
109
|
+
* rather than four) at the cost of reordering rows, which is observable through
|
|
110
|
+
* any server-assigned sequence.
|
|
111
|
+
*
|
|
112
|
+
* A uniform array, the overwhelmingly common shape, yields exactly one run
|
|
113
|
+
* holding the ORIGINAL array, so the caller makes the one call it always made.
|
|
114
|
+
*
|
|
115
|
+
* A key whose value is `undefined` is not named, matching `definedKeys` in
|
|
116
|
+
* query/writes.ts (and single-row `create`).
|
|
117
|
+
*
|
|
118
|
+
* @internal shared by the nested-write batch path and turbine-orm/prisma-compat.
|
|
119
|
+
*/
|
|
120
|
+
function createManyShapeRuns(rows) {
|
|
121
|
+
if (rows.length === 0)
|
|
122
|
+
return [];
|
|
123
|
+
const runs = [];
|
|
124
|
+
let current = [];
|
|
125
|
+
let currentShape = '';
|
|
126
|
+
for (const row of rows) {
|
|
127
|
+
const shape = rowShapeKey(row);
|
|
128
|
+
if (current.length === 0) {
|
|
129
|
+
currentShape = shape;
|
|
130
|
+
}
|
|
131
|
+
else if (shape !== currentShape) {
|
|
132
|
+
runs.push(current);
|
|
133
|
+
current = [];
|
|
134
|
+
currentShape = shape;
|
|
135
|
+
}
|
|
136
|
+
current.push(row);
|
|
137
|
+
}
|
|
138
|
+
runs.push(current);
|
|
139
|
+
// One run means every row agreed: hand back the caller's own array so the
|
|
140
|
+
// resulting call is indistinguishable from the ungrouped one.
|
|
141
|
+
return runs.length === 1 ? [rows] : runs;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Order-independent identity of the fields a row names. Each key is written
|
|
145
|
+
* length-prefixed so no delimiter can appear inside one: a property name may
|
|
146
|
+
* legally contain any character at all, so a plain join would let two distinct
|
|
147
|
+
* key sets collide on the same string.
|
|
148
|
+
*/
|
|
149
|
+
function rowShapeKey(row) {
|
|
150
|
+
return Object.keys(row)
|
|
151
|
+
.filter((k) => row[k] !== undefined)
|
|
152
|
+
.sort()
|
|
153
|
+
.map((k) => `${k.length}:${k}`)
|
|
154
|
+
.join(',');
|
|
155
|
+
}
|
|
89
156
|
// ---------------------------------------------------------------------------
|
|
90
157
|
// Internal helpers
|
|
91
158
|
// ---------------------------------------------------------------------------
|
|
@@ -841,9 +908,20 @@ async function processHasManyCreate(ctx, rel, ops, parentRow, depth, path, relNa
|
|
|
841
908
|
}
|
|
842
909
|
}
|
|
843
910
|
else {
|
|
844
|
-
// Batch via createMany (UNNEST), fast path
|
|
911
|
+
// Batch via createMany (UNNEST), fast path.
|
|
912
|
+
//
|
|
913
|
+
// A nested `create: [...]` array is ordinary user input and may
|
|
914
|
+
// legitimately mix row shapes (`[{ title }, { title, published }]`),
|
|
915
|
+
// which one createMany cannot express, see createManyShapeRuns. Split
|
|
916
|
+
// it into contiguous same-shape runs and issue one createMany per run:
|
|
917
|
+
// every run stays uniform, so an omitted field takes its column default
|
|
918
|
+
// instead of being written as NULL, and the rows still reach the
|
|
919
|
+
// database in the caller's array order exactly as the single statement
|
|
920
|
+
// put them there. A uniform array is one run and one identical call.
|
|
845
921
|
const injected = items.map((item) => injectForeignKey(item, rel, parentRow, ctx.schema));
|
|
846
|
-
|
|
922
|
+
for (const run of createManyShapeRuns(injected)) {
|
|
923
|
+
await ctx.tx.table(rel.to).createMany({ data: run });
|
|
924
|
+
}
|
|
847
925
|
}
|
|
848
926
|
}
|
|
849
927
|
}
|
|
@@ -45,7 +45,16 @@
|
|
|
45
45
|
* round-trip; only plain `unique`/`index` columns appear in `indexes`.
|
|
46
46
|
* - `datetime` / `uuid` / `bytes` columns map to read-oriented TS types
|
|
47
47
|
* (`Date` / `string` / `Uint8Array`). Turbine never emits those PowQL types
|
|
48
|
-
* on write, so writing to such a column may not round-trip.
|
|
48
|
+
* on write, so writing to such a column may not round-trip. This introspector
|
|
49
|
+
* is the ONLY producer of `dialectType: 'datetime'` metadata, and a predicate
|
|
50
|
+
* on such a column is version-gated: comparing it against the integer
|
|
51
|
+
* microseconds Turbine binds was silently wrong below engine 0.20, so those
|
|
52
|
+
* predicates raise a typed E017 there rather than being answered wrongly (see
|
|
53
|
+
* `PowdbCapabilities.datetimeCompare`). The `in` / `not in` list forms are
|
|
54
|
+
* still wrong upstream at 0.20, so Turbine never emits one for such a column:
|
|
55
|
+
* it compiles to the equality chain the engine answers correctly, bounded by
|
|
56
|
+
* `MAX_POWQL_DATETIME_TERMS`. Reads, ordering, grouping and null checks on
|
|
57
|
+
* the column are unaffected.
|
|
49
58
|
*
|
|
50
59
|
* v1 is a PROGRAMMATIC API (exported from `turbine-orm/powdb`); the CLI's
|
|
51
60
|
* `turbine generate` still defaults to Postgres. Routing a `powdb://` URL
|
|
@@ -46,7 +46,16 @@
|
|
|
46
46
|
* round-trip; only plain `unique`/`index` columns appear in `indexes`.
|
|
47
47
|
* - `datetime` / `uuid` / `bytes` columns map to read-oriented TS types
|
|
48
48
|
* (`Date` / `string` / `Uint8Array`). Turbine never emits those PowQL types
|
|
49
|
-
* on write, so writing to such a column may not round-trip.
|
|
49
|
+
* on write, so writing to such a column may not round-trip. This introspector
|
|
50
|
+
* is the ONLY producer of `dialectType: 'datetime'` metadata, and a predicate
|
|
51
|
+
* on such a column is version-gated: comparing it against the integer
|
|
52
|
+
* microseconds Turbine binds was silently wrong below engine 0.20, so those
|
|
53
|
+
* predicates raise a typed E017 there rather than being answered wrongly (see
|
|
54
|
+
* `PowdbCapabilities.datetimeCompare`). The `in` / `not in` list forms are
|
|
55
|
+
* still wrong upstream at 0.20, so Turbine never emits one for such a column:
|
|
56
|
+
* it compiles to the equality chain the engine answers correctly, bounded by
|
|
57
|
+
* `MAX_POWQL_DATETIME_TERMS`. Reads, ordering, grouping and null checks on
|
|
58
|
+
* the column are unaffected.
|
|
50
59
|
*
|
|
51
60
|
* v1 is a PROGRAMMATIC API (exported from `turbine-orm/powdb`); the CLI's
|
|
52
61
|
* `turbine generate` still defaults to Postgres. Routing a `powdb://` URL
|
package/dist/cjs/powdb.d.ts
CHANGED
|
@@ -290,11 +290,70 @@ export interface PowdbCapabilities {
|
|
|
290
290
|
* ALL_POWDB_CAPABILITIES: it must only light up behind a real version probe.
|
|
291
291
|
*/
|
|
292
292
|
linkPaths: boolean;
|
|
293
|
+
/**
|
|
294
|
+
* ≥ 0.20: a comparison between a `datetime` column and an integer timestamp
|
|
295
|
+
* literal evaluates as microseconds. Below 0.20 that pairing was unhandled and
|
|
296
|
+
* fell back to comparing TYPE TAGS (every DateTime sorted above every Int), so
|
|
297
|
+
* `>` matched every non-null row, `=` and `<` matched none, and the answer
|
|
298
|
+
* additionally depended on whether the column carried an index. Turbine binds
|
|
299
|
+
* a JS `Date` as int micros, so that is exactly the shape it emits: every
|
|
300
|
+
* datetime predicate was silently wrong on an older engine.
|
|
301
|
+
*
|
|
302
|
+
* The `in` / `not in` LIST form is a separate, still-open engine bug that 0.20
|
|
303
|
+
* did NOT fix, so this flag does not unlock it: a datetime `in` list is
|
|
304
|
+
* COMPILED AWAY into the equality chain the engine does answer correctly (see
|
|
305
|
+
* `PowqlInterface.buildInList`). That expansion needs working binary
|
|
306
|
+
* comparisons, so it too sits behind this flag.
|
|
307
|
+
*
|
|
308
|
+
* Predominantly a refusal gate, but the `in` rewrite makes it a (bounded)
|
|
309
|
+
* generation flip as well. It stays ON in {@link ALL_POWDB_CAPABILITIES}
|
|
310
|
+
* anyway: with the flag OFF the datetime paths do not fall back to some other
|
|
311
|
+
* SQL, they refuse outright, so a hand-constructed pool defaulting to OFF
|
|
312
|
+
* would break datetime queries that work rather than protect anything.
|
|
313
|
+
*/
|
|
314
|
+
datetimeCompare: boolean;
|
|
315
|
+
/**
|
|
316
|
+
* ≥ 0.20: `count(T { .col })` counts non-null values of `.col` (SQL's
|
|
317
|
+
* `COUNT(col)`), which is what Turbine's per-field `_count` means. Below 0.20
|
|
318
|
+
* both frontends ignored the projection and returned the ROW count, so
|
|
319
|
+
* `aggregate({ _count: { field: true } })` silently disagreed with every SQL
|
|
320
|
+
* engine on a nullable column. `count(T)` / `_count: true` is unaffected on
|
|
321
|
+
* every version. Refusal-only gate (the emitted PowQL does not change).
|
|
322
|
+
*/
|
|
323
|
+
projectedCountNonNull: boolean;
|
|
293
324
|
/** Networked only: server ≥ 0.13 AND the client exposes `queryNativeRaw`. */
|
|
294
325
|
nativeRaw: boolean;
|
|
295
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* PowQL's parser bounds the SHAPE of the AST it produces, not just its own
|
|
329
|
+
* recursion: an `and` / `or` chain is parsed iteratively but re-wraps its
|
|
330
|
+
* accumulator once per term, so a flat chain counts against the same 64-level
|
|
331
|
+
* nesting budget as nested parentheses (PowDB 0.20 security fix, an unbounded
|
|
332
|
+
* chain overflowed the stack in a later recursive walk).
|
|
333
|
+
*
|
|
334
|
+
* The practical ceiling for a Turbine-generated predicate is therefore around
|
|
335
|
+
* **63 terms in one `OR` / `AND` array at the top level**, and fewer inside a
|
|
336
|
+
* nested projection block (measured: 63 flat, 61 one level of parens deep),
|
|
337
|
+
* because the budget is shared with whatever depth the predicate sits at.
|
|
338
|
+
* Turbine does NOT pre-check this client-side: the true remaining budget
|
|
339
|
+
* depends on the engine's parse depth at that point, so a client-side estimate
|
|
340
|
+
* would refuse valid queries. The engine's refusal is mapped to a typed
|
|
341
|
+
* {@link ValidationError} with a split-the-array hint instead (see
|
|
342
|
+
* {@link wrapPowdbError}).
|
|
343
|
+
*
|
|
344
|
+
* A literal `in (a, b, c, …)` list is a single flat node, NOT a chain, so it
|
|
345
|
+
* does not count against the budget (verified to 5,000 elements). Turbine's
|
|
346
|
+
* relation-key chunking (`MAX_RELATION_KEYS`) is unaffected.
|
|
347
|
+
*
|
|
348
|
+
* The one exception is a key list on a PowDB-native `datetime` column, which
|
|
349
|
+
* cannot use the list form at all (the engine still compares it by type tag) and
|
|
350
|
+
* is expanded into exactly such a chain. Those lists ARE pre-checked and chunked,
|
|
351
|
+
* against a deliberately conservative cap, see `MAX_POWQL_DATETIME_TERMS` in
|
|
352
|
+
* powql.ts.
|
|
353
|
+
*/
|
|
354
|
+
export declare const POWQL_MAX_NESTING_DEPTH = 64;
|
|
296
355
|
/** The feature-gate capability keys (everything except the version/nativeRaw metadata). */
|
|
297
|
-
type PowdbFeatureKey = 'jsonDocs' | 'docFieldIndexes' | 'introspection' | 'serverJoins' | 'nestedProjections' | 'entityLinks' | 'linkIntrospection' | 'linkPaths';
|
|
356
|
+
type PowdbFeatureKey = 'jsonDocs' | 'docFieldIndexes' | 'introspection' | 'serverJoins' | 'nestedProjections' | 'entityLinks' | 'linkIntrospection' | 'linkPaths' | 'datetimeCompare' | 'projectedCountNonNull';
|
|
298
357
|
/**
|
|
299
358
|
* Trusted-caller default: every FEATURE gate on, engine version unknown. Used
|
|
300
359
|
* for a directly-constructed {@link PowdbPool} / {@link PowdbEmbeddedPool} that
|
|
@@ -312,6 +371,14 @@ type PowdbFeatureKey = 'jsonDocs' | 'docFieldIndexes' | 'introspection' | 'serve
|
|
|
312
371
|
* projections), and `linkIntrospection` is only meaningful once genuinely
|
|
313
372
|
* probed, so both must come from a real version resolution, never a bare
|
|
314
373
|
* construction.
|
|
374
|
+
* `datetimeCompare` / `projectedCountNonNull` stay ON here for the same
|
|
375
|
+
* trusted-caller reason as `jsonDocs` and `serverJoins`. Neither is a fallback
|
|
376
|
+
* gate: with the flag OFF the affected query is REFUSED, not served by some
|
|
377
|
+
* other statement, so defaulting them off would break working queries rather
|
|
378
|
+
* than protect anything. Every path that can learn the engine version
|
|
379
|
+
* (`turbinePowDB`, embedded or networked) resolves them from a real probe; this
|
|
380
|
+
* fallback only covers a hand-constructed or injected pool, whose owner is
|
|
381
|
+
* asserting the engine is current.
|
|
315
382
|
*/
|
|
316
383
|
export declare const ALL_POWDB_CAPABILITIES: PowdbCapabilities;
|
|
317
384
|
/**
|
|
@@ -327,8 +394,18 @@ export declare function capabilitiesFromVersion(version: string | undefined | nu
|
|
|
327
394
|
* Throw a version-hinting {@link UnsupportedFeatureError} (E017) when a gated
|
|
328
395
|
* PowQL feature is used on an engine that does not support it. Keeps old engines
|
|
329
396
|
* getting clean typed errors instead of raw PowQL parse failures.
|
|
397
|
+
*
|
|
398
|
+
* The error's first sentence already names the feature (`<feature> is
|
|
399
|
+
* unsupported on "PowDB".`), so the hint says "Requires PowDB >= x" rather than
|
|
400
|
+
* repeating the label: a long feature description read twice in one message
|
|
401
|
+
* (`per-field \`_count\` … is unsupported … per-field \`_count\` … requires …`)
|
|
402
|
+
* buries the version floor that is the actionable part.
|
|
403
|
+
*
|
|
404
|
+
* `extra` appends one more sentence for gates that have a workaround worth
|
|
405
|
+
* naming (e.g. the read path that answers the same query without the gated
|
|
406
|
+
* comparison).
|
|
330
407
|
*/
|
|
331
|
-
export declare function requireCapability(caps: PowdbCapabilities, key: PowdbFeatureKey, feature: string): void;
|
|
408
|
+
export declare function requireCapability(caps: PowdbCapabilities, key: PowdbFeatureKey, feature: string, extra?: string): void;
|
|
332
409
|
/**
|
|
333
410
|
* PowQL column types Turbine emits: the four writable scalars plus PowDB's
|
|
334
411
|
* native `json` document type (added to the map in the 0.12/0.13 parity round,
|
|
@@ -357,6 +434,23 @@ export declare function isJsonColumn(col: ColumnMetadata): boolean;
|
|
|
357
434
|
* Array (non-json) and bytes columns throw, they have no PowDB equivalent.
|
|
358
435
|
*/
|
|
359
436
|
export declare function powqlColumnType(col: ColumnMetadata): PowqlType;
|
|
437
|
+
/**
|
|
438
|
+
* Is this column stored in PowDB's NATIVE `datetime` type (as opposed to the
|
|
439
|
+
* `int` epoch micros Turbine's own DDL emits for a `Date` column)?
|
|
440
|
+
*
|
|
441
|
+
* Only the literal PowQL type name counts. `powqlColumnType` never returns
|
|
442
|
+
* `datetime`, so a Turbine-provisioned table can never have one; the shapes that
|
|
443
|
+
* do are a table created outside Turbine and read back through
|
|
444
|
+
* `introspectPowdbDatabase` (which maps `datetime` → `{ tsType: 'Date',
|
|
445
|
+
* dialectType: 'datetime' }`), or hand-written metadata declaring it. Deliberately
|
|
446
|
+
* strict: a Postgres-sourced `timestamptz` column is DDL'd as PowQL `int`, so it
|
|
447
|
+
* is NOT a PowDB datetime and must not be caught here.
|
|
448
|
+
*
|
|
449
|
+
* Matters because comparing a datetime column against the integer timestamp
|
|
450
|
+
* literal Turbine binds was silently wrong below engine 0.20 (see
|
|
451
|
+
* {@link PowdbCapabilities.datetimeCompare}).
|
|
452
|
+
*/
|
|
453
|
+
export declare function isPowdbDatetimeColumn(col: ColumnMetadata): boolean;
|
|
360
454
|
/**
|
|
361
455
|
* Generate PowQL DDL (`type T { … }`) for every table in a schema. Used to
|
|
362
456
|
* provision a PowDB database from a code-first `defineSchema`/`SchemaMetadata`
|
|
@@ -480,6 +574,14 @@ export declare function coerceValue(raw: string, col: ColumnMetadata): unknown;
|
|
|
480
574
|
* str `"null"` stays the string `"null"` (fixes the legacy-wire wart on the
|
|
481
575
|
* native transport). `datetime`-shaped cells (int micros) become `Date`; a
|
|
482
576
|
* bigint on a `number` column follows the int8 safe-integer policy.
|
|
577
|
+
*
|
|
578
|
+
* A date cell can also arrive as a DIGIT STRING: a nested-projection block's
|
|
579
|
+
* children ride a JSON array, and micros exceed `Number.MAX_SAFE_INTEGER`'s
|
|
580
|
+
* decimal comfort, so the engine renders them as a JSON string. Before that
|
|
581
|
+
* string was parsed here, a nested `with` handed back the raw micros text while
|
|
582
|
+
* the batched loader and the native join both handed back a `Date` (the same
|
|
583
|
+
* relation, three answers). Only an all-digit string is parsed; any other text
|
|
584
|
+
* on a date column passes through untouched.
|
|
483
585
|
*/
|
|
484
586
|
export declare function coerceNativeValue(value: unknown, col: ColumnMetadata): unknown;
|
|
485
587
|
/**
|
|
@@ -723,11 +825,19 @@ export declare function encodePowqlLiteral(value: unknown, position?: string): s
|
|
|
723
825
|
* pre-existing tokens, so the tokenization / escape surface this ceiling guards is
|
|
724
826
|
* unmoved. The 0.19.1 link-introspection / link-path round is likewise lexer-neutral:
|
|
725
827
|
* `git diff v0.19.0 v0.19.1 -- crates/query/src/lexer.rs` is empty (the bare-dotted-path
|
|
726
|
-
* hard error is parser-level, not tokenization)
|
|
727
|
-
*
|
|
728
|
-
*
|
|
828
|
+
* hard error is parser-level, not tokenization). The guard in
|
|
829
|
+
* {@link PowdbEmbeddedPool.exec} compares major.minor only, so one entry covers every
|
|
830
|
+
* patch of a line.
|
|
831
|
+
*
|
|
832
|
+
* Verification for the 0.20 line: `git diff v0.19.1 v0.20.0 -- crates/query/src/lexer.rs
|
|
833
|
+
* crates/query/src/token.rs` is EMPTY, both files are byte-identical. Everything 0.20
|
|
834
|
+
* changed in the query crate is downstream of tokenization: the operator-chain nesting
|
|
835
|
+
* budget and the count-projection lift are in `parser.rs`, and the unknown-column /
|
|
836
|
+
* type-mismatch / negative-limit refusals are a new executor validation pass
|
|
837
|
+
* (`executor/plan_exec/validate.rs`). No new escape sequence, so the escaper's contract
|
|
838
|
+
* is unmoved and the ceiling advances to `'0.20'`.
|
|
729
839
|
*/
|
|
730
|
-
export declare const POWQL_LEXER_TESTED_CEILING = "0.
|
|
840
|
+
export declare const POWQL_LEXER_TESTED_CEILING = "0.20";
|
|
731
841
|
/**
|
|
732
842
|
* Substitute every `$N` placeholder in a generator-produced PowQL template with
|
|
733
843
|
* the encoded literal of `params[N-1]`. Safe because the template is produced by
|
package/dist/cjs/powdb.js
CHANGED
|
@@ -90,13 +90,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
90
90
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
91
91
|
};
|
|
92
92
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
93
|
-
exports.PowqlInterface = exports.introspectPowdbDatabase = exports.PowdbEmbeddedPool = exports.POWQL_LEXER_TESTED_CEILING = exports.PowdbPool = exports.DEFAULT_TX_QUEUE_TIMEOUT_MS = exports.POWQL_KEYWORDS = exports.ALL_POWDB_CAPABILITIES = exports.MIN_POWDB_VERSION = exports.PowdbJsonParam = exports.PowdbFloatParam = exports.powdbDialect = void 0;
|
|
93
|
+
exports.PowqlInterface = exports.introspectPowdbDatabase = exports.PowdbEmbeddedPool = exports.POWQL_LEXER_TESTED_CEILING = exports.PowdbPool = exports.DEFAULT_TX_QUEUE_TIMEOUT_MS = exports.POWQL_KEYWORDS = exports.ALL_POWDB_CAPABILITIES = exports.POWQL_MAX_NESTING_DEPTH = exports.MIN_POWDB_VERSION = exports.PowdbJsonParam = exports.PowdbFloatParam = exports.powdbDialect = void 0;
|
|
94
94
|
exports.parsePowdbUrl = parsePowdbUrl;
|
|
95
95
|
exports.assertSupportedPowdbVersion = assertSupportedPowdbVersion;
|
|
96
96
|
exports.capabilitiesFromVersion = capabilitiesFromVersion;
|
|
97
97
|
exports.requireCapability = requireCapability;
|
|
98
98
|
exports.isJsonColumn = isJsonColumn;
|
|
99
99
|
exports.powqlColumnType = powqlColumnType;
|
|
100
|
+
exports.isPowdbDatetimeColumn = isPowdbDatetimeColumn;
|
|
100
101
|
exports.quotePowqlIdent = quotePowqlIdent;
|
|
101
102
|
exports.deriveDesiredLinks = deriveDesiredLinks;
|
|
102
103
|
exports.powdbLinkStatement = powdbLinkStatement;
|
|
@@ -258,6 +259,34 @@ function assertSupportedPowdbVersion(version) {
|
|
|
258
259
|
throw new errors_js_1.ConnectionError(`[turbine] turbine-orm/powdb requires PowDB >= ${exports.MIN_POWDB_VERSION}; the server reports "${version}". ` +
|
|
259
260
|
'Upgrade the PowDB server (0.7.0 added the `returning` keyword and the int->float coercion fix Turbine relies on).');
|
|
260
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* PowQL's parser bounds the SHAPE of the AST it produces, not just its own
|
|
264
|
+
* recursion: an `and` / `or` chain is parsed iteratively but re-wraps its
|
|
265
|
+
* accumulator once per term, so a flat chain counts against the same 64-level
|
|
266
|
+
* nesting budget as nested parentheses (PowDB 0.20 security fix, an unbounded
|
|
267
|
+
* chain overflowed the stack in a later recursive walk).
|
|
268
|
+
*
|
|
269
|
+
* The practical ceiling for a Turbine-generated predicate is therefore around
|
|
270
|
+
* **63 terms in one `OR` / `AND` array at the top level**, and fewer inside a
|
|
271
|
+
* nested projection block (measured: 63 flat, 61 one level of parens deep),
|
|
272
|
+
* because the budget is shared with whatever depth the predicate sits at.
|
|
273
|
+
* Turbine does NOT pre-check this client-side: the true remaining budget
|
|
274
|
+
* depends on the engine's parse depth at that point, so a client-side estimate
|
|
275
|
+
* would refuse valid queries. The engine's refusal is mapped to a typed
|
|
276
|
+
* {@link ValidationError} with a split-the-array hint instead (see
|
|
277
|
+
* {@link wrapPowdbError}).
|
|
278
|
+
*
|
|
279
|
+
* A literal `in (a, b, c, …)` list is a single flat node, NOT a chain, so it
|
|
280
|
+
* does not count against the budget (verified to 5,000 elements). Turbine's
|
|
281
|
+
* relation-key chunking (`MAX_RELATION_KEYS`) is unaffected.
|
|
282
|
+
*
|
|
283
|
+
* The one exception is a key list on a PowDB-native `datetime` column, which
|
|
284
|
+
* cannot use the list form at all (the engine still compares it by type tag) and
|
|
285
|
+
* is expanded into exactly such a chain. Those lists ARE pre-checked and chunked,
|
|
286
|
+
* against a deliberately conservative cap, see `MAX_POWQL_DATETIME_TERMS` in
|
|
287
|
+
* powql.ts.
|
|
288
|
+
*/
|
|
289
|
+
exports.POWQL_MAX_NESTING_DEPTH = 64;
|
|
261
290
|
/**
|
|
262
291
|
* Minimum engine version each gated feature needs, for the E017 hint text.
|
|
263
292
|
* Most gates carry a `major.minor` floor (patch-insensitive); the two link
|
|
@@ -275,6 +304,8 @@ const POWDB_FEATURE_MIN_VERSION = {
|
|
|
275
304
|
entityLinks: '0.19',
|
|
276
305
|
linkIntrospection: '0.19.1',
|
|
277
306
|
linkPaths: '0.19.1',
|
|
307
|
+
datetimeCompare: '0.20',
|
|
308
|
+
projectedCountNonNull: '0.20',
|
|
278
309
|
};
|
|
279
310
|
/**
|
|
280
311
|
* Trusted-caller default: every FEATURE gate on, engine version unknown. Used
|
|
@@ -293,6 +324,14 @@ const POWDB_FEATURE_MIN_VERSION = {
|
|
|
293
324
|
* projections), and `linkIntrospection` is only meaningful once genuinely
|
|
294
325
|
* probed, so both must come from a real version resolution, never a bare
|
|
295
326
|
* construction.
|
|
327
|
+
* `datetimeCompare` / `projectedCountNonNull` stay ON here for the same
|
|
328
|
+
* trusted-caller reason as `jsonDocs` and `serverJoins`. Neither is a fallback
|
|
329
|
+
* gate: with the flag OFF the affected query is REFUSED, not served by some
|
|
330
|
+
* other statement, so defaulting them off would break working queries rather
|
|
331
|
+
* than protect anything. Every path that can learn the engine version
|
|
332
|
+
* (`turbinePowDB`, embedded or networked) resolves them from a real probe; this
|
|
333
|
+
* fallback only covers a hand-constructed or injected pool, whose owner is
|
|
334
|
+
* asserting the engine is current.
|
|
296
335
|
*/
|
|
297
336
|
exports.ALL_POWDB_CAPABILITIES = {
|
|
298
337
|
engineVersion: null,
|
|
@@ -304,6 +343,8 @@ exports.ALL_POWDB_CAPABILITIES = {
|
|
|
304
343
|
entityLinks: false,
|
|
305
344
|
linkIntrospection: false,
|
|
306
345
|
linkPaths: false,
|
|
346
|
+
datetimeCompare: true,
|
|
347
|
+
projectedCountNonNull: true,
|
|
307
348
|
nativeRaw: false,
|
|
308
349
|
};
|
|
309
350
|
/** Parse a PowDB semver prefix (`0.13.0`, `0.13`, `1.2.3-rc`) into components, or `null`. */
|
|
@@ -347,6 +388,8 @@ function capabilitiesFromVersion(version, opts = {}) {
|
|
|
347
388
|
entityLinks: false,
|
|
348
389
|
linkIntrospection: false,
|
|
349
390
|
linkPaths: false,
|
|
391
|
+
datetimeCompare: false,
|
|
392
|
+
projectedCountNonNull: false,
|
|
350
393
|
nativeRaw: false,
|
|
351
394
|
};
|
|
352
395
|
}
|
|
@@ -363,6 +406,8 @@ function capabilitiesFromVersion(version, opts = {}) {
|
|
|
363
406
|
// never 0.19.0.
|
|
364
407
|
linkIntrospection: atLeastVersion(sem, 0, 19, 1),
|
|
365
408
|
linkPaths: atLeastVersion(sem, 0, 19, 1),
|
|
409
|
+
datetimeCompare: atLeastVersion(sem, 0, 20),
|
|
410
|
+
projectedCountNonNull: atLeastVersion(sem, 0, 20),
|
|
366
411
|
nativeRaw: Boolean(opts.hasNativeRaw) && atLeastVersion(sem, 0, 13),
|
|
367
412
|
};
|
|
368
413
|
}
|
|
@@ -370,16 +415,26 @@ function capabilitiesFromVersion(version, opts = {}) {
|
|
|
370
415
|
* Throw a version-hinting {@link UnsupportedFeatureError} (E017) when a gated
|
|
371
416
|
* PowQL feature is used on an engine that does not support it. Keeps old engines
|
|
372
417
|
* getting clean typed errors instead of raw PowQL parse failures.
|
|
418
|
+
*
|
|
419
|
+
* The error's first sentence already names the feature (`<feature> is
|
|
420
|
+
* unsupported on "PowDB".`), so the hint says "Requires PowDB >= x" rather than
|
|
421
|
+
* repeating the label: a long feature description read twice in one message
|
|
422
|
+
* (`per-field \`_count\` … is unsupported … per-field \`_count\` … requires …`)
|
|
423
|
+
* buries the version floor that is the actionable part.
|
|
424
|
+
*
|
|
425
|
+
* `extra` appends one more sentence for gates that have a workaround worth
|
|
426
|
+
* naming (e.g. the read path that answers the same query without the gated
|
|
427
|
+
* comparison).
|
|
373
428
|
*/
|
|
374
|
-
function requireCapability(caps, key, feature) {
|
|
429
|
+
function requireCapability(caps, key, feature, extra) {
|
|
375
430
|
if (caps[key])
|
|
376
431
|
return;
|
|
377
432
|
const min = POWDB_FEATURE_MIN_VERSION[key];
|
|
378
433
|
const reported = caps.engineVersion
|
|
379
434
|
? `this connection reports ${caps.engineVersion}`
|
|
380
435
|
: 'this connection could not report a version';
|
|
381
|
-
throw new errors_js_1.UnsupportedFeatureError(feature, 'PowDB',
|
|
382
|
-
|
|
436
|
+
throw new errors_js_1.UnsupportedFeatureError(feature, 'PowDB', `Requires PowDB >= ${min}; ${reported}. Upgrade powdb-server / @zvndev/powdb-embedded ` +
|
|
437
|
+
`(or pass \`assumeEngineVersion\` if the version cannot be detected).${extra ? ` ${extra}` : ''}`);
|
|
383
438
|
}
|
|
384
439
|
/**
|
|
385
440
|
* Does this column map to PowDB's native `json` document type? A Postgres
|
|
@@ -443,6 +498,25 @@ function isFloatColumn(col) {
|
|
|
443
498
|
function isDateColumn(col) {
|
|
444
499
|
return col.tsType.replace(/\s*\|\s*null$/i, '').trim() === 'Date';
|
|
445
500
|
}
|
|
501
|
+
/**
|
|
502
|
+
* Is this column stored in PowDB's NATIVE `datetime` type (as opposed to the
|
|
503
|
+
* `int` epoch micros Turbine's own DDL emits for a `Date` column)?
|
|
504
|
+
*
|
|
505
|
+
* Only the literal PowQL type name counts. `powqlColumnType` never returns
|
|
506
|
+
* `datetime`, so a Turbine-provisioned table can never have one; the shapes that
|
|
507
|
+
* do are a table created outside Turbine and read back through
|
|
508
|
+
* `introspectPowdbDatabase` (which maps `datetime` → `{ tsType: 'Date',
|
|
509
|
+
* dialectType: 'datetime' }`), or hand-written metadata declaring it. Deliberately
|
|
510
|
+
* strict: a Postgres-sourced `timestamptz` column is DDL'd as PowQL `int`, so it
|
|
511
|
+
* is NOT a PowDB datetime and must not be caught here.
|
|
512
|
+
*
|
|
513
|
+
* Matters because comparing a datetime column against the integer timestamp
|
|
514
|
+
* literal Turbine binds was silently wrong below engine 0.20 (see
|
|
515
|
+
* {@link PowdbCapabilities.datetimeCompare}).
|
|
516
|
+
*/
|
|
517
|
+
function isPowdbDatetimeColumn(col) {
|
|
518
|
+
return (col.dialectType ?? col.pgType ?? '').toLowerCase() === 'datetime';
|
|
519
|
+
}
|
|
446
520
|
/**
|
|
447
521
|
* Generate PowQL DDL (`type T { … }`) for every table in a schema. Used to
|
|
448
522
|
* provision a PowDB database from a code-first `defineSchema`/`SchemaMetadata`
|
|
@@ -736,7 +810,7 @@ async function applyPowdbLinks(exec, schema, options = {}) {
|
|
|
736
810
|
requireCapability(caps, 'linkIntrospection', 'PowDB `schema links` introspection');
|
|
737
811
|
}
|
|
738
812
|
const existing = await listPowdbLinks(exec);
|
|
739
|
-
const byOwnerName = new Map(existing.map((l) => [`${l.owner}
|
|
813
|
+
const byOwnerName = new Map(existing.map((l) => [`${l.owner}\u0000${l.name}`, l]));
|
|
740
814
|
const desired = deriveDesiredLinks(schema, (owner, name) => {
|
|
741
815
|
if ((0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.powdbLinks, `collide:${owner}.${name}`)) {
|
|
742
816
|
console.warn(`[turbine] applyPowdbLinks: relation "${name}" on "${owner}" collides with a column of the same name; ` +
|
|
@@ -745,7 +819,7 @@ async function applyPowdbLinks(exec, schema, options = {}) {
|
|
|
745
819
|
});
|
|
746
820
|
const executed = [];
|
|
747
821
|
for (const link of desired) {
|
|
748
|
-
const found = byOwnerName.get(`${link.owner}
|
|
822
|
+
const found = byOwnerName.get(`${link.owner}\u0000${link.name}`);
|
|
749
823
|
if (found) {
|
|
750
824
|
const same = found.target === link.target && found.localKey === link.localKey && found.targetKey === link.targetKey;
|
|
751
825
|
if (!same && (0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.powdbLinks, `drift:${link.owner}.${link.name}`)) {
|
|
@@ -883,6 +957,14 @@ function coerceValue(raw, col) {
|
|
|
883
957
|
* str `"null"` stays the string `"null"` (fixes the legacy-wire wart on the
|
|
884
958
|
* native transport). `datetime`-shaped cells (int micros) become `Date`; a
|
|
885
959
|
* bigint on a `number` column follows the int8 safe-integer policy.
|
|
960
|
+
*
|
|
961
|
+
* A date cell can also arrive as a DIGIT STRING: a nested-projection block's
|
|
962
|
+
* children ride a JSON array, and micros exceed `Number.MAX_SAFE_INTEGER`'s
|
|
963
|
+
* decimal comfort, so the engine renders them as a JSON string. Before that
|
|
964
|
+
* string was parsed here, a nested `with` handed back the raw micros text while
|
|
965
|
+
* the batched loader and the native join both handed back a `Date` (the same
|
|
966
|
+
* relation, three answers). Only an all-digit string is parsed; any other text
|
|
967
|
+
* on a date column passes through untouched.
|
|
886
968
|
*/
|
|
887
969
|
function coerceNativeValue(value, col) {
|
|
888
970
|
if (value === undefined || value === null)
|
|
@@ -892,6 +974,8 @@ function coerceNativeValue(value, col) {
|
|
|
892
974
|
return new Date(Number(value) / 1000);
|
|
893
975
|
if (typeof value === 'number')
|
|
894
976
|
return new Date(value / 1000);
|
|
977
|
+
if (typeof value === 'string' && /^-?\d+$/.test(value))
|
|
978
|
+
return new Date(Number(value) / 1000);
|
|
895
979
|
return value;
|
|
896
980
|
}
|
|
897
981
|
const ts = col.tsType.replace(/\s*\|\s*null$/i, '').trim();
|
|
@@ -1063,6 +1147,73 @@ function wrapPowdbError(err) {
|
|
|
1063
1147
|
if (/is ambiguous in a projection|aggregates over a nested or link projection/i.test(msg)) {
|
|
1064
1148
|
return new errors_js_1.ValidationError(`[turbine] PowDB query rejected: ${msg}`);
|
|
1065
1149
|
}
|
|
1150
|
+
// Corrupt storage → ConnectionError (E004). PowDB 0.20 verifies page checksums
|
|
1151
|
+
// at table-OPEN time and fails closed (previously the open scan skipped the bad
|
|
1152
|
+
// page and the failure surfaced later, on the read that touched it). This is a
|
|
1153
|
+
// data-integrity / availability failure, not a query defect, so it joins the
|
|
1154
|
+
// connection families ABOVE the generic validation regexes (whose `StorageError`
|
|
1155
|
+
// token would otherwise class it E003). There is no salvage mode: restoring
|
|
1156
|
+
// from a backup is the documented recovery, so say so.
|
|
1157
|
+
if (/page corrupt|catalog corrupt|corrupt heap superblock|CRC32 mismatch/i.test(msg)) {
|
|
1158
|
+
return new errors_js_1.ConnectionError(`[turbine] PowDB refused to open a corrupt data directory: ${msg}. PowDB verifies page checksums on open ` +
|
|
1159
|
+
'and fails closed rather than serving partial data; there is no skip-corrupt-pages mode, so recover by ' +
|
|
1160
|
+
'restoring the directory from a backup.', { cause: err });
|
|
1161
|
+
}
|
|
1162
|
+
// Unknown column → ValidationError (E003), naming the column. PowDB 0.20 turned
|
|
1163
|
+
// an unknown column in `filter` / a projection from a silent NULL into an error
|
|
1164
|
+
// (the old behavior made `count(T filter .agee = null)` match every row, so a
|
|
1165
|
+
// delete on that predicate emptied the table). Turbine validates field names
|
|
1166
|
+
// against its own metadata first, so reaching here means the runtime metadata
|
|
1167
|
+
// has drifted from the live catalog.
|
|
1168
|
+
{
|
|
1169
|
+
const m = /column '([^']+)' not found(?: in table '([^']+)')?/i.exec(msg);
|
|
1170
|
+
if (m) {
|
|
1171
|
+
const where = m[2] ? ` on table "${m[2]}"` : '';
|
|
1172
|
+
return new errors_js_1.ValidationError(`[turbine] PowDB rejected column "${m[1]}"${where}: it does not exist in the live catalog. ` +
|
|
1173
|
+
'The schema metadata Turbine is using has drifted from the database; re-derive it ' +
|
|
1174
|
+
'(`schemaDefToMetadata` / `introspectPowdbDatabase`) or apply the missing DDL. ' +
|
|
1175
|
+
`(engine: ${msg})`);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
// Type-mismatched comparison → ValidationError (E003), naming the column. Also
|
|
1179
|
+
// new in 0.20: comparing e.g. a `str` column against an integer literal used to
|
|
1180
|
+
// evaluate true for every row. Runs before the generic `type mismatch` regex
|
|
1181
|
+
// below so the column name and the fix survive into the message.
|
|
1182
|
+
{
|
|
1183
|
+
const m = /type mismatch for column '([^']+)': expected ([^,]+), got (\w+)/i.exec(msg);
|
|
1184
|
+
if (m) {
|
|
1185
|
+
return new errors_js_1.ValidationError(`[turbine] PowDB rejected a comparison on column "${m[1]}": the column is ${m[2]} but the bound value is ` +
|
|
1186
|
+
`${m[3]}. PowQL never coerces across types in a comparison (before engine 0.20 this silently matched ` +
|
|
1187
|
+
"every row), so bind a value of the column's own type.");
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
// Operator-chain / nesting budget → ValidationError (E003) with the actual fix.
|
|
1191
|
+
// PowDB 0.20 counts flat `and` / `or` chains against the same 64-level budget as
|
|
1192
|
+
// nested parentheses, so a machine-built predicate with very many terms is now
|
|
1193
|
+
// rejected outright (see POWQL_MAX_NESTING_DEPTH).
|
|
1194
|
+
if (/nesting depth exceeds maximum/i.test(msg)) {
|
|
1195
|
+
return new errors_js_1.ValidationError(`[turbine] PowDB rejected the query: ${msg}. PowQL bounds the shape of the predicate tree, and a flat ` +
|
|
1196
|
+
`\`OR\` / \`AND\` array counts one level per term (roughly ${exports.POWQL_MAX_NESTING_DEPTH - 1} terms at the top ` +
|
|
1197
|
+
'level, fewer inside a nested `with` block). Split a large `OR` / `AND` array into several queries and ' +
|
|
1198
|
+
'merge the results, or express it as a single `in` list, which is one flat node and does not count ' +
|
|
1199
|
+
'against the budget.');
|
|
1200
|
+
}
|
|
1201
|
+
// Negative limit / offset → ValidationError (E003). Turbine validates these
|
|
1202
|
+
// client-side before emitting, so this is the backstop for a raw PowQL string.
|
|
1203
|
+
if (/(limit|offset) must not be negative/i.test(msg)) {
|
|
1204
|
+
return new errors_js_1.ValidationError(`[turbine] PowDB rejected the query: ${msg}. Pass a non-negative \`limit\` / \`offset\` ` +
|
|
1205
|
+
'(before engine 0.20 a negative limit was ignored and returned every row).');
|
|
1206
|
+
}
|
|
1207
|
+
// Client-side result-frame cell cap (`@zvndev/powdb-client` >= 0.20 rejects a
|
|
1208
|
+
// declared result shape over 2,000,000 cells before decoding it, so a hostile or
|
|
1209
|
+
// MITM'd server cannot force a multi-gigabyte allocation). It is a plain driver
|
|
1210
|
+
// Error with no code, so match the message and give the caller the two real
|
|
1211
|
+
// remedies instead of letting it fall through untyped.
|
|
1212
|
+
if (/result too large: \d+ cells/i.test(msg)) {
|
|
1213
|
+
return new errors_js_1.ValidationError(`[turbine] PowDB result too large to decode: ${msg}. The client caps one result frame at 2,000,000 cells ` +
|
|
1214
|
+
'(rows x columns). Page the query with `limit` / `offset`, or narrow the row with `select` so each row ' +
|
|
1215
|
+
'carries fewer columns.');
|
|
1216
|
+
}
|
|
1066
1217
|
// Typed wire error class (networked, server >= 0.17): the client surfaces the
|
|
1067
1218
|
// stable one-byte class from the error frame as `.wireErrorClass`. Classify by
|
|
1068
1219
|
// it BEFORE the generic message regexes: the server sanitizes non-allowlisted
|
|
@@ -1774,11 +1925,19 @@ function powqlNumberText(n) {
|
|
|
1774
1925
|
* pre-existing tokens, so the tokenization / escape surface this ceiling guards is
|
|
1775
1926
|
* unmoved. The 0.19.1 link-introspection / link-path round is likewise lexer-neutral:
|
|
1776
1927
|
* `git diff v0.19.0 v0.19.1 -- crates/query/src/lexer.rs` is empty (the bare-dotted-path
|
|
1777
|
-
* hard error is parser-level, not tokenization)
|
|
1778
|
-
*
|
|
1779
|
-
*
|
|
1928
|
+
* hard error is parser-level, not tokenization). The guard in
|
|
1929
|
+
* {@link PowdbEmbeddedPool.exec} compares major.minor only, so one entry covers every
|
|
1930
|
+
* patch of a line.
|
|
1931
|
+
*
|
|
1932
|
+
* Verification for the 0.20 line: `git diff v0.19.1 v0.20.0 -- crates/query/src/lexer.rs
|
|
1933
|
+
* crates/query/src/token.rs` is EMPTY, both files are byte-identical. Everything 0.20
|
|
1934
|
+
* changed in the query crate is downstream of tokenization: the operator-chain nesting
|
|
1935
|
+
* budget and the count-projection lift are in `parser.rs`, and the unknown-column /
|
|
1936
|
+
* type-mismatch / negative-limit refusals are a new executor validation pass
|
|
1937
|
+
* (`executor/plan_exec/validate.rs`). No new escape sequence, so the escaper's contract
|
|
1938
|
+
* is unmoved and the ceiling advances to `'0.20'`.
|
|
1780
1939
|
*/
|
|
1781
|
-
exports.POWQL_LEXER_TESTED_CEILING = '0.
|
|
1940
|
+
exports.POWQL_LEXER_TESTED_CEILING = '0.20';
|
|
1782
1941
|
/**
|
|
1783
1942
|
* Escape a string into a PowQL `"…"` literal, matching the engine lexer's
|
|
1784
1943
|
* escape rules.
|