turbine-orm 0.72.0 → 0.73.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 +6 -3
- package/dist/cjs/cli/index.d.ts +7 -0
- package/dist/cjs/cli/index.js +113 -0
- package/dist/cjs/powql.d.ts +17 -4
- package/dist/cjs/powql.js +43 -9
- package/dist/cjs/query/builder.d.ts +18 -0
- package/dist/cjs/query/builder.js +64 -12
- package/dist/cjs/query/compound-unique.d.ts +44 -0
- package/dist/cjs/query/compound-unique.js +119 -0
- package/dist/cjs/query/option-surface.d.ts +22 -0
- package/dist/cjs/query/option-surface.js +91 -0
- package/dist/cjs/query/types.d.ts +15 -1
- package/dist/cjs/query/utils.d.ts +23 -0
- package/dist/cjs/query/utils.js +57 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.js +113 -0
- package/dist/powql.d.ts +17 -4
- package/dist/powql.js +45 -11
- package/dist/query/builder.d.ts +18 -0
- package/dist/query/builder.js +66 -14
- package/dist/query/compound-unique.d.ts +44 -0
- package/dist/query/compound-unique.js +116 -0
- package/dist/query/option-surface.d.ts +22 -0
- package/dist/query/option-surface.js +90 -0
- package/dist/query/types.d.ts +15 -1
- package/dist/query/utils.d.ts +23 -0
- package/dist/query/utils.js +56 -0
- package/package.json +2 -1
- package/skills/turbine-orm/SKILL.md +247 -0
package/dist/powql.js
CHANGED
|
@@ -39,14 +39,15 @@ import { NotFoundError, ReadOnlyError, TimeoutError, UnsupportedFeatureError, Va
|
|
|
39
39
|
import { executeNestedCreate, executeNestedUpdate, hasRelationFields, } from './nested-write.js';
|
|
40
40
|
import { ALL_POWDB_CAPABILITIES, coerceNativeValue, isJsonColumn, isPowdbDatetimeColumn, isStaleFramePowdbError, PowdbFloatParam, PowdbJsonParam, powqlColumnType, quotePowqlDotted, quotePowqlIdent, requireCapability, rowToEntity, } from './powdb.js';
|
|
41
41
|
import { assertAggregatePiiOptIn } from './query/aggregates.js';
|
|
42
|
-
import { expandCompoundUniqueWhere } from './query/compound-unique.js';
|
|
42
|
+
import { assertWhereIdentifiesOneRow, expandCompoundUniqueWhere } from './query/compound-unique.js';
|
|
43
43
|
import { isJsonFilter, isRelationPickOrderBy, orderByEntries } from './query/filters.js';
|
|
44
|
+
import { warnUnknownQueryOptions } from './query/option-surface.js';
|
|
44
45
|
import { normalizeWithClause } from './query/relation-names.js';
|
|
45
46
|
// The privilege sentinel and its resolver: `includePii` / `allowFullTableScan`
|
|
46
47
|
// are unlocked ONLY by the UNSAFE symbol, on this engine exactly as on the SQL
|
|
47
48
|
// engines, so a spread request body cannot turn either on here either.
|
|
48
49
|
import { assertDirectionToken, resolveUnsafeFlag, UNSAFE } from './query/types.js';
|
|
49
|
-
import { escapeLike, ownLookup, relationInProjectionMessage, resolveColumnName, resolveRelationDef, selectNamesNothingMessage, selectOmitExclusiveMessage, } from './query/utils.js';
|
|
50
|
+
import { escapeLike, normalizePagination, ownLookup, relationInProjectionMessage, resolveColumnName, resolveRelationDef, selectNamesNothingMessage, selectOmitExclusiveMessage, } from './query/utils.js';
|
|
50
51
|
import { assertJsonFilterKeys, jsonStringEntries } from './query/where.js';
|
|
51
52
|
import { normalizeKeyColumns, snakeToCamel, } from './schema.js';
|
|
52
53
|
/**
|
|
@@ -338,13 +339,14 @@ export class PowqlInterface {
|
|
|
338
339
|
return this.pool.capabilities ?? ALL_POWDB_CAPABILITIES;
|
|
339
340
|
}
|
|
340
341
|
/**
|
|
341
|
-
* The `limit` a query actually emits: the explicit `limit`,
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
342
|
+
* The `limit` a query actually emits: the explicit `limit`, then the
|
|
343
|
+
* client-level `defaultLimit`. Prisma's `take` alias is already folded into
|
|
344
|
+
* `limit` by `normalizeArgs`, so there is one spelling by the time this runs.
|
|
345
|
+
* Shared by {@link buildFind} and the {@link findMany} zero short-circuit so
|
|
346
|
+
* the two can never disagree about which limit is in force.
|
|
345
347
|
*/
|
|
346
348
|
effectiveLimit(args) {
|
|
347
|
-
return args.limit ??
|
|
349
|
+
return args.limit ?? this.defaultLimit;
|
|
348
350
|
}
|
|
349
351
|
/**
|
|
350
352
|
* Reject a negative `limit` / `offset` before it reaches the engine. PowDB
|
|
@@ -383,12 +385,28 @@ export class PowqlInterface {
|
|
|
383
385
|
const normalized = normalizeWithClause(this.schema, this.table, args.with);
|
|
384
386
|
return normalized === args.with ? args : { ...args, with: normalized };
|
|
385
387
|
}
|
|
388
|
+
/**
|
|
389
|
+
* Caller args in canonical form: declared relation spellings, and `take` /
|
|
390
|
+
* `skip` folded into `limit` / `offset`.
|
|
391
|
+
*
|
|
392
|
+
* Same composition as `QueryInterface.normalizeArgs` and here for the same
|
|
393
|
+
* reason the method above is here: nothing about a parallel implementation
|
|
394
|
+
* makes a core rule arrive on its own, and an engine that reads `take` but
|
|
395
|
+
* not `skip` pages differently from one that reads both.
|
|
396
|
+
*/
|
|
397
|
+
normalizeArgs(args) {
|
|
398
|
+
return normalizePagination(this.withDeclaredRelationNames(args));
|
|
399
|
+
}
|
|
386
400
|
assertNoForceCustomPlan(args) {
|
|
387
401
|
if (args?.forceCustomPlan !== true)
|
|
388
402
|
return;
|
|
389
403
|
throw new UnsupportedFeatureError('The forceCustomPlan query option', 'powdb', 'Forcing a per-query custom plan means keeping the statement out of the PostgreSQL plan cache, and PowDB ' +
|
|
390
404
|
'has no such cache to keep it out of. Remove the option, or set it only on PostgreSQL queries.');
|
|
391
405
|
}
|
|
406
|
+
/** See query/compound-unique.ts: one rule and one message across engines. */
|
|
407
|
+
assertIdentifiesOneRow(where) {
|
|
408
|
+
assertWhereIdentifiesOneRow(this.meta, this.table, where);
|
|
409
|
+
}
|
|
392
410
|
assertPagination(limit, offset, context) {
|
|
393
411
|
for (const [name, value] of [
|
|
394
412
|
['limit', limit],
|
|
@@ -1302,6 +1320,11 @@ export class PowqlInterface {
|
|
|
1302
1320
|
}
|
|
1303
1321
|
/** Run a method body through the middleware chain (mirrors QueryInterface). */
|
|
1304
1322
|
async withMiddleware(action, args, executor) {
|
|
1323
|
+
// The unknown-key diagnostic, at the same seam and for the same reason as
|
|
1324
|
+
// `QueryInterface.executeWithMiddleware`. The option surface is the CORE
|
|
1325
|
+
// one because these args ARE core's args; PowDB reads them, it does not
|
|
1326
|
+
// define them.
|
|
1327
|
+
warnUnknownQueryOptions(this.table, action, args);
|
|
1305
1328
|
if (this.middlewares.length === 0)
|
|
1306
1329
|
return executor();
|
|
1307
1330
|
let index = 0;
|
|
@@ -1327,7 +1350,7 @@ export class PowqlInterface {
|
|
|
1327
1350
|
// -------------------------------------------------------------------------
|
|
1328
1351
|
async findMany(args = {}) {
|
|
1329
1352
|
this.assertNoForceCustomPlan(args);
|
|
1330
|
-
args = this.
|
|
1353
|
+
args = this.normalizeArgs(args);
|
|
1331
1354
|
return this.withMiddleware('findMany', args, async () => {
|
|
1332
1355
|
// `limit: 0` means "no rows" (SQL `LIMIT 0`), and answering it client-side
|
|
1333
1356
|
// is correct on every engine version: PowDB's projection fast path returned
|
|
@@ -1467,6 +1490,12 @@ export class PowqlInterface {
|
|
|
1467
1490
|
* too, so both engines agree.
|
|
1468
1491
|
*/
|
|
1469
1492
|
async explain(args = {}) {
|
|
1493
|
+
// `explain` must compile the statement `findMany` would run, so it
|
|
1494
|
+
// normalizes the same args the same way. Without this, explaining a query
|
|
1495
|
+
// written with `take` / `skip` would report a plan for a DIFFERENT
|
|
1496
|
+
// statement than the one that executes, which is the one thing a
|
|
1497
|
+
// diagnostic must not do.
|
|
1498
|
+
args = this.normalizeArgs(args);
|
|
1470
1499
|
const params = [];
|
|
1471
1500
|
const { powql } = await this.buildFind(args, params);
|
|
1472
1501
|
const { rows } = await this.exec(`explain ${powql}`, params, args.timeout, 'explain');
|
|
@@ -1479,7 +1508,7 @@ export class PowqlInterface {
|
|
|
1479
1508
|
}
|
|
1480
1509
|
async findUnique(args) {
|
|
1481
1510
|
this.assertNoForceCustomPlan(args);
|
|
1482
|
-
args = this.
|
|
1511
|
+
args = this.normalizeArgs(args);
|
|
1483
1512
|
// Prisma compound-unique selector → column conjunction (engine parity with
|
|
1484
1513
|
// the SQL findUnique family; pure metadata, so this is a one-line adoption).
|
|
1485
1514
|
if (args.where) {
|
|
@@ -1487,6 +1516,11 @@ export class PowqlInterface {
|
|
|
1487
1516
|
if (expanded !== args.where)
|
|
1488
1517
|
args = { ...args, where: expanded };
|
|
1489
1518
|
}
|
|
1519
|
+
// AFTER the selector expansion, so a compound selector counts as the key it
|
|
1520
|
+
// is. Same rule and same message as the SQL engines: a `where` that matches
|
|
1521
|
+
// many rows plus `limit 1` returns an arbitrary one of them, and PowDB has
|
|
1522
|
+
// no more of an ordering guarantee there than Postgres does.
|
|
1523
|
+
this.assertIdentifiesOneRow(args.where);
|
|
1490
1524
|
return this.withMiddleware('findUnique', args, async () => {
|
|
1491
1525
|
const { rows, native, nestedPlans, linkPlans, residualWith, forcedPk } = await this.runFind({ ...args, limit: 1 }, 'findUnique');
|
|
1492
1526
|
if (!rows.length)
|
|
@@ -1504,7 +1538,7 @@ export class PowqlInterface {
|
|
|
1504
1538
|
});
|
|
1505
1539
|
}
|
|
1506
1540
|
async findFirst(args = {}) {
|
|
1507
|
-
args = this.
|
|
1541
|
+
args = this.normalizeArgs(args);
|
|
1508
1542
|
this.assertNoForceCustomPlan(args);
|
|
1509
1543
|
return this.withMiddleware('findFirst', args, async () => {
|
|
1510
1544
|
const { rows, native, nestedPlans, linkPlans, residualWith, forcedPk } = await this.runFind({ ...args, limit: 1 }, 'findFirst');
|
|
@@ -1858,7 +1892,7 @@ export class PowqlInterface {
|
|
|
1858
1892
|
* one loader chunk (the loader limits per chunk, the join once globally).
|
|
1859
1893
|
*/
|
|
1860
1894
|
joinEligible(rel, opt, args, parentCount) {
|
|
1861
|
-
const effLimit = args.limit ??
|
|
1895
|
+
const effLimit = args.limit ?? this.defaultLimit;
|
|
1862
1896
|
if (effLimit !== undefined || args.offset)
|
|
1863
1897
|
return false;
|
|
1864
1898
|
const options = (opt === true ? {} : opt);
|
package/dist/query/builder.d.ts
CHANGED
|
@@ -551,6 +551,24 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
551
551
|
* query/relation-names.ts.
|
|
552
552
|
*/
|
|
553
553
|
private withDeclaredRelationNames;
|
|
554
|
+
/**
|
|
555
|
+
* Caller args in canonical form: declared relation spellings, and the Prisma
|
|
556
|
+
* pagination aliases folded into `limit` / `offset`.
|
|
557
|
+
*
|
|
558
|
+
* ONE method rather than two calls at each seam, because the two
|
|
559
|
+
* normalizations have the same requirement and the same failure mode: both
|
|
560
|
+
* must happen before `withFingerprint` / the cache key, and a seam that
|
|
561
|
+
* applies one but not the other is a seam where the alias survives into a
|
|
562
|
+
* fingerprint. Both return their input by reference when there was nothing to
|
|
563
|
+
* change, so the common path still allocates nothing.
|
|
564
|
+
*/
|
|
565
|
+
private normalizeArgs;
|
|
566
|
+
/**
|
|
567
|
+
* Refuse a `findUnique` whose `where` names no unique key. The rule and the
|
|
568
|
+
* message live in query/compound-unique.ts, beside the definition of what
|
|
569
|
+
* counts as unique and shared with the PowDB engine.
|
|
570
|
+
*/
|
|
571
|
+
private assertFindUniqueKey;
|
|
554
572
|
/**
|
|
555
573
|
* Fill a PK-ascending `orderBy` into every to-many `with` relation that has no
|
|
556
574
|
* explicit one, recursing into nested `with`. Returns a CLONED clause (user
|
package/dist/query/builder.js
CHANGED
|
@@ -17,12 +17,13 @@ import { executeNestedCreate, executeNestedUpdate, hasRelationFields, } from '..
|
|
|
17
17
|
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
|
-
import { expandCompoundUniqueWhere } from './compound-unique.js';
|
|
20
|
+
import { assertWhereIdentifiesOneRow, expandCompoundUniqueWhere } from './compound-unique.js';
|
|
21
21
|
import { dedupeColumnList, dedupeOrderEntries, isJsonPathOrderBy, isOrderBySpec, isRelationPickOrderBy, isVectorOrderBy, isWhereOperator, orderByEntries, sortedEntries, } from './filters.js';
|
|
22
|
+
import { warnUnknownQueryOptions } from './option-surface.js';
|
|
22
23
|
import { normalizeWithClause } from './relation-names.js';
|
|
23
24
|
import * as relationsMod from './relations.js';
|
|
24
25
|
import { resolveSkipGlobalFilters, resolveUnsafeFlag, UNSAFE } from './types.js';
|
|
25
|
-
import { isTemporalInfinity, LRUCache, ownLookup, parseDbDate, resolveColumnName, resolveRelation, resolveRelationDef, sqlToPreparedName, unknownFieldMessage, warnRedundantSortTerm, } from './utils.js';
|
|
26
|
+
import { isTemporalInfinity, LRUCache, normalizePagination, ownLookup, parseDbDate, resolveColumnName, resolveRelation, resolveRelationDef, sqlToPreparedName, unknownFieldMessage, warnRedundantSortTerm, } from './utils.js';
|
|
26
27
|
import { shouldWarnOnce, WARN_NS } from './warn-registry.js';
|
|
27
28
|
import * as whereMod from './where.js';
|
|
28
29
|
import * as writesMod from './writes.js';
|
|
@@ -1083,6 +1084,28 @@ export class QueryInterface {
|
|
|
1083
1084
|
const normalized = normalizeWithClause(this.schema, this.table, args.with);
|
|
1084
1085
|
return normalized === args.with ? args : { ...args, with: normalized };
|
|
1085
1086
|
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Caller args in canonical form: declared relation spellings, and the Prisma
|
|
1089
|
+
* pagination aliases folded into `limit` / `offset`.
|
|
1090
|
+
*
|
|
1091
|
+
* ONE method rather than two calls at each seam, because the two
|
|
1092
|
+
* normalizations have the same requirement and the same failure mode: both
|
|
1093
|
+
* must happen before `withFingerprint` / the cache key, and a seam that
|
|
1094
|
+
* applies one but not the other is a seam where the alias survives into a
|
|
1095
|
+
* fingerprint. Both return their input by reference when there was nothing to
|
|
1096
|
+
* change, so the common path still allocates nothing.
|
|
1097
|
+
*/
|
|
1098
|
+
normalizeArgs(args) {
|
|
1099
|
+
return normalizePagination(this.withDeclaredRelationNames(args));
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Refuse a `findUnique` whose `where` names no unique key. The rule and the
|
|
1103
|
+
* message live in query/compound-unique.ts, beside the definition of what
|
|
1104
|
+
* counts as unique and shared with the PowDB engine.
|
|
1105
|
+
*/
|
|
1106
|
+
assertFindUniqueKey(where) {
|
|
1107
|
+
assertWhereIdentifiesOneRow(this.tableMeta, this.table, where);
|
|
1108
|
+
}
|
|
1086
1109
|
/**
|
|
1087
1110
|
* Fill a PK-ascending `orderBy` into every to-many `with` relation that has no
|
|
1088
1111
|
* explicit one, recursing into nested `with`. Returns a CLONED clause (user
|
|
@@ -1222,7 +1245,7 @@ export class QueryInterface {
|
|
|
1222
1245
|
return false;
|
|
1223
1246
|
if (!isEmptyOrderBy(args.orderBy))
|
|
1224
1247
|
return false;
|
|
1225
|
-
if (args.limit !== undefined || args.
|
|
1248
|
+
if (args.limit !== undefined || args.offset !== undefined)
|
|
1226
1249
|
return true;
|
|
1227
1250
|
return this.cursorFields(args.cursor).length > 0;
|
|
1228
1251
|
}
|
|
@@ -1287,7 +1310,6 @@ export class QueryInterface {
|
|
|
1287
1310
|
const shape = [
|
|
1288
1311
|
cursorFields.length > 0 ? 'cursor' : '',
|
|
1289
1312
|
args?.limit !== undefined ? 'limit' : '',
|
|
1290
|
-
args?.take !== undefined ? 'take' : '',
|
|
1291
1313
|
args?.offset !== undefined ? 'offset' : '',
|
|
1292
1314
|
]
|
|
1293
1315
|
.filter(Boolean)
|
|
@@ -1417,7 +1439,7 @@ export class QueryInterface {
|
|
|
1417
1439
|
* not an estimate.
|
|
1418
1440
|
*/
|
|
1419
1441
|
autoParentBound(args) {
|
|
1420
|
-
return args?.
|
|
1442
|
+
return args?.limit ?? this.defaultLimit;
|
|
1421
1443
|
}
|
|
1422
1444
|
/**
|
|
1423
1445
|
* The parent-row count at which `'auto'` stops preferring the single-statement
|
|
@@ -1741,7 +1763,7 @@ export class QueryInterface {
|
|
|
1741
1763
|
// Declared relation spellings first, exactly as the join path does in
|
|
1742
1764
|
// buildFindMany: the loader reads `args.with` itself, so without this the
|
|
1743
1765
|
// two strategies would disagree about which relation names are valid.
|
|
1744
|
-
args = this.
|
|
1766
|
+
args = this.normalizeArgs(args);
|
|
1745
1767
|
// Stable relation order (opt-in): the batched loader forwards each relation's
|
|
1746
1768
|
// orderBy into its follow-up query, so filling the synthesized PK order here
|
|
1747
1769
|
// makes the batched output deterministic exactly like the join path.
|
|
@@ -2128,6 +2150,12 @@ export class QueryInterface {
|
|
|
2128
2150
|
*/
|
|
2129
2151
|
async executeWithMiddleware(action, args, executor) {
|
|
2130
2152
|
this.currentAction = action;
|
|
2153
|
+
// Every public operation passes through here with its own name and the
|
|
2154
|
+
// caller's args, which makes this the one place the unknown-key diagnostic
|
|
2155
|
+
// can be complete. Putting it in each method instead would mean fifteen
|
|
2156
|
+
// sites and a new one every time an operation is added, which is precisely
|
|
2157
|
+
// how the surface it checks came to need checking.
|
|
2158
|
+
warnUnknownQueryOptions(this.table, action, args);
|
|
2131
2159
|
if (this.middlewares.length === 0) {
|
|
2132
2160
|
return executor();
|
|
2133
2161
|
}
|
|
@@ -2156,7 +2184,7 @@ export class QueryInterface {
|
|
|
2156
2184
|
// changes the warning text from the declared name back to the caller's and
|
|
2157
2185
|
// nothing else). It is here so the invariant holds at the seam rather than
|
|
2158
2186
|
// depending on every consumer to re-establish it.
|
|
2159
|
-
args = this.
|
|
2187
|
+
args = this.normalizeArgs(args);
|
|
2160
2188
|
return this.executeWithMiddleware('findUnique', args, async () => {
|
|
2161
2189
|
if (args.with) {
|
|
2162
2190
|
const strategy = this.resolveLoadStrategy(args.relationLoadStrategy);
|
|
@@ -2185,7 +2213,7 @@ export class QueryInterface {
|
|
|
2185
2213
|
*/
|
|
2186
2214
|
async runFindUniqueBatched(args) {
|
|
2187
2215
|
// Declared relation spellings first, see runFindManyBatched.
|
|
2188
|
-
args = this.
|
|
2216
|
+
args = this.normalizeArgs(args);
|
|
2189
2217
|
// Stable relation order (opt-in), see runFindManyBatched.
|
|
2190
2218
|
const withClause = this.resolveStableOrder(args.stableRelationOrder)
|
|
2191
2219
|
? this.applyStableRelationOrder(args.with, this.table)
|
|
@@ -2239,9 +2267,22 @@ export class QueryInterface {
|
|
|
2239
2267
|
'A key whose value is `undefined` does not count, check that the value you are looking up is defined. ' +
|
|
2240
2268
|
'If you meant "any row matching an optional filter", use `findFirst`.');
|
|
2241
2269
|
}
|
|
2270
|
+
// ...and a where that HAS a predicate but does not name a unique key is the
|
|
2271
|
+
// same hazard one step along (0.73.0). `findUnique({ where: { status:
|
|
2272
|
+
// 'active' } })` used to emit `WHERE status = $1 LIMIT 1` with no ORDER BY:
|
|
2273
|
+
// one row out of many, chosen by the engine, different between two calls
|
|
2274
|
+
// with the same argument and between two plans for the same call. The
|
|
2275
|
+
// caller who wrote `findUnique` asked for the row, not a row, and the
|
|
2276
|
+
// `null` branch they wrote reads as "no such row" when it means "none
|
|
2277
|
+
// matched this filter".
|
|
2278
|
+
//
|
|
2279
|
+
// Checked against the USER's where for the same reason as the guard above:
|
|
2280
|
+
// a global filter is not an identity, and letting one satisfy this would
|
|
2281
|
+
// hand back an arbitrary row from inside the tenant.
|
|
2282
|
+
this.assertFindUniqueKey(args.where);
|
|
2242
2283
|
// Declared relation spellings first, before stable-order and the
|
|
2243
2284
|
// fingerprint (see buildFindMany).
|
|
2244
|
-
args = this.
|
|
2285
|
+
args = this.normalizeArgs(args);
|
|
2245
2286
|
// Stable relation order (opt-in): fill PK-asc orderBy into unordered to-many
|
|
2246
2287
|
// relations before fingerprinting (see buildFindMany).
|
|
2247
2288
|
if (args.with && this.resolveStableOrder(args.stableRelationOrder)) {
|
|
@@ -2390,7 +2431,7 @@ export class QueryInterface {
|
|
|
2390
2431
|
// changes the warning text from the declared name back to the caller's and
|
|
2391
2432
|
// nothing else). It is here so the invariant holds at the seam rather than
|
|
2392
2433
|
// depending on every consumer to re-establish it.
|
|
2393
|
-
args = this.
|
|
2434
|
+
args = this.normalizeArgs(args);
|
|
2394
2435
|
this.maybeWarnUnlimited(args);
|
|
2395
2436
|
this.maybeWarnUnorderedPage(args);
|
|
2396
2437
|
// Dev-only: warn on deeply nested with clauses
|
|
@@ -2498,7 +2539,7 @@ export class QueryInterface {
|
|
|
2498
2539
|
return;
|
|
2499
2540
|
if (this.defaultLimit !== undefined)
|
|
2500
2541
|
return;
|
|
2501
|
-
const hasExplicitLimit = args?.limit !== undefined || args?.
|
|
2542
|
+
const hasExplicitLimit = args?.limit !== undefined || args?.cursor !== undefined;
|
|
2502
2543
|
if (hasExplicitLimit)
|
|
2503
2544
|
return;
|
|
2504
2545
|
if (this.whereMatchesAtMostOneRow(args?.where))
|
|
@@ -2577,7 +2618,7 @@ export class QueryInterface {
|
|
|
2577
2618
|
// pass and the fingerprint below and before any of the six `with` walkers,
|
|
2578
2619
|
// so none of them needs to know a relation has two accepted spellings and
|
|
2579
2620
|
// both spellings share one cache entry. See query/relation-names.ts.
|
|
2580
|
-
args = this.
|
|
2621
|
+
args = this.normalizeArgs(args);
|
|
2581
2622
|
// Stable relation order (opt-in): fill PK-asc orderBy into unordered to-many
|
|
2582
2623
|
// relations BEFORE fingerprinting, so the two orderings get distinct cache
|
|
2583
2624
|
// entries and every downstream path (SQL build, collect, parser) inherits it.
|
|
@@ -2708,7 +2749,7 @@ export class QueryInterface {
|
|
|
2708
2749
|
// the caller's column order, so a permuted array rebuilds different SQL and
|
|
2709
2750
|
// must not collapse onto the same cache entry (would trip the cross-check).
|
|
2710
2751
|
const distinctFp = args?.distinct ? args.distinct.join(',') : '';
|
|
2711
|
-
const effectiveLimit = args?.
|
|
2752
|
+
const effectiveLimit = args?.limit ?? this.defaultLimit;
|
|
2712
2753
|
// On engines that inline the literal LIMIT/OFFSET into the SQL text
|
|
2713
2754
|
// (dialect.inlineLimitOffset, MySQL), the value is part of the SQL, not the
|
|
2714
2755
|
// params, so it MUST be part of the fingerprint or two different limits share
|
|
@@ -2994,8 +3035,19 @@ export class QueryInterface {
|
|
|
2994
3035
|
* non-empty.
|
|
2995
3036
|
*/
|
|
2996
3037
|
async *streamRaw(args, action) {
|
|
3038
|
+
// Pagination aliases folded BEFORE the speculative build below, which
|
|
3039
|
+
// spreads `args` and overrides `limit`. A surviving `take` would reach that
|
|
3040
|
+
// spread alongside the override and be read as two different values for one
|
|
3041
|
+
// bound. Relation names are left to `buildFindMany`, which both branches go
|
|
3042
|
+
// through.
|
|
3043
|
+
args = normalizePagination(args);
|
|
2997
3044
|
const batchSize = Math.max(1, Math.floor(Number(args?.batchSize ?? 1000)));
|
|
2998
3045
|
this.currentAction = action;
|
|
3046
|
+
// The two streaming methods do NOT go through `executeWithMiddleware`, so
|
|
3047
|
+
// the unknown-key diagnostic has to be hung here as well or a stream would
|
|
3048
|
+
// be the one read that silently drops an `include`. Both public methods
|
|
3049
|
+
// reach this, and each passes its own name.
|
|
3050
|
+
warnUnknownQueryOptions(this.table, action, args);
|
|
2999
3051
|
// Streaming is ALREADY immune to the generic-plan cliff: the speculative
|
|
3000
3052
|
// fetch has never passed a prepared name, and the cursor path runs through
|
|
3001
3053
|
// DECLARE, so neither statement enters the plan cache. `preparedNameFor` is
|
|
@@ -3176,7 +3228,7 @@ export class QueryInterface {
|
|
|
3176
3228
|
// changes the warning text from the declared name back to the caller's and
|
|
3177
3229
|
// nothing else). It is here so the invariant holds at the seam rather than
|
|
3178
3230
|
// depending on every consumer to re-establish it.
|
|
3179
|
-
args = this.
|
|
3231
|
+
args = this.normalizeArgs(args);
|
|
3180
3232
|
return this.executeWithMiddleware('findFirst', (args ?? {}), async () => {
|
|
3181
3233
|
if (args?.with) {
|
|
3182
3234
|
const strategy = this.resolveLoadStrategy(args.relationLoadStrategy);
|
|
@@ -49,3 +49,47 @@ import type { TableMetadata } from '../schema.js';
|
|
|
49
49
|
* unknown-column error at SQL-build time.
|
|
50
50
|
*/
|
|
51
51
|
export declare function expandCompoundUniqueWhere(meta: TableMetadata, where: Record<string, unknown>): Record<string, unknown>;
|
|
52
|
+
/**
|
|
53
|
+
* Every column set that identifies AT MOST ONE ROW of this table.
|
|
54
|
+
*
|
|
55
|
+
* Same sources and the same partial-index rule as {@link syntheticKeyMap}, and
|
|
56
|
+
* in this module for that reason: "what identifies one row" is one question,
|
|
57
|
+
* and answering it in two places is how a compound selector comes to be
|
|
58
|
+
* accepted by the name and refused by its members (0.72.0 fixed exactly that).
|
|
59
|
+
* The difference is only the arity: a synthetic SELECTOR needs two or more
|
|
60
|
+
* columns to have a joined name, while a single-column unique identifies a row
|
|
61
|
+
* perfectly well.
|
|
62
|
+
*/
|
|
63
|
+
/**
|
|
64
|
+
* Throw unless `where` identifies a single row. The refusal for `findUnique` on
|
|
65
|
+
* EVERY engine, message included.
|
|
66
|
+
*
|
|
67
|
+
* Shared rather than written twice because `PowqlInterface` is a parallel
|
|
68
|
+
* implementation: two copies of a rule this specific (which sources count as
|
|
69
|
+
* unique, whether a null identifies, which keys the message lists) is how two
|
|
70
|
+
* engines come to disagree about whether a query is valid, which is the exact
|
|
71
|
+
* divergence 0.64.0 and 0.72.0 were both spent on.
|
|
72
|
+
*
|
|
73
|
+
* The message lists the keys that WOULD work, because the fix is almost always
|
|
74
|
+
* one of them and a caller cannot be expected to know which columns the
|
|
75
|
+
* database considers unique. A table with no unique key at all gets its own
|
|
76
|
+
* sentence: no `where` satisfies this, and "name a unique key" is advice that
|
|
77
|
+
* person cannot take.
|
|
78
|
+
*/
|
|
79
|
+
export declare function assertWhereIdentifiesOneRow(meta: TableMetadata, table: string, where: Record<string, unknown> | undefined): void;
|
|
80
|
+
export declare function uniqueKeyNames(meta: TableMetadata): string[][];
|
|
81
|
+
/**
|
|
82
|
+
* True when `where` pins every column of at least one unique key to a single
|
|
83
|
+
* value, so the row it names is the row it gets.
|
|
84
|
+
*
|
|
85
|
+
* Deliberately reads only the TOP LEVEL of the user's where. A unique key
|
|
86
|
+
* buried inside an `OR` does not identify a row (the other branch matches
|
|
87
|
+
* whatever it matches), and one inside an `AND` array is a shape nobody writes
|
|
88
|
+
* for a lookup by identity. Extra predicates alongside the key are fine: they
|
|
89
|
+
* can only narrow a set that already holds at most one row.
|
|
90
|
+
*
|
|
91
|
+
* A NULL is not an identity. `WHERE email IS NULL` matches every row whose
|
|
92
|
+
* email is null, which a UNIQUE constraint permits any number of, so a null
|
|
93
|
+
* value satisfies no key here even on a unique column.
|
|
94
|
+
*/
|
|
95
|
+
export declare function whereIdentifiesOneRow(meta: TableMetadata, where: Record<string, unknown>): boolean;
|
|
@@ -200,3 +200,119 @@ export function expandCompoundUniqueWhere(meta, where) {
|
|
|
200
200
|
}
|
|
201
201
|
return result ?? where;
|
|
202
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Every column set that identifies AT MOST ONE ROW of this table.
|
|
205
|
+
*
|
|
206
|
+
* Same sources and the same partial-index rule as {@link syntheticKeyMap}, and
|
|
207
|
+
* in this module for that reason: "what identifies one row" is one question,
|
|
208
|
+
* and answering it in two places is how a compound selector comes to be
|
|
209
|
+
* accepted by the name and refused by its members (0.72.0 fixed exactly that).
|
|
210
|
+
* The difference is only the arity: a synthetic SELECTOR needs two or more
|
|
211
|
+
* columns to have a joined name, while a single-column unique identifies a row
|
|
212
|
+
* perfectly well.
|
|
213
|
+
*/
|
|
214
|
+
/**
|
|
215
|
+
* Throw unless `where` identifies a single row. The refusal for `findUnique` on
|
|
216
|
+
* EVERY engine, message included.
|
|
217
|
+
*
|
|
218
|
+
* Shared rather than written twice because `PowqlInterface` is a parallel
|
|
219
|
+
* implementation: two copies of a rule this specific (which sources count as
|
|
220
|
+
* unique, whether a null identifies, which keys the message lists) is how two
|
|
221
|
+
* engines come to disagree about whether a query is valid, which is the exact
|
|
222
|
+
* divergence 0.64.0 and 0.72.0 were both spent on.
|
|
223
|
+
*
|
|
224
|
+
* The message lists the keys that WOULD work, because the fix is almost always
|
|
225
|
+
* one of them and a caller cannot be expected to know which columns the
|
|
226
|
+
* database considers unique. A table with no unique key at all gets its own
|
|
227
|
+
* sentence: no `where` satisfies this, and "name a unique key" is advice that
|
|
228
|
+
* person cannot take.
|
|
229
|
+
*/
|
|
230
|
+
export function assertWhereIdentifiesOneRow(meta, table, where) {
|
|
231
|
+
if (whereIdentifiesOneRow(meta, where ?? {}))
|
|
232
|
+
return;
|
|
233
|
+
const field = (c) => meta.reverseColumnMap[c] ?? c;
|
|
234
|
+
const keys = uniqueKeyNames(meta).map((cols) => cols.length === 1 ? `\`${field(cols[0])}\`` : `\`{ ${cols.map(field).join(', ')} }\``);
|
|
235
|
+
const advice = keys.length > 0
|
|
236
|
+
? `Name a unique key (${keys.join(', ')}), or use \`findFirst\` if you meant "any row matching a filter".`
|
|
237
|
+
: `Table "${table}" declares no primary key and no unique constraint, so no \`where\` can identify one row ` +
|
|
238
|
+
'here. Use `findFirst` (add an `orderBy` to make which row it is deterministic).';
|
|
239
|
+
throw new ValidationError(`[turbine] findUnique on "${table}" refused: the \`where\` clause does not identify a single row, ` +
|
|
240
|
+
`so this would return an arbitrary one of the rows that match. ${advice}`);
|
|
241
|
+
}
|
|
242
|
+
export function uniqueKeyNames(meta) {
|
|
243
|
+
return dedupeColumnSets(uniqueColumnSets(meta));
|
|
244
|
+
}
|
|
245
|
+
/** Distinct column sets, preserving first-seen order (a PK is often also a declared unique). */
|
|
246
|
+
function dedupeColumnSets(sets) {
|
|
247
|
+
const seen = new Set();
|
|
248
|
+
const out = [];
|
|
249
|
+
for (const cols of sets) {
|
|
250
|
+
const sig = cols.join('\u0000');
|
|
251
|
+
if (seen.has(sig))
|
|
252
|
+
continue;
|
|
253
|
+
seen.add(sig);
|
|
254
|
+
out.push(cols);
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
function uniqueColumnSets(meta) {
|
|
259
|
+
const sets = [];
|
|
260
|
+
if (meta.primaryKey.length > 0)
|
|
261
|
+
sets.push(meta.primaryKey);
|
|
262
|
+
for (const uc of meta.uniqueColumns)
|
|
263
|
+
if (uc.length > 0)
|
|
264
|
+
sets.push(uc);
|
|
265
|
+
for (const idx of meta.indexes) {
|
|
266
|
+
if (idx.unique && !idx.docPath && !idx.partial && idx.columns.length > 0)
|
|
267
|
+
sets.push(idx.columns);
|
|
268
|
+
}
|
|
269
|
+
return sets;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* True when `where` pins every column of at least one unique key to a single
|
|
273
|
+
* value, so the row it names is the row it gets.
|
|
274
|
+
*
|
|
275
|
+
* Deliberately reads only the TOP LEVEL of the user's where. A unique key
|
|
276
|
+
* buried inside an `OR` does not identify a row (the other branch matches
|
|
277
|
+
* whatever it matches), and one inside an `AND` array is a shape nobody writes
|
|
278
|
+
* for a lookup by identity. Extra predicates alongside the key are fine: they
|
|
279
|
+
* can only narrow a set that already holds at most one row.
|
|
280
|
+
*
|
|
281
|
+
* A NULL is not an identity. `WHERE email IS NULL` matches every row whose
|
|
282
|
+
* email is null, which a UNIQUE constraint permits any number of, so a null
|
|
283
|
+
* value satisfies no key here even on a unique column.
|
|
284
|
+
*/
|
|
285
|
+
export function whereIdentifiesOneRow(meta, where) {
|
|
286
|
+
const pinned = new Set();
|
|
287
|
+
for (const [key, value] of Object.entries(where)) {
|
|
288
|
+
if (!isPinnedToOneValue(value))
|
|
289
|
+
continue;
|
|
290
|
+
const column = resolveColumnName(meta, key);
|
|
291
|
+
if (column !== undefined)
|
|
292
|
+
pinned.add(column);
|
|
293
|
+
}
|
|
294
|
+
if (pinned.size === 0)
|
|
295
|
+
return false;
|
|
296
|
+
return uniqueColumnSets(meta).some((cols) => cols.every((c) => pinned.has(c)));
|
|
297
|
+
}
|
|
298
|
+
/** A bare value, or an operator object whose `equals` is a value. */
|
|
299
|
+
function isPinnedToOneValue(value) {
|
|
300
|
+
if (value === undefined || value === null)
|
|
301
|
+
return false;
|
|
302
|
+
if (isWhereOperator(value)) {
|
|
303
|
+
const eq = value.equals;
|
|
304
|
+
return eq !== undefined && eq !== null;
|
|
305
|
+
}
|
|
306
|
+
// A JSON / array / vector filter narrows, it does not identify.
|
|
307
|
+
if (isJsonFilter(value) || isArrayFilter(value) || isVectorFilter(value))
|
|
308
|
+
return false;
|
|
309
|
+
// Anything else that is a plain object is a relation filter or a sub-where,
|
|
310
|
+
// neither of which pins a column. Dates, Buffers and primitives are values.
|
|
311
|
+
return !isPlainObject(value);
|
|
312
|
+
}
|
|
313
|
+
function isPlainObject(value) {
|
|
314
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
315
|
+
return false;
|
|
316
|
+
const proto = Object.getPrototypeOf(value);
|
|
317
|
+
return proto === Object.prototype || proto === null;
|
|
318
|
+
}
|
|
@@ -108,4 +108,26 @@ export declare const ALL_OPTION_TABLES: Readonly<Record<string, Readonly<Record<
|
|
|
108
108
|
export declare function applyNativeOptions(table: Readonly<Record<string, OptionKind>>, src: Record<string, unknown>, dst: Record<string, unknown>): void;
|
|
109
109
|
/** The keys of `table` with the given kind, as a set. */
|
|
110
110
|
export declare function optionKeysOfKind(table: Readonly<Record<string, OptionKind>>, ...kinds: OptionKind[]): string[];
|
|
111
|
+
/**
|
|
112
|
+
* Dev-mode warning for a key that is not part of the operation's option
|
|
113
|
+
* surface, and is therefore doing nothing.
|
|
114
|
+
*
|
|
115
|
+
* The motivating case is `include`. It is Prisma's word for `with`, it is what
|
|
116
|
+
* a model or a developer coming from Prisma reaches for first, and an
|
|
117
|
+
* unrecognized key is simply ignored: the query runs, returns rows, and the
|
|
118
|
+
* relation the caller asked for is absent. No error, no empty array, just a
|
|
119
|
+
* missing key on every row. A cross-model eval measured this as the single
|
|
120
|
+
* largest source of confidently-wrong queries against Turbine, and every one of
|
|
121
|
+
* them looked like a success from inside the process.
|
|
122
|
+
*
|
|
123
|
+
* A WARNING and never an error, deliberately. Refusing an unknown key would
|
|
124
|
+
* break `findMany({ ...someOptionsBag })`, which is ordinary code, and the
|
|
125
|
+
* option surface grows: a caller pinned to an older minor would have their
|
|
126
|
+
* working query start throwing. A warning costs a correct program nothing and
|
|
127
|
+
* tells an incorrect one exactly what happened.
|
|
128
|
+
*
|
|
129
|
+
* Dev-only, once per `table.operation.key` per process, and total: the whole
|
|
130
|
+
* body is wrapped, because a diagnostic must never be the reason a query fails.
|
|
131
|
+
*/
|
|
132
|
+
export declare function warnUnknownQueryOptions(table: string, operation: string, args: unknown): void;
|
|
111
133
|
export {};
|