@vantreeseba/drizzle-graphql 15.1.0 → 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/dist/index.cjs CHANGED
@@ -1616,13 +1616,20 @@ const nullableOps = {
1616
1616
  isNull: drizzle_orm.isNull,
1617
1617
  isNotNull: drizzle_orm.isNotNull
1618
1618
  };
1619
+ /** Operators whose operand is an on/off flag rather than a value: `false` adds no condition. */
1620
+ const FLAG_OPERATORS = /* @__PURE__ */ new Set([
1621
+ "isNull",
1622
+ "isNotNull",
1623
+ "isEmpty"
1624
+ ]);
1619
1625
  const extractFiltersColumn = (column, columnName, operators) => {
1620
1626
  const { OR, AND, NOT, insensitive, ...restOperators } = operators;
1621
1627
  const foldCase = insensitive === true;
1622
1628
  const entries = Object.entries(restOperators);
1623
1629
  const variants = [];
1624
1630
  for (const [operatorName, operatorValue] of entries) {
1625
- if (operatorValue === null || operatorValue === false) continue;
1631
+ if (operatorValue === null || operatorValue === void 0) continue;
1632
+ if (operatorValue === false && FLAG_OPERATORS.has(operatorName)) continue;
1626
1633
  if (operatorName in singleValueOps) {
1627
1634
  const singleValue = remapFromGraphQLCore(operatorValue, column, columnName);
1628
1635
  variants.push(foldCase ? lowerComparison(column, operatorName, singleValue) : singleValueOps[operatorName](column, singleValue));
@@ -2411,8 +2418,8 @@ const columnEnumCache = /* @__PURE__ */ new WeakMap();
2411
2418
  * input field the enum would have typed, rather than emitting an empty enum, which is
2412
2419
  * invalid GraphQL.
2413
2420
  */
2414
- const generateColumnEnum = (table, enumName, description, predicate = () => true) => {
2415
- const cacheKey = `${enumName}\u0000${excludedColumnsKey(table)}`;
2421
+ const generateColumnEnum = (table, enumName, description, predicate = () => true, variant = "") => {
2422
+ const cacheKey = `${enumName}\u0000${excludedColumnsKey(table)}\u0000${variant}`;
2416
2423
  let tableCache = columnEnumCache.get(table);
2417
2424
  const cached = tableCache?.get(cacheKey);
2418
2425
  if (cached) return cached;
@@ -2472,6 +2479,9 @@ const conflictActionEnumType = (typeName) => sharedType(typeName, {
2472
2479
  * `ON DUPLICATE KEY UPDATE` fires on any unique key and takes no predicate, so offering
2473
2480
  * either there would mean silently ignoring it.
2474
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
+ *
2475
2485
  * Returns `undefined` when the table has nothing to conflict on (`withTarget` dialects
2476
2486
  * only) — the caller then generates no upsert mutations for that table at all, rather than
2477
2487
  * an operation whose every call is a database error.
@@ -2479,12 +2489,13 @@ const conflictActionEnumType = (typeName) => sharedType(typeName, {
2479
2489
  const generateOnConflictInput = (params) => {
2480
2490
  const { table, tableName, typeName, uniqueSets, tableFilters, withTarget, cacheCtx } = params;
2481
2491
  const resolveName = cacheCtx.typeName;
2492
+ const serverOwned = Object.keys(cacheCtx.contextValuesOf?.(tableName) ?? {}).sort();
2482
2493
  const updateEnum = generateColumnEnum(table, resolveName({
2483
2494
  kind: "columnEnum",
2484
2495
  defaultName: `${typeName}UpdateColumn`,
2485
2496
  table: tableName,
2486
2497
  operation: "conflictUpdate"
2487
- }), `Columns of ${typeName} that an upsert can overwrite`);
2498
+ }), `Columns of ${typeName} that an upsert can overwrite`, (_column, columnName) => !serverOwned.includes(columnName), serverOwned.join(","));
2488
2499
  if (!updateEnum) return;
2489
2500
  const fields = {
2490
2501
  action: {
@@ -2494,11 +2505,11 @@ const generateOnConflictInput = (params) => {
2494
2505
  },
2495
2506
  update: {
2496
2507
  type: new graphql.GraphQLList(new graphql.GraphQLNonNull(updateEnum)),
2497
- 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.`
2498
2509
  }
2499
2510
  };
2500
2511
  if (withTarget) {
2501
- const uniqueColumns = new Set(uniqueSets.flat());
2512
+ const uniqueColumns = new Set(uniqueSets.flatMap((set) => set.columns));
2502
2513
  const targetEnum = generateColumnEnum(table, resolveName({
2503
2514
  kind: "columnEnum",
2504
2515
  defaultName: `${typeName}ConflictTarget`,
@@ -2526,55 +2537,88 @@ const generateOnConflictInput = (params) => {
2526
2537
  });
2527
2538
  };
2528
2539
  /**
2529
- * Turns the request's `onConflict` argument and the rows it is inserting into the clause a
2530
- * 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.
2531
2549
  *
2532
2550
  * `excludedRef` names the row that failed to insert in the dialect's own terms
2533
2551
  * (`excluded.col` on PostgreSQL and SQLite, `values(col)` on MySQL), which is what makes a
2534
2552
  * batch upsert update each row with its own values instead of the last row's.
2535
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
+ *
2536
2559
  * An UPDATE with nothing left to write degrades to NOTHING: `DO UPDATE SET` with an empty
2537
2560
  * body is not valid SQL, and doing nothing is what the request asked for anyway.
2538
2561
  */
2539
2562
  const resolveConflictPlan = (params) => {
2540
2563
  const { table, values, onConflict, pkNames, uniqueSets, excludedRef, withTarget, buildWhere } = params;
2541
2564
  const columns = (0, drizzle_orm.getColumns)(table);
2565
+ const serverOwned = new Set(params.serverOwned ?? []);
2542
2566
  let target;
2567
+ let targetWhere;
2543
2568
  if (withTarget) {
2544
2569
  const targetNames = onConflict?.target?.length ? onConflict.target : [...pkNames];
2545
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" });
2546
2571
  const requested = [...targetNames].sort().join(",");
2547
- 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" });
2548
2574
  target = targetNames.map((name) => columns[name]);
2575
+ targetWhere = matched.where;
2549
2576
  }
2550
- if ((onConflict?.action ?? "UPDATE") === "NOTHING") return {
2577
+ const nothing = {
2551
2578
  action: "NOTHING",
2552
2579
  target,
2580
+ targetWhere,
2553
2581
  set: {},
2554
2582
  setWhere: void 0
2555
2583
  };
2584
+ if ((onConflict?.action ?? "UPDATE") === "NOTHING") return [{
2585
+ values,
2586
+ plan: nothing
2587
+ }];
2556
2588
  const supplied = new Set(values.flatMap((row) => Object.keys(row)));
2557
- const targetNames = new Set(withTarget ? onConflict?.target?.length ? onConflict.target : pkNames : []);
2558
- let updateNames;
2589
+ const neverWritten = new Set(withTarget && onConflict?.target?.length ? onConflict.target : pkNames);
2590
+ let candidates;
2559
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" });
2560
2594
  const unsupplied = onConflict.update.filter((name) => !supplied.has(name));
2561
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" });
2562
- updateNames = onConflict.update;
2563
- } else updateNames = [...supplied].filter((name) => !targetNames.has(name));
2564
- if (!updateNames.length) return {
2565
- action: "NOTHING",
2566
- target,
2567
- set: {},
2568
- setWhere: void 0
2569
- };
2570
- 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));
2571
2598
  const setWhere = onConflict?.where && buildWhere ? buildWhere(onConflict.where) : void 0;
2572
- return {
2599
+ const planFor = (updateNames) => updateNames.length ? {
2573
2600
  action: "UPDATE",
2574
2601
  target,
2575
- set,
2602
+ targetWhere,
2603
+ set: Object.fromEntries(updateNames.map((name) => [name, excludedRef(columns[name].name)])),
2576
2604
  setWhere
2577
- };
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
+ }));
2578
2622
  };
2579
2623
  /** `excluded.<column>` — PostgreSQL and SQLite name the rejected row this way. */
2580
2624
  const excludedColumnRef = (columnName) => drizzle_orm.sql`excluded.${drizzle_orm.sql.identifier(columnName)}`;
@@ -2756,6 +2800,45 @@ const cursorOrderExprs = (table, entries) => {
2756
2800
  return entries.flatMap(([column, direction, nulls]) => orderExpressions(cols[column], direction, nulls ?? void 0, cols[column]));
2757
2801
  };
2758
2802
  /**
2803
+ * Property a row carries an ordering column's full-precision text under, when the column holds
2804
+ * more than a JS Date can. Namespaced like {@link ROW_CURSOR_PROP}, and removed from the row
2805
+ * once its cursor is encoded.
2806
+ */
2807
+ const preciseTextProp = (column) => `__drizzle_graphql_cursor_text_${column}`;
2808
+ /**
2809
+ * Whether a column reads into a JS Date (milliseconds) while the database keeps finer
2810
+ * fractional seconds: a PostgreSQL `timestamp` / `timestamptz` in `mode: 'date'` (precision 6
2811
+ * unless it says otherwise) or a MySQL `datetime` / `timestamp` in `mode: 'date'` with
2812
+ * `fsp` above 3. String-mode columns already arrive as the database's own text, and `time` is
2813
+ * always a string, so neither loses anything.
2814
+ */
2815
+ const outgrowsJsDate = (column) => {
2816
+ if ((0, drizzle_orm.is)(column, drizzle_orm_pg_core.PgTimestamp)) return column.precision === void 0 || column.precision > 3;
2817
+ if ((0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlDateTime) || (0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlTimestamp)) return (column.fsp ?? 0) > 3;
2818
+ return false;
2819
+ };
2820
+ /**
2821
+ * Extra SQL expressions a cursor-computing query selects alongside the row: for each ordering
2822
+ * column whose values a JS Date would truncate, the value as the database's own text. The
2823
+ * cursor then encodes that text rather than the Date, and `after` compares the column against
2824
+ * it — so two rows a microsecond apart stay distinct positions. `table` may be the aliased RQB
2825
+ * proxy. Empty when no ordering column needs it.
2826
+ */
2827
+ const cursorPreciseExtras = (table, entries) => {
2828
+ const cols = (0, drizzle_orm.getColumns)(table);
2829
+ const extras = {};
2830
+ for (const [name] of entries) {
2831
+ const column = cols[name];
2832
+ if (column && outgrowsJsDate(column)) extras[preciseTextProp(name)] = (0, drizzle_orm.is)(column, drizzle_orm_pg_core.PgTimestamp) ? drizzle_orm.sql`${column}::text` : drizzle_orm.sql`cast(${column} as char)`;
2833
+ }
2834
+ return extras;
2835
+ };
2836
+ /**
2837
+ * {@link cursorPreciseExtras} aliased under their property names, for a `select({...})` that
2838
+ * may be wrapped in a subquery (where a bare SQL expression would have no name).
2839
+ */
2840
+ const cursorPreciseSelection = (table, entries) => Object.fromEntries(Object.entries(cursorPreciseExtras(table, entries)).map(([prop, expression]) => [prop, expression.as(prop)]));
2841
+ /**
2759
2842
  * Serializes one ordering-tuple value into a JSON-safe shape. Dates and bigints don't survive
2760
2843
  * JSON.stringify losslessly (bigint throws, Date turns into an untagged string), so they are
2761
2844
  * tagged; decimals already arrive from the driver as strings, which are lossless as-is.
@@ -2790,7 +2873,11 @@ const cursorEncoder = (entries) => {
2790
2873
  const spec = JSON.stringify(entries);
2791
2874
  const columns = entries.map(([column]) => column);
2792
2875
  return (row) => {
2793
- const values = JSON.stringify(columns.map((column) => encodeCursorValue(row[column] ?? null)));
2876
+ const values = JSON.stringify(columns.map((column) => {
2877
+ const value = row[column] ?? null;
2878
+ const precise = row[preciseTextProp(column)];
2879
+ return value !== null && typeof precise === "string" ? precise : encodeCursorValue(value);
2880
+ }));
2794
2881
  return Buffer.from(`{"o":${spec},"v":${values}}`, "utf8").toString("base64url");
2795
2882
  };
2796
2883
  };
@@ -2883,7 +2970,11 @@ const selectsCursorField = (info, table) => {
2883
2970
  */
2884
2971
  const attachRowCursors = (rows, entries) => {
2885
2972
  const encode = cursorEncoder(entries);
2886
- for (const row of rows) row[ROW_CURSOR_PROP] = encode(row);
2973
+ const preciseProps = entries.map(([column]) => preciseTextProp(column));
2974
+ for (const row of rows) {
2975
+ row[ROW_CURSOR_PROP] = encode(row);
2976
+ for (const prop of preciseProps) delete row[prop];
2977
+ }
2887
2978
  };
2888
2979
  /** Resolver for the `cursor` meta field: reads the value a list resolver attached, if any. */
2889
2980
  const rowCursorResolver = (source) => source?.[ROW_CURSOR_PROP] ?? null;
@@ -2971,9 +3062,9 @@ const primaryKeyOrderExprs = (table, pkNames) => {
2971
3062
  return pkNames.map((n) => cols[n]).filter(Boolean).map((col) => (0, drizzle_orm.asc)(col));
2972
3063
  };
2973
3064
  /**
2974
- * Every set of columns that uniquely identifies a row of `table`: the primary key first,
2975
- * then each unique constraint and unique index, then each column declared `.unique()`
2976
- * 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.
2977
3068
  *
2978
3069
  * `getTableConfig` is the dialect's own — the three dialects expose the same
2979
3070
  * `{ primaryKeys, uniqueConstraints, indexes }` shape but from different modules, so the
@@ -2982,9 +3073,13 @@ const primaryKeyOrderExprs = (table, pkNames) => {
2982
3073
  * Index entries whose columns are SQL expressions rather than plain columns are skipped:
2983
3074
  * an expression index is a valid conflict target in the database but cannot be named by a
2984
3075
  * column enum. Deduplicated, order-insensitive — a column that is both the primary key and
2985
- * 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.
2986
3081
  */
2987
- const getUniqueColumnSets = (table, getTableConfig) => {
3082
+ const getUniqueTargets = (table, getTableConfig) => {
2988
3083
  const cols = visibleColumns(table);
2989
3084
  const propNameByColumnName = new Map(Object.entries(cols).map(([propName, col]) => [col.name, propName]));
2990
3085
  const toPropNames = (columnNames) => {
@@ -2996,25 +3091,40 @@ const getUniqueColumnSets = (table, getTableConfig) => {
2996
3091
  }
2997
3092
  return propNames.length ? propNames : void 0;
2998
3093
  };
3094
+ const whole = (columns) => columns ? { columns } : void 0;
2999
3095
  const config = getTableConfig(table);
3000
3096
  const candidates = [
3001
- Object.entries(cols).filter(([, col]) => col.primary).map(([propName]) => propName),
3002
- ...config.primaryKeys.map((pk) => toPropNames(pk.columns.map((c) => c.name))),
3003
- ...(config.uniqueConstraints ?? []).map((uc) => toPropNames(uc.columns.map((c) => c.name))),
3004
- ...(config.indexes ?? []).filter((index) => index.config.unique).map((index) => toPropNames(index.config.columns.map((c) => c?.name))),
3005
- ...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] }))
3006
3108
  ];
3007
- const seen = /* @__PURE__ */ new Set();
3008
- const sets = [];
3009
- for (const set of candidates) {
3010
- if (!set?.length) continue;
3011
- const key = [...set].sort().join(",");
3012
- if (seen.has(key)) continue;
3013
- seen.add(key);
3014
- 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 });
3015
3116
  }
3016
- return sets;
3117
+ return [...byKey.values()];
3017
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);
3018
3128
  //#endregion
3019
3129
  //#region src/util/builders/common/distinct.ts
3020
3130
  /**
@@ -3241,6 +3351,21 @@ const withDefaultOrderBy = (args, tableName, defaults) => {
3241
3351
  } : args;
3242
3352
  };
3243
3353
  /**
3354
+ * Rejects a negative `limit` or `offset`. Neither is a page size or a position, and the
3355
+ * dialects disagree on what one means: SQLite reads `LIMIT -1` as "no limit" — which would walk
3356
+ * straight past `maxLimit` — while PostgreSQL and MySQL fail with a raw database error. Applied
3357
+ * whether or not the table has a limit policy.
3358
+ */
3359
+ const assertPagingNotNegative = (paging, errorCtx) => {
3360
+ for (const name of ["limit", "offset"]) {
3361
+ const value = paging[name];
3362
+ if (value != null && value < 0) throw drizzleError(`'${name}' must not be negative, got ${value}.`, {
3363
+ code: "DRIZZLE_INVALID_INPUT_VALUE",
3364
+ ...errorCtx
3365
+ });
3366
+ }
3367
+ };
3368
+ /**
3244
3369
  * The `limit` a query actually runs with, given the policy for the table it reads.
3245
3370
  *
3246
3371
  * A request that passes no `limit` falls back to `defaultLimit`, or to `maxLimit` when only
@@ -3252,6 +3377,7 @@ const withDefaultOrderBy = (args, tableName, defaults) => {
3252
3377
  * fail a client's query over.
3253
3378
  */
3254
3379
  const applyLimitPolicy = (limit, policy, errorCtx) => {
3380
+ assertPagingNotNegative({ limit }, errorCtx);
3255
3381
  if (!policy) return limit ?? void 0;
3256
3382
  const { defaultLimit, maxLimit, clampToMax } = policy;
3257
3383
  if (limit == null) {
@@ -3769,6 +3895,11 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
3769
3895
  thisRecord.columns = columns;
3770
3896
  const relationArgs = (0, drizzle_orm.is)(relEntry.relation, drizzle_orm.One) ? field.args : withDefaultOrderBy(field.args ?? {}, targetTableName, defaultOrderBy);
3771
3897
  const offset = relationArgs?.offset ?? void 0;
3898
+ assertPagingNotNegative({ offset }, {
3899
+ table: targetTableName,
3900
+ operation: "relation",
3901
+ relation: relName
3902
+ });
3772
3903
  const limit = (0, drizzle_orm.is)(relEntry.relation, drizzle_orm.One) ? relationArgs?.limit ?? void 0 : applyLimitPolicy(relationArgs?.limit, limits?.(targetTableName), {
3773
3904
  table: targetTableName,
3774
3905
  operation: "relation",
@@ -3812,6 +3943,10 @@ const pruneNonEagerRelations = (relationMap, shouldEagerLoad) => {
3812
3943
  const runRelationalSelect = async (opts) => {
3813
3944
  const { queryBase, tables, tableName, table, relationMap, typeName, typeNameMapper, parsedInfo, offset, orderBy, where, single, filterCtx, pkNames, after, scope, deleted } = opts;
3814
3945
  const distinct = opts.distinct?.length ? opts.distinct : void 0;
3946
+ assertPagingNotNegative({
3947
+ limit: opts.limit,
3948
+ offset
3949
+ });
3815
3950
  const cursorSelected = !single && isCursorFieldSelected(parsedInfo.fieldsByTypeName[typeName], table);
3816
3951
  let cursorEntries;
3817
3952
  if (!single && (after != null || cursorSelected)) {
@@ -3863,7 +3998,11 @@ const runRelationalSelect = async (opts) => {
3863
3998
  const result = await queryBase.findFirst(omitUndefinedKeys(params));
3864
3999
  return result ? remapToGraphQLSingleOutput(result, tableName, table, relationMap) : void 0;
3865
4000
  }
3866
- if (cursorEntries && cursorSelected) for (const [column] of cursorEntries) params.columns[column] = true;
4001
+ if (cursorEntries && cursorSelected) {
4002
+ for (const [column] of cursorEntries) params.columns[column] = true;
4003
+ const preciseProps = Object.keys(cursorPreciseExtras(table, cursorEntries));
4004
+ if (preciseProps.length) params.extras = Object.fromEntries(preciseProps.map((prop) => [prop, (aliasedTable) => cursorPreciseExtras(aliasedTable, cursorEntries)[prop]]));
4005
+ }
3867
4006
  params.limit = distinctKeys ? void 0 : opts.limit;
3868
4007
  const result = await queryBase.findMany(omitUndefinedKeys(params));
3869
4008
  if (cursorEntries && cursorSelected) attachRowCursors(result, cursorEntries);
@@ -4615,6 +4754,19 @@ const keysFromWhere = (where, primaryKey) => {
4615
4754
  return none;
4616
4755
  };
4617
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
+ /**
4618
4770
  * The rows a generated field's arguments are about, as primary-key values.
4619
4771
  *
4620
4772
  * The row identity of a mutation lives in a different argument for every operation —
@@ -4650,7 +4802,7 @@ const identifyRows = (field, args) => {
4650
4802
  table,
4651
4803
  primaryKey,
4652
4804
  rows: rows.filter((row) => !!row),
4653
- complete: entries.length > 0 && rows.every(Boolean)
4805
+ complete: entries.length > 0 && rows.every(Boolean) && (meta.operation !== "upsert" || conflictsOnKeyAlone(args?.["onConflict"], primaryKey))
4654
4806
  };
4655
4807
  }
4656
4808
  if (targetArg === "updates") {
@@ -4767,7 +4919,10 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
4767
4919
  const column = targetColumns[columnName];
4768
4920
  if (column) columns[columnName] = column;
4769
4921
  }
4770
- return columns;
4922
+ return cursorEntries ? {
4923
+ ...columns,
4924
+ ...cursorPreciseSelection(targetTable, cursorEntries)
4925
+ } : columns;
4771
4926
  };
4772
4927
  const resolve = async (parent, args, context, info) => {
4773
4928
  if (parent[relationName] !== void 0) return parent[relationName];
@@ -4777,6 +4932,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
4777
4932
  const { orderByArg, limit, distinct, cursorEntries, cursorValues, columns, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes?.[0], `relation:${tableName}::${relationName}`, () => {
4778
4933
  const orderByArg = isOne ? args?.orderBy : withDefaultOrderBy(args ?? {}, targetTableName, policies?.defaultOrderBy).orderBy;
4779
4934
  const limit = applyLimitPolicy(requestedLimit, limitPolicy, errorCtx);
4935
+ assertPagingNotNegative({ offset }, errorCtx);
4780
4936
  const distinct = (args ?? {}).distinct?.length ? (args ?? {}).distinct : void 0;
4781
4937
  const cursorSelected = !isOne && !!info && selectsCursorField(info, targetTable);
4782
4938
  let cursorEntries;
@@ -7123,7 +7279,7 @@ const createSchemaBuilder = (adapter) => {
7123
7279
  uniqueKeysOf: (tableName) => uniqueKeys[tableName]
7124
7280
  };
7125
7281
  adapter.preflight?.(db, options);
7126
- 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;
7127
7283
  if (nestedPlans) {
7128
7284
  const omitted = gateNestedWritePlans(nestedPlans, featureOf, (tableName) => excludedTables.has(tableName));
7129
7285
  if (omitted.length) {
@@ -7411,20 +7567,33 @@ const generateUpsert = (db, tableName, table, baseType, onConflictType, fieldNam
7411
7567
  }
7412
7568
  },
7413
7569
  run: async ({ executor, args, context, before, after }) => {
7414
- 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);
7415
7572
  if (!input.length) throw drizzleError("No values were provided!", { code: "DRIZZLE_NO_VALUES" });
7416
7573
  await before();
7417
- const plan = resolveConflictPlan({
7574
+ const batches = resolveConflictPlan({
7418
7575
  table,
7419
7576
  values: input,
7420
7577
  onConflict: args.onConflict,
7421
7578
  pkNames,
7422
7579
  uniqueSets: [],
7423
7580
  excludedRef: mysqlValuesColumnRef,
7424
- 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);
7425
7596
  });
7426
- if (plan.action === "NOTHING") await executor.insert(table).ignore().values(input);
7427
- else await executor.insert(table).values(input).onDuplicateKeyUpdate({ set: plan.set });
7428
7597
  await after();
7429
7598
  return isSuccess;
7430
7599
  }
@@ -7870,7 +8039,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7870
8039
  });
7871
8040
  const returning = withHookReturning(nestedEntries ? nested.withJoinColumns(tableName, mergedOps(nestedEntries), { ...columns }, table) : columns);
7872
8041
  const runUpsert = async (target, values) => {
7873
- const plan = resolveConflictPlan({
8042
+ const batches = resolveConflictPlan({
7874
8043
  table,
7875
8044
  values,
7876
8045
  onConflict: args.onConflict,
@@ -7878,16 +8047,29 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7878
8047
  uniqueSets,
7879
8048
  excludedRef: excludedColumnRef,
7880
8049
  withTarget: true,
8050
+ serverOwned: Object.keys(contextColumns ?? {}),
7881
8051
  buildWhere: (where) => extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName))
7882
8052
  });
7883
- let query = target.insert(table).values(values).returning(returning);
7884
- const setWhere = withScope(scope, tableName, table, plan.setWhere);
7885
- query = plan.action === "NOTHING" ? query.onConflictDoNothing(plan.target ? { target: plan.target } : void 0) : query.onConflictDoUpdate({
7886
- target: plan.target,
7887
- set: plan.set,
7888
- 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;
7889
8072
  });
7890
- return await query;
7891
8073
  };
7892
8074
  const written = nestedEntries ? await writeWithNestedOps({
7893
8075
  executor,
@@ -8067,7 +8249,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8067
8249
  */
8068
8250
  const createSchemaDataGenerator = (adapter) => {
8069
8251
  const primaryKeyPropNames = adapter.primaryKeyPropNames;
8070
- const uniqueColumnSets = (table) => getUniqueColumnSets(table, adapter.getTableConfig);
8252
+ const uniqueTargets = (table) => getUniqueTargets(table, adapter.getTableConfig);
8071
8253
  const { generateInsert, generateUpsert, generateUpdate, generateDelete } = buildWriteResolvers(primaryKeyPropNames);
8072
8254
  const { prepareBuild, forEachTable, finalizeBuild } = createSchemaBuilder(adapter);
8073
8255
  return (db, schema, relations, options) => {
@@ -8108,7 +8290,7 @@ const createSchemaDataGenerator = (adapter) => {
8108
8290
  single: true,
8109
8291
  conflictDoNothing
8110
8292
  }) : void 0;
8111
- const uniqueSets = tableFeatures.upsert ? uniqueColumnSets(schema[tableName]) : [];
8293
+ const uniqueSets = tableFeatures.upsert ? uniqueTargets(schema[tableName]) : [];
8112
8294
  const onConflictInput = tableFeatures.upsert ? generateOnConflictInput({
8113
8295
  table: schema[tableName],
8114
8296
  tableName,