turbine-orm 0.65.0 → 0.66.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 +34 -32
- package/dist/adapters/cockroachdb.js +21 -3
- package/dist/adapters/index.d.ts +15 -0
- package/dist/adapters/yugabytedb.js +20 -3
- package/dist/cjs/adapters/cockroachdb.js +21 -3
- package/dist/cjs/adapters/index.d.ts +15 -0
- package/dist/cjs/adapters/yugabytedb.js +20 -3
- package/dist/cjs/cli/destructive.d.ts +18 -4
- package/dist/cjs/cli/destructive.js +230 -122
- package/dist/cjs/cli/index.d.ts +21 -4
- package/dist/cjs/cli/index.js +119 -22
- package/dist/cjs/cli/mcp.d.ts +28 -8
- package/dist/cjs/cli/mcp.js +170 -127
- package/dist/cjs/cli/migrate.d.ts +134 -13
- package/dist/cjs/cli/migrate.js +349 -241
- package/dist/cjs/cli/pii-predicate-guard.d.ts +112 -0
- package/dist/cjs/cli/pii-predicate-guard.js +390 -0
- package/dist/cjs/cli/prisma-resolve.js +75 -4
- package/dist/cjs/cli/prisma-schema.d.ts +17 -1
- package/dist/cjs/cli/prisma-schema.js +83 -17
- package/dist/cjs/cli/sql-statements.d.ts +125 -0
- package/dist/cjs/cli/sql-statements.js +378 -0
- package/dist/cjs/cli/studio.js +49 -118
- package/dist/cjs/cli/ui.d.ts +1 -1
- package/dist/cjs/client.d.ts +43 -0
- package/dist/cjs/client.js +125 -6
- package/dist/cjs/dialect.d.ts +123 -0
- package/dist/cjs/dialect.js +33 -0
- package/dist/cjs/errors.d.ts +74 -1
- package/dist/cjs/errors.js +239 -25
- package/dist/cjs/index-advisor.d.ts +33 -1
- package/dist/cjs/index-advisor.js +32 -1
- package/dist/cjs/introspect.d.ts +48 -0
- package/dist/cjs/introspect.js +222 -91
- package/dist/cjs/mssql.js +43 -1
- package/dist/cjs/mysql.d.ts +5 -2
- package/dist/cjs/mysql.js +202 -17
- package/dist/cjs/nested-write.js +6 -1
- package/dist/cjs/pipeline-submittable.js +17 -3
- package/dist/cjs/pipeline.js +75 -9
- package/dist/cjs/powdb.d.ts +23 -0
- package/dist/cjs/powdb.js +33 -1
- package/dist/cjs/powql.d.ts +61 -9
- package/dist/cjs/powql.js +186 -49
- package/dist/cjs/prisma-compat.js +160 -41
- package/dist/cjs/query/aggregates.d.ts +1 -1
- package/dist/cjs/query/aggregates.js +80 -18
- package/dist/cjs/query/batched-loader.d.ts +10 -0
- package/dist/cjs/query/batched-loader.js +268 -7
- package/dist/cjs/query/builder.d.ts +73 -0
- package/dist/cjs/query/builder.js +225 -28
- package/dist/cjs/query/filters.d.ts +162 -0
- package/dist/cjs/query/filters.js +250 -1
- package/dist/cjs/query/relations.d.ts +10 -10
- package/dist/cjs/query/relations.js +93 -12
- package/dist/cjs/query/types.d.ts +14 -1
- package/dist/cjs/query/utils.d.ts +146 -2
- package/dist/cjs/query/utils.js +210 -4
- package/dist/cjs/query/warn-registry.d.ts +10 -0
- package/dist/cjs/query/warn-registry.js +10 -0
- package/dist/cjs/query/where-compile.d.ts +30 -0
- package/dist/cjs/query/where-compile.js +41 -0
- package/dist/cjs/query/where.d.ts +128 -13
- package/dist/cjs/query/where.js +215 -77
- package/dist/cjs/query/writes.d.ts +1 -1
- package/dist/cjs/query/writes.js +39 -15
- package/dist/cjs/schema-builder.d.ts +2 -1
- package/dist/cjs/schema-sql.d.ts +94 -4
- package/dist/cjs/schema-sql.js +506 -30
- package/dist/cjs/schema.d.ts +3 -1
- package/dist/cjs/sqlite.d.ts +6 -0
- package/dist/cjs/sqlite.js +151 -10
- package/dist/cjs/typed-sql.d.ts +29 -1
- package/dist/cjs/typed-sql.js +30 -12
- package/dist/cli/destructive.d.ts +18 -4
- package/dist/cli/destructive.js +229 -121
- package/dist/cli/index.d.ts +21 -4
- package/dist/cli/index.js +120 -24
- package/dist/cli/mcp.d.ts +28 -8
- package/dist/cli/mcp.js +172 -129
- package/dist/cli/migrate.d.ts +134 -13
- package/dist/cli/migrate.js +347 -238
- package/dist/cli/pii-predicate-guard.d.ts +112 -0
- package/dist/cli/pii-predicate-guard.js +386 -0
- package/dist/cli/prisma-resolve.js +75 -4
- package/dist/cli/prisma-schema.d.ts +17 -1
- package/dist/cli/prisma-schema.js +83 -17
- package/dist/cli/sql-statements.d.ts +125 -0
- package/dist/cli/sql-statements.js +373 -0
- package/dist/cli/studio.js +49 -118
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +43 -0
- package/dist/client.js +126 -7
- package/dist/dialect.d.ts +123 -0
- package/dist/dialect.js +33 -0
- package/dist/errors.d.ts +74 -1
- package/dist/errors.js +228 -19
- package/dist/index-advisor.d.ts +33 -1
- package/dist/index-advisor.js +31 -1
- package/dist/introspect.d.ts +48 -0
- package/dist/introspect.js +221 -91
- package/dist/mssql.js +44 -2
- package/dist/mysql.d.ts +5 -2
- package/dist/mysql.js +203 -18
- package/dist/nested-write.js +7 -2
- package/dist/pipeline-submittable.js +18 -4
- package/dist/pipeline.js +76 -10
- package/dist/powdb.d.ts +23 -0
- package/dist/powdb.js +33 -2
- package/dist/powql.d.ts +61 -9
- package/dist/powql.js +187 -50
- package/dist/prisma-compat.js +160 -41
- package/dist/query/aggregates.d.ts +1 -1
- package/dist/query/aggregates.js +82 -20
- package/dist/query/batched-loader.d.ts +10 -0
- package/dist/query/batched-loader.js +270 -9
- package/dist/query/builder.d.ts +73 -0
- package/dist/query/builder.js +226 -30
- package/dist/query/filters.d.ts +162 -0
- package/dist/query/filters.js +246 -1
- package/dist/query/relations.d.ts +10 -10
- package/dist/query/relations.js +94 -14
- package/dist/query/types.d.ts +14 -1
- package/dist/query/utils.d.ts +146 -2
- package/dist/query/utils.js +204 -3
- package/dist/query/warn-registry.d.ts +10 -0
- package/dist/query/warn-registry.js +10 -0
- package/dist/query/where-compile.d.ts +30 -0
- package/dist/query/where-compile.js +40 -1
- package/dist/query/where.d.ts +128 -13
- package/dist/query/where.js +216 -80
- package/dist/query/writes.d.ts +1 -1
- package/dist/query/writes.js +40 -16
- package/dist/schema-builder.d.ts +2 -1
- package/dist/schema-sql.d.ts +94 -4
- package/dist/schema-sql.js +505 -30
- package/dist/schema.d.ts +3 -1
- package/dist/sqlite.d.ts +6 -0
- package/dist/sqlite.js +151 -10
- package/dist/typed-sql.d.ts +29 -1
- package/dist/typed-sql.js +30 -12
- package/package.json +6 -4
package/dist/query/builder.js
CHANGED
|
@@ -18,10 +18,10 @@ import { normalizeKeyColumns, snakeToCamel } from '../schema.js';
|
|
|
18
18
|
import * as aggMod from './aggregates.js';
|
|
19
19
|
import { assertProjectionShape, defaultProjectionFields, includeKeysForBatching, loadRelationsBatched, neededParentKeyFields, rejectNestedPickOrder, resolveCountRelations, stripFields, } from './batched-loader.js';
|
|
20
20
|
import { expandCompoundUniqueWhere } from './compound-unique.js';
|
|
21
|
-
import { isJsonPathOrderBy, isOrderBySpec, isRelationPickOrderBy, isVectorOrderBy, isWhereOperator, orderByEntries, sortedEntries, } from './filters.js';
|
|
21
|
+
import { dedupeColumnList, dedupeOrderEntries, isJsonPathOrderBy, isOrderBySpec, isRelationPickOrderBy, isVectorOrderBy, isWhereOperator, orderByEntries, sortedEntries, } from './filters.js';
|
|
22
22
|
import * as relationsMod from './relations.js';
|
|
23
23
|
import { resolveSkipGlobalFilters, resolveUnsafeFlag, UNSAFE } from './types.js';
|
|
24
|
-
import { isTemporalInfinity, LRUCache, ownLookup, parseDbDate, resolveColumnName, sqlToPreparedName, unknownFieldMessage, } from './utils.js';
|
|
24
|
+
import { isTemporalInfinity, LRUCache, ownLookup, parseDbDate, resolveColumnName, sqlToPreparedName, unknownFieldMessage, warnRedundantSortTerm, } from './utils.js';
|
|
25
25
|
import { shouldWarnOnce, WARN_NS } from './warn-registry.js';
|
|
26
26
|
import * as whereMod from './where.js';
|
|
27
27
|
import * as writesMod from './writes.js';
|
|
@@ -50,18 +50,55 @@ import * as writesMod from './writes.js';
|
|
|
50
50
|
* throws, same as dev. `0`, unset, or an unparseable value means never check.
|
|
51
51
|
*/
|
|
52
52
|
function cacheCrossCheckMode() {
|
|
53
|
+
const env = crossCheckEnv ?? readCrossCheckEnv();
|
|
54
|
+
if (env.mode !== 'sampled')
|
|
55
|
+
return env.mode;
|
|
56
|
+
// The SAMPLING decision, and only it, is genuinely per hit.
|
|
57
|
+
return env.rate >= 1 || Math.random() < env.rate ? 'sampled' : 'off';
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The env-derived half of {@link cacheCrossCheckMode}, resolved ONCE per
|
|
61
|
+
* process and memoized.
|
|
62
|
+
*
|
|
63
|
+
* `process.env` is not a plain object: every property read crosses a C++
|
|
64
|
+
* interceptor. Measured at 112.7 ns per read against 3.6 ns for a module-level
|
|
65
|
+
* constant, and this function ran TWO reads on every cache hit, which put it
|
|
66
|
+
* at the top of the SQL-build CPU profile (12.53% self time, with a further
|
|
67
|
+
* ~12% inside the interceptor) for a value that does not change.
|
|
68
|
+
*
|
|
69
|
+
* It is memoized rather than hoisted to a `const` because the toggle IS
|
|
70
|
+
* observed at runtime, contrary to what a quick look suggests: the
|
|
71
|
+
* cross-check and sampling suites flip `NODE_ENV` /
|
|
72
|
+
* `TURBINE_DISABLE_CACHE_CHECK` / `TURBINE_CACHE_CHECK_SAMPLE` around a
|
|
73
|
+
* synchronous block in-process, not at spawn. So the read is deferred to first
|
|
74
|
+
* use and {@link resetCacheCrossCheckEnv} re-arms it; that is the entire
|
|
75
|
+
* contract, and the only thing lost versus reading every time is that a
|
|
76
|
+
* process which mutates these variables mid-run must say so.
|
|
77
|
+
*/
|
|
78
|
+
let crossCheckEnv;
|
|
79
|
+
function readCrossCheckEnv() {
|
|
80
|
+
let resolved;
|
|
53
81
|
if (process.env.NODE_ENV !== 'production') {
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
82
|
+
resolved = { mode: process.env.TURBINE_DISABLE_CACHE_CHECK === '1' ? 'off' : 'dev' };
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const raw = process.env.TURBINE_CACHE_CHECK_SAMPLE;
|
|
86
|
+
const rate = raw === undefined ? Number.NaN : Number.parseFloat(raw);
|
|
87
|
+
resolved = !Number.isFinite(rate) || rate <= 0 ? { mode: 'off' } : { mode: 'sampled', rate };
|
|
88
|
+
}
|
|
89
|
+
crossCheckEnv = resolved;
|
|
90
|
+
return resolved;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Discard the memoized cross-check environment so the next cache hit re-reads
|
|
94
|
+
* `process.env`.
|
|
95
|
+
*
|
|
96
|
+
* @internal Exposed for the tests that toggle these variables in-process (see
|
|
97
|
+
* {@link crossCheckEnv}). Production code sets them before the first query and
|
|
98
|
+
* never again.
|
|
99
|
+
*/
|
|
100
|
+
export function resetCacheCrossCheckEnv() {
|
|
101
|
+
crossCheckEnv = undefined;
|
|
65
102
|
}
|
|
66
103
|
/**
|
|
67
104
|
* Distinct cache-mismatch fingerprints already logged by the sampled production
|
|
@@ -380,6 +417,16 @@ export class QueryInterface {
|
|
|
380
417
|
* inline, not through the top-level cache).
|
|
381
418
|
*/
|
|
382
419
|
lastCacheHit = false;
|
|
420
|
+
/**
|
|
421
|
+
* Set while a build closure runs when the statement's WHERE (or HAVING)
|
|
422
|
+
* carries a caller-written `AND`/`OR` combinator array, i.e. when its SQL text
|
|
423
|
+
* is a function of an arity the caller chose. Read (and reset) by
|
|
424
|
+
* {@link acquireSql}, which brackets the single `build()` call, so the flag
|
|
425
|
+
* has no lifetime outside that synchronous window and cannot leak between
|
|
426
|
+
* queries. See {@link BuilderCtx.markVariableArity} for the unbounded
|
|
427
|
+
* server-side prepared-statement growth this exists to stop.
|
|
428
|
+
*/
|
|
429
|
+
variableArityShape = false;
|
|
383
430
|
middlewares;
|
|
384
431
|
defaultLimit;
|
|
385
432
|
warnOnUnlimited;
|
|
@@ -466,6 +513,17 @@ export class QueryInterface {
|
|
|
466
513
|
* to Date as well (otherwise nested dates leak through as strings).
|
|
467
514
|
*/
|
|
468
515
|
camelDateFieldCache = new Map();
|
|
516
|
+
/**
|
|
517
|
+
* Per-table memo of `Object.entries(meta.relations)`, consumed by the nested
|
|
518
|
+
* row parser (see `getRelationEntries` in relations.ts). Same rationale and
|
|
519
|
+
* same lifetime as {@link camelDateFieldCache}: the metadata is immutable, and
|
|
520
|
+
* the parser reads it once per row.
|
|
521
|
+
*/
|
|
522
|
+
relationEntryCache = new Map();
|
|
523
|
+
/** Per-table memo of batched-loader child readers (see {@link batchedChild}). */
|
|
524
|
+
batchedChildCache = new Map();
|
|
525
|
+
/** The (option-invariant) options every batched child is built with. */
|
|
526
|
+
batchedChildOptions;
|
|
469
527
|
/** True when this QI runs inside an active transaction (set via _txScoped option). */
|
|
470
528
|
txScoped;
|
|
471
529
|
/** Original options reference, forwarded to child QIs in nested writes. */
|
|
@@ -662,6 +720,9 @@ export class QueryInterface {
|
|
|
662
720
|
set currentSkip(v) {
|
|
663
721
|
self.currentSkip = v;
|
|
664
722
|
},
|
|
723
|
+
markVariableArity: () => {
|
|
724
|
+
this.variableArityShape = true;
|
|
725
|
+
},
|
|
665
726
|
q: (name) => this.q(name),
|
|
666
727
|
p: (index) => this.p(index),
|
|
667
728
|
inParam: (values) => this.inParam(values),
|
|
@@ -679,6 +740,7 @@ export class QueryInterface {
|
|
|
679
740
|
crossCheckCache: (op, cacheKey, entry, build, collectedParams) => this.crossCheckCache(op, cacheKey, entry, build, collectedParams),
|
|
680
741
|
jsonEncoding: this.jsonEncoding,
|
|
681
742
|
camelDateFieldCache: this.camelDateFieldCache,
|
|
743
|
+
relationEntryCache: this.relationEntryCache,
|
|
682
744
|
limitOneClause: () => this.limitOneClause(),
|
|
683
745
|
buildPagination: (limitPh, offsetPh, hasOrderBy) => this.buildPagination(limitPh, offsetPh, hasOrderBy),
|
|
684
746
|
paginationRef: (value, params, arg) => this.paginationRef(value, params, arg),
|
|
@@ -1441,15 +1503,10 @@ export class QueryInterface {
|
|
|
1441
1503
|
includePii, forceCustomPlan = false) {
|
|
1442
1504
|
// The loader's own global-filter callback below needs the RESOLVED form.
|
|
1443
1505
|
const resolvedSkip = resolveSkipGlobalFilters(skip);
|
|
1444
|
-
const childOptions = {
|
|
1445
|
-
...this.options,
|
|
1446
|
-
defaultLimit: undefined,
|
|
1447
|
-
warnOnUnlimited: false,
|
|
1448
|
-
};
|
|
1449
1506
|
return {
|
|
1450
1507
|
parentMeta: this.tableMeta,
|
|
1451
1508
|
schema: this.schema,
|
|
1452
|
-
makeChild: (table) =>
|
|
1509
|
+
makeChild: (table) => this.batchedChild(table),
|
|
1453
1510
|
// The per-query `forceCustomPlan` opt-in covers the relation follow-ups
|
|
1454
1511
|
// too: a batched load re-issues the SAME tenant-shaped predicate one
|
|
1455
1512
|
// level down, so leaving those named would keep exactly the plan-cache
|
|
@@ -1459,6 +1516,23 @@ export class QueryInterface {
|
|
|
1459
1516
|
buildInClause: (expr, paramRef, negated) => this.inClause(expr, paramRef, negated),
|
|
1460
1517
|
inClauseParam: (values) => this.inParam(values),
|
|
1461
1518
|
paramPlaceholder: (index) => this.p(index),
|
|
1519
|
+
// Bound each relation follow-up per correlation key when the engine can
|
|
1520
|
+
// express it; absent leaves the loader on its client-side slice. Bound as
|
|
1521
|
+
// a closure rather than passing `this.dialect` so the loader keeps
|
|
1522
|
+
// needing nothing else from the dialect.
|
|
1523
|
+
//
|
|
1524
|
+
// The `dialect.name` test is NOT redundant with the hook's presence, and
|
|
1525
|
+
// this is the same trap the `distinct` gate documents: every engine
|
|
1526
|
+
// dialect is built by SPREADING `postgresDialect`, so an optional hook
|
|
1527
|
+
// added there is INHERITED by sqlite / mysql / mssql, whose "absent"
|
|
1528
|
+
// fallback would then never be taken. The wrapper is not portable as
|
|
1529
|
+
// written either (T-SQL rejects an ORDER BY inside a derived table
|
|
1530
|
+
// without TOP/OFFSET, and the compiled child query carries one), so the
|
|
1531
|
+
// pushdown stays with the dialect that owns the implementation until
|
|
1532
|
+
// another engine ships its own and asserts it.
|
|
1533
|
+
buildPartitionLimit: this.dialect.name === 'postgresql' && this.dialect.buildPartitionLimit
|
|
1534
|
+
? (input) => this.dialect.buildPartitionLimit(input)
|
|
1535
|
+
: undefined,
|
|
1462
1536
|
skipGlobalFilters: skip,
|
|
1463
1537
|
// Query-level opt-in threaded onto every follow-up child `buildFindMany`,
|
|
1464
1538
|
// so a batched load excludes/includes PII exactly as the join strategy.
|
|
@@ -1480,6 +1554,36 @@ export class QueryInterface {
|
|
|
1480
1554
|
},
|
|
1481
1555
|
};
|
|
1482
1556
|
}
|
|
1557
|
+
/**
|
|
1558
|
+
* The child {@link QueryInterface} the batched loader uses for `table`,
|
|
1559
|
+
* memoized per table for the lifetime of this accessor.
|
|
1560
|
+
*
|
|
1561
|
+
* It used to be constructed fresh for every relation of every query, which
|
|
1562
|
+
* threw away that child's SQL template cache each time: a relation follow-up
|
|
1563
|
+
* therefore MISSED the cache on every single request and rebuilt its SQL,
|
|
1564
|
+
* which is the one thing the template cache exists to avoid, and it allocated
|
|
1565
|
+
* a whole QueryInterface (column type maps, camel-date memo, the ctx literal)
|
|
1566
|
+
* per relation per query. `childOptions` was likewise a fresh spread per
|
|
1567
|
+
* query although it is a pure function of this instance's options.
|
|
1568
|
+
*
|
|
1569
|
+
* Safe to share across queries: a QueryInterface holds no per-query state
|
|
1570
|
+
* beyond the transient build fields, which live and die inside one
|
|
1571
|
+
* synchronous `build*` call, and every child is bound to THIS instance's pool
|
|
1572
|
+
* (so it still joins an active transaction) with `defaultLimit` cleared and
|
|
1573
|
+
* unlimited-warnings silenced, because a relation load must fetch every
|
|
1574
|
+
* matching child. Per-query values (`skipGlobalFilters`, `includePii`,
|
|
1575
|
+
* `timeout`, `forceCustomPlan`) are passed as ARGUMENTS by the loader, never
|
|
1576
|
+
* baked into the child, which is what makes the memo sound.
|
|
1577
|
+
*/
|
|
1578
|
+
batchedChild(table) {
|
|
1579
|
+
let child = this.batchedChildCache.get(table);
|
|
1580
|
+
if (!child) {
|
|
1581
|
+
this.batchedChildOptions ??= { ...this.options, defaultLimit: undefined, warnOnUnlimited: false };
|
|
1582
|
+
child = new QueryInterface(this.pool, table, this.schema, [], this.batchedChildOptions);
|
|
1583
|
+
this.batchedChildCache.set(table, child);
|
|
1584
|
+
}
|
|
1585
|
+
return child;
|
|
1586
|
+
}
|
|
1483
1587
|
/**
|
|
1484
1588
|
* Run a findMany with the batched strategy: execute the base query WITHOUT
|
|
1485
1589
|
* relation subqueries (all other clauses intact), then load each relation via
|
|
@@ -1564,9 +1668,7 @@ export class QueryInterface {
|
|
|
1564
1668
|
acquireSql(cacheKey, build) {
|
|
1565
1669
|
if (!this.sqlCacheEnabled) {
|
|
1566
1670
|
this.lastCacheHit = false;
|
|
1567
|
-
|
|
1568
|
-
this.cacheMisses++;
|
|
1569
|
-
return { sql, name: sqlToPreparedName(sql) };
|
|
1671
|
+
return this.buildCacheEntry(build);
|
|
1570
1672
|
}
|
|
1571
1673
|
const cached = this.sqlTemplateCache.get(cacheKey);
|
|
1572
1674
|
if (cached) {
|
|
@@ -1575,12 +1677,38 @@ export class QueryInterface {
|
|
|
1575
1677
|
return cached;
|
|
1576
1678
|
}
|
|
1577
1679
|
this.lastCacheHit = false;
|
|
1578
|
-
const
|
|
1579
|
-
const entry = { sql, name: sqlToPreparedName(sql) };
|
|
1680
|
+
const entry = this.buildCacheEntry(build);
|
|
1580
1681
|
this.sqlTemplateCache.set(cacheKey, entry);
|
|
1581
|
-
this.cacheMisses++;
|
|
1582
1682
|
return entry;
|
|
1583
1683
|
}
|
|
1684
|
+
/**
|
|
1685
|
+
* Run one build closure and pair its SQL with the prepared-statement name it
|
|
1686
|
+
* should execute under.
|
|
1687
|
+
*
|
|
1688
|
+
* The `variableArityShape` flag is reset immediately BEFORE the build and read
|
|
1689
|
+
* immediately AFTER it, so this method is the flag's entire lifetime: the
|
|
1690
|
+
* build walk (`buildWhereClause` / `buildScopedWhere` / the HAVING combinator)
|
|
1691
|
+
* is the only thing that can set it, nothing can observe a stale value, and a
|
|
1692
|
+
* throw out of `build()` leaves nothing behind to affect the next query.
|
|
1693
|
+
*
|
|
1694
|
+
* An EMPTY name is how "send this unnamed" travels: `queryWithTimeout` tests
|
|
1695
|
+
* the name for truthiness, so `''` takes the plain `(text, values)` form the
|
|
1696
|
+
* driver never registers a named statement for. Doing it HERE rather than at
|
|
1697
|
+
* the execute seam is what makes it survive the cache: the entry is created
|
|
1698
|
+
* once, on the miss, and a later HIT reuses the same (empty) name, so a
|
|
1699
|
+
* variable-arity shape can never acquire a name from a warmed template.
|
|
1700
|
+
*
|
|
1701
|
+
* See {@link BuilderCtx.markVariableArity} for what makes a shape
|
|
1702
|
+
* variable-arity and the measured reason it must not be named.
|
|
1703
|
+
*/
|
|
1704
|
+
buildCacheEntry(build) {
|
|
1705
|
+
this.variableArityShape = false;
|
|
1706
|
+
const sql = build([]);
|
|
1707
|
+
const variableArity = this.variableArityShape;
|
|
1708
|
+
this.variableArityShape = false;
|
|
1709
|
+
this.cacheMisses++;
|
|
1710
|
+
return { sql, name: variableArity ? '' : sqlToPreparedName(sql) };
|
|
1711
|
+
}
|
|
1584
1712
|
/**
|
|
1585
1713
|
* Dev-mode SQL-cache lockstep cross-check (see {@link cacheCrossCheckEnabled}).
|
|
1586
1714
|
*
|
|
@@ -1910,7 +2038,6 @@ export class QueryInterface {
|
|
|
1910
2038
|
stripFields([entity], proj.strip);
|
|
1911
2039
|
return entity;
|
|
1912
2040
|
}
|
|
1913
|
-
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause", matches TypedWithClause default
|
|
1914
2041
|
buildFindUnique(args) {
|
|
1915
2042
|
this.currentSkip = resolveSkipGlobalFilters(args.skipGlobalFilters);
|
|
1916
2043
|
// Prisma compound-unique selector expansion (before global-filter merge and
|
|
@@ -2249,7 +2376,6 @@ export class QueryInterface {
|
|
|
2249
2376
|
}
|
|
2250
2377
|
return maxDepth;
|
|
2251
2378
|
}
|
|
2252
|
-
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause", matches TypedWithClause default
|
|
2253
2379
|
buildFindMany(args) {
|
|
2254
2380
|
this.currentSkip = resolveSkipGlobalFilters(args?.skipGlobalFilters);
|
|
2255
2381
|
// Stable relation order (opt-in): fill PK-asc orderBy into unordered to-many
|
|
@@ -2270,6 +2396,32 @@ export class QueryInterface {
|
|
|
2270
2396
|
if (args?.orderBy !== undefined && isEmptyOrderBy(args.orderBy)) {
|
|
2271
2397
|
args = { ...args, orderBy: undefined };
|
|
2272
2398
|
}
|
|
2399
|
+
// Drop `orderBy` terms that sort by an expression an earlier term already
|
|
2400
|
+
// sorted by, and `distinct` columns named twice. HERE, before the
|
|
2401
|
+
// fingerprint, for the reason every other normalization in this method is
|
|
2402
|
+
// here: the fingerprint, the SQL build and the param collect must all see
|
|
2403
|
+
// ONE list, or the cache key and the statement it caches describe different
|
|
2404
|
+
// queries. Rewritten into the array form, which flattens through
|
|
2405
|
+
// `orderByEntries` to the identical entry list the object form does, so a
|
|
2406
|
+
// caller who wrote no duplicate is untouched down to the cache key (the
|
|
2407
|
+
// helpers return null unless something was actually dropped).
|
|
2408
|
+
if (args?.orderBy !== undefined) {
|
|
2409
|
+
const deduped = dedupeOrderEntries(this.tableMeta, orderByEntries(args.orderBy), this.table);
|
|
2410
|
+
if (deduped) {
|
|
2411
|
+
warnRedundantSortTerm(this.table, 'orderBy', deduped.dropped);
|
|
2412
|
+
args = {
|
|
2413
|
+
...args,
|
|
2414
|
+
orderBy: deduped.entries.map(([k, v]) => ({ [k]: v })),
|
|
2415
|
+
};
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
if (args?.distinct && args.distinct.length > 0) {
|
|
2419
|
+
const deduped = dedupeColumnList(this.tableMeta, args.distinct);
|
|
2420
|
+
if (deduped) {
|
|
2421
|
+
warnRedundantSortTerm(this.table, 'distinct', deduped.dropped);
|
|
2422
|
+
args = { ...args, distinct: deduped.columns };
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2273
2425
|
// Deterministic pagination (opt-in): fill a PK-asc orderBy into a paginating
|
|
2274
2426
|
// query that declares none, BEFORE fingerprinting so the ordered and
|
|
2275
2427
|
// unordered shapes get distinct cache entries. No-op unless
|
|
@@ -2277,6 +2429,29 @@ export class QueryInterface {
|
|
|
2277
2429
|
const implicitOrder = this.implicitPkOrderBy(args);
|
|
2278
2430
|
if (implicitOrder)
|
|
2279
2431
|
args = { ...args, orderBy: implicitOrder };
|
|
2432
|
+
// `distinct` compiles to `SELECT DISTINCT ON (...)`, which is PostgreSQL
|
|
2433
|
+
// syntax and nothing else's. Every other engine parsed it as far as the
|
|
2434
|
+
// word `ON` and answered with a RAW driver error carrying no TURBINE_ code
|
|
2435
|
+
// (SQLite: `near "ON": syntax error`; MySQL: ER_PARSE_ERROR), i.e. the one
|
|
2436
|
+
// shape the typed-error surface exists to prevent. Refused with the same
|
|
2437
|
+
// E017 the OTHER spelling of this feature already used, `groupBy({
|
|
2438
|
+
// distinctOn })` in aggregates.ts, which was gated correctly all along.
|
|
2439
|
+
//
|
|
2440
|
+
// `dialect.name` rather than a new capability flag, deliberately, and this
|
|
2441
|
+
// is the exception to the "capability flags, not engine names" rule the
|
|
2442
|
+
// rest of the dialect follows: every engine dialect is built by SPREADING
|
|
2443
|
+
// `postgresDialect`, so a flag defaulting to `true` would be inherited by
|
|
2444
|
+
// all of them (which is the bug) and one defaulting to `false` would have
|
|
2445
|
+
// to be re-declared on Postgres-compatible dialects that already work.
|
|
2446
|
+
// The established precedent for exactly this feature is the `dialect.name`
|
|
2447
|
+
// test in `buildDistinctOnSource`.
|
|
2448
|
+
//
|
|
2449
|
+
// Before the cache, like the two guards below, so a warmed template cannot
|
|
2450
|
+
// serve the query the cold path refuses.
|
|
2451
|
+
if (args?.distinct && args.distinct.length > 0 && this.dialect.name !== 'postgresql') {
|
|
2452
|
+
throw new UnsupportedFeatureError('DISTINCT ON (findMany distinct)', this.dialect.name, 'findMany({ distinct }) requires PostgreSQL: SELECT DISTINCT ON is not portable. Group in ' +
|
|
2453
|
+
'application code, or use groupBy({ by }) for one row per combination.');
|
|
2454
|
+
}
|
|
2280
2455
|
// `distinct` + relation orderBy is refused up front (E003): the distinct
|
|
2281
2456
|
// path re-orders in an outer wrapper (`... AS "<table>_distinct" ORDER BY
|
|
2282
2457
|
// <userOrder>`) where a correlated relation subquery (pick-row, `_count`,
|
|
@@ -2367,6 +2542,30 @@ export class QueryInterface {
|
|
|
2367
2542
|
if (args?.distinct && args.distinct.length > 0) {
|
|
2368
2543
|
distinctCols = args.distinct.map((k) => this.toSqlColumn(k));
|
|
2369
2544
|
distinctPrefix = `DISTINCT ON (${distinctCols.join(', ')}) `;
|
|
2545
|
+
// A MULTI-column `distinct` gives up its server-side prepared-statement
|
|
2546
|
+
// name, for the same reason a caller-written `OR` array does and by the
|
|
2547
|
+
// same mechanism (see markVariableArity). The channel here is ORDER, not
|
|
2548
|
+
// arity: the column list is written into the SQL text one term at a
|
|
2549
|
+
// time, so the reachable statement set is the ORDERED subsets of the
|
|
2550
|
+
// table's columns. Measured on PostgreSQL 16, a seven-column table:
|
|
2551
|
+
// 5,040 `distinct` permutations left 5,040 further permanent statements
|
|
2552
|
+
// and took CachedPlanSource from 39.4 MB to 59.1 MB on ONE connection.
|
|
2553
|
+
//
|
|
2554
|
+
// UNNAMED rather than canonicalized, which is what `select` and the
|
|
2555
|
+
// write `data` list do, and the asymmetry is the point. This list is
|
|
2556
|
+
// re-emitted as the leading terms of the inner ORDER BY a few lines
|
|
2557
|
+
// below, and with no `orderBy` at all PostgreSQL's choice of
|
|
2558
|
+
// representative row per group is explicitly unpredictable, so
|
|
2559
|
+
// reordering it could hand a caller a different row. Withholding the
|
|
2560
|
+
// name changes no SQL text and therefore cannot. The cost is one extra
|
|
2561
|
+
// server-side parse per execution on an already-rare query shape.
|
|
2562
|
+
//
|
|
2563
|
+
// `length > 1` because a single column has exactly one ordering and so
|
|
2564
|
+
// no permutation space: the common `distinct: ['tenantId']` keeps its
|
|
2565
|
+
// name. Repeats are already dropped upstream (dedupeColumnList), so the
|
|
2566
|
+
// list here is distinct columns and nothing else.
|
|
2567
|
+
if (args.distinct.length > 1)
|
|
2568
|
+
this.ctx.markVariableArity();
|
|
2370
2569
|
}
|
|
2371
2570
|
// Join-sink for `relationLoadStrategy: 'flatten'`. Filled while the SELECT
|
|
2372
2571
|
// list is built (so the flattened relations' ON-clause params interleave
|
|
@@ -2634,7 +2833,6 @@ export class QueryInterface {
|
|
|
2634
2833
|
return deferred.transform(result);
|
|
2635
2834
|
});
|
|
2636
2835
|
}
|
|
2637
|
-
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause", matches TypedWithClause default
|
|
2638
2836
|
buildFindFirst(args) {
|
|
2639
2837
|
// Reuse findMany's SQL builder but force LIMIT 1
|
|
2640
2838
|
const findManyArgs = { ...args, limit: 1 };
|
|
@@ -2659,7 +2857,6 @@ export class QueryInterface {
|
|
|
2659
2857
|
return deferred.transform(result);
|
|
2660
2858
|
});
|
|
2661
2859
|
}
|
|
2662
|
-
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause", matches TypedWithClause default
|
|
2663
2860
|
buildFindFirstOrThrow(args) {
|
|
2664
2861
|
const inner = this.buildFindFirst(args);
|
|
2665
2862
|
return {
|
|
@@ -2689,7 +2886,6 @@ export class QueryInterface {
|
|
|
2689
2886
|
return deferred.transform(result);
|
|
2690
2887
|
});
|
|
2691
2888
|
}
|
|
2692
|
-
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause", matches TypedWithClause default
|
|
2693
2889
|
buildFindUniqueOrThrow(args) {
|
|
2694
2890
|
const inner = this.buildFindUnique(args);
|
|
2695
2891
|
return {
|
package/dist/query/filters.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* and execution rather than filter-shape bookkeeping.
|
|
7
7
|
*/
|
|
8
8
|
import type { ArrayFilter, ColumnRef, JsonFilter, JsonPathOrderBy, OrderBySpec, OrderDirection, RelationPickOrderBy, TextSearchFilter, VectorFilter, VectorOrderBy, WhereOperator } from './types.js';
|
|
9
|
+
import { type ColumnNameSource } from './utils.js';
|
|
9
10
|
/** Check if a value is a where operator object (has at least one known operator key) */
|
|
10
11
|
export declare function isWhereOperator(value: unknown): value is WhereOperator;
|
|
11
12
|
/**
|
|
@@ -21,6 +22,41 @@ export declare function isUnmatchedPlainObject(value: unknown): boolean;
|
|
|
21
22
|
* take values only.
|
|
22
23
|
*/
|
|
23
24
|
export declare const COLUMN_REF_OPERATORS: Set<string>;
|
|
25
|
+
/**
|
|
26
|
+
* THE relation-filter wrappers: the keys whose body is a clause against the
|
|
27
|
+
* relation's TARGET table rather than against this one.
|
|
28
|
+
*
|
|
29
|
+
* One named list, here, because everything that walks a where clause has to
|
|
30
|
+
* agree about them and the copies had already spread. It lived inlined in
|
|
31
|
+
* `where-compile.ts` (`'some' in x || 'every' in x || …`, the SQL compiler's
|
|
32
|
+
* own answer), again in `normalizeRelationFilter` (where.ts) as the negated
|
|
33
|
+
* conjunction of the same five, again in `cli/pii-predicate-guard.ts`, and a
|
|
34
|
+
* fourth time in `prisma-compat.ts` SPLIT across a
|
|
35
|
+
* `RELATION_QUANTIFIERS` set (`some`/`every`/`none`) plus two inline
|
|
36
|
+
* `k === 'is' || k === 'isNot'` tests, which is the copy most likely to drift
|
|
37
|
+
* because half of it does not read as a list and a grep for the list does not
|
|
38
|
+
* find it.
|
|
39
|
+
*
|
|
40
|
+
* The failure mode is not cosmetic: a wrapper the SQL compiler treats as a
|
|
41
|
+
* relation filter but a WALKER does not is a wrapper whose body reaches the
|
|
42
|
+
* builder unwalked. That is precisely the operand-position channel this
|
|
43
|
+
* release closed elsewhere.
|
|
44
|
+
*
|
|
45
|
+
* `query/` is the right home rather than `cli/` because the direction of the
|
|
46
|
+
* dependency is fixed: `cli/` and the prisma-compat shim may import from the
|
|
47
|
+
* query path, and the query path may never import from `cli/`
|
|
48
|
+
* (`scripts/check-import-cycles.mjs`).
|
|
49
|
+
*/
|
|
50
|
+
export declare const RELATION_FILTER_WRAPPERS: readonly ["some", "none", "every", "is", "isNot"];
|
|
51
|
+
/** {@link RELATION_FILTER_WRAPPERS} as a membership set, for the walkers. */
|
|
52
|
+
export declare const RELATION_FILTER_WRAPPER_SET: ReadonlySet<string>;
|
|
53
|
+
/**
|
|
54
|
+
* True when a normalized relation-filter body carries at least one cardinality
|
|
55
|
+
* wrapper. THE predicate the SQL compiler branches on: a key that names a
|
|
56
|
+
* relation but whose value is not one of these falls through to the scalar
|
|
57
|
+
* path.
|
|
58
|
+
*/
|
|
59
|
+
export declare function hasRelationFilterWrapper(filterObj: Record<string, unknown>): boolean;
|
|
24
60
|
/**
|
|
25
61
|
* Check if an operator value is a column reference: a plain object whose ONLY
|
|
26
62
|
* key is `col` with a string value. Anything else (extra keys, non-string
|
|
@@ -210,6 +246,132 @@ export declare function isRelationPickOrderBy(value: unknown): value is Relation
|
|
|
210
246
|
* lockstep across build, param-collect, and cache-key fingerprint.
|
|
211
247
|
*/
|
|
212
248
|
export declare function orderByEntries(orderBy: unknown): [string, unknown][];
|
|
249
|
+
/**
|
|
250
|
+
* The longest `orderBy` whose statement keeps a NAMED prepared statement.
|
|
251
|
+
* Past this, the compile paths call `markVariableArity` and the statement is
|
|
252
|
+
* sent unnamed for the same reason a caller-written `OR` array is.
|
|
253
|
+
*
|
|
254
|
+
* WHY 3, and why a threshold at all. Refusing duplicate sort keys
|
|
255
|
+
* ({@link dedupeOrderEntries}) bounds an `orderBy` to the table's column
|
|
256
|
+
* count, which sounds like enough and is not: what is left is the PERMUTATION
|
|
257
|
+
* space. A 20-column table admits 20 one-key sorts, 380 two-key sorts, 6,840
|
|
258
|
+
* three-key sorts and 116,280 four-key sorts, and every one of those is a
|
|
259
|
+
* distinct SQL text and therefore a distinct un-reclaimable server-side
|
|
260
|
+
* prepared statement. The count is dominated by its longest term, so a cap on
|
|
261
|
+
* LENGTH is what actually bounds the total.
|
|
262
|
+
*
|
|
263
|
+
* 3 because that is where real sorts stop and generated ones start. The two
|
|
264
|
+
* shapes that show up in application code are a single sort key, and a sort key
|
|
265
|
+
* plus a tiebreaker for stable pagination (`[{createdAt:'desc'},{id:'asc'}]`).
|
|
266
|
+
* Three covers the widest genuinely hand-written form, a category or priority
|
|
267
|
+
* followed by recency followed by a primary-key tiebreak. A fourth key is
|
|
268
|
+
* effectively always a UI that lets the caller stack sort columns, which is
|
|
269
|
+
* exactly the shape that should not be minting named statements. The asymmetry
|
|
270
|
+
* of the two errors also points this way: naming a 4-key sort risks the 116,280
|
|
271
|
+
* above, while NOT naming one costs a single extra server-side parse per
|
|
272
|
+
* execution on a query that is already rare.
|
|
273
|
+
*/
|
|
274
|
+
export declare const MAX_NAMED_ORDER_KEYS = 3;
|
|
275
|
+
/**
|
|
276
|
+
* Canonical identity of ONE order term's sort EXPRESSION, or `null` when this
|
|
277
|
+
* rule declines to decide for that shape.
|
|
278
|
+
*
|
|
279
|
+
* DIRECTION IS DELIBERATELY EXCLUDED. `ORDER BY id ASC, id DESC` sorts exactly
|
|
280
|
+
* like `ORDER BY id ASC`: the first term already totally orders the rows it
|
|
281
|
+
* covers, so no later term on the same expression can move anything, whichever
|
|
282
|
+
* way it points. Including the direction would have missed the very case that
|
|
283
|
+
* motivated this, a pair of entries spelled differently AND pointing
|
|
284
|
+
* differently.
|
|
285
|
+
*
|
|
286
|
+
* The column is RESOLVED rather than compared as spelled, via the same
|
|
287
|
+
* `resolveColumnName` every SQL builder resolves keys through, so `userId` and
|
|
288
|
+
* `user_id` on an introspected schema are recognised as the one column they
|
|
289
|
+
* both compile to. Falls back to the raw key when the key resolves to nothing
|
|
290
|
+
* (metadata-less test schemas): the compile path a few lines later is what
|
|
291
|
+
* reports an unknown field, and this function must not pre-empt that error with
|
|
292
|
+
* a worse one.
|
|
293
|
+
*
|
|
294
|
+
* `null` (not compared) for two shapes:
|
|
295
|
+
* - VECTOR KNN ordering. Two distance terms on one column with DIFFERENT
|
|
296
|
+
* target vectors are genuinely different expressions and the second is not a
|
|
297
|
+
* no-op, so deciding would mean hashing the operand, which is a 1,536-float
|
|
298
|
+
* array on a typical embedding column. Not worth the per-build cost for a
|
|
299
|
+
* shape that {@link MAX_NAMED_ORDER_KEYS} already bounds.
|
|
300
|
+
* - RELATION ordering (`{posts:{_count:'desc'}}`, `{author:{name:'asc'}}`,
|
|
301
|
+
* pick-row). Same reason in a different key: the expression is a correlated
|
|
302
|
+
* subquery whose identity is its whole nested shape.
|
|
303
|
+
* Both are bounded by the length cap; neither can be refused wrongly here.
|
|
304
|
+
*/
|
|
305
|
+
export declare function orderKeyIdentity(meta: ColumnNameSource | undefined, key: string, value: unknown): string | null;
|
|
306
|
+
/** One term dropped as redundant: the key removed, and the key that outranked it. */
|
|
307
|
+
export interface DroppedTerm {
|
|
308
|
+
key: string;
|
|
309
|
+
first: string;
|
|
310
|
+
/** What both keys resolved to: a column name, or a groupBy SELECT expression. */
|
|
311
|
+
resolved: string;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Remove `orderBy` entries that sort by an expression an earlier entry already
|
|
315
|
+
* sorted by. Returns `null` when nothing is redundant, which is the common case
|
|
316
|
+
* and the one that must stay allocation-light and byte-identical.
|
|
317
|
+
*
|
|
318
|
+
* WHY THIS DROPS RATHER THAN REFUSES, because the reverse was specified and the
|
|
319
|
+
* premise turned out not to hold. A repeated sort key is a no-op in SQL, and it
|
|
320
|
+
* is the only way to push an `orderBy` past the table's column count (measured:
|
|
321
|
+
* `[{id:'asc'}] x n` produced a distinct named prepared statement for every n,
|
|
322
|
+
* from ONE column, with no ceiling short of the request body size). Both of
|
|
323
|
+
* those are true. What is NOT true is that it is always a caller bug. The
|
|
324
|
+
* idiomatic stable-pagination shape is
|
|
325
|
+
*
|
|
326
|
+
* orderBy: [{ [sortField]: sortDir }, { id: 'asc' }]
|
|
327
|
+
*
|
|
328
|
+
* appending a primary-key tiebreak unconditionally, which is correct defensive
|
|
329
|
+
* code, and which produces a duplicate exactly when the caller sorts by the
|
|
330
|
+
* primary key. That is a column header a user clicks, so refusing would turn a
|
|
331
|
+
* working table into a 500 on one column and no others. This repo's own
|
|
332
|
+
* differential fuzz generator writes that pattern, comment included ("a random
|
|
333
|
+
* key first, the PK as final tiebreaker"), and picks the sort field from a pool
|
|
334
|
+
* that contains `id`, so a refusal would have failed the suite on 10-35% of
|
|
335
|
+
* generated cases depending on table width.
|
|
336
|
+
*
|
|
337
|
+
* Dropping has no such failure mode: the removed term provably cannot move a
|
|
338
|
+
* row, so the result set and its order are unchanged, and the caller is told
|
|
339
|
+
* about the redundancy through a dev-only warn-once instead of an exception.
|
|
340
|
+
* The security outcome is identical, because what actually bounds the emitted
|
|
341
|
+
* statement set is the length cap ({@link MAX_NAMED_ORDER_KEYS}), which is
|
|
342
|
+
* shape-blind and applies to repeats and permutations alike. This function's
|
|
343
|
+
* job is narrower: keep the ordinary two-key sort under that cap so it keeps
|
|
344
|
+
* its named statement.
|
|
345
|
+
*
|
|
346
|
+
* A DROPPED TERM IS STILL VALIDATED, see {@link assertDroppedDirection}. The
|
|
347
|
+
* drop is an optimization, and an optimization must not decide whether an input
|
|
348
|
+
* is legal.
|
|
349
|
+
*
|
|
350
|
+
* @param table the table the sort is against, so a refused direction reads
|
|
351
|
+
* exactly as it does when the compile path raises it. Defaults to the
|
|
352
|
+
* metadata's own name, which is what a caller holding only a `TableMetadata`
|
|
353
|
+
* would have passed anyway.
|
|
354
|
+
*/
|
|
355
|
+
export declare function dedupeOrderEntries(meta: ColumnNameSource | undefined, entries: [string, unknown][], table?: string | undefined): {
|
|
356
|
+
entries: [string, unknown][];
|
|
357
|
+
dropped: DroppedTerm[];
|
|
358
|
+
} | null;
|
|
359
|
+
/**
|
|
360
|
+
* The {@link dedupeOrderEntries} rule applied to a `DISTINCT ON` column list,
|
|
361
|
+
* the other caller-supplied list written into the SQL one term per element.
|
|
362
|
+
* `DISTINCT ON (a, a)` groups exactly as `DISTINCT ON (a)` does, so a repeat is
|
|
363
|
+
* a no-op here too, and the same generated-code shape produces it (a grouping
|
|
364
|
+
* column chosen by the caller, plus a fixed one appended by the code).
|
|
365
|
+
*
|
|
366
|
+
* No length cap to go with it, deliberately. Unlike `orderBy`, `DISTINCT ON`
|
|
367
|
+
* has a meaning that degrades as the list grows (every added column makes the
|
|
368
|
+
* result strictly less distinct, converging on the plain query), so a long list
|
|
369
|
+
* is self-limiting in a way a long sort is not.
|
|
370
|
+
*/
|
|
371
|
+
export declare function dedupeColumnList(meta: ColumnNameSource | undefined, columns: readonly string[]): {
|
|
372
|
+
columns: string[];
|
|
373
|
+
dropped: DroppedTerm[];
|
|
374
|
+
} | null;
|
|
213
375
|
/**
|
|
214
376
|
* Normalize an orderBy value into `{ direction, nulls }`. Accepts a plain
|
|
215
377
|
* direction string or an {@link OrderBySpec}. Used by every ORDER BY compile
|