turbine-orm 0.80.0 → 0.81.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.d.ts CHANGED
@@ -889,19 +889,22 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
889
889
  }>;
890
890
  upsert(args: UpsertArgs<T>): Promise<T>;
891
891
  /**
892
- * Upsert as a reselect-or-write inside one flat transaction, for every shape
893
- * the native `upsert … on .col` statement cannot express: a conflict target
894
- * other than a single-column primary key (it takes one column and PowDB has
895
- * no composite unique), and a table under a global filter (its conflict
896
- * branch takes no predicate). PowDB's single writer makes the read-then-write
897
- * safe from concurrent writers; the transaction makes it atomic with the
898
- * write.
899
- *
900
- * The row is looked up by the conflict columns with `create`'s values for
901
- * them, which is what `ON CONFLICT (<cols>)` compares on the SQL engines. The
902
- * find and the update run through the transaction's table interface, so a
903
- * configured global filter applies to both exactly as it does to any other
904
- * read or write, and `skipGlobalFilters` is forwarded to both.
892
+ * `upsert` as Prisma defines it, for every shape the native `upsert … on`
893
+ * statement cannot express: find the row `where` names, update it if it
894
+ * exists, else insert `create`, in one flat transaction. PowDB's single
895
+ * writer makes the read-then-write safe from concurrent writers; the
896
+ * transaction makes it atomic with the write.
897
+ *
898
+ * The lookup is by `where`'s own values. It used to be by `create`'s values
899
+ * for the conflict columns, which is what the native statement compares, and
900
+ * so answered a different question whenever the two disagreed: `where: { id }`
901
+ * with a `create` that leaves the key to the engine inserted a new row every
902
+ * time. The find and the update run through the transaction's table
903
+ * interface, so a configured global filter applies to both exactly as it does
904
+ * to any other read or write, and `skipGlobalFilters` is forwarded to both.
905
+ *
906
+ * Both halves are compiled before the lookup, so an unknown field in `update`
907
+ * is refused whether or not the row exists (the SQL engines do the same).
905
908
  */
906
909
  private upsertLookupFirst;
907
910
  count(args?: CountArgs<T>): Promise<number>;
package/dist/powql.js CHANGED
@@ -37,10 +37,10 @@
37
37
  import { randomUUID } from 'node:crypto';
38
38
  import { guardCheckout } from './connection-guard.js';
39
39
  import { NotFoundError, OptimisticLockError, ReadOnlyError, TimeoutError, UnsupportedFeatureError, ValidationError, } from './errors.js';
40
- import { executeNestedCreate, executeNestedUpdate, hasRelationFields, } from './nested-write.js';
40
+ import { executeNestedCreate, executeNestedUpdate, extractRelationFields, hasRelationFields, } from './nested-write.js';
41
41
  import { ALL_POWDB_CAPABILITIES, atLeastVersion, baseTsType, coerceNativeValue, isJsonColumn, isPowdbDatetimeColumn, isStaleFramePowdbError, PowdbFloatParam, PowdbJsonParam, parsePowdbSemver, powqlColumnType, quotePowqlDotted, quotePowqlIdent, requireCapability, rowToEntity, } from './powdb-shared.js';
42
42
  import { assertAggregatePiiOptIn } from './query/aggregates.js';
43
- import { assertMutationWhereIdentifiesOneRow, assertWhereIdentifiesOneRow, expandCompoundUniqueWhere, } from './query/compound-unique.js';
43
+ import { assertMutationWhereIdentifiesOneRow, assertWhereIdentifiesOneRow, expandCompoundUniqueWhere, upsertWhereMatchesCreate, } from './query/compound-unique.js';
44
44
  import { ARRAY_OPERATOR_KEYS, isJsonFilter, isRelationPickOrderBy, orderByEntries } from './query/filters.js';
45
45
  import { warnUnknownQueryOptions } from './query/option-surface.js';
46
46
  import { applyStableRelationOrderTo, normalizeWithClause } from './query/relation-names.js';
@@ -2665,7 +2665,7 @@ export class PowqlInterface {
2665
2665
  if (value === undefined)
2666
2666
  continue;
2667
2667
  if (resolveRelationDef(this.meta.relations, field)) {
2668
- throw new UnsupportedFeatureError('nested writes', 'PowDB', `relation "${field}", nested writes need create()/update(), not createMany()/upsert()`);
2668
+ throw new UnsupportedFeatureError('nested writes', 'PowDB', `relation "${field}", nested writes need create()/update()/upsert(), not createMany()`);
2669
2669
  }
2670
2670
  out.push({ col: this.column(field), value });
2671
2671
  }
