turbine-orm 0.71.0 → 0.72.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 +2 -2
- package/dist/cjs/client.d.ts +0 -18
- package/dist/cjs/client.js +2 -29
- package/dist/cjs/connection-url.d.ts +30 -0
- package/dist/cjs/connection-url.js +15 -17
- package/dist/cjs/powql.d.ts +38 -1
- package/dist/cjs/powql.js +106 -18
- package/dist/cjs/query/aggregates.d.ts +0 -13
- package/dist/cjs/query/aggregates.js +81 -33
- package/dist/cjs/query/batched-loader.d.ts +13 -1
- package/dist/cjs/query/batched-loader.js +46 -11
- package/dist/cjs/query/builder.d.ts +13 -0
- package/dist/cjs/query/builder.js +104 -14
- package/dist/cjs/query/compound-unique.js +29 -5
- package/dist/cjs/query/relation-names.d.ts +52 -0
- package/dist/cjs/query/relation-names.js +120 -0
- package/dist/cjs/query/relations.d.ts +11 -6
- package/dist/cjs/query/relations.js +45 -27
- package/dist/cjs/query/utils.d.ts +107 -3
- package/dist/cjs/query/utils.js +408 -7
- package/dist/cjs/query/where-compile.js +9 -4
- package/dist/cjs/query/where.js +9 -5
- package/dist/client.d.ts +0 -18
- package/dist/client.js +2 -29
- package/dist/connection-url.d.ts +30 -0
- package/dist/connection-url.js +15 -18
- package/dist/powql.d.ts +38 -1
- package/dist/powql.js +107 -19
- package/dist/query/aggregates.d.ts +0 -13
- package/dist/query/aggregates.js +82 -34
- package/dist/query/batched-loader.d.ts +13 -1
- package/dist/query/batched-loader.js +47 -12
- package/dist/query/builder.d.ts +13 -0
- package/dist/query/builder.js +105 -15
- package/dist/query/compound-unique.js +30 -6
- package/dist/query/relation-names.d.ts +52 -0
- package/dist/query/relation-names.js +117 -0
- package/dist/query/relations.d.ts +11 -6
- package/dist/query/relations.js +47 -29
- package/dist/query/utils.d.ts +107 -3
- package/dist/query/utils.js +404 -8
- package/dist/query/where-compile.js +10 -5
- package/dist/query/where.js +10 -6
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -43,8 +43,8 @@ Measured **2026-08-15 against turbine-orm 0.71.0**, versus **Prisma 7.9.1** (`@p
|
|
|
43
43
|
What that run says:
|
|
44
44
|
|
|
45
45
|
- **Turbine ran at 1.08x hand-written `pg`**, where Drizzle ran at 1.47x and Prisma at 1.81x (geometric mean over the eight scenarios with a raw control). That 1.08x is the conservative reading: the raw L2 control statement still uses the `json_build_object` encoding Turbine has moved off, which is worth 1.83x on its own, so the figure the harness records unadjusted is 1.00x. The adjusted one is published instead.
|
|
46
|
-
- Across all ten scenarios Turbine was **2.02x faster than Prisma 7.9** and **1.46x faster than Drizzle 0.45** by geometric mean. Turbine
|
|
47
|
-
- **Drizzle
|
|
46
|
+
- Across all ten scenarios Turbine was **2.02x faster than Prisma 7.9** and **1.46x faster than Drizzle 0.45** by geometric mean. Turbine took nine scenarios, Drizzle one, Prisma none. The one Drizzle took was streaming, and 0.72.0 takes it back (below).
|
|
47
|
+
- **Streaming was Drizzle's one win, by 22%** (39.49 ms vs 50.39 ms) on 0.71.0. **0.72.0 closes it**: the remaining gap was temporal decoding rather than the cursor, and a direct scan for the canonical ISO wire shape (declining anything else back to `pg`'s own parser) takes `findManyStream` from 52.44 ms to **37.06 ms** against Drizzle's 42.09 ms re-measured in the same rotation, and `findManyStreamBatches` to **31.65 ms**. Scope, because it matters: that is a targeted re-measurement of one scenario on a paired one-process harness, not a re-run of the ten above, so the table's stream row still shows 0.71.0 and the absolute values are not comparable. Method and controls in [`benchmarks/RESULTS-0.72.0.md`](benchmarks/RESULTS-0.72.0.md).
|
|
48
48
|
- **Pipelining is Turbine's clearest win**: one TCP flush for 5 queries runs 2.04x faster than Prisma and 2.07x faster than Drizzle, level with raw `pg`.
|
|
49
49
|
- Three scenarios (L2, L3, atomic increment) are marked *contested*: the contiguous cross-check harness disagrees with itself across runs of the same configuration there, so neither side claims them. Both nested reads were losses in the previous run and the improvement behind them reproduces on both harnesses; whether it is enough to pass Drizzle is what is unsettled.
|
|
50
50
|
- The sub-0.15 ms scenarios carry roughly one third uncertainty in their absolute values. Orderings held across runs; margins should not be quoted, and that includes L2's 2.5%.
|
package/dist/cjs/client.d.ts
CHANGED
|
@@ -800,24 +800,6 @@ export declare class TurbineClient {
|
|
|
800
800
|
* (`ALTER ROLE ... SET plan_cache_mode = ...`).
|
|
801
801
|
*/
|
|
802
802
|
private static withPlanCacheMode;
|
|
803
|
-
/**
|
|
804
|
-
* `connectionString` with `setting` appended to its existing `options` query
|
|
805
|
-
* parameter, or `null` when it carries no `options` (in which case the caller
|
|
806
|
-
* should use the `options` pool field, which is not overridden).
|
|
807
|
-
*
|
|
808
|
-
* Only the query string is rewritten, never the userinfo or host, so a
|
|
809
|
-
* percent-encoded password cannot be mangled by a round trip through `URL`.
|
|
810
|
-
* The split is on the first `?`, which is also where pg's own parser puts the
|
|
811
|
-
* query-string boundary: a connection string with an unencoded `?` inside the
|
|
812
|
-
* password is not parseable by pg either, so there is no shape this handles
|
|
813
|
-
* differently from the driver.
|
|
814
|
-
*
|
|
815
|
-
* A twin of this lives in `src/connection-url.ts`, which `turbine doctor`
|
|
816
|
-
* uses for `statement_timeout`. Unifying them is the obvious refactor and it
|
|
817
|
-
* is deliberately NOT done; the reason (a c8 merge artifact that costs almost
|
|
818
|
-
* all of the coverage gate's headroom) is written up over there.
|
|
819
|
-
*/
|
|
820
|
-
private static mergeConnectionStringOptions;
|
|
821
803
|
/**
|
|
822
804
|
* Refuse a `utcTimestamps` value that contradicts the one an earlier client
|
|
823
805
|
* in this process settled the zone-less temporal read parsers (OIDs 1114,
|
package/dist/cjs/client.js
CHANGED
|
@@ -29,6 +29,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
29
29
|
exports.TurbineClient = exports.TransactionClient = exports.READ_OPERATIONS = void 0;
|
|
30
30
|
exports.withRetry = withRetry;
|
|
31
31
|
const pg_1 = __importDefault(require("pg"));
|
|
32
|
+
const connection_url_js_1 = require("./connection-url.js");
|
|
32
33
|
const dialect_js_1 = require("./dialect.js");
|
|
33
34
|
const errors_js_1 = require("./errors.js");
|
|
34
35
|
const observe_js_1 = require("./observe.js");
|
|
@@ -1146,7 +1147,7 @@ class TurbineClient {
|
|
|
1146
1147
|
// cannot be a bind parameter.
|
|
1147
1148
|
const setting = `-c plan_cache_mode=${mode}`;
|
|
1148
1149
|
const merged = poolConfig.connectionString
|
|
1149
|
-
?
|
|
1150
|
+
? (0, connection_url_js_1.mergeConnectionStringOptions)(poolConfig.connectionString, setting)
|
|
1150
1151
|
: null;
|
|
1151
1152
|
if (merged)
|
|
1152
1153
|
return { ...poolConfig, connectionString: merged };
|
|
@@ -1156,34 +1157,6 @@ class TurbineClient {
|
|
|
1156
1157
|
const existing = poolConfig.options || (typeof process !== 'undefined' ? process.env?.PGOPTIONS : undefined);
|
|
1157
1158
|
return { ...poolConfig, options: existing ? `${existing} ${setting}` : setting };
|
|
1158
1159
|
}
|
|
1159
|
-
/**
|
|
1160
|
-
* `connectionString` with `setting` appended to its existing `options` query
|
|
1161
|
-
* parameter, or `null` when it carries no `options` (in which case the caller
|
|
1162
|
-
* should use the `options` pool field, which is not overridden).
|
|
1163
|
-
*
|
|
1164
|
-
* Only the query string is rewritten, never the userinfo or host, so a
|
|
1165
|
-
* percent-encoded password cannot be mangled by a round trip through `URL`.
|
|
1166
|
-
* The split is on the first `?`, which is also where pg's own parser puts the
|
|
1167
|
-
* query-string boundary: a connection string with an unencoded `?` inside the
|
|
1168
|
-
* password is not parseable by pg either, so there is no shape this handles
|
|
1169
|
-
* differently from the driver.
|
|
1170
|
-
*
|
|
1171
|
-
* A twin of this lives in `src/connection-url.ts`, which `turbine doctor`
|
|
1172
|
-
* uses for `statement_timeout`. Unifying them is the obvious refactor and it
|
|
1173
|
-
* is deliberately NOT done; the reason (a c8 merge artifact that costs almost
|
|
1174
|
-
* all of the coverage gate's headroom) is written up over there.
|
|
1175
|
-
*/
|
|
1176
|
-
static mergeConnectionStringOptions(connectionString, setting) {
|
|
1177
|
-
const q = connectionString.indexOf('?');
|
|
1178
|
-
if (q === -1)
|
|
1179
|
-
return null;
|
|
1180
|
-
const params = new URLSearchParams(connectionString.slice(q + 1));
|
|
1181
|
-
const existing = params.get('options');
|
|
1182
|
-
if (existing === null)
|
|
1183
|
-
return null;
|
|
1184
|
-
params.set('options', `${existing} ${setting}`);
|
|
1185
|
-
return connectionString.slice(0, q + 1) + params.toString();
|
|
1186
|
-
}
|
|
1187
1160
|
/**
|
|
1188
1161
|
* Refuse a `utcTimestamps` value that contradicts the one an earlier client
|
|
1189
1162
|
* in this process settled the zone-less temporal read parsers (OIDs 1114,
|
|
@@ -158,3 +158,33 @@ export interface ConnectionOptionsConfig {
|
|
|
158
158
|
* a value that can carry a space can carry a second `-c`.
|
|
159
159
|
*/
|
|
160
160
|
export declare function withStatementTimeoutOption(config: ConnectionOptionsConfig, statementTimeoutMs: number): ConnectionOptionsConfig;
|
|
161
|
+
/**
|
|
162
|
+
* `connectionString` with `setting` appended to an existing `options` query
|
|
163
|
+
* parameter, or `null` when it carries none (in which case the caller uses the
|
|
164
|
+
* `options` config field, which the connection string does not override).
|
|
165
|
+
*
|
|
166
|
+
* Only the query string is rewritten, never the userinfo or host, so a
|
|
167
|
+
* percent-encoded password cannot be mangled by a round trip through `URL`. The
|
|
168
|
+
* split is on the first `?`, which is also where pg's own parser puts the
|
|
169
|
+
* query-string boundary: a connection string with an unencoded `?` inside the
|
|
170
|
+
* password is not parseable by pg either, so there is no shape this handles
|
|
171
|
+
* differently from the driver.
|
|
172
|
+
*
|
|
173
|
+
* Shared with `client.ts`, which needs the identical merge for
|
|
174
|
+
* `plan_cache_mode`. It was duplicated there until 2026-08-16, and the reason
|
|
175
|
+
* is worth keeping because it was wrong in an instructive way: importing it
|
|
176
|
+
* from `client.ts` adds an import edge from a module ~100 test processes load,
|
|
177
|
+
* and this file's merged coverage read 40% while measuring 100% in isolation,
|
|
178
|
+
* which moved the aggregate gate 75.48% -> 75.03% against a 75% floor. The
|
|
179
|
+
* conclusion drawn was that the refactor cost the gate's headroom.
|
|
180
|
+
*
|
|
181
|
+
* The 40% was a c8 merge defect, not an import cost: c8's V8-level merge is not
|
|
182
|
+
* monotonic, and once the report merges at the istanbul level instead this file
|
|
183
|
+
* reads 100% with the edge in place (see `//merge-bug` in .c8rc.json). So the
|
|
184
|
+
* duplication was paying for an artifact, and the two copies are now one.
|
|
185
|
+
*
|
|
186
|
+
* Keep this module import-free regardless. That part was never about coverage:
|
|
187
|
+
* `query/` and `cli/` both reach it, and an edge back to `client.ts` would be
|
|
188
|
+
* the cycle `scripts/check-import-cycles.mjs` exists to refuse.
|
|
189
|
+
*/
|
|
190
|
+
export declare function mergeConnectionStringOptions(connectionString: string, setting: string): string | null;
|
|
@@ -39,6 +39,7 @@ exports.parseConnectionTarget = parseConnectionTarget;
|
|
|
39
39
|
exports.detectPooler = detectPooler;
|
|
40
40
|
exports.poolerRefusalMessage = poolerRefusalMessage;
|
|
41
41
|
exports.withStatementTimeoutOption = withStatementTimeoutOption;
|
|
42
|
+
exports.mergeConnectionStringOptions = mergeConnectionStringOptions;
|
|
42
43
|
/**
|
|
43
44
|
* Host and port from a connection string, in either shape libpq accepts.
|
|
44
45
|
*
|
|
@@ -263,25 +264,22 @@ function withStatementTimeoutOption(config, statementTimeoutMs) {
|
|
|
263
264
|
* password is not parseable by pg either, so there is no shape this handles
|
|
264
265
|
* differently from the driver.
|
|
265
266
|
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
267
|
+
* Shared with `client.ts`, which needs the identical merge for
|
|
268
|
+
* `plan_cache_mode`. It was duplicated there until 2026-08-16, and the reason
|
|
269
|
+
* is worth keeping because it was wrong in an instructive way: importing it
|
|
270
|
+
* from `client.ts` adds an import edge from a module ~100 test processes load,
|
|
271
|
+
* and this file's merged coverage read 40% while measuring 100% in isolation,
|
|
272
|
+
* which moved the aggregate gate 75.48% -> 75.03% against a 75% floor. The
|
|
273
|
+
* conclusion drawn was that the refactor cost the gate's headroom.
|
|
271
274
|
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
* reports 40% in the merged report while measuring 100% in isolation. Real
|
|
277
|
-
* coverage is unchanged either way, but the aggregate gate moved 75.48% ->
|
|
278
|
-
* 75.03% against a 75% floor, i.e. the refactor spent almost all the headroom
|
|
279
|
-
* on a reporting artifact.
|
|
275
|
+
* The 40% was a c8 merge defect, not an import cost: c8's V8-level merge is not
|
|
276
|
+
* monotonic, and once the report merges at the istanbul level instead this file
|
|
277
|
+
* reads 100% with the edge in place (see `//merge-bug` in .c8rc.json). So the
|
|
278
|
+
* duplication was paying for an artifact, and the two copies are now one.
|
|
280
279
|
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
* import-free regardless.
|
|
280
|
+
* Keep this module import-free regardless. That part was never about coverage:
|
|
281
|
+
* `query/` and `cli/` both reach it, and an edge back to `client.ts` would be
|
|
282
|
+
* the cycle `scripts/check-import-cycles.mjs` exists to refuse.
|
|
285
283
|
*/
|
|
286
284
|
function mergeConnectionStringOptions(connectionString, setting) {
|
|
287
285
|
const q = connectionString.indexOf('?');
|
package/dist/cjs/powql.d.ts
CHANGED
|
@@ -67,8 +67,34 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
67
67
|
private readonly onQuery?;
|
|
68
68
|
private warnedUnlimited;
|
|
69
69
|
constructor(pool: PowdbPool, table: string, schema: SchemaMetadata, middlewares?: MiddlewareFn[], options?: QueryInterfaceOptions);
|
|
70
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* Resolve a camelCase field name (or raw snake) to its column metadata.
|
|
72
|
+
*
|
|
73
|
+
* THE single key→column decision on this engine: every PowQL surface
|
|
74
|
+
* reaches a column through here, which is why PowDB never grew the
|
|
75
|
+
* per-argument asymmetry the SQL builders did. It resolves through the
|
|
76
|
+
* shared {@link resolveColumnName} so both engines answer alike; the direct
|
|
77
|
+
* name/field match stays as a fallback, since a PowDB schema may carry names
|
|
78
|
+
* outside the snake_case↔camelCase round trip and narrowing that would be a
|
|
79
|
+
* silent break rather than a fix.
|
|
80
|
+
*/
|
|
71
81
|
private column;
|
|
82
|
+
/**
|
|
83
|
+
* {@link lookupGroupKey} for the `${aggKey}:${field}` composite the aggregate
|
|
84
|
+
* ordering registry is keyed by: only the FIELD half needs canonicalizing.
|
|
85
|
+
*/
|
|
86
|
+
private aggOrderExpr;
|
|
87
|
+
/**
|
|
88
|
+
* Read a caller-supplied groupBy result key out of a registry keyed by the
|
|
89
|
+
* canonical FIELD name.
|
|
90
|
+
*
|
|
91
|
+
* `by`, `orderBy` and `having` are three arguments of one call, each free to
|
|
92
|
+
* spell a column either way, so `by`'s choice must not decide what the others
|
|
93
|
+
* may name. Try the key as written (which is what carries a JSON group-key
|
|
94
|
+
* ALIAS, not a column), then its canonical field. Mirrors `lookupGroupKey` in
|
|
95
|
+
* query/aggregates.ts.
|
|
96
|
+
*/
|
|
97
|
+
private lookupGroupKey;
|
|
72
98
|
/**
|
|
73
99
|
* PowQL column reference for a field: a dotted field reference
|
|
74
100
|
* (`.snake_name`), or `alias.snake_name` when an `alias` is supplied (the F2
|
|
@@ -149,6 +175,17 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
149
175
|
* which is precisely the "reports a guarantee that was never made" failure the
|
|
150
176
|
* option's refusal exists to prevent.
|
|
151
177
|
*/
|
|
178
|
+
/**
|
|
179
|
+
* `args` with every `with` relation key replaced by the relation's DECLARED
|
|
180
|
+
* spelling, so PowDB accepts a snake_case relation name exactly as the SQL
|
|
181
|
+
* engines do. Returns `args` by reference when nothing needed rewriting.
|
|
182
|
+
*
|
|
183
|
+
* PowqlInterface is a parallel implementation rather than a subclass, so
|
|
184
|
+
* nothing makes this happen here automatically: a rule adopted only on the
|
|
185
|
+
* SQL side becomes an engine that disagrees about which queries are valid,
|
|
186
|
+
* which is the divergence class the projection resolver already cost.
|
|
187
|
+
*/
|
|
188
|
+
private withDeclaredRelationNames;
|
|
152
189
|
private assertNoForceCustomPlan;
|
|
153
190
|
private assertPagination;
|
|
154
191
|
/** A predicate that is always false, the empty-`in` / contradiction sentinel. */
|
package/dist/cjs/powql.js
CHANGED
|
@@ -77,6 +77,7 @@ const powdb_js_1 = require("./powdb.js");
|
|
|
77
77
|
const aggregates_js_1 = require("./query/aggregates.js");
|
|
78
78
|
const compound_unique_js_1 = require("./query/compound-unique.js");
|
|
79
79
|
const filters_js_1 = require("./query/filters.js");
|
|
80
|
+
const relation_names_js_1 = require("./query/relation-names.js");
|
|
80
81
|
// The privilege sentinel and its resolver: `includePii` / `allowFullTableScan`
|
|
81
82
|
// are unlocked ONLY by the UNSAFE symbol, on this engine exactly as on the SQL
|
|
82
83
|
// engines, so a spread request body cannot turn either on here either.
|
|
@@ -231,9 +232,20 @@ class PowqlInterface {
|
|
|
231
232
|
// -------------------------------------------------------------------------
|
|
232
233
|
// Column / value helpers
|
|
233
234
|
// -------------------------------------------------------------------------
|
|
234
|
-
/**
|
|
235
|
+
/**
|
|
236
|
+
* Resolve a camelCase field name (or raw snake) to its column metadata.
|
|
237
|
+
*
|
|
238
|
+
* THE single key→column decision on this engine: every PowQL surface
|
|
239
|
+
* reaches a column through here, which is why PowDB never grew the
|
|
240
|
+
* per-argument asymmetry the SQL builders did. It resolves through the
|
|
241
|
+
* shared {@link resolveColumnName} so both engines answer alike; the direct
|
|
242
|
+
* name/field match stays as a fallback, since a PowDB schema may carry names
|
|
243
|
+
* outside the snake_case↔camelCase round trip and narrowing that would be a
|
|
244
|
+
* silent break rather than a fix.
|
|
245
|
+
*/
|
|
235
246
|
column(field) {
|
|
236
|
-
const
|
|
247
|
+
const resolved = (0, utils_js_1.resolveColumnName)(this.meta, field);
|
|
248
|
+
const snake = resolved ?? (0, utils_js_1.ownLookup)(this.meta.columnMap, field) ?? field;
|
|
237
249
|
const col = this.meta.columns.find((c) => c.name === snake || c.field === field);
|
|
238
250
|
if (!col) {
|
|
239
251
|
throw new errors_js_1.ValidationError(`[turbine] Unknown column "${field}" on table "${this.table}". Known: ${this.meta.columns
|
|
@@ -242,6 +254,41 @@ class PowqlInterface {
|
|
|
242
254
|
}
|
|
243
255
|
return col;
|
|
244
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* {@link lookupGroupKey} for the `${aggKey}:${field}` composite the aggregate
|
|
259
|
+
* ordering registry is keyed by: only the FIELD half needs canonicalizing.
|
|
260
|
+
*/
|
|
261
|
+
aggOrderExpr(registry, aggKey, field) {
|
|
262
|
+
try {
|
|
263
|
+
return registry.get(`${aggKey}:${this.column(field).field}`);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return undefined; // not a column: the caller's own E003 is the right error
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Read a caller-supplied groupBy result key out of a registry keyed by the
|
|
271
|
+
* canonical FIELD name.
|
|
272
|
+
*
|
|
273
|
+
* `by`, `orderBy` and `having` are three arguments of one call, each free to
|
|
274
|
+
* spell a column either way, so `by`'s choice must not decide what the others
|
|
275
|
+
* may name. Try the key as written (which is what carries a JSON group-key
|
|
276
|
+
* ALIAS, not a column), then its canonical field. Mirrors `lookupGroupKey` in
|
|
277
|
+
* query/aggregates.ts.
|
|
278
|
+
*/
|
|
279
|
+
lookupGroupKey(registry, key) {
|
|
280
|
+
const direct = registry.get(key);
|
|
281
|
+
if (direct !== undefined)
|
|
282
|
+
return direct;
|
|
283
|
+
let field;
|
|
284
|
+
try {
|
|
285
|
+
field = this.column(key).field;
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
return undefined; // not a column: the caller's own E003 below is the right error
|
|
289
|
+
}
|
|
290
|
+
return registry.get(field);
|
|
291
|
+
}
|
|
245
292
|
/**
|
|
246
293
|
* PowQL column reference for a field: a dotted field reference
|
|
247
294
|
* (`.snake_name`), or `alias.snake_name` when an `alias` is supplied (the F2
|
|
@@ -356,6 +403,22 @@ class PowqlInterface {
|
|
|
356
403
|
* which is precisely the "reports a guarantee that was never made" failure the
|
|
357
404
|
* option's refusal exists to prevent.
|
|
358
405
|
*/
|
|
406
|
+
/**
|
|
407
|
+
* `args` with every `with` relation key replaced by the relation's DECLARED
|
|
408
|
+
* spelling, so PowDB accepts a snake_case relation name exactly as the SQL
|
|
409
|
+
* engines do. Returns `args` by reference when nothing needed rewriting.
|
|
410
|
+
*
|
|
411
|
+
* PowqlInterface is a parallel implementation rather than a subclass, so
|
|
412
|
+
* nothing makes this happen here automatically: a rule adopted only on the
|
|
413
|
+
* SQL side becomes an engine that disagrees about which queries are valid,
|
|
414
|
+
* which is the divergence class the projection resolver already cost.
|
|
415
|
+
*/
|
|
416
|
+
withDeclaredRelationNames(args) {
|
|
417
|
+
if (!args?.with)
|
|
418
|
+
return args;
|
|
419
|
+
const normalized = (0, relation_names_js_1.normalizeWithClause)(this.schema, this.table, args.with);
|
|
420
|
+
return normalized === args.with ? args : { ...args, with: normalized };
|
|
421
|
+
}
|
|
359
422
|
assertNoForceCustomPlan(args) {
|
|
360
423
|
if (args?.forceCustomPlan !== true)
|
|
361
424
|
return;
|
|
@@ -796,8 +859,8 @@ class PowqlInterface {
|
|
|
796
859
|
else if (key === 'NOT') {
|
|
797
860
|
scalar[key] = await this.resolveRelationFilters(value, timeout);
|
|
798
861
|
}
|
|
799
|
-
else if (this.meta.relations
|
|
800
|
-
relConds.push(await this.resolveRelationCondition(this.meta.relations
|
|
862
|
+
else if ((0, utils_js_1.resolveRelationDef)(this.meta.relations, key)) {
|
|
863
|
+
relConds.push(await this.resolveRelationCondition((0, utils_js_1.resolveRelationDef)(this.meta.relations, key), value, timeout));
|
|
801
864
|
}
|
|
802
865
|
else {
|
|
803
866
|
scalar[key] = value;
|
|
@@ -932,7 +995,7 @@ class PowqlInterface {
|
|
|
932
995
|
* for a misspelling that is not there. Same message as the SQL engines.
|
|
933
996
|
*/
|
|
934
997
|
projectionColumn(field, clause) {
|
|
935
|
-
if ((0, utils_js_1.
|
|
998
|
+
if ((0, utils_js_1.resolveRelationDef)(this.meta.relations, field)) {
|
|
936
999
|
throw new errors_js_1.ValidationError((0, utils_js_1.relationInProjectionMessage)(this.table, field, clause));
|
|
937
1000
|
}
|
|
938
1001
|
return this.column(field).name;
|
|
@@ -1300,6 +1363,7 @@ class PowqlInterface {
|
|
|
1300
1363
|
// -------------------------------------------------------------------------
|
|
1301
1364
|
async findMany(args = {}) {
|
|
1302
1365
|
this.assertNoForceCustomPlan(args);
|
|
1366
|
+
args = this.withDeclaredRelationNames(args);
|
|
1303
1367
|
return this.withMiddleware('findMany', args, async () => {
|
|
1304
1368
|
// `limit: 0` means "no rows" (SQL `LIMIT 0`), and answering it client-side
|
|
1305
1369
|
// is correct on every engine version: PowDB's projection fast path returned
|
|
@@ -1381,6 +1445,12 @@ class PowqlInterface {
|
|
|
1381
1445
|
const nest = nestedPlans.length > 0 || linkPlans.length > 0;
|
|
1382
1446
|
const alias = nest ? 't0' : undefined;
|
|
1383
1447
|
const where = this.buildWhere(resolvedWhere, params, alias);
|
|
1448
|
+
// PowQL's `distinct` is row-wide, so these names never reach the emitted
|
|
1449
|
+
// statement. They are still caller-supplied names, and a name resolves or
|
|
1450
|
+
// throws: reading the array for its LENGTH alone let `distinct: ['nope']`
|
|
1451
|
+
// succeed here while every SQL engine refuses it. Validation only.
|
|
1452
|
+
for (const key of args.distinct ?? [])
|
|
1453
|
+
this.column(key);
|
|
1384
1454
|
const distinct = args.distinct?.length ? ' distinct' : '';
|
|
1385
1455
|
const filter = where ? ` filter ${where}` : '';
|
|
1386
1456
|
const order = this.buildOrder(args.orderBy, params, alias);
|
|
@@ -1445,6 +1515,7 @@ class PowqlInterface {
|
|
|
1445
1515
|
}
|
|
1446
1516
|
async findUnique(args) {
|
|
1447
1517
|
this.assertNoForceCustomPlan(args);
|
|
1518
|
+
args = this.withDeclaredRelationNames(args);
|
|
1448
1519
|
// Prisma compound-unique selector → column conjunction (engine parity with
|
|
1449
1520
|
// the SQL findUnique family; pure metadata, so this is a one-line adoption).
|
|
1450
1521
|
if (args.where) {
|
|
@@ -1469,6 +1540,7 @@ class PowqlInterface {
|
|
|
1469
1540
|
});
|
|
1470
1541
|
}
|
|
1471
1542
|
async findFirst(args = {}) {
|
|
1543
|
+
args = this.withDeclaredRelationNames(args);
|
|
1472
1544
|
this.assertNoForceCustomPlan(args);
|
|
1473
1545
|
return this.withMiddleware('findFirst', args, async () => {
|
|
1474
1546
|
const { rows, native, nestedPlans, linkPlans, residualWith, forcedPk } = await this.runFind({ ...args, limit: 1 }, 'findFirst');
|
|
@@ -2400,7 +2472,7 @@ class PowqlInterface {
|
|
|
2400
2472
|
for (const [field, value] of Object.entries(data)) {
|
|
2401
2473
|
if (value === undefined)
|
|
2402
2474
|
continue;
|
|
2403
|
-
if (this.meta.relations
|
|
2475
|
+
if ((0, utils_js_1.resolveRelationDef)(this.meta.relations, field)) {
|
|
2404
2476
|
throw new errors_js_1.UnsupportedFeatureError('nested writes', 'PowDB', `relation "${field}", nested writes need create()/update(), not createMany()/upsert()`);
|
|
2405
2477
|
}
|
|
2406
2478
|
out.push({ col: this.column(field), value });
|
|
@@ -2418,7 +2490,11 @@ class PowqlInterface {
|
|
|
2418
2490
|
for (const pk of this.meta.primaryKey) {
|
|
2419
2491
|
const field = this.meta.reverseColumnMap[pk] ?? pk;
|
|
2420
2492
|
const col = this.meta.columns.find((c) => c.name === pk);
|
|
2493
|
+
// "Supplied" means under EITHER spelling: `create` resolves both, so
|
|
2494
|
+
// testing the camelCase field alone made `{ user_id: 'x' }` look absent,
|
|
2495
|
+
// generated a UUID under `userId`, and wrote the column twice.
|
|
2421
2496
|
if (out[field] == null &&
|
|
2497
|
+
(field === pk || out[pk] == null) &&
|
|
2422
2498
|
col?.hasDefault &&
|
|
2423
2499
|
!col.isGenerated &&
|
|
2424
2500
|
col.tsType.replace(/\s*\|\s*null$/, '').trim() === 'string') {
|
|
@@ -2557,7 +2633,7 @@ class PowqlInterface {
|
|
|
2557
2633
|
for (const [field, value] of Object.entries(data)) {
|
|
2558
2634
|
if (value === undefined)
|
|
2559
2635
|
continue;
|
|
2560
|
-
if (this.meta.relations
|
|
2636
|
+
if ((0, utils_js_1.resolveRelationDef)(this.meta.relations, field)) {
|
|
2561
2637
|
throw new errors_js_1.UnsupportedFeatureError('nested writes', 'PowDB', `relation "${field}", nested writes need create()/update(), not updateMany()/upsert()`);
|
|
2562
2638
|
}
|
|
2563
2639
|
const colMeta = this.column(field);
|
|
@@ -2720,7 +2796,10 @@ class PowqlInterface {
|
|
|
2720
2796
|
// reselect-by-PK fetch; create/update/delete all use `returning`.
|
|
2721
2797
|
await this.exec(`upsert ${this.qt} on ${this.colRefName(pkCol)} { ${createBody} } on conflict { ${updateBody} }`, params, args.timeout, 'upsert');
|
|
2722
2798
|
const pkField = this.meta.reverseColumnMap[pkCol] ?? pkCol;
|
|
2723
|
-
|
|
2799
|
+
// Either spelling, as `upsertComposite` already does below: reading the
|
|
2800
|
+
// camelCase field alone reselected `undefined` for a snake-spelled PK and
|
|
2801
|
+
// reported a write that had SUCCEEDED as a NotFoundError.
|
|
2802
|
+
const row = await this.reselectByPk(createData[pkField] ?? createData[pkCol], args.timeout);
|
|
2724
2803
|
if (!row)
|
|
2725
2804
|
throw new errors_js_1.NotFoundError({ table: this.table, where: createData });
|
|
2726
2805
|
return row;
|
|
@@ -2888,13 +2967,18 @@ class PowqlInterface {
|
|
|
2888
2967
|
if (typeof entry === 'string') {
|
|
2889
2968
|
const col = this.column(entry);
|
|
2890
2969
|
(0, aggregates_js_1.assertAggregatePiiOptIn)(this.table, this.meta, entry, col.name, 'groupBy `by` key', (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii'));
|
|
2891
|
-
|
|
2892
|
-
|
|
2970
|
+
// Keyed by the canonical FIELD, never by whichever of the column's
|
|
2971
|
+
// two legal spellings the caller wrote (`column` accepts both), so
|
|
2972
|
+
// neither the result shape nor the keys `orderBy` accepts depends on
|
|
2973
|
+
// that choice; `lookupGroupKey` reconciles the other spelling. Same
|
|
2974
|
+
// rule and same result shape as the SQL groupBy (query/aggregates.ts).
|
|
2975
|
+
claim(col.field, `column "${col.name}"`);
|
|
2976
|
+
if (col.name !== col.field)
|
|
2893
2977
|
claim(col.name, `column "${col.name}"`);
|
|
2894
2978
|
groupExprs.push(this.colRefName(col.name));
|
|
2895
2979
|
proj.push(this.colRefName(col.name));
|
|
2896
|
-
byOrderExprs.set(
|
|
2897
|
-
byReaders.push({ kind: 'plain', resultKey:
|
|
2980
|
+
byOrderExprs.set(col.field, this.colRefName(col.name));
|
|
2981
|
+
byReaders.push({ kind: 'plain', resultKey: col.field, rowKey: col.name, col });
|
|
2898
2982
|
}
|
|
2899
2983
|
else {
|
|
2900
2984
|
const col = this.column(entry.field);
|
|
@@ -2957,9 +3041,11 @@ class PowqlInterface {
|
|
|
2957
3041
|
claim(`${fn}_${col.name}`, `${fn} of column "${col.name}"`);
|
|
2958
3042
|
const inner = this.colRefName(col.name);
|
|
2959
3043
|
proj.push(`${alias}: ${powfn}(${inner})`);
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
3044
|
+
// Canonical field, so the result bucket and the keys `orderBy` /
|
|
3045
|
+
// `having` accept are the same whichever spelling was requested.
|
|
3046
|
+
aggReaders.push({ alias, outKey: `${fn}:${col.field}`, numeric: true });
|
|
3047
|
+
aggOrderExprs.set(`${fn}:${col.field}`, `.${alias}`);
|
|
3048
|
+
aggInner.set(col.field, inner);
|
|
2963
3049
|
}
|
|
2964
3050
|
else {
|
|
2965
3051
|
const col = this.column(target.field);
|
|
@@ -3095,7 +3181,7 @@ class PowqlInterface {
|
|
|
3095
3181
|
throw new errors_js_1.ValidationError(`[turbine] Unknown aggregate "${aggKey}" in having for field "${key}" on table "${this.table}". ` +
|
|
3096
3182
|
`Supported: ${[...POWQL_HAVING_AGG_FNS.keys()].join(', ')}.`);
|
|
3097
3183
|
}
|
|
3098
|
-
const inner =
|
|
3184
|
+
const inner = this.lookupGroupKey(aggInner, key) ?? this.ref(key);
|
|
3099
3185
|
conds.push(cmp(`${fn}(${inner})`, filter));
|
|
3100
3186
|
}
|
|
3101
3187
|
}
|
|
@@ -3143,7 +3229,9 @@ class PowqlInterface {
|
|
|
3143
3229
|
for (const [field, dirSpec] of Object.entries(value)) {
|
|
3144
3230
|
if (dirSpec === undefined)
|
|
3145
3231
|
continue;
|
|
3146
|
-
|
|
3232
|
+
// `field` is the caller's spelling of the aggregate's target column;
|
|
3233
|
+
// the registry is keyed by the canonical one.
|
|
3234
|
+
const expr = aggOrderExprs.get(`${key}:${field}`) ?? this.aggOrderExpr(aggOrderExprs, key, field);
|
|
3147
3235
|
if (!expr) {
|
|
3148
3236
|
throw new errors_js_1.ValidationError(`[turbine] Cannot order groupBy by "${key}.${field}" on table "${this.table}": ` +
|
|
3149
3237
|
`that aggregate is not requested in this call. Orderable keys: ${validKeys()}.`);
|
|
@@ -3152,7 +3240,7 @@ class PowqlInterface {
|
|
|
3152
3240
|
}
|
|
3153
3241
|
continue;
|
|
3154
3242
|
}
|
|
3155
|
-
const expr =
|
|
3243
|
+
const expr = this.lookupGroupKey(byOrderExprs, key);
|
|
3156
3244
|
if (!expr) {
|
|
3157
3245
|
throw new errors_js_1.ValidationError(`[turbine] Unknown field "${key}" in groupBy orderBy on table "${this.table}". Orderable keys: ${validKeys()}.`);
|
|
3158
3246
|
}
|
|
@@ -30,19 +30,6 @@ import type { BuilderCtx } from './where.js';
|
|
|
30
30
|
*/
|
|
31
31
|
export declare function assertAggregatePiiOptIn(table: string, meta: TableMetadata | undefined, field: string, column: string, usage: string, includePii: boolean | undefined): void;
|
|
32
32
|
export declare function buildGroupBy<T extends object>(qi: BuilderCtx, args: GroupByArgs<T>): DeferredQuery<Record<string, unknown>[]>;
|
|
33
|
-
/**
|
|
34
|
-
* Compile a groupBy `orderBy` into an ORDER BY body. Unlike findMany ORDER BY
|
|
35
|
-
* ({@link buildOrderBy}, which validates keys against the table's physical
|
|
36
|
-
* columns), groupBy ordering targets the columns the RESULT actually
|
|
37
|
-
* contains: plain by-fields, JSON group-key aliases, and requested aggregates
|
|
38
|
-
* (`_count` / `_sum` / `_avg` / `_min` / `_max`). Each key re-emits the exact
|
|
39
|
-
* SELECT expression that produced it (`byOrderExprs` / `aggOrderExprs`),
|
|
40
|
-
* mirroring how HAVING re-emits aggregate expressions, so no dialect ever has
|
|
41
|
-
* to accept a SELECT-alias reference in ORDER BY, and any already-bound
|
|
42
|
-
* JSON-path placeholder is reused verbatim (ORDER BY is the last clause, so
|
|
43
|
-
* no `$n` renumbering). An aggregate key that was not requested, or an unknown
|
|
44
|
-
* by-key, throws {@link ValidationError} E003 listing the valid keys.
|
|
45
|
-
*/
|
|
46
33
|
export declare function buildGroupByOrderBy(qi: BuilderCtx, orderBy: GroupByOrderBy | GroupByOrderBy[], byOrderExprs: Map<string, string>, aggOrderExprs: Map<string, string>): string;
|
|
47
34
|
/**
|
|
48
35
|
* Validate a JSON-path target (group key or aggregate target) in groupBy:
|