@vantreeseba/drizzle-graphql 15.1.1 → 15.1.3

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.js CHANGED
@@ -2,7 +2,7 @@ import { Column, One, SQL, Table, aliasedTable, and, arrayContains, arrayOverlap
2
2
  import { MySqlAsyncDatabase, MySqlDate, MySqlDateString, MySqlDateTime, MySqlDateTimeString, MySqlInt, MySqlSerial, MySqlTable, MySqlTimestamp, MySqlTimestampString, getTableConfig } from "drizzle-orm/mysql-core";
3
3
  import { PgAsyncDatabase, PgDate, PgDateString, PgInteger, PgSerial, PgTable, PgTimestamp, PgTimestampString, PgUUID, getTableConfig as getTableConfig$1 } from "drizzle-orm/pg-core";
4
4
  import { SQLiteAsyncDatabase, SQLiteInteger, SQLiteTable, SQLiteTimestamp, getTableConfig as getTableConfig$2 } from "drizzle-orm/sqlite-core";
5
- import { GraphQLBoolean, GraphQLEnumType, GraphQLError, GraphQLFloat, GraphQLInputObjectType, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLSchema, GraphQLString, Kind, getArgumentValues } from "graphql";
5
+ import { GraphQLBoolean, GraphQLEnumType, GraphQLError, GraphQLFloat, GraphQLInputObjectType, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLSchema, GraphQLString, Kind, getArgumentValues, getNamedType } from "graphql";
6
6
  import { GraphQLDateTime, GraphQLJSON, GraphQLUUID } from "graphql-scalars";
7
7
  import pluralize from "pluralize";
8
8
  //#region src/util/builders/common/errors.ts
@@ -1983,6 +1983,13 @@ const extractUniqueKeyFilter = (table, tableName, fieldName, members, value) =>
1983
1983
  */
1984
1984
  const narrowedCtxCache = /* @__PURE__ */ new WeakMap();
1985
1985
  /**
1986
+ * One alias counter per build, shared by every table's narrowed context. A statement can
1987
+ * combine subqueries compiled from different contexts — a relation filter on one table whose
1988
+ * target's scope is a filter object reaching through another relation, say — and nested
1989
+ * subqueries that drew the same alias from two separate counters would shadow each other.
1990
+ */
1991
+ const aliasCounters = /* @__PURE__ */ new WeakMap();
1992
+ /**
1986
1993
  * Narrows the build-scoped relation filter context to the table a resolver is filtering.
1987
1994
  *
1988
1995
  * Cached rather than rebuilt: the base carries the build's whole `tables` and `relationMap`,
@@ -1996,7 +2003,7 @@ const narrowedCtxCache = /* @__PURE__ */ new WeakMap();
1996
2003
  * aliases only ever have to be unique *within* one statement, which a number that never
1997
2004
  * repeats gives more surely than one that restarts.
1998
2005
  */
1999
- const relationFilterCtx = (base, tableKey) => {
2006
+ const relationFilterCtx = (base, tableKey, scope) => {
2000
2007
  if (!base) return;
2001
2008
  let byTable = narrowedCtxCache.get(base);
2002
2009
  if (!byTable) {
@@ -2005,13 +2012,33 @@ const relationFilterCtx = (base, tableKey) => {
2005
2012
  }
2006
2013
  let ctx = byTable.get(tableKey);
2007
2014
  if (!ctx) {
2015
+ let aliases = aliasCounters.get(base);
2016
+ if (!aliases) {
2017
+ aliases = { n: 0 };
2018
+ aliasCounters.set(base, aliases);
2019
+ }
2008
2020
  ctx = {
2009
2021
  ...base,
2010
- tableKey
2022
+ tableKey,
2023
+ aliases
2011
2024
  };
2012
2025
  byTable.set(tableKey, ctx);
2013
2026
  }
2014
- return ctx;
2027
+ return scope ? {
2028
+ ...ctx,
2029
+ scope
2030
+ } : ctx;
2031
+ };
2032
+ /**
2033
+ * The row-policy predicate a subquery over a relation's target applies: the target's scope
2034
+ * and soft-delete condition, in the `deleted` mode the relation field defaults to — so a
2035
+ * relation filter or a relation ordering sees exactly the rows the relation field returns.
2036
+ */
2037
+ const relationTargetPolicy = (ctx, relation, targetTableName, aliasedTarget) => {
2038
+ const scope = ctx.scope;
2039
+ if (!scope) return;
2040
+ const mode = scope.relationDefault(targetTableName, is(relation, One) && relation.optional === false);
2041
+ return scope.has(targetTableName, mode) ? scope.on(targetTableName, aliasedTarget, mode) : void 0;
2015
2042
  };
2016
2043
  /**
2017
2044
  * Correlates the parent row with the aliased target table using the relation's own join
@@ -2102,8 +2129,11 @@ const buildRelationExists = (parentTable, relationName, relEntry, innerFilters,
2102
2129
  const throughTable = relation.throughTable;
2103
2130
  let fromClause;
2104
2131
  let joinCondition;
2132
+ let junctionPolicy;
2105
2133
  if (throughTable) {
2106
2134
  const aliasedThrough = aliasedTable(throughTable, `dgql_rel_${aliases.n++}`);
2135
+ const throughTableName = ctx.scope ? Object.keys(ctx.tables).find((name) => ctx.tables[name] === throughTable) : void 0;
2136
+ if (throughTableName && ctx.scope.has(throughTableName)) junctionPolicy = ctx.scope.on(throughTableName, aliasedThrough);
2107
2137
  const { correlation, junctionJoin } = buildThroughJoinConditions(parentTable, relation, aliasedThrough, aliasedTarget, relationName);
2108
2138
  fromClause = sql`${getTableAsAliasSQL(aliasedTarget)} inner join ${getTableAsAliasSQL(aliasedThrough)} on ${junctionJoin}`;
2109
2139
  joinCondition = correlation;
@@ -2113,6 +2143,7 @@ const buildRelationExists = (parentTable, relationName, relEntry, innerFilters,
2113
2143
  }
2114
2144
  const ownWhere = relationOwnWhere(relation);
2115
2145
  const relationWhere = ownWhere ? relationsFilterToSQL(isRelationFilterReversed(relation) ? parentTable : aliasedTarget, ownWhere) : void 0;
2146
+ const policy = and(relationTargetPolicy(ctx, relation, targetTableName, aliasedTarget), junctionPolicy);
2116
2147
  const inner = innerFilters ? extractFilters(aliasedTarget, targetTableName, innerFilters, {
2117
2148
  ...ctx,
2118
2149
  tableKey: targetTableName,
@@ -2120,9 +2151,9 @@ const buildRelationExists = (parentTable, relationName, relEntry, innerFilters,
2120
2151
  }) : void 0;
2121
2152
  if (mode === "every") {
2122
2153
  if (!inner) return;
2123
- return sql`not exists (select 1 from ${fromClause} where ${and(joinCondition, relationWhere, not(inner))})`;
2154
+ return sql`not exists (select 1 from ${fromClause} where ${and(joinCondition, relationWhere, policy, not(inner))})`;
2124
2155
  }
2125
- const condition = and(joinCondition, relationWhere, inner);
2156
+ const condition = and(joinCondition, relationWhere, policy, inner);
2126
2157
  return mode === "none" ? sql`not exists (select 1 from ${fromClause} where ${condition})` : sql`exists (select 1 from ${fromClause} where ${condition})`;
2127
2158
  };
2128
2159
  /**
@@ -2394,8 +2425,8 @@ const columnEnumCache = /* @__PURE__ */ new WeakMap();
2394
2425
  * input field the enum would have typed, rather than emitting an empty enum, which is
2395
2426
  * invalid GraphQL.
2396
2427
  */
2397
- const generateColumnEnum = (table, enumName, description, predicate = () => true) => {
2398
- const cacheKey = `${enumName}\u0000${excludedColumnsKey(table)}`;
2428
+ const generateColumnEnum = (table, enumName, description, predicate = () => true, variant = "") => {
2429
+ const cacheKey = `${enumName}\u0000${excludedColumnsKey(table)}\u0000${variant}`;
2399
2430
  let tableCache = columnEnumCache.get(table);
2400
2431
  const cached = tableCache?.get(cacheKey);
2401
2432
  if (cached) return cached;
@@ -2455,6 +2486,9 @@ const conflictActionEnumType = (typeName) => sharedType(typeName, {
2455
2486
  * `ON DUPLICATE KEY UPDATE` fires on any unique key and takes no predicate, so offering
2456
2487
  * either there would mean silently ignoring it.
2457
2488
  *
2489
+ * `update` never offers a `contextValues` column: the server owns it, and the conflict
2490
+ * branch is an update, which must not hand the row to another owner.
2491
+ *
2458
2492
  * Returns `undefined` when the table has nothing to conflict on (`withTarget` dialects
2459
2493
  * only) — the caller then generates no upsert mutations for that table at all, rather than
2460
2494
  * an operation whose every call is a database error.
@@ -2462,12 +2496,13 @@ const conflictActionEnumType = (typeName) => sharedType(typeName, {
2462
2496
  const generateOnConflictInput = (params) => {
2463
2497
  const { table, tableName, typeName, uniqueSets, tableFilters, withTarget, cacheCtx } = params;
2464
2498
  const resolveName = cacheCtx.typeName;
2499
+ const serverOwned = Object.keys(cacheCtx.contextValuesOf?.(tableName) ?? {}).sort();
2465
2500
  const updateEnum = generateColumnEnum(table, resolveName({
2466
2501
  kind: "columnEnum",
2467
2502
  defaultName: `${typeName}UpdateColumn`,
2468
2503
  table: tableName,
2469
2504
  operation: "conflictUpdate"
2470
- }), `Columns of ${typeName} that an upsert can overwrite`);
2505
+ }), `Columns of ${typeName} that an upsert can overwrite`, (_column, columnName) => !serverOwned.includes(columnName), serverOwned.join(","));
2471
2506
  if (!updateEnum) return;
2472
2507
  const fields = {
2473
2508
  action: {
@@ -2477,11 +2512,11 @@ const generateOnConflictInput = (params) => {
2477
2512
  },
2478
2513
  update: {
2479
2514
  type: new GraphQLList(new GraphQLNonNull(updateEnum)),
2480
- 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."
2515
+ 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.`
2481
2516
  }
2482
2517
  };
2483
2518
  if (withTarget) {
2484
- const uniqueColumns = new Set(uniqueSets.flat());
2519
+ const uniqueColumns = new Set(uniqueSets.flatMap((set) => set.columns));
2485
2520
  const targetEnum = generateColumnEnum(table, resolveName({
2486
2521
  kind: "columnEnum",
2487
2522
  defaultName: `${typeName}ConflictTarget`,
@@ -2509,55 +2544,88 @@ const generateOnConflictInput = (params) => {
2509
2544
  });
2510
2545
  };
2511
2546
  /**
2512
- * Turns the request's `onConflict` argument and the rows it is inserting into the clause a
2513
- * dialect should attach.
2547
+ * Turns the request's `onConflict` argument and the rows it is inserting into the
2548
+ * statements a dialect should run — one per run of consecutive rows that write the same
2549
+ * columns on conflict.
2550
+ *
2551
+ * The split is what keeps a partial row partial. A multi-row insert fills a key one row
2552
+ * left out with DEFAULT, and `excluded.<column>` would carry that default onto the existing
2553
+ * row, so rows that supply different columns cannot share a `SET` list. Splitting into
2554
+ * *consecutive* runs, rather than grouping, keeps the rows a caller gets back in the order
2555
+ * it sent them. A batch whose rows agree — the usual case — is still one statement.
2514
2556
  *
2515
2557
  * `excludedRef` names the row that failed to insert in the dialect's own terms
2516
2558
  * (`excluded.col` on PostgreSQL and SQLite, `values(col)` on MySQL), which is what makes a
2517
2559
  * batch upsert update each row with its own values instead of the last row's.
2518
2560
  *
2561
+ * Never written on conflict, even when supplied: the conflict target (it already matches);
2562
+ * the primary key on a dialect with no target (MySQL's conflict can be on another unique
2563
+ * key, and copying a generated id over the existing row's would move it); and `serverOwned`
2564
+ * — the `contextValues` columns, which an update never reassigns.
2565
+ *
2519
2566
  * An UPDATE with nothing left to write degrades to NOTHING: `DO UPDATE SET` with an empty
2520
2567
  * body is not valid SQL, and doing nothing is what the request asked for anyway.
2521
2568
  */
2522
2569
  const resolveConflictPlan = (params) => {
2523
2570
  const { table, values, onConflict, pkNames, uniqueSets, excludedRef, withTarget, buildWhere } = params;
2524
2571
  const columns = getColumns(table);
2572
+ const serverOwned = new Set(params.serverOwned ?? []);
2525
2573
  let target;
2574
+ let targetWhere;
2526
2575
  if (withTarget) {
2527
2576
  const targetNames = onConflict?.target?.length ? onConflict.target : [...pkNames];
2528
2577
  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" });
2529
2578
  const requested = [...targetNames].sort().join(",");
2530
- 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" });
2579
+ const matched = uniqueSets.find((set) => [...set.columns].sort().join(",") === requested);
2580
+ 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" });
2531
2581
  target = targetNames.map((name) => columns[name]);
2582
+ targetWhere = matched.where;
2532
2583
  }
2533
- if ((onConflict?.action ?? "UPDATE") === "NOTHING") return {
2584
+ const nothing = {
2534
2585
  action: "NOTHING",
2535
2586
  target,
2587
+ targetWhere,
2536
2588
  set: {},
2537
2589
  setWhere: void 0
2538
2590
  };
2591
+ if ((onConflict?.action ?? "UPDATE") === "NOTHING") return [{
2592
+ values,
2593
+ plan: nothing
2594
+ }];
2539
2595
  const supplied = new Set(values.flatMap((row) => Object.keys(row)));
2540
- const targetNames = new Set(withTarget ? onConflict?.target?.length ? onConflict.target : pkNames : []);
2541
- let updateNames;
2596
+ const neverWritten = new Set(withTarget && onConflict?.target?.length ? onConflict.target : pkNames);
2597
+ let candidates;
2542
2598
  if (onConflict?.update?.length) {
2599
+ const owned = onConflict.update.filter((name) => serverOwned.has(name));
2600
+ 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" });
2543
2601
  const unsupplied = onConflict.update.filter((name) => !supplied.has(name));
2544
2602
  if (unsupplied.length) throw drizzleError(`Unable to upsert: onConflict.update lists ${unsupplied.join(", ")}, which the values do not supply.`, { code: "DRIZZLE_INVALID_ON_CONFLICT" });
2545
- updateNames = onConflict.update;
2546
- } else updateNames = [...supplied].filter((name) => !targetNames.has(name));
2547
- if (!updateNames.length) return {
2548
- action: "NOTHING",
2549
- target,
2550
- set: {},
2551
- setWhere: void 0
2552
- };
2553
- const set = Object.fromEntries(updateNames.map((name) => [name, excludedRef(columns[name].name)]));
2603
+ candidates = onConflict.update;
2604
+ } else candidates = [...supplied].filter((name) => !neverWritten.has(name) && !serverOwned.has(name));
2554
2605
  const setWhere = onConflict?.where && buildWhere ? buildWhere(onConflict.where) : void 0;
2555
- return {
2606
+ const planFor = (updateNames) => updateNames.length ? {
2556
2607
  action: "UPDATE",
2557
2608
  target,
2558
- set,
2609
+ targetWhere,
2610
+ set: Object.fromEntries(updateNames.map((name) => [name, excludedRef(columns[name].name)])),
2559
2611
  setWhere
2560
- };
2612
+ } : nothing;
2613
+ const batches = [];
2614
+ for (const row of values) {
2615
+ const updateNames = candidates.filter((name) => name in row);
2616
+ const key = updateNames.join(",");
2617
+ const last = batches[batches.length - 1];
2618
+ if (last?.key === key) last.values.push(row);
2619
+ else batches.push({
2620
+ key,
2621
+ values: [row],
2622
+ plan: planFor(updateNames)
2623
+ });
2624
+ }
2625
+ return batches.map(({ values: rows, plan }) => ({
2626
+ values: rows,
2627
+ plan
2628
+ }));
2561
2629
  };
2562
2630
  /** `excluded.<column>` — PostgreSQL and SQLite name the rejected row this way. */
2563
2631
  const excludedColumnRef = (columnName) => sql`excluded.${sql.identifier(columnName)}`;
@@ -2611,12 +2679,14 @@ const extendRelationOrderChain = (parentTable, relationName, relEntry, ctx, chai
2611
2679
  const joinCondition = buildRelationJoinCondition(parentTable, relation, aliasedTarget, relationName);
2612
2680
  const ownWhere = relationOwnWhere(relation);
2613
2681
  const relationWhere = ownWhere ? relationsFilterToSQL(isRelationFilterReversed(relation) ? parentTable : aliasedTarget, ownWhere) : void 0;
2682
+ const policy = relationTargetPolicy(ctx, relation, relEntry.targetTableName, aliasedTarget);
2614
2683
  return {
2615
2684
  fromParts: [...chain?.fromParts ?? [], getTableAsAliasSQL(aliasedTarget)],
2616
2685
  conditions: [
2617
2686
  ...chain?.conditions ?? [],
2618
2687
  ...joinCondition ? [joinCondition] : [],
2619
- ...relationWhere ? [relationWhere] : []
2688
+ ...relationWhere ? [relationWhere] : [],
2689
+ ...policy ? [policy] : []
2620
2690
  ],
2621
2691
  table: aliasedTarget
2622
2692
  };
@@ -3001,9 +3071,9 @@ const primaryKeyOrderExprs = (table, pkNames) => {
3001
3071
  return pkNames.map((n) => cols[n]).filter(Boolean).map((col) => asc(col));
3002
3072
  };
3003
3073
  /**
3004
- * Every set of columns that uniquely identifies a row of `table`: the primary key first,
3005
- * then each unique constraint and unique index, then each column declared `.unique()`
3006
- * inline. Sets are property names (what GraphQL inputs use), not database column names.
3074
+ * Every set of columns that is unique on `table`: the primary key first, then each unique
3075
+ * constraint and unique index, then each column declared `.unique()` inline. Sets are
3076
+ * property names (what GraphQL inputs use), not database column names.
3007
3077
  *
3008
3078
  * `getTableConfig` is the dialect's own — the three dialects expose the same
3009
3079
  * `{ primaryKeys, uniqueConstraints, indexes }` shape but from different modules, so the
@@ -3012,9 +3082,13 @@ const primaryKeyOrderExprs = (table, pkNames) => {
3012
3082
  * Index entries whose columns are SQL expressions rather than plain columns are skipped:
3013
3083
  * an expression index is a valid conflict target in the database but cannot be named by a
3014
3084
  * column enum. Deduplicated, order-insensitive — a column that is both the primary key and
3015
- * a unique constraint yields one set.
3085
+ * a unique constraint yields one set, and a set that is unique both outright and under a
3086
+ * partial index keeps the outright one.
3087
+ *
3088
+ * A partial unique index carries its predicate as `where`: it is a conflict target only
3089
+ * together with that predicate, and it does not make the set unique across the whole table.
3016
3090
  */
3017
- const getUniqueColumnSets = (table, getTableConfig) => {
3091
+ const getUniqueTargets = (table, getTableConfig) => {
3018
3092
  const cols = visibleColumns(table);
3019
3093
  const propNameByColumnName = new Map(Object.entries(cols).map(([propName, col]) => [col.name, propName]));
3020
3094
  const toPropNames = (columnNames) => {
@@ -3026,25 +3100,40 @@ const getUniqueColumnSets = (table, getTableConfig) => {
3026
3100
  }
3027
3101
  return propNames.length ? propNames : void 0;
3028
3102
  };
3103
+ const whole = (columns) => columns ? { columns } : void 0;
3029
3104
  const config = getTableConfig(table);
3030
3105
  const candidates = [
3031
- Object.entries(cols).filter(([, col]) => col.primary).map(([propName]) => propName),
3032
- ...config.primaryKeys.map((pk) => toPropNames(pk.columns.map((c) => c.name))),
3033
- ...(config.uniqueConstraints ?? []).map((uc) => toPropNames(uc.columns.map((c) => c.name))),
3034
- ...(config.indexes ?? []).filter((index) => index.config.unique).map((index) => toPropNames(index.config.columns.map((c) => c?.name))),
3035
- ...Object.entries(cols).filter(([, col]) => col.isUnique).map(([propName]) => [propName])
3106
+ whole(Object.entries(cols).filter(([, col]) => col.primary).map(([propName]) => propName)),
3107
+ ...config.primaryKeys.map((pk) => whole(toPropNames(pk.columns.map((c) => c.name)))),
3108
+ ...(config.uniqueConstraints ?? []).map((uc) => whole(toPropNames(uc.columns.map((c) => c.name)))),
3109
+ ...(config.indexes ?? []).filter((index) => index.config.unique).map((index) => {
3110
+ const columns = toPropNames(index.config.columns.map((c) => c?.name));
3111
+ return columns ? {
3112
+ columns,
3113
+ ...index.config.where ? { where: index.config.where } : {}
3114
+ } : void 0;
3115
+ }),
3116
+ ...Object.entries(cols).filter(([, col]) => col.isUnique).map(([propName]) => ({ columns: [propName] }))
3036
3117
  ];
3037
- const seen = /* @__PURE__ */ new Set();
3038
- const sets = [];
3039
- for (const set of candidates) {
3040
- if (!set?.length) continue;
3041
- const key = [...set].sort().join(",");
3042
- if (seen.has(key)) continue;
3043
- seen.add(key);
3044
- sets.push(set);
3118
+ const byKey = /* @__PURE__ */ new Map();
3119
+ for (const candidate of candidates) {
3120
+ if (!candidate?.columns.length) continue;
3121
+ const key = [...candidate.columns].sort().join(",");
3122
+ const seen = byKey.get(key);
3123
+ if (!seen) byKey.set(key, candidate);
3124
+ else if (seen.where && !candidate.where) byKey.set(key, { columns: seen.columns });
3045
3125
  }
3046
- return sets;
3126
+ return [...byKey.values()];
3047
3127
  };
3128
+ /**
3129
+ * The column sets that identify at most one row of `table` — {@link getUniqueTargets}
3130
+ * without the partial unique indexes, which only constrain the rows their predicate
3131
+ * matches (a soft-deleted duplicate is allowed), and so do not.
3132
+ *
3133
+ * Pass `{ partial: true }` to keep them, for callers that only need to know a set is
3134
+ * unique among the rows that matter.
3135
+ */
3136
+ const getUniqueColumnSets = (table, getTableConfig, options = {}) => getUniqueTargets(table, getTableConfig).filter((target) => options.partial || !target.where).map((target) => target.columns);
3048
3137
  //#endregion
3049
3138
  //#region src/util/builders/common/distinct.ts
3050
3139
  /**
@@ -3827,10 +3916,10 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
3827
3916
  });
3828
3917
  const relWhere = relationArgs?.where;
3829
3918
  const relDeleted = relationArgs?.deleted ?? scope?.relationDefault(targetTableName, is(relEntry.relation, One) && relEntry.relation.optional === false);
3830
- thisRecord.where = relWhere || scope?.has(targetTableName, relDeleted) ? { RAW: (aliasedTable) => withScope(scope, targetTableName, aliasedTable, relWhere ? extractFilters(aliasedTable, relName, relWhere, relationFilterCtx(filterCtx, targetTableName)) : void 0, relDeleted) } : void 0;
3919
+ thisRecord.where = relWhere || scope?.has(targetTableName, relDeleted) ? { RAW: (aliasedTable) => withScope(scope, targetTableName, aliasedTable, relWhere ? extractFilters(aliasedTable, relName, relWhere, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0, relDeleted) } : void 0;
3831
3920
  const hasPagination = offset != null || limit != null;
3832
3921
  const pkNames = targetPkNames ?? [];
3833
- thisRecord.orderBy = relationArgs?.orderBy ? (aliasedTable) => extractOrderBy(aliasedTable, relationArgs.orderBy, relationFilterCtx(filterCtx, targetTableName), relWhere) : hasPagination && pkNames.length ? (aliasedTable) => primaryKeyOrderExprs(aliasedTable, pkNames) : void 0;
3922
+ thisRecord.orderBy = relationArgs?.orderBy ? (aliasedTable) => extractOrderBy(aliasedTable, relationArgs.orderBy, relationFilterCtx(filterCtx, targetTableName, scope), relWhere) : hasPagination && pkNames.length ? (aliasedTable) => primaryKeyOrderExprs(aliasedTable, pkNames) : void 0;
3834
3923
  thisRecord.offset = offset;
3835
3924
  thisRecord.limit = limit;
3836
3925
  thisRecord.with = extractRelationsParams(relationMap, tables, targetTableName, field, relTypeName, options);
@@ -3888,7 +3977,7 @@ const runRelationalSelect = async (opts) => {
3888
3977
  tableName,
3889
3978
  distinct,
3890
3979
  pkNames: pkNames ?? [],
3891
- where: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName)) : void 0, deleted),
3980
+ where: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope)) : void 0, deleted),
3892
3981
  orderBy,
3893
3982
  limit: single ? 1 : opts.limit,
3894
3983
  offset
@@ -3903,8 +3992,8 @@ const runRelationalSelect = async (opts) => {
3903
3992
  allRelations: filterCtx?.relationMap
3904
3993
  }),
3905
3994
  offset: distinctKeys ? void 0 : offset,
3906
- orderBy: distinctKeys ? (aliasedTable) => [...orderBy ? extractOrderBy(aliasedTable, orderBy, relationFilterCtx(filterCtx, tableName), where) : [], ...primaryKeyOrderExprs(aliasedTable, pkNames)] : cursorEntries ? (aliasedTable) => cursorOrderExprs(aliasedTable, cursorEntries) : orderBy ? (aliasedTable) => extractOrderBy(aliasedTable, orderBy, relationFilterCtx(filterCtx, tableName), where) : needsDefaultOrder && pkNames?.length ? (aliasedTable) => primaryKeyOrderExprs(aliasedTable, pkNames) : void 0,
3907
- where: distinctKeys ? { RAW: (aliasedTable) => primaryKeyRestriction(aliasedTable, pkNames, distinctKeys) } : where || cursorValues || scope?.has(tableName, deleted) ? { RAW: (aliasedTable) => withScope(scope, tableName, aliasedTable, and(where ? extractFilters(aliasedTable, tableName, where, relationFilterCtx(filterCtx, tableName)) : void 0, cursorValues ? buildCursorCondition(aliasedTable, cursorEntries, cursorValues, opts.nullOrdering ?? "nulls-smallest") : void 0), deleted) } : void 0,
3995
+ orderBy: distinctKeys ? (aliasedTable) => [...orderBy ? extractOrderBy(aliasedTable, orderBy, relationFilterCtx(filterCtx, tableName, scope), where) : [], ...primaryKeyOrderExprs(aliasedTable, pkNames)] : cursorEntries ? (aliasedTable) => cursorOrderExprs(aliasedTable, cursorEntries) : orderBy ? (aliasedTable) => extractOrderBy(aliasedTable, orderBy, relationFilterCtx(filterCtx, tableName, scope), where) : needsDefaultOrder && pkNames?.length ? (aliasedTable) => primaryKeyOrderExprs(aliasedTable, pkNames) : void 0,
3996
+ where: distinctKeys ? { RAW: (aliasedTable) => primaryKeyRestriction(aliasedTable, pkNames, distinctKeys) } : where || cursorValues || scope?.has(tableName, deleted) ? { RAW: (aliasedTable) => withScope(scope, tableName, aliasedTable, and(where ? extractFilters(aliasedTable, tableName, where, relationFilterCtx(filterCtx, tableName, scope)) : void 0, cursorValues ? buildCursorCondition(aliasedTable, cursorEntries, cursorValues, opts.nullOrdering ?? "nulls-smallest") : void 0), deleted) } : void 0,
3908
3997
  with: relationMap[tableName] ? extractRelationsParams(relationMap, tables, tableName, parsedInfo, typeName, {
3909
3998
  typeNameMapper,
3910
3999
  filterCtx,
@@ -4486,7 +4575,7 @@ const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, f
4486
4575
  tableName,
4487
4576
  table,
4488
4577
  where,
4489
- relationCtx: relationFilterCtx(filterCtx, tableName),
4578
+ relationCtx: relationFilterCtx(filterCtx, tableName, scope),
4490
4579
  required: requireWhere,
4491
4580
  deleted: hard ? "INCLUDE" : void 0
4492
4581
  });
@@ -4674,6 +4763,19 @@ const keysFromWhere = (where, primaryKey) => {
4674
4763
  return none;
4675
4764
  };
4676
4765
  /**
4766
+ * Whether an upsert's `onConflict` leaves the primary-key values in `values` naming exactly
4767
+ * the rows it writes. It does not when the conflict is detected on another unique key — the
4768
+ * row updated is whichever one holds that key, not the one the supplied primary key names —
4769
+ * nor when conflicting rows can be skipped: `action: NOTHING`, or a `where` guard.
4770
+ */
4771
+ const conflictsOnKeyAlone = (onConflict, primaryKey) => {
4772
+ if (!onConflict || typeof onConflict !== "object") return true;
4773
+ const { target, action, where } = onConflict;
4774
+ if (action === "NOTHING" || where !== void 0 && where !== null) return false;
4775
+ if (!Array.isArray(target) || !target.length) return true;
4776
+ return [...target].sort().join(",") === [...primaryKey].sort().join(",");
4777
+ };
4778
+ /**
4677
4779
  * The rows a generated field's arguments are about, as primary-key values.
4678
4780
  *
4679
4781
  * The row identity of a mutation lives in a different argument for every operation —
@@ -4709,7 +4811,7 @@ const identifyRows = (field, args) => {
4709
4811
  table,
4710
4812
  primaryKey,
4711
4813
  rows: rows.filter((row) => !!row),
4712
- complete: entries.length > 0 && rows.every(Boolean)
4814
+ complete: entries.length > 0 && rows.every(Boolean) && (meta.operation !== "upsert" || conflictsOnKeyAlone(args?.["onConflict"], primaryKey))
4713
4815
  };
4714
4816
  }
4715
4817
  if (targetArg === "updates") {
@@ -4879,7 +4981,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
4879
4981
  const executor = resolveExecutor(db, context);
4880
4982
  const uniqueIds = [...new Set(parentIds)];
4881
4983
  const scope = resolveScope(policies, context, filterCtx);
4882
- let whereCondition = withScope(scope, targetTableName, targetTable, and(inArray(groupCol, uniqueIds), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName)) : void 0, cursorValues ? buildCursorCondition(targetTable, cursorEntries, cursorValues, nullOrdering) : void 0), deleted ?? defaultDeleted);
4984
+ let whereCondition = withScope(scope, targetTableName, targetTable, and(inArray(groupCol, uniqueIds), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0, cursorValues ? buildCursorCondition(targetTable, cursorEntries, cursorValues, nullOrdering) : void 0), deleted ?? defaultDeleted);
4883
4985
  if (distinct) {
4884
4986
  const keys = await selectDistinctKeys({
4885
4987
  db: executor,
@@ -4904,10 +5006,10 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
4904
5006
  [PARENT_KEY]: groupCol
4905
5007
  } : columns;
4906
5008
  let rows;
4907
- if (limit != null || offset != null) rows = await batchedPaginatedRelationQuery(executor, targetTable, foreignCol, whereCondition, orderByArg, limit ?? null, offset ?? null, targetPkNames, selection, relationFilterCtx(filterCtx, targetTableName), whereArg, junction);
5009
+ if (limit != null || offset != null) rows = await batchedPaginatedRelationQuery(executor, targetTable, foreignCol, whereCondition, orderByArg, limit ?? null, offset ?? null, targetPkNames, selection, relationFilterCtx(filterCtx, targetTableName, scope), whereArg, junction);
4908
5010
  else {
4909
5011
  let q = withJunction((selection ? executor.select(selection) : executor.select()).from(targetTable), junction, foreignCol).where(whereCondition);
4910
- const orderExprs = cursorEntries ? cursorOrderExprs(targetTable, cursorEntries) : orderByArg ? extractOrderBy(targetTable, orderByArg, relationFilterCtx(filterCtx, targetTableName), whereArg) : [];
5012
+ const orderExprs = cursorEntries ? cursorOrderExprs(targetTable, cursorEntries) : orderByArg ? extractOrderBy(targetTable, orderByArg, relationFilterCtx(filterCtx, targetTableName, scope), whereArg) : [];
4911
5013
  if (orderExprs.length) q = q.orderBy(...orderExprs);
4912
5014
  rows = await q;
4913
5015
  }
@@ -5587,6 +5689,7 @@ const resolveBuildConfig = (config, schema) => {
5587
5689
  const normalized = {};
5588
5690
  for (const [columnName, entry] of Object.entries(orderBy)) {
5589
5691
  if (!columns[columnName]) throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy names '${columnName}', which is not a column of '${tableName}'.`);
5692
+ if (config?.exclude?.columns?.[tableName]?.includes(columnName)) throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy names '${columnName}', which config.exclude.columns hides. A default ordering is encoded into every row's cursor, so it can only use columns the schema exposes.`);
5590
5693
  const direction = typeof entry === "string" ? entry : entry?.direction;
5591
5694
  if (direction !== "asc" && direction !== "desc") throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy.${columnName} must be 'asc', 'desc', or { direction, priority? }.`);
5592
5695
  const priority = typeof entry === "string" ? void 0 : entry?.priority;
@@ -5967,7 +6070,8 @@ const generateAggregate = (db, tableName, table, typeName, fieldName, filterArgs
5967
6070
  const request = parseAggregateRequest(info, target);
5968
6071
  if (!Object.keys(request.selection).length) return {};
5969
6072
  let query = resolveExecutor(db, context).select(request.selection).from(table);
5970
- const where = withScope(resolveScope(policies, context, filterCtx), tableName, table, args.where ? extractFilters(table, tableName, args.where, relationFilterCtx(filterCtx, tableName)) : void 0, args.deleted);
6073
+ const scope = resolveScope(policies, context, filterCtx);
6074
+ const where = withScope(scope, tableName, table, args.where ? extractFilters(table, tableName, args.where, relationFilterCtx(filterCtx, tableName, scope)) : void 0, args.deleted);
5971
6075
  if (where) query = query.where(where);
5972
6076
  const rows = await query;
5973
6077
  return assembleAggregateRow(rows[0] ?? {}, request, target);
@@ -6044,7 +6148,8 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
6044
6148
  const loader = getOrCreateLoader(context, loaderKey, async (parentIds) => {
6045
6149
  const executor = resolveExecutor(db, context);
6046
6150
  const uniqueIds = [...new Set(parentIds)];
6047
- const whereCondition = withScope(resolveScope(policies, context, filterCtx), targetTableName, targetTable, and(inArray(groupCol, uniqueIds), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName)) : void 0), deleted ?? defaultDeleted);
6151
+ const scope = resolveScope(policies, context, filterCtx);
6152
+ const whereCondition = withScope(scope, targetTableName, targetTable, and(inArray(groupCol, uniqueIds), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0), deleted ?? defaultDeleted);
6048
6153
  const selected = executor.select({
6049
6154
  [GROUP_KEY]: groupCol,
6050
6155
  ...request.selection
@@ -6260,7 +6365,8 @@ const generateGroupBy = (db, tableName, table, typeName, fieldName, filterArgs,
6260
6365
  ...request.selection
6261
6366
  };
6262
6367
  let query = resolveExecutor(db, context).select(selection).from(table);
6263
- const where = withScope(resolveScope(policies, context, filterCtx), tableName, table, args.where ? extractFilters(table, tableName, args.where, relationFilterCtx(filterCtx, tableName)) : void 0, args.deleted);
6368
+ const scope = resolveScope(policies, context, filterCtx);
6369
+ const where = withScope(scope, tableName, table, args.where ? extractFilters(table, tableName, args.where, relationFilterCtx(filterCtx, tableName, scope)) : void 0, args.deleted);
6264
6370
  if (where) query = query.where(where);
6265
6371
  query = query.groupBy(...keyColumns.map(([, column]) => column));
6266
6372
  const havingCondition = buildHavingCondition(args.having, columns);
@@ -6299,6 +6405,54 @@ const generateGroupBy = (db, tableName, table, typeName, fieldName, filterArgs,
6299
6405
  };
6300
6406
  };
6301
6407
  //#endregion
6408
+ //#region src/util/builders/common/build-registry.ts
6409
+ /**
6410
+ * Keyed by the `extensions.drizzle` object of every object type the build generated, rather
6411
+ * than by the type: a schema composed from `entities.types` keeps the types, and one rebuilt
6412
+ * by a schema-transforming tool (`mapSchema`, stitching) keeps their extensions.
6413
+ */
6414
+ const runtimeByTypeMeta = /* @__PURE__ */ new WeakMap();
6415
+ /**
6416
+ * What is known about the builds of one drizzle instance, for a caller with no resolve info to
6417
+ * look its build up by. Only a lone build is kept — with two or more there is no telling which
6418
+ * one a selection belongs to — so rebuilding a schema repeatedly holds on to nothing extra.
6419
+ */
6420
+ const buildsByDb = /* @__PURE__ */ new WeakMap();
6421
+ /** Whether a build changes what a read returns at all: a row policy, a limit or a default ordering. */
6422
+ const hasPolicies = (runtime) => !!(runtime.policies?.scope || runtime.policies?.defaultOrderBy || runtime.limits);
6423
+ /** Records a finished build against the object types it generated and the instance it read. */
6424
+ const registerBuildRuntime = (runtime, types) => {
6425
+ for (const type of types) {
6426
+ const meta = drizzleExtension(type);
6427
+ if (meta) runtimeByTypeMeta.set(meta, runtime);
6428
+ }
6429
+ const previous = buildsByDb.get(runtime.db);
6430
+ buildsByDb.set(runtime.db, previous ? {
6431
+ count: previous.count + 1,
6432
+ anyPolicies: previous.anyPolicies || hasPolicies(runtime)
6433
+ } : {
6434
+ count: 1,
6435
+ only: runtime,
6436
+ anyPolicies: hasPolicies(runtime)
6437
+ });
6438
+ };
6439
+ /** The build that generated `type`, when one did. */
6440
+ const runtimeForType = (type) => {
6441
+ const meta = type && typeof type === "object" ? drizzleExtension(type) : void 0;
6442
+ return meta ? runtimeByTypeMeta.get(meta) : void 0;
6443
+ };
6444
+ /**
6445
+ * The build of `db` to use when no generated type identifies one: the only build, or none
6446
+ * when no build applies any policy. Throws when several builds exist and one of them would
6447
+ * change the read, since picking either could apply the wrong policies — or none.
6448
+ */
6449
+ const runtimeForDb = (db, helper) => {
6450
+ const builds = buildsByDb.get(db);
6451
+ if (!builds) return;
6452
+ if (builds.count === 1) return builds.only;
6453
+ if (builds.anyPolicies) throw new Error(`Drizzle-GraphQL Error: ${helper} cannot tell which of the ${builds.count} schemas built over this database the selection belongs to, and at least one applies row policies (scope, softDelete, limits or defaults). Pass the resolver's \`info\` rather than a parsed selection tree, from a field whose type the build generated.`);
6454
+ };
6455
+ //#endregion
6302
6456
  //#region src/util/builders/nested-writes.ts
6303
6457
  const ALL_OPS = [
6304
6458
  "create",
@@ -6791,7 +6945,7 @@ const createNestedWriteRuntime = (params) => {
6791
6945
  * nothing is rejected: `connect: {}` would otherwise attach every row in the table.
6792
6946
  */
6793
6947
  const conditionOf = (plan, filter, operation, scope) => {
6794
- const condition = extractFilters(plan.targetTable, plan.targetTableName, filter, relationFilterCtx(filterCtx, plan.targetTableName));
6948
+ const condition = extractFilters(plan.targetTable, plan.targetTableName, filter, relationFilterCtx(filterCtx, plan.targetTableName, scope));
6795
6949
  if (!condition) throw drizzleError(`Drizzle-GraphQL Error: '${operation}' on '${plan.relationName}' needs a filter that selects rows — it was given one that matches everything.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
6796
6950
  return withScope(scope, plan.targetTableName, plan.targetTable, condition);
6797
6951
  };
@@ -7186,7 +7340,7 @@ const createSchemaBuilder = (adapter) => {
7186
7340
  uniqueKeysOf: (tableName) => uniqueKeys[tableName]
7187
7341
  };
7188
7342
  adapter.preflight?.(db, options);
7189
- const nestedPlans = features.nestedWrites ? buildNestedWritePlans(tables, namedRelations, (target) => uniqueColumnSets(target), (target) => primaryKeyPropNames(target), extractRelationJoinColumns, (junction) => schemaEntries.find(([, value]) => value === junction)?.[0]) : void 0;
7343
+ 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;
7190
7344
  if (nestedPlans) {
7191
7345
  const omitted = gateNestedWritePlans(nestedPlans, featureOf, (tableName) => excludedTables.has(tableName));
7192
7346
  if (omitted.length) {
@@ -7402,6 +7556,14 @@ const createSchemaBuilder = (adapter) => {
7402
7556
  }
7403
7557
  if (Object.keys(relResolvers).length > 0) fieldResolvers[tableName] = relResolvers;
7404
7558
  }
7559
+ registerBuildRuntime({
7560
+ db: ctx.db,
7561
+ filterCtx: ctx.filterCtx,
7562
+ policies: ctx.policies,
7563
+ scoped: !!ctx.tablePolicies?.scope,
7564
+ limits: ctx.limits,
7565
+ nullOrdering: adapter.nullOrdering
7566
+ }, Object.values(ctx.outputs));
7405
7567
  return {
7406
7568
  queries: ctx.queries,
7407
7569
  mutations: ctx.mutations,
@@ -7474,20 +7636,33 @@ const generateUpsert = (db, tableName, table, baseType, onConflictType, fieldNam
7474
7636
  }
7475
7637
  },
7476
7638
  run: async ({ executor, args, context, before, after }) => {
7477
- const input = applyContextValuesAll(single ? [remapFromGraphQLSingleInput(args.values, table)] : remapFromGraphQLArrayInput(args.values, table), policies?.contextValues?.(tableName), context);
7639
+ const contextColumns = policies?.contextValues?.(tableName);
7640
+ const input = applyContextValuesAll(single ? [remapFromGraphQLSingleInput(args.values, table)] : remapFromGraphQLArrayInput(args.values, table), contextColumns, context);
7478
7641
  if (!input.length) throw drizzleError("No values were provided!", { code: "DRIZZLE_NO_VALUES" });
7479
7642
  await before();
7480
- const plan = resolveConflictPlan({
7643
+ const batches = resolveConflictPlan({
7481
7644
  table,
7482
7645
  values: input,
7483
7646
  onConflict: args.onConflict,
7484
7647
  pkNames,
7485
7648
  uniqueSets: [],
7486
7649
  excludedRef: mysqlValuesColumnRef,
7487
- withTarget: false
7650
+ withTarget: false,
7651
+ serverOwned: Object.keys(contextColumns ?? {})
7652
+ });
7653
+ const requestedNothing = args.onConflict?.action === "NOTHING";
7654
+ const runBatch = async (target, { values, plan }) => {
7655
+ if (plan.action === "NOTHING" && !requestedNothing) {
7656
+ const columns = getColumns(table);
7657
+ const keyName = pkNames[0] ?? Object.keys(columns)[0];
7658
+ await target.insert(table).values(values).onDuplicateKeyUpdate({ set: { [keyName]: sql`${sql.identifier(columns[keyName].name)}` } });
7659
+ } else if (plan.action === "NOTHING") await target.insert(table).ignore().values(values);
7660
+ else await target.insert(table).values(values).onDuplicateKeyUpdate({ set: plan.set });
7661
+ };
7662
+ if (batches.length === 1) await runBatch(executor, batches[0]);
7663
+ else await executor.transaction(async (tx) => {
7664
+ for (const batch of batches) await runBatch(tx, batch);
7488
7665
  });
7489
- if (plan.action === "NOTHING") await executor.insert(table).ignore().values(input);
7490
- else await executor.insert(table).values(input).onDuplicateKeyUpdate({ set: plan.set });
7491
7666
  await after();
7492
7667
  return isSuccess;
7493
7668
  }
@@ -7511,7 +7686,7 @@ const generateUpdate = (db, tableName, table, setArgs, filterArgs, fieldName, si
7511
7686
  const input = stripContextValues(remapUpdateInput(set, table, tableName), policies?.contextValues?.(tableName));
7512
7687
  if (!Object.keys(input).length) throw drizzleError("Unable to update with no values specified!", { code: "DRIZZLE_NO_VALUES" });
7513
7688
  await before();
7514
- const relationCtx = relationFilterCtx(filterCtx, tableName);
7689
+ const relationCtx = relationFilterCtx(filterCtx, tableName, scope);
7515
7690
  const filters = scopedWhere({
7516
7691
  scope,
7517
7692
  tableName,
@@ -7557,7 +7732,7 @@ const generateUpdateMany$2 = (db, tableName, table, updateManyInput, fieldName,
7557
7732
  if (!Object.keys(input).length) throw drizzleError("Unable to update with no values specified!", { code: "DRIZZLE_NO_VALUES" });
7558
7733
  return {
7559
7734
  set: input,
7560
- filters: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName)) : void 0)
7735
+ filters: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope)) : void 0)
7561
7736
  };
7562
7737
  });
7563
7738
  await executor.transaction(async (tx) => {
@@ -7600,7 +7775,7 @@ const generateDelete = (db, tableName, table, filterArgs, fieldName, single, req
7600
7775
  const { where } = args;
7601
7776
  const hard = canHardDelete && args.hard === true;
7602
7777
  const scope = policies?.scope?.(context);
7603
- const relationCtx = relationFilterCtx(filterCtx, tableName);
7778
+ const relationCtx = relationFilterCtx(filterCtx, tableName, scope);
7604
7779
  const filters = scopedWhere({
7605
7780
  scope,
7606
7781
  tableName,
@@ -7933,7 +8108,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7933
8108
  });
7934
8109
  const returning = withHookReturning(nestedEntries ? nested.withJoinColumns(tableName, mergedOps(nestedEntries), { ...columns }, table) : columns);
7935
8110
  const runUpsert = async (target, values) => {
7936
- const plan = resolveConflictPlan({
8111
+ const batches = resolveConflictPlan({
7937
8112
  table,
7938
8113
  values,
7939
8114
  onConflict: args.onConflict,
@@ -7941,16 +8116,29 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
7941
8116
  uniqueSets,
7942
8117
  excludedRef: excludedColumnRef,
7943
8118
  withTarget: true,
7944
- buildWhere: (where) => extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName))
8119
+ serverOwned: Object.keys(contextColumns ?? {}),
8120
+ buildWhere: (where) => extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope))
7945
8121
  });
7946
- let query = target.insert(table).values(values).returning(returning);
7947
- const setWhere = withScope(scope, tableName, table, plan.setWhere);
7948
- query = plan.action === "NOTHING" ? query.onConflictDoNothing(plan.target ? { target: plan.target } : void 0) : query.onConflictDoUpdate({
7949
- target: plan.target,
7950
- set: plan.set,
7951
- setWhere
8122
+ const runBatch = async (executor, { values: rows, plan }) => {
8123
+ let query = executor.insert(table).values(rows).returning(returning);
8124
+ const setWhere = withScope(scope, tableName, table, plan.setWhere);
8125
+ query = plan.action === "NOTHING" ? query.onConflictDoNothing(plan.target ? {
8126
+ target: plan.target,
8127
+ where: plan.targetWhere
8128
+ } : void 0) : query.onConflictDoUpdate({
8129
+ target: plan.target,
8130
+ targetWhere: plan.targetWhere,
8131
+ set: plan.set,
8132
+ setWhere
8133
+ });
8134
+ return await query;
8135
+ };
8136
+ if (batches.length === 1) return runBatch(target, batches[0]);
8137
+ return target.transaction(async (tx) => {
8138
+ const rows = [];
8139
+ for (const batch of batches) rows.push(...await runBatch(tx, batch));
8140
+ return rows;
7952
8141
  });
7953
- return await query;
7954
8142
  };
7955
8143
  const written = nestedEntries ? await writeWithNestedOps({
7956
8144
  executor,
@@ -8011,7 +8199,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8011
8199
  const nestedOps = entry && nested.hasOps(entry.ops) ? entry.ops : void 0;
8012
8200
  const input = stripContextValues(remapUpdateInput(entry ? entry.columns : set, table, tableName), policies?.contextValues?.(tableName));
8013
8201
  if (!Object.keys(input).length && !nestedOps) throw drizzleError("Unable to update with no values specified!", { code: "DRIZZLE_NO_VALUES" });
8014
- const relationCtx = relationFilterCtx(filterCtx, tableName);
8202
+ const relationCtx = relationFilterCtx(filterCtx, tableName, scope);
8015
8203
  const filters = scopedWhere({
8016
8204
  scope,
8017
8205
  tableName,
@@ -8090,7 +8278,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8090
8278
  await before();
8091
8279
  const parsedInfo = parseResolveInfo(info, { deep: true });
8092
8280
  const columns = extractSelectedColumnsFromTreeSQLFormat(parsedInfo.fieldsByTypeName[applyObjectTypeName(typeName, tableName, resolveName)], table, selectionCtx);
8093
- const relationCtx = relationFilterCtx(filterCtx, tableName);
8281
+ const relationCtx = relationFilterCtx(filterCtx, tableName, scope);
8094
8282
  const filters = scopedWhere({
8095
8283
  scope,
8096
8284
  tableName,
@@ -8130,7 +8318,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8130
8318
  */
8131
8319
  const createSchemaDataGenerator = (adapter) => {
8132
8320
  const primaryKeyPropNames = adapter.primaryKeyPropNames;
8133
- const uniqueColumnSets = (table) => getUniqueColumnSets(table, adapter.getTableConfig);
8321
+ const uniqueTargets = (table) => getUniqueTargets(table, adapter.getTableConfig);
8134
8322
  const { generateInsert, generateUpsert, generateUpdate, generateDelete } = buildWriteResolvers(primaryKeyPropNames);
8135
8323
  const { prepareBuild, forEachTable, finalizeBuild } = createSchemaBuilder(adapter);
8136
8324
  return (db, schema, relations, options) => {
@@ -8171,7 +8359,7 @@ const createSchemaDataGenerator = (adapter) => {
8171
8359
  single: true,
8172
8360
  conflictDoNothing
8173
8361
  }) : void 0;
8174
- const uniqueSets = tableFeatures.upsert ? uniqueColumnSets(schema[tableName]) : [];
8362
+ const uniqueSets = tableFeatures.upsert ? uniqueTargets(schema[tableName]) : [];
8175
8363
  const onConflictInput = tableFeatures.upsert ? generateOnConflictInput({
8176
8364
  table: schema[tableName],
8177
8365
  tableName,
@@ -8541,7 +8729,7 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
8541
8729
  return {
8542
8730
  set: input,
8543
8731
  ops,
8544
- filters: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName)) : void 0)
8732
+ filters: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope)) : void 0)
8545
8733
  };
8546
8734
  });
8547
8735
  const anyNested = entries.some((entry) => entry.ops);
@@ -8711,6 +8899,36 @@ const namingRuleOf = (options) => resolveGeneratedTypeNames({
8711
8899
  typeNameSuffix: options.typeNameSuffix
8712
8900
  });
8713
8901
  /**
8902
+ * The build whose policies the read applies. Looked up through the generated types the
8903
+ * resolve info reaches — the field's return type, the root table's type in the executing
8904
+ * schema, the parent type — so a resolver over a generated type finds its own build. Without
8905
+ * resolve info (a parsed tree was passed), the instance's only build is used.
8906
+ */
8907
+ const runtimeFor = (info, db, typeName, helper) => {
8908
+ if (!("fieldsByTypeName" in info)) {
8909
+ const candidates = [
8910
+ info.returnType ? getNamedType(info.returnType) : void 0,
8911
+ info.schema?.getType(typeName),
8912
+ info.parentType
8913
+ ];
8914
+ for (const candidate of candidates) {
8915
+ const runtime = runtimeForType(candidate);
8916
+ if (runtime && runtime.db === db) return runtime;
8917
+ }
8918
+ }
8919
+ return runtimeForDb(db, helper);
8920
+ };
8921
+ /**
8922
+ * The request's row policies under `runtime`, bound to the context the caller passed. A build
8923
+ * with `scope` hooks cannot be read without that context: the hooks would be called with
8924
+ * nothing, and running the read unscoped is the leak the option exists to prevent.
8925
+ */
8926
+ const scopeFor = (runtime, options, helper) => {
8927
+ if (!runtime?.policies?.scope) return;
8928
+ if (runtime.scoped && !("context" in options)) throw new Error(`Drizzle-GraphQL Error: the schema this selection belongs to configures \`scope\`, so ${helper} needs the request context to apply it. Pass the resolver's context as the \`context\` option.`);
8929
+ return runtime.policies.scope(options.context);
8930
+ };
8931
+ /**
8714
8932
  * Reads the parsed selection tree out of a resolver's `info`, resolving fragments and
8715
8933
  * inline fragments. Accepts an already-parsed tree so a caller that has one (or is chaining
8716
8934
  * these helpers) does not pay for the walk twice.
@@ -8737,6 +8955,10 @@ const parseInfo = (info) => {
8737
8955
  * const row = await tx.query.Users.findFirst({ where: { id: { eq } }, with: withTree });
8738
8956
  * ```
8739
8957
  *
8958
+ * Every nested entry carries the build's row policies — `scope`, `softDelete`, `limits` and
8959
+ * `defaults.orderBy` — exactly as the generated `with:` clause does. The build is found
8960
+ * through the resolve info; pass `context` when it configures `scope`.
8961
+ *
8740
8962
  * Returns `undefined` when the selection asks for no relations, which is the value the
8741
8963
  * query builder wants for "no `with` clause".
8742
8964
  */
@@ -8746,11 +8968,16 @@ const selectionToWith = (info, options) => {
8746
8968
  const { tables, relationMap, filterCtx } = contextFor(db);
8747
8969
  if (!tables[table]) throw new Error(`Drizzle-GraphQL Error: '${table}' is not a table in the Drizzle schema.`);
8748
8970
  if (!relationMap[table]) return;
8971
+ const typeName = resolveObjectTypeName(table, typeNameMapper, resolveName);
8972
+ const runtime = runtimeFor(info, db, typeName, "selectionToWith");
8749
8973
  const parsed = parseInfo(info);
8750
- const params = extractRelationsParams(relationMap, tables, table, parsed, resolveObjectTypeName(table, typeNameMapper, resolveName), {
8974
+ const params = extractRelationsParams(relationMap, tables, table, parsed, typeName, {
8751
8975
  typeNameMapper,
8752
- filterCtx,
8753
- resolveName
8976
+ filterCtx: runtime?.filterCtx ?? filterCtx,
8977
+ resolveName,
8978
+ scope: scopeFor(runtime, options, "selectionToWith"),
8979
+ limits: runtime?.limits,
8980
+ defaultOrderBy: runtime?.policies?.defaultOrderBy
8754
8981
  });
8755
8982
  return params && Object.keys(params).length ? params : void 0;
8756
8983
  };
@@ -8762,14 +8989,16 @@ const selectionToWith = (info, options) => {
8762
8989
  * This is what an override usually wants after it has done its own work:
8763
8990
  *
8764
8991
  * ```ts
8765
- * const rows = await resolveSelection(info, { db, table: 'Users', where, executor: tx });
8992
+ * const rows = await resolveSelection(info, { db, table: 'Users', context, where, executor: tx });
8766
8993
  * ```
8767
8994
  *
8768
8995
  * Relation arguments and aggregate fields keep behaving as they do in the generated schema,
8769
- * because it is the generated schema's own code path.
8996
+ * because it is the generated schema's own code path. So do the build's row policies: the
8997
+ * root table and every nested relation are scoped and soft-delete-filtered, and the limit
8998
+ * policy and default ordering apply. Pass `context` when the build configures `scope`.
8770
8999
  */
8771
9000
  const resolveSelection = async (info, options) => {
8772
- const { db, table: tableName, typeNameMapper, executor, single = false, where, orderBy, limit, offset, after } = options;
9001
+ const { db, table: tableName, typeNameMapper, executor, single = false, where, orderBy, limit, offset, after, deleted } = options;
8773
9002
  const resolveName = namingRuleOf(options);
8774
9003
  const { tables, relationMap, filterCtx, primaryKeyOf } = contextFor(db);
8775
9004
  const table = tables[tableName];
@@ -8779,25 +9008,34 @@ const resolveSelection = async (info, options) => {
8779
9008
  const source = executor ?? db;
8780
9009
  const queryBase = source.query?.[tableName];
8781
9010
  if (!queryBase) throw new Error(`Drizzle-GraphQL Error: the executor passed to resolveSelection has no relational query builder for '${tableName}'.`);
9011
+ const typeName = resolveObjectTypeName(tableName, typeNameMapper, resolveName);
9012
+ const runtime = runtimeFor(info, db, typeName, "resolveSelection");
9013
+ const scope = scopeFor(runtime, options, "resolveSelection");
9014
+ const defaultOrderBy = runtime?.policies?.defaultOrderBy;
8782
9015
  return runRelationalSelect({
8783
9016
  queryBase,
8784
9017
  tables,
8785
9018
  tableName,
8786
9019
  table,
8787
9020
  relationMap,
8788
- typeName: resolveObjectTypeName(tableName, typeNameMapper, resolveName),
9021
+ typeName,
8789
9022
  typeNameMapper,
8790
9023
  resolveName,
8791
9024
  parsedInfo,
8792
9025
  single,
8793
9026
  where,
8794
- orderBy,
8795
- limit,
9027
+ orderBy: withDefaultOrderBy({ orderBy }, tableName, defaultOrderBy).orderBy,
9028
+ limit: single ? limit : applyLimitPolicy(limit, runtime?.limits?.(tableName)),
8796
9029
  offset,
8797
9030
  after,
8798
- filterCtx,
9031
+ filterCtx: runtime?.filterCtx ?? filterCtx,
8799
9032
  pkNames: primaryKeyOf(tableName),
8800
- db: source
9033
+ db: source,
9034
+ scope,
9035
+ deleted,
9036
+ limits: runtime?.limits,
9037
+ defaultOrderBy,
9038
+ nullOrdering: runtime?.nullOrdering
8801
9039
  });
8802
9040
  };
8803
9041
  //#endregion