turbine-orm 0.56.0 → 0.58.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.
@@ -31,6 +31,15 @@
31
31
  * atomic.
32
32
  * - **Result reshaping**: `_count` objects keyed back to Prisma relation names,
33
33
  * and to-one relations surfaced as `object | null`.
34
+ * - **Turbine-native query options** (`timeout`, `forceCustomPlan`,
35
+ * `warnOnUnlimited`, `skipGlobalFilters`, `stableRelationOrder`,
36
+ * `allowFullTableScan`, `includePii`, `optimisticLock`, `distinctOn`) reach
37
+ * core on every operation whose arg surface declares them. The set is not a
38
+ * hand-maintained list here: it comes from the compiler-checked tables in
39
+ * `query/option-surface.ts`, so a new core option cannot be silently stranded
40
+ * by this layer, and a key that is neither a Prisma arg nor a turbine option
41
+ * gets a dev-mode warning instead of vanishing (see
42
+ * {@link PRISMA_ARG_KEYS} and `warnUnknownQueryOptions`).
34
43
  *
35
44
  * ## What it deliberately does NOT do (documented divergences)
36
45
  *
@@ -63,6 +72,14 @@
63
72
  * exclusive-cursor + `offset` translation.
64
73
  * - **Negative `take`** (take-from-end) and **`skip` on a nested relation
65
74
  * include** throw, Turbine's `with` clause has no offset and no reverse-take.
75
+ * - **`limit` on `updateMany` / `deleteMany`** (Prisma 6.7+) throws. Turbine has
76
+ * no row-bounded mass mutation, and dropping a SAFETY BOUND with a warning
77
+ * would turn "change at most 10 rows" into "change every matching row".
78
+ * - **Write projections** (`select` / `include` / `omit` on
79
+ * create/update/delete/upsert), **`select` on `count`**, and
80
+ * **`orderBy` / `cursor` / `take` / `skip` on `aggregate`** are accepted and
81
+ * IGNORED (they are legitimate Prisma, so they never warn); the full row / a
82
+ * plain number comes back.
66
83
  *
67
84
  * ## Type dependencies (0.41.0)
68
85
  *
@@ -94,7 +111,9 @@
94
111
  */
95
112
  import { TurbineError, TurbineErrorCode, UnsupportedFeatureError, ValidationError, wrapPgError } from './errors.js';
96
113
  import { createManyShapeRuns } from './nested-write.js';
97
- import { shouldWarnOnce } from './query/warn-registry.js';
114
+ import { AGGREGATE_OPTIONS, applyNativeOptions, COUNT_OPTIONS, CREATE_MANY_OPTIONS, CREATE_OPTIONS, DELETE_MANY_OPTIONS, DELETE_OPTIONS, FIND_MANY_OPTIONS, FIND_UNIQUE_OPTIONS, GROUP_BY_OPTIONS, optionKeysOfKind, UPDATE_MANY_OPTIONS, UPDATE_OPTIONS, UPSERT_OPTIONS, } from './query/index.js';
115
+ import { suggestKey } from './query/utils.js';
116
+ import { shouldWarnOnce, WARN_NS } from './query/warn-registry.js';
98
117
  // ---------------------------------------------------------------------------
99
118
  // Prisma.sql-style raw fragments (local, minimal, never imports @prisma/client)
100
119
  // ---------------------------------------------------------------------------
