@vantreeseba/drizzle-graphql 15.1.5 → 16.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { Column, One, SQL, Table, aliasedTable, and, arrayContains, arrayOverlaps, asc, avg, count, countDistinct, desc, eq, extractExtendedColumnType, getColumns, getTableAsAliasSQL, gt, gte, ilike, inArray, is, isNotNull, isNull, like, lt, lte, max, min, ne, not, notIlike, notInArray, notLike, or, relationsFilterToSQL, sql, sum } from "drizzle-orm";
2
- import { MySqlAsyncDatabase, MySqlDate, MySqlDateString, MySqlDateTime, MySqlDateTimeString, MySqlInt, MySqlSerial, MySqlTable, MySqlTimestamp, MySqlTimestampString, getTableConfig } from "drizzle-orm/mysql-core";
1
+ import { Column, One, SQL, Subquery, Table, aliasedTable, and, arrayContains, arrayOverlaps, asc, avg, count, countDistinct, desc, eq, extractExtendedColumnType, getColumns, getTableAsAliasSQL, gt, gte, ilike, inArray, is, isNotNull, isNull, like, lt, lte, max, min, ne, not, notIlike, notInArray, notLike, or, relationsFilterToSQL, sql, sum } from "drizzle-orm";
2
+ import { MySqlAsyncDatabase, MySqlDate, MySqlDateString, MySqlDateTime, MySqlDateTimeString, MySqlEnumColumn, MySqlEnumObjectColumn, 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
- import { SQLiteAsyncDatabase, SQLiteInteger, SQLiteTable, SQLiteTimestamp, getTableConfig as getTableConfig$2 } from "drizzle-orm/sqlite-core";
4
+ import { SQLiteAsyncDatabase, SQLiteTable, SQLiteTimestamp, getTableConfig as getTableConfig$2 } from "drizzle-orm/sqlite-core";
5
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";
@@ -262,7 +262,6 @@ const singularizeMapper = (tableName) => ({
262
262
  });
263
263
  //#endregion
264
264
  //#region src/util/type-converter/index.ts
265
- const allowedNameChars = /^[a-zA-Z0-9_]+$/;
266
265
  /** drizzle's `number` constraints whose values always fit a GraphQL `Int` (32-bit signed). */
267
266
  const smallIntegerConstraints = /* @__PURE__ */ new Set([
268
267
  "int8",
@@ -288,8 +287,15 @@ const declaredEnum = (column) => {
288
287
  schema: typeof declared.schema === "string" ? declared.schema : void 0
289
288
  } : void 0;
290
289
  };
291
- /** `user_status` / `user-status` / `user status` → `UserStatus`; an already-PascalCase name is left alone. */
292
- const pascalize = (input) => input.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => capitalize(part)).join("");
290
+ /**
291
+ * `user_status` / `user-status` / `user status` → `UserStatus`; an already-PascalCase name is left
292
+ * alone. A GraphQL name cannot start with a digit, so a name that would (`2fa_mode`) is prefixed
293
+ * with an underscore: `_2faMode`.
294
+ */
295
+ const pascalize = (input) => {
296
+ const pascal = input.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => capitalize(part)).join("");
297
+ return /^[0-9]/.test(pascal) ? `_${pascal}` : pascal;
298
+ };
293
299
  /**
294
300
  * Identity of the values a GraphQL enum was built from, so a name claimed twice can be told
295
301
  * apart from the same enum reached twice.
@@ -364,7 +370,7 @@ const generateEnumCached = (column, columnName, tableName) => {
364
370
  index,
365
371
  typeName: name
366
372
  });
367
- if (mapped === void 0) return allowedNameChars.test(value) ? value : `Option${index}`;
373
+ if (mapped === void 0) return validEnumValueName(value) ? value : `Option${index}`;
368
374
  if (!validEnumValueName(mapped)) throw new Error(`Drizzle-GraphQL Error: config.enumValueNameMapper named value '${value}' of enum '${name}' (on '${tableName}.${columnName}') '${mapped}', which is not a valid GraphQL enum value name. A name must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ and not be true, false or null.`);
369
375
  return mapped;
370
376
  });
@@ -460,6 +466,27 @@ const sqliteTimestampToDate = (column, value) => new Date(column.mode === "times
460
466
  * mode it uses, for the same reason as {@link isDateOnlyColumn}.
461
467
  */
462
468
  const isDateTimeColumn = (column) => is(column, PgTimestamp) || isPgTimestampStringColumn(column) || is(column, MySqlTimestamp) || is(column, MySqlDateTime) || isMySqlDateTimeStringColumn(column) || isSQLiteTimestampColumn(column);
