turbine-orm 0.77.1 → 0.78.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 +13 -9
- package/dist/cjs/cli/config.d.ts +7 -1
- package/dist/cjs/cli/config.js +11 -2
- package/dist/cjs/cli/destructive.d.ts +1 -1
- package/dist/cjs/cli/destructive.js +307 -9
- package/dist/cjs/cli/index.js +252 -42
- package/dist/cjs/cli/mcp.d.ts +23 -0
- package/dist/cjs/cli/mcp.js +190 -152
- package/dist/cjs/cli/migrate.d.ts +243 -3
- package/dist/cjs/cli/migrate.js +432 -43
- package/dist/cjs/cli/sql-statements.js +27 -0
- package/dist/cjs/cli/studio.d.ts +0 -1
- package/dist/cjs/cli/studio.js +9 -7
- package/dist/cjs/client.d.ts +8 -1
- package/dist/cjs/client.js +7 -0
- package/dist/cjs/connection-url.d.ts +82 -0
- package/dist/cjs/connection-url.js +187 -1
- package/dist/cjs/errors.d.ts +112 -12
- package/dist/cjs/errors.js +558 -114
- package/dist/cjs/generate.js +47 -15
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/introspect.d.ts +33 -0
- package/dist/cjs/introspect.js +54 -1
- package/dist/cjs/mssql.js +21 -1
- package/dist/cjs/nested-write.js +85 -14
- package/dist/cjs/pipeline-submittable.d.ts +2 -0
- package/dist/cjs/pipeline-submittable.js +88 -3
- package/dist/cjs/pipeline.js +13 -1
- package/dist/cjs/powdb-introspect.d.ts +5 -1
- package/dist/cjs/powdb-introspect.js +5 -1
- package/dist/cjs/powql.d.ts +14 -0
- package/dist/cjs/powql.js +44 -4
- package/dist/cjs/prisma-compat.js +95 -8
- package/dist/cjs/query/aggregates.js +56 -6
- package/dist/cjs/query/builder.d.ts +76 -13
- package/dist/cjs/query/builder.js +188 -58
- package/dist/cjs/query/compound-unique.d.ts +76 -0
- package/dist/cjs/query/compound-unique.js +129 -0
- package/dist/cjs/query/index.d.ts +1 -1
- package/dist/cjs/query/types.d.ts +65 -11
- package/dist/cjs/query/where.d.ts +85 -19
- package/dist/cjs/query/where.js +262 -47
- package/dist/cjs/query/writes.d.ts +11 -2
- package/dist/cjs/query/writes.js +116 -21
- package/dist/cjs/seed.d.ts +16 -0
- package/dist/cjs/seed.js +16 -0
- package/dist/cli/config.d.ts +7 -1
- package/dist/cli/config.js +11 -2
- package/dist/cli/destructive.d.ts +1 -1
- package/dist/cli/destructive.js +307 -9
- package/dist/cli/index.js +254 -44
- package/dist/cli/mcp.d.ts +23 -0
- package/dist/cli/mcp.js +187 -150
- package/dist/cli/migrate.d.ts +243 -3
- package/dist/cli/migrate.js +423 -45
- package/dist/cli/sql-statements.js +27 -0
- package/dist/cli/studio.d.ts +0 -1
- package/dist/cli/studio.js +10 -7
- package/dist/client.d.ts +8 -1
- package/dist/client.js +7 -0
- package/dist/connection-url.d.ts +82 -0
- package/dist/connection-url.js +183 -0
- package/dist/errors.d.ts +112 -12
- package/dist/errors.js +558 -114
- package/dist/generate.js +47 -15
- package/dist/index.d.ts +1 -1
- package/dist/introspect.d.ts +33 -0
- package/dist/introspect.js +53 -1
- package/dist/mssql.js +21 -1
- package/dist/nested-write.js +85 -14
- package/dist/pipeline-submittable.d.ts +2 -0
- package/dist/pipeline-submittable.js +87 -3
- package/dist/pipeline.js +14 -2
- package/dist/powdb-introspect.d.ts +5 -1
- package/dist/powdb-introspect.js +5 -1
- package/dist/powql.d.ts +14 -0
- package/dist/powql.js +45 -5
- package/dist/prisma-compat.js +96 -9
- package/dist/query/aggregates.js +56 -6
- package/dist/query/builder.d.ts +76 -13
- package/dist/query/builder.js +188 -58
- package/dist/query/compound-unique.d.ts +76 -0
- package/dist/query/compound-unique.js +126 -1
- package/dist/query/index.d.ts +1 -1
- package/dist/query/types.d.ts +65 -11
- package/dist/query/where.d.ts +85 -19
- package/dist/query/where.js +260 -47
- package/dist/query/writes.d.ts +11 -2
- package/dist/query/writes.js +117 -22
- package/dist/seed.d.ts +16 -0
- package/dist/seed.js +16 -0
- package/package.json +3 -3
- package/skills/turbine-orm/SKILL.md +37 -10
package/dist/cjs/powql.d.ts
CHANGED
|
@@ -218,6 +218,20 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
218
218
|
private assertNoForceCustomPlan;
|
|
219
219
|
/** See query/compound-unique.ts: one rule and one message across engines. */
|
|
220
220
|
private assertIdentifiesOneRow;
|
|
221
|
+
/**
|
|
222
|
+
* The single-row WRITE rule (`update` / `delete` return one row, so their
|
|
223
|
+
* `where` must identify one; `upsert`'s `where` names the row it conflicts
|
|
224
|
+
* on), shared with the SQL engines through query/compound-unique.ts so the
|
|
225
|
+
* two cannot disagree about which writes are valid. Skipped under the
|
|
226
|
+
* explicit full-table opt-in, which already means "every row". For `update` /
|
|
227
|
+
* `delete` it runs AFTER `assertCompiledWhere`, so an empty selector keeps
|
|
228
|
+
* the empty-where message and this only refuses a non-empty one that names no
|
|
229
|
+
* key; `upsert` takes no `allowFullTableScan` and has no empty-where guard to
|
|
230
|
+
* run first, so this is its only where check and it covers `{}` as well.
|
|
231
|
+
*/
|
|
232
|
+
private assertMutationIdentifiesOneRow;
|
|
233
|
+
/** A caller `where` with a Prisma compound-unique selector expanded to its member columns. */
|
|
234
|
+
private expandedWhere;
|
|
221
235
|
private assertPagination;
|
|
222
236
|
/** A predicate that is always false, the empty-`in` / contradiction sentinel. */
|
|
223
237
|
private alwaysFalse;
|
package/dist/cjs/powql.js
CHANGED
|
@@ -482,6 +482,28 @@ class PowqlInterface {
|
|
|
482
482
|
assertIdentifiesOneRow(where) {
|
|
483
483
|
(0, compound_unique_js_1.assertWhereIdentifiesOneRow)(this.meta, this.table, where);
|
|
484
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* The single-row WRITE rule (`update` / `delete` return one row, so their
|
|
487
|
+
* `where` must identify one; `upsert`'s `where` names the row it conflicts
|
|
488
|
+
* on), shared with the SQL engines through query/compound-unique.ts so the
|
|
489
|
+
* two cannot disagree about which writes are valid. Skipped under the
|
|
490
|
+
* explicit full-table opt-in, which already means "every row". For `update` /
|
|
491
|
+
* `delete` it runs AFTER `assertCompiledWhere`, so an empty selector keeps
|
|
492
|
+
* the empty-where message and this only refuses a non-empty one that names no
|
|
493
|
+
* key; `upsert` takes no `allowFullTableScan` and has no empty-where guard to
|
|
494
|
+
* run first, so this is its only where check and it covers `{}` as well.
|
|
495
|
+
*/
|
|
496
|
+
assertMutationIdentifiesOneRow(where, allowFullTableScan, operation) {
|
|
497
|
+
if (allowFullTableScan)
|
|
498
|
+
return;
|
|
499
|
+
(0, compound_unique_js_1.assertMutationWhereIdentifiesOneRow)(this.meta, this.table, where, operation);
|
|
500
|
+
}
|
|
501
|
+
/** A caller `where` with a Prisma compound-unique selector expanded to its member columns. */
|
|
502
|
+
expandedWhere(where) {
|
|
503
|
+
if (!where)
|
|
504
|
+
return where;
|
|
505
|
+
return (0, compound_unique_js_1.expandCompoundUniqueWhere)(this.meta, where);
|
|
506
|
+
}
|
|
485
507
|
assertPagination(limit, offset, context) {
|
|
486
508
|
for (const [name, value] of [
|
|
487
509
|
['limit', limit],
|
|
@@ -2701,14 +2723,22 @@ class PowqlInterface {
|
|
|
2701
2723
|
async update(args) {
|
|
2702
2724
|
return this.withMiddleware('update', args, async () => {
|
|
2703
2725
|
if ((0, nested_write_js_1.hasRelationFields)(args.data, this.meta)) {
|
|
2726
|
+
// The nested engine re-enters `update` / `findUnique` for the parent
|
|
2727
|
+
// row before it writes anything, so the identity rule below still runs
|
|
2728
|
+
// first on this path.
|
|
2704
2729
|
return this.nestedUpdate(args);
|
|
2705
2730
|
}
|
|
2731
|
+
const allowFullTableScan = (0, types_js_1.resolveUnsafeFlag)(args.allowFullTableScan, 'allowFullTableScan');
|
|
2732
|
+
// Prisma compound-unique selector → column conjunction, so the selector
|
|
2733
|
+
// counts as the key it is (engine parity with the SQL buildUpdate).
|
|
2734
|
+
const userWhere = this.expandedWhere(args.where);
|
|
2706
2735
|
const params = [];
|
|
2707
|
-
const resolvedWhere = await this.resolveRelationFilters(
|
|
2736
|
+
const resolvedWhere = await this.resolveRelationFilters(userWhere, args.timeout);
|
|
2708
2737
|
let where = this.buildWhere(resolvedWhere, params);
|
|
2709
2738
|
// `false` here refused an empty where even WITH the opt-in, while every
|
|
2710
2739
|
// SQL engine accepted it: verified by probe on buildUpdate/buildDelete.
|
|
2711
|
-
this.assertCompiledWhere(where,
|
|
2740
|
+
this.assertCompiledWhere(where, allowFullTableScan, 'update');
|
|
2741
|
+
this.assertMutationIdentifiesOneRow(userWhere, allowFullTableScan, 'update');
|
|
2712
2742
|
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters);
|
|
2713
2743
|
let setClause = this.buildUpdateAssignments(args.data, params);
|
|
2714
2744
|
// Optimistic locking, matching the SQL engines exactly: bump the version
|
|
@@ -2882,10 +2912,13 @@ class PowqlInterface {
|
|
|
2882
2912
|
}
|
|
2883
2913
|
async delete(args) {
|
|
2884
2914
|
return this.withMiddleware('delete', args, async () => {
|
|
2915
|
+
const allowFullTableScan = (0, types_js_1.resolveUnsafeFlag)(args.allowFullTableScan, 'allowFullTableScan');
|
|
2916
|
+
const userWhere = this.expandedWhere(args.where);
|
|
2885
2917
|
const params = [];
|
|
2886
|
-
const resolvedWhere = await this.resolveRelationFilters(
|
|
2918
|
+
const resolvedWhere = await this.resolveRelationFilters(userWhere, args.timeout);
|
|
2887
2919
|
let where = this.buildWhere(resolvedWhere, params);
|
|
2888
|
-
this.assertCompiledWhere(where,
|
|
2920
|
+
this.assertCompiledWhere(where, allowFullTableScan, 'delete');
|
|
2921
|
+
this.assertMutationIdentifiesOneRow(userWhere, allowFullTableScan, 'delete');
|
|
2889
2922
|
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters);
|
|
2890
2923
|
// `returning` hands back the deleted row(s), no separate pre-image reselect needed.
|
|
2891
2924
|
const { rows, native } = await this.exec(`${this.qt} filter ${where} delete returning`, params, args.timeout, 'delete');
|
|
@@ -2909,6 +2942,13 @@ class PowqlInterface {
|
|
|
2909
2942
|
}
|
|
2910
2943
|
async upsert(args) {
|
|
2911
2944
|
return this.withMiddleware('upsert', args, async () => {
|
|
2945
|
+
// The single-row rule for `upsert`, shared with the SQL engines through
|
|
2946
|
+
// query/compound-unique.ts. PowqlInterface is a PARALLEL implementation,
|
|
2947
|
+
// not a subclass, so a rule adopted only on the SQL side is two engines
|
|
2948
|
+
// disagreeing about whether a query is VALID, which is the failure class
|
|
2949
|
+
// 0.64.0 and 0.72.0 were both spent on. Before `applyPkDefault`, so a
|
|
2950
|
+
// refused upsert mints no client-side UUID and sends nothing.
|
|
2951
|
+
this.assertMutationIdentifiesOneRow(this.expandedWhere(args.where), false, 'upsert');
|
|
2912
2952
|
const createData = this.applyPkDefault(args.create);
|
|
2913
2953
|
const pkCol = this.meta.primaryKey[0];
|
|
2914
2954
|
if (this.meta.primaryKey.length !== 1 || !pkCol) {
|
|
@@ -297,6 +297,33 @@ function lookupsFor(ctx, mm) {
|
|
|
297
297
|
return l;
|
|
298
298
|
}
|
|
299
299
|
/** Resolve a turbine relation's target Prisma model map (for nested translation). */
|
|
300
|
+
/**
|
|
301
|
+
* A `where` fragment no row satisfies, for Prisma's empty `OR`.
|
|
302
|
+
*
|
|
303
|
+
* `{ <key>: { in: [] } }`, which core compiles to `= ANY($n)` with an empty
|
|
304
|
+
* array: false for every row, NULL-valued ones included, and one ordinary bound
|
|
305
|
+
* param rather than a literal spliced into the SQL. The column is the first
|
|
306
|
+
* primary-key column, else the table's first column, so it is always a real
|
|
307
|
+
* column of this table and the predicate stays a plain scalar comparison the
|
|
308
|
+
* cache fingerprints like any other.
|
|
309
|
+
*
|
|
310
|
+
* A table this layer cannot name a column of THROWS. The fallback used to be
|
|
311
|
+
* `{}`, an empty `where` fragment - which is not "no rows" but its exact
|
|
312
|
+
* opposite, EVERY row, and it would have been merged in silently beside the
|
|
313
|
+
* caller's other predicates. A sentinel builder that cannot build a sentinel
|
|
314
|
+
* has not succeeded, and the one shape it exists to compile is the one where
|
|
315
|
+
* answering "all of them" instead of "none of them" is worst.
|
|
316
|
+
*/
|
|
317
|
+
function matchNothing(ctx, mm) {
|
|
318
|
+
const meta = ctx.schema.tables[mm.table];
|
|
319
|
+
const column = meta?.primaryKey?.[0] ?? meta?.allColumns?.[0];
|
|
320
|
+
if (!meta || !column) {
|
|
321
|
+
throw new errors_js_1.ValidationError(`An empty \`OR\` on model ${mm.table} means "no rows", and compiling that needs one real column ` +
|
|
322
|
+
`of the table, but the schema metadata for "${mm.table}" lists none. Regenerate the client ` +
|
|
323
|
+
'(`npx turbine generate`) so the table carries its columns, or drop the empty `OR`.');
|
|
324
|
+
}
|
|
325
|
+
return { [meta.reverseColumnMap?.[column] ?? column]: { in: [] } };
|
|
326
|
+
}
|
|
300
327
|
function relTargetModel(ctx, mm, turbineRel) {
|
|
301
328
|
const rd = ctx.schema.tables[mm.table]?.relations?.[turbineRel];
|
|
302
329
|
if (!rd)
|
|
@@ -637,8 +664,30 @@ function translateWhere(ctx, mm, where, depth = 0) {
|
|
|
637
664
|
if (!isPlainObject(where))
|
|
638
665
|
return where;
|
|
639
666
|
const out = {};
|
|
667
|
+
// The empty-`OR` sentinel, held aside rather than merged into `out`. At most
|
|
668
|
+
// one per level, since `OR` is a single object key.
|
|
669
|
+
let matchNone;
|
|
640
670
|
for (const [key, val] of Object.entries(where)) {
|
|
641
671
|
if (COMBINATORS.has(key)) {
|
|
672
|
+
// An EMPTY `OR` is false in Prisma (no branch can match), while an empty
|
|
673
|
+
// `AND` / `NOT` is true. Core drops an empty combinator entirely, so the
|
|
674
|
+
// same call returned EVERY row here: the classic trigger is
|
|
675
|
+
// `OR: ids.map(...)` over a selection that turned out to be empty, i.e.
|
|
676
|
+
// "none of these" answered with "all of them". Compiled to a predicate
|
|
677
|
+
// that matches nothing so this layer keeps Prisma's meaning; core's own
|
|
678
|
+
// semantics are unchanged and documented separately.
|
|
679
|
+
//
|
|
680
|
+
// Held aside, NOT `Object.assign`ed onto `out`: the sentinel is keyed on
|
|
681
|
+
// a real column of the table, so merging it made two predicates on one
|
|
682
|
+
// key collide and JavaScript's key ORDER decided which survived.
|
|
683
|
+
// `{ OR: [], id: 5 }` kept the caller's `id = 5` and dropped the
|
|
684
|
+
// sentinel (returning the row Prisma excludes); `{ id: 5, OR: [] }` kept
|
|
685
|
+
// the sentinel and dropped the caller's `id = 5`. Same query, same
|
|
686
|
+
// meaning, opposite predicate.
|
|
687
|
+
if (key === 'OR' && Array.isArray(val) && val.length === 0) {
|
|
688
|
+
matchNone = matchNothing(ctx, mm);
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
642
691
|
// An `AND` / `OR` array of N conditions is ONE level, not N: the elements
|
|
643
692
|
// are siblings, so they all translate at the same incremented depth.
|
|
644
693
|
out[key] = Array.isArray(val)
|
|
@@ -660,6 +709,17 @@ function translateWhere(ctx, mm, where, depth = 0) {
|
|
|
660
709
|
// Relation filter.
|
|
661
710
|
const rel = mm.relations[key];
|
|
662
711
|
if (rel) {
|
|
712
|
+
// Prisma spells "this to-one relation has no related row" as a bare
|
|
713
|
+
// `null` on the relation key; core spells it `{ is: null }` and answers a
|
|
714
|
+
// bare null with E003. The generic message then degraded into naming the
|
|
715
|
+
// key that was just passed ("Did you mean "user" (a relation)?"), because
|
|
716
|
+
// the adapter had already renamed it. To-many keys are NOT mapped: Prisma
|
|
717
|
+
// has no bare-null shape there, so core still refuses them by name rather
|
|
718
|
+
// than this layer inventing a meaning.
|
|
719
|
+
if (val === null && rel.cardinality === 'one') {
|
|
720
|
+
out[rel.name] = { is: null };
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
663
723
|
const target = relTargetModel(ctx, mm, rel.name);
|
|
664
724
|
out[rel.name] = translateRelationFilter(ctx, target, val, depth + 1);
|
|
665
725
|
continue;
|
|
@@ -668,7 +728,20 @@ function translateWhere(ctx, mm, where, depth = 0) {
|
|
|
668
728
|
// through unchanged (Prisma operator names match Turbine's).
|
|
669
729
|
out[renameField(mm, key)] = val;
|
|
670
730
|
}
|
|
671
|
-
|
|
731
|
+
if (!matchNone)
|
|
732
|
+
return out;
|
|
733
|
+
// Nothing else at this level: the sentinel IS the where, no wrapper needed.
|
|
734
|
+
if (Object.keys(out).length === 0)
|
|
735
|
+
return matchNone;
|
|
736
|
+
// Otherwise conjoin, so neither half can overwrite the other whatever order
|
|
737
|
+
// the caller wrote the keys in. BRANDED, because an `AND` array is the shape
|
|
738
|
+
// that makes a statement unnamed (a caller-written combinator has a
|
|
739
|
+
// caller-sized arity); this one is Turbine's, with a fixed arity of two, and
|
|
740
|
+
// an unbranded wrapper here would quietly take every compat query carrying an
|
|
741
|
+
// empty `OR` off named prepared statements. The caller's own combinators sit
|
|
742
|
+
// INSIDE `out` and are walked at the next level with their own brand check,
|
|
743
|
+
// so they still count.
|
|
744
|
+
return (0, utils_js_1.markInternalCombinator)({ AND: [out, matchNone] });
|
|
672
745
|
}
|
|
673
746
|
function translateRelationFilter(ctx, target, val, depth) {
|
|
674
747
|
if (!isPlainObject(val))
|
|
@@ -1198,15 +1271,29 @@ function translateCursor(ctx, mm, prismaArgs, t) {
|
|
|
1198
1271
|
const op = desc ? 'lte' : 'gte';
|
|
1199
1272
|
t.where = mergeKeyset(t.where ?? {}, field, op, value);
|
|
1200
1273
|
}
|
|
1201
|
-
/**
|
|
1274
|
+
/**
|
|
1275
|
+
* Flatten a Turbine orderBy (object, or an array of them) into [field, isDesc]
|
|
1276
|
+
* pairs.
|
|
1277
|
+
*
|
|
1278
|
+
* EVERY array element is read. Reading only an array of length one returned
|
|
1279
|
+
* `[]` for a two-element array, and `[]` is also what "no orderBy at all"
|
|
1280
|
+
* returns, so {@link translateCursor} took its no-orderBy branch: it found a
|
|
1281
|
+
* bare cursor on the primary key, wrote `orderBy = { id: 'asc' }` over the
|
|
1282
|
+
* caller's array, and returned the rows following the anchor in PK order
|
|
1283
|
+
* instead of in the caller's order. The single-sort-key check downstream is
|
|
1284
|
+
* what refuses a shape a bare (inclusive) cursor cannot express, and it can
|
|
1285
|
+
* only fire if the pairs it counts are all there.
|
|
1286
|
+
*/
|
|
1202
1287
|
function orderByPairs(ob) {
|
|
1203
|
-
const
|
|
1204
|
-
if (!isPlainObject(one))
|
|
1205
|
-
return [];
|
|
1288
|
+
const entries = Array.isArray(ob) ? ob : [ob];
|
|
1206
1289
|
const out = [];
|
|
1207
|
-
for (const
|
|
1208
|
-
|
|
1209
|
-
|
|
1290
|
+
for (const entry of entries) {
|
|
1291
|
+
if (!isPlainObject(entry))
|
|
1292
|
+
continue;
|
|
1293
|
+
for (const [k, v] of Object.entries(entry)) {
|
|
1294
|
+
const dir = isPlainObject(v) ? v.sort : v;
|
|
1295
|
+
out.push([k, dir === 'desc']);
|
|
1296
|
+
}
|
|
1210
1297
|
}
|
|
1211
1298
|
return out;
|
|
1212
1299
|
}
|
|
@@ -254,7 +254,7 @@ function buildGroupBy(qi, args) {
|
|
|
254
254
|
// would emit two "_sum_total_price" columns and silently drop one.
|
|
255
255
|
claimResultKey(`${aggKey}_${col}`, `${aggKey} of column "${col}"`);
|
|
256
256
|
const inner = `${sqlFn}(${qi.q(col)})`;
|
|
257
|
-
const expr = aggKey === '_avg' ? qi
|
|
257
|
+
const expr = aggKey === '_avg' ? plainAvgExpr(qi, col) : inner;
|
|
258
258
|
selectExprs.push(`${expr} AS ${qi.q(`${aggKey}_${col}`)}`);
|
|
259
259
|
// Canonical field, matching the result bucket the transform fills;
|
|
260
260
|
// `orderBy` may spell it either way (see {@link lookupGroupKey}).
|
|
@@ -365,13 +365,17 @@ function buildGroupBy(qi, args) {
|
|
|
365
365
|
// aggregates keep the snake→camel field mapping.
|
|
366
366
|
const jsonAgg = (rawKey) => jsonAggFields.get(rawKey);
|
|
367
367
|
const fieldFor = (rawKey, col) => jsonAgg(rawKey)?.field ?? qi.tableMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col);
|
|
368
|
+
// A JSON-path `_sum` / `_avg` casts its extracted text to numeric in
|
|
369
|
+
// SQL and is always a number; a plain column follows its source type
|
|
370
|
+
// (see {@link isExactNumericType}).
|
|
371
|
+
const sumAvg = (rawKey, rawValue) => jsonAgg(rawKey) ? (rawValue !== null ? Number(rawValue) : null) : sumAvgValue(qi, rawKey.slice(5), rawValue);
|
|
368
372
|
for (const [rawKey, rawValue] of Object.entries(row)) {
|
|
369
373
|
if (rawKey.startsWith('_sum_')) {
|
|
370
|
-
sumObj[fieldFor(rawKey, rawKey.slice(5))] =
|
|
374
|
+
sumObj[fieldFor(rawKey, rawKey.slice(5))] = sumAvg(rawKey, rawValue);
|
|
371
375
|
hasSums = true;
|
|
372
376
|
}
|
|
373
377
|
else if (rawKey.startsWith('_avg_')) {
|
|
374
|
-
avgObj[fieldFor(rawKey, rawKey.slice(5))] =
|
|
378
|
+
avgObj[fieldFor(rawKey, rawKey.slice(5))] = sumAvg(rawKey, rawValue);
|
|
375
379
|
hasAvgs = true;
|
|
376
380
|
}
|
|
377
381
|
else if (rawKey.startsWith('_min_')) {
|
|
@@ -926,6 +930,52 @@ function temporalAggValue(qi, col, value) {
|
|
|
926
930
|
return qi.utcTimestamps !== false ? (0, utils_js_1.parseDbDate)(value) : new Date(value);
|
|
927
931
|
return value;
|
|
928
932
|
}
|
|
933
|
+
/**
|
|
934
|
+
* Whether `SUM` / `AVG` over a column of `pgType` is EXACT on the wire and must
|
|
935
|
+
* not be narrowed to a JS number.
|
|
936
|
+
*
|
|
937
|
+
* PostgreSQL widens both past their input: over int8 they are `numeric`, over
|
|
938
|
+
* numeric they stay `numeric`. (An int2 / int4 sum is int8 and its average
|
|
939
|
+
* `numeric` too, but those totals fit a double, which is the line this draws.)
|
|
940
|
+
* The driver delivers `numeric` as its exact text, because no parser is
|
|
941
|
+
* registered for it, deliberately: the type is arbitrary-precision. `Number()`
|
|
942
|
+
* over that text rounded a `SUM(int8)` of 461168601842738790350 to
|
|
943
|
+
* 461168601842738800000 and a numeric(12,2) total of 1020.50 to 1020.5, while
|
|
944
|
+
* `_min` / `_max` on the very same columns came back exact, because they hand
|
|
945
|
+
* the driver's value through untouched.
|
|
946
|
+
*
|
|
947
|
+
* So for these source types the aggregate is returned as the driver delivered
|
|
948
|
+
* it (on PostgreSQL the text, a string; an engine whose driver already hands
|
|
949
|
+
* back a number keeps that number) and `_avg` is not cast to float in SQL,
|
|
950
|
+
* which would otherwise round on the server before the value reached the
|
|
951
|
+
* wire. Every other type keeps `Number()` and the cast. Spelled for every
|
|
952
|
+
* engine's type names: the PostgreSQL `udt_name` (`int8`, `numeric`) and the
|
|
953
|
+
* SQL names the other dialects report (`bigint`, `decimal`), with case and any
|
|
954
|
+
* `(precision, scale)` suffix ignored. JSON-path aggregates never reach this:
|
|
955
|
+
* they cast the extracted text to numeric themselves and stay numbers.
|
|
956
|
+
*/
|
|
957
|
+
function isExactNumericType(pgType) {
|
|
958
|
+
const paren = pgType.indexOf('(');
|
|
959
|
+
const base = (paren === -1 ? pgType : pgType.slice(0, paren)).trim().toLowerCase();
|
|
960
|
+
return base === 'int8' || base === 'bigint' || base === 'numeric' || base === 'decimal';
|
|
961
|
+
}
|
|
962
|
+
/** `AVG(col)`, float-cast unless the source column is exact (see {@link isExactNumericType}). */
|
|
963
|
+
function plainAvgExpr(qi, col) {
|
|
964
|
+
const inner = `AVG(${qi.q(col)})`;
|
|
965
|
+
return isExactNumericType(whereMod.getColumnPgType(qi, col)) ? inner : qi.castAgg(inner, 'float');
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* A `_sum` / `_avg` value over a plain column: the driver's value verbatim
|
|
969
|
+
* for an exact source type (see {@link isExactNumericType}), a JS number for
|
|
970
|
+
* every other, `null` for an aggregate over zero rows.
|
|
971
|
+
*/
|
|
972
|
+
function sumAvgValue(qi, col, value) {
|
|
973
|
+
if (value === null || value === undefined)
|
|
974
|
+
return null;
|
|
975
|
+
if (isExactNumericType(whereMod.getColumnPgType(qi, col)))
|
|
976
|
+
return value;
|
|
977
|
+
return Number(value);
|
|
978
|
+
}
|
|
929
979
|
function buildAggregate(qi, args) {
|
|
930
980
|
qi.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
|
|
931
981
|
// Resolved once, up front: see buildGroupBy.
|
|
@@ -987,7 +1037,7 @@ function buildAggregate(qi, args) {
|
|
|
987
1037
|
for (const [field, enabled] of Object.entries(args._avg)) {
|
|
988
1038
|
if (enabled) {
|
|
989
1039
|
const col = qi.toColumn(field);
|
|
990
|
-
selectExprs.push(`${
|
|
1040
|
+
selectExprs.push(`${plainAvgExpr(qi, col)} AS ${qi.q(`_avg_${col}`)}`);
|
|
991
1041
|
}
|
|
992
1042
|
}
|
|
993
1043
|
}
|
|
@@ -1057,13 +1107,13 @@ function buildAggregate(qi, args) {
|
|
|
1057
1107
|
if (key.startsWith('_sum_')) {
|
|
1058
1108
|
const col = key.slice(5);
|
|
1059
1109
|
const field = qi.tableMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col);
|
|
1060
|
-
sumObj[field] =
|
|
1110
|
+
sumObj[field] = sumAvgValue(qi, col, val);
|
|
1061
1111
|
hasSums = true;
|
|
1062
1112
|
}
|
|
1063
1113
|
else if (key.startsWith('_avg_')) {
|
|
1064
1114
|
const col = key.slice(5);
|
|
1065
1115
|
const field = qi.tableMeta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col);
|
|
1066
|
-
avgObj[field] =
|
|
1116
|
+
avgObj[field] = sumAvgValue(qi, col, val);
|
|
1067
1117
|
hasAvgs = true;
|
|
1068
1118
|
}
|
|
1069
1119
|
else if (key.startsWith('_min_')) {
|
|
@@ -589,32 +589,80 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
589
589
|
private pkOrderBy;
|
|
590
590
|
/**
|
|
591
591
|
* The field names a `cursor` actually seeks on (its own keys with a defined
|
|
592
|
-
* value), in
|
|
593
|
-
*
|
|
594
|
-
*
|
|
592
|
+
* value), in canonical sorted order. This is the diagnostic / implicit-
|
|
593
|
+
* ordering view; the seek itself orders the fields by their `orderBy`
|
|
594
|
+
* precedence (see {@link cursorSeekEntries}). Empty for a missing cursor or
|
|
595
|
+
* one whose every value is `undefined` (which emits no seek condition at
|
|
596
|
+
* all, so it does not paginate).
|
|
595
597
|
*/
|
|
596
598
|
private cursorFields;
|
|
597
599
|
/**
|
|
598
600
|
* The ascending ordering implied by a `cursor`, or `undefined` when the shape
|
|
599
601
|
* is too ambiguous to order safely.
|
|
600
602
|
*
|
|
601
|
-
* A cursor seek
|
|
602
|
-
* so the ONLY ordering coherent with it is on
|
|
603
|
-
* a seek on column X by column Y walks the table in an
|
|
604
|
-
* follow, which skips and repeats rows just as badly
|
|
605
|
-
* is why this orders on the cursor field rather than
|
|
606
|
-
* key when the two differ.
|
|
603
|
+
* A cursor seek compares against the cursor's own field(s) (`col > $n`, `<`
|
|
604
|
+
* when the orderBy says desc), so the ONLY ordering coherent with it is on
|
|
605
|
+
* those fields: ordering a seek on column X by column Y walks the table in an
|
|
606
|
+
* order the seek does not follow, which skips and repeats rows just as badly
|
|
607
|
+
* as no order at all. That is why this orders on the cursor field rather than
|
|
608
|
+
* blindly on the primary key when the two differ.
|
|
607
609
|
*
|
|
608
610
|
* Returns `undefined` (warn, inject nothing) for two shapes:
|
|
609
|
-
* - a MULTI-field cursor.
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
*
|
|
611
|
+
* - a MULTI-field cursor. The seek is a proper keyset predicate, but its
|
|
612
|
+
* column PRECEDENCE comes from the `orderBy` ({@link cursorSeekEntries}),
|
|
613
|
+
* and with no orderBy there is nothing to derive it from: the cursor
|
|
614
|
+
* object's key order is canonicalized away, so `(a asc, b asc)` would be a
|
|
615
|
+
* guess at which key the caller meant to lead. The caller must state the
|
|
616
|
+
* order, and the unordered-page warning says so.
|
|
613
617
|
* - a field that does not resolve to a real column. Column validation belongs
|
|
614
618
|
* to the normal build path, which raises a precise error; synthesizing an
|
|
615
619
|
* ORDER BY on it here would only change which error the caller sees.
|
|
616
620
|
*/
|
|
617
621
|
private cursorOrderBy;
|
|
622
|
+
/**
|
|
623
|
+
* The fields a `cursor` seeks on, in KEYSET order with their seek direction.
|
|
624
|
+
* THE single authority the cache fingerprint (`cur=`), the SQL build and the
|
|
625
|
+
* cache-hit param collect all consume, so the three cannot disagree about
|
|
626
|
+
* which value binds to which `$n`.
|
|
627
|
+
*
|
|
628
|
+
* Keyset order is the `orderBy` precedence: the predicate for
|
|
629
|
+
* `orderBy: [{ viewCount }, { id }]` must test `view_count` first and `id`
|
|
630
|
+
* only within equal `view_count`, whatever order the caller wrote the cursor
|
|
631
|
+
* object in (its keys are canonicalized for the cache anyway). A cursor field
|
|
632
|
+
* the orderBy does not name trails the named ones in sorted key order and
|
|
633
|
+
* seeks ascending, the same default a single-field cursor with no orderBy has
|
|
634
|
+
* always had.
|
|
635
|
+
*
|
|
636
|
+
* Directions are indexed by the RESOLVED COLUMN, never the caller's key: both
|
|
637
|
+
* `cursor` and `orderBy` take either spelling, so a cursor written
|
|
638
|
+
* `{ created_at }` against `orderBy: { createdAt: 'desc' }` used to miss the
|
|
639
|
+
* lookup, default to ascending, and seek the wrong side of the page. The
|
|
640
|
+
* `{ sort, nulls }` spec form is normalized for the same reason. A relation /
|
|
641
|
+
* JSON-path / vector orderBy key resolves to no column and is skipped.
|
|
642
|
+
*
|
|
643
|
+
* An unknown cursor field throws the same E003 here that the build path
|
|
644
|
+
* threw before this helper existed; the fingerprint simply meets it first.
|
|
645
|
+
*/
|
|
646
|
+
private cursorSeekEntries;
|
|
647
|
+
/**
|
|
648
|
+
* The keyset predicate for cursor terms in keyset order.
|
|
649
|
+
*
|
|
650
|
+
* One field is the plain `col > $1` (byte-identical to every single-field
|
|
651
|
+
* cursor ever emitted). Two or more expand to
|
|
652
|
+
* `(c1 > $1 OR (c1 = $1 AND c2 > $2) OR (c1 = $1 AND c2 = $2 AND c3 > $3))`,
|
|
653
|
+
* with `<` for a `desc` field: the row-value form `(c1, c2) > ($1, $2)` is
|
|
654
|
+
* shorter, but it cannot express a MIXED direction at all and SQL Server has
|
|
655
|
+
* no row-value comparison, so the expanded form is the one path every engine
|
|
656
|
+
* and every direction set share. Each value is bound ONCE and its `$n` is
|
|
657
|
+
* referenced by every branch that needs it (legal on every supported
|
|
658
|
+
* placeholder syntax), which is what keeps the param count equal to the field
|
|
659
|
+
* count on both the build and the collect side.
|
|
660
|
+
*
|
|
661
|
+
* The former `c1 > $1 AND c2 > $2` was a conjunction, not a seek: it skipped
|
|
662
|
+
* every row whose leading key EQUALED the cursor's, so a table with few
|
|
663
|
+
* distinct leading values lost most of its rows across a page walk, silently.
|
|
664
|
+
*/
|
|
665
|
+
private keysetPredicate;
|
|
618
666
|
/**
|
|
619
667
|
* Whether a findMany paginates (`limit` / `take` / `offset` / `cursor`) but
|
|
620
668
|
* declares no ordering, which makes the returned page NON-DETERMINISTIC:
|
|
@@ -1307,6 +1355,21 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
1307
1355
|
private toColumn;
|
|
1308
1356
|
/** Convert camelCase field name to a double-quoted SQL identifier */
|
|
1309
1357
|
private toSqlColumn;
|
|
1358
|
+
/**
|
|
1359
|
+
* The explicit outer SELECT list of the `distinct` + `orderBy` wrapper, used
|
|
1360
|
+
* only when the inner derived table had to carry ORDER BY columns the
|
|
1361
|
+
* projection leaves out (see the distinct branch of {@link buildFindMany}):
|
|
1362
|
+
* the projected base columns, then one column per `with` relation, then one
|
|
1363
|
+
* per counted relation. These are the aliases `buildSelectWithRelations`
|
|
1364
|
+
* (relations.ts) gives the same columns, `AS "<relName>"` per entry in sorted
|
|
1365
|
+
* order and `AS "_count__<rel>"` per counted relation, over the SAME
|
|
1366
|
+
* `resolveCountRelations` it uses, so the two lists name the same columns.
|
|
1367
|
+
* `flatten` never reaches here (the strategy is gated off under `distinct`),
|
|
1368
|
+
* so no prefixed scalar aliases exist to enumerate. A drift between the two
|
|
1369
|
+
* would fail LOUDLY, as a 42703 on the outer list, never as a silently wrong
|
|
1370
|
+
* row, and the live distinct test pins the `with` + `_count` shape.
|
|
1371
|
+
*/
|
|
1372
|
+
private distinctOuterSelectList;
|
|
1310
1373
|
fingerprintWhere(where: Record<string, unknown>): string;
|
|
1311
1374
|
collectWhereParams(where: Record<string, unknown>, params: unknown[]): void;
|
|
1312
1375
|
private resolveGlobalFilter;
|