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/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/powql.js
CHANGED
|
@@ -39,7 +39,7 @@ import { NotFoundError, OptimisticLockError, ReadOnlyError, TimeoutError, Unsupp
|
|
|
39
39
|
import { executeNestedCreate, executeNestedUpdate, hasRelationFields, } from './nested-write.js';
|
|
40
40
|
import { ALL_POWDB_CAPABILITIES, baseTsType, coerceNativeValue, isJsonColumn, isPowdbDatetimeColumn, isStaleFramePowdbError, PowdbFloatParam, PowdbJsonParam, powqlColumnType, quotePowqlDotted, quotePowqlIdent, requireCapability, rowToEntity, } from './powdb-shared.js';
|
|
41
41
|
import { assertAggregatePiiOptIn } from './query/aggregates.js';
|
|
42
|
-
import { assertWhereIdentifiesOneRow, expandCompoundUniqueWhere } from './query/compound-unique.js';
|
|
42
|
+
import { assertMutationWhereIdentifiesOneRow, assertWhereIdentifiesOneRow, expandCompoundUniqueWhere, } from './query/compound-unique.js';
|
|
43
43
|
import { ARRAY_OPERATOR_KEYS, isJsonFilter, isRelationPickOrderBy, orderByEntries } from './query/filters.js';
|
|
44
44
|
import { warnUnknownQueryOptions } from './query/option-surface.js';
|
|
45
45
|
import { applyStableRelationOrderTo, normalizeWithClause } from './query/relation-names.js';
|
|
@@ -446,6 +446,28 @@ export class PowqlInterface {
|
|
|
446
446
|
assertIdentifiesOneRow(where) {
|
|
447
447
|
assertWhereIdentifiesOneRow(this.meta, this.table, where);
|
|
448
448
|
}
|
|
449
|
+
/**
|
|
450
|
+
* The single-row WRITE rule (`update` / `delete` return one row, so their
|
|
451
|
+
* `where` must identify one; `upsert`'s `where` names the row it conflicts
|
|
452
|
+
* on), shared with the SQL engines through query/compound-unique.ts so the
|
|
453
|
+
* two cannot disagree about which writes are valid. Skipped under the
|
|
454
|
+
* explicit full-table opt-in, which already means "every row". For `update` /
|
|
455
|
+
* `delete` it runs AFTER `assertCompiledWhere`, so an empty selector keeps
|
|
456
|
+
* the empty-where message and this only refuses a non-empty one that names no
|
|
457
|
+
* key; `upsert` takes no `allowFullTableScan` and has no empty-where guard to
|
|
458
|
+
* run first, so this is its only where check and it covers `{}` as well.
|
|
459
|
+
*/
|
|
460
|
+
assertMutationIdentifiesOneRow(where, allowFullTableScan, operation) {
|
|
461
|
+
if (allowFullTableScan)
|
|
462
|
+
return;
|
|
463
|
+
assertMutationWhereIdentifiesOneRow(this.meta, this.table, where, operation);
|
|
464
|
+
}
|
|
465
|
+
/** A caller `where` with a Prisma compound-unique selector expanded to its member columns. */
|
|
466
|
+
expandedWhere(where) {
|
|
467
|
+
if (!where)
|
|
468
|
+
return where;
|
|
469
|
+
return expandCompoundUniqueWhere(this.meta, where);
|
|
470
|
+
}
|
|
449
471
|
assertPagination(limit, offset, context) {
|
|
450
472
|
for (const [name, value] of [
|
|
451
473
|
['limit', limit],
|
|
@@ -2665,14 +2687,22 @@ export class PowqlInterface {
|
|
|
2665
2687
|
async update(args) {
|
|
2666
2688
|
return this.withMiddleware('update', args, async () => {
|
|
2667
2689
|
if (hasRelationFields(args.data, this.meta)) {
|
|
2690
|
+
// The nested engine re-enters `update` / `findUnique` for the parent
|
|
2691
|
+
// row before it writes anything, so the identity rule below still runs
|
|
2692
|
+
// first on this path.
|
|
2668
2693
|
return this.nestedUpdate(args);
|
|
2669
2694
|
}
|
|
2695
|
+
const allowFullTableScan = resolveUnsafeFlag(args.allowFullTableScan, 'allowFullTableScan');
|
|
2696
|
+
// Prisma compound-unique selector → column conjunction, so the selector
|
|
2697
|
+
// counts as the key it is (engine parity with the SQL buildUpdate).
|
|
2698
|
+
const userWhere = this.expandedWhere(args.where);
|
|
2670
2699
|
const params = [];
|
|
2671
|
-
const resolvedWhere = await this.resolveRelationFilters(
|
|
2700
|
+
const resolvedWhere = await this.resolveRelationFilters(userWhere, args.timeout);
|
|
2672
2701
|
let where = this.buildWhere(resolvedWhere, params);
|
|
2673
2702
|
// `false` here refused an empty where even WITH the opt-in, while every
|
|
2674
2703
|
// SQL engine accepted it: verified by probe on buildUpdate/buildDelete.
|
|
2675
|
-
this.assertCompiledWhere(where,
|
|
2704
|
+
this.assertCompiledWhere(where, allowFullTableScan, 'update');
|
|
2705
|
+
this.assertMutationIdentifiesOneRow(userWhere, allowFullTableScan, 'update');
|
|
2676
2706
|
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters);
|
|
2677
2707
|
let setClause = this.buildUpdateAssignments(args.data, params);
|
|
2678
2708
|
// Optimistic locking, matching the SQL engines exactly: bump the version
|
|
@@ -2846,10 +2876,13 @@ export class PowqlInterface {
|
|
|
2846
2876
|
}
|
|
2847
2877
|
async delete(args) {
|
|
2848
2878
|
return this.withMiddleware('delete', args, async () => {
|
|
2879
|
+
const allowFullTableScan = resolveUnsafeFlag(args.allowFullTableScan, 'allowFullTableScan');
|
|
2880
|
+
const userWhere = this.expandedWhere(args.where);
|
|
2849
2881
|
const params = [];
|
|
2850
|
-
const resolvedWhere = await this.resolveRelationFilters(
|
|
2882
|
+
const resolvedWhere = await this.resolveRelationFilters(userWhere, args.timeout);
|
|
2851
2883
|
let where = this.buildWhere(resolvedWhere, params);
|
|
2852
|
-
this.assertCompiledWhere(where,
|
|
2884
|
+
this.assertCompiledWhere(where, allowFullTableScan, 'delete');
|
|
2885
|
+
this.assertMutationIdentifiesOneRow(userWhere, allowFullTableScan, 'delete');
|
|
2853
2886
|
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters);
|
|
2854
2887
|
// `returning` hands back the deleted row(s), no separate pre-image reselect needed.
|
|
2855
2888
|
const { rows, native } = await this.exec(`${this.qt} filter ${where} delete returning`, params, args.timeout, 'delete');
|
|
@@ -2873,6 +2906,13 @@ export class PowqlInterface {
|
|
|
2873
2906
|
}
|
|
2874
2907
|
async upsert(args) {
|
|
2875
2908
|
return this.withMiddleware('upsert', args, async () => {
|
|
2909
|
+
// The single-row rule for `upsert`, shared with the SQL engines through
|
|
2910
|
+
// query/compound-unique.ts. PowqlInterface is a PARALLEL implementation,
|
|
2911
|
+
// not a subclass, so a rule adopted only on the SQL side is two engines
|
|
2912
|
+
// disagreeing about whether a query is VALID, which is the failure class
|
|
2913
|
+
// 0.64.0 and 0.72.0 were both spent on. Before `applyPkDefault`, so a
|
|
2914
|
+
// refused upsert mints no client-side UUID and sends nothing.
|
|
2915
|
+
this.assertMutationIdentifiesOneRow(this.expandedWhere(args.where), false, 'upsert');
|
|
2876
2916
|
const createData = this.applyPkDefault(args.create);
|
|
2877
2917
|
const pkCol = this.meta.primaryKey[0];
|
|
2878
2918
|
if (this.meta.primaryKey.length !== 1 || !pkCol) {
|
package/dist/prisma-compat.js
CHANGED
|
@@ -123,7 +123,7 @@ import { TurbineError, TurbineErrorCode, UnsupportedFeatureError, ValidationErro
|
|
|
123
123
|
import { createManyShapeRuns } from './nested-write.js';
|
|
124
124
|
import { fingerprintPrismaSchema } from './prisma-schema-fingerprint.js';
|
|
125
125
|
import { AGGREGATE_OPTIONS, applyNativeOptions, COUNT_OPTIONS, CREATE_MANY_OPTIONS, CREATE_OPTIONS, DELETE_MANY_OPTIONS, DELETE_OPTIONS, FIND_MANY_OPTIONS, FIND_UNIQUE_OPTIONS, GROUP_BY_OPTIONS, optionKeysOfKind, UNSAFE, UPDATE_MANY_OPTIONS, UPDATE_OPTIONS, UPSERT_OPTIONS, } from './query/index.js';
|
|
126
|
-
import { suggestKey } from './query/utils.js';
|
|
126
|
+
import { markInternalCombinator, suggestKey } from './query/utils.js';
|
|
127
127
|
import { shouldWarnOnce, WARN_NS } from './query/warn-registry.js';
|
|
128
128
|
import { assertWhereDepth, MAX_WHERE_DEPTH } from './query/where-compile.js';
|
|
129
129
|
// ---------------------------------------------------------------------------
|
|
@@ -260,6 +260,33 @@ function lookupsFor(ctx, mm) {
|
|
|
260
260
|
return l;
|
|
261
261
|
}
|
|
262
262
|
/** Resolve a turbine relation's target Prisma model map (for nested translation). */
|
|
263
|
+
/**
|
|
264
|
+
* A `where` fragment no row satisfies, for Prisma's empty `OR`.
|
|
265
|
+
*
|
|
266
|
+
* `{ <key>: { in: [] } }`, which core compiles to `= ANY($n)` with an empty
|
|
267
|
+
* array: false for every row, NULL-valued ones included, and one ordinary bound
|
|
268
|
+
* param rather than a literal spliced into the SQL. The column is the first
|
|
269
|
+
* primary-key column, else the table's first column, so it is always a real
|
|
270
|
+
* column of this table and the predicate stays a plain scalar comparison the
|
|
271
|
+
* cache fingerprints like any other.
|
|
272
|
+
*
|
|
273
|
+
* A table this layer cannot name a column of THROWS. The fallback used to be
|
|
274
|
+
* `{}`, an empty `where` fragment - which is not "no rows" but its exact
|
|
275
|
+
* opposite, EVERY row, and it would have been merged in silently beside the
|
|
276
|
+
* caller's other predicates. A sentinel builder that cannot build a sentinel
|
|
277
|
+
* has not succeeded, and the one shape it exists to compile is the one where
|
|
278
|
+
* answering "all of them" instead of "none of them" is worst.
|
|
279
|
+
*/
|
|
280
|
+
function matchNothing(ctx, mm) {
|
|
281
|
+
const meta = ctx.schema.tables[mm.table];
|
|
282
|
+
const column = meta?.primaryKey?.[0] ?? meta?.allColumns?.[0];
|
|
283
|
+
if (!meta || !column) {
|
|
284
|
+
throw new ValidationError(`An empty \`OR\` on model ${mm.table} means "no rows", and compiling that needs one real column ` +
|
|
285
|
+
`of the table, but the schema metadata for "${mm.table}" lists none. Regenerate the client ` +
|
|
286
|
+
'(`npx turbine generate`) so the table carries its columns, or drop the empty `OR`.');
|
|
287
|
+
}
|
|
288
|
+
return { [meta.reverseColumnMap?.[column] ?? column]: { in: [] } };
|
|
289
|
+
}
|
|
263
290
|
function relTargetModel(ctx, mm, turbineRel) {
|
|
264
291
|
const rd = ctx.schema.tables[mm.table]?.relations?.[turbineRel];
|
|
265
292
|
if (!rd)
|
|
@@ -600,8 +627,30 @@ function translateWhere(ctx, mm, where, depth = 0) {
|
|
|
600
627
|
if (!isPlainObject(where))
|
|
601
628
|
return where;
|
|
602
629
|
const out = {};
|
|
630
|
+
// The empty-`OR` sentinel, held aside rather than merged into `out`. At most
|
|
631
|
+
// one per level, since `OR` is a single object key.
|
|
632
|
+
let matchNone;
|
|
603
633
|
for (const [key, val] of Object.entries(where)) {
|
|
604
634
|
if (COMBINATORS.has(key)) {
|
|
635
|
+
// An EMPTY `OR` is false in Prisma (no branch can match), while an empty
|
|
636
|
+
// `AND` / `NOT` is true. Core drops an empty combinator entirely, so the
|
|
637
|
+
// same call returned EVERY row here: the classic trigger is
|
|
638
|
+
// `OR: ids.map(...)` over a selection that turned out to be empty, i.e.
|
|
639
|
+
// "none of these" answered with "all of them". Compiled to a predicate
|
|
640
|
+
// that matches nothing so this layer keeps Prisma's meaning; core's own
|
|
641
|
+
// semantics are unchanged and documented separately.
|
|
642
|
+
//
|
|
643
|
+
// Held aside, NOT `Object.assign`ed onto `out`: the sentinel is keyed on
|
|
644
|
+
// a real column of the table, so merging it made two predicates on one
|
|
645
|
+
// key collide and JavaScript's key ORDER decided which survived.
|
|
646
|
+
// `{ OR: [], id: 5 }` kept the caller's `id = 5` and dropped the
|
|
647
|
+
// sentinel (returning the row Prisma excludes); `{ id: 5, OR: [] }` kept
|
|
648
|
+
// the sentinel and dropped the caller's `id = 5`. Same query, same
|
|
649
|
+
// meaning, opposite predicate.
|
|
650
|
+
if (key === 'OR' && Array.isArray(val) && val.length === 0) {
|
|
651
|
+
matchNone = matchNothing(ctx, mm);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
605
654
|
// An `AND` / `OR` array of N conditions is ONE level, not N: the elements
|
|
606
655
|
// are siblings, so they all translate at the same incremented depth.
|
|
607
656
|
out[key] = Array.isArray(val)
|
|
@@ -623,6 +672,17 @@ function translateWhere(ctx, mm, where, depth = 0) {
|
|
|
623
672
|
// Relation filter.
|
|
624
673
|
const rel = mm.relations[key];
|
|
625
674
|
if (rel) {
|
|
675
|
+
// Prisma spells "this to-one relation has no related row" as a bare
|
|
676
|
+
// `null` on the relation key; core spells it `{ is: null }` and answers a
|
|
677
|
+
// bare null with E003. The generic message then degraded into naming the
|
|
678
|
+
// key that was just passed ("Did you mean "user" (a relation)?"), because
|
|
679
|
+
// the adapter had already renamed it. To-many keys are NOT mapped: Prisma
|
|
680
|
+
// has no bare-null shape there, so core still refuses them by name rather
|
|
681
|
+
// than this layer inventing a meaning.
|
|
682
|
+
if (val === null && rel.cardinality === 'one') {
|
|
683
|
+
out[rel.name] = { is: null };
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
626
686
|
const target = relTargetModel(ctx, mm, rel.name);
|
|
627
687
|
out[rel.name] = translateRelationFilter(ctx, target, val, depth + 1);
|
|
628
688
|
continue;
|
|
@@ -631,7 +691,20 @@ function translateWhere(ctx, mm, where, depth = 0) {
|
|
|
631
691
|
// through unchanged (Prisma operator names match Turbine's).
|
|
632
692
|
out[renameField(mm, key)] = val;
|
|
633
693
|
}
|
|
634
|
-
|
|
694
|
+
if (!matchNone)
|
|
695
|
+
return out;
|
|
696
|
+
// Nothing else at this level: the sentinel IS the where, no wrapper needed.
|
|
697
|
+
if (Object.keys(out).length === 0)
|
|
698
|
+
return matchNone;
|
|
699
|
+
// Otherwise conjoin, so neither half can overwrite the other whatever order
|
|
700
|
+
// the caller wrote the keys in. BRANDED, because an `AND` array is the shape
|
|
701
|
+
// that makes a statement unnamed (a caller-written combinator has a
|
|
702
|
+
// caller-sized arity); this one is Turbine's, with a fixed arity of two, and
|
|
703
|
+
// an unbranded wrapper here would quietly take every compat query carrying an
|
|
704
|
+
// empty `OR` off named prepared statements. The caller's own combinators sit
|
|
705
|
+
// INSIDE `out` and are walked at the next level with their own brand check,
|
|
706
|
+
// so they still count.
|
|
707
|
+
return markInternalCombinator({ AND: [out, matchNone] });
|
|
635
708
|
}
|
|
636
709
|
function translateRelationFilter(ctx, target, val, depth) {
|
|
637
710
|
if (!isPlainObject(val))
|
|
@@ -1161,15 +1234,29 @@ function translateCursor(ctx, mm, prismaArgs, t) {
|
|
|
1161
1234
|
const op = desc ? 'lte' : 'gte';
|
|
1162
1235
|
t.where = mergeKeyset(t.where ?? {}, field, op, value);
|
|
1163
1236
|
}
|
|
1164
|
-
/**
|
|
1237
|
+
/**
|
|
1238
|
+
* Flatten a Turbine orderBy (object, or an array of them) into [field, isDesc]
|
|
1239
|
+
* pairs.
|
|
1240
|
+
*
|
|
1241
|
+
* EVERY array element is read. Reading only an array of length one returned
|
|
1242
|
+
* `[]` for a two-element array, and `[]` is also what "no orderBy at all"
|
|
1243
|
+
* returns, so {@link translateCursor} took its no-orderBy branch: it found a
|
|
1244
|
+
* bare cursor on the primary key, wrote `orderBy = { id: 'asc' }` over the
|
|
1245
|
+
* caller's array, and returned the rows following the anchor in PK order
|
|
1246
|
+
* instead of in the caller's order. The single-sort-key check downstream is
|
|
1247
|
+
* what refuses a shape a bare (inclusive) cursor cannot express, and it can
|
|
1248
|
+
* only fire if the pairs it counts are all there.
|
|
1249
|
+
*/
|
|
1165
1250
|
function orderByPairs(ob) {
|
|
1166
|
-
const
|
|
1167
|
-
if (!isPlainObject(one))
|
|
1168
|
-
return [];
|
|
1251
|
+
const entries = Array.isArray(ob) ? ob : [ob];
|
|
1169
1252
|
const out = [];
|
|
1170
|
-
for (const
|
|
1171
|
-
|
|
1172
|
-
|
|
1253
|
+
for (const entry of entries) {
|
|
1254
|
+
if (!isPlainObject(entry))
|
|
1255
|
+
continue;
|
|
1256
|
+
for (const [k, v] of Object.entries(entry)) {
|
|
1257
|
+
const dir = isPlainObject(v) ? v.sort : v;
|
|
1258
|
+
out.push([k, dir === 'desc']);
|
|
1259
|
+
}
|
|
1173
1260
|
}
|
|
1174
1261
|
return out;
|
|
1175
1262
|
}
|
package/dist/query/aggregates.js
CHANGED
|
@@ -211,7 +211,7 @@ export function buildGroupBy(qi, args) {
|
|
|
211
211
|
// would emit two "_sum_total_price" columns and silently drop one.
|
|
212
212
|
claimResultKey(`${aggKey}_${col}`, `${aggKey} of column "${col}"`);
|
|
213
213
|
const inner = `${sqlFn}(${qi.q(col)})`;
|
|
214
|
-
const expr = aggKey === '_avg' ? qi
|
|
214
|
+
const expr = aggKey === '_avg' ? plainAvgExpr(qi, col) : inner;
|
|
215
215
|
selectExprs.push(`${expr} AS ${qi.q(`${aggKey}_${col}`)}`);
|
|
216
216
|
// Canonical field, matching the result bucket the transform fills;
|
|
217
217
|
// `orderBy` may spell it either way (see {@link lookupGroupKey}).
|
|
@@ -322,13 +322,17 @@ export function buildGroupBy(qi, args) {
|
|
|
322
322
|
// aggregates keep the snake→camel field mapping.
|
|
323
323
|
const jsonAgg = (rawKey) => jsonAggFields.get(rawKey);
|
|
324
324
|
const fieldFor = (rawKey, col) => jsonAgg(rawKey)?.field ?? qi.tableMeta.reverseColumnMap[col] ?? snakeToCamel(col);
|
|
325
|
+
// A JSON-path `_sum` / `_avg` casts its extracted text to numeric in
|
|
326
|
+
// SQL and is always a number; a plain column follows its source type
|
|
327
|
+
// (see {@link isExactNumericType}).
|
|
328
|
+
const sumAvg = (rawKey, rawValue) => jsonAgg(rawKey) ? (rawValue !== null ? Number(rawValue) : null) : sumAvgValue(qi, rawKey.slice(5), rawValue);
|
|
325
329
|
for (const [rawKey, rawValue] of Object.entries(row)) {
|
|
326
330
|
if (rawKey.startsWith('_sum_')) {
|
|
327
|
-
sumObj[fieldFor(rawKey, rawKey.slice(5))] =
|
|
331
|
+
sumObj[fieldFor(rawKey, rawKey.slice(5))] = sumAvg(rawKey, rawValue);
|
|
328
332
|
hasSums = true;
|
|
329
333
|
}
|
|
330
334
|
else if (rawKey.startsWith('_avg_')) {
|
|
331
|
-
avgObj[fieldFor(rawKey, rawKey.slice(5))] =
|
|
335
|
+
avgObj[fieldFor(rawKey, rawKey.slice(5))] = sumAvg(rawKey, rawValue);
|
|
332
336
|
hasAvgs = true;
|
|
333
337
|
}
|
|
334
338
|
else if (rawKey.startsWith('_min_')) {
|
|
@@ -883,6 +887,52 @@ function temporalAggValue(qi, col, value) {
|
|
|
883
887
|
return qi.utcTimestamps !== false ? parseDbDate(value) : new Date(value);
|
|
884
888
|
return value;
|
|
885
889
|
}
|
|
890
|
+
/**
|
|
891
|
+
* Whether `SUM` / `AVG` over a column of `pgType` is EXACT on the wire and must
|
|
892
|
+
* not be narrowed to a JS number.
|
|
893
|
+
*
|
|
894
|
+
* PostgreSQL widens both past their input: over int8 they are `numeric`, over
|
|
895
|
+
* numeric they stay `numeric`. (An int2 / int4 sum is int8 and its average
|
|
896
|
+
* `numeric` too, but those totals fit a double, which is the line this draws.)
|
|
897
|
+
* The driver delivers `numeric` as its exact text, because no parser is
|
|
898
|
+
* registered for it, deliberately: the type is arbitrary-precision. `Number()`
|
|
899
|
+
* over that text rounded a `SUM(int8)` of 461168601842738790350 to
|
|
900
|
+
* 461168601842738800000 and a numeric(12,2) total of 1020.50 to 1020.5, while
|
|
901
|
+
* `_min` / `_max` on the very same columns came back exact, because they hand
|
|
902
|
+
* the driver's value through untouched.
|
|
903
|
+
*
|
|
904
|
+
* So for these source types the aggregate is returned as the driver delivered
|
|
905
|
+
* it (on PostgreSQL the text, a string; an engine whose driver already hands
|
|
906
|
+
* back a number keeps that number) and `_avg` is not cast to float in SQL,
|
|
907
|
+
* which would otherwise round on the server before the value reached the
|
|
908
|
+
* wire. Every other type keeps `Number()` and the cast. Spelled for every
|
|
909
|
+
* engine's type names: the PostgreSQL `udt_name` (`int8`, `numeric`) and the
|
|
910
|
+
* SQL names the other dialects report (`bigint`, `decimal`), with case and any
|
|
911
|
+
* `(precision, scale)` suffix ignored. JSON-path aggregates never reach this:
|
|
912
|
+
* they cast the extracted text to numeric themselves and stay numbers.
|
|
913
|
+
*/
|
|
914
|
+
function isExactNumericType(pgType) {
|
|
915
|
+
const paren = pgType.indexOf('(');
|
|
916
|
+
const base = (paren === -1 ? pgType : pgType.slice(0, paren)).trim().toLowerCase();
|
|
917
|
+
return base === 'int8' || base === 'bigint' || base === 'numeric' || base === 'decimal';
|
|
918
|
+
}
|
|
919
|
+
/** `AVG(col)`, float-cast unless the source column is exact (see {@link isExactNumericType}). */
|
|
920
|
+
function plainAvgExpr(qi, col) {
|
|
921
|
+
const inner = `AVG(${qi.q(col)})`;
|
|
922
|
+
return isExactNumericType(whereMod.getColumnPgType(qi, col)) ? inner : qi.castAgg(inner, 'float');
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* A `_sum` / `_avg` value over a plain column: the driver's value verbatim
|
|
926
|
+
* for an exact source type (see {@link isExactNumericType}), a JS number for
|
|
927
|
+
* every other, `null` for an aggregate over zero rows.
|
|
928
|
+
*/
|
|
929
|
+
function sumAvgValue(qi, col, value) {
|
|
930
|
+
if (value === null || value === undefined)
|
|
931
|
+
return null;
|
|
932
|
+
if (isExactNumericType(whereMod.getColumnPgType(qi, col)))
|
|
933
|
+
return value;
|
|
934
|
+
return Number(value);
|
|
935
|
+
}
|
|
886
936
|
export function buildAggregate(qi, args) {
|
|
887
937
|
qi.currentSkip = resolveSkipGlobalFilters(args.skipGlobalFilters);
|
|
888
938
|
// Resolved once, up front: see buildGroupBy.
|
|
@@ -944,7 +994,7 @@ export function buildAggregate(qi, args) {
|
|
|
944
994
|
for (const [field, enabled] of Object.entries(args._avg)) {
|
|
945
995
|
if (enabled) {
|
|
946
996
|
const col = qi.toColumn(field);
|
|
947
|
-
selectExprs.push(`${
|
|
997
|
+
selectExprs.push(`${plainAvgExpr(qi, col)} AS ${qi.q(`_avg_${col}`)}`);
|
|
948
998
|
}
|
|
949
999
|
}
|
|
950
1000
|
}
|
|
@@ -1014,13 +1064,13 @@ export function buildAggregate(qi, args) {
|
|
|
1014
1064
|
if (key.startsWith('_sum_')) {
|
|
1015
1065
|
const col = key.slice(5);
|
|
1016
1066
|
const field = qi.tableMeta.reverseColumnMap[col] ?? snakeToCamel(col);
|
|
1017
|
-
sumObj[field] =
|
|
1067
|
+
sumObj[field] = sumAvgValue(qi, col, val);
|
|
1018
1068
|
hasSums = true;
|
|
1019
1069
|
}
|
|
1020
1070
|
else if (key.startsWith('_avg_')) {
|
|
1021
1071
|
const col = key.slice(5);
|
|
1022
1072
|
const field = qi.tableMeta.reverseColumnMap[col] ?? snakeToCamel(col);
|
|
1023
|
-
avgObj[field] =
|
|
1073
|
+
avgObj[field] = sumAvgValue(qi, col, val);
|
|
1024
1074
|
hasAvgs = true;
|
|
1025
1075
|
}
|
|
1026
1076
|
else if (key.startsWith('_min_')) {
|
package/dist/query/builder.d.ts
CHANGED
|
@@ -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;
|