@@ -252,6 +271,199 @@ function isPlainObject(v) {
252
271
  function renameField(mm, prismaField) {
253
272
  return mm.fields[prismaField] ?? prismaField;
254
273
  }
274
+ /** The turbine arg surface each operation compiles into. */
275
+ const OPERATION_TABLES = {
276
+ findMany: FIND_MANY_OPTIONS,
277
+ findFirst: FIND_MANY_OPTIONS,
278
+ findFirstOrThrow: FIND_MANY_OPTIONS,
279
+ findUnique: FIND_UNIQUE_OPTIONS,
280
+ findUniqueOrThrow: FIND_UNIQUE_OPTIONS,
281
+ create: CREATE_OPTIONS,
282
+ createMany: CREATE_MANY_OPTIONS,
283
+ update: UPDATE_OPTIONS,
284
+ updateMany: UPDATE_MANY_OPTIONS,
285
+ delete: DELETE_OPTIONS,
286
+ deleteMany: DELETE_MANY_OPTIONS,
287
+ upsert: UPSERT_OPTIONS,
288
+ count: COUNT_OPTIONS,
289
+ aggregate: AGGREGATE_OPTIONS,
290
+ groupBy: GROUP_BY_OPTIONS,
291
+ };
292
+ /**
293
+ * Every argument key Prisma itself accepts, per operation.
294
+ *
295
+ * Extracted from a generated `@prisma/client` 7.9.0 `index.d.ts` (the
296
+ * `<Model><Op>Args` blocks). It must stay the UNION across the Prisma majors
297
+ * this adapter supports, never one version's set: a key a newer major
298
+ * introduces should degrade to one noisy dev line, never to a throw, and a key
299
+ * an older major had must keep working. The drift test in
300
+ * `src/test/prisma-compat-option-surface.test.ts` re-extracts from a generated
301
+ * client when one is present and asserts this stays a superset.
302
+ */
303
+ export const PRISMA_ARG_KEYS = {
304
+ findMany: [
305
+ 'select',
306
+ 'omit',
307
+ 'include',
308
+ 'where',
309
+ 'orderBy',
310
+ 'cursor',
311
+ 'take',
312
+ 'skip',
313
+ 'distinct',
314
+ 'relationLoadStrategy',
315
+ ],
316
+ findFirst: [
317
+ 'select',
318
+ 'omit',
319
+ 'include',
320
+ 'where',
321
+ 'orderBy',
322
+ 'cursor',
323
+ 'take',
324
+ 'skip',
325
+ 'distinct',
326
+ 'relationLoadStrategy',
327
+ ],
328
+ findFirstOrThrow: [
329
+ 'select',
330
+ 'omit',
331
+ 'include',
332
+ 'where',
333
+ 'orderBy',
334
+ 'cursor',
335
+ 'take',
336
+ 'skip',
337
+ 'distinct',
338
+ 'relationLoadStrategy',
339
+ ],
340
+ findUnique: ['select', 'omit', 'include', 'where', 'relationLoadStrategy'],
341
+ findUniqueOrThrow: ['select', 'omit', 'include', 'where', 'relationLoadStrategy'],
342
+ create: ['select', 'omit', 'include', 'data', 'relationLoadStrategy'],
343
+ createMany: ['data', 'skipDuplicates'],
344
+ update: ['select', 'omit', 'include', 'data', 'where', 'relationLoadStrategy'],
345
+ updateMany: ['data', 'where', 'limit'],
346
+ delete: ['select', 'omit', 'include', 'where', 'relationLoadStrategy'],
347
+ deleteMany: ['where', 'limit'],
348
+ upsert: ['select', 'omit', 'include', 'where', 'create', 'update', 'relationLoadStrategy'],
349
+ count: ['where', 'orderBy', 'cursor', 'take', 'skip', 'select'],
350
+ aggregate: ['where', 'orderBy', 'cursor', 'take', 'skip', '_count', '_avg', '_sum', '_min', '_max'],
351
+ groupBy: ['where', 'orderBy', 'by', 'having', 'take', 'skip', '_count', '_avg', '_sum', '_min', '_max'],
352
+ };
353
+ /**
354
+ * Turbine spellings whose Prisma equivalent is spelled differently. A caller
355
+ * who reaches for one of these is not confused about the NAME, they are
356
+ * confused about which surface they are on, so they get a specific message
357
+ * rather than a fuzzy did-you-mean.
358
+ */
359
+ const ALIAS_HINT = {
360
+ limit: 'take',
361
+ offset: 'skip',
362
+ with: 'include',
363
+ };
364
+ /**
365
+ * Known(op) = Prisma's own keys for that operation, plus every key the
366
+ * operation's turbine arg surface declares except the `'internal'` ones.
367
+ *
368
+ * The second half is NOT just the `'native'` keys. Two turbine-only options are
369
+ * classified `'prisma'` because their values carry field names and so must be
370
+ * hand-translated (`optimisticLock`, `distinctOn`); they are fully honoured, and
371
+ * warning about a key the adapter just acted on would be the worst possible
372
+ * diagnostic. `'nativeAlias'` members are in the set on purpose too: `limit` on
373
+ * `findMany` IS recognized, it simply gets the alias message rather than the
374
+ * generic one. Only `'internal'` is excluded, because nothing on this surface
375
+ * can reach it.
376
+ *
377
+ * A `Set` rather than an `in` test on a record, so an inherited
378
+ * `Object.prototype` name (`toString`, `constructor`) is treated as the unknown
379
+ * key it is.
380
+ */
381
+ const KNOWN_KEYS = (() => {
382
+ const out = {};
383
+ for (const op of Object.keys(OPERATION_TABLES)) {
384
+ out[op] = new Set([
385
+ ...PRISMA_ARG_KEYS[op],
386
+ ...optionKeysOfKind(OPERATION_TABLES[op], 'prisma', 'native', 'nativeAlias'),
387
+ ]);
388
+ }
389
+ return out;
390
+ })();
391
+ /**
392
+ * Dev-mode notice for a key on a delegate's args object that this operation has
393
+ * no meaning for.
394
+ *
395
+ * An unrecognized key is silently ignored (JavaScript objects have no schema),
396
+ * which makes a typo, a turbine spelling, and a genuinely missing feature all
397
+ * look identical: nothing happens. The client-config warner
398
+ * (`warnUnknownConfigKeys` in client.ts) exists for the same reason and this is
399
+ * its query-level twin, down to the ranking of the suggestion.
400
+ *
401
+ * Deliberately a WARNING, never an error. Throwing would turn a working app
402
+ * into a failing one on upgrade over one stray key, and a key from a Prisma
403
+ * major newer than {@link PRISMA_ARG_KEYS} would be exactly that key. The whole
404
+ * body is wrapped so a hostile or exotic args object (a Proxy whose `ownKeys`
405
+ * throws) can never be the reason a query fails. Dev-only and once per
406
+ * `model.operation.key` per process, like every other advisory here.
407
+ */
408
+ function warnUnknownQueryOptions(model, op, args) {
409
+ if (process.env.NODE_ENV === 'production')
410
+ return;
411
+ if (!isPlainObject(args))
412
+ return;
413
+ try {
414
+ const known = KNOWN_KEYS[op];
415
+ const table = OPERATION_TABLES[op];
416
+ for (const key of Object.keys(args)) {
417
+ // `{ ...maybeOptions }` routinely materializes keys with no value.
418
+ // Nothing is being dropped when the value is undefined.
419
+ if (args[key] === undefined)
420
+ continue;
421
+ const alias = table[key] === 'nativeAlias' ? ALIAS_HINT[key] : undefined;
422
+ if (!alias && known.has(key))
423
+ continue;
424
+ if (!shouldWarnOnce(WARN_NS.unknownQueryOption, `${model}.${op}.${key}`))
425
+ continue;
426
+ if (alias) {
427
+ console.warn(`[turbine] prisma-compat: "${key}" is Turbine's spelling and is ignored here;` +
428
+ ` prisma-compat takes Prisma's "${alias}". (${model}.${op})`);
429
+ continue;
430
+ }
431
+ const suggestion = suggestKey(key, known);
432
+ console.warn(`[turbine] prisma-compat: unknown option "${key}" in ${model}.${op}(), it is ignored.` +
433
+ (suggestion ? ` Did you mean "${suggestion}"?` : ''));
434
+ }
435
+ }
436
+ catch {
437
+ // Key enumeration is the only thing that can fail here, and a diagnostic
438
+ // must never be the reason a query fails.
439
+ }
440
+ }
441
+ /**
442
+ * Prisma 6.7+ accepts `limit` on `updateMany` / `deleteMany` to BOUND how many
443
+ * rows a mass mutation touches. Turbine has no row-bounded mass mutation, so
444
+ * this one is refused rather than warned about: everywhere else in this design
445
+ * a dropped option costs the caller a feature, but silently dropping a safety
446
+ * bound turns "change at most 10 rows" into "change every row".
447
+ */
448
+ function refuseRowLimit(model, op, args) {
449
+ if (!isPlainObject(args) || args.limit === undefined)
450
+ return;
451
+ throw new UnsupportedFeatureError(`\`limit\` on ${op} (row-bounded mass mutation)`, 'prisma-compat', `Turbine has no row-bounded ${op}; select the rows you mean with \`where\`` +
452
+ ` (on ${model}.${op}), or page them and mutate by primary key.`);
453
+ }
454
+ /**
455
+ * Prisma's `relationLoadStrategy` and Turbine's share a name and NOT a value
456
+ * domain: Prisma has `'query' | 'join'`, Turbine has
457
+ * `'join' | 'batched' | 'auto' | 'flatten'`. Core's strategy resolution has no
458
+ * branch for `'query'` and returns the value unchanged, so forwarding it
459
+ * verbatim gave a caller who asked for the per-relation query plan the JOIN
460
+ * plan, the exact opposite of the request. Prisma's `'query'` IS Turbine's
461
+ * `'batched'` (one follow-up statement per relation); the turbine-only values
462
+ * pass through so a compat caller can still reach them.
463
+ */
464
+ function mapRelationLoadStrategy(value) {
465
+ return value === 'query' ? 'batched' : value;
466
+ }
255
467
  /**
256
468
  * Translate a Prisma `where` (or nested relation where) into a Turbine `where`.
257
469
  * Renames scalar field keys and relation keys through the map, recurses into
@@ -446,6 +658,13 @@ function modelName(ctx, mm) {
446
658
  */
447
659
  function translateReadArgs(ctx, mm, prismaArgs, kind) {
448
660
  const t = {};
661
+ // FIRST, so a per-call `stableRelationOrder` overrides it: the client-level
662
+ // option is a default, the arg is an instruction.
663
+ if (ctx.options.stablePkOrder)
664
+ t.stableRelationOrder = true;
665
+ // Every turbine-native option the arg surface declares, in one line that
666
+ // cannot fall behind the interface (see the option-surface tables).
667
+ applyNativeOptions(kind === 'unique' ? FIND_UNIQUE_OPTIONS : FIND_MANY_OPTIONS, prismaArgs, t);
449
668
  if (prismaArgs.where !== undefined)
450
669
  t.where = translateWhere(ctx, mm, prismaArgs.where);
451
670
  if (prismaArgs.orderBy !== undefined)
@@ -458,17 +677,9 @@ function translateReadArgs(ctx, mm, prismaArgs, kind) {
458
677
  if (Array.isArray(prismaArgs.distinct)) {
459
678
  t.distinct = prismaArgs.distinct.map((f) => renameField(mm, f));
460
679
  }
461
- if (prismaArgs.relationLoadStrategy !== undefined)
462
- t.relationLoadStrategy = prismaArgs.relationLoadStrategy;
463
- if (typeof prismaArgs.timeout === 'number')
464
- t.timeout = prismaArgs.timeout;
465
- // Turbine-only passthrough. Prisma has no PII concept, so a compat caller
466
- // whose schema tags columns needs SOME way to opt in; without this the
467
- // adapter is a one-way door into redacted reads and refused aggregates.
468
- if (prismaArgs.includePii !== undefined)
469
- t.includePii = prismaArgs.includePii;
470
- if (ctx.options.stablePkOrder)
471
- t.stableRelationOrder = true;
680
+ if (prismaArgs.relationLoadStrategy !== undefined) {
681
+ t.relationLoadStrategy = mapRelationLoadStrategy(prismaArgs.relationLoadStrategy);
682
+ }
472
683
  // ORDER IS LOAD-BEARING: translateCursor MUST run BEFORE applyImplicitPkOrder.
473
684
  // The cursor translation reads `t.orderBy` to decide the seek direction and to
474
685
  // validate that a bare inclusive cursor names the sort key; it must see the
@@ -767,6 +978,7 @@ function translateUpsertNested(ctx, target, p) {
767
978
  const AGG_FIELD_BLOCKS = ['_sum', '_avg', '_min', '_max'];
768
979
  function translateAggregateArgs(ctx, mm, args, isGroupBy) {
769
980
  const t = {};
981
+ applyNativeOptions(isGroupBy ? GROUP_BY_OPTIONS : AGGREGATE_OPTIONS, args, t);
770
982
  if (args.where !== undefined)
771
983
  t.where = translateWhere(ctx, mm, args.where);
772
984
  if (args._count !== undefined)
@@ -775,12 +987,6 @@ function translateAggregateArgs(ctx, mm, args, isGroupBy) {
775
987
  if (args[block] !== undefined)
776
988
  t[block] = renameAggBlock(mm, args[block], false);
777
989
  }
778
- if (typeof args.timeout === 'number')
779
- t.timeout = args.timeout;
780
- // Turbine-only passthrough: the PII gate on groupBy keys and _min/_max needs
781
- // an opt-in that Prisma's arg shape has no equivalent for.
782
- if (args.includePii !== undefined)
783
- t.includePii = args.includePii;
784
990
  if (isGroupBy) {
785
991
  if (Array.isArray(args.by))
786
992
  t.by = args.by.map((f) => renameField(mm, f));
@@ -788,6 +994,8 @@ function translateAggregateArgs(ctx, mm, args, isGroupBy) {
788
994
  t.orderBy = translateOrderBy(ctx, mm, args.orderBy);
789
995
  if (args.having !== undefined)
790
996
  t.having = renameHaving(mm, args.having);
997
+ if (args.distinctOn !== undefined)
998
+ t.distinctOn = translateDistinctOn(ctx, mm, args.distinctOn);
791
999
  if (typeof args.take === 'number')
792
1000
  t.limit = mapTake(args.take);
793
1001
  if (typeof args.skip === 'number')
@@ -795,6 +1003,23 @@ function translateAggregateArgs(ctx, mm, args, isGroupBy) {
795
1003
  }
796
1004
  return t;
797
1005
  }
1006
+ /**
1007
+ * Translate a turbine-native groupBy `distinctOn`. Both halves of its value
1008
+ * (`columns`, `orderBy`) are FIELD NAMES, so this is the concrete reason the
1009
+ * option surface refuses to copy unknown keys through by default: a blind
1010
+ * passthrough is correct on a model whose Prisma and turbine names coincide and
1011
+ * silently wrong on one with a `@map`.
1012
+ */
1013
+ function translateDistinctOn(ctx, mm, val) {
1014
+ if (!isPlainObject(val))
1015
+ return val;
1016
+ const out = { ...val };
1017
+ if (Array.isArray(val.columns))
1018
+ out.columns = val.columns.map((f) => renameField(mm, f));
1019
+ if (val.orderBy !== undefined)
1020
+ out.orderBy = translateOrderBy(ctx, mm, val.orderBy);
1021
+ return out;
1022
+ }
798
1023
  /** Rename field keys inside an aggregate block; `_count`'s `_all` passes through. */
799
1024
  function renameAggBlock(mm, block, isCount) {
800
1025
  if (block === true || !isPlainObject(block))
@@ -1139,6 +1364,8 @@ async function createManyByRun(qi, t, runs) {
1139
1364
  const args = { data: run };
1140
1365
  if (t.skipDuplicates)
1141
1366
  args.skipDuplicates = true;
1367
+ if (t.timeout !== undefined)
1368
+ args.timeout = t.timeout;
1142
1369
  count += (await qi.createMany(args)).length;
1143
1370
  }
1144
1371
  return { count };
@@ -1154,7 +1381,14 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
1154
1381
  // async run wrapper also converts any synchronous throw from the underlying
1155
1382
  // `qi.*` build into a rejection; the array `$transaction([...])` batch path
1156
1383
  // catches the same throw from `batch.build`.
1157
- const defer = (translate, run, batch) => {
1384
+ const defer = (op, rawArgs, translateRaw, run, batch) => {
1385
+ // The unknown-key check rides the SAME deferred boundary as the translation
1386
+ // itself, for the same reason (see the comment above): a compat promise that
1387
+ // is built and never awaited must produce no output at all.
1388
+ const translate = () => {
1389
+ warnUnknownQueryOptions(modelName(ctx, mm), op, rawArgs);
1390
+ return translateRaw();
1391
+ };
1158
1392
  const batchable = batch
1159
1393
  ? {
1160
1394
  build: () => batch.build(getQI(), translate()),
@@ -1194,27 +1428,27 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
1194
1428
  return args;
1195
1429
  };
1196
1430
  return {
1197
- findMany: (args = {}) => defer(() => translateReadArgs(ctx, mm, args, 'many'), (qi, t) => qi.findMany(t).then((r) => reshapeRows(ctx, mm, r)), { build: (qi, t) => qi.buildFindMany(t), reshape: (raw) => reshapeRows(ctx, mm, raw) }),
1198
- findFirst: (args = {}) => defer(() => translateReadArgs(ctx, mm, args, 'first'), (qi, t) => qi.findFirst(t).then((r) => reshapeRowOrNull(ctx, mm, r)), { build: (qi, t) => qi.buildFindFirst(t), reshape: (raw) => reshapeRowOrNull(ctx, mm, raw) }),
1199
- findUnique: (args) => defer(() => translateReadArgs(ctx, mm, requireWhere(args, 'findUnique'), 'unique'), (qi, t) => qi.findUnique(t).then((r) => reshapeRowOrNull(ctx, mm, r)), { build: (qi, t) => qi.buildFindUnique(t), reshape: (raw) => reshapeRowOrNull(ctx, mm, raw) }),
1200
- findFirstOrThrow: (args = {}) => defer(() => translateReadArgs(ctx, mm, args, 'first'), (qi, t) => qi.findFirstOrThrow(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildFindFirstOrThrow(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
1201
- findUniqueOrThrow: (args) => defer(() => translateReadArgs(ctx, mm, requireWhere(args, 'findUniqueOrThrow'), 'unique'), (qi, t) => qi.findUniqueOrThrow(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildFindUniqueOrThrow(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
1202
- create: (args) => defer(() => {
1431
+ findMany: (args = {}) => defer('findMany', args, () => translateReadArgs(ctx, mm, args, 'many'), (qi, t) => qi.findMany(t).then((r) => reshapeRows(ctx, mm, r)), { build: (qi, t) => qi.buildFindMany(t), reshape: (raw) => reshapeRows(ctx, mm, raw) }),
1432
+ findFirst: (args = {}) => defer('findFirst', args, () => translateReadArgs(ctx, mm, args, 'first'), (qi, t) => qi.findFirst(t).then((r) => reshapeRowOrNull(ctx, mm, r)), { build: (qi, t) => qi.buildFindFirst(t), reshape: (raw) => reshapeRowOrNull(ctx, mm, raw) }),
1433
+ findUnique: (args) => defer('findUnique', args, () => translateReadArgs(ctx, mm, requireWhere(args, 'findUnique'), 'unique'), (qi, t) => qi.findUnique(t).then((r) => reshapeRowOrNull(ctx, mm, r)), { build: (qi, t) => qi.buildFindUnique(t), reshape: (raw) => reshapeRowOrNull(ctx, mm, raw) }),
1434
+ findFirstOrThrow: (args = {}) => defer('findFirstOrThrow', args, () => translateReadArgs(ctx, mm, args, 'first'), (qi, t) => qi.findFirstOrThrow(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildFindFirstOrThrow(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
1435
+ findUniqueOrThrow: (args) => defer('findUniqueOrThrow', args, () => translateReadArgs(ctx, mm, requireWhere(args, 'findUniqueOrThrow'), 'unique'), (qi, t) => qi.findUniqueOrThrow(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildFindUniqueOrThrow(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
1436
+ create: (args) => defer('create', args, () => {
1203
1437
  const t = { data: translateWriteData(ctx, mm, applyCreateDefaults(mm, args.data)) };
1204
- if (typeof args.timeout === 'number')
1205
- t.timeout = args.timeout;
1438
+ applyNativeOptions(CREATE_OPTIONS, args, t);
1206
1439
  return t;
1207
1440
  }, (qi, t) => qi.create(t).then((r) => reshapeRow(ctx, mm, r)), {
1208
1441
  build: (qi, t) => qi.buildCreate(t),
1209
1442
  reshape: (raw) => reshapeRow(ctx, mm, raw),
1210
1443
  nested: (t) => hasNestedKeys(ctx, mm, t.data),
1211
1444
  }),
1212
- createMany: (args) => defer(() => {
1445
+ createMany: (args) => defer('createMany', args, () => {
1213
1446
  const data = args.data;
1214
1447
  const rows = Array.isArray(data)
1215
1448
  ? data.map((d) => translateWriteData(ctx, mm, applyCreateDefaults(mm, d)))
1216
1449
  : [];
1217
1450
  const t = { data: rows };
1451
+ applyNativeOptions(CREATE_MANY_OPTIONS, args, t);
1218
1452
  if (args.skipDuplicates)
1219
1453
  t.skipDuplicates = true;
1220
1454
  return t;
@@ -1235,45 +1469,68 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
1235
1469
  nested: (t) => createManyRunsOf(t).length > 1,
1236
1470
  execInTx: (table, t) => createManyByRun(table(mm.table), t, createManyRunsOf(t)),
1237
1471
  }),
1238
- update: (args) => defer(() => {
1472
+ update: (args) => defer('update', args, () => {
1239
1473
  const a = requireWhere(args, 'update');
1240
- return {
1474
+ const t = {
1241
1475
  where: translateWhere(ctx, mm, a.where),
1242
1476
  data: translateWriteData(ctx, mm, applyUpdateTouch(mm, a.data)),
1243
1477
  };
1478
+ applyNativeOptions(UPDATE_OPTIONS, a, t);
1479
+ // `optimisticLock.field` is a FIELD NAME, so it is renamed rather than
1480
+ // copied: a blind passthrough would send the Prisma spelling into core
1481
+ // and break on any model whose column is `@map`ped.
1482
+ if (isPlainObject(a.optimisticLock)) {
1483
+ t.optimisticLock = {
1484
+ ...a.optimisticLock,
1485
+ field: renameField(mm, String(a.optimisticLock.field)),
1486
+ };
1487
+ }
1488
+ return t;
1244
1489
  }, (qi, t) => qi.update(t).then((r) => reshapeRow(ctx, mm, r)), {
1245
1490
  build: (qi, t) => qi.buildUpdate(t),
1246
1491
  reshape: (raw) => reshapeRow(ctx, mm, raw),
1247
1492
  nested: (t) => hasNestedKeys(ctx, mm, t.data),
1248
1493
  }),
1249
- updateMany: (args) => defer(() => {
1494
+ updateMany: (args) => defer('updateMany', args, () => {
1250
1495
  const a = args;
1496
+ refuseRowLimit(modelName(ctx, mm), 'updateMany', a);
1251
1497
  const t = {
1252
1498
  where: translateWhere(ctx, mm, a.where ?? {}),
1253
1499
  data: translateWriteData(ctx, mm, applyUpdateTouch(mm, a.data)),
1254
1500
  };
1501
+ applyNativeOptions(UPDATE_MANY_OPTIONS, a, t);
1502
+ // LAST, so it wins: a Prisma updateMany with no `where` affects every
1503
+ // row, and an explicit `allowFullTableScan: false` must not be able to
1504
+ // turn that parity into a thrown empty-where guard.
1255
1505
  if (a.where === undefined)
1256
1506
  t.allowFullTableScan = true;
1257
1507
  return t;
1258
1508
  }, (qi, t) => qi.updateMany(t), { build: (qi, t) => qi.buildUpdateMany(t), reshape: (raw) => raw }),
1259
- delete: (args) => defer(() => {
1509
+ delete: (args) => defer('delete', args, () => {
1260
1510
  const a = requireWhere(args, 'delete');
1261
- return { where: translateWhere(ctx, mm, a.where) };
1511
+ const t = { where: translateWhere(ctx, mm, a.where) };
1512
+ applyNativeOptions(DELETE_OPTIONS, a, t);
1513
+ return t;
1262
1514
  }, (qi, t) => qi.delete(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildDelete(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
1263
- deleteMany: (args = {}) => defer(() => {
1515
+ deleteMany: (args = {}) => defer('deleteMany', args, () => {
1264
1516
  const a = args;
1517
+ refuseRowLimit(modelName(ctx, mm), 'deleteMany', a);
1265
1518
  const t = { where: translateWhere(ctx, mm, a.where ?? {}) };
1519
+ applyNativeOptions(DELETE_MANY_OPTIONS, a, t);
1520
+ // LAST, so it wins. See the same note on updateMany.
1266
1521
  if (a.where === undefined)
1267
1522
  t.allowFullTableScan = true;
1268
1523
  return t;
1269
1524
  }, (qi, t) => qi.deleteMany(t), { build: (qi, t) => qi.buildDeleteMany(t), reshape: (raw) => raw }),
1270
- upsert: (args) => defer(() => {
1525
+ upsert: (args) => defer('upsert', args, () => {
1271
1526
  const a = requireWhere(args, 'upsert');
1272
- return {
1527
+ const t = {
1273
1528
  where: translateWhere(ctx, mm, a.where),
1274
1529
  create: translateWriteData(ctx, mm, applyCreateDefaults(mm, a.create)),
1275
1530
  update: translateWriteData(ctx, mm, applyUpdateTouch(mm, a.update)),
1276
1531
  };
1532
+ applyNativeOptions(UPSERT_OPTIONS, a, t);
1533
+ return t;
1277
1534
  }, (qi, t) => {
1278
1535
  // Native ON CONFLICT upsert is only Prisma-equivalent when the where
1279
1536
  // key values equal the create values AND no nested write data is
@@ -1296,16 +1553,15 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
1296
1553
  return reshapeRow(ctx, mm, await upsertLookupFirst(table(mm.table), t));
1297
1554
  },
1298
1555
  }),
1299
- count: (args = {}) => defer(() => {
1556
+ count: (args = {}) => defer('count', args, () => {
1300
1557
  const t = {};
1558
+ applyNativeOptions(COUNT_OPTIONS, args, t);
1301
1559
  if (args.where !== undefined)
1302
1560
  t.where = translateWhere(ctx, mm, args.where);
1303
- if (typeof args.timeout === 'number')
1304
- t.timeout = args.timeout;
1305
1561
  return t;
1306
1562
  }, (qi, t) => qi.count(t), { build: (qi, t) => qi.buildCount(t), reshape: (raw) => raw }),
1307
- aggregate: (args) => defer(() => translateAggregateArgs(ctx, mm, args, false), (qi, t) => qi.aggregate(t).then((r) => reshapeAggregate(ctx, mm, r)), { build: (qi, t) => qi.buildAggregate(t), reshape: (raw) => reshapeAggregate(ctx, mm, raw) }),
1308
- groupBy: (args) => defer(() => translateAggregateArgs(ctx, mm, args, true), (qi, t) => qi.groupBy(t).then((rows) => rows.map((r) => reshapeGroupRow(ctx, mm, r))), {
1563
+ aggregate: (args) => defer('aggregate', args, () => translateAggregateArgs(ctx, mm, args, false), (qi, t) => qi.aggregate(t).then((r) => reshapeAggregate(ctx, mm, r)), { build: (qi, t) => qi.buildAggregate(t), reshape: (raw) => reshapeAggregate(ctx, mm, raw) }),
1564
+ groupBy: (args) => defer('groupBy', args, () => translateAggregateArgs(ctx, mm, args, true), (qi, t) => qi.groupBy(t).then((rows) => rows.map((r) => reshapeGroupRow(ctx, mm, r))), {
1309
1565
  build: (qi, t) => qi.buildGroupBy(t),
1310
1566
  reshape: (raw) => raw.map((r) => reshapeGroupRow(ctx, mm, r)),
1311
1567
  }),
@@ -8,6 +8,8 @@
8
8
  export type { AggregateArgs, AggregateResult, ArrayFilter, ColumnRef, ConnectOrCreateOp, CountArgs, CreateArgs, CreateDataInput, CreateManyArgs, DeleteArgs, DeleteManyArgs, FieldResult, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GlobalFilters, GroupByAggregateSpec, GroupByArgs, GroupByDistinctOn, GroupByResult, HavingClause, JsonFilter, JsonPathAggregateTarget, JsonPathGroupKey, JsonPathOrderBy, NestedCreateOp, NestedUpdateOp, NestedUpdateOpItem, NestedUpsertOpItem, OmitResult, OrderByClause, OrderByObject, OrderDirection, QueryResult, RelationDescriptor, RelationFilter, RelationLoadStrategy, RelationPickBy, RelationPickOrderBy, SelectResult, SkipGlobalFilters, TextSearchFilter, TypedWithClause, UpdateArgs, UpdateDataInput, UpdateInput, UpdateManyArgs, UpdateOperatorInput, UpsertArgs, VectorDistanceFilter, VectorFilter, VectorMetric, VectorOrderBy, VectorOrderByDistance, WhereClause, WhereOperator, WhereValue, WithClause, WithOptions, WithOrderByObject, WithResult, } from './types.js';
9
9
  export type { BuiltStatement, BulkInsertStatementInput, ColumnDefinitionInput, ColumnTypeInput, CreateIndexStatementInput, CreateTableStatementInput, Dialect, InsertStatementInput, UpsertStatementInput, } from '../dialect.js';
10
10
  export { postgresDialect } from '../dialect.js';
11
+ export type { OptionKind, OptionTable } from './option-surface.js';
12
+ export { AGGREGATE_OPTIONS, ALL_OPTION_TABLES, applyNativeOptions, COUNT_OPTIONS, CREATE_MANY_OPTIONS, CREATE_OPTIONS, DELETE_MANY_OPTIONS, DELETE_OPTIONS, FIND_MANY_OPTIONS, FIND_MANY_STREAM_OPTIONS, FIND_UNIQUE_OPTIONS, GROUP_BY_OPTIONS, optionKeysOfKind, UPDATE_MANY_OPTIONS, UPDATE_OPTIONS, UPSERT_OPTIONS, } from './option-surface.js';
11
13
  export type { SqlCacheEntry } from './utils.js';
12
14
  export { buildCorrelation, escapeLike, escSingleQuote, fnv1a64Hex, LRUCache, OPERATOR_KEYS, quoteIdent, sqlToPreparedName, } from './utils.js';
13
15
  export type { DeferredQuery, MiddlewareFn, QueryEvent, QueryEventListener, QueryInterfaceOptions, ReselectExecutor, TemporalInfinityReading, } from './builder.js';
@@ -6,5 +6,6 @@
6
6
  * former monolithic `import { … } from './query.js'`.
7
7
  */
8
8
  export { postgresDialect } from '../dialect.js';
9
+ export { AGGREGATE_OPTIONS, ALL_OPTION_TABLES, applyNativeOptions, COUNT_OPTIONS, CREATE_MANY_OPTIONS, CREATE_OPTIONS, DELETE_MANY_OPTIONS, DELETE_OPTIONS, FIND_MANY_OPTIONS, FIND_MANY_STREAM_OPTIONS, FIND_UNIQUE_OPTIONS, GROUP_BY_OPTIONS, optionKeysOfKind, UPDATE_MANY_OPTIONS, UPDATE_OPTIONS, UPSERT_OPTIONS, } from './option-surface.js';
9
10
  export { buildCorrelation, escapeLike, escSingleQuote, fnv1a64Hex, LRUCache, OPERATOR_KEYS, quoteIdent, sqlToPreparedName, } from './utils.js';
10
11
  export { AUTO_ASSUMED_ROUND_TRIP_MS, AUTO_COUNT_BATCH_MIN_PARENT_ROWS, AUTO_JOIN_PENALTY_MS_PER_ROW, AUTO_TO_ONE_JOIN_MAX_ROWS, AUTO_TO_ONE_JOIN_ROWS_MAX, AUTO_TO_ONE_JOIN_ROWS_MIN, QueryInterface, } from './builder.js';
@@ -0,0 +1,100 @@
1
+ /**
2
+ * turbine-orm, the query-argument OPTION SURFACE as runtime data.
3
+ *
4
+ * ## Why this file exists
5
+ *
6
+ * TypeScript erases interfaces, so `FindManyArgs` does not exist at runtime and
7
+ * any layer that has to decide, key by key, what to do with an args object has
8
+ * to keep its own hand-written list. `turbine-orm/prisma-compat` is exactly
9
+ * such a layer: it builds a FRESH turbine args object out of Prisma-shaped
10
+ * input and copies over the keys it recognizes. Every time core gained a
11
+ * query-level option, that ad-hoc allowlist silently failed to gain it, and the
12
+ * option was accepted by the caller's type-checker and then dropped on the
13
+ * floor. There was no feedback of any kind: no error, no warning, no test.
14
+ *
15
+ * These tables are the fix. Each one is a `Record<keyof SomeArgs<Row>,
16
+ * OptionKind>`, the same mechanism `TURBINE_CONFIG_KEYS` (client.ts) uses for
17
+ * the client-config surface, and it binds the compiler in BOTH directions:
18
+ *
19
+ * - add an option to an arg interface and this file stops compiling until a
20
+ * human classifies it ("Property 'fooMode' is missing in type ..."), so an
21
+ * option can no longer be stranded BY OMISSION;
22
+ * - list a key here that is not on the interface and it fails as an excess
23
+ * property, so a table can never drift into describing an option that does
24
+ * not exist.
25
+ *
26
+ * It deliberately does NOT make "add the option in one place" sufficient: it
27
+ * makes the second edit a BUILD FAILURE rather than a silent drop. That trade is
28
+ * intentional. A passthrough-by-default translator would satisfy the shorter
29
+ * wording and be actively wrong, because two of the options below carry FIELD
30
+ * NAMES in their values (`optimisticLock.field`, `distinctOn.columns`), which a
31
+ * compat layer must rename before core ever sees them. Copying those blind
32
+ * works on a schema whose names happen to coincide and breaks on one that
33
+ * renames a column, i.e. it makes the failure mode depend on the schema.
34
+ *
35
+ * ## THE ONE RULE for classifying a new option
36
+ *
37
+ * Classify a key `'native'` ONLY when its value contains no field, relation,
38
+ * column, or model NAME. If the value names anything in the schema, it is
39
+ * `'prisma'`: a name-translating consumer has to walk it by hand.
40
+ *
41
+ * @module
42
+ */
43
+ import type { AggregateArgs, CountArgs, CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GroupByArgs, UpdateArgs, UpdateManyArgs, UpsertArgs } from './types.js';
44
+ /**
45
+ * How a name-translating consumer (today: `turbine-orm/prisma-compat`) must
46
+ * handle one key of a turbine query-arg interface.
47
+ *
48
+ * - `'prisma'`, the key is a Prisma concept too, or its VALUE carries names
49
+ * that live in the caller's naming space. Translated by hand; NEVER copied
50
+ * verbatim.
51
+ * - `'native'`, turbine-only and its value is opaque to naming (a boolean, a
52
+ * number, a list of table names). Copied through untouched.
53
+ * - `'nativeAlias'`, the turbine SPELLING of a concept the caller's surface
54
+ * already has under another name (`with`/`limit`/`offset` vs
55
+ * `include`/`take`/`skip`). Refused, because forwarding it would collide with
56
+ * the translated key and would carry turbine relation names into a call
57
+ * written in the caller's names. The diagnostic names the right key instead.
58
+ * - `'internal'`, not reachable through the compat surface at all
59
+ * (`batchSize` belongs to a streaming method compat does not expose), so it
60
+ * is not part of any known set and passing it is reported as unknown.
61
+ */
62
+ export type OptionKind = 'prisma' | 'native' | 'nativeAlias' | 'internal';
63
+ /**
64
+ * The generic parameter the tables are instantiated at. `keyof FindManyArgs<T>`
65
+ * is the literal union of the DECLARED key names regardless of `T`, so a
66
+ * neutral row type keeps the tables stable and free of entity coupling.
67
+ */
68
+ type Row = Record<string, unknown>;
69
+ /** One option table: every declared key of one arg interface, classified. */
70
+ export type OptionTable<A> = Readonly<Record<keyof A, OptionKind>>;
71
+ export declare const FIND_UNIQUE_OPTIONS: OptionTable<FindUniqueArgs<Row>>;
72
+ export declare const FIND_MANY_OPTIONS: OptionTable<FindManyArgs<Row>>;
73
+ export declare const FIND_MANY_STREAM_OPTIONS: OptionTable<FindManyStreamArgs<Row>>;
74
+ export declare const CREATE_OPTIONS: OptionTable<CreateArgs<Row>>;
75
+ export declare const CREATE_MANY_OPTIONS: OptionTable<CreateManyArgs<Row>>;
76
+ export declare const UPDATE_OPTIONS: OptionTable<UpdateArgs<Row>>;
77
+ export declare const UPDATE_MANY_OPTIONS: OptionTable<UpdateManyArgs<Row>>;
78
+ export declare const DELETE_OPTIONS: OptionTable<DeleteArgs<Row>>;
79
+ export declare const DELETE_MANY_OPTIONS: OptionTable<DeleteManyArgs<Row>>;
80
+ export declare const UPSERT_OPTIONS: OptionTable<UpsertArgs<Row>>;
81
+ export declare const COUNT_OPTIONS: OptionTable<CountArgs<Row>>;
82
+ export declare const AGGREGATE_OPTIONS: OptionTable<AggregateArgs<Row>>;
83
+ export declare const GROUP_BY_OPTIONS: OptionTable<GroupByArgs<Row>>;
84
+ /**
85
+ * Every table, so a test can assert the set is complete and well-formed without
86
+ * naming each one (a table stubbed out during a refactor shows up here).
87
+ */
88
+ export declare const ALL_OPTION_TABLES: Readonly<Record<string, Readonly<Record<string, OptionKind>>>>;
89
+ /**
90
+ * Copy every `'native'` key present on `src` onto `dst`.
91
+ *
92
+ * Iterates `src` (a small caller-supplied object) rather than the table, so the
93
+ * cost is proportional to what was actually passed. `undefined` values are
94
+ * skipped: `{ ...maybeOpts }` routinely materializes keys with no value, and
95
+ * writing `undefined` through would be indistinguishable from passing it.
96
+ */
97
+ export declare function applyNativeOptions(table: Readonly<Record<string, OptionKind>>, src: Record<string, unknown>, dst: Record<string, unknown>): void;
98
+ /** The keys of `table` with the given kind, as a set. */
99
+ export declare function optionKeysOfKind(table: Readonly<Record<string, OptionKind>>, ...kinds: OptionKind[]): string[];
100
+ export {};