@@ -2864,7 +2864,7 @@ export class PowqlInterface {
2864
2864
  if (value === undefined)
2865
2865
  continue;
2866
2866
  if (resolveRelationDef(this.meta.relations, field)) {
2867
- throw new UnsupportedFeatureError('nested writes', 'PowDB', `relation "${field}", nested writes need create()/update(), not updateMany()/upsert()`);
2867
+ throw new UnsupportedFeatureError('nested writes', 'PowDB', `relation "${field}", nested writes need create()/update()/upsert(), not updateMany()`);
2868
2868
  }
2869
2869
  const colMeta = this.column(field);
2870
2870
  const ref = this.ref(field);
@@ -3031,46 +3031,47 @@ export class PowqlInterface {
3031
3031
  const plan = this.writeReturnPlan(args);
3032
3032
  const createData = this.applyPkDefault(args.create);
3033
3033
  const pkCol = this.meta.primaryKey[0];
3034
- // The conflict target is the columns `where` names, as on the SQL
3035
- // engines (`ON CONFLICT (<where keys>)`), with the conflicting VALUES
3036
- // taken from `create`. This path used to conflict on the primary key
3037
- // whatever `where` named, so an upsert keyed on another unique column
3038
- // (`where: { email }`) never found the existing row.
3039
3034
  const conflictColumns = Object.keys(upsertWhere)
3040
3035
  .filter((k) => upsertWhere[k] !== undefined)
3041
3036
  .map((k) => this.column(k).name);
3037
+ const updateData = (args.update ?? {});
3042
3038
  // PowQL's native `upsert … on .col` expresses exactly one shape: a single
3043
- // conflict column that is the primary key, with no predicate on its
3044
- // conflict branch. Everything else is an atomic reselect-or-write
3045
- // transaction.
3039
+ // conflict column that is the primary key, pinned to the value `create`
3040
+ // inserts, with a non-empty update and no predicate on its conflict
3041
+ // branch. Every other shape looks the row up by `where` first, in one
3042
+ // transaction, which is what the SQL engines do for the same shapes and
3043
+ // what the call means (query/compound-unique.ts upsertWhereMatchesCreate).
3046
3044
  //
3047
3045
  // That includes a table under a global filter. The native statement's
3048
3046
  // `on conflict` branch carries no predicate, so a tenant-scoped client
3049
3047
  // whose `create` key matched ANOTHER tenant's row updated that row, and
3050
3048
  // the tenant-filtered reselect then reported the write it had just made
3051
- // as a NotFoundError. The SQL engines guard the same branch with the
3052
- // filter (`DO UPDATE … WHERE tenant_id = $n`). The lookup-first path runs
3053
- // its find and its update through the filtered interface, so another
3054
- // tenant's row is invisible to it: the insert then collides on the key
3055
- // and fails, as it does on the SQL engines, and nothing is overwritten.
3049
+ // as a NotFoundError. The lookup runs its find and its update through the
3050
+ // filtered interface, so another tenant's row is invisible to it: the
3051
+ // insert then collides on the key and fails, and nothing is overwritten.
3056
3052
  const filtered = this.applyGlobalFilter('', [], args.skipGlobalFilters) !== '';
3057
3053
  const native = this.meta.primaryKey.length === 1 &&
3058
3054
  pkCol !== undefined &&
3059
3055
  conflictColumns.length === 1 &&
3060
3056
  conflictColumns[0] === pkCol &&
3057
+ upsertWhereMatchesCreate(this.meta, upsertWhere, (args.create ?? {})) &&
3058
+ !hasRelationFields(args.create, this.meta) &&
3059
+ !hasRelationFields(updateData, this.meta) &&
3060
+ Object.values(updateData).some((v) => v !== undefined) &&
3061
3061
  !filtered;
3062
3062
  if (!native) {
3063
- return this.upsertLookupFirst(createData, args.update, conflictColumns, {
3063
+ return this.upsertLookupFirst(upsertWhere, createData, updateData, {
3064
3064
  select: args.select,
3065
3065
  omit: args.omit,
3066
3066
  skipGlobalFilters: args.skipGlobalFilters,
3067
+ timeout: args.timeout,
3067
3068
  });
3068
3069
  }
3069
3070
  const params = [];
3070
3071
  const createBody = this.scalarData(createData)
3071
3072
  .map((a) => `${quotePowqlIdent(a.col.name)} := ${this.writeRef(a.value, a.col, params)}`)
3072
3073
  .join(', ');
3073
- const updateBody = this.buildUpdateAssignments(args.update, params);
3074
+ const updateBody = this.buildUpdateAssignments(updateData, params);
3074
3075
  // PowDB 0.7.0's `upsert` statement does NOT accept a trailing `returning`
3075
3076
  // (verified: "unexpected trailing token … 'returning'"), because it is one
3076
3077
  // atomic insert-or-update, not two branches. So upsert alone keeps the
@@ -3090,48 +3091,42 @@ export class PowqlInterface {
3090
3091
  });
3091
3092
  }
3092
3093
  /**
3093
- * Upsert as a reselect-or-write inside one flat transaction, for every shape
3094
- * the native `upsert … on .col` statement cannot express: a conflict target
3095
- * other than a single-column primary key (it takes one column and PowDB has
3096
- * no composite unique), and a table under a global filter (its conflict
3097
- * branch takes no predicate). PowDB's single writer makes the read-then-write
3098
- * safe from concurrent writers; the transaction makes it atomic with the
3099
- * write.
3094
+ * `upsert` as Prisma defines it, for every shape the native `upsert … on`
3095
+ * statement cannot express: find the row `where` names, update it if it
3096
+ * exists, else insert `create`, in one flat transaction. PowDB's single
3097
+ * writer makes the read-then-write safe from concurrent writers; the
3098
+ * transaction makes it atomic with the write.
3100
3099
  *
3101
- * The row is looked up by the conflict columns with `create`'s values for
3102
- * them, which is what `ON CONFLICT (<cols>)` compares on the SQL engines. The
3103
- * find and the update run through the transaction's table interface, so a
3104
- * configured global filter applies to both exactly as it does to any other
3105
- * read or write, and `skipGlobalFilters` is forwarded to both.
3106
- */
3107
- async upsertLookupFirst(createData, updateData, conflictColumns, shape) {
3108
- const { skipGlobalFilters, ...projection } = shape;
3100
+ * The lookup is by `where`'s own values. It used to be by `create`'s values
3101
+ * for the conflict columns, which is what the native statement compares, and
3102
+ * so answered a different question whenever the two disagreed: `where: { id }`
3103
+ * with a `create` that leaves the key to the engine inserted a new row every
3104
+ * time. The find and the update run through the transaction's table
3105
+ * interface, so a configured global filter applies to both exactly as it does
3106
+ * to any other read or write, and `skipGlobalFilters` is forwarded to both.
3107
+ *
3108
+ * Both halves are compiled before the lookup, so an unknown field in `update`
3109
+ * is refused whether or not the row exists (the SQL engines do the same).
3110
+ */
3111
+ async upsertLookupFirst(where, createData, updateData, shape) {
3112
+ const { skipGlobalFilters, timeout, ...projection } = shape;
3109
3113
  const skip = skipGlobalFilters === undefined ? {} : { skipGlobalFilters };
3110
- // Accept either the camelCase field or the snake_case column in `create`
3111
- // (create() resolves both), and key the where by field name.
3112
- const keyPairs = conflictColumns.map((col) => {
3113
- const field = this.meta.reverseColumnMap[col] ?? col;
3114
- return { field, value: createData[field] ?? createData[col] };
3115
- });
3116
- const keyless = keyPairs.some((p) => p.value == null);
3117
- const isCompositePk = keyPairs.length > 1 &&
3118
- keyPairs.length === this.meta.primaryKey.length &&
3119
- conflictColumns.every((c) => this.meta.primaryKey.includes(c));
3120
- if (keyless && isCompositePk) {
3121
- throw new ValidationError(`upsert on "${this.table}" needs every composite-PK field in \`create\` (${keyPairs
3122
- .map((p) => p.field)
3123
- .join(', ')}).`);
3124
- }
3125
- const keyWhere = Object.fromEntries(keyPairs.map((p) => [p.field, p.value]));
3114
+ const time = timeout === undefined ? {} : { timeout };
3115
+ this.scalarData(extractRelationFields(createData, this.meta).scalars);
3116
+ const scalarUpdate = extractRelationFields(updateData, this.meta).scalars;
3117
+ if (Object.values(scalarUpdate).some((v) => v !== undefined))
3118
+ this.buildUpdateAssignments(scalarUpdate, []);
3119
+ const updateIsEmpty = Object.values(updateData).every((v) => v === undefined);
3126
3120
  const run = async (ctx) => {
3127
3121
  const tbl = ctx.tx.table(this.table);
3128
- // A row whose `create` leaves a conflict column unset cannot collide on
3129
- // it (NULL never equals NULL in a unique index, and a server-assigned key
3130
- // is new), which is what `ON CONFLICT` concludes on the SQL engines too.
3131
- const existing = keyless ? null : await tbl.findUnique({ where: keyWhere, ...skip });
3132
- return existing
3133
- ? (await tbl.update({ where: keyWhere, data: updateData, ...projection, ...skip }))
3134
- : (await tbl.create({ data: createData, ...projection }));
3122
+ const existing = await tbl.findUnique({ where, ...projection, ...skip, ...time });
3123
+ if (existing) {
3124
+ // An empty `update` means "leave it as it is": the row found is the answer.
3125
+ if (updateIsEmpty)
3126
+ return existing;
3127
+ return (await tbl.update({ where, data: updateData, ...projection, ...skip, ...time }));
3128
+ }
3129
+ return (await tbl.create({ data: createData, ...projection, ...time }));
3135
3130
  };
3136
3131
  return this.isTxScoped() ? run(this.buildNestedCtx()) : this.runInImplicitTx(run);
3137
3132
  }
@@ -159,6 +159,8 @@ export interface CompatQueryInterface {
159
159
  count: number;
160
160
  }>;
161
161
  buildUpsert(args: Record<string, unknown>): DeferredQuery<unknown>;
162
+ /** Whether `upsert()` will look the row up first (and `buildUpsert` refuse). Absent on PowDB. */
163
+ upsertNeedsLookup?(args: Record<string, unknown>): boolean;
162
164
  buildCount(args?: Record<string, unknown>): DeferredQuery<number>;
163
165
  buildAggregate(args: Record<string, unknown>): DeferredQuery<unknown>;
164
166
  buildGroupBy(args: Record<string, unknown>): DeferredQuery<unknown[]>;
@@ -1738,53 +1738,6 @@ function hasNestedKeys(ctx, mm, data) {
1738
1738
  return v !== null && typeof v === 'object' && !Array.isArray(v) && !(v instanceof Date);
1739
1739
  });
1740
1740
  }
1741
- /**
1742
- * Whether an upsert's translated `where` key values all equal the
1743
- * corresponding `create` values. When they do (the common Prisma idiom), the
1744
- * native single-statement ON CONFLICT upsert is semantically identical to
1745
- * Prisma's lookup-first and stays atomic. When they differ, native upsert
1746
- * would insert the `create` row even though the `where` row exists, so the
1747
- * adapter must emulate lookup-first instead.
1748
- */
1749
- function upsertKeysMatch(t) {
1750
- const where = t.where;
1751
- const create = t.create;
1752
- if (!isPlainObject(where) || !isPlainObject(create))
1753
- return false;
1754
- const scalarEq = (a, b) => {
1755
- if (a instanceof Date || b instanceof Date) {
1756
- return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
1757
- }
1758
- return a === b;
1759
- };
1760
- for (const [k, v] of Object.entries(where)) {
1761
- if (v !== null && typeof v === 'object' && !(v instanceof Date)) {
1762
- // Compound-unique selector object: every member must scalar-match create.
1763
- if (Array.isArray(v))
1764
- return false;
1765
- for (const [mk, mv] of Object.entries(v)) {
1766
- if (mv !== null && typeof mv === 'object' && !(mv instanceof Date))
1767
- return false;
1768
- if (!scalarEq(mv, create[mk]))
1769
- return false;
1770
- }
1771
- continue;
1772
- }
1773
- if (!scalarEq(v, create[k]))
1774
- return false;
1775
- }
1776
- return true;
1777
- }
1778
- /** Prisma upsert semantics: look up by where; update the found row, else insert create. */
1779
- async function upsertLookupFirst(qi, t) {
1780
- const existing = await qi.findUnique({ where: t.where });
1781
- // The projection rides along so both branches return what the native upsert
1782
- // path returns (an explicitly selected PII field included).
1783
- const shape = { select: t.select, omit: t.omit };
1784
- if (existing)
1785
- return qi.update({ where: t.where, data: t.update, ...shape });
1786
- return qi.create({ data: t.create, ...shape });
1787
- }
1788
1741
  /**
1789
1742
  * The row shapes a translated `createMany` has to insert, as contiguous runs
1790
1743
  * that each name the same fields (see {@link createManyShapeRuns}).
@@ -1846,7 +1799,7 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
1846
1799
  reshape: batch.reshape,
1847
1800
  nested: () => {
1848
1801
  try {
1849
- return batch.nested?.(translate()) ?? false;
1802
+ return batch.nested?.(translate(), getQI()) ?? false;
1850
1803
  }
1851
1804
  catch {
1852
1805
  return false; // let the build path surface the translation error consistently
@@ -2008,24 +1961,17 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
2008
1961
  };
2009
1962
  applyNativeOptions(UPSERT_OPTIONS, a, t);
2010
1963
  return t;
2011
- }, (qi, t) => {
2012
- // Native ON CONFLICT upsert is only Prisma-equivalent when the where
2013
- // key values equal the create values AND no nested write data is
2014
- // present; otherwise emulate Prisma's lookup-first atomically.
2015
- if (upsertKeysMatch(t) && !hasNestedKeys(ctx, mm, t.create) && !hasNestedKeys(ctx, mm, t.update)) {
2016
- return qi.upsert(t).then(shape);
2017
- }
2018
- return runInTx(async (table) => shape(await upsertLookupFirst(table(mm.table), t)));
2019
- }, {
1964
+ },
1965
+ // Core `upsert` has Prisma's semantics itself (it looks the row up by
1966
+ // `where` whenever one conflict statement would mean something else,
1967
+ // and runs nested writes), so the adapter passes it straight through.
1968
+ (qi, t) => qi.upsert(t).then(shape), {
2020
1969
  build: (qi, t) => qi.buildUpsert(t),
2021
1970
  reshape: shape,
2022
- nested: (t) => !upsertKeysMatch(t) || hasNestedKeys(ctx, mm, t.create) || hasNestedKeys(ctx, mm, t.update),
2023
- execInTx: async (table, t) => {
2024
- if (upsertKeysMatch(t) && !hasNestedKeys(ctx, mm, t.create) && !hasNestedKeys(ctx, mm, t.update)) {
2025
- return shape(await table(mm.table).upsert(t));
2026
- }
2027
- return shape(await upsertLookupFirst(table(mm.table), t));
2028
- },
1971
+ // Batchable only when core would run it as one statement. PowDB has
1972
+ // no deferred statements at all, so it always takes the tx path.
1973
+ nested: (t, qi) => qi.upsertNeedsLookup?.(t) ?? true,
1974
+ execInTx: async (table, t) => shape(await table(mm.table).upsert(t)),
2029
1975
  });
2030
1976
  },
2031
1977
  count: (args = {}) => defer('count', args, () => {
@@ -1283,6 +1283,35 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
1283
1283
  private makeTxProxy;
1284
1284
  delete<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: DeleteArgs<T, R, S, O>): Promise<FieldResult<T, S, O>>;
1285
1285
  upsert<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: UpsertArgs<T, R, S, O>): Promise<FieldResult<T, S, O>>;
1286
+ /**
1287
+ * @internal Whether `upsert()` looks the row up rather than running one
1288
+ * statement, which is also exactly when `buildUpsert` refuses these args.
1289
+ * prisma-compat reads it to decide whether an array `$transaction` can batch
1290
+ * an upsert or has to run the array inside a transaction.
1291
+ */
1292
+ upsertNeedsLookup(args: UpsertArgs<T, R, Record<string, boolean> | undefined, Record<string, boolean> | undefined>): boolean;
1293
+ /**
1294
+ * `upsert` as Prisma defines it: find the row `where` names, update it if it
1295
+ * exists, else insert `create`, in one transaction (the caller's, when there
1296
+ * is one). Taken whenever the single conflict statement would mean something
1297
+ * else (`upsertLookupReason`: `where` not pinned to `create`'s values, an
1298
+ * empty `update`, a global filter the engine's statement cannot carry) and
1299
+ * whenever `create` or `update` holds nested relation writes, which only the
1300
+ * nested-write engine can run.
1301
+ *
1302
+ * Validation is DATA-INDEPENDENT, as it is for the batched relation loader:
1303
+ * both halves are compiled before the lookup runs, so an unknown field in
1304
+ * `update` is refused whether or not the row exists, and a call is never
1305
+ * valid on one row and invalid on the next. Only the scalar part of each half
1306
+ * is compiled here; nested relation writes are validated by the engine that
1307
+ * runs them.
1308
+ *
1309
+ * Not atomic against a concurrent insert of the same key, exactly as Prisma's
1310
+ * own lookup is not: two callers can both miss, and the second insert then
1311
+ * fails on the unique constraint (E008) when `create` carries the key. It
1312
+ * never writes two rows over one key the database enforces.
1313
+ */
1314
+ private upsertLookupFirst;
1286
1315
  updateMany(args: UpdateManyArgs<T, R>): Promise<{
1287
1316
  count: number;
1288
1317
  }>;
@@ -15,11 +15,11 @@ import { settleEventLoop } from '../connection-guard.js';
15
15
  import { postgresDialect } from '../dialect.js';
16
16
  import { explainConnectionLoss, isStaleConnectionError, NotFoundError, TimeoutError, UnsupportedFeatureError, ValidationError, wrapPgError, } from '../errors.js';
17
17
  import { missingIndexForRelation, schemaHasIndexInfo } from '../index-advisor.js';
18
- import { executeNestedCreate, executeNestedUpdate, hasRelationFields, } from '../nested-write.js';
18
+ import { executeNestedCreate, executeNestedUpdate, extractRelationFields, hasRelationFields, } from '../nested-write.js';
19
19
  import { normalizeKeyColumns, snakeToCamel } from '../schema.js';
20
20
  import * as aggMod from './aggregates.js';
21
21
  import { assertProjectionShape, defaultProjectionFields, includeKeysForBatching, loadRelationsBatched, neededParentKeyFields, rejectNestedPickOrder, resolveCountRelations, stripFields, } from './batched-loader.js';
22
- import { assertWhereIdentifiesOneRow, expandCompoundUniqueWhere } from './compound-unique.js';
22
+ import { assertMutationWhereIdentifiesOneRow, assertWhereIdentifiesOneRow, expandCompoundUniqueWhere, } from './compound-unique.js';
23
23
  import { dedupeColumnList, dedupeOrderEntries, isEmptyOrderBy, isJsonPathOrderBy, isOrderBySpec, isRelationPickOrderBy, isVectorOrderBy, isWhereOperator, orderByEntries, sortedEntries, } from './filters.js';
24
24
  import { warnUnknownQueryOptions } from './option-surface.js';
25
25
  import { applyStableRelationOrderTo, normalizeWithClause } from './relation-names.js';
@@ -3600,10 +3600,78 @@ export class QueryInterface {
3600
3600
  // -------------------------------------------------------------------------
3601
3601
  async upsert(args) {
3602
3602
  return this.executeWithMiddleware('upsert', args, async () => {
3603
+ if (this.upsertNeedsLookup(args)) {
3604
+ return this.upsertLookupFirst(args);
3605
+ }
3603
3606
  const deferred = this.buildUpsert(args);
3604
3607
  return this.executeMutation(deferred, args.timeout);
3605
3608
  });
3606
3609
  }
3610
+ /**
3611
+ * @internal Whether `upsert()` looks the row up rather than running one
3612
+ * statement, which is also exactly when `buildUpsert` refuses these args.
3613
+ * prisma-compat reads it to decide whether an array `$transaction` can batch
3614
+ * an upsert or has to run the array inside a transaction.
3615
+ */
3616
+ upsertNeedsLookup(args) {
3617
+ return (hasRelationFields((args.create ?? {}), this.tableMeta) ||
3618
+ hasRelationFields((args.update ?? {}), this.tableMeta) ||
3619
+ writesMod.upsertLookupReason(this.ctx, args) !== null);
3620
+ }
3621
+ /**
3622
+ * `upsert` as Prisma defines it: find the row `where` names, update it if it
3623
+ * exists, else insert `create`, in one transaction (the caller's, when there
3624
+ * is one). Taken whenever the single conflict statement would mean something
3625
+ * else (`upsertLookupReason`: `where` not pinned to `create`'s values, an
3626
+ * empty `update`, a global filter the engine's statement cannot carry) and
3627
+ * whenever `create` or `update` holds nested relation writes, which only the
3628
+ * nested-write engine can run.
3629
+ *
3630
+ * Validation is DATA-INDEPENDENT, as it is for the batched relation loader:
3631
+ * both halves are compiled before the lookup runs, so an unknown field in
3632
+ * `update` is refused whether or not the row exists, and a call is never
3633
+ * valid on one row and invalid on the next. Only the scalar part of each half
3634
+ * is compiled here; nested relation writes are validated by the engine that
3635
+ * runs them.
3636
+ *
3637
+ * Not atomic against a concurrent insert of the same key, exactly as Prisma's
3638
+ * own lookup is not: two callers can both miss, and the second insert then
3639
+ * fails on the unique constraint (E008) when `create` carries the key. It
3640
+ * never writes two rows over one key the database enforces.
3641
+ */
3642
+ async upsertLookupFirst(args) {
3643
+ const where = (args.where ?? {});
3644
+ const create = (args.create ?? {});
3645
+ const update = (args.update ?? {});
3646
+ assertMutationWhereIdentifiesOneRow(this.tableMeta, this.table, expandCompoundUniqueWhere(this.tableMeta, where), 'upsert');
3647
+ this.resolveWriteProjection(args);
3648
+ // An undefined option is an absent one on every call below.
3649
+ const skip = { skipGlobalFilters: args.skipGlobalFilters };
3650
+ const shape = { select: args.select, omit: args.omit, timeout: args.timeout };
3651
+ const scalarCreate = extractRelationFields(create, this.tableMeta).scalars;
3652
+ const scalarUpdate = extractRelationFields(update, this.tableMeta).scalars;
3653
+ const updateIsEmpty = Object.values(update).every((v) => v === undefined);
3654
+ if (Object.keys(scalarCreate).length > 0)
3655
+ this.buildCreate({ data: scalarCreate });
3656
+ if (Object.values(scalarUpdate).some((v) => v !== undefined)) {
3657
+ this.buildUpdate({ where, data: scalarUpdate, ...skip });
3658
+ }
3659
+ const run = async (table) => {
3660
+ const found = await table.findUnique({ where, ...shape, ...skip });
3661
+ if (found) {
3662
+ // Prisma reads an empty `update` as "leave it as it is": the row found
3663
+ // IS the answer, projected as the write would have projected it.
3664
+ if (updateIsEmpty)
3665
+ return found;
3666
+ return (await table.update({ where, data: update, ...shape, ...skip }));
3667
+ }
3668
+ return (await table.create({ data: create, ...shape }));
3669
+ };
3670
+ if (this.txScoped) {
3671
+ return run(this.buildNestedCtx().tx.table(this.table));
3672
+ }
3673
+ return this.runInImplicitTx((ctx) => run(ctx.tx.table(this.table)));
3674
+ }
3607
3675
  // -------------------------------------------------------------------------
3608
3676
  // updateMany, UPDATE ... WHERE ... returning count
3609
3677
  // -------------------------------------------------------------------------
@@ -169,3 +169,27 @@ export declare function isInternalRowSelector(where: unknown): boolean;
169
169
  * uniqueness source there.
170
170
  */
171
171
  export declare function whereIdentifiesOneRow(meta: TableMetadata, where: Record<string, unknown>): boolean;
172
+ /**
173
+ * Whether an `upsert` means the same thing as ONE conflict statement.
174
+ *
175
+ * An upsert's contract is Prisma's: find the row `where` names; update it if
176
+ * it exists, else insert `create`. A single `INSERT ... ON CONFLICT (<where
177
+ * keys>)` (or PowDB's `upsert ... on`) compares the conflict columns against
178
+ * the values being INSERTED, which are `create`'s, and never reads `where`'s
179
+ * values at all. The two agree exactly when every `where` key is pinned to the
180
+ * value `create` carries for that column. Otherwise the statement answers a
181
+ * different question: `where: { id: 5 }` with a `create` that leaves `id` to
182
+ * its sequence inserted a new row every time and never touched row 5, and
183
+ * `where: { email: a }` with `create: { email: b }` updated the row holding
184
+ * `b`, silently.
185
+ *
186
+ * Shared by every engine, like the one-row rule above: each one decides here
187
+ * whether it may take its single-statement fast path, and looks the row up by
188
+ * `where` first when it may not. `false` is always a SAFE answer (the lookup
189
+ * path is correct for every shape, only slower), so anything this does not
190
+ * recognise as a plain matching value answers `false`: an operator other than a
191
+ * bare `equals`, a different object (two Dates compare by instant), a key that
192
+ * names no column, and NULL, which a unique index never treats as equal to
193
+ * anything.
194
+ */
195
+ export declare function upsertWhereMatchesCreate(meta: TableMetadata, where: Record<string, unknown>, create: Record<string, unknown>): boolean;
@@ -420,6 +420,60 @@ export function whereIdentifiesOneRow(meta, where) {
420
420
  return false;
421
421
  return uniqueColumnSets(meta).some((cols) => cols.every((c) => pinned.has(c)));
422
422
  }
423
+ /**
424
+ * Whether an `upsert` means the same thing as ONE conflict statement.
425
+ *
426
+ * An upsert's contract is Prisma's: find the row `where` names; update it if
427
+ * it exists, else insert `create`. A single `INSERT ... ON CONFLICT (<where
428
+ * keys>)` (or PowDB's `upsert ... on`) compares the conflict columns against
429
+ * the values being INSERTED, which are `create`'s, and never reads `where`'s
430
+ * values at all. The two agree exactly when every `where` key is pinned to the
431
+ * value `create` carries for that column. Otherwise the statement answers a
432
+ * different question: `where: { id: 5 }` with a `create` that leaves `id` to
433
+ * its sequence inserted a new row every time and never touched row 5, and
434
+ * `where: { email: a }` with `create: { email: b }` updated the row holding
435
+ * `b`, silently.
436
+ *
437
+ * Shared by every engine, like the one-row rule above: each one decides here
438
+ * whether it may take its single-statement fast path, and looks the row up by
439
+ * `where` first when it may not. `false` is always a SAFE answer (the lookup
440
+ * path is correct for every shape, only slower), so anything this does not
441
+ * recognise as a plain matching value answers `false`: an operator other than a
442
+ * bare `equals`, a different object (two Dates compare by instant), a key that
443
+ * names no column, and NULL, which a unique index never treats as equal to
444
+ * anything.
445
+ */
446
+ export function upsertWhereMatchesCreate(meta, where, create) {
447
+ const createByColumn = new Map();
448
+ for (const [key, value] of Object.entries(create)) {
449
+ if (value === undefined)
450
+ continue;
451
+ const column = resolveColumnName(meta, key);
452
+ if (column !== undefined)
453
+ createByColumn.set(column, value);
454
+ }
455
+ let keys = 0;
456
+ for (const [key, raw] of Object.entries(expandCompoundUniqueWhere(meta, where))) {
457
+ if (raw === undefined)
458
+ continue;
459
+ const column = resolveColumnName(meta, key);
460
+ if (column === undefined || !createByColumn.has(column))
461
+ return false;
462
+ const value = isBareEquals(raw) ? raw.equals : raw;
463
+ if (!sameKeyValue(value, createByColumn.get(column)))
464
+ return false;
465
+ keys++;
466
+ }
467
+ return keys > 0;
468
+ }
469
+ /** `{ equals: v }` and nothing else (a `mode` beside it changes the match). */
470
+ function isBareEquals(value) {
471
+ return isPlainObject(value) && Object.keys(value).length === 1 && 'equals' in value;
472
+ }
473
+ /** Equal non-null key values; two Dates compare by instant, other objects by identity. */
474
+ function sameKeyValue(a, b) {
475
+ return a != null && (a instanceof Date && b instanceof Date ? a.getTime() === b.getTime() : a === b);
476
+ }
423
477
  /** A bare value, or an operator object whose `equals` is a value. */
424
478
  function isPinnedToOneValue(value) {
425
479
  if (value === undefined || value === null)
@@ -59,6 +59,29 @@ export declare function makeCreateReselect<T extends object>(qi: BuilderCtx, ins
59
59
  export declare function buildCreateMany<T extends object>(qi: BuilderCtx, args: CreateManyArgs<T>): DeferredQuery<T[]>;
60
60
  export declare function buildUpdate<T extends object>(qi: BuilderCtx, args: UpdateArgs<T>, projection?: WriteProjection): DeferredQuery<T>;
61
61
  export declare function buildDelete<T extends object>(qi: BuilderCtx, args: DeleteArgs<T>, projection?: WriteProjection): DeferredQuery<T>;
62
+ /**
63
+ * Why an `upsert` cannot run as this engine's single conflict statement, or
64
+ * `null` when it can. The one authority for that decision: `upsert()` asks it
65
+ * to choose between the statement and a lookup by `where` inside a
66
+ * transaction, and `buildUpsert` asks it to refuse the shapes its one
67
+ * statement cannot express, since a `DeferredQuery` (a pipeline, an array
68
+ * `$transaction`) has no second round trip to look anything up with.
69
+ *
70
+ * - `where-differs`: `where` is not pinned to `create`'s values, so the
71
+ * conflict statement would compare the wrong row (upsertWhereMatchesCreate).
72
+ * - `empty-update`: an `update` with no fields has no `DO UPDATE SET` to emit;
73
+ * every engine rejected the statement as a syntax error. Prisma reads it as
74
+ * "create it if it is missing", which the lookup answers.
75
+ * - `global-filter`: MySQL's `ON DUPLICATE KEY UPDATE` has no predicate slot
76
+ * and SQL Server's MERGE cannot take the builder's column references there,
77
+ * so the table's global filter cannot guard the conflict update. The lookup
78
+ * and the update both carry the filter, so another tenant's row is invisible
79
+ * to them.
80
+ *
81
+ * Reads `currentSkip`, so it resolves `skipGlobalFilters` itself first.
82
+ */
83
+ export type UpsertLookupReason = 'where-differs' | 'empty-update' | 'global-filter';
84
+ export declare function upsertLookupReason<T extends object>(qi: BuilderCtx, args: UpsertArgs<T>): UpsertLookupReason | null;
62
85
  export declare function buildUpsert<T extends object>(qi: BuilderCtx, args: UpsertArgs<T>, projection?: WriteProjection): DeferredQuery<T>;
63
86
  export declare function buildUpdateMany<T extends object>(qi: BuilderCtx, args: UpdateManyArgs<T>): DeferredQuery<{
64
87
  count: number;