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.
@@ -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/cjs/powql.js CHANGED
@@ -2701,7 +2701,7 @@ class PowqlInterface {
2701
2701
  if (value === undefined)
2702
2702
  continue;
2703
2703
  if ((0, utils_js_1.resolveRelationDef)(this.meta.relations, field)) {
2704
- throw new errors_js_1.UnsupportedFeatureError('nested writes', 'PowDB', `relation "${field}", nested writes need create()/update(), not createMany()/upsert()`);
2704
+ throw new errors_js_1.UnsupportedFeatureError('nested writes', 'PowDB', `relation "${field}", nested writes need create()/update()/upsert(), not createMany()`);
2705
2705
  }
2706
2706
  out.push({ col: this.column(field), value });
2707
2707
  }
@@ -2900,7 +2900,7 @@ class PowqlInterface {
2900
2900
  if (value === undefined)
2901
2901
  continue;
2902
2902
  if ((0, utils_js_1.resolveRelationDef)(this.meta.relations, field)) {
2903
- throw new errors_js_1.UnsupportedFeatureError('nested writes', 'PowDB', `relation "${field}", nested writes need create()/update(), not updateMany()/upsert()`);
2903
+ throw new errors_js_1.UnsupportedFeatureError('nested writes', 'PowDB', `relation "${field}", nested writes need create()/update()/upsert(), not updateMany()`);
2904
2904
  }
2905
2905
  const colMeta = this.column(field);
2906
2906
  const ref = this.ref(field);
@@ -3067,46 +3067,47 @@ class PowqlInterface {
3067
3067
  const plan = this.writeReturnPlan(args);
3068
3068
  const createData = this.applyPkDefault(args.create);
3069
3069
  const pkCol = this.meta.primaryKey[0];
3070
- // The conflict target is the columns `where` names, as on the SQL
3071
- // engines (`ON CONFLICT (<where keys>)`), with the conflicting VALUES
3072
- // taken from `create`. This path used to conflict on the primary key
3073
- // whatever `where` named, so an upsert keyed on another unique column
3074
- // (`where: { email }`) never found the existing row.
3075
3070
  const conflictColumns = Object.keys(upsertWhere)
3076
3071
  .filter((k) => upsertWhere[k] !== undefined)
3077
3072
  .map((k) => this.column(k).name);
3073
+ const updateData = (args.update ?? {});
3078
3074
  // PowQL's native `upsert … on .col` expresses exactly one shape: a single
3079
- // conflict column that is the primary key, with no predicate on its
3080
- // conflict branch. Everything else is an atomic reselect-or-write
3081
- // transaction.
3075
+ // conflict column that is the primary key, pinned to the value `create`
3076
+ // inserts, with a non-empty update and no predicate on its conflict
3077
+ // branch. Every other shape looks the row up by `where` first, in one
3078
+ // transaction, which is what the SQL engines do for the same shapes and
3079
+ // what the call means (query/compound-unique.ts upsertWhereMatchesCreate).
3082
3080
  //
3083
3081
  // That includes a table under a global filter. The native statement's
3084
3082
  // `on conflict` branch carries no predicate, so a tenant-scoped client
3085
3083
  // whose `create` key matched ANOTHER tenant's row updated that row, and
3086
3084
  // the tenant-filtered reselect then reported the write it had just made
3087
- // as a NotFoundError. The SQL engines guard the same branch with the
3088
- // filter (`DO UPDATE … WHERE tenant_id = $n`). The lookup-first path runs
3089
- // its find and its update through the filtered interface, so another
3090
- // tenant's row is invisible to it: the insert then collides on the key
3091
- // and fails, as it does on the SQL engines, and nothing is overwritten.
3085
+ // as a NotFoundError. The lookup runs its find and its update through the
3086
+ // filtered interface, so another tenant's row is invisible to it: the
3087
+ // insert then collides on the key and fails, and nothing is overwritten.
3092
3088
  const filtered = this.applyGlobalFilter('', [], args.skipGlobalFilters) !== '';
3093
3089
  const native = this.meta.primaryKey.length === 1 &&
3094
3090
  pkCol !== undefined &&
3095
3091
  conflictColumns.length === 1 &&
3096
3092
  conflictColumns[0] === pkCol &&
3093
+ (0, compound_unique_js_1.upsertWhereMatchesCreate)(this.meta, upsertWhere, (args.create ?? {})) &&
3094
+ !(0, nested_write_js_1.hasRelationFields)(args.create, this.meta) &&
3095
+ !(0, nested_write_js_1.hasRelationFields)(updateData, this.meta) &&
3096
+ Object.values(updateData).some((v) => v !== undefined) &&
3097
3097
  !filtered;
3098
3098
  if (!native) {
3099
- return this.upsertLookupFirst(createData, args.update, conflictColumns, {
3099
+ return this.upsertLookupFirst(upsertWhere, createData, updateData, {
3100
3100
  select: args.select,
3101
3101
  omit: args.omit,
3102
3102
  skipGlobalFilters: args.skipGlobalFilters,
3103
+ timeout: args.timeout,
3103
3104
  });
3104
3105
  }
3105
3106
  const params = [];
3106
3107
  const createBody = this.scalarData(createData)
3107
3108
  .map((a) => `${(0, powdb_shared_js_1.quotePowqlIdent)(a.col.name)} := ${this.writeRef(a.value, a.col, params)}`)
3108
3109
  .join(', ');
3109
- const updateBody = this.buildUpdateAssignments(args.update, params);
3110
+ const updateBody = this.buildUpdateAssignments(updateData, params);
3110
3111
  // PowDB 0.7.0's `upsert` statement does NOT accept a trailing `returning`
3111
3112
  // (verified: "unexpected trailing token … 'returning'"), because it is one
3112
3113
  // atomic insert-or-update, not two branches. So upsert alone keeps the
@@ -3126,48 +3127,42 @@ class PowqlInterface {
3126
3127
  });
3127
3128
  }
3128
3129
  /**
3129
- * Upsert as a reselect-or-write inside one flat transaction, for every shape
3130
- * the native `upsert … on .col` statement cannot express: a conflict target
3131
- * other than a single-column primary key (it takes one column and PowDB has
3132
- * no composite unique), and a table under a global filter (its conflict
3133
- * branch takes no predicate). PowDB's single writer makes the read-then-write
3134
- * safe from concurrent writers; the transaction makes it atomic with the
3135
- * write.
3130
+ * `upsert` as Prisma defines it, for every shape the native `upsert … on`
3131
+ * statement cannot express: find the row `where` names, update it if it
3132
+ * exists, else insert `create`, in one flat transaction. PowDB's single
3133
+ * writer makes the read-then-write safe from concurrent writers; the
3134
+ * transaction makes it atomic with the write.
3136
3135
  *
3137
- * The row is looked up by the conflict columns with `create`'s values for
3138
- * them, which is what `ON CONFLICT (<cols>)` compares on the SQL engines. The
3139
- * find and the update run through the transaction's table interface, so a
3140
- * configured global filter applies to both exactly as it does to any other
3141
- * read or write, and `skipGlobalFilters` is forwarded to both.
3142
- */
3143
- async upsertLookupFirst(createData, updateData, conflictColumns, shape) {
3144
- const { skipGlobalFilters, ...projection } = shape;
3136
+ * The lookup is by `where`'s own values. It used to be by `create`'s values
3137
+ * for the conflict columns, which is what the native statement compares, and
3138
+ * so answered a different question whenever the two disagreed: `where: { id }`
3139
+ * with a `create` that leaves the key to the engine inserted a new row every
3140
+ * time. The find and the update run through the transaction's table
3141
+ * interface, so a configured global filter applies to both exactly as it does
3142
+ * to any other read or write, and `skipGlobalFilters` is forwarded to both.
3143
+ *
3144
+ * Both halves are compiled before the lookup, so an unknown field in `update`
3145
+ * is refused whether or not the row exists (the SQL engines do the same).
3146
+ */
3147
+ async upsertLookupFirst(where, createData, updateData, shape) {
3148
+ const { skipGlobalFilters, timeout, ...projection } = shape;
3145
3149
  const skip = skipGlobalFilters === undefined ? {} : { skipGlobalFilters };
3146
- // Accept either the camelCase field or the snake_case column in `create`
3147
- // (create() resolves both), and key the where by field name.
3148
- const keyPairs = conflictColumns.map((col) => {
3149
- const field = this.meta.reverseColumnMap[col] ?? col;
3150
- return { field, value: createData[field] ?? createData[col] };
3151
- });
3152
- const keyless = keyPairs.some((p) => p.value == null);
3153
- const isCompositePk = keyPairs.length > 1 &&
3154
- keyPairs.length === this.meta.primaryKey.length &&
3155
- conflictColumns.every((c) => this.meta.primaryKey.includes(c));
3156
- if (keyless && isCompositePk) {
3157
- throw new errors_js_1.ValidationError(`upsert on "${this.table}" needs every composite-PK field in \`create\` (${keyPairs
3158
- .map((p) => p.field)
3159
- .join(', ')}).`);
3160
- }
3161
- const keyWhere = Object.fromEntries(keyPairs.map((p) => [p.field, p.value]));
3150
+ const time = timeout === undefined ? {} : { timeout };
3151
+ this.scalarData((0, nested_write_js_1.extractRelationFields)(createData, this.meta).scalars);
3152
+ const scalarUpdate = (0, nested_write_js_1.extractRelationFields)(updateData, this.meta).scalars;
3153
+ if (Object.values(scalarUpdate).some((v) => v !== undefined))
3154
+ this.buildUpdateAssignments(scalarUpdate, []);
3155
+ const updateIsEmpty = Object.values(updateData).every((v) => v === undefined);
3162
3156
  const run = async (ctx) => {
3163
3157
  const tbl = ctx.tx.table(this.table);
3164
- // A row whose `create` leaves a conflict column unset cannot collide on
3165
- // it (NULL never equals NULL in a unique index, and a server-assigned key
3166
- // is new), which is what `ON CONFLICT` concludes on the SQL engines too.
3167
- const existing = keyless ? null : await tbl.findUnique({ where: keyWhere, ...skip });
3168
- return existing
3169
- ? (await tbl.update({ where: keyWhere, data: updateData, ...projection, ...skip }))
3170
- : (await tbl.create({ data: createData, ...projection }));
3158
+ const existing = await tbl.findUnique({ where, ...projection, ...skip, ...time });
3159
+ if (existing) {
3160
+ // An empty `update` means "leave it as it is": the row found is the answer.
3161
+ if (updateIsEmpty)
3162
+ return existing;
3163
+ return (await tbl.update({ where, data: updateData, ...projection, ...skip, ...time }));
3164
+ }
3165
+ return (await tbl.create({ data: createData, ...projection, ...time }));
3171
3166
  };
3172
3167
  return this.isTxScoped() ? run(this.buildNestedCtx()) : this.runInImplicitTx(run);
3173
3168
  }
@@ -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[]>;
@@ -1775,53 +1775,6 @@ function hasNestedKeys(ctx, mm, data) {
1775
1775
  return v !== null && typeof v === 'object' && !Array.isArray(v) && !(v instanceof Date);
1776
1776
  });
1777
1777
  }
1778
- /**
1779
- * Whether an upsert's translated `where` key values all equal the
1780
- * corresponding `create` values. When they do (the common Prisma idiom), the
1781
- * native single-statement ON CONFLICT upsert is semantically identical to
1782
- * Prisma's lookup-first and stays atomic. When they differ, native upsert
1783
- * would insert the `create` row even though the `where` row exists, so the
1784
- * adapter must emulate lookup-first instead.
1785
- */
1786
- function upsertKeysMatch(t) {
1787
- const where = t.where;
1788
- const create = t.create;
1789
- if (!isPlainObject(where) || !isPlainObject(create))
1790
- return false;
1791
- const scalarEq = (a, b) => {
1792
- if (a instanceof Date || b instanceof Date) {
1793
- return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
1794
- }
1795
- return a === b;
1796
- };
1797
- for (const [k, v] of Object.entries(where)) {
1798
- if (v !== null && typeof v === 'object' && !(v instanceof Date)) {
1799
- // Compound-unique selector object: every member must scalar-match create.
1800
- if (Array.isArray(v))
1801
- return false;
1802
- for (const [mk, mv] of Object.entries(v)) {
1803
- if (mv !== null && typeof mv === 'object' && !(mv instanceof Date))
1804
- return false;
1805
- if (!scalarEq(mv, create[mk]))
1806
- return false;
1807
- }
1808
- continue;
1809
- }
1810
- if (!scalarEq(v, create[k]))
1811
- return false;
1812
- }
1813
- return true;
1814
- }
1815
- /** Prisma upsert semantics: look up by where; update the found row, else insert create. */
1816
- async function upsertLookupFirst(qi, t) {
1817
- const existing = await qi.findUnique({ where: t.where });
1818
- // The projection rides along so both branches return what the native upsert
1819
- // path returns (an explicitly selected PII field included).
1820
- const shape = { select: t.select, omit: t.omit };
1821
- if (existing)
1822
- return qi.update({ where: t.where, data: t.update, ...shape });
1823
- return qi.create({ data: t.create, ...shape });
1824
- }
1825
1778
  /**
1826
1779
  * The row shapes a translated `createMany` has to insert, as contiguous runs
1827
1780
  * that each name the same fields (see {@link createManyShapeRuns}).
@@ -1883,7 +1836,7 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
1883
1836
  reshape: batch.reshape,
1884
1837
  nested: () => {
1885
1838
  try {
1886
- return batch.nested?.(translate()) ?? false;
1839
+ return batch.nested?.(translate(), getQI()) ?? false;
1887
1840
  }
1888
1841
  catch {
1889
1842
  return false; // let the build path surface the translation error consistently
@@ -2045,24 +1998,17 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
2045
1998
  };
2046
1999
  (0, index_js_1.applyNativeOptions)(index_js_1.UPSERT_OPTIONS, a, t);
2047
2000
  return t;
2048
- }, (qi, t) => {
2049
- // Native ON CONFLICT upsert is only Prisma-equivalent when the where
2050
- // key values equal the create values AND no nested write data is
2051
- // present; otherwise emulate Prisma's lookup-first atomically.
2052
- if (upsertKeysMatch(t) && !hasNestedKeys(ctx, mm, t.create) && !hasNestedKeys(ctx, mm, t.update)) {
2053
- return qi.upsert(t).then(shape);
2054
- }
2055
- return runInTx(async (table) => shape(await upsertLookupFirst(table(mm.table), t)));
2056
- }, {
2001
+ },
2002
+ // Core `upsert` has Prisma's semantics itself (it looks the row up by
2003
+ // `where` whenever one conflict statement would mean something else,
2004
+ // and runs nested writes), so the adapter passes it straight through.
2005
+ (qi, t) => qi.upsert(t).then(shape), {
2057
2006
  build: (qi, t) => qi.buildUpsert(t),
2058
2007
  reshape: shape,
2059
- nested: (t) => !upsertKeysMatch(t) || hasNestedKeys(ctx, mm, t.create) || hasNestedKeys(ctx, mm, t.update),
2060
- execInTx: async (table, t) => {
2061
- if (upsertKeysMatch(t) && !hasNestedKeys(ctx, mm, t.create) && !hasNestedKeys(ctx, mm, t.update)) {
2062
- return shape(await table(mm.table).upsert(t));
2063
- }
2064
- return shape(await upsertLookupFirst(table(mm.table), t));
2065
- },
2008
+ // Batchable only when core would run it as one statement. PowDB has
2009
+ // no deferred statements at all, so it always takes the tx path.
2010
+ nested: (t, qi) => qi.upsertNeedsLookup?.(t) ?? true,
2011
+ execInTx: async (table, t) => shape(await table(mm.table).upsert(t)),
2066
2012
  });
2067
2013
  },
2068
2014
  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
  }>;
@@ -3638,10 +3638,78 @@ class QueryInterface {
3638
3638
  // -------------------------------------------------------------------------
3639
3639
  async upsert(args) {
3640
3640
  return this.executeWithMiddleware('upsert', args, async () => {
3641
+ if (this.upsertNeedsLookup(args)) {
3642
+ return this.upsertLookupFirst(args);
3643
+ }
3641
3644
  const deferred = this.buildUpsert(args);
3642
3645
  return this.executeMutation(deferred, args.timeout);
3643
3646
  });
3644
3647
  }
3648
+ /**
3649
+ * @internal Whether `upsert()` looks the row up rather than running one
3650
+ * statement, which is also exactly when `buildUpsert` refuses these args.
3651
+ * prisma-compat reads it to decide whether an array `$transaction` can batch
3652
+ * an upsert or has to run the array inside a transaction.
3653
+ */
3654
+ upsertNeedsLookup(args) {
3655
+ return ((0, nested_write_js_1.hasRelationFields)((args.create ?? {}), this.tableMeta) ||
3656
+ (0, nested_write_js_1.hasRelationFields)((args.update ?? {}), this.tableMeta) ||
3657
+ writesMod.upsertLookupReason(this.ctx, args) !== null);
3658
+ }
3659
+ /**
3660
+ * `upsert` as Prisma defines it: find the row `where` names, update it if it
3661
+ * exists, else insert `create`, in one transaction (the caller's, when there
3662
+ * is one). Taken whenever the single conflict statement would mean something
3663
+ * else (`upsertLookupReason`: `where` not pinned to `create`'s values, an
3664
+ * empty `update`, a global filter the engine's statement cannot carry) and
3665
+ * whenever `create` or `update` holds nested relation writes, which only the
3666
+ * nested-write engine can run.
3667
+ *
3668
+ * Validation is DATA-INDEPENDENT, as it is for the batched relation loader:
3669
+ * both halves are compiled before the lookup runs, so an unknown field in
3670
+ * `update` is refused whether or not the row exists, and a call is never
3671
+ * valid on one row and invalid on the next. Only the scalar part of each half
3672
+ * is compiled here; nested relation writes are validated by the engine that
3673
+ * runs them.
3674
+ *
3675
+ * Not atomic against a concurrent insert of the same key, exactly as Prisma's
3676
+ * own lookup is not: two callers can both miss, and the second insert then
3677
+ * fails on the unique constraint (E008) when `create` carries the key. It
3678
+ * never writes two rows over one key the database enforces.
3679
+ */
3680
+ async upsertLookupFirst(args) {
3681
+ const where = (args.where ?? {});
3682
+ const create = (args.create ?? {});
3683
+ const update = (args.update ?? {});
3684
+ (0, compound_unique_js_1.assertMutationWhereIdentifiesOneRow)(this.tableMeta, this.table, (0, compound_unique_js_1.expandCompoundUniqueWhere)(this.tableMeta, where), 'upsert');
3685
+ this.resolveWriteProjection(args);
3686
+ // An undefined option is an absent one on every call below.
3687
+ const skip = { skipGlobalFilters: args.skipGlobalFilters };
3688
+ const shape = { select: args.select, omit: args.omit, timeout: args.timeout };
3689
+ const scalarCreate = (0, nested_write_js_1.extractRelationFields)(create, this.tableMeta).scalars;
3690
+ const scalarUpdate = (0, nested_write_js_1.extractRelationFields)(update, this.tableMeta).scalars;
3691
+ const updateIsEmpty = Object.values(update).every((v) => v === undefined);
3692
+ if (Object.keys(scalarCreate).length > 0)
3693
+ this.buildCreate({ data: scalarCreate });
3694
+ if (Object.values(scalarUpdate).some((v) => v !== undefined)) {
3695
+ this.buildUpdate({ where, data: scalarUpdate, ...skip });
3696
+ }
3697
+ const run = async (table) => {
3698
+ const found = await table.findUnique({ where, ...shape, ...skip });
3699
+ if (found) {
3700
+ // Prisma reads an empty `update` as "leave it as it is": the row found
3701
+ // IS the answer, projected as the write would have projected it.
3702
+ if (updateIsEmpty)
3703
+ return found;
3704
+ return (await table.update({ where, data: update, ...shape, ...skip }));
3705
+ }
3706
+ return (await table.create({ data: create, ...shape }));
3707
+ };
3708
+ if (this.txScoped) {
3709
+ return run(this.buildNestedCtx().tx.table(this.table));
3710
+ }
3711
+ return this.runInImplicitTx((ctx) => run(ctx.tx.table(this.table)));
3712
+ }
3645
3713
  // -------------------------------------------------------------------------
3646
3714
  // updateMany, UPDATE ... WHERE ... returning count
3647
3715
  // -------------------------------------------------------------------------
@@ -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;
@@ -48,6 +48,7 @@ exports.uniqueKeyNames = uniqueKeyNames;
48
48
  exports.markInternalRowSelector = markInternalRowSelector;
49
49
  exports.isInternalRowSelector = isInternalRowSelector;
50
50
  exports.whereIdentifiesOneRow = whereIdentifiesOneRow;
51
+ exports.upsertWhereMatchesCreate = upsertWhereMatchesCreate;
51
52
  const errors_js_1 = require("../errors.js");
52
53
  const filters_js_1 = require("./filters.js");
53
54
  const utils_js_1 = require("./utils.js");
@@ -430,6 +431,60 @@ function whereIdentifiesOneRow(meta, where) {
430
431
  return false;
431
432
  return uniqueColumnSets(meta).some((cols) => cols.every((c) => pinned.has(c)));
432
433
  }
434
+ /**
435
+ * Whether an `upsert` means the same thing as ONE conflict statement.
436
+ *
437
+ * An upsert's contract is Prisma's: find the row `where` names; update it if
438
+ * it exists, else insert `create`. A single `INSERT ... ON CONFLICT (<where
439
+ * keys>)` (or PowDB's `upsert ... on`) compares the conflict columns against
440
+ * the values being INSERTED, which are `create`'s, and never reads `where`'s
441
+ * values at all. The two agree exactly when every `where` key is pinned to the
442
+ * value `create` carries for that column. Otherwise the statement answers a
443
+ * different question: `where: { id: 5 }` with a `create` that leaves `id` to
444
+ * its sequence inserted a new row every time and never touched row 5, and
445
+ * `where: { email: a }` with `create: { email: b }` updated the row holding
446
+ * `b`, silently.
447
+ *
448
+ * Shared by every engine, like the one-row rule above: each one decides here
449
+ * whether it may take its single-statement fast path, and looks the row up by
450
+ * `where` first when it may not. `false` is always a SAFE answer (the lookup
451
+ * path is correct for every shape, only slower), so anything this does not
452
+ * recognise as a plain matching value answers `false`: an operator other than a
453
+ * bare `equals`, a different object (two Dates compare by instant), a key that
454
+ * names no column, and NULL, which a unique index never treats as equal to
455
+ * anything.
456
+ */
457
+ function upsertWhereMatchesCreate(meta, where, create) {
458
+ const createByColumn = new Map();
459
+ for (const [key, value] of Object.entries(create)) {
460
+ if (value === undefined)
461
+ continue;
462
+ const column = (0, utils_js_1.resolveColumnName)(meta, key);
463
+ if (column !== undefined)
464
+ createByColumn.set(column, value);
465
+ }
466
+ let keys = 0;
467
+ for (const [key, raw] of Object.entries(expandCompoundUniqueWhere(meta, where))) {
468
+ if (raw === undefined)
469
+ continue;
470
+ const column = (0, utils_js_1.resolveColumnName)(meta, key);
471
+ if (column === undefined || !createByColumn.has(column))
472
+ return false;
473
+ const value = isBareEquals(raw) ? raw.equals : raw;
474
+ if (!sameKeyValue(value, createByColumn.get(column)))
475
+ return false;
476
+ keys++;
477
+ }
478
+ return keys > 0;
479
+ }
480
+ /** `{ equals: v }` and nothing else (a `mode` beside it changes the match). */
481
+ function isBareEquals(value) {
482
+ return isPlainObject(value) && Object.keys(value).length === 1 && 'equals' in value;
483
+ }
484
+ /** Equal non-null key values; two Dates compare by instant, other objects by identity. */
485
+ function sameKeyValue(a, b) {
486
+ return a != null && (a instanceof Date && b instanceof Date ? a.getTime() === b.getTime() : a === b);
487
+ }
433
488
  /** A bare value, or an operator object whose `equals` is a value. */
434
489
  function isPinnedToOneValue(value) {
435
490
  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;
@@ -52,6 +52,7 @@ exports.makeCreateReselect = makeCreateReselect;
52
52
  exports.buildCreateMany = buildCreateMany;
53
53
  exports.buildUpdate = buildUpdate;
54
54
  exports.buildDelete = buildDelete;
55
+ exports.upsertLookupReason = upsertLookupReason;
55
56
  exports.buildUpsert = buildUpsert;
56
57
  exports.buildUpdateMany = buildUpdateMany;
57
58
  exports.buildDeleteMany = buildDeleteMany;
@@ -599,6 +600,35 @@ function buildDelete(qi, args, projection) {
599
600
  : undefined,
600
601
  };
601
602
  }
603
+ function upsertLookupReason(qi, args) {
604
+ const where = (args.where ?? {});
605
+ const create = (args.create ?? {});
606
+ if (!(0, compound_unique_js_1.upsertWhereMatchesCreate)(qi.tableMeta, where, create))
607
+ return 'where-differs';
608
+ if (writeEntries(qi, (args.update ?? {})).length === 0)
609
+ return 'empty-update';
610
+ if (!qi.dialect.supportsUpsertUpdateWhere) {
611
+ qi.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
612
+ const filter = whereMod.resolveGlobalFilter(qi, qi.table);
613
+ if (filter && whereMod.buildRenderedRefWhere(qi, qi.table, qi.tableMeta, qi.q(qi.table), filter, [])) {
614
+ return 'global-filter';
615
+ }
616
+ }
617
+ return null;
618
+ }
619
+ /** The refusal `buildUpsert` raises for each reason, naming the way forward. */
620
+ function upsertStatementRefusal(qi, reason, where) {
621
+ const call = 'Call `upsert()` unbatched: it looks the row up by `where` first.';
622
+ if (reason === 'global-filter') {
623
+ return new errors_js_1.UnsupportedFeatureError(`a batched upsert on "${qi.table}", which has a global filter,`, qi.dialect.name, `Its upsert statement cannot carry the filter. ${call} Or pass \`skipGlobalFilters: UNSAFE\`.`);
624
+ }
625
+ const keys = Object.keys(where).filter((k) => where[k] !== undefined);
626
+ return new errors_js_1.ValidationError(`A batched upsert on "${qi.table}" cannot be one statement: ` +
627
+ (reason === 'empty-update'
628
+ ? 'its `update` is empty. '
629
+ : `\`where\` (${keys.join(', ')}) does not carry \`create\`'s values, which are what one statement matches on. `) +
630
+ call);
631
+ }
602
632
  function buildUpsert(qi, args, projection) {
603
633
  assertWritable(qi, 'upsert');
604
634
  assertNoGeneratedColumns(qi, args.create, 'upsert');
@@ -623,6 +653,11 @@ function buildUpsert(qi, args, projection) {
623
653
  // `allowFullTableScan: UNSAFE`, which is not an option on `UpsertArgs` at
624
654
  // all, so borrowing it here would name a way out that does not exist.
625
655
  (0, compound_unique_js_1.assertMutationWhereIdentifiesOneRow)(qi.tableMeta, qi.table, upsertWhere, 'upsert');
656
+ // `upsert()` never reaches here with a reason (it looks the row up instead),
657
+ // so this refuses only a batched upsert, which has no round trip to spare.
658
+ const lookupReason = upsertLookupReason(qi, args);
659
+ if (lookupReason)
660
+ throw upsertStatementRefusal(qi, lookupReason, upsertWhere);
626
661
  // Build the INSERT part from create data
627
662
  const createEntries = writeEntries(qi, args.create);
628
663
  const columns = createEntries.map(([k]) => qi.toSqlColumn(k));
@@ -656,20 +691,6 @@ function buildUpsert(qi, args, projection) {
656
691
  // globally filtered table at parse time (42702), insert path included.
657
692
  let updateWhere;
658
693
  const upsertFilter = whereMod.resolveGlobalFilter(qi, qi.table);
659
- if (upsertFilter && !qi.dialect.supportsUpsertUpdateWhere) {
660
- // MySQL's `ON DUPLICATE KEY UPDATE` has no predicate slot and SQL Server's
661
- // MERGE cannot take the builder's column references there, so on those
662
- // engines the filter used to be DROPPED from the conflict update, silently.
663
- // A tenant-scoped upsert whose key matched another tenant's row updated
664
- // that row. Refused instead, when the filter would compile to anything:
665
- // the global-filter contract is that it scopes every update, and an
666
- // upsert that cannot honour it must not run as if it did.
667
- if (whereMod.buildRenderedRefWhere(qi, qi.table, qi.tableMeta, qi.q(qi.table), upsertFilter, [])) {
668
- throw new errors_js_1.UnsupportedFeatureError(`upsert on "${qi.table}", which has a global filter,`, qi.dialect.name, "This engine's upsert statement cannot carry the filter on its conflict update, so it could update a row " +
669
- 'the filter hides. Use findUnique, then update or create, inside $transaction; or pass ' +
670
- '`skipGlobalFilters: UNSAFE` if the upsert is meant to reach every row.');
671
- }
672
- }
673
694
  if (qi.dialect.supportsUpsertUpdateWhere) {
674
695
  const gf = upsertFilter;
675
696
  if (gf) {