@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.d.cts CHANGED
@@ -385,7 +385,9 @@ type IdentifiedRows = {
385
385
  *
386
386
  * `false` means the arguments do not pin the rows down — an insert whose key the
387
387
  * database generates, a `where` that filters on something other than the primary key,
388
- * a plural update with no filter at all, or a table with no primary key. `rows` is then
388
+ * a plural update with no filter at all, a table with no primary key, or an upsert whose
389
+ * `onConflict` conflicts on another unique key (the row updated is the one holding that
390
+ * key) or can skip a conflicting row (`action: NOTHING`, a `where` guard). `rows` is then
389
391
  * a best-effort subset-free superset: every entry in it is a row the key values name,
390
392
  * but other filters may narrow the real set further, and it may be empty.
391
393
  */
@@ -449,7 +451,7 @@ type DrizzleErrorCode =
449
451
  'DRIZZLE_INVALID_DISTINCT' |
450
452
  /** An `onConflict` named a target or update column the upsert cannot use. */
451
453
  'DRIZZLE_INVALID_ON_CONFLICT' |
452
- /** A written value could not be converted to what its column stores. */
454
+ /** A written value could not be converted to what its column stores, or a `limit` / `offset` was negative. */
453
455
  'DRIZZLE_INVALID_INPUT_VALUE' |
454
456
  /** An update's `set` gave a column that cannot hold null an explicit null. */
455
457
  'DRIZZLE_NOT_NULL' |
@@ -1178,6 +1180,9 @@ type UpdateManyArgs<TTable extends Table> = {
1178
1180
  * `target` and `where` exist on PostgreSQL and SQLite only: MySQL's
1179
1181
  * `ON DUPLICATE KEY UPDATE` fires on whichever unique key was violated and takes no
1180
1182
  * predicate, so neither field is generated there.
1183
+ *
1184
+ * `target` may name a partial unique index; its predicate is repeated in the statement's
1185
+ * `ON CONFLICT (…) WHERE …`, which is what lets the database match it.
1181
1186
  */
1182
1187
  type UpsertConflictArgs<TTable extends Table> = {
1183
1188
  action?: 'UPDATE' | 'NOTHING';
@@ -1604,7 +1609,9 @@ type SchemaFeatures = {
1604
1609
  * have — the guarantee is in the input type.
1605
1610
  *
1606
1611
  * Single-column constraints get no field (`eq` on the column already says it), and a
1607
- * constraint whose field name is taken by a column or a relation is skipped. The fields live
1612
+ * constraint whose field name is taken by a column or a relation is skipped. So is a partial
1613
+ * unique index (`uniqueIndex().on(…).where(…)`): it is unique only among the rows its
1614
+ * predicate matches, so its columns do not name one row of the table. The fields live
1608
1615
  * on `<Type>Filters`, so they are accepted wherever a filter is: the `Single` reads and
1609
1616
  * writes they are meant for, but also list queries, relation filters and `onConflict.where`.
1610
1617
  *
@@ -2091,7 +2098,8 @@ type BuildSchemaConfig = {
2091
2098
  * - The column is removed from the generated insert and update inputs, so a client cannot
2092
2099
  * send it at all — the schema itself documents that it is server-owned.
2093
2100
  * - Every insert stamps it, including the rows created by a nested `create`.
2094
- * - Updates never write it: an update cannot hand a row to another owner.
2101
+ * - Updates never write it: an update cannot hand a row to another owner. That includes
2102
+ * the conflict branch of an upsert, whose `onConflict.update` does not offer it.
2095
2103
  * - Keyed by the Drizzle schema key, then by the column's property name (not its database
2096
2104
  * name), the same keying {@link BuildSchemaConfig.exclude} uses.
2097
2105
  */
package/dist/index.d.ts CHANGED
@@ -385,7 +385,9 @@ type IdentifiedRows = {
385
385
  *
386
386
  * `false` means the arguments do not pin the rows down — an insert whose key the
387
387
  * database generates, a `where` that filters on something other than the primary key,
388
- * a plural update with no filter at all, or a table with no primary key. `rows` is then
388
+ * a plural update with no filter at all, a table with no primary key, or an upsert whose
389
+ * `onConflict` conflicts on another unique key (the row updated is the one holding that
390
+ * key) or can skip a conflicting row (`action: NOTHING`, a `where` guard). `rows` is then
389
391
  * a best-effort subset-free superset: every entry in it is a row the key values name,
390
392
  * but other filters may narrow the real set further, and it may be empty.
391
393
  */
@@ -449,7 +451,7 @@ type DrizzleErrorCode =
449
451
  'DRIZZLE_INVALID_DISTINCT' |
450
452
  /** An `onConflict` named a target or update column the upsert cannot use. */
451
453
  'DRIZZLE_INVALID_ON_CONFLICT' |
452
- /** A written value could not be converted to what its column stores. */
454
+ /** A written value could not be converted to what its column stores, or a `limit` / `offset` was negative. */
453
455
  'DRIZZLE_INVALID_INPUT_VALUE' |
454
456
  /** An update's `set` gave a column that cannot hold null an explicit null. */
455
457
  'DRIZZLE_NOT_NULL' |
@@ -1178,6 +1180,9 @@ type UpdateManyArgs<TTable extends Table> = {
1178
1180
  * `target` and `where` exist on PostgreSQL and SQLite only: MySQL's
1179
1181
  * `ON DUPLICATE KEY UPDATE` fires on whichever unique key was violated and takes no
1180
1182
  * predicate, so neither field is generated there.
1183
+ *
1184
+ * `target` may name a partial unique index; its predicate is repeated in the statement's
1185
+ * `ON CONFLICT (…) WHERE …`, which is what lets the database match it.
1181
1186
  */
1182
1187
  type UpsertConflictArgs<TTable extends Table> = {
1183
1188
  action?: 'UPDATE' | 'NOTHING';
@@ -1604,7 +1609,9 @@ type SchemaFeatures = {
1604
1609
  * have — the guarantee is in the input type.
1605
1610
  *
1606
1611
  * Single-column constraints get no field (`eq` on the column already says it), and a
1607
- * constraint whose field name is taken by a column or a relation is skipped. The fields live
1612
+ * constraint whose field name is taken by a column or a relation is skipped. So is a partial
1613
+ * unique index (`uniqueIndex().on(…).where(…)`): it is unique only among the rows its
1614
+ * predicate matches, so its columns do not name one row of the table. The fields live
1608
1615
  * on `<Type>Filters`, so they are accepted wherever a filter is: the `Single` reads and
1609
1616
  * writes they are meant for, but also list queries, relation filters and `onConflict.where`.
1610
1617
  *
@@ -2091,7 +2098,8 @@ type BuildSchemaConfig = {
2091
2098
  * - The column is removed from the generated insert and update inputs, so a client cannot
2092
2099
  * send it at all — the schema itself documents that it is server-owned.
2093
2100
  * - Every insert stamps it, including the rows created by a nested `create`.
2094
- * - Updates never write it: an update cannot hand a row to another owner.
2101
+ * - Updates never write it: an update cannot hand a row to another owner. That includes
2102
+ * the conflict branch of an upsert, whose `onConflict.update` does not offer it.
2095
2103
  * - Keyed by the Drizzle schema key, then by the column's property name (not its database
2096
2104
  * name), the same keying {@link BuildSchemaConfig.exclude} uses.
2097
2105
  */
package/dist/index.js CHANGED
@@ -1592,13 +1592,20 @@ const nullableOps = {
1592
1592
  isNull,
1593
1593
  isNotNull
1594
1594
  };
1595
+ /** Operators whose operand is an on/off flag rather than a value: `false` adds no condition. */
1596
+ const FLAG_OPERATORS = /* @__PURE__ */ new Set([
1597
+ "isNull",
1598
+ "isNotNull",
1599
+ "isEmpty"
1600
+ ]);
1595
1601
  const extractFiltersColumn = (column, columnName, operators) => {
1596
1602
  const { OR, AND, NOT, insensitive, ...restOperators } = operators;
1597
1603
  const foldCase = insensitive === true;
1598
1604
  const entries = Object.entries(restOperators);
1599
1605
  const variants = [];
1600
1606
  for (const [operatorName, operatorValue] of entries) {
1601
- if (operatorValue === null || operatorValue === false) continue;
1607
+ if (operatorValue === null || operatorValue === void 0) continue;
1608
+ if (operatorValue === false && FLAG_OPERATORS.has(operatorName)) continue;
1602
1609
  if (operatorName in singleValueOps) {
1603
1610
  const singleValue = remapFromGraphQLCore(operatorValue, column, columnName);
1604
1611
  variants.push(foldCase ? lowerComparison(column, operatorName, singleValue) : singleValueOps[operatorName](column, singleValue));
@@ -2387,8 +2394,8 @@ const columnEnumCache = /* @__PURE__ */ new WeakMap();
2387
2394
  * input field the enum would have typed, rather than emitting an empty enum, which is
2388
2395
  * invalid GraphQL.
2389
2396
  */
2390
- const generateColumnEnum = (table, enumName, description, predicate = () => true) => {
2391
- const cacheKey = `${enumName}\u0000${excludedColumnsKey(table)}`;
2397
+ const generateColumnEnum = (table, enumName, description, predicate = () => true, variant = "") => {
2398
+ const cacheKey = `${enumName}\u0000${excludedColumnsKey(table)}\u0000${variant}`;
2392
2399
  let tableCache = columnEnumCache.get(table);
2393
2400
  const cached = tableCache?.get(cacheKey);
2394
2401
  if (cached) return cached;
@@ -2448,6 +2455,9 @@ const conflictActionEnumType = (typeName) => sharedType(typeName, {
2448
2455
  * `ON DUPLICATE KEY UPDATE` fires on any unique key and takes no predicate, so offering
2449
2456
  * either there would mean silently ignoring it.
2450
2457
  *
2458
+ * `update` never offers a `contextValues` column: the server owns it, and the conflict
2459
+ * branch is an update, which must not hand the row to another owner.
2460
+ *
2451
2461
  * Returns `undefined` when the table has nothing to conflict on (`withTarget` dialects
2452
2462
  * only) — the caller then generates no upsert mutations for that table at all, rather than
2453
2463
  * an operation whose every call is a database error.
@@ -2455,12 +2465,13 @@ const conflictActionEnumType = (typeName) => sharedType(typeName, {
2455
2465
  const generateOnConflictInput = (params) => {
2456
2466
  const { table, tableName, typeName, uniqueSets, tableFilters, withTarget, cacheCtx } = params;
2457
2467
  const resolveName = cacheCtx.typeName;
2468
+ const serverOwned = Object.keys(cacheCtx.contextValuesOf?.(tableName) ?? {}).sort();
2458
2469
  const updateEnum = generateColumnEnum(table, resolveName({
2459
2470
  kind: "columnEnum",
2460
2471
  defaultName: `${typeName}UpdateColumn`,
2461
2472
  table: tableName,
2462
2473
  operation: "conflictUpdate"
2463
- }), `Columns of ${typeName} that an upsert can overwrite`);
2474
+ }), `Columns of ${typeName} that an upsert can overwrite`, (_column, columnName) => !serverOwned.includes(columnName), serverOwned.join(","));
2464
2475
  if (!updateEnum) return;
2465
2476
  const fields = {
2466
2477
  action: {
@@ -2470,11 +2481,11 @@ const generateOnConflictInput = (params) => {
2470
2481
  },
2471
2482
  update: {
2472
2483
  type: new GraphQLList(new GraphQLNonNull(updateEnum)),
2473
- 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."
2484
+ 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.`
2474
2485
  }
2475
2486
  };
2476
2487
  if (withTarget) {
2477
- const uniqueColumns = new Set(uniqueSets.flat());
2488
+ const uniqueColumns = new Set(uniqueSets.flatMap((set) => set.columns));
2478
2489
  const targetEnum = generateColumnEnum(table, resolveName({
2479
2490
  kind: "columnEnum",
2480
2491
  defaultName: `${typeName}ConflictTarget`,
@@ -2502,55 +2513,88 @@ const generateOnConflictInput = (params) => {
2502
2513
  });
2503
2514
  };
2504
2515
  /**
2505
- * Turns the request's `onConflict` argument and the rows it is inserting into the clause a
2506
- * dialect should attach.
2516
+ * Turns the request's `onConflict` argument and the rows it is inserting into the
2517
+ * statements a dialect should run — one per run of consecutive rows that write the same
2518
+ * columns on conflict.
2519
+ *
2520
+ * The split is what keeps a partial row partial. A multi-row insert fills a key one row
2521
+ * left out with DEFAULT, and `excluded.<column>` would carry that default onto the existing
2522
+ * row, so rows that supply different columns cannot share a `SET` list. Splitting into
2523
+ * *consecutive* runs, rather than grouping, keeps the rows a caller gets back in the order
2524
+ * it sent them. A batch whose rows agree — the usual case — is still one statement.
2507
2525
  *
2508
2526
  * `excludedRef` names the row that failed to insert in the dialect's own terms
2509
2527
  * (`excluded.col` on PostgreSQL and SQLite, `values(col)` on MySQL), which is what makes a
2510
2528
  * batch upsert update each row with its own values instead of the last row's.
2511
2529
  *
2530
+ * Never written on conflict, even when supplied: the conflict target (it already matches);
2531
+ * the primary key on a dialect with no target (MySQL's conflict can be on another unique
2532
+ * key, and copying a generated id over the existing row's would move it); and `serverOwned`
2533
+ * — the `contextValues` columns, which an update never reassigns.
2534
+ *
2512
2535
  * An UPDATE with nothing left to write degrades to NOTHING: `DO UPDATE SET` with an empty
2513
2536
  * body is not valid SQL, and doing nothing is what the request asked for anyway.
2514
2537
  */
2515
2538
  const resolveConflictPlan = (params) => {
2516
2539
  const { table, values, onConflict, pkNames, uniqueSets, excludedRef, withTarget, buildWhere } = params;
2517
2540
  const columns = getColumns(table);
2541
+ const serverOwned = new Set(params.serverOwned ?? []);
2518
2542
  let target;
2543
+ let targetWhere;
2519
2544
  if (withTarget) {
2520
2545
  const targetNames = onConflict?.target?.length ? onConflict.target : [...pkNames];
2521
2546
  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" });
2522
2547
  const requested = [...targetNames].sort().join(",");
2523
- 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" });
2548
+ const matched = uniqueSets.find((set) => [...set.columns].sort().join(",") === requested);
2549
+ 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" });
2524
2550
  target = targetNames.map((name) => columns[name]);
2551
+ targetWhere = matched.where;
2525
2552
  }
2526
- if ((onConflict?.action ?? "UPDATE") === "NOTHING") return {
2553
+ const nothing = {
2527
2554
  action: "NOTHING",
2528
2555
  target,
2556
+ targetWhere,
2529
2557
  set: {},
2530
2558
  setWhere: void 0
2531
2559
  };
2560
+ if ((onConflict?.action ?? "UPDATE") === "NOTHING") return [{
2561
+ values,
2562
+ plan: nothing
2563
+ }];
2532
2564
  const supplied = new Set(values.flatMap((row) => Object.keys(row)));
2533
- const targetNames = new Set(withTarget ? onConflict?.target?.length ? onConflict.target : pkNames : []);
2534
- let updateNames;
2565
+ const neverWritten = new Set(withTarget && onConflict?.target?.length ? onConflict.target : pkNames);
2566
+ let candidates;
2535
2567
  if (onConflict?.update?.length) {
2568
+ const owned = onConflict.update.filter((name) => serverOwned.has(name));
2569
+ 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" });
2536
2570
  const unsupplied = onConflict.update.filter((name) => !supplied.has(name));
2537
2571
  if (unsupplied.length) throw drizzleError(`Unable to upsert: onConflict.update lists ${unsupplied.join(", ")}, which the values do not supply.`, { code: "DRIZZLE_INVALID_ON_CONFLICT" });
2538
- updateNames = onConflict.update;
2539
- } else updateNames = [...supplied].filter((name) => !targetNames.has(name));
2540
- if (!updateNames.length) return {
2541
- action: "NOTHING",
2542
- target,
2543
- set: {},
2544
- setWhere: void 0
2545
- };
2546
- const set = Object.fromEntries(updateNames.map((name) => [name, excludedRef(columns[name].name)]));
2572
+ candidates = onConflict.update;
2573
+ } else candidates = [...supplied].filter((name) => !neverWritten.has(name) && !serverOwned.has(name));
2547
2574
  const setWhere = onConflict?.where && buildWhere ? buildWhere(onConflict.where) : void 0;
2548
- return {
2575
+ const planFor = (updateNames) => updateNames.length ? {
2549
2576
  action: "UPDATE",
2550
2577
  target,
2551
- set,
2578
+ targetWhere,
2579
+ set: Object.fromEntries(updateNames.map((name) => [name, excludedRef(columns[name].name)])),
2552
2580
  setWhere
2553
- };
2581
+ } : nothing;
2582
+ const batches = [];
2583
+ for (const row of values) {
2584
+ const updateNames = candidates.filter((name) => name in row);
2585
+ const key = updateNames.join(",");
2586
+ const last = batches[batches.length - 1];
2587
+ if (last?.key === key) last.values.push(row);
2588
+ else batches.push({
2589
+ key,
2590
+ values: [row],
2591
+ plan: planFor(updateNames)
2592
+ });
2593
+ }
2594
+ return batches.map(({ values: rows, plan }) => ({
2595
+ values: rows,
2596
+ plan
2597
+ }));
2554
2598
  };
2555
2599
  /** `excluded.<column>` — PostgreSQL and SQLite name the rejected row this way. */
2556
2600
  const excludedColumnRef = (columnName) => sql`excluded.${sql.identifier(columnName)}`;
@@ -2732,6 +2776,45 @@ const cursorOrderExprs = (table, entries) => {
2732
2776
  return entries.flatMap(([column, direction, nulls]) => orderExpressions(cols[column], direction, nulls ?? void 0, cols[column]));
2733
2777
  };
2734
2778
  /**
2779
+ * Property a row carries an ordering column's full-precision text under, when the column holds
2780
+ * more than a JS Date can. Namespaced like {@link ROW_CURSOR_PROP}, and removed from the row
2781
+ * once its cursor is encoded.
2782
+ */
2783
+ const preciseTextProp = (column) => `__drizzle_graphql_cursor_text_${column}`;
2784
+ /**
2785
+ * Whether a column reads into a JS Date (milliseconds) while the database keeps finer
2786
+ * fractional seconds: a PostgreSQL `timestamp` / `timestamptz` in `mode: 'date'` (precision 6
2787
+ * unless it says otherwise) or a MySQL `datetime` / `timestamp` in `mode: 'date'` with
2788
+ * `fsp` above 3. String-mode columns already arrive as the database's own text, and `time` is
2789
+ * always a string, so neither loses anything.
2790
+ */
2791
+ const outgrowsJsDate = (column) => {
2792
+ if (is(column, PgTimestamp)) return column.precision === void 0 || column.precision > 3;
2793
+ if (is(column, MySqlDateTime) || is(column, MySqlTimestamp)) return (column.fsp ?? 0) > 3;
2794
+ return false;
2795
+ };
2796
+ /**
2797
+ * Extra SQL expressions a cursor-computing query selects alongside the row: for each ordering
2798
+ * column whose values a JS Date would truncate, the value as the database's own text. The
2799
+ * cursor then encodes that text rather than the Date, and `after` compares the column against
2800
+ * it — so two rows a microsecond apart stay distinct positions. `table` may be the aliased RQB
2801
+ * proxy. Empty when no ordering column needs it.
2802
+ */
2803
+ const cursorPreciseExtras = (table, entries) => {
2804
+ const cols = getColumns(table);
2805
+ const extras = {};
2806
+ for (const [name] of entries) {
2807
+ const column = cols[name];
2808
+ if (column && outgrowsJsDate(column)) extras[preciseTextProp(name)] = is(column, PgTimestamp) ? sql`${column}::text` : sql`cast(${column} as char)`;
2809
+ }
2810
+ return extras;
2811
+ };
2812
+ /**
2813
+ * {@link cursorPreciseExtras} aliased under their property names, for a `select({...})` that
2814
+ * may be wrapped in a subquery (where a bare SQL expression would have no name).
2815
+ */
2816
+ const cursorPreciseSelection = (table, entries) => Object.fromEntries(Object.entries(cursorPreciseExtras(table, entries)).map(([prop, expression]) => [prop, expression.as(prop)]));
2817
+ /**
2735
2818
  * Serializes one ordering-tuple value into a JSON-safe shape. Dates and bigints don't survive
2736
2819
  * JSON.stringify losslessly (bigint throws, Date turns into an untagged string), so they are
2737
2820
  * tagged; decimals already arrive from the driver as strings, which are lossless as-is.
@@ -2766,7 +2849,11 @@ const cursorEncoder = (entries) => {
2766
2849
  const spec = JSON.stringify(entries);
2767
2850
  const columns = entries.map(([column]) => column);
2768
2851
  return (row) => {
2769
- const values = JSON.stringify(columns.map((column) => encodeCursorValue(row[column] ?? null)));
2852
+ const values = JSON.stringify(columns.map((column) => {
2853
+ const value = row[column] ?? null;
2854
+ const precise = row[preciseTextProp(column)];
2855
+ return value !== null && typeof precise === "string" ? precise : encodeCursorValue(value);
2856
+ }));
2770
2857
  return Buffer.from(`{"o":${spec},"v":${values}}`, "utf8").toString("base64url");
2771
2858
  };
2772
2859
  };
@@ -2859,7 +2946,11 @@ const selectsCursorField = (info, table) => {
2859
2946
  */
2860
2947
  const attachRowCursors = (rows, entries) => {
2861
2948
  const encode = cursorEncoder(entries);
2862
- for (const row of rows) row[ROW_CURSOR_PROP] = encode(row);
2949
+ const preciseProps = entries.map(([column]) => preciseTextProp(column));
2950
+ for (const row of rows) {
2951
+ row[ROW_CURSOR_PROP] = encode(row);
2952
+ for (const prop of preciseProps) delete row[prop];
2953
+ }
2863
2954
  };
2864
2955
  /** Resolver for the `cursor` meta field: reads the value a list resolver attached, if any. */
2865
2956
  const rowCursorResolver = (source) => source?.[ROW_CURSOR_PROP] ?? null;
@@ -2947,9 +3038,9 @@ const primaryKeyOrderExprs = (table, pkNames) => {
2947
3038
  return pkNames.map((n) => cols[n]).filter(Boolean).map((col) => asc(col));
2948
3039
  };
2949
3040
  /**
2950
- * Every set of columns that uniquely identifies a row of `table`: the primary key first,
2951
- * then each unique constraint and unique index, then each column declared `.unique()`
2952
- * inline. Sets are property names (what GraphQL inputs use), not database column names.
3041
+ * Every set of columns that is unique on `table`: the primary key first, then each unique
3042
+ * constraint and unique index, then each column declared `.unique()` inline. Sets are
3043
+ * property names (what GraphQL inputs use), not database column names.
2953
3044
  *
2954
3045
  * `getTableConfig` is the dialect's own — the three dialects expose the same
2955
3046
  * `{ primaryKeys, uniqueConstraints, indexes }` shape but from different modules, so the
@@ -2958,9 +3049,13 @@ const primaryKeyOrderExprs = (table, pkNames) => {
2958
3049
  * Index entries whose columns are SQL expressions rather than plain columns are skipped:
2959
3050
  * an expression index is a valid conflict target in the database but cannot be named by a
2960
3051
  * column enum. Deduplicated, order-insensitive — a column that is both the primary key and
2961
- * a unique constraint yields one set.
3052
+ * a unique constraint yields one set, and a set that is unique both outright and under a
3053
+ * partial index keeps the outright one.
3054
+ *
3055
+ * A partial unique index carries its predicate as `where`: it is a conflict target only
3056
+ * together with that predicate, and it does not make the set unique across the whole table.
2962
3057
  */
2963
- const getUniqueColumnSets = (table, getTableConfig) => {
3058
+ const getUniqueTargets = (table, getTableConfig) => {
2964
3059
  const cols = visibleColumns(table);
2965
3060
  const propNameByColumnName = new Map(Object.entries(cols).map(([propName, col]) => [col.name, propName]));
2966
3061
  const toPropNames = (columnNames) => {
@@ -2972,25 +3067,40 @@ const getUniqueColumnSets = (table, getTableConfig) => {
2972
3067
  }
2973
3068
  return propNames.length ? propNames : void 0;
2974
3069
  };
3070
+ const whole = (columns) => columns ? { columns } : void 0;
2975
3071
  const config = getTableConfig(table);
2976
3072
  const candidates = [
2977
- Object.entries(cols).filter(([, col]) => col.primary).map(([propName]) => propName),
2978
- ...config.primaryKeys.map((pk) => toPropNames(pk.columns.map((c) => c.name))),
2979
- ...(config.uniqueConstraints ?? []).map((uc) => toPropNames(uc.columns.map((c) => c.name))),
2980
- ...(config.indexes ?? []).filter((index) => index.config.unique).map((index) => toPropNames(index.config.columns.map((c) => c?.name))),
2981
- ...Object.entries(cols).filter(([, col]) => col.isUnique).map(([propName]) => [propName])
3073
+ whole(Object.entries(cols).filter(([, col]) => col.primary).map(([propName]) => propName)),
3074
+ ...config.primaryKeys.map((pk) => whole(toPropNames(pk.columns.map((c) => c.name)))),
3075
+ ...(config.uniqueConstraints ?? []).map((uc) => whole(toPropNames(uc.columns.map((c) => c.name)))),
3076
+ ...(config.indexes ?? []).filter((index) => index.config.unique).map((index) => {
3077
+ const columns = toPropNames(index.config.columns.map((c) => c?.name));
3078
+ return columns ? {
3079
+ columns,
3080
+ ...index.config.where ? { where: index.config.where } : {}
3081
+ } : void 0;
3082
+ }),
3083
+ ...Object.entries(cols).filter(([, col]) => col.isUnique).map(([propName]) => ({ columns: [propName] }))
2982
3084
  ];
2983
- const seen = /* @__PURE__ */ new Set();
2984
- const sets = [];
2985
- for (const set of candidates) {
2986
- if (!set?.length) continue;
2987
- const key = [...set].sort().join(",");
2988
- if (seen.has(key)) continue;
2989
- seen.add(key);
2990
- sets.push(set);
3085
+ const byKey = /* @__PURE__ */ new Map();
3086
+ for (const candidate of candidates) {
3087
+ if (!candidate?.columns.length) continue;
3088
+ const key = [...candidate.columns].sort().join(",");
3089
+ const seen = byKey.get(key);
3090
+ if (!seen) byKey.set(key, candidate);
3091
+ else if (seen.where && !candidate.where) byKey.set(key, { columns: seen.columns });
2991
3092
  }
2992
- return sets;
3093
+ return [...byKey.values()];
2993
3094
  };
3095
+ /**
3096
+ * The column sets that identify at most one row of `table` — {@link getUniqueTargets}
3097
+ * without the partial unique indexes, which only constrain the rows their predicate
3098
+ * matches (a soft-deleted duplicate is allowed), and so do not.
3099
+ *
3100
+ * Pass `{ partial: true }` to keep them, for callers that only need to know a set is
3101
+ * unique among the rows that matter.
3102
+ */
3103
+ const getUniqueColumnSets = (table, getTableConfig, options = {}) => getUniqueTargets(table, getTableConfig).filter((target) => options.partial || !target.where).map((target) => target.columns);
2994
3104
  //#endregion
2995
3105
  //#region src/util/builders/common/distinct.ts
2996
3106
  /**
@@ -3217,6 +3327,21 @@ const withDefaultOrderBy = (args, tableName, defaults) => {
3217
3327
  } : args;
3218
3328
  };
3219
3329
  /**
3330
+ * Rejects a negative `limit` or `offset`. Neither is a page size or a position, and the
3331
+ * dialects disagree on what one means: SQLite reads `LIMIT -1` as "no limit" — which would walk
3332
+ * straight past `maxLimit` — while PostgreSQL and MySQL fail with a raw database error. Applied
3333
+ * whether or not the table has a limit policy.
3334
+ */
3335
+ const assertPagingNotNegative = (paging, errorCtx) => {
3336
+ for (const name of ["limit", "offset"]) {
3337
+ const value = paging[name];
3338
+ if (value != null && value < 0) throw drizzleError(`'${name}' must not be negative, got ${value}.`, {
3339
+ code: "DRIZZLE_INVALID_INPUT_VALUE",
3340
+ ...errorCtx
3341
+ });
3342
+ }
3343
+ };
3344
+ /**
3220
3345
  * The `limit` a query actually runs with, given the policy for the table it reads.
3221
3346
  *
3222
3347
  * A request that passes no `limit` falls back to `defaultLimit`, or to `maxLimit` when only
@@ -3228,6 +3353,7 @@ const withDefaultOrderBy = (args, tableName, defaults) => {
3228
3353
  * fail a client's query over.
3229
3354
  */
3230
3355
  const applyLimitPolicy = (limit, policy, errorCtx) => {
3356
+ assertPagingNotNegative({ limit }, errorCtx);
3231
3357
  if (!policy) return limit ?? void 0;
3232
3358
  const { defaultLimit, maxLimit, clampToMax } = policy;
3233
3359
  if (limit == null) {
@@ -3745,6 +3871,11 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
3745
3871
  thisRecord.columns = columns;
3746
3872
  const relationArgs = is(relEntry.relation, One) ? field.args : withDefaultOrderBy(field.args ?? {}, targetTableName, defaultOrderBy);
3747
3873
  const offset = relationArgs?.offset ?? void 0;
3874
+ assertPagingNotNegative({ offset }, {
3875
+ table: targetTableName,
3876
+ operation: "relation",
3877
+ relation: relName
3878
+ });
3748
3879
  const limit = is(relEntry.relation, One) ? relationArgs?.limit ?? void 0 : applyLimitPolicy(relationArgs?.limit, limits?.(targetTableName), {
3749
3880
  table: targetTableName,
3750
3881
  operation: "relation",
@@ -3788,6 +3919,10 @@ const pruneNonEagerRelations = (relationMap, shouldEagerLoad) => {
3788
3919
  const runRelationalSelect = async (opts) => {
3789
3920
  const { queryBase, tables, tableName, table, relationMap, typeName, typeNameMapper, parsedInfo, offset, orderBy, where, single, filterCtx, pkNames, after, scope, deleted } = opts;
3790
3921
  const distinct = opts.distinct?.length ? opts.distinct : void 0;
3922
+ assertPagingNotNegative({
3923
+ limit: opts.limit,
3924
+ offset
3925
+ });
3791
3926
  const cursorSelected = !single && isCursorFieldSelected(parsedInfo.fieldsByTypeName[typeName], table);
3792
3927
  let cursorEntries;
3793
3928
  if (!single && (after != null || cursorSelected)) {
@@ -3839,7 +3974,11 @@ const runRelationalSelect = async (opts) => {
3839
3974
  const result = await queryBase.findFirst(omitUndefinedKeys(params));
3840
3975
  return result ? remapToGraphQLSingleOutput(result, tableName, table, relationMap) : void 0;
3841
3976
  }
3842
- if (cursorEntries && cursorSelected) for (const [column] of cursorEntries) params.columns[column] = true;
3977
+ if (cursorEntries && cursorSelected) {
3978
+ for (const [column] of cursorEntries) params.columns[column] = true;
3979
+ const preciseProps = Object.keys(cursorPreciseExtras(table, cursorEntries));
3980
+ if (preciseProps.length) params.extras = Object.fromEntries(preciseProps.map((prop) => [prop, (aliasedTable) => cursorPreciseExtras(aliasedTable, cursorEntries)[prop]]));
3981
+ }
3843
3982
  params.limit = distinctKeys ? void 0 : opts.limit;
3844
3983
  const result = await queryBase.findMany(omitUndefinedKeys(params));
3845
3984
  if (cursorEntries && cursorSelected) attachRowCursors(result, cursorEntries);
@@ -4591,6 +4730,19 @@ const keysFromWhere = (where, primaryKey) => {
4591
4730
  return none;
4592
4731
  };
4593
4732
  /**
4733
+ * Whether an upsert's `onConflict` leaves the primary-key values in `values` naming exactly
4734
+ * the rows it writes. It does not when the conflict is detected on another unique key — the
4735
+ * row updated is whichever one holds that key, not the one the supplied primary key names —
4736
+ * nor when conflicting rows can be skipped: `action: NOTHING`, or a `where` guard.
4737
+ */
4738
+ const conflictsOnKeyAlone = (onConflict, primaryKey) => {
4739
+ if (!onConflict || typeof onConflict !== "object") return true;
4740
+ const { target, action, where } = onConflict;
4741
+ if (action === "NOTHING" || where !== void 0 && where !== null) return false;
4742
+ if (!Array.isArray(target) || !target.length) return true;
4743
+ return [...target].sort().join(",") === [...primaryKey].sort().join(",");
4744
+ };
4745
+ /**
4594
4746
  * The rows a generated field's arguments are about, as primary-key values.
4595
4747
  *
4596
4748
  * The row identity of a mutation lives in a different argument for every operation —
@@ -4626,7 +4778,7 @@ const identifyRows = (field, args) => {
4626
4778
  table,
4627
4779
  primaryKey,
4628
4780
  rows: rows.filter((row) => !!row),
4629
- complete: entries.length > 0 && rows.every(Boolean)
4781
+ complete: entries.length > 0 && rows.every(Boolean) && (meta.operation !== "upsert" || conflictsOnKeyAlone(args?.["onConflict"], primaryKey))
4630
4782
  };
4631
4783
  }
4632
4784
  if (targetArg === "updates") {
@@ -4743,7 +4895,10 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
4743
4895
  const column = targetColumns[columnName];
4744
4896
  if (column) columns[columnName] = column;
4745
4897
  }
4746
- return columns;
4898
+ return cursorEntries ? {
4899
+ ...columns,
4900
+ ...cursorPreciseSelection(targetTable, cursorEntries)
4901
+ } : columns;
4747
4902
  };
4748
4903
  const resolve = async (parent, args, context, info) => {
4749
4904
  if (parent[relationName] !== void 0) return parent[relationName];
@@ -4753,6 +4908,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
4753
4908
  const { orderByArg, limit, distinct, cursorEntries, cursorValues, columns, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes?.[0], `relation:${tableName}::${relationName}`, () => {
4754
4909
  const orderByArg = isOne ? args?.orderBy : withDefaultOrderBy(args ?? {}, targetTableName, policies?.defaultOrderBy).orderBy;
4755
4910
  const limit = applyLimitPolicy(requestedLimit, limitPolicy, errorCtx);
4911
+ assertPagingNotNegative({ offset }, errorCtx);
4756
4912
  const distinct = (args ?? {}).distinct?.length ? (args ?? {}).distinct : void 0;
4757
4913
  const cursorSelected = !isOne && !!info && selectsCursorField(info, targetTable);
4758
4914
  let cursorEntries;
@@ -7099,7 +7255,7 @@ const createSchemaBuilder = (adapter) => {
7099
7255
  uniqueKeysOf: (tableName) => uniqueKeys[tableName]
7100
7256
  };
7101
7257
  adapter.preflight?.(db, options);
7102
- const nestedPlans = features.nestedWrites ? buildNestedWritePlans(tables, namedRelations, (target) => uniqueColumnSets(target), (target) => primaryKeyPropNames(target), extractRelationJoinColumns, (junction) => schemaEntries.find(([, value]) => value === junction)?.[0]) : void 0;
7258
+ 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;
7103
7259
  if (nestedPlans) {
7104
7260
  const omitted = gateNestedWritePlans(nestedPlans, featureOf, (tableName) => excludedTables.has(tableName));
7105
7261
  if (omitted.length) {
@@ -7387,20 +7543,33 @@ const generateUpsert = (db, tableName, table, baseType, onConflictType, fieldNam
7387
7543
  }
7388
7544
  },
7389
7545
  run: async ({ executor, args, context, before, after }) => {
7390
- const input = applyContextValuesAll(single ? [remapFromGraphQLSingleInput(args.values, table)] : remapFromGraphQLArrayInput(args.values, table), policies?.contextValues?.(tableName), context);
7546
+ const contextColumns = policies?.contextValues?.(tableName);
7547
+ const input = applyContextValuesAll(single ? [remapFromGraphQLSingleInput(args.values, table)] : remapFromGraphQLArrayInput(args.values, table), contextColumns, context);
7391
7548
  if (!input.length) throw drizzleError("No values were provided!", { code: "DRIZZLE_NO_VALUES" });
7392
7549
  await before();
7393
- const plan = resolveConflictPlan({
7550
+ const batches = resolveConflictPlan({
7394
7551
  table,
7395
7552
  values: input,
7396
7553
  onConflict: args.onConflict,
7397
7554
  pkNames,
7398
7555
  uniqueSets: [],
7399
7556
  excludedRef: mysqlValuesColumnRef,
7400
- withTarget: false
7557
+ withTarget: false,
7558
+ serverOwned: Object.keys(contextColumns ?? {})
7559
+ });
7560
+ const requestedNothing = args.onConflict?.action === "NOTHING";
7561
+ const runBatch = async (target, { values, plan }) => {
7562
+ if (plan.action === "NOTHING" && !requestedNothing) {
7563
+ const columns = getColumns(table);
7564
+ const keyName = pkNames[0] ?? Object.keys(columns)[0];
7565
+ await target.insert(table).values(values).onDuplicateKeyUpdate({ set: { [keyName]: sql`${sql.identifier(columns[keyName].name)}` } });
7566
+ } else if (plan.action === "NOTHING") await target.insert(table).ignore().values(values);
7567
+ else await target.insert(table).values(values).onDuplicateKeyUpdate({ set: plan.set });
7568
+ };
7569
+ if (batches.length === 1) await runBatch(executor, batches[0]);
7570
+ else await executor.transaction(async (tx) => {
7571
+ for (const batch of batches) await runBatch(tx, batch);
7401
7572
  });
7402
- if (plan.action === "NOTHING") await executor.insert(table).ignore().values(input);
7403
- else await executor.insert(table).values(input).onDuplicateKeyUpdate({ set: plan.set });
7404
7573
  await after();
7405
7574
  return isSuccess;
7406
7575
  }
@@ -7846,7 +8015,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7846
8015
  });
7847
8016
  const returning = withHookReturning(nestedEntries ? nested.withJoinColumns(tableName, mergedOps(nestedEntries), { ...columns }, table) : columns);
7848
8017
  const runUpsert = async (target, values) => {
7849
- const plan = resolveConflictPlan({
8018
+ const batches = resolveConflictPlan({
7850
8019
  table,
7851
8020
  values,
7852
8021
  onConflict: args.onConflict,
@@ -7854,16 +8023,29 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7854
8023
  uniqueSets,
7855
8024
  excludedRef: excludedColumnRef,
7856
8025
  withTarget: true,
8026
+ serverOwned: Object.keys(contextColumns ?? {}),
7857
8027
  buildWhere: (where) => extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName))
7858
8028
  });
7859
- let query = target.insert(table).values(values).returning(returning);
7860
- const setWhere = withScope(scope, tableName, table, plan.setWhere);
7861
- query = plan.action === "NOTHING" ? query.onConflictDoNothing(plan.target ? { target: plan.target } : void 0) : query.onConflictDoUpdate({
7862
- target: plan.target,
7863
- set: plan.set,
7864
- setWhere
8029
+ const runBatch = async (executor, { values: rows, plan }) => {
8030
+ let query = executor.insert(table).values(rows).returning(returning);
8031
+ const setWhere = withScope(scope, tableName, table, plan.setWhere);
8032
+ query = plan.action === "NOTHING" ? query.onConflictDoNothing(plan.target ? {
8033
+ target: plan.target,
8034
+ where: plan.targetWhere
8035
+ } : void 0) : query.onConflictDoUpdate({
8036
+ target: plan.target,
8037
+ targetWhere: plan.targetWhere,
8038
+ set: plan.set,
8039
+ setWhere
8040
+ });
8041
+ return await query;
8042
+ };
8043
+ if (batches.length === 1) return runBatch(target, batches[0]);
8044
+ return target.transaction(async (tx) => {
8045
+ const rows = [];
8046
+ for (const batch of batches) rows.push(...await runBatch(tx, batch));
8047
+ return rows;
7865
8048
  });
7866
- return await query;
7867
8049
  };
7868
8050
  const written = nestedEntries ? await writeWithNestedOps({
7869
8051
  executor,
@@ -8043,7 +8225,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8043
8225
  */
8044
8226
  const createSchemaDataGenerator = (adapter) => {
8045
8227
  const primaryKeyPropNames = adapter.primaryKeyPropNames;
8046
- const uniqueColumnSets = (table) => getUniqueColumnSets(table, adapter.getTableConfig);
8228
+ const uniqueTargets = (table) => getUniqueTargets(table, adapter.getTableConfig);
8047
8229
  const { generateInsert, generateUpsert, generateUpdate, generateDelete } = buildWriteResolvers(primaryKeyPropNames);
8048
8230
  const { prepareBuild, forEachTable, finalizeBuild } = createSchemaBuilder(adapter);
8049
8231
  return (db, schema, relations, options) => {
@@ -8084,7 +8266,7 @@ const createSchemaDataGenerator = (adapter) => {
8084
8266
  single: true,
8085
8267
  conflictDoNothing
8086
8268
  }) : void 0;
8087
- const uniqueSets = tableFeatures.upsert ? uniqueColumnSets(schema[tableName]) : [];
8269
+ const uniqueSets = tableFeatures.upsert ? uniqueTargets(schema[tableName]) : [];
8088
8270
  const onConflictInput = tableFeatures.upsert ? generateOnConflictInput({
8089
8271
  table: schema[tableName],
8090
8272
  tableName,