@vantreeseba/drizzle-graphql 15.1.1 → 15.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -335,7 +335,9 @@ const { schema } = buildSchema(db, {
335
335
  send it at all — the SDL itself says the column is server-owned. It stays readable on the
336
336
  object type.
337
337
  - Every insert stamps it, including rows created by a nested `create`.
338
- - Updates never write it, so an update cannot hand a row to another owner.
338
+ - Updates never write it, so an update cannot hand a row to another owner. Neither does the
339
+ conflict branch of an upsert: it is left out of the default overwrite and of the columns
340
+ `onConflict.update` can name.
339
341
 
340
342
  Together the two close the loop: `contextValues` decides which tenant a new row belongs to, and
341
343
  `scope` decides which rows a request can see and change afterwards.
@@ -984,6 +986,9 @@ input StockItemIdLocationIdKey {
984
986
  composes with column filters, relation filters, `OR` / `AND` / `NOT`
985
987
  - Composite primary keys get a field like any other unique constraint
986
988
  - Single-column constraints do not: `eq` on the column already names the row
989
+ - Partial unique indexes (`uniqueIndex().on(…).where(…)`) do not either: they are unique
990
+ only among the rows the predicate matches, so with soft-deleted rows included
991
+ (`deleted: INCLUDE`) the key can match more than one
987
992
  - A field name a column or relation already holds is skipped, the same way a relation
988
993
  yields to a same-named column
989
994
  - It compiles to exactly the equalities you would have written by hand; the guarantee is in
@@ -1566,9 +1571,17 @@ mutation {
1566
1571
  - Columns the request did not supply are never overwritten — a partial upsert does not null
1567
1572
  out the rest of the row. Listing an unsupplied column in `update` is an error rather than
1568
1573
  a silent no-op
1574
+ - That holds per row: a batch whose rows supply different columns is split into one
1575
+ statement per run of consecutive rows that write the same columns, run in one transaction,
1576
+ so a row that left a column out keeps it. The rows come back in the order they were sent
1569
1577
  - `target` must match one of the table's unique constraints exactly; anything else is
1570
1578
  rejected with the list of valid targets, instead of the database's opaque "no unique or
1571
1579
  exclusion constraint matching" error
1580
+ - `target` can be a partial unique index — the `WHERE deleted_at IS NULL` companion of
1581
+ [soft delete](#soft-delete), say. Its predicate goes into `ON CONFLICT (…) WHERE …`, so a
1582
+ conflict is detected only against the rows the index covers
1583
+ - `contextValues` columns are never overwritten on conflict — see
1584
+ [columns the server owns](#columns-the-server-owns)
1572
1585
  - `action: NOTHING` inserts nothing and returns nothing for the conflicting row. An `UPDATE`
1573
1586
  with no columns left to write degrades to the same thing
1574
1587
  - `values` is the same `Create<Type>Input` the insert mutations take, so turning `insert`
@@ -1603,7 +1616,9 @@ Dialect differences:
1603
1616
  - **PostgreSQL** and **SQLite** — the full surface above. A table with no primary key and no
1604
1617
  unique constraint has nothing to conflict on, so it gets no upsert mutations at all
1605
1618
  - **MySQL** — `ON DUPLICATE KEY UPDATE` fires on whichever unique key was violated and takes
1606
- no predicate, so `<Type>OnConflict` there has only `action` and `update`. `action: NOTHING`
1619
+ no predicate, so `<Type>OnConflict` there has only `action` and `update`. Because the
1620
+ conflict can be on any unique key, the default overwrite leaves out the primary key as well
1621
+ — copying a new row's key over the conflicting row's would move it. `action: NOTHING`
1607
1622
  becomes `INSERT IGNORE`, and the mutations return `MutationReturn` like every other MySQL
1608
1623
  mutation
1609
1624
 
@@ -2580,8 +2595,11 @@ It understands `values` (one object or a list), `where` (`eq` for one row, `inAr
2580
2595
  many, an equality on every column of a composite key), and the per-entry `where` inside a
2581
2596
  batch update's `updates`. Check `complete` before treating `rows` as the affected set: it
2582
2597
  is `false` when the arguments do not pin the rows down — an insert whose key the database
2583
- generates, a filter on something other than the primary key, an unbounded update, or a
2584
- table with no primary key at all.
2598
+ generates, a filter on something other than the primary key, an unbounded update, a
2599
+ table with no primary key at all, or an upsert whose `onConflict` targets another unique key
2600
+ or can skip a conflicting row (`action: NOTHING`, a `where`). On MySQL, where an upsert has
2601
+ no `target` and conflicts on whichever unique key it hits, `complete` assumes the primary
2602
+ key: do not rely on it for a table with other unique keys.
2585
2603
 
2586
2604
  > [!NOTE]
2587
2605
  > `extensions.drizzle` is stable API. The point of publishing it is that consumers stop
package/dist/index.cjs CHANGED
@@ -2418,8 +2418,8 @@ const columnEnumCache = /* @__PURE__ */ new WeakMap();
2418
2418
  * input field the enum would have typed, rather than emitting an empty enum, which is
2419
2419
  * invalid GraphQL.
2420
2420
  */
2421
- const generateColumnEnum = (table, enumName, description, predicate = () => true) => {
2422
- const cacheKey = `${enumName}\u0000${excludedColumnsKey(table)}`;
2421
+ const generateColumnEnum = (table, enumName, description, predicate = () => true, variant = "") => {
2422
+ const cacheKey = `${enumName}\u0000${excludedColumnsKey(table)}\u0000${variant}`;
2423
2423
  let tableCache = columnEnumCache.get(table);
2424
2424
  const cached = tableCache?.get(cacheKey);
2425
2425
  if (cached) return cached;
@@ -2479,6 +2479,9 @@ const conflictActionEnumType = (typeName) => sharedType(typeName, {
2479
2479
  * `ON DUPLICATE KEY UPDATE` fires on any unique key and takes no predicate, so offering
2480
2480
  * either there would mean silently ignoring it.
2481
2481
  *
2482
+ * `update` never offers a `contextValues` column: the server owns it, and the conflict
2483
+ * branch is an update, which must not hand the row to another owner.
2484
+ *
2482
2485
  * Returns `undefined` when the table has nothing to conflict on (`withTarget` dialects
2483
2486
  * only) — the caller then generates no upsert mutations for that table at all, rather than
2484
2487
  * an operation whose every call is a database error.
@@ -2486,12 +2489,13 @@ const conflictActionEnumType = (typeName) => sharedType(typeName, {
2486
2489
  const generateOnConflictInput = (params) => {
2487
2490
  const { table, tableName, typeName, uniqueSets, tableFilters, withTarget, cacheCtx } = params;
2488
2491
  const resolveName = cacheCtx.typeName;
2492
+ const serverOwned = Object.keys(cacheCtx.contextValuesOf?.(tableName) ?? {}).sort();
2489
2493
  const updateEnum = generateColumnEnum(table, resolveName({
2490
2494
  kind: "columnEnum",
2491
2495
  defaultName: `${typeName}UpdateColumn`,
2492
2496
  table: tableName,
2493
2497
  operation: "conflictUpdate"
2494
- }), `Columns of ${typeName} that an upsert can overwrite`);
2498
+ }), `Columns of ${typeName} that an upsert can overwrite`, (_column, columnName) => !serverOwned.includes(columnName), serverOwned.join(","));
2495
2499
  if (!updateEnum) return;
2496
2500
  const fields = {
2497
2501
  action: {
@@ -2501,11 +2505,11 @@ const generateOnConflictInput = (params) => {
2501
2505
  },
2502
2506
  update: {
2503
2507
  type: new graphql.GraphQLList(new graphql.GraphQLNonNull(updateEnum)),
2504
- description: "Columns to overwrite on conflict. Defaults to every column the request supplied, minus the conflict target. Columns the request did not supply cannot be listed here — there would be no value to write."
2508
+ description: `Columns to overwrite on conflict. Defaults to every column the request supplied, minus the ${withTarget ? "conflict target" : "primary key"}. Columns the request did not supply cannot be listed here — there would be no value to write.`
2505
2509
  }
2506
2510
  };
2507
2511
  if (withTarget) {
2508
- const uniqueColumns = new Set(uniqueSets.flat());
2512
+ const uniqueColumns = new Set(uniqueSets.flatMap((set) => set.columns));
2509
2513
  const targetEnum = generateColumnEnum(table, resolveName({
2510
2514
  kind: "columnEnum",
2511
2515
  defaultName: `${typeName}ConflictTarget`,
@@ -2533,55 +2537,88 @@ const generateOnConflictInput = (params) => {
2533
2537
  });
2534
2538
  };
2535
2539
  /**
2536
- * Turns the request's `onConflict` argument and the rows it is inserting into the clause a
2537
- * dialect should attach.
2540
+ * Turns the request's `onConflict` argument and the rows it is inserting into the
2541
+ * statements a dialect should run — one per run of consecutive rows that write the same
2542
+ * columns on conflict.
2543
+ *
2544
+ * The split is what keeps a partial row partial. A multi-row insert fills a key one row
2545
+ * left out with DEFAULT, and `excluded.<column>` would carry that default onto the existing
2546
+ * row, so rows that supply different columns cannot share a `SET` list. Splitting into
2547
+ * *consecutive* runs, rather than grouping, keeps the rows a caller gets back in the order
2548
+ * it sent them. A batch whose rows agree — the usual case — is still one statement.
2538
2549
  *
2539
2550
  * `excludedRef` names the row that failed to insert in the dialect's own terms
2540
2551
  * (`excluded.col` on PostgreSQL and SQLite, `values(col)` on MySQL), which is what makes a
2541
2552
  * batch upsert update each row with its own values instead of the last row's.
2542
2553
  *
2554
+ * Never written on conflict, even when supplied: the conflict target (it already matches);
2555
+ * the primary key on a dialect with no target (MySQL's conflict can be on another unique
2556
+ * key, and copying a generated id over the existing row's would move it); and `serverOwned`
2557
+ * — the `contextValues` columns, which an update never reassigns.
2558
+ *
2543
2559
  * An UPDATE with nothing left to write degrades to NOTHING: `DO UPDATE SET` with an empty
2544
2560
  * body is not valid SQL, and doing nothing is what the request asked for anyway.
2545
2561
  */
2546
2562
  const resolveConflictPlan = (params) => {
2547
2563
  const { table, values, onConflict, pkNames, uniqueSets, excludedRef, withTarget, buildWhere } = params;
2548
2564
  const columns = (0, drizzle_orm.getColumns)(table);
2565
+ const serverOwned = new Set(params.serverOwned ?? []);
2549
2566
  let target;
2567
+ let targetWhere;
2550
2568
  if (withTarget) {
2551
2569
  const targetNames = onConflict?.target?.length ? onConflict.target : [...pkNames];
2552
2570
  if (!targetNames.length) throw drizzleError("Unable to upsert: no conflict target was given and this table has no primary key. Pass onConflict.target.", { code: "DRIZZLE_INVALID_ON_CONFLICT" });
2553
2571
  const requested = [...targetNames].sort().join(",");
2554
- if (!uniqueSets.some((set) => [...set].sort().join(",") === requested)) throw drizzleError(`Unable to upsert: [${targetNames.join(", ")}] is not a unique constraint on this table. Valid conflict targets: ${uniqueSets.map((set) => `[${set.join(", ")}]`).join(", ")}.`, { code: "DRIZZLE_INVALID_ON_CONFLICT" });
2572
+ const matched = uniqueSets.find((set) => [...set.columns].sort().join(",") === requested);
2573
+ if (!matched) throw drizzleError(`Unable to upsert: [${targetNames.join(", ")}] is not a unique constraint on this table. Valid conflict targets: ${uniqueSets.map((set) => `[${set.columns.join(", ")}]`).join(", ")}.`, { code: "DRIZZLE_INVALID_ON_CONFLICT" });
2555
2574
  target = targetNames.map((name) => columns[name]);
2575
+ targetWhere = matched.where;
2556
2576
  }
2557
- if ((onConflict?.action ?? "UPDATE") === "NOTHING") return {
2577
+ const nothing = {
2558
2578
  action: "NOTHING",
2559
2579
  target,
2580
+ targetWhere,
2560
2581
  set: {},
2561
2582
  setWhere: void 0
2562
2583
  };
2584
+ if ((onConflict?.action ?? "UPDATE") === "NOTHING") return [{
2585
+ values,
2586
+ plan: nothing
2587
+ }];
2563
2588
  const supplied = new Set(values.flatMap((row) => Object.keys(row)));
2564
- const targetNames = new Set(withTarget ? onConflict?.target?.length ? onConflict.target : pkNames : []);
2565
- let updateNames;
2589
+ const neverWritten = new Set(withTarget && onConflict?.target?.length ? onConflict.target : pkNames);
2590
+ let candidates;
2566
2591
  if (onConflict?.update?.length) {
2592
+ const owned = onConflict.update.filter((name) => serverOwned.has(name));
2593
+ if (owned.length) throw drizzleError(`Unable to upsert: onConflict.update lists ${owned.join(", ")}, which the server sets and an update never overwrites.`, { code: "DRIZZLE_INVALID_ON_CONFLICT" });
2567
2594
  const unsupplied = onConflict.update.filter((name) => !supplied.has(name));
2568
2595
  if (unsupplied.length) throw drizzleError(`Unable to upsert: onConflict.update lists ${unsupplied.join(", ")}, which the values do not supply.`, { code: "DRIZZLE_INVALID_ON_CONFLICT" });
2569
- updateNames = onConflict.update;
2570
- } else updateNames = [...supplied].filter((name) => !targetNames.has(name));
2571
- if (!updateNames.length) return {
2572
- action: "NOTHING",
2573
- target,
2574
- set: {},
2575
- setWhere: void 0
2576
- };
2577
- const set = Object.fromEntries(updateNames.map((name) => [name, excludedRef(columns[name].name)]));
2596
+ candidates = onConflict.update;
2597
+ } else candidates = [...supplied].filter((name) => !neverWritten.has(name) && !serverOwned.has(name));
2578
2598
  const setWhere = onConflict?.where && buildWhere ? buildWhere(onConflict.where) : void 0;
2579
- return {
2599
+ const planFor = (updateNames) => updateNames.length ? {
2580
2600
  action: "UPDATE",
2581
2601
  target,
2582
- set,
2602
+ targetWhere,
2603
+ set: Object.fromEntries(updateNames.map((name) => [name, excludedRef(columns[name].name)])),
2583
2604
  setWhere
2584
- };
2605
+ } : nothing;
2606
+ const batches = [];
2607
+ for (const row of values) {
2608
+ const updateNames = candidates.filter((name) => name in row);
2609
+ const key = updateNames.join(",");
2610
+ const last = batches[batches.length - 1];
2611
+ if (last?.key === key) last.values.push(row);
2612
+ else batches.push({
2613
+ key,
2614
+ values: [row],
2615
+ plan: planFor(updateNames)
2616
+ });
2617
+ }
2618
+ return batches.map(({ values: rows, plan }) => ({
2619
+ values: rows,
2620
+ plan
2621
+ }));
2585
2622
  };
2586
2623
  /** `excluded.<column>` — PostgreSQL and SQLite name the rejected row this way. */
2587
2624
  const excludedColumnRef = (columnName) => drizzle_orm.sql`excluded.${drizzle_orm.sql.identifier(columnName)}`;
@@ -3025,9 +3062,9 @@ const primaryKeyOrderExprs = (table, pkNames) => {
3025
3062
  return pkNames.map((n) => cols[n]).filter(Boolean).map((col) => (0, drizzle_orm.asc)(col));
3026
3063
  };
3027
3064
  /**
3028
- * Every set of columns that uniquely identifies a row of `table`: the primary key first,
3029
- * then each unique constraint and unique index, then each column declared `.unique()`
3030
- * inline. Sets are property names (what GraphQL inputs use), not database column names.
3065
+ * Every set of columns that is unique on `table`: the primary key first, then each unique
3066
+ * constraint and unique index, then each column declared `.unique()` inline. Sets are
3067
+ * property names (what GraphQL inputs use), not database column names.
3031
3068
  *
3032
3069
  * `getTableConfig` is the dialect's own — the three dialects expose the same
3033
3070
  * `{ primaryKeys, uniqueConstraints, indexes }` shape but from different modules, so the
@@ -3036,9 +3073,13 @@ const primaryKeyOrderExprs = (table, pkNames) => {
3036
3073
  * Index entries whose columns are SQL expressions rather than plain columns are skipped:
3037
3074
  * an expression index is a valid conflict target in the database but cannot be named by a
3038
3075
  * column enum. Deduplicated, order-insensitive — a column that is both the primary key and
3039
- * a unique constraint yields one set.
3076
+ * a unique constraint yields one set, and a set that is unique both outright and under a
3077
+ * partial index keeps the outright one.
3078
+ *
3079
+ * A partial unique index carries its predicate as `where`: it is a conflict target only
3080
+ * together with that predicate, and it does not make the set unique across the whole table.
3040
3081
  */
3041
- const getUniqueColumnSets = (table, getTableConfig) => {
3082
+ const getUniqueTargets = (table, getTableConfig) => {
3042
3083
  const cols = visibleColumns(table);
3043
3084
  const propNameByColumnName = new Map(Object.entries(cols).map(([propName, col]) => [col.name, propName]));
3044
3085
  const toPropNames = (columnNames) => {
@@ -3050,25 +3091,40 @@ const getUniqueColumnSets = (table, getTableConfig) => {
3050
3091
  }
3051
3092
  return propNames.length ? propNames : void 0;
3052
3093
  };
3094
+ const whole = (columns) => columns ? { columns } : void 0;
3053
3095
  const config = getTableConfig(table);
3054
3096
  const candidates = [
3055
- Object.entries(cols).filter(([, col]) => col.primary).map(([propName]) => propName),
3056
- ...config.primaryKeys.map((pk) => toPropNames(pk.columns.map((c) => c.name))),
3057
- ...(config.uniqueConstraints ?? []).map((uc) => toPropNames(uc.columns.map((c) => c.name))),
3058
- ...(config.indexes ?? []).filter((index) => index.config.unique).map((index) => toPropNames(index.config.columns.map((c) => c?.name))),
3059
- ...Object.entries(cols).filter(([, col]) => col.isUnique).map(([propName]) => [propName])
3097
+ whole(Object.entries(cols).filter(([, col]) => col.primary).map(([propName]) => propName)),
3098
+ ...config.primaryKeys.map((pk) => whole(toPropNames(pk.columns.map((c) => c.name)))),
3099
+ ...(config.uniqueConstraints ?? []).map((uc) => whole(toPropNames(uc.columns.map((c) => c.name)))),
3100
+ ...(config.indexes ?? []).filter((index) => index.config.unique).map((index) => {
3101
+ const columns = toPropNames(index.config.columns.map((c) => c?.name));
3102
+ return columns ? {
3103
+ columns,
3104
+ ...index.config.where ? { where: index.config.where } : {}
3105
+ } : void 0;
3106
+ }),
3107
+ ...Object.entries(cols).filter(([, col]) => col.isUnique).map(([propName]) => ({ columns: [propName] }))
3060
3108
  ];
3061
- const seen = /* @__PURE__ */ new Set();
3062
- const sets = [];
3063
- for (const set of candidates) {
3064
- if (!set?.length) continue;
3065
- const key = [...set].sort().join(",");
3066
- if (seen.has(key)) continue;
3067
- seen.add(key);
3068
- sets.push(set);
3109
+ const byKey = /* @__PURE__ */ new Map();
3110
+ for (const candidate of candidates) {
3111
+ if (!candidate?.columns.length) continue;
3112
+ const key = [...candidate.columns].sort().join(",");
3113
+ const seen = byKey.get(key);
3114
+ if (!seen) byKey.set(key, candidate);
3115
+ else if (seen.where && !candidate.where) byKey.set(key, { columns: seen.columns });
3069
3116
  }
3070
- return sets;
3117
+ return [...byKey.values()];
3071
3118
  };
3119
+ /**
3120
+ * The column sets that identify at most one row of `table` — {@link getUniqueTargets}
3121
+ * without the partial unique indexes, which only constrain the rows their predicate
3122
+ * matches (a soft-deleted duplicate is allowed), and so do not.
3123
+ *
3124
+ * Pass `{ partial: true }` to keep them, for callers that only need to know a set is
3125
+ * unique among the rows that matter.
3126
+ */
3127
+ const getUniqueColumnSets = (table, getTableConfig, options = {}) => getUniqueTargets(table, getTableConfig).filter((target) => options.partial || !target.where).map((target) => target.columns);
3072
3128
  //#endregion
3073
3129
  //#region src/util/builders/common/distinct.ts
3074
3130
  /**
@@ -4698,6 +4754,19 @@ const keysFromWhere = (where, primaryKey) => {
4698
4754
  return none;
4699
4755
  };
4700
4756
  /**
4757
+ * Whether an upsert's `onConflict` leaves the primary-key values in `values` naming exactly
4758
+ * the rows it writes. It does not when the conflict is detected on another unique key — the
4759
+ * row updated is whichever one holds that key, not the one the supplied primary key names —
4760
+ * nor when conflicting rows can be skipped: `action: NOTHING`, or a `where` guard.
4761
+ */
4762
+ const conflictsOnKeyAlone = (onConflict, primaryKey) => {
4763
+ if (!onConflict || typeof onConflict !== "object") return true;
4764
+ const { target, action, where } = onConflict;
4765
+ if (action === "NOTHING" || where !== void 0 && where !== null) return false;
4766
+ if (!Array.isArray(target) || !target.length) return true;
4767
+ return [...target].sort().join(",") === [...primaryKey].sort().join(",");
4768
+ };
4769
+ /**
4701
4770
  * The rows a generated field's arguments are about, as primary-key values.
4702
4771
  *
4703
4772
  * The row identity of a mutation lives in a different argument for every operation —
@@ -4733,7 +4802,7 @@ const identifyRows = (field, args) => {
4733
4802
  table,
4734
4803
  primaryKey,
4735
4804
  rows: rows.filter((row) => !!row),
4736
- complete: entries.length > 0 && rows.every(Boolean)
4805
+ complete: entries.length > 0 && rows.every(Boolean) && (meta.operation !== "upsert" || conflictsOnKeyAlone(args?.["onConflict"], primaryKey))
4737
4806
  };
4738
4807
  }
4739
4808
  if (targetArg === "updates") {
@@ -7210,7 +7279,7 @@ const createSchemaBuilder = (adapter) => {
7210
7279
  uniqueKeysOf: (tableName) => uniqueKeys[tableName]
7211
7280
  };
7212
7281
  adapter.preflight?.(db, options);
7213
- const nestedPlans = features.nestedWrites ? buildNestedWritePlans(tables, namedRelations, (target) => uniqueColumnSets(target), (target) => primaryKeyPropNames(target), extractRelationJoinColumns, (junction) => schemaEntries.find(([, value]) => value === junction)?.[0]) : void 0;
7282
+ const nestedPlans = features.nestedWrites ? buildNestedWritePlans(tables, namedRelations, (target) => getUniqueColumnSets(target, adapter.getTableConfig, { partial: true }), (target) => primaryKeyPropNames(target), extractRelationJoinColumns, (junction) => schemaEntries.find(([, value]) => value === junction)?.[0]) : void 0;
7214
7283
  if (nestedPlans) {
7215
7284
  const omitted = gateNestedWritePlans(nestedPlans, featureOf, (tableName) => excludedTables.has(tableName));
7216
7285
  if (omitted.length) {
@@ -7498,20 +7567,33 @@ const generateUpsert = (db, tableName, table, baseType, onConflictType, fieldNam
7498
7567
  }
7499
7568
  },
7500
7569
  run: async ({ executor, args, context, before, after }) => {
7501
- const input = applyContextValuesAll(single ? [remapFromGraphQLSingleInput(args.values, table)] : remapFromGraphQLArrayInput(args.values, table), policies?.contextValues?.(tableName), context);
7570
+ const contextColumns = policies?.contextValues?.(tableName);
7571
+ const input = applyContextValuesAll(single ? [remapFromGraphQLSingleInput(args.values, table)] : remapFromGraphQLArrayInput(args.values, table), contextColumns, context);
7502
7572
  if (!input.length) throw drizzleError("No values were provided!", { code: "DRIZZLE_NO_VALUES" });
7503
7573
  await before();
7504
- const plan = resolveConflictPlan({
7574
+ const batches = resolveConflictPlan({
7505
7575
  table,
7506
7576
  values: input,
7507
7577
  onConflict: args.onConflict,
7508
7578
  pkNames,
7509
7579
  uniqueSets: [],
7510
7580
  excludedRef: mysqlValuesColumnRef,
7511
- withTarget: false
7581
+ withTarget: false,
7582
+ serverOwned: Object.keys(contextColumns ?? {})
7583
+ });
7584
+ const requestedNothing = args.onConflict?.action === "NOTHING";
7585
+ const runBatch = async (target, { values, plan }) => {
7586
+ if (plan.action === "NOTHING" && !requestedNothing) {
7587
+ const columns = (0, drizzle_orm.getColumns)(table);
7588
+ const keyName = pkNames[0] ?? Object.keys(columns)[0];
7589
+ await target.insert(table).values(values).onDuplicateKeyUpdate({ set: { [keyName]: drizzle_orm.sql`${drizzle_orm.sql.identifier(columns[keyName].name)}` } });
7590
+ } else if (plan.action === "NOTHING") await target.insert(table).ignore().values(values);
7591
+ else await target.insert(table).values(values).onDuplicateKeyUpdate({ set: plan.set });
7592
+ };
7593
+ if (batches.length === 1) await runBatch(executor, batches[0]);
7594
+ else await executor.transaction(async (tx) => {
7595
+ for (const batch of batches) await runBatch(tx, batch);
7512
7596
  });
7513
- if (plan.action === "NOTHING") await executor.insert(table).ignore().values(input);
7514
- else await executor.insert(table).values(input).onDuplicateKeyUpdate({ set: plan.set });
7515
7597
  await after();
7516
7598
  return isSuccess;
7517
7599
  }
@@ -7957,7 +8039,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7957
8039
  });
7958
8040
  const returning = withHookReturning(nestedEntries ? nested.withJoinColumns(tableName, mergedOps(nestedEntries), { ...columns }, table) : columns);
7959
8041
  const runUpsert = async (target, values) => {
7960
- const plan = resolveConflictPlan({
8042
+ const batches = resolveConflictPlan({
7961
8043
  table,
7962
8044
  values,
7963
8045
  onConflict: args.onConflict,
@@ -7965,16 +8047,29 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7965
8047
  uniqueSets,
7966
8048
  excludedRef: excludedColumnRef,
7967
8049
  withTarget: true,
8050
+ serverOwned: Object.keys(contextColumns ?? {}),
7968
8051
  buildWhere: (where) => extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName))
7969
8052
  });
7970
- let query = target.insert(table).values(values).returning(returning);
7971
- const setWhere = withScope(scope, tableName, table, plan.setWhere);
7972
- query = plan.action === "NOTHING" ? query.onConflictDoNothing(plan.target ? { target: plan.target } : void 0) : query.onConflictDoUpdate({
7973
- target: plan.target,
7974
- set: plan.set,
7975
- setWhere
8053
+ const runBatch = async (executor, { values: rows, plan }) => {
8054
+ let query = executor.insert(table).values(rows).returning(returning);
8055
+ const setWhere = withScope(scope, tableName, table, plan.setWhere);
8056
+ query = plan.action === "NOTHING" ? query.onConflictDoNothing(plan.target ? {
8057
+ target: plan.target,
8058
+ where: plan.targetWhere
8059
+ } : void 0) : query.onConflictDoUpdate({
8060
+ target: plan.target,
8061
+ targetWhere: plan.targetWhere,
8062
+ set: plan.set,
8063
+ setWhere
8064
+ });
8065
+ return await query;
8066
+ };
8067
+ if (batches.length === 1) return runBatch(target, batches[0]);
8068
+ return target.transaction(async (tx) => {
8069
+ const rows = [];
8070
+ for (const batch of batches) rows.push(...await runBatch(tx, batch));
8071
+ return rows;
7976
8072
  });
7977
- return await query;
7978
8073
  };
7979
8074
  const written = nestedEntries ? await writeWithNestedOps({
7980
8075
  executor,
@@ -8154,7 +8249,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8154
8249
  */
8155
8250
  const createSchemaDataGenerator = (adapter) => {
8156
8251
  const primaryKeyPropNames = adapter.primaryKeyPropNames;
8157
- const uniqueColumnSets = (table) => getUniqueColumnSets(table, adapter.getTableConfig);
8252
+ const uniqueTargets = (table) => getUniqueTargets(table, adapter.getTableConfig);
8158
8253
  const { generateInsert, generateUpsert, generateUpdate, generateDelete } = buildWriteResolvers(primaryKeyPropNames);
8159
8254
  const { prepareBuild, forEachTable, finalizeBuild } = createSchemaBuilder(adapter);
8160
8255
  return (db, schema, relations, options) => {
@@ -8195,7 +8290,7 @@ const createSchemaDataGenerator = (adapter) => {
8195
8290
  single: true,
8196
8291
  conflictDoNothing
8197
8292
  }) : void 0;
8198
- const uniqueSets = tableFeatures.upsert ? uniqueColumnSets(schema[tableName]) : [];
8293
+ const uniqueSets = tableFeatures.upsert ? uniqueTargets(schema[tableName]) : [];
8199
8294
  const onConflictInput = tableFeatures.upsert ? generateOnConflictInput({
8200
8295
  table: schema[tableName],
8201
8296
  tableName,