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/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`, Prisma's `take`
342
- * alias, then the client-level `defaultLimit`. Shared by {@link buildFind} and
343
- * the {@link findMany} zero short-circuit so the two can never disagree about
344
- * which limit is in force.
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 ?? args.take ?? this.defaultLimit;
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.withDeclaredRelationNames(args);
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.withDeclaredRelationNames(args);
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.withDeclaredRelationNames(args);
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 ?? args.take ?? this.defaultLimit;
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);
@@ -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
@@ -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.take !== undefined || args.offset !== undefined)
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?.take ?? args?.limit ?? this.defaultLimit;
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.withDeclaredRelationNames(args);
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.withDeclaredRelationNames(args);
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.withDeclaredRelationNames(args);
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.withDeclaredRelationNames(args);
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.withDeclaredRelationNames(args);
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?.take !== undefined || args?.cursor !== undefined;
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.withDeclaredRelationNames(args);
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?.take ?? args?.limit ?? this.defaultLimit;
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.withDeclaredRelationNames(args);
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 {};