469
+ /**
470
+ * Whether the column holds raw bytes: Postgres `bytea` and SQLite `blob({ mode: 'buffer' })`,
471
+ * both `object buffer` in drizzle v1. Their values are `[Int!]` lists of the bytes.
472
+ */
473
+ const isBufferColumn = (column) => (column.dataType ?? "").endsWith(" buffer");
474
+ /**
475
+ * Whether the column is a float tuple or vector: drizzle v1 gives every such column an
476
+ * `array …` data type — `point` / `line` in tuple mode (`array point`, `array line`), PostGIS
477
+ * `geometry` in tuple mode, pgvector `vector` and `halfvec`. Each value is one list of numbers,
478
+ * typed `[Float!]`, not a database array of elements.
479
+ */
480
+ const isFloatTupleColumn = (column) => (column.dataType ?? "").startsWith("array ");
481
+ /** The fixed length of a float tuple column's values, or `undefined` for a vector. */
482
+ const floatTupleLength = (column) => {
483
+ switch (column.dataType) {
484
+ case "array point":
485
+ case "array geometry": return 2;
486
+ case "array line": return 3;
487
+ default: return;
488
+ }
489
+ };
463
490
  const columnToGraphQLCore = (column, columnName, tableName, isInput) => {
464
491
  const dimensions = column.dimensions;
465
492
  if (dimensions !== void 0 && dimensions > 0) {
@@ -497,7 +524,7 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
497
524
  return column.columnType === "PgGeometryObject" ? {
498
525
  type: isInput ? geoXyInputType : geoXyType,
499
526
  typeLabel: "Geometry points XY"
500
- } : column.columnType === "PgBytea" ? {
527
+ } : isBufferColumn(column) ? {
501
528
  type: new GraphQLList(new GraphQLNonNull(GraphQLInt)),
502
529
  typeLabel: "Buffer"
503
530
  } : {
@@ -530,7 +557,7 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
530
557
  type: GraphQLBigIntString,
531
558
  typeLabel: "BigInt"
532
559
  };
533
- case "number": return constraint !== void 0 && smallIntegerConstraints.has(constraint) || is(column, PgInteger) || is(column, PgSerial) || is(column, MySqlInt) || is(column, MySqlSerial) || is(column, SQLiteInteger) ? {
560
+ case "number": return constraint !== void 0 && smallIntegerConstraints.has(constraint) || is(column, PgInteger) || is(column, PgSerial) || is(column, MySqlInt) || is(column, MySqlSerial) ? {
534
561
  type: GraphQLInt,
535
562
  typeLabel: "Integer"
536
563
  } : {
@@ -538,18 +565,10 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
538
565
  typeLabel: "Float"
539
566
  };
540
567
  case "array": {
541
- if (column.columnType === "PgVector") return {
542
- type: new GraphQLList(new GraphQLNonNull(GraphQLFloat)),
543
- typeLabel: "Array<Float>"
544
- };
545
- if (column.columnType === "PgGeometry") return {
546
- type: new GraphQLList(new GraphQLNonNull(GraphQLFloat)),
547
- typeLabel: "Tuple<[Float, Float]>"
548
- };
549
- const innerType = columnToGraphQLCore(column.baseColumn, columnName, tableName, isInput);
568
+ const length = floatTupleLength(column);
550
569
  return {
551
- type: new GraphQLList(new GraphQLNonNull(innerType.type)),
552
- typeLabel: `Array<${innerType.typeLabel}>`
570
+ type: new GraphQLList(new GraphQLNonNull(GraphQLFloat)),
571
+ typeLabel: length === void 0 ? "Array<Float>" : `Tuple<[${Array(length).fill("Float").join(", ")}]>`
553
572
  };
554
573
  }
555
574
  default: throw new Error(`Drizzle-GraphQL Error: Type ${column.dataType} is not implemented!`);
@@ -691,6 +710,18 @@ const columnDialect = (column) => {
691
710
  */
692
711
  const isJsonColumn = (column) => (column.dataType ?? "").includes("json") && column.columnType !== "PgGeometryObject";
693
712
  /**
713
+ * SQLite `blob({ mode: 'bigint' })`: the bigint is stored as the bytes of its decimal text, and
714
+ * SQLite compares blobs byte by byte, so `9` sorts above `10`. Equality still holds, so it can be
715
+ * grouped and made distinct on, but not ordered or given a min/max.
716
+ */
717
+ const isSqliteBlobBigIntColumn = (column) => column.columnType === "SQLiteBigInt";
718
+ /**
719
+ * Whether `orderBy` (and so cursor pagination) offers a column. JSON has no meaningful order:
720
+ * PostgreSQL `json` cannot be ordered at all, and MySQL's JSON ordering disagrees with any
721
+ * keyset comparison a cursor could make. A SQLite blob bigint orders as text.
722
+ */
723
+ const isOrderableColumn = (column) => !isJsonColumn(column) && !isSqliteBlobBigIntColumn(column);
724
+ /**
694
725
  * Maps a Drizzle column to the generic filter type to use.
695
726
  *
696
727
  * Selection is keyed on the column's data type, never its name: a text column named
@@ -1197,6 +1228,36 @@ const pgTimestampTextToIso = (value) => {
1197
1228
  const utc = (/* @__PURE__ */ new Date(wallMs - offsetSeconds * 1e3)).toISOString();
1198
1229
  return utc.length === 24 ? `${utc.slice(0, 19)}${fraction}Z` : value;
1199
1230
  };
1231
+ /** Marks a decoder installed by {@link installSqliteBlobJsonDecoders}, so a rebuild never wraps it twice. */
1232
+ const BLOB_JSON_DECODER = Symbol.for("drizzle-graphql.sqliteBlobJsonDecoder");
1233
+ /**
1234
+ * Gives SQLite's blob columns a decoder for the relational query builder's JSON path, so a NULL
1235
+ * there reads back as null.
1236
+ *
1237
+ * Inside a nested relation drizzle selects a blob as `hex(column)` (JSON cannot hold a blob), and
1238
+ * `hex(NULL)` is `''`. drizzle then decodes that `''` with the column's ordinary decoder: a
1239
+ * `blob({ mode: 'bigint' })` reads it as `0n` — indistinguishable from a stored 0 — and a
1240
+ * `blob({ mode: 'json' })` throws on it. drizzle consults a column's `mapFromJsonValue`, when it
1241
+ * has one, instead of `mapFromDriverValue` for exactly this path, so one is installed here that
1242
+ * reads `''` as NULL and hands everything else to the column's own decoder.
1243
+ *
1244
+ * `''` is never a stored bigint or JSON blob, so for those it can only be NULL. For a buffer
1245
+ * blob it is also how an empty blob arrives, so it is read as NULL only when the column is
1246
+ * nullable — a `notNull` column cannot hold the NULL, so there it is the empty blob.
1247
+ */
1248
+ const installSqliteBlobJsonDecoders = (tables) => {
1249
+ for (const table of Object.values(tables)) for (const column of Object.values(getColumns(table))) {
1250
+ const columnType = column.columnType ?? "";
1251
+ const isBuffer = columnType === "SQLiteBlobBuffer";
1252
+ if (columnType !== "SQLiteBigInt" && columnType !== "SQLiteBlobJson" && !isBuffer) continue;
1253
+ const existing = column.mapFromJsonValue;
1254
+ if (existing && !existing[BLOB_JSON_DECODER]) continue;
1255
+ const emptyIsNull = !isBuffer || !column.notNull;
1256
+ const decoder = (value) => value === "" && emptyIsNull ? null : column.mapFromDriverValue(value);
1257
+ decoder[BLOB_JSON_DECODER] = true;
1258
+ column.mapFromJsonValue = decoder;
1259
+ }
1260
+ };
1200
1261
  const remapToGraphQLCore = (key, value, tableName, column, relationMap) => {
1201
1262
  if (Array.isArray(value)) {
1202
1263
  const relations = relationMap?.[tableName];
@@ -1223,18 +1284,51 @@ const remapToGraphQLCore = (key, value, tableName, column, relationMap) => {
1223
1284
  if (value instanceof Date) return value.toISOString();
1224
1285
  if (typeof value === "string" && isMySqlDateTimeStringColumn(column)) return mySqlDateTimeTextToIso(value);
1225
1286
  if (typeof value === "string" && isPgTimestampStringColumn(column)) return pgTimestampTextToIso(value);
1226
- if (value instanceof Buffer) return Array.from(value);
1287
+ if (value instanceof Uint8Array) return Array.from(value);
1227
1288
  if (typeof value === "bigint") return value.toString();
1228
1289
  if (Array.isArray(value)) {
1229
- if (column.columnType === "PgGeometry" || column.columnType === "PgVector") return value;
1290
+ if (isFloatTupleColumn(column)) return value;
1230
1291
  return value.map((arrVal) => remapToGraphQLCore(key, arrVal, tableName, column, relationMap));
1231
1292
  }
1232
1293
  if (typeof value === "object" && value !== null) {
1233
- if (column.columnType === "PgGeometryObject") return value;
1294
+ if ((column.dataType ?? "").startsWith("object")) return value;
1234
1295
  return JSON.stringify(value);
1235
1296
  }
1236
1297
  return value;
1237
1298
  };
1299
+ /**
1300
+ * Where a remapped row keeps the values its columns held before remapping, by property name —
1301
+ * only for the columns remapping changed. The output form is not always reversible: a
1302
+ * microsecond timestamp read in `'string'` mode leaves as an ISO string the `DateTime` input
1303
+ * side would cut to milliseconds. A relation field correlating on the row's join columns reads
1304
+ * them from here (see `parentJoinValues`). Non-enumerable, so the row's own keys, JSON form and
1305
+ * equality are unchanged.
1306
+ */
1307
+ const RAW_COLUMN_VALUES = Symbol("drizzle-graphql.rawColumnValues");
1308
+ /**
1309
+ * The value column `key` held in `row` before remapping: `{ found: true, value }` when remapping
1310
+ * changed it, `{ found: false }` when it did not (or the row was never remapped), in which case
1311
+ * `row[key]` is still the column's own value.
1312
+ */
1313
+ const rawColumnValue = (row, key) => {
1314
+ const raw = row[RAW_COLUMN_VALUES];
1315
+ return raw && key in raw ? {
1316
+ found: true,
1317
+ value: raw[key]
1318
+ } : { found: false };
1319
+ };
1320
+ const keepRawColumnValue = (row, key, value) => {
1321
+ let raw = row[RAW_COLUMN_VALUES];
1322
+ if (!raw) {
1323
+ raw = Object.create(null);
1324
+ Object.defineProperty(row, RAW_COLUMN_VALUES, {
1325
+ value: raw,
1326
+ enumerable: false,
1327
+ configurable: true
1328
+ });
1329
+ }
1330
+ if (!(key in raw)) raw[key] = value;
1331
+ };
1238
1332
  const remapToGraphQLSingleOutput = (queryOutput, tableName, table, relationMap) => {
1239
1333
  const columns = getColumns(table);
1240
1334
  for (const [key, value] of Object.entries(queryOutput)) {
@@ -1248,11 +1342,9 @@ const remapToGraphQLSingleOutput = (queryOutput, tableName, table, relationMap)
1248
1342
  continue;
1249
1343
  }
1250
1344
  const column = columns[key];
1251
- if (value === 0n && column && column.columnType === "SQLiteBigInt" && !column.notNull) {
1252
- delete queryOutput[key];
1253
- continue;
1254
- }
1255
- queryOutput[key] = remapToGraphQLCore(key, value, tableName, column, relationMap);
1345
+ const remapped = remapToGraphQLCore(key, value, tableName, column, relationMap);
1346
+ if (column && remapped !== value) keepRawColumnValue(queryOutput, key, value);
1347
+ queryOutput[key] = remapped;
1256
1348
  }
1257
1349
  return queryOutput;
1258
1350
  };
@@ -1269,6 +1361,7 @@ const remapFromGraphQLCore = (value, column, columnName) => {
1269
1361
  const dataType = column.dataType ?? "";
1270
1362
  const columnType = column.columnType ?? "";
1271
1363
  if (isDateTimeColumn(column)) {
1364
+ if (Array.isArray(value)) return value.map((element) => remapFromGraphQLCore(element, column, columnName));
1272
1365
  const isMySql = columnType.startsWith("MySql");
1273
1366
  const isPgString = isPgTimestampStringColumn(column);
1274
1367
  const toIso = isMySql ? mySqlDateTimeTextToIso : isPgString ? pgTimestampTextToIso : void 0;
@@ -1283,41 +1376,29 @@ const remapFromGraphQLCore = (value, column, columnName) => {
1283
1376
  if (typeof day !== "string" || !isCalendarDate(day)) throw drizzleError(`Field '${columnName}' is not a valid date! Expected a calendar date as YYYY-MM-DD.`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1284
1377
  return day;
1285
1378
  }
1286
- if (dataType.includes("bigint")) try {
1287
- return BigInt(value);
1288
- } catch {
1289
- throw drizzleError(`Field '${columnName}' is not a BigInt!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1290
- }
1291
- if (dataType.includes("json") && column.columnType !== "PgGeometryObject") return value;
1292
- switch (dataType) {
1293
- case "date": {
1294
- const formatted = new Date(value);
1295
- if (Number.isNaN(formatted.getTime())) throw drizzleError(`Field '${columnName}' is not a valid date!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1296
- return formatted;
1297
- }
1298
- case "buffer":
1299
- if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1300
- return Buffer.from(value);
1301
- case "json":
1302
- if (column.columnType === "PgGeometryObject") return value;
1303
- try {
1304
- return JSON.parse(value);
1305
- } catch (e) {
1306
- throw drizzleError(`Invalid JSON in field '${columnName}':\n${e instanceof Error ? e.message : "Unknown error"}`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1307
- }
1308
- case "array":
1309
- if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1310
- if (column.columnType === "PgGeometry" && value.length !== 2) throw drizzleError(`Invalid float tuple in field '${columnName}': expected array with length of 2, received ${value.length}`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1311
- return value;
1312
- case "bigint": try {
1379
+ if (dataType.includes("bigint")) {
1380
+ if (Array.isArray(value)) return value.map((element) => remapFromGraphQLCore(element, column, columnName));
1381
+ try {
1313
1382
  return BigInt(value);
1314
- } catch (_error) {
1383
+ } catch {
1315
1384
  throw drizzleError(`Field '${columnName}' is not a BigInt!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1316
1385
  }
1317
- default:
1318
- if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === null) return Object.assign({}, value);
1319
- return value;
1320
1386
  }
1387
+ if (isJsonColumn(column)) return value;
1388
+ if (isBufferColumn(column)) {
1389
+ if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1390
+ if (column.dimensions > 0 && value.every((element) => element === null || Array.isArray(element))) return value.map((element) => remapFromGraphQLCore(element, column, columnName));
1391
+ if (!value.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) throw drizzleError(`Field '${columnName}' is not a list of bytes! Expected integers from 0 to 255.`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1392
+ return Buffer.from(value);
1393
+ }
1394
+ if (isFloatTupleColumn(column)) {
1395
+ if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1396
+ const length = floatTupleLength(column);
1397
+ if (length !== void 0 && value.length !== length) throw drizzleError(`Invalid float tuple in field '${columnName}': expected array with length of ${length}, received ${value.length}`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1398
+ return value;
1399
+ }
1400
+ if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === null) return Object.assign({}, value);
1401
+ return value;
1321
1402
  };
1322
1403
  const remapFromGraphQLSingleInput = (queryInput, table, operation = "insert") => {
1323
1404
  const columns = getColumns(table);
@@ -1359,36 +1440,60 @@ const escapeLikeValue = (value) => value.replace(/[\\%_]/g, (char) => `\\${char}
1359
1440
  const safeLikeOps = {
1360
1441
  startsWith: {
1361
1442
  buildPattern: (value) => `${escapeLikeValue(value)}%`,
1443
+ buildGlob: (value) => `${escapeGlobValue(value)}*`,
1362
1444
  insensitive: false
1363
1445
  },
1364
1446
  endsWith: {
1365
1447
  buildPattern: (value) => `%${escapeLikeValue(value)}`,
1448
+ buildGlob: (value) => `*${escapeGlobValue(value)}`,
1366
1449
  insensitive: false
1367
1450
  },
1368
1451
  contains: {
1369
1452
  buildPattern: (value) => `%${escapeLikeValue(value)}%`,
1453
+ buildGlob: (value) => `*${escapeGlobValue(value)}*`,
1370
1454
  insensitive: false
1371
1455
  },
1372
1456
  iStartsWith: {
1373
1457
  buildPattern: (value) => `${escapeLikeValue(value)}%`,
1458
+ buildGlob: (value) => `${escapeGlobValue(value)}*`,
1374
1459
  insensitive: true
1375
1460
  },
1376
1461
  iEndsWith: {
1377
1462
  buildPattern: (value) => `%${escapeLikeValue(value)}`,
1463
+ buildGlob: (value) => `*${escapeGlobValue(value)}`,
1378
1464
  insensitive: true
1379
1465
  },
1380
1466
  iContains: {
1381
1467
  buildPattern: (value) => `%${escapeLikeValue(value)}%`,
1468
+ buildGlob: (value) => `*${escapeGlobValue(value)}*`,
1382
1469
  insensitive: true
1383
1470
  }
1384
1471
  };
1385
1472
  /**
1386
- * `LIKE <pattern> ESCAPE '\'` for a safe string operator. Case-insensitive variants use
1387
- * Postgres's native `ILIKE`; MySQL and SQLite have no `ILIKE`, so they compare `lower()`
1388
- * on both sides instead (mirroring how the raw `ilike` operator is Postgres-only).
1473
+ * Escapes SQLite's `GLOB` wildcards (`*`, `?`, `[`) in a literal search term by wrapping each in
1474
+ * a one-character class, so the term only ever matches literally inside a GLOB pattern. GLOB
1475
+ * has no escape character; `]` outside a class and everything else is already literal.
1389
1476
  */
1390
- const safeLikeCondition = (column, pattern, insensitive) => {
1391
- if (!insensitive) return sql`${column} like ${pattern} escape ${LIKE_ESCAPE_CHAR}`;
1477
+ const escapeGlobValue = (value) => value.replace(/[*?[]/g, (char) => `[${char}]`);
1478
+ /**
1479
+ * A caller-written LIKE pattern as the GLOB pattern matching the same strings, for SQLite's
1480
+ * case-sensitive `like` / `notLike`: `%` → `*`, `_` → `?`, and GLOB's own wildcards escaped.
1481
+ * The raw operators bind no `ESCAPE` clause, so `\` is an ordinary character in both.
1482
+ */
1483
+ const likePatternToGlob = (pattern) => pattern.replace(/[%_*?[]/g, (char) => char === "%" ? "*" : char === "_" ? "?" : `[${char}]`);
1484
+ /**
1485
+ * The safe string operator's predicate. Case-insensitive variants use Postgres's native
1486
+ * `ILIKE`; MySQL and SQLite have no `ILIKE`, so they compare `lower()` on both sides instead
1487
+ * (mirroring how the raw `ilike` operator is Postgres-only).
1488
+ *
1489
+ * The case-sensitive variants are `LIKE <pattern> ESCAPE '\'`, except on SQLite, whose `LIKE`
1490
+ * folds ASCII case — `contains: "App"` would match `apple`. There they are `GLOB`, which is
1491
+ * case-sensitive, as `LIKE` is on Postgres. `dialectColumn` names the column the dialect is read
1492
+ * from when `column` is an expression over it (a JSON path read).
1493
+ */
1494
+ const safeLikeCondition = (column, value, op, insensitive, dialectColumn = column) => {
1495
+ const pattern = op.buildPattern(value);
1496
+ if (!insensitive) return columnDialect(dialectColumn) === "sqlite" ? sql`${column} glob ${op.buildGlob(value)}` : sql`${column} like ${pattern} escape ${LIKE_ESCAPE_CHAR}`;
1392
1497
  return (column.columnType ?? "").startsWith("Pg") ? sql`${column} ilike ${pattern} escape ${LIKE_ESCAPE_CHAR}` : sql`lower(${column}) like ${pattern.toLowerCase()} escape ${LIKE_ESCAPE_CHAR}`;
1393
1498
  };
1394
1499
  /**
@@ -1444,6 +1549,35 @@ const jsonContains = (column, columnName, value) => {
1444
1549
  }
1445
1550
  };
1446
1551
  /**
1552
+ * The nodes of a SQLite JSON document as `(path, kind, value)` rows, for a structural
1553
+ * comparison. Integers and reals share one kind so `1` and `1.0` are the same number, as they
1554
+ * are on the other two dialects.
1555
+ */
1556
+ const sqliteJsonNodes = (document) => sql`select fullkey, case when type in ('integer', 'real') then 'number' else type end, atom from json_tree(${document})`;
1557
+ /**
1558
+ * Whole-document `eq` / `ne` on a JSON column, comparing documents rather than their text:
1559
+ * - Postgres: as `jsonb` (a plain `json` column has no `=` operator at all)
1560
+ * - MySQL: against `CAST(? AS JSON)`; a bare string parameter would be a JSON string scalar
1561
+ * - SQLite: the two documents' node sets must match both ways, so object key order and
1562
+ * whitespace do not matter, while array order still does
1563
+ * A SQL NULL column matches neither, as with `=` / `<>` on any other column.
1564
+ */
1565
+ const jsonEquality = (column, columnName, value, negated) => {
1566
+ const serialized = JSON.stringify(value);
1567
+ switch (columnDialect(column)) {
1568
+ case "pg": {
1569
+ const left = column.columnType === "PgJson" ? sql`${column}::jsonb` : sql`${column}`;
1570
+ return negated ? sql`${left} <> ${serialized}::jsonb` : sql`${left} = ${serialized}::jsonb`;
1571
+ }
1572
+ case "mysql": return negated ? sql`${column} <> cast(${serialized} as json)` : sql`${column} = cast(${serialized} as json)`;
1573
+ case "sqlite": {
1574
+ const equal = sql`(not exists (${sqliteJsonNodes(column)} except ${sqliteJsonNodes(serialized)}) and not exists (${sqliteJsonNodes(serialized)} except ${sqliteJsonNodes(column)}))`;
1575
+ return negated ? sql`(${column} is not null and not ${equal})` : sql`(${column} is not null and ${equal})`;
1576
+ }
1577
+ default: throw drizzleError(`WHERE ${columnName}: JSON equality is not supported for this dialect!`, { code: "DRIZZLE_INVALID_FILTER" });
1578
+ }
1579
+ };
1580
+ /**
1447
1581
  * Constant predicates, used where an operator's operand list is empty and the answer is
1448
1582
  * therefore known without touching the column. Written as `1 = 0` / `1 = 1` rather than the
1449
1583
  * `FALSE` / `TRUE` keywords so they compile identically on all three dialects. Built fresh
@@ -1478,7 +1612,8 @@ const NUMERIC_TEXT_PATTERN = "^\\s*-?(\\d+\\.?\\d*|\\.\\d+)([eE][-+]?\\d+)?\\s*$
1478
1612
  * pattern guard.
1479
1613
  * - **SQLite** — `json_extract` returns the value in SQLite's own type, so the reads cast
1480
1614
  * explicitly: without that, SQLite's cross-type ordering puts every string above every
1481
- * number and `'admin' > 0` would be true.
1615
+ * number and `'admin' > 0` would be true. A JSON boolean comes back as 1 / 0, so every read
1616
+ * also checks `json_type` to keep booleans and numbers apart.
1482
1617
  */
1483
1618
  const jsonPathExprs = (column, path) => {
1484
1619
  switch (columnDialect(column)) {
@@ -1504,10 +1639,11 @@ const jsonPathExprs = (column, path) => {
1504
1639
  }
1505
1640
  default: {
1506
1641
  const extracted = sql`json_extract(${column}, ${jsonPathString(path)})`;
1642
+ const type = sql`json_type(${column}, ${jsonPathString(path)})`;
1507
1643
  return {
1508
- text: sql`cast(${extracted} as text)`,
1509
- number: sql`(case when typeof(${extracted}) in ('integer', 'real') then ${extracted} end)`,
1510
- boolean: extracted,
1644
+ text: sql`(case ${type} when 'true' then 'true' when 'false' then 'false' else cast(${extracted} as text) end)`,
1645
+ number: sql`(case when ${type} in ('integer', 'real') then ${extracted} end)`,
1646
+ boolean: sql`(case when ${type} in ('true', 'false') then ${extracted} end)`,
1511
1647
  encodeBoolean: (value) => value ? 1 : 0
1512
1648
  };
1513
1649
  }
@@ -1546,9 +1682,9 @@ const jsonPathCondition = (column, columnName, filter) => {
1546
1682
  continue;
1547
1683
  }
1548
1684
  if (operatorName in safeLikeOps) {
1549
- const { buildPattern, insensitive } = safeLikeOps[operatorName];
1685
+ const op = safeLikeOps[operatorName];
1550
1686
  if (typeof operatorValue !== "string") throw drizzleError(`WHERE ${locator}: operator '${operatorName}' takes a string`, { code: "DRIZZLE_INVALID_FILTER" });
1551
- variants.push(safeLikeCondition(exprs.text, buildPattern(operatorValue), insensitive));
1687
+ variants.push(safeLikeCondition(exprs.text, operatorValue, op, op.insensitive, column));
1552
1688
  continue;
1553
1689
  }
1554
1690
  const comparison = JSON_PATH_COMPARISONS[operatorName];
@@ -1606,20 +1742,25 @@ const extractFiltersColumn = (column, columnName, operators) => {
1606
1742
  for (const [operatorName, operatorValue] of entries) {
1607
1743
  if (operatorValue === null || operatorValue === void 0) continue;
1608
1744
  if (operatorValue === false && FLAG_OPERATORS.has(operatorName)) continue;
1609
- if (operatorName in singleValueOps) {
1745
+ if ((operatorName === "eq" || operatorName === "ne") && isJsonColumn(column)) variants.push(jsonEquality(column, columnName, operatorValue, operatorName === "ne"));
1746
+ else if (operatorName in singleValueOps) {
1610
1747
  const singleValue = remapFromGraphQLCore(operatorValue, column, columnName);
1611
1748
  variants.push(foldCase ? lowerComparison(column, operatorName, singleValue) : singleValueOps[operatorName](column, singleValue));
1612
1749
  } else if (operatorName in stringValueOps) {
1613
- const insensitivePattern = foldCase && (operatorName === "like" || operatorName === "notLike");
1614
- variants.push(insensitivePattern ? insensitiveLike(column, operatorValue, operatorName === "notLike") : stringValueOps[operatorName](column, operatorValue));
1750
+ const isLike = operatorName === "like" || operatorName === "notLike";
1751
+ if (isLike && foldCase || (operatorName === "ilike" || operatorName === "notIlike") && columnDialect(column) !== "pg") variants.push(insensitiveLike(column, operatorValue, operatorName === "notLike" || operatorName === "notIlike"));
1752
+ else if (isLike && columnDialect(column) === "sqlite") {
1753
+ const glob = likePatternToGlob(operatorValue);
1754
+ variants.push(operatorName === "like" ? sql`${column} glob ${glob}` : sql`${column} not glob ${glob}`);
1755
+ } else variants.push(stringValueOps[operatorName](column, operatorValue));
1615
1756
  } else if (operatorName === "path" && isJsonColumn(column)) for (const pathFilter of operatorValue) {
1616
1757
  const extracted = jsonPathCondition(column, columnName, pathFilter);
1617
1758
  if (extracted) variants.push(extracted);
1618
1759
  }
1619
1760
  else if (operatorName === "contains" && isJsonColumn(column)) variants.push(jsonContains(column, columnName, operatorValue));
1620
1761
  else if (operatorName in safeLikeOps) {
1621
- const { buildPattern, insensitive: alwaysInsensitive } = safeLikeOps[operatorName];
1622
- variants.push(safeLikeCondition(column, buildPattern(operatorValue), alwaysInsensitive || foldCase));
1762
+ const op = safeLikeOps[operatorName];
1763
+ variants.push(safeLikeCondition(column, operatorValue, op, op.insensitive || foldCase));
1623
1764
  } else if (operatorName in arrayValueOps) {
1624
1765
  if (!operatorValue.length) {
1625
1766
  variants.push(operatorName === "inArray" ? sqlFalse() : sqlTrue());
@@ -1627,13 +1768,13 @@ const extractFiltersColumn = (column, columnName, operators) => {
1627
1768
  }
1628
1769
  const arrayValue = operatorValue.map((val) => remapFromGraphQLCore(val, column, columnName));
1629
1770
  variants.push(foldCase ? lowerMembership(column, arrayValue, operatorName === "notInArray") : arrayValueOps[operatorName](column, arrayValue));
1630
- } else if (operatorName === "has") variants.push(arrayContains(column, [operatorValue]));
1771
+ } else if (operatorName === "has") variants.push(arrayContains(column, [remapFromGraphQLCore(operatorValue, column, columnName)]));
1631
1772
  else if (operatorName in arrayMembershipOps) {
1632
1773
  if (!operatorValue.length) {
1633
1774
  variants.push(operatorName === "hasSome" ? sqlFalse() : sqlTrue());
1634
1775
  continue;
1635
1776
  }
1636
- variants.push(arrayMembershipOps[operatorName](column, operatorValue));
1777
+ variants.push(arrayMembershipOps[operatorName](column, operatorValue.map((element) => remapFromGraphQLCore(element, column, columnName))));
1637
1778
  } else if (operatorName === "isEmpty") variants.push(sql`cardinality(${column}) = 0`);
1638
1779
  else if (operatorName in nullableOps) variants.push(nullableOps[operatorName](column));
1639
1780
  else throw drizzleError(`WHERE ${columnName}: Unknown operator: ${operatorName}`, { code: "DRIZZLE_INVALID_FILTER" });
@@ -1831,17 +1972,104 @@ const extractRelationJoinColumnPairs = (relEntry, parentTable, targetTable) => {
1831
1972
  }
1832
1973
  return pairs;
1833
1974
  };
1975
+ /** A decimal written as text, in a form `canonicalDecimal` can normalise. */
1976
+ const DECIMAL_TEXT = /^([+-]?)(\d*)(?:\.(\d*))?$/;
1977
+ /**
1978
+ * Decimal text without the zeros that do not change its value: `1.500` and `01.5` are both
1979
+ * `1.5`, as SQL compares them. Anything else (an exponent, `NaN`) is returned unchanged.
1980
+ */
1981
+ const canonicalDecimal = (value) => {
1982
+ const match = DECIMAL_TEXT.exec(value);
1983
+ if (!match || !match[2] && !match[3]) return value;
1984
+ const whole = match[2].replace(/^0+(?=\d)/, "") || "0";
1985
+ const fraction = (match[3] ?? "").replace(/0+$/, "");
1986
+ const digits = fraction ? `${whole}.${fraction}` : whole;
1987
+ return match[1] === "-" && digits !== "0" ? `-${digits}` : digits;
1988
+ };
1989
+ /** An ISO instant without trailing zeros in its fraction: `…:00.100Z` and `…:00.1Z` agree. */
1990
+ const trimIsoFraction = (iso) => iso.replace(/(\.\d*?)0+Z$/, "$1Z").replace(/\.Z$/, "Z");
1991
+ /**
1992
+ * One join value in a form both sides of a relation produce for the same SQL value. The parent's
1993
+ * key and a related row's can arrive in different JS forms — a `Date` and the text of the same
1994
+ * instant, `1.50` and `1.500` from decimals of different scale — and `String()` would tell them
1995
+ * apart (it even drops a `Date`'s milliseconds).
1996
+ */
1997
+ const joinValueKey = (value, column) => {
1998
+ if (value instanceof Date) {
1999
+ if (Number.isNaN(value.getTime())) return String(value);
2000
+ if (column && isDateOnlyColumn(column)) return dateToCalendarDate(value) ?? value.toISOString();
2001
+ return trimIsoFraction(value.toISOString());
2002
+ }
2003
+ if (typeof value === "bigint") return value.toString();
2004
+ if (value instanceof Uint8Array) return Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString("hex");
2005
+ if (typeof value === "string" && column) {
2006
+ if (isMySqlDateTimeStringColumn(column)) return trimIsoFraction(mySqlDateTimeTextToIso(value));
2007
+ if (isPgTimestampStringColumn(column)) return trimIsoFraction(pgTimestampTextToIso(value));
2008
+ if ((column.dataType ?? "") === "string numeric") return canonicalDecimal(value);
2009
+ }
2010
+ return String(value);
2011
+ };
1834
2012
  /**
1835
2013
  * A string identifying one combination of join-column values, for grouping a batch's rows by
1836
- * parent. Values are compared by their string form, as a single-column key always has been.
2014
+ * parent. `columns` are the columns the values were read from, in the same order: the parent's
2015
+ * join columns for a parent key, the target's (or the junction's) for a related row. Values the
2016
+ * database compares equal give the same key — see {@link joinValueKey}.
2017
+ */
2018
+ const joinKeyOf = (values, columns) => values.length === 1 ? joinValueKey(values[0], columns?.[0]) : JSON.stringify(values.map((value, i) => joinValueKey(value, columns?.[i])));
2019
+ /**
2020
+ * The parent row's values for the relation's join columns, as the database holds them. The row
2021
+ * a relation field resolves under has been remapped to its GraphQL form — a `Date` is an ISO
2022
+ * string by then, a `bigint` a decimal string — which can neither be bound back through every
2023
+ * column's encoder (SQLite's integer timestamp calls `getTime()` on it) nor be compared with the
2024
+ * related rows' decoded values. The values from before remapping are used when the row kept
2025
+ * them; otherwise the output form is mapped back the way an input value would be.
2026
+ */
2027
+ const parentJoinValues = (parent, pairs) => pairs.map(({ localColPropName, localCol }) => {
2028
+ const raw = rawColumnValue(parent, localColPropName);
2029
+ if (raw.found) return raw.value;
2030
+ const value = parent[localColPropName];
2031
+ if (value == null) return value;
2032
+ try {
2033
+ return remapFromGraphQLCore(value, localCol, localColPropName);
2034
+ } catch {
2035
+ return value;
2036
+ }
2037
+ });
2038
+ /**
2039
+ * At most this many bound parameters for the parent keys of one batched relation query. SQLite
2040
+ * accepts 32,766 per statement, Postgres and MySQL 65,535; the rest is left to the request's own
2041
+ * filters. A batch with more parents runs as several queries.
2042
+ */
2043
+ const MAX_PARENT_KEY_PARAMS = 1e4;
2044
+ /**
2045
+ * The parent keys of one batch split into the groups each query reads, so that no query binds
2046
+ * more than {@link MAX_PARENT_KEY_PARAMS} key values. Every parent lands in exactly one group, so
2047
+ * a per-parent result — its rows, its window, its count — is the same whichever query made it.
2048
+ */
2049
+ const parentKeyChunks = (keys, width) => {
2050
+ const size = Math.max(1, Math.floor(MAX_PARENT_KEY_PARAMS / Math.max(1, width)));
2051
+ const chunks = [];
2052
+ for (let i = 0; i < keys.length; i += size) chunks.push(keys.slice(i, i + size));
2053
+ return chunks;
2054
+ };
2055
+ /**
2056
+ * `or` over `conditions` as a balanced tree. A flat `a OR b OR c …` is parsed left-deep, and
2057
+ * SQLite refuses an expression more than 1,000 levels deep; balanced, a batch of thousands of
2058
+ * parents stays a few levels deep.
1837
2059
  */
1838
- const joinKeyOf = (values) => values.length === 1 ? String(values[0]) : JSON.stringify(values.map((value) => String(value)));
2060
+ const balancedOr = (conditions) => {
2061
+ if (conditions.length <= 2) return or(...conditions);
2062
+ const middle = Math.ceil(conditions.length / 2);
2063
+ return or(balancedOr(conditions.slice(0, middle)), balancedOr(conditions.slice(middle)));
2064
+ };
1839
2065
  /**
1840
2066
  * The rows whose `cols` hold one of `keys`: `col IN (…)` for a single join column, and an `OR`
1841
2067
  * of per-parent equalities for several — `(a = 1 AND b = 1) OR (a = 1 AND b = 2)` — which
1842
- * every dialect accepts, unlike a row-value `IN` list.
2068
+ * every dialect accepts, unlike a row-value `IN` list. The `OR` is nested as a balanced tree.
2069
+ * Callers split a large batch with {@link parentKeyChunks} first, which keeps either form within
2070
+ * the dialects' parameter limits.
1843
2071
  */
1844
- const parentKeyCondition = (cols, keys) => cols.length === 1 ? inArray(cols[0], keys.map((key) => key[0])) : or(...keys.map((key) => and(...cols.map((col, i) => eq(col, key[i])))));
2072
+ const parentKeyCondition = (cols, keys) => cols.length === 1 ? inArray(cols[0], keys.map((key) => key[0])) : balancedOr(keys.map((key) => and(...cols.map((col, i) => eq(col, key[i])))));
1845
2073
  /**
1846
2074
  * The junction table and its two columns for a `.through()` (many-to-many) relation:
1847
2075
  * `sourceCol` holds the parent's key, `targetCol` the target's. `undefined` for a relation
@@ -2394,6 +2622,32 @@ const withScope = (scope, tableName, table, condition, mode) => {
2394
2622
  const predicate = scope?.on(tableName, table, mode);
2395
2623
  return predicate ? and(condition, predicate) : condition;
2396
2624
  };
2625
+ /**
2626
+ * The junction table a `.through()` relation links through, by its schema key — the key its
2627
+ * `scope` and `softDelete` are configured under. `undefined` for a relation with no junction,
2628
+ * or a junction that is not one of the build's tables.
2629
+ */
2630
+ const junctionTableName = (tables, relEntry) => {
2631
+ const throughTable = (relEntry?.relation ?? relEntry)?.throughTable;
2632
+ return throughTable ? Object.keys(tables).find((name) => tables[name] === throughTable) : void 0;
2633
+ };
2634
+ /**
2635
+ * Whether a `.through()` relation's junction restricts which links exist for this request —
2636
+ * a `scope` on the junction table, or a soft delete it declares.
2637
+ */
2638
+ const hasJunctionPolicy = (scope, junctionName) => !!junctionName && !!scope?.has(junctionName, "EXCLUDE");
2639
+ /**
2640
+ * The junction's own row policy, compiled against the junction table as the query names it.
2641
+ * A `.through()` relation links only through the junction rows this keeps, on every path that
2642
+ * reads the relation — the relation field, `<relation>Aggregate`, and a relation filter — so
2643
+ * they all agree on which rows are related.
2644
+ *
2645
+ * The junction is always read at `EXCLUDE`: a marked junction row is a removed link. The
2646
+ * relation's `deleted` argument is about the *target* rows, and the junction's own
2647
+ * `softDelete.scope` only governs reads of the junction table's own fields — a link row has no
2648
+ * field of its own on the relation that `'root'` could keep rendering.
2649
+ */
2650
+ const junctionPolicy = (scope, junctionName, table) => hasJunctionPolicy(scope, junctionName) ? scope.on(junctionName, table, "EXCLUDE") : void 0;
2397
2651
  /** Binds a build's {@link TablePolicies} to its relation context, once, at schema-build time. */
2398
2652
  const bindPolicies = (policies, filterCtx) => policies?.scope || policies?.contextValues || policies?.softDelete || policies?.onWrite || policies?.defaultOrderBy ? {
2399
2653
  scope: policies.scope || policies.softDelete ? (context) => resolveScope(policies, context, filterCtx) : void 0,
@@ -2444,7 +2698,7 @@ const deletedArg = (softDelete, tableName, resolveName) => softDelete?.(tableNam
2444
2698
  const selectArrayArgs = (orderArgs, filterArgs, distinctEnum, softDelete, tableName, resolveName) => ({
2445
2699
  offset: { type: GraphQLInt },
2446
2700
  limit: { type: GraphQLInt },
2447
- orderBy: { type: orderArgs },
2701
+ ...orderArgs ? { orderBy: { type: orderArgs } } : {},
2448
2702
  where: { type: filterArgs },
2449
2703
  after: {
2450
2704
  type: GraphQLString,
@@ -2456,7 +2710,7 @@ const selectArrayArgs = (orderArgs, filterArgs, distinctEnum, softDelete, tableN
2456
2710
  /** GraphQL argument map for a single-row select field (no `limit`). */
2457
2711
  const selectSingleArgs = (orderArgs, filterArgs, softDelete, tableName, resolveName) => ({
2458
2712
  offset: { type: GraphQLInt },
2459
- orderBy: { type: orderArgs },
2713
+ ...orderArgs ? { orderBy: { type: orderArgs } } : {},
2460
2714
  where: { type: filterArgs },
2461
2715
  ...deletedArg(softDelete, tableName, resolveName)
2462
2716
  });
@@ -2506,7 +2760,7 @@ const generateDistinctEnum = (table, typeName, resolveName, tableKey) => {
2506
2760
  defaultName,
2507
2761
  table: tableKey,
2508
2762
  operation: "distinct"
2509
- }) ?? defaultName, `Columns of ${typeName} that a query can be made distinct on`);
2763
+ }) ?? defaultName, `Columns of ${typeName} that a query can be made distinct on`, (column) => !isJsonColumn(column));
2510
2764
  };
2511
2765
  //#endregion
2512
2766
  //#region src/util/builders/common/conflict.ts
@@ -2676,6 +2930,44 @@ const resolveConflictPlan = (params) => {
2676
2930
  plan
2677
2931
  }));
2678
2932
  };
2933
+ /**
2934
+ * Adds a {@link ConflictPlan}'s clause to a PostgreSQL or SQLite insert.
2935
+ *
2936
+ * SQLite needs two things drizzle's builder does not give it when the target is a partial
2937
+ * unique index:
2938
+ *
2939
+ * - The index predicate written literally. SQLite matches a conflict target to an index by
2940
+ * comparing the predicates as written, and a bound `?` never equals `'active'`, so a
2941
+ * predicate that compares to a value would never find its index. Its params are inlined.
2942
+ * - A `DO NOTHING` it can parse. Drizzle writes the predicate after `do nothing`, which is a
2943
+ * syntax error, so the action is spelled as an update that matches no row instead:
2944
+ * `ON CONFLICT (…) WHERE … DO UPDATE SET col = col WHERE 0 = 1`. A row that is not updated is
2945
+ * not returned, which is what `DO NOTHING` does too.
2946
+ */
2947
+ const applyConflictPlan = (query, table, plan, setWhere) => {
2948
+ const sqlite = is(table, SQLiteTable);
2949
+ const targetWhere = sqlite && plan.targetWhere ? sql`${plan.targetWhere}`.inlineParams() : plan.targetWhere;
2950
+ if (plan.action === "UPDATE") return query.onConflictDoUpdate({
2951
+ target: plan.target,
2952
+ targetWhere,
2953
+ set: plan.set,
2954
+ setWhere
2955
+ });
2956
+ if (!plan.target) return query.onConflictDoNothing();
2957
+ if (sqlite && targetWhere) {
2958
+ const [key] = Object.entries(getColumns(table)).find(([, column]) => column === plan.target[0]);
2959
+ return query.onConflictDoUpdate({
2960
+ target: plan.target,
2961
+ targetWhere,
2962
+ set: { [key]: sql`${plan.target[0]}` },
2963
+ setWhere: sql`0 = 1`
2964
+ });
2965
+ }
2966
+ return query.onConflictDoNothing({
2967
+ target: plan.target,
2968
+ where: targetWhere
2969
+ });
2970
+ };
2679
2971
  /** `excluded.<column>` — PostgreSQL and SQLite name the rejected row this way. */
2680
2972
  const excludedColumnRef = (columnName) => sql`excluded.${sql.identifier(columnName)}`;
2681
2973
  /** `values(<column>)` — MySQL's equivalent inside ON DUPLICATE KEY UPDATE. */
@@ -2957,6 +3249,21 @@ const decodeCursor = (after, entries) => {
2957
3249
  return payload.v.map(decodeCursorValue);
2958
3250
  };
2959
3251
  /**
3252
+ * `col > value` (asc) or `col < value` (desc), compared by the rule the ORDER BY sorts by.
3253
+ * MySQL sorts an ENUM by its declaration index but compares it with a string as a string, so
3254
+ * there the comparison is between `col + 0` (the 1-based index) and the value's own index.
3255
+ */
3256
+ const strictComparison = (col, direction, value) => {
3257
+ if (is(col, MySqlEnumColumn) || is(col, MySqlEnumObjectColumn)) {
3258
+ const index = col.enumValues?.indexOf(value) ?? -1;
3259
+ if (index >= 0) {
3260
+ const position = sql`(${col} + 0)`;
3261
+ return direction === "asc" ? sql`${position} > ${index + 1}` : sql`${position} < ${index + 1}`;
3262
+ }
3263
+ }
3264
+ return direction === "asc" ? gt(col, value) : lt(col, value);
3265
+ };
3266
+ /**
2960
3267
  * The keyset predicate selecting rows strictly after the cursor position in the given total
2961
3268
  * order — the expanded lexicographic form
2962
3269
  * `after(k0) OR (k0 = v0 AND after(k1)) OR (k0 = v0 AND k1 = v1 AND after(k2)) …`,
@@ -2981,7 +3288,7 @@ const buildCursorCondition = (table, entries, values, nullOrdering) => {
2981
3288
  let strictlyAfter;
2982
3289
  if (value === null || value === void 0) strictlyAfter = nullsSortLast ? void 0 : isNotNull(col);
2983
3290
  else {
2984
- const comparison = direction === "asc" ? gt(col, value) : lt(col, value);
3291
+ const comparison = strictComparison(col, direction, value);
2985
3292
  strictlyAfter = nullsSortLast ? or(comparison, isNull(col)) : comparison;
2986
3293
  }
2987
3294
  if (strictlyAfter) disjuncts.push(equalities.length ? and(...equalities, strictlyAfter) : strictlyAfter);
@@ -3184,6 +3491,115 @@ const getUniqueTargets = (table, getTableConfig) => {
3184
3491
  */
3185
3492
  const getUniqueColumnSets = (table, getTableConfig, options = {}) => getUniqueTargets(table, getTableConfig).filter((target) => options.partial || !target.where).map((target) => target.columns);
3186
3493
  //#endregion
3494
+ //#region src/util/builders/common/limits.ts
3495
+ /**
3496
+ * The `limit` to send with an `offset` the request gave without one. SQLite and MySQL accept
3497
+ * `OFFSET` only after a `LIMIT`, so `offset` alone has to carry a limit that means "all of
3498
+ * them": SQLite reads `LIMIT -1` that way, and on MySQL it is the largest integer every driver
3499
+ * binds exactly (MySQL's own documented 2^64-1 does not fit a JS number). PostgreSQL takes
3500
+ * `OFFSET` alone, so the limit is left as it was.
3501
+ */
3502
+ const offsetOnlyLimit = (table, limit, offset) => {
3503
+ if (limit != null || !offset) return limit;
3504
+ const firstColumn = Object.values(getColumns(table))[0];
3505
+ switch (firstColumn ? columnDialect(firstColumn) : void 0) {
3506
+ case "sqlite": return -1;
3507
+ case "mysql": return Number.MAX_SAFE_INTEGER;
3508
+ default: return limit;
3509
+ }
3510
+ };
3511
+ /**
3512
+ * Substitutes a table's default ordering for an absent `orderBy`.
3513
+ *
3514
+ * Applied where the arguments are first read, so everything downstream — the cursor tuple, a
3515
+ * `distinct` pass, the plain-select fallback — sees one effective ordering and cannot
3516
+ * disagree about it. Only a missing argument is replaced: `orderBy: {}` is a request for no
3517
+ * ordering, and stays one.
3518
+ */
3519
+ const withDefaultOrderBy = (args, tableName, defaults) => {
3520
+ if (!defaults || args?.orderBy != null) return args;
3521
+ const fallback = defaults(tableName);
3522
+ return fallback ? {
3523
+ ...args,
3524
+ orderBy: fallback
3525
+ } : args;
3526
+ };
3527
+ /**
3528
+ * Rejects a negative `limit` or `offset`. Neither is a page size or a position, and the
3529
+ * dialects disagree on what one means: SQLite reads `LIMIT -1` as "no limit" — which would walk
3530
+ * straight past `maxLimit` — while PostgreSQL and MySQL fail with a raw database error. Applied
3531
+ * whether or not the table has a limit policy.
3532
+ */
3533
+ const assertPagingNotNegative = (paging, errorCtx) => {
3534
+ for (const name of ["limit", "offset"]) {
3535
+ const value = paging[name];
3536
+ if (value != null && value < 0) throw drizzleError(`'${name}' must not be negative, got ${value}.`, {
3537
+ code: "DRIZZLE_INVALID_INPUT_VALUE",
3538
+ ...errorCtx
3539
+ });
3540
+ }
3541
+ };
3542
+ /**
3543
+ * The `limit` a query actually runs with, given the policy for the table it reads.
3544
+ *
3545
+ * A request that passes no `limit` falls back to `defaultLimit`, or to `maxLimit` when only
3546
+ * that is configured — an absent limit means every row, which is above any maximum. A limit
3547
+ * the client did ask for is rejected when it exceeds `maxLimit`, since silently truncating a
3548
+ * page tells a paginating client it has reached the end when it has not; `clampToMax` opts
3549
+ * into the truncation instead. A limit the *policy* supplied is capped rather than rejected —
3550
+ * a misconfigured `defaultLimit` above `maxLimit` is the operator's problem, not something to
3551
+ * fail a client's query over.
3552
+ */
3553
+ const applyLimitPolicy = (limit, policy, errorCtx) => {
3554
+ assertPagingNotNegative({ limit }, errorCtx);
3555
+ if (!policy) return limit ?? void 0;
3556
+ const { defaultLimit, maxLimit, clampToMax } = policy;
3557
+ if (limit == null) {
3558
+ const fallback = defaultLimit ?? maxLimit;
3559
+ if (fallback === void 0) return;
3560
+ return maxLimit === void 0 ? fallback : Math.min(fallback, maxLimit);
3561
+ }
3562
+ if (maxLimit !== void 0 && limit > maxLimit) {
3563
+ if (clampToMax) return maxLimit;
3564
+ throw drizzleError(`'limit' of ${limit} exceeds the maximum of ${maxLimit}.`, {
3565
+ code: "DRIZZLE_LIMIT_EXCEEDED",
3566
+ ...errorCtx
3567
+ });
3568
+ }
3569
+ return limit;
3570
+ };
3571
+ /** Merges a per-table override over the global policy, or `undefined` when neither bounds anything. */
3572
+ const resolveLimitPolicy = (global, table) => {
3573
+ const defaultLimit = table?.defaultLimit ?? global?.defaultLimit;
3574
+ const maxLimit = table?.maxLimit ?? global?.maxLimit;
3575
+ if (defaultLimit === void 0 && maxLimit === void 0) return;
3576
+ return {
3577
+ defaultLimit,
3578
+ maxLimit,
3579
+ clampToMax: table?.clampToMax ?? global?.clampToMax ?? false
3580
+ };
3581
+ };
3582
+ /**
3583
+ * A paginated field costs its page size times whatever one row of it costs, so `users(limit: 100)
3584
+ * { posts(limit: 10) { id } }` is charged for the thousand rows it can return rather than the two
3585
+ * fields it mentions. `childComplexity` floors at 1 so a row is never free.
3586
+ *
3587
+ * The page size is the *effective* one: a request without `limit` is priced at the policy's
3588
+ * `defaultLimit` (or its `maxLimit` when that is all it sets) rather than the estimator's guess, and no request is priced above `maxLimit`,
3589
+ * so the cost a query is charged matches the rows it can actually pull.
3590
+ */
3591
+ const listFieldComplexity = (options, policy) => ({ args, childComplexity }) => {
3592
+ const limit = args["limit"];
3593
+ let rows = (typeof limit === "number" && limit > 0 ? limit : void 0) ?? policy?.defaultLimit ?? policy?.maxLimit ?? options.defaultListSize;
3594
+ if (policy?.maxLimit !== void 0) rows = Math.min(rows, policy.maxLimit);
3595
+ return rows * Math.max(childComplexity, 1);
3596
+ };
3597
+ /**
3598
+ * An aggregate returns a single row but reads however many match, so its cost tracks the scan
3599
+ * rather than the response.
3600
+ */
3601
+ const aggregateFieldComplexity = (options) => ({ childComplexity }) => options.aggregateCost + childComplexity;
3602
+ //#endregion
3187
3603
  //#region src/util/builders/common/distinct.ts
3188
3604
  /**
3189
3605
  * Alias the parent key comes back under when the pass runs through a junction table. See
@@ -3231,13 +3647,15 @@ const selectDistinctKeys = async (params) => {
3231
3647
  ...junction ? { [DISTINCT_PARENT]: sub[DISTINCT_PARENT] } : {}
3232
3648
  }).from(sub).where(eq(sub[DISTINCT_RN], 1)).orderBy(...outerOrder);
3233
3649
  if (offset) query = query.offset(offset);
3234
- if (limit != null) query = query.limit(limit);
3650
+ const effectiveLimit = offsetOnlyLimit(table, limit, offset);
3651
+ if (effectiveLimit != null) query = query.limit(effectiveLimit < 0 ? sql`${effectiveLimit}` : effectiveLimit);
3235
3652
  return await query;
3236
3653
  };
3237
3654
  /**
3238
3655
  * Condition matching exactly the rows identified by `keys` — an `IN (…)` for a single-column
3239
- * primary key, an `OR` of per-row equality for a composite one. `table` may be the aliased
3240
- * RQB proxy.
3656
+ * primary key, an `OR` of per-row equality for a composite one — nested as a balanced tree, so
3657
+ * thousands of keys stay within SQLite's expression-depth limit. `table` may be the aliased RQB
3658
+ * proxy.
3241
3659
  */
3242
3660
  const primaryKeyRestriction = (table, pkNames, keys) => {
3243
3661
  const cols = getColumns(table);
@@ -3245,7 +3663,7 @@ const primaryKeyRestriction = (table, pkNames, keys) => {
3245
3663
  const name = pkNames[0];
3246
3664
  return inArray(cols[name], keys.map((key) => key[name]));
3247
3665
  }
3248
- return or(...keys.map((key) => and(...pkNames.map((name) => eq(cols[name], key[name])))));
3666
+ return balancedOr(keys.map((key) => and(...pkNames.map((name) => eq(cols[name], key[name])))));
3249
3667
  };
3250
3668
  //#endregion
3251
3669
  //#region src/util/builders/common/docs.ts
@@ -3391,99 +3809,6 @@ const innerOrderType = (typeName) => sharedType(typeName, {
3391
3809
  }
3392
3810
  }));
3393
3811
  //#endregion
3394
- //#region src/util/builders/common/limits.ts
3395
- /**
3396
- * Substitutes a table's default ordering for an absent `orderBy`.
3397
- *
3398
- * Applied where the arguments are first read, so everything downstream — the cursor tuple, a
3399
- * `distinct` pass, the plain-select fallback — sees one effective ordering and cannot
3400
- * disagree about it. Only a missing argument is replaced: `orderBy: {}` is a request for no
3401
- * ordering, and stays one.
3402
- */
3403
- const withDefaultOrderBy = (args, tableName, defaults) => {
3404
- if (!defaults || args?.orderBy != null) return args;
3405
- const fallback = defaults(tableName);
3406
- return fallback ? {
3407
- ...args,
3408
- orderBy: fallback
3409
- } : args;
3410
- };
3411
- /**
3412
- * Rejects a negative `limit` or `offset`. Neither is a page size or a position, and the
3413
- * dialects disagree on what one means: SQLite reads `LIMIT -1` as "no limit" — which would walk
3414
- * straight past `maxLimit` — while PostgreSQL and MySQL fail with a raw database error. Applied
3415
- * whether or not the table has a limit policy.
3416
- */
3417
- const assertPagingNotNegative = (paging, errorCtx) => {
3418
- for (const name of ["limit", "offset"]) {
3419
- const value = paging[name];
3420
- if (value != null && value < 0) throw drizzleError(`'${name}' must not be negative, got ${value}.`, {
3421
- code: "DRIZZLE_INVALID_INPUT_VALUE",
3422
- ...errorCtx
3423
- });
3424
- }
3425
- };
3426
- /**
3427
- * The `limit` a query actually runs with, given the policy for the table it reads.
3428
- *
3429
- * A request that passes no `limit` falls back to `defaultLimit`, or to `maxLimit` when only
3430
- * that is configured — an absent limit means every row, which is above any maximum. A limit
3431
- * the client did ask for is rejected when it exceeds `maxLimit`, since silently truncating a
3432
- * page tells a paginating client it has reached the end when it has not; `clampToMax` opts
3433
- * into the truncation instead. A limit the *policy* supplied is capped rather than rejected —
3434
- * a misconfigured `defaultLimit` above `maxLimit` is the operator's problem, not something to
3435
- * fail a client's query over.
3436
- */
3437
- const applyLimitPolicy = (limit, policy, errorCtx) => {
3438
- assertPagingNotNegative({ limit }, errorCtx);
3439
- if (!policy) return limit ?? void 0;
3440
- const { defaultLimit, maxLimit, clampToMax } = policy;
3441
- if (limit == null) {
3442
- const fallback = defaultLimit ?? maxLimit;
3443
- if (fallback === void 0) return;
3444
- return maxLimit === void 0 ? fallback : Math.min(fallback, maxLimit);
3445
- }
3446
- if (maxLimit !== void 0 && limit > maxLimit) {
3447
- if (clampToMax) return maxLimit;
3448
- throw drizzleError(`'limit' of ${limit} exceeds the maximum of ${maxLimit}.`, {
3449
- code: "DRIZZLE_LIMIT_EXCEEDED",
3450
- ...errorCtx
3451
- });
3452
- }
3453
- return limit;
3454
- };
3455
- /** Merges a per-table override over the global policy, or `undefined` when neither bounds anything. */
3456
- const resolveLimitPolicy = (global, table) => {
3457
- const defaultLimit = table?.defaultLimit ?? global?.defaultLimit;
3458
- const maxLimit = table?.maxLimit ?? global?.maxLimit;
3459
- if (defaultLimit === void 0 && maxLimit === void 0) return;
3460
- return {
3461
- defaultLimit,
3462
- maxLimit,
3463
- clampToMax: table?.clampToMax ?? global?.clampToMax ?? false
3464
- };
3465
- };
3466
- /**
3467
- * A paginated field costs its page size times whatever one row of it costs, so `users(limit: 100)
3468
- * { posts(limit: 10) { id } }` is charged for the thousand rows it can return rather than the two
3469
- * fields it mentions. `childComplexity` floors at 1 so a row is never free.
3470
- *
3471
- * The page size is the *effective* one: a request without `limit` is priced at the policy's
3472
- * `defaultLimit` rather than the estimator's guess, and no request is priced above `maxLimit`,
3473
- * so the cost a query is charged matches the rows it can actually pull.
3474
- */
3475
- const listFieldComplexity = (options, policy) => ({ args, childComplexity }) => {
3476
- const limit = args["limit"];
3477
- let rows = (typeof limit === "number" && limit > 0 ? limit : void 0) ?? policy?.defaultLimit ?? options.defaultListSize;
3478
- if (policy?.maxLimit !== void 0) rows = Math.min(rows, policy.maxLimit);
3479
- return rows * Math.max(childComplexity, 1);
3480
- };
3481
- /**
3482
- * An aggregate returns a single row but reads however many match, so its cost tracks the scan
3483
- * rather than the response.
3484
- */
3485
- const aggregateFieldComplexity = (options) => ({ childComplexity }) => options.aggregateCost + childComplexity;
3486
- //#endregion
3487
3812
  //#region src/util/parse-resolve-info.ts
3488
3813
  /** Unwraps `[T]!`-style wrappers down to the named type. `getNamedType` without the realm tie. */
3489
3814
  const namedTypeOf = (type) => {
@@ -3647,14 +3972,10 @@ const NUMERIC_SCALARS = /* @__PURE__ */ new Set([
3647
3972
  ]);
3648
3973
  /**
3649
3974
  * Column types that convert to a GraphQL list without being a list *in the database* —
3650
- * a bytea reads as `[Int]`, a vector and a geometry point as `[Float]` — so appending to
3651
- * them is not the operation `push` means.
3975
+ * a bytea or SQLite buffer blob reads as `[Int]`, a vector, a point or a line as `[Float]` —
3976
+ * so appending to them is not the operation `push` means.
3652
3977
  */
3653
- const NON_LIST_ARRAY_COLUMNS = /* @__PURE__ */ new Set([
3654
- "PgVector",
3655
- "PgGeometry",
3656
- "PgBytea"
3657
- ]);
3978
+ const isNonListArrayColumn = (column) => isFloatTupleColumn(column) || isBufferColumn(column);
3658
3979
  const kindCache = /* @__PURE__ */ new WeakMap();
3659
3980
  /**
3660
3981
  * Whether a column takes an operations input, and which one.
@@ -3671,7 +3992,7 @@ const fieldUpdateKind = (column, columnName, tableName) => {
3671
3992
  if (!getColumnScalarOverride(column, true)) {
3672
3993
  const { type } = drizzleColumnToGraphQLType(column, columnName, tableName, true, false, true);
3673
3994
  if (NUMERIC_SCALARS.has(type)) kind = "numeric";
3674
- else if (type instanceof GraphQLList && !NON_LIST_ARRAY_COLUMNS.has(column.columnType)) kind = "list";
3995
+ else if (type instanceof GraphQLList && !isNonListArrayColumn(column)) kind = "list";
3675
3996
  }
3676
3997
  kindCache.set(column, kind);
3677
3998
  return kind;
@@ -3942,6 +4263,16 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
3942
4263
  if (!relFieldSelection) continue;
3943
4264
  const eagerArgs = field.args;
3944
4265
  if (!is(relEntry.relation ?? relEntry, One) && (eagerArgs?.after != null || !!(eagerArgs?.distinct)?.length || isCursorFieldSelected(relFieldSelection, tables[targetTableName]))) continue;
4266
+ const junctionName = junctionTableName(tables, relEntry);
4267
+ if (hasJunctionPolicy(scope, junctionName)) {
4268
+ if (!extractJunctionColumns(relEntry)) throw drizzleError(`Relation '${tableName}.${relName}' goes through '${junctionName}', which has a scope or soft delete, over a composite key. Its links can only be filtered when the junction joins on one column per side.`, {
4269
+ code: "DRIZZLE_UNSUPPORTED_RELATION",
4270
+ table: tableName,
4271
+ operation: "relation",
4272
+ relation: relName
4273
+ });
4274
+ continue;
4275
+ }
3945
4276
  const columns = extractSelectedColumnsFromTree(relFieldSelection, tables[targetTableName], {
3946
4277
  tableName: targetTableName,
3947
4278
  relationMap,
@@ -3967,9 +4298,11 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
3967
4298
  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;
3968
4299
  const hasPagination = offset != null || limit != null;
3969
4300
  const pkNames = targetPkNames ?? [];
3970
- thisRecord.orderBy = relationArgs?.orderBy ? (aliasedTable) => extractOrderBy(aliasedTable, relationArgs.orderBy, relationFilterCtx(filterCtx, targetTableName, scope), relWhere) : hasPagination && pkNames.length ? (aliasedTable) => primaryKeyOrderExprs(aliasedTable, pkNames) : void 0;
4301
+ const tiebreak = hasPagination && pkNames.length > 0;
4302
+ const requestOrder = relationArgs?.orderBy;
4303
+ thisRecord.orderBy = requestOrder || tiebreak ? (aliasedTable) => [...requestOrder ? extractOrderBy(aliasedTable, requestOrder, relationFilterCtx(filterCtx, targetTableName, scope), relWhere) : [], ...tiebreak ? primaryKeyOrderExprs(aliasedTable, pkNames) : []] : void 0;
3971
4304
  thisRecord.offset = offset;
3972
- thisRecord.limit = limit;
4305
+ thisRecord.limit = offsetOnlyLimit(tables[targetTableName], limit, offset);
3973
4306
  thisRecord.with = extractRelationsParams(relationMap, tables, targetTableName, field, relTypeName, options);
3974
4307
  args[relName] = omitUndefinedKeys(thisRecord);
3975
4308
  }
@@ -4060,7 +4393,7 @@ const runRelationalSelect = async (opts) => {
4060
4393
  const preciseProps = Object.keys(cursorPreciseExtras(table, cursorEntries));
4061
4394
  if (preciseProps.length) params.extras = Object.fromEntries(preciseProps.map((prop) => [prop, (aliasedTable) => cursorPreciseExtras(aliasedTable, cursorEntries)[prop]]));
4062
4395
  }
4063
- params.limit = distinctKeys ? void 0 : opts.limit;
4396
+ params.limit = distinctKeys ? void 0 : offsetOnlyLimit(table, opts.limit, offset);
4064
4397
  const result = await queryBase.findMany(omitUndefinedKeys(params));
4065
4398
  if (cursorEntries && cursorSelected) attachRowCursors(result, cursorEntries);
4066
4399
  return remapToGraphQLArrayOutput(result, tableName, table, relationMap);
@@ -4117,6 +4450,8 @@ const eagerLoadMutationRelations = async (db, tableName, rows, pkNames, withPara
4117
4450
  }
4118
4451
  return rows;
4119
4452
  };
4453
+ /** The longest delay `setTimeout` honours; anything above, Infinity included, fires after 1 ms. */
4454
+ const MAX_TIMER_MS = 2 ** 31 - 1;
4120
4455
  const createMutationTxCtx = (options) => options ? {
4121
4456
  timeoutMs: options.timeoutMs,
4122
4457
  fieldNames: /* @__PURE__ */ new Set(),
@@ -4143,28 +4478,28 @@ const includedByDirectives = (node, variables) => {
4143
4478
  * this build did not generate — its completion cannot be tracked, so the caller must not
4144
4479
  * open a transaction it might never be able to close.
4145
4480
  */
4146
- const collectRootMutationKeys = (selectionSet, fragments, variables, known, keys, visitedFragments) => {
4481
+ const collectRootMutationKeys = (selectionSet, fragments, variables, known, keys, visitedFragments, argumentsCoerce) => {
4147
4482
  for (const selection of selectionSet.selections) {
4148
4483
  if (!includedByDirectives(selection, variables)) continue;
4149
4484
  if (selection.kind === "Field") {
4150
4485
  const name = selection.name.value;
4151
4486
  if (name === "__typename") continue;
4152
4487
  if (!known.has(name)) return false;
4153
- keys.add(selection.alias?.value ?? name);
4488
+ if (argumentsCoerce(selection)) keys.add(selection.alias?.value ?? name);
4154
4489
  } else if (selection.kind === "InlineFragment") {
4155
- if (!collectRootMutationKeys(selection.selectionSet, fragments, variables, known, keys, visitedFragments)) return false;
4490
+ if (!collectRootMutationKeys(selection.selectionSet, fragments, variables, known, keys, visitedFragments, argumentsCoerce)) return false;
4156
4491
  } else if (selection.kind === "FragmentSpread") {
4157
4492
  const fragmentName = selection.name.value;
4158
4493
  if (visitedFragments.has(fragmentName)) continue;
4159
4494
  visitedFragments.add(fragmentName);
4160
4495
  const fragment = fragments?.[fragmentName];
4161
4496
  if (!fragment) return false;
4162
- if (!collectRootMutationKeys(fragment.selectionSet, fragments, variables, known, keys, visitedFragments)) return false;
4497
+ if (!collectRootMutationKeys(fragment.selectionSet, fragments, variables, known, keys, visitedFragments, argumentsCoerce)) return false;
4163
4498
  }
4164
4499
  }
4165
4500
  return true;
4166
4501
  };
4167
- const abortedBatchError = () => drizzleError("Drizzle-GraphQL Error: Mutation was not executed because an earlier mutation in the same request failed and the shared transaction was rolled back.", { code: "DRIZZLE_TRANSACTION_ABORTED" });
4502
+ const abortedBatchError = (state, timeoutMs) => drizzleError(state.timedOut ? `Drizzle-GraphQL Error: Mutation was not executed because the shared transaction timed out after ${timeoutMs}ms waiting for the remaining mutation fields and was rolled back.` : "Drizzle-GraphQL Error: Mutation was not executed because an earlier mutation in the same request failed and the shared transaction was rolled back.", { code: "DRIZZLE_TRANSACTION_ABORTED" });
4168
4503
  /** Opens the request-wide transaction and wires up latch, inactivity timer and cleanup. */
4169
4504
  const openSharedTx = (db, context, txCtx, stateKey, expected) => {
4170
4505
  let release;
@@ -4199,6 +4534,7 @@ const openSharedTx = (db, context, txCtx, stateKey, expected) => {
4199
4534
  completed: 0,
4200
4535
  failure: void 0,
4201
4536
  aborted: false,
4537
+ timedOut: false,
4202
4538
  done: false,
4203
4539
  ready,
4204
4540
  settled,
@@ -4213,8 +4549,9 @@ const openSharedTx = (db, context, txCtx, stateKey, expected) => {
4213
4549
  suspendTimer: clearTimer,
4214
4550
  bumpTimer: () => {
4215
4551
  clearTimer();
4216
- if (state.done) return;
4552
+ if (state.done || txCtx.timeoutMs === Number.POSITIVE_INFINITY) return;
4217
4553
  timer = setTimeout(() => {
4554
+ if (!state.aborted) state.timedOut = true;
4218
4555
  state.abort(drizzleError(`Drizzle-GraphQL Error: Shared mutation transaction timed out after ${txCtx.timeoutMs}ms waiting for the remaining mutation fields; rolling back.`, { code: "DRIZZLE_TRANSACTION_ABORTED" }));
4219
4556
  }, txCtx.timeoutMs);
4220
4557
  timer.unref?.();
@@ -4233,15 +4570,15 @@ const openSharedTx = (db, context, txCtx, stateKey, expected) => {
4233
4570
  state.bumpTimer();
4234
4571
  return state;
4235
4572
  };
4236
- const runOnSharedTx = async (state, body) => {
4573
+ const runOnSharedTx = async (state, timeoutMs, body) => {
4237
4574
  if (state.aborted) {
4238
4575
  await state.settled.catch(() => {});
4239
- throw abortedBatchError();
4576
+ throw abortedBatchError(state, timeoutMs);
4240
4577
  }
4241
4578
  state.suspendTimer();
4242
4579
  const tx = await state.ready;
4243
4580
  try {
4244
- if (state.aborted) throw abortedBatchError();
4581
+ if (state.aborted) throw abortedBatchError(state, timeoutMs);
4245
4582
  const result = await body(tx);
4246
4583
  state.completed += 1;
4247
4584
  if (state.completed >= state.expected) {
@@ -4274,14 +4611,24 @@ const runMutation = async (db, context, info, txCtx, body, forceTx) => {
4274
4611
  if (contextIsObject && context[drizzleExecutorKey]) return body(context[drizzleExecutorKey]);
4275
4612
  if (!txCtx || info.operation.operation !== "mutation") return forceTx ? db.transaction((tx) => body(tx)) : body(db);
4276
4613
  const keys = /* @__PURE__ */ new Set();
4277
- state = !collectRootMutationKeys(info.operation.selectionSet, info.fragments, info.variableValues, txCtx.fieldNames, keys, /* @__PURE__ */ new Set()) || keys.size <= 1 ? { kind: "passthrough" } : openSharedTx(db, context, txCtx, stateKey, keys.size);
4614
+ const mutationFields = info.schema.getMutationType()?.getFields() ?? {};
4615
+ state = !collectRootMutationKeys(info.operation.selectionSet, info.fragments, info.variableValues, txCtx.fieldNames, keys, /* @__PURE__ */ new Set(), (node) => {
4616
+ const fieldDef = mutationFields[node.name.value];
4617
+ if (!fieldDef) return false;
4618
+ try {
4619
+ getArgumentValues(fieldDef, node, info.variableValues);
4620
+ return true;
4621
+ } catch {
4622
+ return false;
4623
+ }
4624
+ }) || keys.size <= 1 ? { kind: "passthrough" } : openSharedTx(db, context, txCtx, stateKey, keys.size);
4278
4625
  txCtx.states.set(stateKey, state);
4279
4626
  }
4280
4627
  if (state.kind === "passthrough") {
4281
4628
  const executor = resolveExecutor(db, context);
4282
4629
  return forceTx && executor === db ? db.transaction((tx) => body(tx)) : body(executor);
4283
4630
  }
4284
- return runOnSharedTx(state, body);
4631
+ return runOnSharedTx(state, txCtx.timeoutMs, body);
4285
4632
  };
4286
4633
  //#endregion
4287
4634
  //#region src/util/builders/common/write-hooks.ts
@@ -4435,10 +4782,11 @@ const hardDeleteArg = {
4435
4782
  description: "Remove the matched rows instead of marking them deleted. Reads at `INCLUDE`, so it reaches rows that are already marked; a `scope` still confines what it can reach."
4436
4783
  };
4437
4784
  const prepareMutationRelationColumns = (params) => {
4438
- const { relationMap, tables, tableName, typeNameMapper, table, pkNames, parsedInfo, resolveName } = params;
4785
+ const { relationMap, filterCtx, tables, tableName, typeNameMapper, table, pkNames, parsedInfo, resolveName } = params;
4439
4786
  const typeName = resolveObjectTypeName(tableName, typeNameMapper, resolveName);
4440
4787
  const withParams = relationMap[tableName] ? extractRelationsParams(relationMap, tables, tableName, parsedInfo, typeName, {
4441
4788
  typeNameMapper,
4789
+ filterCtx,
4442
4790
  limits: params.limits,
4443
4791
  scope: params.scope,
4444
4792
  defaultOrderBy: params.defaultOrderBy,
@@ -4448,7 +4796,8 @@ const prepareMutationRelationColumns = (params) => {
4448
4796
  const baseColumns = extractSelectedColumnsFromTreeSQLFormat(parsedInfo.fieldsByTypeName[typeName], table, {
4449
4797
  tableName,
4450
4798
  relationMap,
4451
- tables
4799
+ tables,
4800
+ allRelations: filterCtx?.relationMap
4452
4801
  });
4453
4802
  return {
4454
4803
  columns: hasRelations ? withPrimaryKeyColumns(baseColumns, table, pkNames) : baseColumns,
@@ -4584,7 +4933,7 @@ const assertSingleMatch = async (executor, table, filters) => {
4584
4933
  * MySQL (no `RETURNING`; its hooks see `rows: []` as on every MySQL mutation), skips the
4585
4934
  * `RETURNING` and reads the count off the driver's result via {@link rowsAffected}.
4586
4935
  */
4587
- const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, fieldName, requireWhere, filterCtx, txCtx, nested, policies, pkNames = [], returnsRows }) => {
4936
+ const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, fieldName, requireWhere, filterCtx, txCtx, nested, policies, pkNames = [], returnsRows, writeFilter }) => {
4588
4937
  const softDelete = kind === "delete" ? policies?.softDelete?.(tableName) : void 0;
4589
4938
  const canHardDelete = softDelete?.hardDelete === true;
4590
4939
  const queryArgs = {
@@ -4627,7 +4976,7 @@ const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, f
4627
4976
  required: requireWhere,
4628
4977
  deleted: hard ? "INCLUDE" : void 0
4629
4978
  });
4630
- if (filters) query = query.where(filters);
4979
+ if (filters) query = query.where(writeFilter ? writeFilter(filters) : filters);
4631
4980
  if (!readsRows) {
4632
4981
  const result = await query;
4633
4982
  await after();
@@ -4687,27 +5036,40 @@ const getOrCreateLoader = (context, key, batchFn) => {
4687
5036
  if (!loaders.has(key)) loaders.set(key, new BatchLoader(batchFn));
4688
5037
  return loaders.get(key);
4689
5038
  };
5039
+ const requestCacheNode = () => ({
5040
+ children: /* @__PURE__ */ new WeakMap(),
5041
+ entries: /* @__PURE__ */ new Map()
5042
+ });
4690
5043
  /**
4691
5044
  * Memoizes a value that a resolver derives from its own field — its args and its selection —
4692
5045
  * for the length of one request. Resolvers run once per parent row, so anything derived only
4693
5046
  * from the field is otherwise recomputed for every row of a batch.
4694
5047
  *
4695
- * The cache lives on the context (weakly keyed by the AST node the value is derived from, so a
4696
- * document cached across requests never carries values between them) and `key` separates fields
4697
- * that share a node: one fragment selection can resolve for more than one parent type. Without a
4698
- * context object there is nowhere request-scoped to cache, so the value is simply computed.
5048
+ * The value is keyed by every AST node of the field — `info.fieldNodes`, the nodes graphql-js
5049
+ * merged into this one response field — not only the first. A fragment's field node is the first
5050
+ * node wherever the fragment is spread, but the merged selection differs when a sibling selection
5051
+ * at one spread site adds to it, so keying on one node would serve one site the other's selection.
5052
+ * The nodes are matched by identity, in order, through a chain of weak maps, so a document cached
5053
+ * across requests never carries values between them (the root lives on the context), and an array
5054
+ * graphql rebuilds per call still hits. `key` separates fields that share nodes: one fragment
5055
+ * selection can resolve for more than one parent type. Without a context object, or without
5056
+ * nodes, there is nowhere request-scoped to cache, so the value is simply computed.
4699
5057
  *
4700
5058
  * `compute` must not read the parent row.
4701
5059
  */
4702
- const getOrCreateRequestValue = (context, node, key, compute) => {
4703
- if (!context || typeof context !== "object" || !node || typeof node !== "object") return compute();
4704
- if (!context[DRIZZLE_REQUEST_CACHE_KEY]) context[DRIZZLE_REQUEST_CACHE_KEY] = /* @__PURE__ */ new WeakMap();
4705
- const cache = context[DRIZZLE_REQUEST_CACHE_KEY];
4706
- let entries = cache.get(node);
4707
- if (!entries) {
4708
- entries = /* @__PURE__ */ new Map();
4709
- cache.set(node, entries);
5060
+ const getOrCreateRequestValue = (context, nodes, key, compute) => {
5061
+ if (!context || typeof context !== "object" || !Array.isArray(nodes) || !nodes.length || nodes.some((node) => !node || typeof node !== "object")) return compute();
5062
+ if (!context[DRIZZLE_REQUEST_CACHE_KEY]) context[DRIZZLE_REQUEST_CACHE_KEY] = requestCacheNode();
5063
+ let level = context[DRIZZLE_REQUEST_CACHE_KEY];
5064
+ for (const node of nodes) {
5065
+ let next = level.children.get(node);
5066
+ if (!next) {
5067
+ next = requestCacheNode();
5068
+ level.children.set(node, next);
5069
+ }
5070
+ level = next;
4710
5071
  }
5072
+ const entries = level.entries;
4711
5073
  if (entries.has(key)) return entries.get(key);
4712
5074
  const value = compute();
4713
5075
  entries.set(key, value);
@@ -4937,11 +5299,12 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
4937
5299
  if (!parentTable || !targetTable) return;
4938
5300
  const joinPairs = extractRelationJoinColumnPairs(relEntry, parentTable, targetTable);
4939
5301
  if (!joinPairs) return;
4940
- const localColPropNames = joinPairs.map((pair) => pair.localColPropName);
5302
+ const localCols = joinPairs.map((pair) => pair.localCol);
4941
5303
  const foreignCol = joinPairs[0].foreignCol;
4942
5304
  const junction = extractJunctionColumns(relEntry);
4943
5305
  const groupCols = junction ? [junction.sourceCol] : joinPairs.map((pair) => pair.foreignCol);
4944
5306
  const groupKeys = junction ? [PARENT_KEY] : joinPairs.map((pair) => pair.foreignColPropName);
5307
+ const junctionName = junction ? junctionTableName(tables, relEntry) : void 0;
4945
5308
  const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, joinPairs.map((pair) => pair.localCol), groupCols);
4946
5309
  const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, isOne && relEntry.relation?.optional === false);
4947
5310
  const limitPolicy = isOne ? void 0 : limits?.(targetTableName);
@@ -4983,10 +5346,10 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
4983
5346
  };
4984
5347
  const resolve = async (parent, args, context, info) => {
4985
5348
  if (parent[relationName] !== void 0) return parent[relationName];
4986
- const localValues = localColPropNames.map((name) => parent[name]);
5349
+ const localValues = parentJoinValues(parent, joinPairs);
4987
5350
  if (localValues.some((value) => value == null)) return isOne ? null : [];
4988
5351
  const { where: whereArg, limit: requestedLimit, offset, after, deleted } = args ?? {};
4989
- const { orderByArg, limit, distinct, cursorEntries, cursorValues, columns, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes?.[0], `relation:${tableName}::${relationName}`, () => {
5352
+ const { orderByArg, limit, distinct, cursorEntries, cursorValues, columns, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes, `relation:${tableName}::${relationName}`, () => {
4990
5353
  const orderByArg = isOne ? args?.orderBy : withDefaultOrderBy(args ?? {}, targetTableName, policies?.defaultOrderBy).orderBy;
4991
5354
  const limit = applyLimitPolicy(requestedLimit, limitPolicy, errorCtx);
4992
5355
  assertPagingNotNegative({ offset }, errorCtx);
@@ -5027,55 +5390,60 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
5027
5390
  if (junction === null) throw drizzleError(`Relation '${tableName}.${relationName}' goes through a junction with a composite key, which can only be loaded together with its parent. Leave it to eager loading (eagerLoadRelations).`, { code: "DRIZZLE_UNSUPPORTED_RELATION" });
5028
5391
  return getOrCreateLoader(context, loaderKey, async (parentKeys) => {
5029
5392
  const executor = resolveExecutor(db, context);
5030
- const uniqueKeys = [...new Map(parentKeys.map((key) => [joinKeyOf(key), key])).values()];
5393
+ const uniqueKeys = [...new Map(parentKeys.map((key) => [joinKeyOf(key, localCols), key])).values()];
5031
5394
  const scope = resolveScope(policies, context, filterCtx);
5032
- let whereCondition = withScope(scope, targetTableName, targetTable, and(parentKeyCondition(groupCols, uniqueKeys), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0, cursorValues ? buildCursorCondition(targetTable, cursorEntries, cursorValues, nullOrdering) : void 0), deleted ?? defaultDeleted);
5033
- if (distinct) {
5034
- const keys = await selectDistinctKeys({
5035
- db: executor,
5036
- table: targetTable,
5037
- tableName: targetTableName,
5038
- distinct,
5039
- pkNames: targetPkNames,
5040
- where: whereCondition,
5041
- orderBy: orderByArg,
5042
- partitionBy: groupCols,
5043
- junction: junction ? {
5044
- table: junction.table,
5045
- on: eq(junction.targetCol, foreignCol),
5046
- parentKey: junction.sourceCol
5047
- } : void 0
5048
- });
5049
- if (!keys.length) return parentKeys.map(() => isOne ? null : []);
5050
- whereCondition = and(whereCondition, junction ? or(...keys.map((key) => and(eq(junction.sourceCol, key[DISTINCT_PARENT]), primaryKeyRestriction(targetTable, targetPkNames, [key])))) : primaryKeyRestriction(targetTable, targetPkNames, keys));
5051
- }
5052
- const selection = junction ? {
5053
- ...columns ?? getColumns(targetTable),
5054
- [PARENT_KEY]: junction.sourceCol
5055
- } : columns;
5056
- let rows;
5057
- if (limit != null || offset != null) rows = await batchedPaginatedRelationQuery(executor, targetTable, foreignCol, groupCols, whereCondition, orderByArg, limit ?? null, offset ?? null, targetPkNames, selection, relationFilterCtx(filterCtx, targetTableName, scope), whereArg, junction);
5058
- else {
5059
- let q = withJunction((selection ? executor.select(selection) : executor.select()).from(targetTable), junction, foreignCol).where(whereCondition);
5060
- const orderExprs = cursorEntries ? cursorOrderExprs(targetTable, cursorEntries) : orderByArg ? extractOrderBy(targetTable, orderByArg, relationFilterCtx(filterCtx, targetTableName, scope), whereArg) : [];
5061
- if (orderExprs.length) q = q.orderBy(...orderExprs);
5062
- rows = await q;
5063
- }
5064
- if (cursorEntries) attachRowCursors(rows, cursorEntries);
5065
- const keyOf = (row) => {
5066
- const key = joinKeyOf(groupKeys.map((name) => row[name]));
5067
- if (junction) delete row[PARENT_KEY];
5068
- return key;
5069
- };
5070
- if (isOne) {
5071
- const byKey = new Map(rows.map((row) => [keyOf(row), row]));
5395
+ const loadChunk = async (chunkKeys) => {
5396
+ let whereCondition = withScope(scope, targetTableName, targetTable, and(parentKeyCondition(groupCols, chunkKeys), ownCondition, junction ? junctionPolicy(scope, junctionName, junction.table) : void 0, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0, cursorValues ? buildCursorCondition(targetTable, cursorEntries, cursorValues, nullOrdering) : void 0), deleted ?? defaultDeleted);
5397
+ if (distinct) {
5398
+ const keys = await selectDistinctKeys({
5399
+ db: executor,
5400
+ table: targetTable,
5401
+ tableName: targetTableName,
5402
+ distinct,
5403
+ pkNames: targetPkNames,
5404
+ where: whereCondition,
5405
+ orderBy: orderByArg,
5406
+ partitionBy: groupCols,
5407
+ junction: junction ? {
5408
+ table: junction.table,
5409
+ on: eq(junction.targetCol, foreignCol),
5410
+ parentKey: junction.sourceCol
5411
+ } : void 0
5412
+ });
5413
+ if (!keys.length) return /* @__PURE__ */ new Map();
5414
+ whereCondition = and(whereCondition, junction ? balancedOr(keys.map((key) => and(eq(junction.sourceCol, key[DISTINCT_PARENT]), primaryKeyRestriction(targetTable, targetPkNames, [key])))) : primaryKeyRestriction(targetTable, targetPkNames, keys));
5415
+ }
5416
+ const selection = junction ? {
5417
+ ...columns ?? getColumns(targetTable),
5418
+ [PARENT_KEY]: junction.sourceCol
5419
+ } : columns;
5420
+ let rows;
5421
+ if (limit != null || offset != null) rows = await batchedPaginatedRelationQuery(executor, targetTable, foreignCol, groupCols, whereCondition, orderByArg, limit ?? null, offset ?? null, targetPkNames, selection, relationFilterCtx(filterCtx, targetTableName, scope), whereArg, junction);
5422
+ else {
5423
+ let q = withJunction((selection ? executor.select(selection) : executor.select()).from(targetTable), junction, foreignCol).where(whereCondition);
5424
+ const orderExprs = cursorEntries ? cursorOrderExprs(targetTable, cursorEntries) : orderByArg ? extractOrderBy(targetTable, orderByArg, relationFilterCtx(filterCtx, targetTableName, scope), whereArg) : [];
5425
+ if (orderExprs.length) q = q.orderBy(...orderExprs);
5426
+ rows = await q;
5427
+ }
5428
+ if (cursorEntries) attachRowCursors(rows, cursorEntries);
5429
+ const keyOf = (row) => {
5430
+ const key = joinKeyOf(groupKeys.map((name) => row[name]), groupCols);
5431
+ if (junction) delete row[PARENT_KEY];
5432
+ return key;
5433
+ };
5434
+ if (isOne) {
5435
+ const byKey = new Map(rows.map((row) => [keyOf(row), row]));
5436
+ remapToGraphQLArrayOutput(rows, targetTableName, targetTable);
5437
+ return byKey;
5438
+ }
5439
+ const grouped = new Map(chunkKeys.map((key) => [joinKeyOf(key, localCols), []]));
5440
+ for (const row of rows) grouped.get(keyOf(row))?.push(row);
5072
5441
  remapToGraphQLArrayOutput(rows, targetTableName, targetTable);
5073
- return parentKeys.map((key) => byKey.get(joinKeyOf(key)) ?? null);
5074
- }
5075
- const grouped = new Map(uniqueKeys.map((key) => [joinKeyOf(key), []]));
5076
- for (const row of rows) grouped.get(keyOf(row))?.push(row);
5077
- remapToGraphQLArrayOutput(rows, targetTableName, targetTable);
5078
- return parentKeys.map((key) => grouped.get(joinKeyOf(key)) ?? []);
5442
+ return grouped;
5443
+ };
5444
+ const results = /* @__PURE__ */ new Map();
5445
+ for (const chunk of parentKeyChunks(uniqueKeys, groupCols.length)) for (const [key, value] of await loadChunk(chunk)) results.set(key, value);
5446
+ return parentKeys.map((key) => results.get(joinKeyOf(key, localCols)) ?? (isOne ? null : []));
5079
5447
  }).load(localValues);
5080
5448
  };
5081
5449
  return async (parent, args, context, info) => {
@@ -5180,7 +5548,7 @@ const generateTableOrderCached = (table, innerOrder) => {
5180
5548
  try {
5181
5549
  const columns = visibleColumns(table);
5182
5550
  const columnEntries = Object.entries(columns);
5183
- remapped = Object.fromEntries(columnEntries.map(([columnName, _columnDescription]) => [columnName, { type: innerOrder }]));
5551
+ remapped = Object.fromEntries(columnEntries.filter(([, column]) => isOrderableColumn(column)).map(([columnName]) => [columnName, { type: innerOrder }]));
5184
5552
  if (cacheable) {
5185
5553
  let byInnerOrder = orderMap.get(table);
5186
5554
  if (!byInnerOrder) {
@@ -5223,43 +5591,71 @@ const generateTableSelectTypeFieldsCached = (table, tableName, cacheCtx) => {
5223
5591
  return remapped;
5224
5592
  };
5225
5593
  /**
5594
+ * The to-one relations a table's OrderBy can order through, as `[relationName, targetTableName]`.
5595
+ * To-many relations are left out: "order a parent by its many children" has no single defined
5596
+ * value to sort on. A relation whose name collides with a column is skipped — the column keeps
5597
+ * the name, even one with no ordering of its own, so `extractOrderBy` never reads it as a relation.
5598
+ */
5599
+ const orderRelations = (tableName, relationMap, tables) => {
5600
+ const relations = relationMap?.[tableName];
5601
+ const table = tables?.[tableName];
5602
+ if (!relations || !tables || !table) return [];
5603
+ const columns = visibleColumns(table);
5604
+ return Object.entries(relations).filter(([relationName, relEntry]) => {
5605
+ const relation = relEntry.relation ?? relEntry;
5606
+ return !(relationName in columns) && !!tables[relEntry.targetTableName] && is(relation, One) && isFilterableRelation(relation);
5607
+ }).map(([relationName, relEntry]) => [relationName, relEntry.targetTableName]);
5608
+ };
5609
+ /**
5610
+ * Whether a table's OrderBy would have any field: an orderable column of its own, or a to-one
5611
+ * relation to a table that has one. A table of nothing but JSON columns has none, and an input
5612
+ * object with no fields is an invalid schema, so such a table gets no OrderBy and no `orderBy`
5613
+ * argument at all.
5614
+ */
5615
+ const hasOrderFields = (tableName, relationMap, tables, visiting = /* @__PURE__ */ new Set()) => {
5616
+ const table = tables[tableName];
5617
+ if (table && Object.values(visibleColumns(table)).some((column) => isOrderableColumn(column))) return true;
5618
+ if (visiting.has(tableName)) return false;
5619
+ visiting.add(tableName);
5620
+ return orderRelations(tableName, relationMap, tables).some(([, target]) => hasOrderFields(target, relationMap, tables, visiting));
5621
+ };
5622
+ /**
5226
5623
  * Order fields for a table's to-one relations: each takes the target table's own OrderBy
5227
5624
  * input, so a list can be sorted by a related row's column (compiled as a correlated
5228
- * subquery in the ORDER BY — see `extractOrderBy`). To-many relations are left out: "order
5229
- * a parent by its many children" has no single defined value to sort on. A relation whose
5230
- * name collides with a column is skipped — the column keeps the field.
5625
+ * subquery in the ORDER BY — see `extractOrderBy`). A target with no OrderBy of its own is
5626
+ * left out.
5231
5627
  */
5232
- const generateRelationOrderFields = (tableName, cacheCtx, typeNameMapper, columnFields, relationMap, tables) => {
5233
- const relations = relationMap?.[tableName];
5234
- if (!relations || !tables) return {};
5628
+ const generateRelationOrderFields = (tableName, cacheCtx, typeNameMapper, relationMap, tables) => {
5235
5629
  const fields = {};
5236
- for (const [relationName, relEntry] of Object.entries(relations)) {
5237
- if (relationName in columnFields) continue;
5238
- const targetTable = tables[relEntry.targetTableName];
5239
- const relation = relEntry.relation ?? relEntry;
5240
- if (!targetTable || !is(relation, One) || !isFilterableRelation(relation)) continue;
5241
- fields[relationName] = {
5242
- type: generateTableOrderTypeCached(targetTable, relEntry.targetTableName, typeNameMapper, cacheCtx, relationMap, tables),
5630
+ for (const [relationName, targetTableName] of orderRelations(tableName, relationMap, tables)) {
5631
+ const type = generateTableOrderTypeCached(tables[targetTableName], targetTableName, typeNameMapper, cacheCtx, relationMap, tables);
5632
+ if (type) fields[relationName] = {
5633
+ type,
5243
5634
  description: `Order by columns of the related ${relationName} row`
5244
5635
  };
5245
5636
  }
5246
5637
  return fields;
5247
5638
  };
5639
+ /**
5640
+ * The table's `<Type>OrderBy` input, or `undefined` when it would have no fields (see
5641
+ * {@link hasOrderFields}); the caller then leaves the `orderBy` argument out.
5642
+ */
5248
5643
  const generateTableOrderTypeCached = (table, tableName, typeNameMapper, cacheCtx, relationMap, tables) => {
5249
5644
  if (cacheCtx.orderTypeCache.has(table)) return cacheCtx.orderTypeCache.get(table);
5645
+ if (!hasOrderFields(tableName, relationMap, {
5646
+ ...tables,
5647
+ [tableName]: table
5648
+ })) return;
5250
5649
  const order = new GraphQLInputObjectType({
5251
5650
  name: cacheCtx.typeName({
5252
5651
  kind: "orderBy",
5253
5652
  defaultName: `${resolveTypeName(tableName, typeNameMapper)}OrderBy`,
5254
5653
  table: tableName
5255
5654
  }),
5256
- fields: () => {
5257
- const orderColumns = generateTableOrderCached(table, innerOrderType(cacheCtx.typeName));
5258
- return {
5259
- ...orderColumns,
5260
- ...generateRelationOrderFields(tableName, cacheCtx, typeNameMapper, orderColumns, relationMap, tables)
5261
- };
5262
- }
5655
+ fields: () => ({
5656
+ ...generateTableOrderCached(table, innerOrderType(cacheCtx.typeName)),
5657
+ ...generateRelationOrderFields(tableName, cacheCtx, typeNameMapper, relationMap, tables)
5658
+ })
5263
5659
  });
5264
5660
  cacheCtx.orderTypeCache.set(table, order);
5265
5661
  return order;
@@ -5478,7 +5874,7 @@ const generateSelectFields = (tables, tableName, relationMap, fromTableName, fro
5478
5874
  type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(relType))),
5479
5875
  args: {
5480
5876
  where: { type: relSelectData.filters },
5481
- orderBy: { type: relSelectData.order },
5877
+ ...relSelectData.order ? { orderBy: { type: relSelectData.order } } : {},
5482
5878
  offset: { type: GraphQLInt },
5483
5879
  limit: { type: GraphQLInt },
5484
5880
  ...deletedArg(cacheCtx.softDeleteOf, targetTableName, cacheCtx.typeName),
@@ -5706,6 +6102,7 @@ const resolveBuildConfig = (config, schema) => {
5706
6102
  };
5707
6103
  const transactionsOpt = config?.transactions;
5708
6104
  const transactions = transactionsOpt === void 0 || transactionsOpt === "none" ? void 0 : { timeoutMs: (transactionsOpt === "auto" ? void 0 : transactionsOpt.timeoutMs) ?? 3e4 };
6105
+ if (transactions && transactions.timeoutMs !== Number.POSITIVE_INFINITY && !(Number.isInteger(transactions.timeoutMs) && transactions.timeoutMs >= 1 && transactions.timeoutMs <= MAX_TIMER_MS)) throw new Error(`Drizzle-GraphQL Error: config.transactions.timeoutMs must be a positive integer no greater than ${MAX_TIMER_MS}, or Infinity for no timeout; got ${transactions.timeoutMs}.`);
5709
6106
  const limitsConfig = config?.limits;
5710
6107
  if (limitsConfig) {
5711
6108
  const check = (policy, where) => {
@@ -5738,6 +6135,7 @@ const resolveBuildConfig = (config, schema) => {
5738
6135
  for (const [columnName, entry] of Object.entries(orderBy)) {
5739
6136
  if (!columns[columnName]) throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy names '${columnName}', which is not a column of '${tableName}'.`);
5740
6137
  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.`);
6138
+ if (!isOrderableColumn(columns[columnName])) throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy names '${columnName}', which has no meaningful ordering (a JSON column, or a SQLite blob bigint), so the generated orderBy does not offer it either.`);
5741
6139
  const direction = typeof entry === "string" ? entry : entry?.direction;
5742
6140
  if (direction !== "asc" && direction !== "desc") throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy.${columnName} must be 'asc', 'desc', or { direction, priority? }.`);
5743
6141
  const priority = typeof entry === "string" ? void 0 : entry?.priority;
@@ -5796,7 +6194,7 @@ const resolveBuildConfig = (config, schema) => {
5796
6194
  }
5797
6195
  }
5798
6196
  }
5799
- const onWrite = writeHooks.size ? (tableName) => writeHooks.get(tableName) : void 0;
6197
+ const onWrite = [...writeHooks.values()].some(Boolean) ? (tableName) => writeHooks.get(tableName) : void 0;
5800
6198
  const policies = scopeConfig || contextValuesConfig || softDeleteInfos.size || onWrite || defaultOrderBy ? {
5801
6199
  scope: scopeConfig ? (tableName) => scopeConfig[tableName] : void 0,
5802
6200
  contextValues: contextValuesConfig ? (tableName) => contextValuesConfig[tableName] : void 0,
@@ -5887,8 +6285,14 @@ const AGGREGATE_OPS = [
5887
6285
  ];
5888
6286
  /** Ops whose result is a row count: never null, and returned as `Int!` rather than the column's type. */
5889
6287
  const COUNT_OPS = /* @__PURE__ */ new Set(["countNonNull", "countDistinct"]);
6288
+ /**
6289
+ * `avg`, except on MySQL, where AVG over an integer column is a DECIMAL rounded to
6290
+ * `div_precision_increment` (4 by default) digits. Averaging the value as a DOUBLE keeps the
6291
+ * fraction the `Float` output promises, as the other two dialects do.
6292
+ */
6293
+ const avgOf = (col) => columnDialect(col) === "mysql" ? sql`avg(cast(${col} as double))`.mapWith(String) : avg(col);
5890
6294
  const OP_FNS = {
5891
- avg,
6295
+ avg: avgOf,
5892
6296
  sum,
5893
6297
  min,
5894
6298
  max,
@@ -5898,6 +6302,18 @@ const OP_FNS = {
5898
6302
  /** Separator for flat select aliases (`avg__price`) — reassembled into nested output by the resolver. */
5899
6303
  const SEP = "__";
5900
6304
  /**
6305
+ * Column types PostgreSQL can test for equality but has no `min`/`max` aggregate for, plus the
6306
+ * SQLite bigint blob, whose bytes compare as text (`max(9, 10)` is 9). They still count
6307
+ * distinct values and group.
6308
+ */
6309
+ const NO_MIN_MAX_COLUMN_TYPES = /* @__PURE__ */ new Set([
6310
+ "PgUUID",
6311
+ "PgMacaddr",
6312
+ "PgMacaddr8",
6313
+ "PgBinaryVector",
6314
+ "SQLiteBigInt"
6315
+ ]);
6316
+ /**
5901
6317
  * Classifies a table's columns for aggregation. avg/sum only make sense on numeric scalars;
5902
6318
  * min/max work on any orderable scalar (numbers, strings, bigints, dates, enums). Booleans,
5903
6319
  * arrays (including array-typed number columns), JSON, buffers, and object-shaped columns
@@ -5929,6 +6345,7 @@ const classifyAggregateColumns = (table, tableName) => {
5929
6345
  if (cached) return cached;
5930
6346
  const numeric = {};
5931
6347
  const orderable = {};
6348
+ const comparable = {};
5932
6349
  const all = {};
5933
6350
  for (const [columnName, column] of Object.entries(visibleColumns(table))) {
5934
6351
  all[columnName] = column;
@@ -5938,7 +6355,11 @@ const classifyAggregateColumns = (table, tableName) => {
5938
6355
  const { type: dataType, constraint } = extractExtendedColumnType(column);
5939
6356
  if (dataType === "boolean" || dataType === "array" || dataType === "custom") continue;
5940
6357
  if (dataType === "object" && constraint !== "date") continue;
5941
- orderable[columnName] = {
6358
+ comparable[columnName] = {
6359
+ column,
6360
+ converted
6361
+ };
6362
+ if (!NO_MIN_MAX_COLUMN_TYPES.has(column.columnType)) orderable[columnName] = {
5942
6363
  column,
5943
6364
  converted
5944
6365
  };
@@ -5947,6 +6368,7 @@ const classifyAggregateColumns = (table, tableName) => {
5947
6368
  const sets = {
5948
6369
  numeric,
5949
6370
  orderable,
6371
+ comparable,
5950
6372
  all
5951
6373
  };
5952
6374
  byName.set(tableName, sets);
@@ -5959,13 +6381,13 @@ const classifyAggregateColumns = (table, tableName) => {
5959
6381
  * and avg/sum of integers overflow Int / produce decimals)
5960
6382
  * - `min` / `max` — per orderable column, the column's own (nullable) scalar type
5961
6383
  * - `countNonNull` — per column, `Int!`: how many matching rows have a non-null value there
5962
- * - `countDistinct` — per orderable column, `Int!`: how many distinct non-null values there are
6384
+ * - `countDistinct` — per comparable column, `Int!`: how many distinct non-null values there are
5963
6385
  * Each wrapper is omitted when no column qualifies for it.
5964
6386
  */
5965
6387
  const generateAggregateTypes = (table, tableName, typeName, cacheCtx) => {
5966
6388
  const cached = cacheCtx?.aggregateTypeCache.get(tableName);
5967
6389
  if (cached) return cached;
5968
- const { numeric, orderable, all } = classifyAggregateColumns(table, tableName);
6390
+ const { numeric, orderable, comparable, all } = classifyAggregateColumns(table, tableName);
5969
6391
  const nameOf = (op) => cacheCtx?.typeName({
5970
6392
  kind: "aggregate",
5971
6393
  defaultName: `${typeName}${capitalize(op)}Aggregate`,
@@ -5986,7 +6408,7 @@ const generateAggregateTypes = (table, tableName, typeName, cacheCtx) => {
5986
6408
  }) };
5987
6409
  const countSets = {
5988
6410
  countNonNull: all,
5989
- countDistinct: orderable
6411
+ countDistinct: comparable
5990
6412
  };
5991
6413
  for (const [op, columns] of Object.entries(countSets)) {
5992
6414
  const columnNames = Object.keys(columns);
@@ -6161,11 +6583,12 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
6161
6583
  }
6162
6584
  return;
6163
6585
  }
6164
- const localColPropNames = joinPairs.map((pair) => pair.localColPropName);
6586
+ const localCols = joinPairs.map((pair) => pair.localCol);
6165
6587
  const foreignCol = joinPairs[0].foreignCol;
6166
6588
  const junction = extractJunctionColumns(relEntry);
6167
6589
  if (junction === null) return;
6168
6590
  const groupCols = junction ? [junction.sourceCol] : joinPairs.map((pair) => pair.foreignCol);
6591
+ const junctionName = junction ? junctionTableName(tables, relEntry) : void 0;
6169
6592
  const groupAliases = groupCols.map((_, i) => `${GROUP_KEY}${i}`);
6170
6593
  const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, joinPairs.map((pair) => pair.localCol), groupCols);
6171
6594
  const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, false);
@@ -6179,7 +6602,7 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
6179
6602
  };
6180
6603
  const resolve = async (parent, args, context, info) => {
6181
6604
  try {
6182
- const { request, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes?.[0], `aggregate:${tableName}::${relationName}`, () => {
6605
+ const { request, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes, `aggregate:${tableName}::${relationName}`, () => {
6183
6606
  const request = parseAggregateRequest(info, target);
6184
6607
  return {
6185
6608
  request,
@@ -6191,23 +6614,26 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
6191
6614
  };
6192
6615
  });
6193
6616
  if (!Object.keys(request.selection).length) return {};
6194
- const localValues = localColPropNames.map((name) => parent[name]);
6617
+ const localValues = parentJoinValues(parent, joinPairs);
6195
6618
  if (localValues.some((value) => value == null)) return assembleAggregateRow({}, request, target);
6196
6619
  const whereArg = args?.where;
6197
6620
  const deleted = args?.deleted;
6198
6621
  const loader = getOrCreateLoader(context, loaderKey, async (parentKeys) => {
6199
6622
  const executor = resolveExecutor(db, context);
6200
- const uniqueKeys = [...new Map(parentKeys.map((key) => [joinKeyOf(key), key])).values()];
6623
+ const uniqueKeys = [...new Map(parentKeys.map((key) => [joinKeyOf(key, localCols), key])).values()];
6201
6624
  const scope = resolveScope(policies, context, filterCtx);
6202
- const whereCondition = withScope(scope, targetTableName, targetTable, and(parentKeyCondition(groupCols, uniqueKeys), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0), deleted ?? defaultDeleted);
6203
6625
  const groupSelection = Object.fromEntries(groupAliases.map((alias, i) => [alias, groupCols[i]]));
6204
- const selected = executor.select({
6205
- ...groupSelection,
6206
- ...request.selection
6207
- }).from(targetTable);
6208
- const rows = await (junction ? selected.innerJoin(junction.table, eq(junction.targetCol, foreignCol)) : selected).where(whereCondition).groupBy(...groupCols);
6209
- const byKey = new Map(rows.map((row) => [joinKeyOf(groupAliases.map((alias) => row[alias])), row]));
6210
- return parentKeys.map((key) => byKey.get(joinKeyOf(key)) ?? {});
6626
+ const byKey = /* @__PURE__ */ new Map();
6627
+ for (const chunk of parentKeyChunks(uniqueKeys, groupCols.length)) {
6628
+ const whereCondition = withScope(scope, targetTableName, targetTable, and(parentKeyCondition(groupCols, chunk), ownCondition, junction ? junctionPolicy(scope, junctionName, junction.table) : void 0, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0), deleted ?? defaultDeleted);
6629
+ const selected = executor.select({
6630
+ ...groupSelection,
6631
+ ...request.selection
6632
+ }).from(targetTable);
6633
+ const rows = await (junction ? selected.innerJoin(junction.table, eq(junction.targetCol, foreignCol)) : selected).where(whereCondition).groupBy(...groupCols);
6634
+ for (const row of rows) byKey.set(joinKeyOf(groupAliases.map((alias) => row[alias]), groupCols), row);
6635
+ }
6636
+ return parentKeys.map((key) => byKey.get(joinKeyOf(key, localCols)) ?? {});
6211
6637
  });
6212
6638
  return assembleAggregateRow(await loader.load(localValues), request, target);
6213
6639
  } catch (e) {
@@ -6233,12 +6659,12 @@ const HAVING_OPS = {
6233
6659
  };
6234
6660
  /**
6235
6661
  * Columns a query can group on. Grouping needs an equality operator, which is the same
6236
- * requirement min/max has, plus booleans — orderable excludes them because a min/max over
6662
+ * requirement countDistinct has, plus booleans — comparable excludes them because a min/max over
6237
6663
  * true/false says nothing, but `GROUP BY isConfirmed` is perfectly ordinary.
6238
6664
  */
6239
6665
  const groupableColumns = (table, tableName) => {
6240
- const { orderable } = classifyAggregateColumns(table, tableName);
6241
- const groupable = { ...orderable };
6666
+ const { comparable } = classifyAggregateColumns(table, tableName);
6667
+ const groupable = { ...comparable };
6242
6668
  for (const [columnName, column] of Object.entries(visibleColumns(table))) {
6243
6669
  if (groupable[columnName]) continue;
6244
6670
  const { type: dataType } = extractExtendedColumnType(column);
@@ -6279,7 +6705,7 @@ const aggregateNumberFilterType = (typeName) => sharedType(typeName, {
6279
6705
  * nothing to compare against.
6280
6706
  */
6281
6707
  const generateHavingInput = (table, tableName, typeName, cacheCtx) => {
6282
- const { numeric, orderable, all } = classifyAggregateColumns(table, tableName);
6708
+ const { numeric, comparable, all } = classifyAggregateColumns(table, tableName);
6283
6709
  const resolveName = cacheCtx?.typeName ?? ((info) => info.defaultName);
6284
6710
  const numberFilter = aggregateNumberFilterType(resolveName);
6285
6711
  const fields = { count: {
@@ -6292,7 +6718,7 @@ const generateHavingInput = (table, tableName, typeName, cacheCtx) => {
6292
6718
  min: numeric,
6293
6719
  max: numeric,
6294
6720
  countNonNull: all,
6295
- countDistinct: orderable
6721
+ countDistinct: comparable
6296
6722
  };
6297
6723
  for (const [op, columns] of Object.entries(opColumns)) {
6298
6724
  const columnNames = Object.keys(columns);
@@ -6357,22 +6783,34 @@ const generateGroupByType = (table, tableName, typeName, cacheCtx) => {
6357
6783
  }
6358
6784
  });
6359
6785
  };
6360
- /** Turns one `${typeName}Having` entry into SQL conditions over the matching aggregate expression. */
6361
- const havingComparisons = (expression, filter) => Object.entries(filter).filter(([op, value]) => value != null && op in HAVING_OPS).map(([op, value]) => HAVING_OPS[op](expression, value));
6786
+ /**
6787
+ * Turns one `${typeName}Having` entry into SQL conditions over the matching aggregate expression.
6788
+ *
6789
+ * The operand is a `Float`, but PostgreSQL types an untyped parameter after what it is compared
6790
+ * with: count and sum of integers are bigint, min/max of an int column int, so `gt: 1.5` would
6791
+ * fail to parse. There the aggregate is compared as `numeric`, which holds every one of them
6792
+ * exactly. MySQL and SQLite already compare numbers across types.
6793
+ */
6794
+ const havingComparisons = (expression, filter, isPg) => {
6795
+ const compared = isPg ? sql`cast(${expression} as numeric)` : expression;
6796
+ return Object.entries(filter).filter(([op, value]) => value != null && op in HAVING_OPS).map(([op, value]) => HAVING_OPS[op](compared, value));
6797
+ };
6362
6798
  const buildHavingCondition = (having, columns) => {
6363
6799
  if (!having) return;
6364
6800
  const conditions = [];
6801
+ const firstColumn = Object.values(columns)[0];
6802
+ const isPg = firstColumn ? columnDialect(firstColumn) === "pg" : false;
6365
6803
  for (const [key, value] of Object.entries(having)) {
6366
6804
  if (value == null) continue;
6367
6805
  if (key === "count") {
6368
- conditions.push(...havingComparisons(count(), value));
6806
+ conditions.push(...havingComparisons(count(), value, isPg));
6369
6807
  continue;
6370
6808
  }
6371
6809
  if (!AGGREGATE_OPS.includes(key)) continue;
6372
6810
  for (const [columnName, filter] of Object.entries(value)) {
6373
6811
  const column = columns[columnName];
6374
6812
  if (!column || filter == null) continue;
6375
- conditions.push(...havingComparisons(OP_FNS[key](column), filter));
6813
+ conditions.push(...havingComparisons(OP_FNS[key](column), filter, isPg));
6376
6814
  }
6377
6815
  }
6378
6816
  return conditions.length ? and(...conditions) : void 0;
@@ -6524,12 +6962,32 @@ const isOptionalOnInsert = (column) => !column.notNull || !!column.hasDefault ||
6524
6962
  * relation could come back without it. A `where` inherited from the reverse relation filters
6525
6963
  * the parent instead, and becomes a per-parent check.
6526
6964
  */
6527
- const ownWherePlan = (relEntry, parentTable, targetTable, localCol) => {
6965
+ const ownWherePlan = (relEntry, parentTable, targetTable, localCol, parentPrimaryKey) => {
6528
6966
  const condition = () => relationOwnCondition(relEntry, parentTable, targetTable, [localCol], [localCol]);
6529
6967
  if (!condition()) return {};
6530
- if (!isRelationFilterReversed(relEntry.relation ?? relEntry)) return { ownCondition: () => condition() };
6531
- return { parentShows: async (executor, key) => (await executor.select({ key: localCol }).from(parentTable).where(and(eq(localCol, key), condition())).limit(1)).length > 0 };
6968
+ const relation = relEntry.relation ?? relEntry;
6969
+ if (!isRelationFilterReversed(relation)) return { ownCondition: () => condition() };
6970
+ const parentColumns = getColumns(parentTable);
6971
+ const parentKeyColumns = Object.fromEntries(parentPrimaryKey.filter((name) => parentColumns[name]).map((name) => [name, parentColumns[name]]));
6972
+ const keyNames = Object.keys(parentKeyColumns);
6973
+ return {
6974
+ parentShows: async (executor, key) => (await executor.select({ key: localCol }).from(parentTable).where(and(eq(localCol, key), condition())).limit(1)).length > 0,
6975
+ parentHidden: async (executor, rows) => {
6976
+ if (!rows.length) return false;
6977
+ if (!keyNames.length || !rows.every((row) => keyNames.every((name) => name in row))) throw drizzleError(`Drizzle-GraphQL Error: cannot check the \`where\` of this relation: the written row cannot be identified by a primary key.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
6978
+ const keyOf = (row) => keyNames.map((name) => keyString(row[name])).join("\0");
6979
+ const match = or(...rows.map((row) => and(...keyNames.map((name) => eq(parentColumns[name], row[name])))));
6980
+ const shown = new Set((await executor.select(parentKeyColumns).from(parentTable).where(and(match, relationsFilterToSQL(parentTable, relationOwnWhere(relation))))).map(keyOf));
6981
+ return rows.some((row) => !shown.has(keyOf(row)));
6982
+ },
6983
+ parentKeyColumns
6984
+ };
6532
6985
  };
6986
+ /**
6987
+ * A key value in a form that compares equal across the two reads of it — the driver may hand
6988
+ * back a `Date` or a `bigint` that is a different object each time.
6989
+ */
6990
+ const keyString = (value) => value instanceof Date ? `d${value.getTime()}` : value === null || value === void 0 ? "" : `v${String(value)}`;
6533
6991
  /** The property name a column is stored under on its table, matched by identity. */
6534
6992
  const propNameOf = (table, column) => {
6535
6993
  if (!column) return;
@@ -6591,7 +7049,7 @@ const buildThroughPlan = (relationName, relEntry, relation, table, targetTableNa
6591
7049
  targetCol
6592
7050
  },
6593
7051
  targetKeyColumns: { [foreignColPropName]: foreignCol },
6594
- ...ownWherePlan(relEntry, table, targetTable, localCol),
7052
+ ...ownWherePlan(relEntry, table, targetTable, localCol, []),
6595
7053
  allowed: /* @__PURE__ */ new Set([
6596
7054
  "connect",
6597
7055
  "disconnect",
@@ -6670,7 +7128,7 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
6670
7128
  const targetPrimaryKey = resolvePrimaryKeys(targetTable).filter((name) => targetColumns[name]);
6671
7129
  const targetKeyColumns = Object.fromEntries(targetPrimaryKey.map((name) => [name, targetColumns[name]]));
6672
7130
  targetKeyColumns[foreignColPropName] = foreignCol;
6673
- const ownWhere = ownWherePlan(relEntry, table, targetTable, localCol);
7131
+ const ownWhere = ownWherePlan(relEntry, table, targetTable, localCol, resolvePrimaryKeys(table));
6674
7132
  const ops = !fkColumn.notNull ? isOne ? [
6675
7133
  "create",
6676
7134
  "connect",
@@ -7103,6 +7561,16 @@ const createNestedWriteRuntime = (params) => {
7103
7561
  }
7104
7562
  if (supplied(op["connect"])) await link(asArray(op["connect"]), "connect");
7105
7563
  };
7564
+ /**
7565
+ * The value a parent-side `connect` / `disconnect` writes to an existing row's key: `next`,
7566
+ * unless the key points at a row the relation's own `where` hides, which it then keeps.
7567
+ */
7568
+ const keepingHidden = (plan, next) => {
7569
+ if (!plan.ownCondition) return next;
7570
+ const shown = sql`select ${plan.foreignCol} from ${plan.targetTable} where ${plan.ownCondition()}`;
7571
+ const replacement = next === null ? sql`null` : sql`${sql.param(next, plan.localCol)}`;
7572
+ return sql`case when ${plan.localCol} is null or ${plan.localCol} in (${shown}) then ${replacement} else ${plan.localCol} end`;
7573
+ };
7106
7574
  const detachAll = (executor, plan, key, scope, steps, op) => steps.run(executor.update(plan.targetTable).set({ [plan.foreignColPropName]: null }).where(withScope(scope, plan.targetTableName, plan.targetTable, and(eq(plan.foreignCol, key), plan.ownCondition?.()))), op, "update");
7107
7575
  return {
7108
7576
  enabled: (tableName) => !!plans[tableName],
@@ -7128,7 +7596,10 @@ const createNestedWriteRuntime = (params) => {
7128
7596
  const allColumns = getColumns(table);
7129
7597
  for (const relationName of Object.keys(ops)) {
7130
7598
  const plan = tablePlans[relationName];
7131
- if (plan?.fkSide === "child" && !(plan.localColPropName in columns) && allColumns[plan.localColPropName]) columns[plan.localColPropName] = allColumns[plan.localColPropName];
7599
+ if (plan && !(plan.localColPropName in columns) && allColumns[plan.localColPropName]) columns[plan.localColPropName] = allColumns[plan.localColPropName];
7600
+ if (plan?.fkSide === "parent" && plan.parentHidden) {
7601
+ for (const [name, column] of Object.entries(plan.parentKeyColumns ?? {})) if (!(name in columns)) columns[name] = column;
7602
+ }
7132
7603
  }
7133
7604
  for (const name of selfTargetingPlan(tablePlans, tableName, ops)?.targetPrimaryKey ?? []) if (!(name in columns) && allColumns[name]) columns[name] = allColumns[name];
7134
7605
  return columns;
@@ -7148,10 +7619,15 @@ const createNestedWriteRuntime = (params) => {
7148
7619
  ...fresh.get(keyOf(row))
7149
7620
  }));
7150
7621
  },
7151
- applyParentSide: async (executor, tableName, ops, request) => {
7622
+ applyParentSide: async (executor, tableName, ops, request, existing = false) => {
7152
7623
  const tablePlans = plans[tableName];
7153
- const patch = {};
7154
- if (!tablePlans) return patch;
7624
+ const result = {
7625
+ values: {},
7626
+ expected: {},
7627
+ existing
7628
+ };
7629
+ const patch = result.values;
7630
+ if (!tablePlans) return result;
7155
7631
  const context = request?.context;
7156
7632
  const scope = resolveScope(policies, context, filterCtx);
7157
7633
  for (const [relationName, op] of Object.entries(ops)) {
@@ -7170,14 +7646,30 @@ const createNestedWriteRuntime = (params) => {
7170
7646
  if (!created) throw drizzleError(`Drizzle-GraphQL Error: 'create' on '${relationName}' inserted no row, so there is nothing to attach.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
7171
7647
  await steps.after();
7172
7648
  patch[plan.localColPropName] = created[plan.foreignColPropName];
7649
+ result.expected[relationName] = created[plan.foreignColPropName];
7173
7650
  } else if (op["connect"] !== void 0) {
7174
7651
  const rows = await executor.select({ [plan.foreignColPropName]: plan.foreignCol }).from(plan.targetTable).where(conditionOf(plan, op["connect"], "connect", scope));
7175
7652
  if (rows.length !== 1) throw connectMatchError(relationName, rows.length);
7176
- patch[plan.localColPropName] = rows[0][plan.foreignColPropName];
7177
- } else if (op["disconnect"] === true) patch[plan.localColPropName] = null;
7653
+ const key = rows[0][plan.foreignColPropName];
7654
+ patch[plan.localColPropName] = existing ? keepingHidden(plan, key) : key;
7655
+ result.expected[relationName] = key;
7656
+ } else if (op["disconnect"] === true) patch[plan.localColPropName] = existing ? keepingHidden(plan, null) : null;
7178
7657
  }
7179
7658
  for (const columnName of Object.keys(contextValues?.(tableName) ?? {})) delete patch[columnName];
7180
- return patch;
7659
+ return result;
7660
+ },
7661
+ checkParentSide: async (executor, tableName, ops, rows, patch) => {
7662
+ const tablePlans = plans[tableName];
7663
+ if (!tablePlans || !rows.length) return;
7664
+ for (const relationName of Object.keys(ops)) {
7665
+ const plan = tablePlans[relationName];
7666
+ if (plan?.fkSide !== "parent") continue;
7667
+ if (relationName in patch.expected) {
7668
+ const expected = keyString(patch.expected[relationName]);
7669
+ if (rows.some((row) => keyString(row[plan.localColPropName]) !== expected)) throw drizzleError(patch.existing ? `Drizzle-GraphQL Error: cannot connect through '${relationName}': the row it holds now is one its \`where\` hides, which a write through it cannot detach.` : `Drizzle-GraphQL Error: cannot write through '${relationName}': the ${tableName} row was not written with it — an upsert whose conflict branch did not write '${plan.localColPropName}'. Include it in onConflict.update, or leave update unset.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
7670
+ }
7671
+ if (plan.parentHidden && await plan.parentHidden(executor, rows)) throw drizzleError(`Drizzle-GraphQL Error: cannot write through '${relationName}': its \`where\` hides the relation on this ${tableName} row.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
7672
+ }
7181
7673
  },
7182
7674
  applyChildSide: async (executor, tableName, ops, parentRows, request) => {
7183
7675
  const tablePlans = plans[tableName];
@@ -7244,12 +7736,13 @@ var NoParentRow = class extends Error {};
7244
7736
  * `executor` must already be a transaction.
7245
7737
  */
7246
7738
  const writeNestedEntry = async (params) => {
7247
- const { executor, runtime, tableName, ops, write, request } = params;
7739
+ const { executor, runtime, tableName, ops, write, request, existing } = params;
7248
7740
  const guarded = runtime.createsParentSide(tableName, ops);
7249
7741
  const run = async (tx) => {
7250
- const patch = await runtime.applyParentSide(tx, tableName, ops, request);
7251
- const rows = await write(tx, patch);
7742
+ const patch = await runtime.applyParentSide(tx, tableName, ops, request, existing);
7743
+ const rows = await write(tx, patch.values);
7252
7744
  if (guarded && !rows.length) throw new NoParentRow();
7745
+ await runtime.checkParentSide(tx, tableName, ops, rows, patch);
7253
7746
  await runtime.applyChildSide(tx, tableName, ops, rows, request);
7254
7747
  return runtime.refreshed(tx, tableName, ops, rows);
7255
7748
  };
@@ -7311,7 +7804,8 @@ const updateWithNestedOps = async (params) => {
7311
7804
  ...remapValues(columns),
7312
7805
  ...patch
7313
7806
  }),
7314
- request
7807
+ request,
7808
+ existing: true
7315
7809
  }));
7316
7810
  };
7317
7811
  //#endregion
@@ -7464,6 +7958,7 @@ const createSchemaBuilder = (adapter) => {
7464
7958
  const anyTable = (feature) => tableEntries.some(([name]) => featureOf(name)[feature]);
7465
7959
  if (!tableEntries.length) throw new Error("Drizzle-GraphQL Error: No tables detected in Drizzle-ORM's database instance. Did you forget to pass schema to drizzle constructor?");
7466
7960
  registerScalarOverrides(tables, options);
7961
+ installSqliteBlobJsonDecoders(tables);
7467
7962
  registerEnumConfig(options);
7468
7963
  registerColumnExclusions(tables, options.exclude);
7469
7964
  resetAggregateColumnCache();
@@ -7695,7 +8190,7 @@ const createSchemaBuilder = (adapter) => {
7695
8190
  ...tableFeatures.update ? [updateInput] : [],
7696
8191
  ...built.updateManyInput ? [built.updateManyInput] : [],
7697
8192
  tableFilters,
7698
- tableOrder
8193
+ ...tableOrder ? [tableOrder] : []
7699
8194
  ].forEach((e) => {
7700
8195
  inputs[e.name] = e;
7701
8196
  });
@@ -7825,14 +8320,12 @@ const generateUpsert = (db, tableName, table, baseType, onConflictType, fieldNam
7825
8320
  withTarget: false,
7826
8321
  serverOwned: Object.keys(contextColumns ?? {})
7827
8322
  });
7828
- const requestedNothing = args.onConflict?.action === "NOTHING";
7829
8323
  const runBatch = async (target, { values, plan }) => {
7830
- if (plan.action === "NOTHING" && !requestedNothing) {
8324
+ if (plan.action === "NOTHING") {
7831
8325
  const columns = getColumns(table);
7832
8326
  const keyName = pkNames[0] ?? Object.keys(columns)[0];
7833
8327
  await target.insert(table).values(values).onDuplicateKeyUpdate({ set: { [keyName]: sql`${sql.identifier(columns[keyName].name)}` } });
7834
- } else if (plan.action === "NOTHING") await target.insert(table).ignore().values(values);
7835
- else await target.insert(table).values(values).onDuplicateKeyUpdate({ set: plan.set });
8328
+ } else await target.insert(table).values(values).onDuplicateKeyUpdate({ set: plan.set });
7836
8329
  };
7837
8330
  if (batches.length === 1) await runBatch(executor, batches[0]);
7838
8331
  else await executor.transaction(async (tx) => {
@@ -7872,7 +8365,7 @@ const generateUpdate = (db, tableName, table, setArgs, filterArgs, fieldName, si
7872
8365
  });
7873
8366
  if (single) await assertSingleMatch(executor, table, filters);
7874
8367
  let query = executor.update(table).set(input);
7875
- if (filters) query = query.where(filters);
8368
+ if (filters) query = query.where(mysqlWriteFilter(table, filters));
7876
8369
  await query;
7877
8370
  await after();
7878
8371
  return isSuccess;
@@ -7887,7 +8380,7 @@ const generateUpdate = (db, tableName, table, setArgs, filterArgs, fieldName, si
7887
8380
  * in order. MySQL has no RETURNING, so — like every other MySQL mutation — the result is
7888
8381
  * `{ isSuccess: true }` rather than the updated rows.
7889
8382
  */
7890
- const generateUpdateMany$2 = (db, tableName, table, updateManyInput, fieldName, filterCtx, txCtx, policies) => writeResolver({
8383
+ const generateUpdateMany$2 = (db, tableName, table, updateManyInput, fieldName, requireWhere, filterCtx, txCtx, policies) => writeResolver({
7891
8384
  db,
7892
8385
  tableName,
7893
8386
  operation: "updateMany",
@@ -7907,13 +8400,20 @@ const generateUpdateMany$2 = (db, tableName, table, updateManyInput, fieldName,
7907
8400
  if (!Object.keys(input).length) throw drizzleError("Unable to update with no values specified!", { code: "DRIZZLE_NO_VALUES" });
7908
8401
  return {
7909
8402
  set: input,
7910
- filters: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope)) : void 0)
8403
+ filters: scopedWhere({
8404
+ scope,
8405
+ tableName,
8406
+ table,
8407
+ where,
8408
+ relationCtx: relationFilterCtx(filterCtx, tableName, scope),
8409
+ required: requireWhere
8410
+ })
7911
8411
  };
7912
8412
  });
7913
8413
  await executor.transaction(async (tx) => {
7914
8414
  for (const entry of entries) {
7915
8415
  let query = tx.update(table).set(entry.set);
7916
- if (entry.filters) query = query.where(entry.filters);
8416
+ if (entry.filters) query = query.where(mysqlWriteFilter(table, entry.filters));
7917
8417
  await query;
7918
8418
  }
7919
8419
  });
@@ -7962,7 +8462,7 @@ const generateDelete = (db, tableName, table, filterArgs, fieldName, single, req
7962
8462
  });
7963
8463
  if (single) await assertSingleMatch(executor, table, filters);
7964
8464
  let query = softDelete && !hard ? executor.update(table).set({ [softDelete.columnName]: restore ? softDelete.writeRestored : softDelete.writeDeleted() }) : executor.delete(table);
7965
- if (filters) query = query.where(filters);
8465
+ if (filters) query = query.where(mysqlWriteFilter(table, filters));
7966
8466
  await query;
7967
8467
  await after();
7968
8468
  return isSuccess;
@@ -7973,6 +8473,34 @@ const generateDelete = (db, tableName, table, filterArgs, fieldName, single, req
7973
8473
  const getTableConfig$5 = memoizeTableConfig(getTableConfig);
7974
8474
  /** Primary-key property names for a MySQL table, including table-level composite keys. */
7975
8475
  const mysqlPrimaryKeyPropNames = (table) => getPrimaryKeyPropNamesFromConfig(table, getTableConfig$5);
8476
+ const TABLE_ORIGINAL_NAME = Symbol.for("drizzle:OriginalName");
8477
+ const TABLE_SCHEMA = Symbol.for("drizzle:Schema");
8478
+ /** Whether `chunk` reads `table` anywhere — under an alias, in a subquery, however deep. */
8479
+ const readsTable = (chunk, table) => {
8480
+ if (Array.isArray(chunk)) return chunk.some((part) => readsTable(part, table));
8481
+ if (is(chunk, Table)) return chunk[TABLE_ORIGINAL_NAME] === table[TABLE_ORIGINAL_NAME] && chunk[TABLE_SCHEMA] === table[TABLE_SCHEMA];
8482
+ if (is(chunk, SQL)) return readsTable(chunk.queryChunks, table);
8483
+ if (is(chunk, SQL.Aliased)) return readsTable(chunk.sql, table);
8484
+ if (is(chunk, Subquery)) return readsTable(chunk._.sql, table);
8485
+ return false;
8486
+ };
8487
+ /**
8488
+ * The `where` of an UPDATE or DELETE, in a form MySQL accepts. MySQL refuses a statement whose
8489
+ * `where` reads the table it writes (error 1093, "You can't specify target table … for update
8490
+ * in FROM clause") — which a relation filter through a self-referencing relation, or a scope
8491
+ * written as one, does. Such a filter is resolved to the primary keys it matches first, through
8492
+ * a derived table MySQL materializes (`DISTINCT` keeps the optimizer from merging it back in),
8493
+ * and the write matches those keys. Any other filter is returned as it is.
8494
+ */
8495
+ const mysqlWriteFilter = (table, filters) => {
8496
+ if (!filters || !readsTable(filters, table)) return filters;
8497
+ const columns = getColumns(table);
8498
+ const keys = mysqlPrimaryKeyPropNames(table).map((name) => columns[name]);
8499
+ if (!keys.length) return filters;
8500
+ const keyList = sql.join(keys.map((column) => sql.identifier(column.name)), sql`, `);
8501
+ const target = keys.length === 1 ? sql`${keys[0]}` : sql`(${sql.join(keys, sql`, `)})`;
8502
+ return sql`${target} in (select ${keyList} from (select distinct ${sql.join(keys, sql`, `)} from ${table} where ${filters}) as ${sql.identifier("dgql_write_target")})`;
8503
+ };
7976
8504
  const { prepareBuild, forEachTable, finalizeBuild } = createSchemaBuilder({
7977
8505
  tableClass: MySqlTable,
7978
8506
  getTableConfig: getTableConfig$5,
@@ -8026,7 +8554,7 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
8026
8554
  tableFilters,
8027
8555
  cacheCtx
8028
8556
  }) : void 0;
8029
- const updateManyGenerated = updateManyInput ? generateUpdateMany$2(db, tableName, schema[tableName], updateManyInput, updateManyFieldName, filterCtx, mutationTxCtx, policies) : void 0;
8557
+ const updateManyGenerated = updateManyInput ? generateUpdateMany$2(db, tableName, schema[tableName], updateManyInput, updateManyFieldName, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies) : void 0;
8030
8558
  const deleteGenerated = tableFeatures.delete ? generateDelete(db, tableName, schema[tableName], tableFilters, deleteFieldName, false, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies) : void 0;
8031
8559
  const deleteSingleGenerated = tableFeatures.delete ? generateDelete(db, tableName, schema[tableName], tableFilters, deleteSingleFieldName, true, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies) : void 0;
8032
8560
  const restoreGenerated = softDeleteInfo ? generateDelete(db, tableName, schema[tableName], tableFilters, restoreFieldName, false, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies, true) : void 0;
@@ -8043,7 +8571,8 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
8043
8571
  filterCtx,
8044
8572
  txCtx: mutationTxCtx,
8045
8573
  policies,
8046
- returnsRows: false
8574
+ returnsRows: false,
8575
+ writeFilter: (filters) => mysqlWriteFilter(schema[tableName], filters)
8047
8576
  }) : void 0;
8048
8577
  const deleteCountGenerated = tableFeatures.delete && tableFeatures.countMutations ? generateWriteCount({
8049
8578
  db,
@@ -8056,7 +8585,8 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
8056
8585
  filterCtx,
8057
8586
  txCtx: mutationTxCtx,
8058
8587
  policies,
8059
- returnsRows: false
8588
+ returnsRows: false,
8589
+ writeFilter: (filters) => mysqlWriteFilter(schema[tableName], filters)
8060
8590
  }) : void 0;
8061
8591
  const hardDeleteMeta = softDeleteInfo?.hardDelete ? { hardDelete: true } : {};
8062
8592
  const withMeta = (generated, meta) => ({
@@ -8168,7 +8698,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8168
8698
  * one-element list.
8169
8699
  */
8170
8700
  const generateInsert = (build, opts) => {
8171
- const { db, tables, relationMap, typeNameMapper, txCtx, nested, limits, policies, resolveName } = build;
8701
+ const { db, tables, relationMap, typeNameMapper, filterCtx, txCtx, nested, limits, policies, resolveName } = build;
8172
8702
  const { tableName, table, baseType, fieldName, typeName, single, conflictDoNothing = false } = opts;
8173
8703
  const queryArgs = { values: { type: single ? new GraphQLNonNull(baseType) : new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(baseType))) } };
8174
8704
  const pkNames = primaryKeyPropNames(table);
@@ -8192,6 +8722,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8192
8722
  const input = nestedEntries ? [] : applyContextValuesAll(remapFromGraphQLArrayInput(entries ? entries.map((entry) => entry.columns) : supplied, table), contextColumns, context);
8193
8723
  const { columns, hasRelations, withParams } = mutationSelection(info, {
8194
8724
  relationMap,
8725
+ filterCtx,
8195
8726
  tables,
8196
8727
  tableName,
8197
8728
  typeName,
@@ -8270,6 +8801,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8270
8801
  const input = nestedEntries ? [] : applyContextValuesAll(remapFromGraphQLArrayInput(entries ? entries.map((entry) => entry.columns) : supplied, table), contextColumns, context);
8271
8802
  const { columns, hasRelations, withParams } = mutationSelection(info, {
8272
8803
  relationMap,
8804
+ filterCtx,
8273
8805
  tables,
8274
8806
  tableName,
8275
8807
  typeName,
@@ -8294,24 +8826,25 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8294
8826
  serverOwned: Object.keys(contextColumns ?? {}),
8295
8827
  buildWhere: (where) => extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope))
8296
8828
  });
8297
- const runBatch = async (executor, { values: rows, plan }) => {
8298
- let query = executor.insert(table).values(rows).returning(returning);
8829
+ const batchQuery = (executor, { values: rows, plan }) => {
8830
+ const query = executor.insert(table).values(rows).returning(returning);
8299
8831
  const setWhere = withScope(scope, tableName, table, plan.setWhere);
8300
- query = plan.action === "NOTHING" ? query.onConflictDoNothing(plan.target ? {
8301
- target: plan.target,
8302
- where: plan.targetWhere
8303
- } : void 0) : query.onConflictDoUpdate({
8304
- target: plan.target,
8305
- targetWhere: plan.targetWhere,
8306
- set: plan.set,
8307
- setWhere
8308
- });
8309
- return await query;
8832
+ return applyConflictPlan(query, table, plan, setWhere);
8310
8833
  };
8311
- if (batches.length === 1) return runBatch(target, batches[0]);
8312
- return target.transaction(async (tx) => {
8313
- const rows = [];
8314
- for (const batch of batches) rows.push(...await runBatch(tx, batch));
8834
+ if (batches.length === 1) return await batchQuery(target, batches[0]);
8835
+ return target.transaction((tx) => {
8836
+ const execute = (batch) => {
8837
+ const query = batchQuery(tx, batch);
8838
+ return typeof query.all === "function" ? query.all() : query;
8839
+ };
8840
+ const first = execute(batches[0]);
8841
+ if (typeof first?.then === "function") return (async () => {
8842
+ const rows = [...await first];
8843
+ for (const batch of batches.slice(1)) rows.push(...await execute(batch));
8844
+ return rows;
8845
+ })();
8846
+ const rows = [...first];
8847
+ for (const batch of batches.slice(1)) rows.push(...execute(batch));
8315
8848
  return rows;
8316
8849
  });
8317
8850
  };
@@ -8359,6 +8892,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8359
8892
  await before();
8360
8893
  const { columns, hasRelations, withParams } = mutationSelection(info, {
8361
8894
  relationMap,
8895
+ filterCtx,
8362
8896
  tables,
8363
8897
  tableName,
8364
8898
  typeName,
@@ -8597,7 +9131,8 @@ const createSchemaDataGenerator = (adapter) => {
8597
9131
  table: schema[tableName],
8598
9132
  updateManyInput,
8599
9133
  fieldName: updateManyFieldName,
8600
- typeName
9134
+ typeName,
9135
+ requireWhere: tableFeatures.requireWhere
8601
9136
  }) : void 0;
8602
9137
  const deleteGenerated = tableFeatures.delete ? generateDelete(writeBuild, {
8603
9138
  tableName,
@@ -8852,7 +9387,8 @@ const runUpdateManyBatch = ({ executor, entries, table, returning, nested, table
8852
9387
  tableName,
8853
9388
  ops: entry.ops,
8854
9389
  write,
8855
- request
9390
+ request,
9391
+ existing: true
8856
9392
  }) : await write(tx, {}));
8857
9393
  }
8858
9394
  return results;
@@ -8873,7 +9409,7 @@ const runUpdateManyBatch = ({ executor, entries, table, returning, nested, table
8873
9409
  const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateManyBatch) => {
8874
9410
  return (build, opts) => {
8875
9411
  const { db, tables, relationMap, typeNameMapper, filterCtx, txCtx, nested, limits, policies, resolveName } = build;
8876
- const { tableName, table, updateManyInput, fieldName, typeName } = opts;
9412
+ const { tableName, table, updateManyInput, fieldName, typeName, requireWhere } = opts;
8877
9413
  const queryArgs = { updates: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(updateManyInput))) } };
8878
9414
  const pkNames = primaryKeyPropNames(table);
8879
9415
  return writeResolver({
@@ -8893,6 +9429,7 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
8893
9429
  const contextColumns = policies?.contextValues?.(tableName);
8894
9430
  const { columns, hasRelations, withParams } = mutationSelection(info, {
8895
9431
  relationMap,
9432
+ filterCtx,
8896
9433
  tables,
8897
9434
  tableName,
8898
9435
  typeName,
@@ -8912,7 +9449,14 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
8912
9449
  return {
8913
9450
  set: input,
8914
9451
  ops,
8915
- filters: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope)) : void 0)
9452
+ filters: scopedWhere({
9453
+ scope,
9454
+ tableName,
9455
+ table,
9456
+ where,
9457
+ relationCtx: relationFilterCtx(filterCtx, tableName, scope),
9458
+ required: requireWhere
9459
+ })
8916
9460
  };
8917
9461
  });
8918
9462
  const anyNested = entries.some((entry) => entry.ops);
@@ -9001,7 +9545,8 @@ const runSqliteUpdateManyBatch = ({ executor, entries, table, returning, anyNest
9001
9545
  tableName,
9002
9546
  ops: entry.ops,
9003
9547
  write,
9004
- request
9548
+ request,
9549
+ existing: true
9005
9550
  }) : write(tx, {});
9006
9551
  };
9007
9552
  return executor.transaction((tx) => {
@@ -9031,6 +9576,7 @@ const sqliteSchemaData = createSchemaDataGenerator({
9031
9576
  preflight: (db, options) => {
9032
9577
  if (options.features.nestedWrites && db.resultKind === "sync") throw new Error("Drizzle-GraphQL Error: features.nestedWrites requires an asynchronous SQLite driver (e.g. libsql). Synchronous drivers cannot run the multi-statement transaction a nested write needs.");
9033
9578
  if (options.transactions && db.resultKind === "sync") throw new Error("Drizzle-GraphQL Error: transactions: 'auto' requires an asynchronous SQLite driver (e.g. libsql). Synchronous drivers cannot hold a transaction open across resolvers.");
9579
+ if (options.policies?.onWrite && db.resultKind === "sync") throw new Error("Drizzle-GraphQL Error: onWrite requires an asynchronous SQLite driver (e.g. libsql). Synchronous drivers commit before an awaited hook runs, so a throwing hook could not roll the mutation back.");
9034
9580
  },
9035
9581
  generateUpdateMany
9036
9582
  });