@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.cjs CHANGED
@@ -286,7 +286,6 @@ const singularizeMapper = (tableName) => ({
286
286
  });
287
287
  //#endregion
288
288
  //#region src/util/type-converter/index.ts
289
- const allowedNameChars = /^[a-zA-Z0-9_]+$/;
290
289
  /** drizzle's `number` constraints whose values always fit a GraphQL `Int` (32-bit signed). */
291
290
  const smallIntegerConstraints = /* @__PURE__ */ new Set([
292
291
  "int8",
@@ -312,8 +311,15 @@ const declaredEnum = (column) => {
312
311
  schema: typeof declared.schema === "string" ? declared.schema : void 0
313
312
  } : void 0;
314
313
  };
315
- /** `user_status` / `user-status` / `user status` → `UserStatus`; an already-PascalCase name is left alone. */
316
- const pascalize = (input) => input.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => capitalize(part)).join("");
314
+ /**
315
+ * `user_status` / `user-status` / `user status` → `UserStatus`; an already-PascalCase name is left
316
+ * alone. A GraphQL name cannot start with a digit, so a name that would (`2fa_mode`) is prefixed
317
+ * with an underscore: `_2faMode`.
318
+ */
319
+ const pascalize = (input) => {
320
+ const pascal = input.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => capitalize(part)).join("");
321
+ return /^[0-9]/.test(pascal) ? `_${pascal}` : pascal;
322
+ };
317
323
  /**
318
324
  * Identity of the values a GraphQL enum was built from, so a name claimed twice can be told
319
325
  * apart from the same enum reached twice.
@@ -388,7 +394,7 @@ const generateEnumCached = (column, columnName, tableName) => {
388
394
  index,
389
395
  typeName: name
390
396
  });
391
- if (mapped === void 0) return allowedNameChars.test(value) ? value : `Option${index}`;
397
+ if (mapped === void 0) return validEnumValueName(value) ? value : `Option${index}`;
392
398
  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.`);
393
399
  return mapped;
394
400
  });
@@ -484,6 +490,27 @@ const sqliteTimestampToDate = (column, value) => new Date(column.mode === "times
484
490
  * mode it uses, for the same reason as {@link isDateOnlyColumn}.
485
491
  */
486
492
  const isDateTimeColumn = (column) => (0, drizzle_orm.is)(column, drizzle_orm_pg_core.PgTimestamp) || isPgTimestampStringColumn(column) || (0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlTimestamp) || (0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlDateTime) || isMySqlDateTimeStringColumn(column) || isSQLiteTimestampColumn(column);
493
+ /**
494
+ * Whether the column holds raw bytes: Postgres `bytea` and SQLite `blob({ mode: 'buffer' })`,
495
+ * both `object buffer` in drizzle v1. Their values are `[Int!]` lists of the bytes.
496
+ */
497
+ const isBufferColumn = (column) => (column.dataType ?? "").endsWith(" buffer");
498
+ /**
499
+ * Whether the column is a float tuple or vector: drizzle v1 gives every such column an
500
+ * `array …` data type — `point` / `line` in tuple mode (`array point`, `array line`), PostGIS
501
+ * `geometry` in tuple mode, pgvector `vector` and `halfvec`. Each value is one list of numbers,
502
+ * typed `[Float!]`, not a database array of elements.
503
+ */
504
+ const isFloatTupleColumn = (column) => (column.dataType ?? "").startsWith("array ");
505
+ /** The fixed length of a float tuple column's values, or `undefined` for a vector. */
506
+ const floatTupleLength = (column) => {
507
+ switch (column.dataType) {
508
+ case "array point":
509
+ case "array geometry": return 2;
510
+ case "array line": return 3;
511
+ default: return;
512
+ }
513
+ };
487
514
  const columnToGraphQLCore = (column, columnName, tableName, isInput) => {
488
515
  const dimensions = column.dimensions;
489
516
  if (dimensions !== void 0 && dimensions > 0) {
@@ -521,7 +548,7 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
521
548
  return column.columnType === "PgGeometryObject" ? {
522
549
  type: isInput ? geoXyInputType : geoXyType,
523
550
  typeLabel: "Geometry points XY"
524
- } : column.columnType === "PgBytea" ? {
551
+ } : isBufferColumn(column) ? {
525
552
  type: new graphql.GraphQLList(new graphql.GraphQLNonNull(graphql.GraphQLInt)),
526
553
  typeLabel: "Buffer"
527
554
  } : {
@@ -554,7 +581,7 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
554
581
  type: GraphQLBigIntString,
555
582
  typeLabel: "BigInt"
556
583
  };
557
- case "number": return constraint !== void 0 && smallIntegerConstraints.has(constraint) || (0, drizzle_orm.is)(column, drizzle_orm_pg_core.PgInteger) || (0, drizzle_orm.is)(column, drizzle_orm_pg_core.PgSerial) || (0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlInt) || (0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlSerial) || (0, drizzle_orm.is)(column, drizzle_orm_sqlite_core.SQLiteInteger) ? {
584
+ case "number": return constraint !== void 0 && smallIntegerConstraints.has(constraint) || (0, drizzle_orm.is)(column, drizzle_orm_pg_core.PgInteger) || (0, drizzle_orm.is)(column, drizzle_orm_pg_core.PgSerial) || (0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlInt) || (0, drizzle_orm.is)(column, drizzle_orm_mysql_core.MySqlSerial) ? {
558
585
  type: graphql.GraphQLInt,
559
586
  typeLabel: "Integer"
560
587
  } : {
@@ -562,18 +589,10 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
562
589
  typeLabel: "Float"
563
590
  };
564
591
  case "array": {
565
- if (column.columnType === "PgVector") return {
566
- type: new graphql.GraphQLList(new graphql.GraphQLNonNull(graphql.GraphQLFloat)),
567
- typeLabel: "Array<Float>"
568
- };
569
- if (column.columnType === "PgGeometry") return {
570
- type: new graphql.GraphQLList(new graphql.GraphQLNonNull(graphql.GraphQLFloat)),
571
- typeLabel: "Tuple<[Float, Float]>"
572
- };
573
- const innerType = columnToGraphQLCore(column.baseColumn, columnName, tableName, isInput);
592
+ const length = floatTupleLength(column);
574
593
  return {
575
- type: new graphql.GraphQLList(new graphql.GraphQLNonNull(innerType.type)),
576
- typeLabel: `Array<${innerType.typeLabel}>`
594
+ type: new graphql.GraphQLList(new graphql.GraphQLNonNull(graphql.GraphQLFloat)),
595
+ typeLabel: length === void 0 ? "Array<Float>" : `Tuple<[${Array(length).fill("Float").join(", ")}]>`
577
596
  };
578
597
  }
579
598
  default: throw new Error(`Drizzle-GraphQL Error: Type ${column.dataType} is not implemented!`);
@@ -715,6 +734,18 @@ const columnDialect = (column) => {
715
734
  */
716
735
  const isJsonColumn = (column) => (column.dataType ?? "").includes("json") && column.columnType !== "PgGeometryObject";
717
736
  /**
737
+ * SQLite `blob({ mode: 'bigint' })`: the bigint is stored as the bytes of its decimal text, and
738
+ * SQLite compares blobs byte by byte, so `9` sorts above `10`. Equality still holds, so it can be
739
+ * grouped and made distinct on, but not ordered or given a min/max.
740
+ */
741
+ const isSqliteBlobBigIntColumn = (column) => column.columnType === "SQLiteBigInt";
742
+ /**
743
+ * Whether `orderBy` (and so cursor pagination) offers a column. JSON has no meaningful order:
744
+ * PostgreSQL `json` cannot be ordered at all, and MySQL's JSON ordering disagrees with any
745
+ * keyset comparison a cursor could make. A SQLite blob bigint orders as text.
746
+ */
747
+ const isOrderableColumn = (column) => !isJsonColumn(column) && !isSqliteBlobBigIntColumn(column);
748
+ /**
718
749
  * Maps a Drizzle column to the generic filter type to use.
719
750
  *
720
751
  * Selection is keyed on the column's data type, never its name: a text column named
@@ -1221,6 +1252,36 @@ const pgTimestampTextToIso = (value) => {
1221
1252
  const utc = (/* @__PURE__ */ new Date(wallMs - offsetSeconds * 1e3)).toISOString();
1222
1253
  return utc.length === 24 ? `${utc.slice(0, 19)}${fraction}Z` : value;
1223
1254
  };
1255
+ /** Marks a decoder installed by {@link installSqliteBlobJsonDecoders}, so a rebuild never wraps it twice. */
1256
+ const BLOB_JSON_DECODER = Symbol.for("drizzle-graphql.sqliteBlobJsonDecoder");
1257
+ /**
1258
+ * Gives SQLite's blob columns a decoder for the relational query builder's JSON path, so a NULL
1259
+ * there reads back as null.
1260
+ *
1261
+ * Inside a nested relation drizzle selects a blob as `hex(column)` (JSON cannot hold a blob), and
1262
+ * `hex(NULL)` is `''`. drizzle then decodes that `''` with the column's ordinary decoder: a
1263
+ * `blob({ mode: 'bigint' })` reads it as `0n` — indistinguishable from a stored 0 — and a
1264
+ * `blob({ mode: 'json' })` throws on it. drizzle consults a column's `mapFromJsonValue`, when it
1265
+ * has one, instead of `mapFromDriverValue` for exactly this path, so one is installed here that
1266
+ * reads `''` as NULL and hands everything else to the column's own decoder.
1267
+ *
1268
+ * `''` is never a stored bigint or JSON blob, so for those it can only be NULL. For a buffer
1269
+ * blob it is also how an empty blob arrives, so it is read as NULL only when the column is
1270
+ * nullable — a `notNull` column cannot hold the NULL, so there it is the empty blob.
1271
+ */
1272
+ const installSqliteBlobJsonDecoders = (tables) => {
1273
+ for (const table of Object.values(tables)) for (const column of Object.values((0, drizzle_orm.getColumns)(table))) {
1274
+ const columnType = column.columnType ?? "";
1275
+ const isBuffer = columnType === "SQLiteBlobBuffer";
1276
+ if (columnType !== "SQLiteBigInt" && columnType !== "SQLiteBlobJson" && !isBuffer) continue;
1277
+ const existing = column.mapFromJsonValue;
1278
+ if (existing && !existing[BLOB_JSON_DECODER]) continue;
1279
+ const emptyIsNull = !isBuffer || !column.notNull;
1280
+ const decoder = (value) => value === "" && emptyIsNull ? null : column.mapFromDriverValue(value);
1281
+ decoder[BLOB_JSON_DECODER] = true;
1282
+ column.mapFromJsonValue = decoder;
1283
+ }
1284
+ };
1224
1285
  const remapToGraphQLCore = (key, value, tableName, column, relationMap) => {
1225
1286
  if (Array.isArray(value)) {
1226
1287
  const relations = relationMap?.[tableName];
@@ -1247,18 +1308,51 @@ const remapToGraphQLCore = (key, value, tableName, column, relationMap) => {
1247
1308
  if (value instanceof Date) return value.toISOString();
1248
1309
  if (typeof value === "string" && isMySqlDateTimeStringColumn(column)) return mySqlDateTimeTextToIso(value);
1249
1310
  if (typeof value === "string" && isPgTimestampStringColumn(column)) return pgTimestampTextToIso(value);
1250
- if (value instanceof Buffer) return Array.from(value);
1311
+ if (value instanceof Uint8Array) return Array.from(value);
1251
1312
  if (typeof value === "bigint") return value.toString();
1252
1313
  if (Array.isArray(value)) {
1253
- if (column.columnType === "PgGeometry" || column.columnType === "PgVector") return value;
1314
+ if (isFloatTupleColumn(column)) return value;
1254
1315
  return value.map((arrVal) => remapToGraphQLCore(key, arrVal, tableName, column, relationMap));
1255
1316
  }
1256
1317
  if (typeof value === "object" && value !== null) {
1257
- if (column.columnType === "PgGeometryObject") return value;
1318
+ if ((column.dataType ?? "").startsWith("object")) return value;
1258
1319
  return JSON.stringify(value);
1259
1320
  }
1260
1321
  return value;
1261
1322
  };
1323
+ /**
1324
+ * Where a remapped row keeps the values its columns held before remapping, by property name —
1325
+ * only for the columns remapping changed. The output form is not always reversible: a
1326
+ * microsecond timestamp read in `'string'` mode leaves as an ISO string the `DateTime` input
1327
+ * side would cut to milliseconds. A relation field correlating on the row's join columns reads
1328
+ * them from here (see `parentJoinValues`). Non-enumerable, so the row's own keys, JSON form and
1329
+ * equality are unchanged.
1330
+ */
1331
+ const RAW_COLUMN_VALUES = Symbol("drizzle-graphql.rawColumnValues");
1332
+ /**
1333
+ * The value column `key` held in `row` before remapping: `{ found: true, value }` when remapping
1334
+ * changed it, `{ found: false }` when it did not (or the row was never remapped), in which case
1335
+ * `row[key]` is still the column's own value.
1336
+ */
1337
+ const rawColumnValue = (row, key) => {
1338
+ const raw = row[RAW_COLUMN_VALUES];
1339
+ return raw && key in raw ? {
1340
+ found: true,
1341
+ value: raw[key]
1342
+ } : { found: false };
1343
+ };
1344
+ const keepRawColumnValue = (row, key, value) => {
1345
+ let raw = row[RAW_COLUMN_VALUES];
1346
+ if (!raw) {
1347
+ raw = Object.create(null);
1348
+ Object.defineProperty(row, RAW_COLUMN_VALUES, {
1349
+ value: raw,
1350
+ enumerable: false,
1351
+ configurable: true
1352
+ });
1353
+ }
1354
+ if (!(key in raw)) raw[key] = value;
1355
+ };
1262
1356
  const remapToGraphQLSingleOutput = (queryOutput, tableName, table, relationMap) => {
1263
1357
  const columns = (0, drizzle_orm.getColumns)(table);
1264
1358
  for (const [key, value] of Object.entries(queryOutput)) {
@@ -1272,11 +1366,9 @@ const remapToGraphQLSingleOutput = (queryOutput, tableName, table, relationMap)
1272
1366
  continue;
1273
1367
  }
1274
1368
  const column = columns[key];
1275
- if (value === 0n && column && column.columnType === "SQLiteBigInt" && !column.notNull) {
1276
- delete queryOutput[key];
1277
- continue;
1278
- }
1279
- queryOutput[key] = remapToGraphQLCore(key, value, tableName, column, relationMap);
1369
+ const remapped = remapToGraphQLCore(key, value, tableName, column, relationMap);
1370
+ if (column && remapped !== value) keepRawColumnValue(queryOutput, key, value);
1371
+ queryOutput[key] = remapped;
1280
1372
  }
1281
1373
  return queryOutput;
1282
1374
  };
@@ -1293,6 +1385,7 @@ const remapFromGraphQLCore = (value, column, columnName) => {
1293
1385
  const dataType = column.dataType ?? "";
1294
1386
  const columnType = column.columnType ?? "";
1295
1387
  if (isDateTimeColumn(column)) {
1388
+ if (Array.isArray(value)) return value.map((element) => remapFromGraphQLCore(element, column, columnName));
1296
1389
  const isMySql = columnType.startsWith("MySql");
1297
1390
  const isPgString = isPgTimestampStringColumn(column);
1298
1391
  const toIso = isMySql ? mySqlDateTimeTextToIso : isPgString ? pgTimestampTextToIso : void 0;
@@ -1307,41 +1400,29 @@ const remapFromGraphQLCore = (value, column, columnName) => {
1307
1400
  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" });
1308
1401
  return day;
1309
1402
  }
1310
- if (dataType.includes("bigint")) try {
1311
- return BigInt(value);
1312
- } catch {
1313
- throw drizzleError(`Field '${columnName}' is not a BigInt!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1314
- }
1315
- if (dataType.includes("json") && column.columnType !== "PgGeometryObject") return value;
1316
- switch (dataType) {
1317
- case "date": {
1318
- const formatted = new Date(value);
1319
- if (Number.isNaN(formatted.getTime())) throw drizzleError(`Field '${columnName}' is not a valid date!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1320
- return formatted;
1321
- }
1322
- case "buffer":
1323
- if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1324
- return Buffer.from(value);
1325
- case "json":
1326
- if (column.columnType === "PgGeometryObject") return value;
1327
- try {
1328
- return JSON.parse(value);
1329
- } catch (e) {
1330
- throw drizzleError(`Invalid JSON in field '${columnName}':\n${e instanceof Error ? e.message : "Unknown error"}`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1331
- }
1332
- case "array":
1333
- if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1334
- 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" });
1335
- return value;
1336
- case "bigint": try {
1403
+ if (dataType.includes("bigint")) {
1404
+ if (Array.isArray(value)) return value.map((element) => remapFromGraphQLCore(element, column, columnName));
1405
+ try {
1337
1406
  return BigInt(value);
1338
- } catch (_error) {
1407
+ } catch {
1339
1408
  throw drizzleError(`Field '${columnName}' is not a BigInt!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1340
1409
  }
1341
- default:
1342
- if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === null) return Object.assign({}, value);
1343
- return value;
1344
1410
  }
1411
+ if (isJsonColumn(column)) return value;
1412
+ if (isBufferColumn(column)) {
1413
+ if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1414
+ if (column.dimensions > 0 && value.every((element) => element === null || Array.isArray(element))) return value.map((element) => remapFromGraphQLCore(element, column, columnName));
1415
+ 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" });
1416
+ return Buffer.from(value);
1417
+ }
1418
+ if (isFloatTupleColumn(column)) {
1419
+ if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1420
+ const length = floatTupleLength(column);
1421
+ 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" });
1422
+ return value;
1423
+ }
1424
+ if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === null) return Object.assign({}, value);
1425
+ return value;
1345
1426
  };
1346
1427
  const remapFromGraphQLSingleInput = (queryInput, table, operation = "insert") => {
1347
1428
  const columns = (0, drizzle_orm.getColumns)(table);
@@ -1383,36 +1464,60 @@ const escapeLikeValue = (value) => value.replace(/[\\%_]/g, (char) => `\\${char}
1383
1464
  const safeLikeOps = {
1384
1465
  startsWith: {
1385
1466
  buildPattern: (value) => `${escapeLikeValue(value)}%`,
1467
+ buildGlob: (value) => `${escapeGlobValue(value)}*`,
1386
1468
  insensitive: false
1387
1469
  },
1388
1470
  endsWith: {
1389
1471
  buildPattern: (value) => `%${escapeLikeValue(value)}`,
1472
+ buildGlob: (value) => `*${escapeGlobValue(value)}`,
1390
1473
  insensitive: false
1391
1474
  },
1392
1475
  contains: {
1393
1476
  buildPattern: (value) => `%${escapeLikeValue(value)}%`,
1477
+ buildGlob: (value) => `*${escapeGlobValue(value)}*`,
1394
1478
  insensitive: false
1395
1479
  },
1396
1480
  iStartsWith: {
1397
1481
  buildPattern: (value) => `${escapeLikeValue(value)}%`,
1482
+ buildGlob: (value) => `${escapeGlobValue(value)}*`,
1398
1483
  insensitive: true
1399
1484
  },
1400
1485
  iEndsWith: {
1401
1486
  buildPattern: (value) => `%${escapeLikeValue(value)}`,
1487
+ buildGlob: (value) => `*${escapeGlobValue(value)}`,
1402
1488
  insensitive: true
1403
1489
  },
1404
1490
  iContains: {
1405
1491
  buildPattern: (value) => `%${escapeLikeValue(value)}%`,
1492
+ buildGlob: (value) => `*${escapeGlobValue(value)}*`,
1406
1493
  insensitive: true
1407
1494
  }
1408
1495
  };
1409
1496
  /**
1410
- * `LIKE <pattern> ESCAPE '\'` for a safe string operator. Case-insensitive variants use
1411
- * Postgres's native `ILIKE`; MySQL and SQLite have no `ILIKE`, so they compare `lower()`
1412
- * on both sides instead (mirroring how the raw `ilike` operator is Postgres-only).
1497
+ * Escapes SQLite's `GLOB` wildcards (`*`, `?`, `[`) in a literal search term by wrapping each in
1498
+ * a one-character class, so the term only ever matches literally inside a GLOB pattern. GLOB
1499
+ * has no escape character; `]` outside a class and everything else is already literal.
1413
1500
  */
1414
- const safeLikeCondition = (column, pattern, insensitive) => {
1415
- if (!insensitive) return drizzle_orm.sql`${column} like ${pattern} escape ${LIKE_ESCAPE_CHAR}`;
1501
+ const escapeGlobValue = (value) => value.replace(/[*?[]/g, (char) => `[${char}]`);
1502
+ /**
1503
+ * A caller-written LIKE pattern as the GLOB pattern matching the same strings, for SQLite's
1504
+ * case-sensitive `like` / `notLike`: `%` → `*`, `_` → `?`, and GLOB's own wildcards escaped.
1505
+ * The raw operators bind no `ESCAPE` clause, so `\` is an ordinary character in both.
1506
+ */
1507
+ const likePatternToGlob = (pattern) => pattern.replace(/[%_*?[]/g, (char) => char === "%" ? "*" : char === "_" ? "?" : `[${char}]`);
1508
+ /**
1509
+ * The safe string operator's predicate. Case-insensitive variants use Postgres's native
1510
+ * `ILIKE`; MySQL and SQLite have no `ILIKE`, so they compare `lower()` on both sides instead
1511
+ * (mirroring how the raw `ilike` operator is Postgres-only).
1512
+ *
1513
+ * The case-sensitive variants are `LIKE <pattern> ESCAPE '\'`, except on SQLite, whose `LIKE`
1514
+ * folds ASCII case — `contains: "App"` would match `apple`. There they are `GLOB`, which is
1515
+ * case-sensitive, as `LIKE` is on Postgres. `dialectColumn` names the column the dialect is read
1516
+ * from when `column` is an expression over it (a JSON path read).
1517
+ */
1518
+ const safeLikeCondition = (column, value, op, insensitive, dialectColumn = column) => {
1519
+ const pattern = op.buildPattern(value);
1520
+ if (!insensitive) return columnDialect(dialectColumn) === "sqlite" ? drizzle_orm.sql`${column} glob ${op.buildGlob(value)}` : drizzle_orm.sql`${column} like ${pattern} escape ${LIKE_ESCAPE_CHAR}`;
1416
1521
  return (column.columnType ?? "").startsWith("Pg") ? drizzle_orm.sql`${column} ilike ${pattern} escape ${LIKE_ESCAPE_CHAR}` : drizzle_orm.sql`lower(${column}) like ${pattern.toLowerCase()} escape ${LIKE_ESCAPE_CHAR}`;
1417
1522
  };
1418
1523
  /**
@@ -1468,6 +1573,35 @@ const jsonContains = (column, columnName, value) => {
1468
1573
  }
1469
1574
  };
1470
1575
  /**
1576
+ * The nodes of a SQLite JSON document as `(path, kind, value)` rows, for a structural
1577
+ * comparison. Integers and reals share one kind so `1` and `1.0` are the same number, as they
1578
+ * are on the other two dialects.
1579
+ */
1580
+ const sqliteJsonNodes = (document) => drizzle_orm.sql`select fullkey, case when type in ('integer', 'real') then 'number' else type end, atom from json_tree(${document})`;
1581
+ /**
1582
+ * Whole-document `eq` / `ne` on a JSON column, comparing documents rather than their text:
1583
+ * - Postgres: as `jsonb` (a plain `json` column has no `=` operator at all)
1584
+ * - MySQL: against `CAST(? AS JSON)`; a bare string parameter would be a JSON string scalar
1585
+ * - SQLite: the two documents' node sets must match both ways, so object key order and
1586
+ * whitespace do not matter, while array order still does
1587
+ * A SQL NULL column matches neither, as with `=` / `<>` on any other column.
1588
+ */
1589
+ const jsonEquality = (column, columnName, value, negated) => {
1590
+ const serialized = JSON.stringify(value);
1591
+ switch (columnDialect(column)) {
1592
+ case "pg": {
1593
+ const left = column.columnType === "PgJson" ? drizzle_orm.sql`${column}::jsonb` : drizzle_orm.sql`${column}`;
1594
+ return negated ? drizzle_orm.sql`${left} <> ${serialized}::jsonb` : drizzle_orm.sql`${left} = ${serialized}::jsonb`;
1595
+ }
1596
+ case "mysql": return negated ? drizzle_orm.sql`${column} <> cast(${serialized} as json)` : drizzle_orm.sql`${column} = cast(${serialized} as json)`;
1597
+ case "sqlite": {
1598
+ const equal = drizzle_orm.sql`(not exists (${sqliteJsonNodes(column)} except ${sqliteJsonNodes(serialized)}) and not exists (${sqliteJsonNodes(serialized)} except ${sqliteJsonNodes(column)}))`;
1599
+ return negated ? drizzle_orm.sql`(${column} is not null and not ${equal})` : drizzle_orm.sql`(${column} is not null and ${equal})`;
1600
+ }
1601
+ default: throw drizzleError(`WHERE ${columnName}: JSON equality is not supported for this dialect!`, { code: "DRIZZLE_INVALID_FILTER" });
1602
+ }
1603
+ };
1604
+ /**
1471
1605
  * Constant predicates, used where an operator's operand list is empty and the answer is
1472
1606
  * therefore known without touching the column. Written as `1 = 0` / `1 = 1` rather than the
1473
1607
  * `FALSE` / `TRUE` keywords so they compile identically on all three dialects. Built fresh
@@ -1502,7 +1636,8 @@ const NUMERIC_TEXT_PATTERN = "^\\s*-?(\\d+\\.?\\d*|\\.\\d+)([eE][-+]?\\d+)?\\s*$
1502
1636
  * pattern guard.
1503
1637
  * - **SQLite** — `json_extract` returns the value in SQLite's own type, so the reads cast
1504
1638
  * explicitly: without that, SQLite's cross-type ordering puts every string above every
1505
- * number and `'admin' > 0` would be true.
1639
+ * number and `'admin' > 0` would be true. A JSON boolean comes back as 1 / 0, so every read
1640
+ * also checks `json_type` to keep booleans and numbers apart.
1506
1641
  */
1507
1642
  const jsonPathExprs = (column, path) => {
1508
1643
  switch (columnDialect(column)) {
@@ -1528,10 +1663,11 @@ const jsonPathExprs = (column, path) => {
1528
1663
  }
1529
1664
  default: {
1530
1665
  const extracted = drizzle_orm.sql`json_extract(${column}, ${jsonPathString(path)})`;
1666
+ const type = drizzle_orm.sql`json_type(${column}, ${jsonPathString(path)})`;
1531
1667
  return {
1532
- text: drizzle_orm.sql`cast(${extracted} as text)`,
1533
- number: drizzle_orm.sql`(case when typeof(${extracted}) in ('integer', 'real') then ${extracted} end)`,
1534
- boolean: extracted,
1668
+ text: drizzle_orm.sql`(case ${type} when 'true' then 'true' when 'false' then 'false' else cast(${extracted} as text) end)`,
1669
+ number: drizzle_orm.sql`(case when ${type} in ('integer', 'real') then ${extracted} end)`,
1670
+ boolean: drizzle_orm.sql`(case when ${type} in ('true', 'false') then ${extracted} end)`,
1535
1671
  encodeBoolean: (value) => value ? 1 : 0
1536
1672
  };
1537
1673
  }
@@ -1570,9 +1706,9 @@ const jsonPathCondition = (column, columnName, filter) => {
1570
1706
  continue;
1571
1707
  }
1572
1708
  if (operatorName in safeLikeOps) {
1573
- const { buildPattern, insensitive } = safeLikeOps[operatorName];
1709
+ const op = safeLikeOps[operatorName];
1574
1710
  if (typeof operatorValue !== "string") throw drizzleError(`WHERE ${locator}: operator '${operatorName}' takes a string`, { code: "DRIZZLE_INVALID_FILTER" });
1575
- variants.push(safeLikeCondition(exprs.text, buildPattern(operatorValue), insensitive));
1711
+ variants.push(safeLikeCondition(exprs.text, operatorValue, op, op.insensitive, column));
1576
1712
  continue;
1577
1713
  }
1578
1714
  const comparison = JSON_PATH_COMPARISONS[operatorName];
@@ -1630,20 +1766,25 @@ const extractFiltersColumn = (column, columnName, operators) => {
1630
1766
  for (const [operatorName, operatorValue] of entries) {
1631
1767
  if (operatorValue === null || operatorValue === void 0) continue;
1632
1768
  if (operatorValue === false && FLAG_OPERATORS.has(operatorName)) continue;
1633
- if (operatorName in singleValueOps) {
1769
+ if ((operatorName === "eq" || operatorName === "ne") && isJsonColumn(column)) variants.push(jsonEquality(column, columnName, operatorValue, operatorName === "ne"));
1770
+ else if (operatorName in singleValueOps) {
1634
1771
  const singleValue = remapFromGraphQLCore(operatorValue, column, columnName);
1635
1772
  variants.push(foldCase ? lowerComparison(column, operatorName, singleValue) : singleValueOps[operatorName](column, singleValue));
1636
1773
  } else if (operatorName in stringValueOps) {
1637
- const insensitivePattern = foldCase && (operatorName === "like" || operatorName === "notLike");
1638
- variants.push(insensitivePattern ? insensitiveLike(column, operatorValue, operatorName === "notLike") : stringValueOps[operatorName](column, operatorValue));
1774
+ const isLike = operatorName === "like" || operatorName === "notLike";
1775
+ if (isLike && foldCase || (operatorName === "ilike" || operatorName === "notIlike") && columnDialect(column) !== "pg") variants.push(insensitiveLike(column, operatorValue, operatorName === "notLike" || operatorName === "notIlike"));
1776
+ else if (isLike && columnDialect(column) === "sqlite") {
1777
+ const glob = likePatternToGlob(operatorValue);
1778
+ variants.push(operatorName === "like" ? drizzle_orm.sql`${column} glob ${glob}` : drizzle_orm.sql`${column} not glob ${glob}`);
1779
+ } else variants.push(stringValueOps[operatorName](column, operatorValue));
1639
1780
  } else if (operatorName === "path" && isJsonColumn(column)) for (const pathFilter of operatorValue) {
1640
1781
  const extracted = jsonPathCondition(column, columnName, pathFilter);
1641
1782
  if (extracted) variants.push(extracted);
1642
1783
  }
1643
1784
  else if (operatorName === "contains" && isJsonColumn(column)) variants.push(jsonContains(column, columnName, operatorValue));
1644
1785
  else if (operatorName in safeLikeOps) {
1645
- const { buildPattern, insensitive: alwaysInsensitive } = safeLikeOps[operatorName];
1646
- variants.push(safeLikeCondition(column, buildPattern(operatorValue), alwaysInsensitive || foldCase));
1786
+ const op = safeLikeOps[operatorName];
1787
+ variants.push(safeLikeCondition(column, operatorValue, op, op.insensitive || foldCase));
1647
1788
  } else if (operatorName in arrayValueOps) {
1648
1789
  if (!operatorValue.length) {
1649
1790
  variants.push(operatorName === "inArray" ? sqlFalse() : sqlTrue());
@@ -1651,13 +1792,13 @@ const extractFiltersColumn = (column, columnName, operators) => {
1651
1792
  }
1652
1793
  const arrayValue = operatorValue.map((val) => remapFromGraphQLCore(val, column, columnName));
1653
1794
  variants.push(foldCase ? lowerMembership(column, arrayValue, operatorName === "notInArray") : arrayValueOps[operatorName](column, arrayValue));
1654
- } else if (operatorName === "has") variants.push((0, drizzle_orm.arrayContains)(column, [operatorValue]));
1795
+ } else if (operatorName === "has") variants.push((0, drizzle_orm.arrayContains)(column, [remapFromGraphQLCore(operatorValue, column, columnName)]));
1655
1796
  else if (operatorName in arrayMembershipOps) {
1656
1797
  if (!operatorValue.length) {
1657
1798
  variants.push(operatorName === "hasSome" ? sqlFalse() : sqlTrue());
1658
1799
  continue;
1659
1800
  }
1660
- variants.push(arrayMembershipOps[operatorName](column, operatorValue));
1801
+ variants.push(arrayMembershipOps[operatorName](column, operatorValue.map((element) => remapFromGraphQLCore(element, column, columnName))));
1661
1802
  } else if (operatorName === "isEmpty") variants.push(drizzle_orm.sql`cardinality(${column}) = 0`);
1662
1803
  else if (operatorName in nullableOps) variants.push(nullableOps[operatorName](column));
1663
1804
  else throw drizzleError(`WHERE ${columnName}: Unknown operator: ${operatorName}`, { code: "DRIZZLE_INVALID_FILTER" });
@@ -1855,17 +1996,104 @@ const extractRelationJoinColumnPairs = (relEntry, parentTable, targetTable) => {
1855
1996
  }
1856
1997
  return pairs;
1857
1998
  };
1999
+ /** A decimal written as text, in a form `canonicalDecimal` can normalise. */
2000
+ const DECIMAL_TEXT = /^([+-]?)(\d*)(?:\.(\d*))?$/;
2001
+ /**
2002
+ * Decimal text without the zeros that do not change its value: `1.500` and `01.5` are both
2003
+ * `1.5`, as SQL compares them. Anything else (an exponent, `NaN`) is returned unchanged.
2004
+ */
2005
+ const canonicalDecimal = (value) => {
2006
+ const match = DECIMAL_TEXT.exec(value);
2007
+ if (!match || !match[2] && !match[3]) return value;
2008
+ const whole = match[2].replace(/^0+(?=\d)/, "") || "0";
2009
+ const fraction = (match[3] ?? "").replace(/0+$/, "");
2010
+ const digits = fraction ? `${whole}.${fraction}` : whole;
2011
+ return match[1] === "-" && digits !== "0" ? `-${digits}` : digits;
2012
+ };
2013
+ /** An ISO instant without trailing zeros in its fraction: `…:00.100Z` and `…:00.1Z` agree. */
2014
+ const trimIsoFraction = (iso) => iso.replace(/(\.\d*?)0+Z$/, "$1Z").replace(/\.Z$/, "Z");
2015
+ /**
2016
+ * One join value in a form both sides of a relation produce for the same SQL value. The parent's
2017
+ * key and a related row's can arrive in different JS forms — a `Date` and the text of the same
2018
+ * instant, `1.50` and `1.500` from decimals of different scale — and `String()` would tell them
2019
+ * apart (it even drops a `Date`'s milliseconds).
2020
+ */
2021
+ const joinValueKey = (value, column) => {
2022
+ if (value instanceof Date) {
2023
+ if (Number.isNaN(value.getTime())) return String(value);
2024
+ if (column && isDateOnlyColumn(column)) return dateToCalendarDate(value) ?? value.toISOString();
2025
+ return trimIsoFraction(value.toISOString());
2026
+ }
2027
+ if (typeof value === "bigint") return value.toString();
2028
+ if (value instanceof Uint8Array) return Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString("hex");
2029
+ if (typeof value === "string" && column) {
2030
+ if (isMySqlDateTimeStringColumn(column)) return trimIsoFraction(mySqlDateTimeTextToIso(value));
2031
+ if (isPgTimestampStringColumn(column)) return trimIsoFraction(pgTimestampTextToIso(value));
2032
+ if ((column.dataType ?? "") === "string numeric") return canonicalDecimal(value);
2033
+ }
2034
+ return String(value);
2035
+ };
1858
2036
  /**
1859
2037
  * A string identifying one combination of join-column values, for grouping a batch's rows by
1860
- * parent. Values are compared by their string form, as a single-column key always has been.
2038
+ * parent. `columns` are the columns the values were read from, in the same order: the parent's
2039
+ * join columns for a parent key, the target's (or the junction's) for a related row. Values the
2040
+ * database compares equal give the same key — see {@link joinValueKey}.
2041
+ */
2042
+ const joinKeyOf = (values, columns) => values.length === 1 ? joinValueKey(values[0], columns?.[0]) : JSON.stringify(values.map((value, i) => joinValueKey(value, columns?.[i])));
2043
+ /**
2044
+ * The parent row's values for the relation's join columns, as the database holds them. The row
2045
+ * a relation field resolves under has been remapped to its GraphQL form — a `Date` is an ISO
2046
+ * string by then, a `bigint` a decimal string — which can neither be bound back through every
2047
+ * column's encoder (SQLite's integer timestamp calls `getTime()` on it) nor be compared with the
2048
+ * related rows' decoded values. The values from before remapping are used when the row kept
2049
+ * them; otherwise the output form is mapped back the way an input value would be.
2050
+ */
2051
+ const parentJoinValues = (parent, pairs) => pairs.map(({ localColPropName, localCol }) => {
2052
+ const raw = rawColumnValue(parent, localColPropName);
2053
+ if (raw.found) return raw.value;
2054
+ const value = parent[localColPropName];
2055
+ if (value == null) return value;
2056
+ try {
2057
+ return remapFromGraphQLCore(value, localCol, localColPropName);
2058
+ } catch {
2059
+ return value;
2060
+ }
2061
+ });
2062
+ /**
2063
+ * At most this many bound parameters for the parent keys of one batched relation query. SQLite
2064
+ * accepts 32,766 per statement, Postgres and MySQL 65,535; the rest is left to the request's own
2065
+ * filters. A batch with more parents runs as several queries.
2066
+ */
2067
+ const MAX_PARENT_KEY_PARAMS = 1e4;
2068
+ /**
2069
+ * The parent keys of one batch split into the groups each query reads, so that no query binds
2070
+ * more than {@link MAX_PARENT_KEY_PARAMS} key values. Every parent lands in exactly one group, so
2071
+ * a per-parent result — its rows, its window, its count — is the same whichever query made it.
2072
+ */
2073
+ const parentKeyChunks = (keys, width) => {
2074
+ const size = Math.max(1, Math.floor(MAX_PARENT_KEY_PARAMS / Math.max(1, width)));
2075
+ const chunks = [];
2076
+ for (let i = 0; i < keys.length; i += size) chunks.push(keys.slice(i, i + size));
2077
+ return chunks;
2078
+ };
2079
+ /**
2080
+ * `or` over `conditions` as a balanced tree. A flat `a OR b OR c …` is parsed left-deep, and
2081
+ * SQLite refuses an expression more than 1,000 levels deep; balanced, a batch of thousands of
2082
+ * parents stays a few levels deep.
1861
2083
  */
1862
- const joinKeyOf = (values) => values.length === 1 ? String(values[0]) : JSON.stringify(values.map((value) => String(value)));
2084
+ const balancedOr = (conditions) => {
2085
+ if (conditions.length <= 2) return (0, drizzle_orm.or)(...conditions);
2086
+ const middle = Math.ceil(conditions.length / 2);
2087
+ return (0, drizzle_orm.or)(balancedOr(conditions.slice(0, middle)), balancedOr(conditions.slice(middle)));
2088
+ };
1863
2089
  /**
1864
2090
  * The rows whose `cols` hold one of `keys`: `col IN (…)` for a single join column, and an `OR`
1865
2091
  * of per-parent equalities for several — `(a = 1 AND b = 1) OR (a = 1 AND b = 2)` — which
1866
- * every dialect accepts, unlike a row-value `IN` list.
2092
+ * every dialect accepts, unlike a row-value `IN` list. The `OR` is nested as a balanced tree.
2093
+ * Callers split a large batch with {@link parentKeyChunks} first, which keeps either form within
2094
+ * the dialects' parameter limits.
1867
2095
  */
1868
- const parentKeyCondition = (cols, keys) => cols.length === 1 ? (0, drizzle_orm.inArray)(cols[0], keys.map((key) => key[0])) : (0, drizzle_orm.or)(...keys.map((key) => (0, drizzle_orm.and)(...cols.map((col, i) => (0, drizzle_orm.eq)(col, key[i])))));
2096
+ const parentKeyCondition = (cols, keys) => cols.length === 1 ? (0, drizzle_orm.inArray)(cols[0], keys.map((key) => key[0])) : balancedOr(keys.map((key) => (0, drizzle_orm.and)(...cols.map((col, i) => (0, drizzle_orm.eq)(col, key[i])))));
1869
2097
  /**
1870
2098
  * The junction table and its two columns for a `.through()` (many-to-many) relation:
1871
2099
  * `sourceCol` holds the parent's key, `targetCol` the target's. `undefined` for a relation
@@ -2418,6 +2646,32 @@ const withScope = (scope, tableName, table, condition, mode) => {
2418
2646
  const predicate = scope?.on(tableName, table, mode);
2419
2647
  return predicate ? (0, drizzle_orm.and)(condition, predicate) : condition;
2420
2648
  };
2649
+ /**
2650
+ * The junction table a `.through()` relation links through, by its schema key — the key its
2651
+ * `scope` and `softDelete` are configured under. `undefined` for a relation with no junction,
2652
+ * or a junction that is not one of the build's tables.
2653
+ */
2654
+ const junctionTableName = (tables, relEntry) => {
2655
+ const throughTable = (relEntry?.relation ?? relEntry)?.throughTable;
2656
+ return throughTable ? Object.keys(tables).find((name) => tables[name] === throughTable) : void 0;
2657
+ };
2658
+ /**
2659
+ * Whether a `.through()` relation's junction restricts which links exist for this request —
2660
+ * a `scope` on the junction table, or a soft delete it declares.
2661
+ */
2662
+ const hasJunctionPolicy = (scope, junctionName) => !!junctionName && !!scope?.has(junctionName, "EXCLUDE");
2663
+ /**
2664
+ * The junction's own row policy, compiled against the junction table as the query names it.
2665
+ * A `.through()` relation links only through the junction rows this keeps, on every path that
2666
+ * reads the relation — the relation field, `<relation>Aggregate`, and a relation filter — so
2667
+ * they all agree on which rows are related.
2668
+ *
2669
+ * The junction is always read at `EXCLUDE`: a marked junction row is a removed link. The
2670
+ * relation's `deleted` argument is about the *target* rows, and the junction's own
2671
+ * `softDelete.scope` only governs reads of the junction table's own fields — a link row has no
2672
+ * field of its own on the relation that `'root'` could keep rendering.
2673
+ */
2674
+ const junctionPolicy = (scope, junctionName, table) => hasJunctionPolicy(scope, junctionName) ? scope.on(junctionName, table, "EXCLUDE") : void 0;
2421
2675
  /** Binds a build's {@link TablePolicies} to its relation context, once, at schema-build time. */
2422
2676
  const bindPolicies = (policies, filterCtx) => policies?.scope || policies?.contextValues || policies?.softDelete || policies?.onWrite || policies?.defaultOrderBy ? {
2423
2677
  scope: policies.scope || policies.softDelete ? (context) => resolveScope(policies, context, filterCtx) : void 0,
@@ -2468,7 +2722,7 @@ const deletedArg = (softDelete, tableName, resolveName) => softDelete?.(tableNam
2468
2722
  const selectArrayArgs = (orderArgs, filterArgs, distinctEnum, softDelete, tableName, resolveName) => ({
2469
2723
  offset: { type: graphql.GraphQLInt },
2470
2724
  limit: { type: graphql.GraphQLInt },
2471
- orderBy: { type: orderArgs },
2725
+ ...orderArgs ? { orderBy: { type: orderArgs } } : {},
2472
2726
  where: { type: filterArgs },
2473
2727
  after: {
2474
2728
  type: graphql.GraphQLString,
@@ -2480,7 +2734,7 @@ const selectArrayArgs = (orderArgs, filterArgs, distinctEnum, softDelete, tableN
2480
2734
  /** GraphQL argument map for a single-row select field (no `limit`). */
2481
2735
  const selectSingleArgs = (orderArgs, filterArgs, softDelete, tableName, resolveName) => ({
2482
2736
  offset: { type: graphql.GraphQLInt },
2483
- orderBy: { type: orderArgs },
2737
+ ...orderArgs ? { orderBy: { type: orderArgs } } : {},
2484
2738
  where: { type: filterArgs },
2485
2739
  ...deletedArg(softDelete, tableName, resolveName)
2486
2740
  });
@@ -2530,7 +2784,7 @@ const generateDistinctEnum = (table, typeName, resolveName, tableKey) => {
2530
2784
  defaultName,
2531
2785
  table: tableKey,
2532
2786
  operation: "distinct"
2533
- }) ?? defaultName, `Columns of ${typeName} that a query can be made distinct on`);
2787
+ }) ?? defaultName, `Columns of ${typeName} that a query can be made distinct on`, (column) => !isJsonColumn(column));
2534
2788
  };
2535
2789
  //#endregion
2536
2790
  //#region src/util/builders/common/conflict.ts
@@ -2700,6 +2954,44 @@ const resolveConflictPlan = (params) => {
2700
2954
  plan
2701
2955
  }));
2702
2956
  };
2957
+ /**
2958
+ * Adds a {@link ConflictPlan}'s clause to a PostgreSQL or SQLite insert.
2959
+ *
2960
+ * SQLite needs two things drizzle's builder does not give it when the target is a partial
2961
+ * unique index:
2962
+ *
2963
+ * - The index predicate written literally. SQLite matches a conflict target to an index by
2964
+ * comparing the predicates as written, and a bound `?` never equals `'active'`, so a
2965
+ * predicate that compares to a value would never find its index. Its params are inlined.
2966
+ * - A `DO NOTHING` it can parse. Drizzle writes the predicate after `do nothing`, which is a
2967
+ * syntax error, so the action is spelled as an update that matches no row instead:
2968
+ * `ON CONFLICT (…) WHERE … DO UPDATE SET col = col WHERE 0 = 1`. A row that is not updated is
2969
+ * not returned, which is what `DO NOTHING` does too.
2970
+ */
2971
+ const applyConflictPlan = (query, table, plan, setWhere) => {
2972
+ const sqlite = (0, drizzle_orm.is)(table, drizzle_orm_sqlite_core.SQLiteTable);
2973
+ const targetWhere = sqlite && plan.targetWhere ? drizzle_orm.sql`${plan.targetWhere}`.inlineParams() : plan.targetWhere;
2974
+ if (plan.action === "UPDATE") return query.onConflictDoUpdate({
2975
+ target: plan.target,
2976
+ targetWhere,
2977
+ set: plan.set,
2978
+ setWhere
2979
+ });
2980
+ if (!plan.target) return query.onConflictDoNothing();
2981
+ if (sqlite && targetWhere) {
2982
+ const [key] = Object.entries((0, drizzle_orm.getColumns)(table)).find(([, column]) => column === plan.target[0]);
2983
+ return query.onConflictDoUpdate({
2984
+ target: plan.target,
2985
+ targetWhere,
2986
+ set: { [key]: drizzle_orm.sql`${plan.target[0]}` },
2987
+ setWhere: drizzle_orm.sql`0 = 1`
2988
+ });
2989
+ }
2990
+ return query.onConflictDoNothing({
2991
+ target: plan.target,
2992
+ where: targetWhere
2993
+ });
2994
+ };
2703
2995
  /** `excluded.<column>` — PostgreSQL and SQLite name the rejected row this way. */
2704
2996
  const excludedColumnRef = (columnName) => drizzle_orm.sql`excluded.${drizzle_orm.sql.identifier(columnName)}`;
2705
2997
  /** `values(<column>)` — MySQL's equivalent inside ON DUPLICATE KEY UPDATE. */
@@ -2981,6 +3273,21 @@ const decodeCursor = (after, entries) => {
2981
3273
  return payload.v.map(decodeCursorValue);
2982
3274
  };
2983
3275
  /**
3276
+ * `col > value` (asc) or `col < value` (desc), compared by the rule the ORDER BY sorts by.
3277
+ * MySQL sorts an ENUM by its declaration index but compares it with a string as a string, so
3278
+ * there the comparison is between `col + 0` (the 1-based index) and the value's own index.
3279
+ */
3280
+ const strictComparison = (col, direction, value) => {
3281
+ if ((0, drizzle_orm.is)(col, drizzle_orm_mysql_core.MySqlEnumColumn) || (0, drizzle_orm.is)(col, drizzle_orm_mysql_core.MySqlEnumObjectColumn)) {
3282
+ const index = col.enumValues?.indexOf(value) ?? -1;
3283
+ if (index >= 0) {
3284
+ const position = drizzle_orm.sql`(${col} + 0)`;
3285
+ return direction === "asc" ? drizzle_orm.sql`${position} > ${index + 1}` : drizzle_orm.sql`${position} < ${index + 1}`;
3286
+ }
3287
+ }
3288
+ return direction === "asc" ? (0, drizzle_orm.gt)(col, value) : (0, drizzle_orm.lt)(col, value);
3289
+ };
3290
+ /**
2984
3291
  * The keyset predicate selecting rows strictly after the cursor position in the given total
2985
3292
  * order — the expanded lexicographic form
2986
3293
  * `after(k0) OR (k0 = v0 AND after(k1)) OR (k0 = v0 AND k1 = v1 AND after(k2)) …`,
@@ -3005,7 +3312,7 @@ const buildCursorCondition = (table, entries, values, nullOrdering) => {
3005
3312
  let strictlyAfter;
3006
3313
  if (value === null || value === void 0) strictlyAfter = nullsSortLast ? void 0 : (0, drizzle_orm.isNotNull)(col);
3007
3314
  else {
3008
- const comparison = direction === "asc" ? (0, drizzle_orm.gt)(col, value) : (0, drizzle_orm.lt)(col, value);
3315
+ const comparison = strictComparison(col, direction, value);
3009
3316
  strictlyAfter = nullsSortLast ? (0, drizzle_orm.or)(comparison, (0, drizzle_orm.isNull)(col)) : comparison;
3010
3317
  }
3011
3318
  if (strictlyAfter) disjuncts.push(equalities.length ? (0, drizzle_orm.and)(...equalities, strictlyAfter) : strictlyAfter);
@@ -3208,6 +3515,115 @@ const getUniqueTargets = (table, getTableConfig) => {
3208
3515
  */
3209
3516
  const getUniqueColumnSets = (table, getTableConfig, options = {}) => getUniqueTargets(table, getTableConfig).filter((target) => options.partial || !target.where).map((target) => target.columns);
3210
3517
  //#endregion
3518
+ //#region src/util/builders/common/limits.ts
3519
+ /**
3520
+ * The `limit` to send with an `offset` the request gave without one. SQLite and MySQL accept
3521
+ * `OFFSET` only after a `LIMIT`, so `offset` alone has to carry a limit that means "all of
3522
+ * them": SQLite reads `LIMIT -1` that way, and on MySQL it is the largest integer every driver
3523
+ * binds exactly (MySQL's own documented 2^64-1 does not fit a JS number). PostgreSQL takes
3524
+ * `OFFSET` alone, so the limit is left as it was.
3525
+ */
3526
+ const offsetOnlyLimit = (table, limit, offset) => {
3527
+ if (limit != null || !offset) return limit;
3528
+ const firstColumn = Object.values((0, drizzle_orm.getColumns)(table))[0];
3529
+ switch (firstColumn ? columnDialect(firstColumn) : void 0) {
3530
+ case "sqlite": return -1;
3531
+ case "mysql": return Number.MAX_SAFE_INTEGER;
3532
+ default: return limit;
3533
+ }
3534
+ };
3535
+ /**
3536
+ * Substitutes a table's default ordering for an absent `orderBy`.
3537
+ *
3538
+ * Applied where the arguments are first read, so everything downstream — the cursor tuple, a
3539
+ * `distinct` pass, the plain-select fallback — sees one effective ordering and cannot
3540
+ * disagree about it. Only a missing argument is replaced: `orderBy: {}` is a request for no
3541
+ * ordering, and stays one.
3542
+ */
3543
+ const withDefaultOrderBy = (args, tableName, defaults) => {
3544
+ if (!defaults || args?.orderBy != null) return args;
3545
+ const fallback = defaults(tableName);
3546
+ return fallback ? {
3547
+ ...args,
3548
+ orderBy: fallback
3549
+ } : args;
3550
+ };
3551
+ /**
3552
+ * Rejects a negative `limit` or `offset`. Neither is a page size or a position, and the
3553
+ * dialects disagree on what one means: SQLite reads `LIMIT -1` as "no limit" — which would walk
3554
+ * straight past `maxLimit` — while PostgreSQL and MySQL fail with a raw database error. Applied
3555
+ * whether or not the table has a limit policy.
3556
+ */
3557
+ const assertPagingNotNegative = (paging, errorCtx) => {
3558
+ for (const name of ["limit", "offset"]) {
3559
+ const value = paging[name];
3560
+ if (value != null && value < 0) throw drizzleError(`'${name}' must not be negative, got ${value}.`, {
3561
+ code: "DRIZZLE_INVALID_INPUT_VALUE",
3562
+ ...errorCtx
3563
+ });
3564
+ }
3565
+ };
3566
+ /**
3567
+ * The `limit` a query actually runs with, given the policy for the table it reads.
3568
+ *
3569
+ * A request that passes no `limit` falls back to `defaultLimit`, or to `maxLimit` when only
3570
+ * that is configured — an absent limit means every row, which is above any maximum. A limit
3571
+ * the client did ask for is rejected when it exceeds `maxLimit`, since silently truncating a
3572
+ * page tells a paginating client it has reached the end when it has not; `clampToMax` opts
3573
+ * into the truncation instead. A limit the *policy* supplied is capped rather than rejected —
3574
+ * a misconfigured `defaultLimit` above `maxLimit` is the operator's problem, not something to
3575
+ * fail a client's query over.
3576
+ */
3577
+ const applyLimitPolicy = (limit, policy, errorCtx) => {
3578
+ assertPagingNotNegative({ limit }, errorCtx);
3579
+ if (!policy) return limit ?? void 0;
3580
+ const { defaultLimit, maxLimit, clampToMax } = policy;
3581
+ if (limit == null) {
3582
+ const fallback = defaultLimit ?? maxLimit;
3583
+ if (fallback === void 0) return;
3584
+ return maxLimit === void 0 ? fallback : Math.min(fallback, maxLimit);
3585
+ }
3586
+ if (maxLimit !== void 0 && limit > maxLimit) {
3587
+ if (clampToMax) return maxLimit;
3588
+ throw drizzleError(`'limit' of ${limit} exceeds the maximum of ${maxLimit}.`, {
3589
+ code: "DRIZZLE_LIMIT_EXCEEDED",
3590
+ ...errorCtx
3591
+ });
3592
+ }
3593
+ return limit;
3594
+ };
3595
+ /** Merges a per-table override over the global policy, or `undefined` when neither bounds anything. */
3596
+ const resolveLimitPolicy = (global, table) => {
3597
+ const defaultLimit = table?.defaultLimit ?? global?.defaultLimit;
3598
+ const maxLimit = table?.maxLimit ?? global?.maxLimit;
3599
+ if (defaultLimit === void 0 && maxLimit === void 0) return;
3600
+ return {
3601
+ defaultLimit,
3602
+ maxLimit,
3603
+ clampToMax: table?.clampToMax ?? global?.clampToMax ?? false
3604
+ };
3605
+ };
3606
+ /**
3607
+ * A paginated field costs its page size times whatever one row of it costs, so `users(limit: 100)
3608
+ * { posts(limit: 10) { id } }` is charged for the thousand rows it can return rather than the two
3609
+ * fields it mentions. `childComplexity` floors at 1 so a row is never free.
3610
+ *
3611
+ * The page size is the *effective* one: a request without `limit` is priced at the policy's
3612
+ * `defaultLimit` (or its `maxLimit` when that is all it sets) rather than the estimator's guess, and no request is priced above `maxLimit`,
3613
+ * so the cost a query is charged matches the rows it can actually pull.
3614
+ */
3615
+ const listFieldComplexity = (options, policy) => ({ args, childComplexity }) => {
3616
+ const limit = args["limit"];
3617
+ let rows = (typeof limit === "number" && limit > 0 ? limit : void 0) ?? policy?.defaultLimit ?? policy?.maxLimit ?? options.defaultListSize;
3618
+ if (policy?.maxLimit !== void 0) rows = Math.min(rows, policy.maxLimit);
3619
+ return rows * Math.max(childComplexity, 1);
3620
+ };
3621
+ /**
3622
+ * An aggregate returns a single row but reads however many match, so its cost tracks the scan
3623
+ * rather than the response.
3624
+ */
3625
+ const aggregateFieldComplexity = (options) => ({ childComplexity }) => options.aggregateCost + childComplexity;
3626
+ //#endregion
3211
3627
  //#region src/util/builders/common/distinct.ts
3212
3628
  /**
3213
3629
  * Alias the parent key comes back under when the pass runs through a junction table. See
@@ -3255,13 +3671,15 @@ const selectDistinctKeys = async (params) => {
3255
3671
  ...junction ? { [DISTINCT_PARENT]: sub[DISTINCT_PARENT] } : {}
3256
3672
  }).from(sub).where((0, drizzle_orm.eq)(sub[DISTINCT_RN], 1)).orderBy(...outerOrder);
3257
3673
  if (offset) query = query.offset(offset);
3258
- if (limit != null) query = query.limit(limit);
3674
+ const effectiveLimit = offsetOnlyLimit(table, limit, offset);
3675
+ if (effectiveLimit != null) query = query.limit(effectiveLimit < 0 ? drizzle_orm.sql`${effectiveLimit}` : effectiveLimit);
3259
3676
  return await query;
3260
3677
  };
3261
3678
  /**
3262
3679
  * Condition matching exactly the rows identified by `keys` — an `IN (…)` for a single-column
3263
- * primary key, an `OR` of per-row equality for a composite one. `table` may be the aliased
3264
- * RQB proxy.
3680
+ * primary key, an `OR` of per-row equality for a composite one — nested as a balanced tree, so
3681
+ * thousands of keys stay within SQLite's expression-depth limit. `table` may be the aliased RQB
3682
+ * proxy.
3265
3683
  */
3266
3684
  const primaryKeyRestriction = (table, pkNames, keys) => {
3267
3685
  const cols = (0, drizzle_orm.getColumns)(table);
@@ -3269,7 +3687,7 @@ const primaryKeyRestriction = (table, pkNames, keys) => {
3269
3687
  const name = pkNames[0];
3270
3688
  return (0, drizzle_orm.inArray)(cols[name], keys.map((key) => key[name]));
3271
3689
  }
3272
- return (0, drizzle_orm.or)(...keys.map((key) => (0, drizzle_orm.and)(...pkNames.map((name) => (0, drizzle_orm.eq)(cols[name], key[name])))));
3690
+ return balancedOr(keys.map((key) => (0, drizzle_orm.and)(...pkNames.map((name) => (0, drizzle_orm.eq)(cols[name], key[name])))));
3273
3691
  };
3274
3692
  //#endregion
3275
3693
  //#region src/util/builders/common/docs.ts
@@ -3415,99 +3833,6 @@ const innerOrderType = (typeName) => sharedType(typeName, {
3415
3833
  }
3416
3834
  }));
3417
3835
  //#endregion
3418
- //#region src/util/builders/common/limits.ts
3419
- /**
3420
- * Substitutes a table's default ordering for an absent `orderBy`.
3421
- *
3422
- * Applied where the arguments are first read, so everything downstream — the cursor tuple, a
3423
- * `distinct` pass, the plain-select fallback — sees one effective ordering and cannot
3424
- * disagree about it. Only a missing argument is replaced: `orderBy: {}` is a request for no
3425
- * ordering, and stays one.
3426
- */
3427
- const withDefaultOrderBy = (args, tableName, defaults) => {
3428
- if (!defaults || args?.orderBy != null) return args;
3429
- const fallback = defaults(tableName);
3430
- return fallback ? {
3431
- ...args,
3432
- orderBy: fallback
3433
- } : args;
3434
- };
3435
- /**
3436
- * Rejects a negative `limit` or `offset`. Neither is a page size or a position, and the
3437
- * dialects disagree on what one means: SQLite reads `LIMIT -1` as "no limit" — which would walk
3438
- * straight past `maxLimit` — while PostgreSQL and MySQL fail with a raw database error. Applied
3439
- * whether or not the table has a limit policy.
3440
- */
3441
- const assertPagingNotNegative = (paging, errorCtx) => {
3442
- for (const name of ["limit", "offset"]) {
3443
- const value = paging[name];
3444
- if (value != null && value < 0) throw drizzleError(`'${name}' must not be negative, got ${value}.`, {
3445
- code: "DRIZZLE_INVALID_INPUT_VALUE",
3446
- ...errorCtx
3447
- });
3448
- }
3449
- };
3450
- /**
3451
- * The `limit` a query actually runs with, given the policy for the table it reads.
3452
- *
3453
- * A request that passes no `limit` falls back to `defaultLimit`, or to `maxLimit` when only
3454
- * that is configured — an absent limit means every row, which is above any maximum. A limit
3455
- * the client did ask for is rejected when it exceeds `maxLimit`, since silently truncating a
3456
- * page tells a paginating client it has reached the end when it has not; `clampToMax` opts
3457
- * into the truncation instead. A limit the *policy* supplied is capped rather than rejected —
3458
- * a misconfigured `defaultLimit` above `maxLimit` is the operator's problem, not something to
3459
- * fail a client's query over.
3460
- */
3461
- const applyLimitPolicy = (limit, policy, errorCtx) => {
3462
- assertPagingNotNegative({ limit }, errorCtx);
3463
- if (!policy) return limit ?? void 0;
3464
- const { defaultLimit, maxLimit, clampToMax } = policy;
3465
- if (limit == null) {
3466
- const fallback = defaultLimit ?? maxLimit;
3467
- if (fallback === void 0) return;
3468
- return maxLimit === void 0 ? fallback : Math.min(fallback, maxLimit);
3469
- }
3470
- if (maxLimit !== void 0 && limit > maxLimit) {
3471
- if (clampToMax) return maxLimit;
3472
- throw drizzleError(`'limit' of ${limit} exceeds the maximum of ${maxLimit}.`, {
3473
- code: "DRIZZLE_LIMIT_EXCEEDED",
3474
- ...errorCtx
3475
- });
3476
- }
3477
- return limit;
3478
- };
3479
- /** Merges a per-table override over the global policy, or `undefined` when neither bounds anything. */
3480
- const resolveLimitPolicy = (global, table) => {
3481
- const defaultLimit = table?.defaultLimit ?? global?.defaultLimit;
3482
- const maxLimit = table?.maxLimit ?? global?.maxLimit;
3483
- if (defaultLimit === void 0 && maxLimit === void 0) return;
3484
- return {
3485
- defaultLimit,
3486
- maxLimit,
3487
- clampToMax: table?.clampToMax ?? global?.clampToMax ?? false
3488
- };
3489
- };
3490
- /**
3491
- * A paginated field costs its page size times whatever one row of it costs, so `users(limit: 100)
3492
- * { posts(limit: 10) { id } }` is charged for the thousand rows it can return rather than the two
3493
- * fields it mentions. `childComplexity` floors at 1 so a row is never free.
3494
- *
3495
- * The page size is the *effective* one: a request without `limit` is priced at the policy's
3496
- * `defaultLimit` rather than the estimator's guess, and no request is priced above `maxLimit`,
3497
- * so the cost a query is charged matches the rows it can actually pull.
3498
- */
3499
- const listFieldComplexity = (options, policy) => ({ args, childComplexity }) => {
3500
- const limit = args["limit"];
3501
- let rows = (typeof limit === "number" && limit > 0 ? limit : void 0) ?? policy?.defaultLimit ?? options.defaultListSize;
3502
- if (policy?.maxLimit !== void 0) rows = Math.min(rows, policy.maxLimit);
3503
- return rows * Math.max(childComplexity, 1);
3504
- };
3505
- /**
3506
- * An aggregate returns a single row but reads however many match, so its cost tracks the scan
3507
- * rather than the response.
3508
- */
3509
- const aggregateFieldComplexity = (options) => ({ childComplexity }) => options.aggregateCost + childComplexity;
3510
- //#endregion
3511
3836
  //#region src/util/parse-resolve-info.ts
3512
3837
  /** Unwraps `[T]!`-style wrappers down to the named type. `getNamedType` without the realm tie. */
3513
3838
  const namedTypeOf = (type) => {
@@ -3671,14 +3996,10 @@ const NUMERIC_SCALARS = /* @__PURE__ */ new Set([
3671
3996
  ]);
3672
3997
  /**
3673
3998
  * Column types that convert to a GraphQL list without being a list *in the database* —
3674
- * a bytea reads as `[Int]`, a vector and a geometry point as `[Float]` — so appending to
3675
- * them is not the operation `push` means.
3999
+ * a bytea or SQLite buffer blob reads as `[Int]`, a vector, a point or a line as `[Float]` —
4000
+ * so appending to them is not the operation `push` means.
3676
4001
  */
3677
- const NON_LIST_ARRAY_COLUMNS = /* @__PURE__ */ new Set([
3678
- "PgVector",
3679
- "PgGeometry",
3680
- "PgBytea"
3681
- ]);
4002
+ const isNonListArrayColumn = (column) => isFloatTupleColumn(column) || isBufferColumn(column);
3682
4003
  const kindCache = /* @__PURE__ */ new WeakMap();
3683
4004
  /**
3684
4005
  * Whether a column takes an operations input, and which one.
@@ -3695,7 +4016,7 @@ const fieldUpdateKind = (column, columnName, tableName) => {
3695
4016
  if (!getColumnScalarOverride(column, true)) {
3696
4017
  const { type } = drizzleColumnToGraphQLType(column, columnName, tableName, true, false, true);
3697
4018
  if (NUMERIC_SCALARS.has(type)) kind = "numeric";
3698
- else if (type instanceof graphql.GraphQLList && !NON_LIST_ARRAY_COLUMNS.has(column.columnType)) kind = "list";
4019
+ else if (type instanceof graphql.GraphQLList && !isNonListArrayColumn(column)) kind = "list";
3699
4020
  }
3700
4021
  kindCache.set(column, kind);
3701
4022
  return kind;
@@ -3966,6 +4287,16 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
3966
4287
  if (!relFieldSelection) continue;
3967
4288
  const eagerArgs = field.args;
3968
4289
  if (!(0, drizzle_orm.is)(relEntry.relation ?? relEntry, drizzle_orm.One) && (eagerArgs?.after != null || !!(eagerArgs?.distinct)?.length || isCursorFieldSelected(relFieldSelection, tables[targetTableName]))) continue;
4290
+ const junctionName = junctionTableName(tables, relEntry);
4291
+ if (hasJunctionPolicy(scope, junctionName)) {
4292
+ 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.`, {
4293
+ code: "DRIZZLE_UNSUPPORTED_RELATION",
4294
+ table: tableName,
4295
+ operation: "relation",
4296
+ relation: relName
4297
+ });
4298
+ continue;
4299
+ }
3969
4300
  const columns = extractSelectedColumnsFromTree(relFieldSelection, tables[targetTableName], {
3970
4301
  tableName: targetTableName,
3971
4302
  relationMap,
@@ -3991,9 +4322,11 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
3991
4322
  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;
3992
4323
  const hasPagination = offset != null || limit != null;
3993
4324
  const pkNames = targetPkNames ?? [];
3994
- thisRecord.orderBy = relationArgs?.orderBy ? (aliasedTable) => extractOrderBy(aliasedTable, relationArgs.orderBy, relationFilterCtx(filterCtx, targetTableName, scope), relWhere) : hasPagination && pkNames.length ? (aliasedTable) => primaryKeyOrderExprs(aliasedTable, pkNames) : void 0;
4325
+ const tiebreak = hasPagination && pkNames.length > 0;
4326
+ const requestOrder = relationArgs?.orderBy;
4327
+ thisRecord.orderBy = requestOrder || tiebreak ? (aliasedTable) => [...requestOrder ? extractOrderBy(aliasedTable, requestOrder, relationFilterCtx(filterCtx, targetTableName, scope), relWhere) : [], ...tiebreak ? primaryKeyOrderExprs(aliasedTable, pkNames) : []] : void 0;
3995
4328
  thisRecord.offset = offset;
3996
- thisRecord.limit = limit;
4329
+ thisRecord.limit = offsetOnlyLimit(tables[targetTableName], limit, offset);
3997
4330
  thisRecord.with = extractRelationsParams(relationMap, tables, targetTableName, field, relTypeName, options);
3998
4331
  args[relName] = omitUndefinedKeys(thisRecord);
3999
4332
  }
@@ -4084,7 +4417,7 @@ const runRelationalSelect = async (opts) => {
4084
4417
  const preciseProps = Object.keys(cursorPreciseExtras(table, cursorEntries));
4085
4418
  if (preciseProps.length) params.extras = Object.fromEntries(preciseProps.map((prop) => [prop, (aliasedTable) => cursorPreciseExtras(aliasedTable, cursorEntries)[prop]]));
4086
4419
  }
4087
- params.limit = distinctKeys ? void 0 : opts.limit;
4420
+ params.limit = distinctKeys ? void 0 : offsetOnlyLimit(table, opts.limit, offset);
4088
4421
  const result = await queryBase.findMany(omitUndefinedKeys(params));
4089
4422
  if (cursorEntries && cursorSelected) attachRowCursors(result, cursorEntries);
4090
4423
  return remapToGraphQLArrayOutput(result, tableName, table, relationMap);
@@ -4141,6 +4474,8 @@ const eagerLoadMutationRelations = async (db, tableName, rows, pkNames, withPara
4141
4474
  }
4142
4475
  return rows;
4143
4476
  };
4477
+ /** The longest delay `setTimeout` honours; anything above, Infinity included, fires after 1 ms. */
4478
+ const MAX_TIMER_MS = 2 ** 31 - 1;
4144
4479
  const createMutationTxCtx = (options) => options ? {
4145
4480
  timeoutMs: options.timeoutMs,
4146
4481
  fieldNames: /* @__PURE__ */ new Set(),
@@ -4167,28 +4502,28 @@ const includedByDirectives = (node, variables) => {
4167
4502
  * this build did not generate — its completion cannot be tracked, so the caller must not
4168
4503
  * open a transaction it might never be able to close.
4169
4504
  */
4170
- const collectRootMutationKeys = (selectionSet, fragments, variables, known, keys, visitedFragments) => {
4505
+ const collectRootMutationKeys = (selectionSet, fragments, variables, known, keys, visitedFragments, argumentsCoerce) => {
4171
4506
  for (const selection of selectionSet.selections) {
4172
4507
  if (!includedByDirectives(selection, variables)) continue;
4173
4508
  if (selection.kind === "Field") {
4174
4509
  const name = selection.name.value;
4175
4510
  if (name === "__typename") continue;
4176
4511
  if (!known.has(name)) return false;
4177
- keys.add(selection.alias?.value ?? name);
4512
+ if (argumentsCoerce(selection)) keys.add(selection.alias?.value ?? name);
4178
4513
  } else if (selection.kind === "InlineFragment") {
4179
- if (!collectRootMutationKeys(selection.selectionSet, fragments, variables, known, keys, visitedFragments)) return false;
4514
+ if (!collectRootMutationKeys(selection.selectionSet, fragments, variables, known, keys, visitedFragments, argumentsCoerce)) return false;
4180
4515
  } else if (selection.kind === "FragmentSpread") {
4181
4516
  const fragmentName = selection.name.value;
4182
4517
  if (visitedFragments.has(fragmentName)) continue;
4183
4518
  visitedFragments.add(fragmentName);
4184
4519
  const fragment = fragments?.[fragmentName];
4185
4520
  if (!fragment) return false;
4186
- if (!collectRootMutationKeys(fragment.selectionSet, fragments, variables, known, keys, visitedFragments)) return false;
4521
+ if (!collectRootMutationKeys(fragment.selectionSet, fragments, variables, known, keys, visitedFragments, argumentsCoerce)) return false;
4187
4522
  }
4188
4523
  }
4189
4524
  return true;
4190
4525
  };
4191
- 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" });
4526
+ 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" });
4192
4527
  /** Opens the request-wide transaction and wires up latch, inactivity timer and cleanup. */
4193
4528
  const openSharedTx = (db, context, txCtx, stateKey, expected) => {
4194
4529
  let release;
@@ -4223,6 +4558,7 @@ const openSharedTx = (db, context, txCtx, stateKey, expected) => {
4223
4558
  completed: 0,
4224
4559
  failure: void 0,
4225
4560
  aborted: false,
4561
+ timedOut: false,
4226
4562
  done: false,
4227
4563
  ready,
4228
4564
  settled,
@@ -4237,8 +4573,9 @@ const openSharedTx = (db, context, txCtx, stateKey, expected) => {
4237
4573
  suspendTimer: clearTimer,
4238
4574
  bumpTimer: () => {
4239
4575
  clearTimer();
4240
- if (state.done) return;
4576
+ if (state.done || txCtx.timeoutMs === Number.POSITIVE_INFINITY) return;
4241
4577
  timer = setTimeout(() => {
4578
+ if (!state.aborted) state.timedOut = true;
4242
4579
  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" }));
4243
4580
  }, txCtx.timeoutMs);
4244
4581
  timer.unref?.();
@@ -4257,15 +4594,15 @@ const openSharedTx = (db, context, txCtx, stateKey, expected) => {
4257
4594
  state.bumpTimer();
4258
4595
  return state;
4259
4596
  };
4260
- const runOnSharedTx = async (state, body) => {
4597
+ const runOnSharedTx = async (state, timeoutMs, body) => {
4261
4598
  if (state.aborted) {
4262
4599
  await state.settled.catch(() => {});
4263
- throw abortedBatchError();
4600
+ throw abortedBatchError(state, timeoutMs);
4264
4601
  }
4265
4602
  state.suspendTimer();
4266
4603
  const tx = await state.ready;
4267
4604
  try {
4268
- if (state.aborted) throw abortedBatchError();
4605
+ if (state.aborted) throw abortedBatchError(state, timeoutMs);
4269
4606
  const result = await body(tx);
4270
4607
  state.completed += 1;
4271
4608
  if (state.completed >= state.expected) {
@@ -4298,14 +4635,24 @@ const runMutation = async (db, context, info, txCtx, body, forceTx) => {
4298
4635
  if (contextIsObject && context[drizzleExecutorKey]) return body(context[drizzleExecutorKey]);
4299
4636
  if (!txCtx || info.operation.operation !== "mutation") return forceTx ? db.transaction((tx) => body(tx)) : body(db);
4300
4637
  const keys = /* @__PURE__ */ new Set();
4301
- 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);
4638
+ const mutationFields = info.schema.getMutationType()?.getFields() ?? {};
4639
+ state = !collectRootMutationKeys(info.operation.selectionSet, info.fragments, info.variableValues, txCtx.fieldNames, keys, /* @__PURE__ */ new Set(), (node) => {
4640
+ const fieldDef = mutationFields[node.name.value];
4641
+ if (!fieldDef) return false;
4642
+ try {
4643
+ (0, graphql.getArgumentValues)(fieldDef, node, info.variableValues);
4644
+ return true;
4645
+ } catch {
4646
+ return false;
4647
+ }
4648
+ }) || keys.size <= 1 ? { kind: "passthrough" } : openSharedTx(db, context, txCtx, stateKey, keys.size);
4302
4649
  txCtx.states.set(stateKey, state);
4303
4650
  }
4304
4651
  if (state.kind === "passthrough") {
4305
4652
  const executor = resolveExecutor(db, context);
4306
4653
  return forceTx && executor === db ? db.transaction((tx) => body(tx)) : body(executor);
4307
4654
  }
4308
- return runOnSharedTx(state, body);
4655
+ return runOnSharedTx(state, txCtx.timeoutMs, body);
4309
4656
  };
4310
4657
  //#endregion
4311
4658
  //#region src/util/builders/common/write-hooks.ts
@@ -4459,10 +4806,11 @@ const hardDeleteArg = {
4459
4806
  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."
4460
4807
  };
4461
4808
  const prepareMutationRelationColumns = (params) => {
4462
- const { relationMap, tables, tableName, typeNameMapper, table, pkNames, parsedInfo, resolveName } = params;
4809
+ const { relationMap, filterCtx, tables, tableName, typeNameMapper, table, pkNames, parsedInfo, resolveName } = params;
4463
4810
  const typeName = resolveObjectTypeName(tableName, typeNameMapper, resolveName);
4464
4811
  const withParams = relationMap[tableName] ? extractRelationsParams(relationMap, tables, tableName, parsedInfo, typeName, {
4465
4812
  typeNameMapper,
4813
+ filterCtx,
4466
4814
  limits: params.limits,
4467
4815
  scope: params.scope,
4468
4816
  defaultOrderBy: params.defaultOrderBy,
@@ -4472,7 +4820,8 @@ const prepareMutationRelationColumns = (params) => {
4472
4820
  const baseColumns = extractSelectedColumnsFromTreeSQLFormat(parsedInfo.fieldsByTypeName[typeName], table, {
4473
4821
  tableName,
4474
4822
  relationMap,
4475
- tables
4823
+ tables,
4824
+ allRelations: filterCtx?.relationMap
4476
4825
  });
4477
4826
  return {
4478
4827
  columns: hasRelations ? withPrimaryKeyColumns(baseColumns, table, pkNames) : baseColumns,
@@ -4608,7 +4957,7 @@ const assertSingleMatch = async (executor, table, filters) => {
4608
4957
  * MySQL (no `RETURNING`; its hooks see `rows: []` as on every MySQL mutation), skips the
4609
4958
  * `RETURNING` and reads the count off the driver's result via {@link rowsAffected}.
4610
4959
  */
4611
- const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, fieldName, requireWhere, filterCtx, txCtx, nested, policies, pkNames = [], returnsRows }) => {
4960
+ const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, fieldName, requireWhere, filterCtx, txCtx, nested, policies, pkNames = [], returnsRows, writeFilter }) => {
4612
4961
  const softDelete = kind === "delete" ? policies?.softDelete?.(tableName) : void 0;
4613
4962
  const canHardDelete = softDelete?.hardDelete === true;
4614
4963
  const queryArgs = {
@@ -4651,7 +5000,7 @@ const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, f
4651
5000
  required: requireWhere,
4652
5001
  deleted: hard ? "INCLUDE" : void 0
4653
5002
  });
4654
- if (filters) query = query.where(filters);
5003
+ if (filters) query = query.where(writeFilter ? writeFilter(filters) : filters);
4655
5004
  if (!readsRows) {
4656
5005
  const result = await query;
4657
5006
  await after();
@@ -4711,27 +5060,40 @@ const getOrCreateLoader = (context, key, batchFn) => {
4711
5060
  if (!loaders.has(key)) loaders.set(key, new BatchLoader(batchFn));
4712
5061
  return loaders.get(key);
4713
5062
  };
5063
+ const requestCacheNode = () => ({
5064
+ children: /* @__PURE__ */ new WeakMap(),
5065
+ entries: /* @__PURE__ */ new Map()
5066
+ });
4714
5067
  /**
4715
5068
  * Memoizes a value that a resolver derives from its own field — its args and its selection —
4716
5069
  * for the length of one request. Resolvers run once per parent row, so anything derived only
4717
5070
  * from the field is otherwise recomputed for every row of a batch.
4718
5071
  *
4719
- * The cache lives on the context (weakly keyed by the AST node the value is derived from, so a
4720
- * document cached across requests never carries values between them) and `key` separates fields
4721
- * that share a node: one fragment selection can resolve for more than one parent type. Without a
4722
- * context object there is nowhere request-scoped to cache, so the value is simply computed.
5072
+ * The value is keyed by every AST node of the field — `info.fieldNodes`, the nodes graphql-js
5073
+ * merged into this one response field — not only the first. A fragment's field node is the first
5074
+ * node wherever the fragment is spread, but the merged selection differs when a sibling selection
5075
+ * at one spread site adds to it, so keying on one node would serve one site the other's selection.
5076
+ * The nodes are matched by identity, in order, through a chain of weak maps, so a document cached
5077
+ * across requests never carries values between them (the root lives on the context), and an array
5078
+ * graphql rebuilds per call still hits. `key` separates fields that share nodes: one fragment
5079
+ * selection can resolve for more than one parent type. Without a context object, or without
5080
+ * nodes, there is nowhere request-scoped to cache, so the value is simply computed.
4723
5081
  *
4724
5082
  * `compute` must not read the parent row.
4725
5083
  */
4726
- const getOrCreateRequestValue = (context, node, key, compute) => {
4727
- if (!context || typeof context !== "object" || !node || typeof node !== "object") return compute();
4728
- if (!context[DRIZZLE_REQUEST_CACHE_KEY]) context[DRIZZLE_REQUEST_CACHE_KEY] = /* @__PURE__ */ new WeakMap();
4729
- const cache = context[DRIZZLE_REQUEST_CACHE_KEY];
4730
- let entries = cache.get(node);
4731
- if (!entries) {
4732
- entries = /* @__PURE__ */ new Map();
4733
- cache.set(node, entries);
5084
+ const getOrCreateRequestValue = (context, nodes, key, compute) => {
5085
+ if (!context || typeof context !== "object" || !Array.isArray(nodes) || !nodes.length || nodes.some((node) => !node || typeof node !== "object")) return compute();
5086
+ if (!context[DRIZZLE_REQUEST_CACHE_KEY]) context[DRIZZLE_REQUEST_CACHE_KEY] = requestCacheNode();
5087
+ let level = context[DRIZZLE_REQUEST_CACHE_KEY];
5088
+ for (const node of nodes) {
5089
+ let next = level.children.get(node);
5090
+ if (!next) {
5091
+ next = requestCacheNode();
5092
+ level.children.set(node, next);
5093
+ }
5094
+ level = next;
4734
5095
  }
5096
+ const entries = level.entries;
4735
5097
  if (entries.has(key)) return entries.get(key);
4736
5098
  const value = compute();
4737
5099
  entries.set(key, value);
@@ -4961,11 +5323,12 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
4961
5323
  if (!parentTable || !targetTable) return;
4962
5324
  const joinPairs = extractRelationJoinColumnPairs(relEntry, parentTable, targetTable);
4963
5325
  if (!joinPairs) return;
4964
- const localColPropNames = joinPairs.map((pair) => pair.localColPropName);
5326
+ const localCols = joinPairs.map((pair) => pair.localCol);
4965
5327
  const foreignCol = joinPairs[0].foreignCol;
4966
5328
  const junction = extractJunctionColumns(relEntry);
4967
5329
  const groupCols = junction ? [junction.sourceCol] : joinPairs.map((pair) => pair.foreignCol);
4968
5330
  const groupKeys = junction ? [PARENT_KEY] : joinPairs.map((pair) => pair.foreignColPropName);
5331
+ const junctionName = junction ? junctionTableName(tables, relEntry) : void 0;
4969
5332
  const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, joinPairs.map((pair) => pair.localCol), groupCols);
4970
5333
  const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, isOne && relEntry.relation?.optional === false);
4971
5334
  const limitPolicy = isOne ? void 0 : limits?.(targetTableName);
@@ -5007,10 +5370,10 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
5007
5370
  };
5008
5371
  const resolve = async (parent, args, context, info) => {
5009
5372
  if (parent[relationName] !== void 0) return parent[relationName];
5010
- const localValues = localColPropNames.map((name) => parent[name]);
5373
+ const localValues = parentJoinValues(parent, joinPairs);
5011
5374
  if (localValues.some((value) => value == null)) return isOne ? null : [];
5012
5375
  const { where: whereArg, limit: requestedLimit, offset, after, deleted } = args ?? {};
5013
- const { orderByArg, limit, distinct, cursorEntries, cursorValues, columns, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes?.[0], `relation:${tableName}::${relationName}`, () => {
5376
+ const { orderByArg, limit, distinct, cursorEntries, cursorValues, columns, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes, `relation:${tableName}::${relationName}`, () => {
5014
5377
  const orderByArg = isOne ? args?.orderBy : withDefaultOrderBy(args ?? {}, targetTableName, policies?.defaultOrderBy).orderBy;
5015
5378
  const limit = applyLimitPolicy(requestedLimit, limitPolicy, errorCtx);
5016
5379
  assertPagingNotNegative({ offset }, errorCtx);
@@ -5051,55 +5414,60 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
5051
5414
  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" });
5052
5415
  return getOrCreateLoader(context, loaderKey, async (parentKeys) => {
5053
5416
  const executor = resolveExecutor(db, context);
5054
- const uniqueKeys = [...new Map(parentKeys.map((key) => [joinKeyOf(key), key])).values()];
5417
+ const uniqueKeys = [...new Map(parentKeys.map((key) => [joinKeyOf(key, localCols), key])).values()];
5055
5418
  const scope = resolveScope(policies, context, filterCtx);
5056
- let whereCondition = withScope(scope, targetTableName, targetTable, (0, drizzle_orm.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);
5057
- if (distinct) {
5058
- const keys = await selectDistinctKeys({
5059
- db: executor,
5060
- table: targetTable,
5061
- tableName: targetTableName,
5062
- distinct,
5063
- pkNames: targetPkNames,
5064
- where: whereCondition,
5065
- orderBy: orderByArg,
5066
- partitionBy: groupCols,
5067
- junction: junction ? {
5068
- table: junction.table,
5069
- on: (0, drizzle_orm.eq)(junction.targetCol, foreignCol),
5070
- parentKey: junction.sourceCol
5071
- } : void 0
5072
- });
5073
- if (!keys.length) return parentKeys.map(() => isOne ? null : []);
5074
- whereCondition = (0, drizzle_orm.and)(whereCondition, junction ? (0, drizzle_orm.or)(...keys.map((key) => (0, drizzle_orm.and)((0, drizzle_orm.eq)(junction.sourceCol, key[DISTINCT_PARENT]), primaryKeyRestriction(targetTable, targetPkNames, [key])))) : primaryKeyRestriction(targetTable, targetPkNames, keys));
5075
- }
5076
- const selection = junction ? {
5077
- ...columns ?? (0, drizzle_orm.getColumns)(targetTable),
5078
- [PARENT_KEY]: junction.sourceCol
5079
- } : columns;
5080
- let rows;
5081
- 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);
5082
- else {
5083
- let q = withJunction((selection ? executor.select(selection) : executor.select()).from(targetTable), junction, foreignCol).where(whereCondition);
5084
- const orderExprs = cursorEntries ? cursorOrderExprs(targetTable, cursorEntries) : orderByArg ? extractOrderBy(targetTable, orderByArg, relationFilterCtx(filterCtx, targetTableName, scope), whereArg) : [];
5085
- if (orderExprs.length) q = q.orderBy(...orderExprs);
5086
- rows = await q;
5087
- }
5088
- if (cursorEntries) attachRowCursors(rows, cursorEntries);
5089
- const keyOf = (row) => {
5090
- const key = joinKeyOf(groupKeys.map((name) => row[name]));
5091
- if (junction) delete row[PARENT_KEY];
5092
- return key;
5093
- };
5094
- if (isOne) {
5095
- const byKey = new Map(rows.map((row) => [keyOf(row), row]));
5419
+ const loadChunk = async (chunkKeys) => {
5420
+ let whereCondition = withScope(scope, targetTableName, targetTable, (0, drizzle_orm.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);
5421
+ if (distinct) {
5422
+ const keys = await selectDistinctKeys({
5423
+ db: executor,
5424
+ table: targetTable,
5425
+ tableName: targetTableName,
5426
+ distinct,
5427
+ pkNames: targetPkNames,
5428
+ where: whereCondition,
5429
+ orderBy: orderByArg,
5430
+ partitionBy: groupCols,
5431
+ junction: junction ? {
5432
+ table: junction.table,
5433
+ on: (0, drizzle_orm.eq)(junction.targetCol, foreignCol),
5434
+ parentKey: junction.sourceCol
5435
+ } : void 0
5436
+ });
5437
+ if (!keys.length) return /* @__PURE__ */ new Map();
5438
+ whereCondition = (0, drizzle_orm.and)(whereCondition, junction ? balancedOr(keys.map((key) => (0, drizzle_orm.and)((0, drizzle_orm.eq)(junction.sourceCol, key[DISTINCT_PARENT]), primaryKeyRestriction(targetTable, targetPkNames, [key])))) : primaryKeyRestriction(targetTable, targetPkNames, keys));
5439
+ }
5440
+ const selection = junction ? {
5441
+ ...columns ?? (0, drizzle_orm.getColumns)(targetTable),
5442
+ [PARENT_KEY]: junction.sourceCol
5443
+ } : columns;
5444
+ let rows;
5445
+ 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);
5446
+ else {
5447
+ let q = withJunction((selection ? executor.select(selection) : executor.select()).from(targetTable), junction, foreignCol).where(whereCondition);
5448
+ const orderExprs = cursorEntries ? cursorOrderExprs(targetTable, cursorEntries) : orderByArg ? extractOrderBy(targetTable, orderByArg, relationFilterCtx(filterCtx, targetTableName, scope), whereArg) : [];
5449
+ if (orderExprs.length) q = q.orderBy(...orderExprs);
5450
+ rows = await q;
5451
+ }
5452
+ if (cursorEntries) attachRowCursors(rows, cursorEntries);
5453
+ const keyOf = (row) => {
5454
+ const key = joinKeyOf(groupKeys.map((name) => row[name]), groupCols);
5455
+ if (junction) delete row[PARENT_KEY];
5456
+ return key;
5457
+ };
5458
+ if (isOne) {
5459
+ const byKey = new Map(rows.map((row) => [keyOf(row), row]));
5460
+ remapToGraphQLArrayOutput(rows, targetTableName, targetTable);
5461
+ return byKey;
5462
+ }
5463
+ const grouped = new Map(chunkKeys.map((key) => [joinKeyOf(key, localCols), []]));
5464
+ for (const row of rows) grouped.get(keyOf(row))?.push(row);
5096
5465
  remapToGraphQLArrayOutput(rows, targetTableName, targetTable);
5097
- return parentKeys.map((key) => byKey.get(joinKeyOf(key)) ?? null);
5098
- }
5099
- const grouped = new Map(uniqueKeys.map((key) => [joinKeyOf(key), []]));
5100
- for (const row of rows) grouped.get(keyOf(row))?.push(row);
5101
- remapToGraphQLArrayOutput(rows, targetTableName, targetTable);
5102
- return parentKeys.map((key) => grouped.get(joinKeyOf(key)) ?? []);
5466
+ return grouped;
5467
+ };
5468
+ const results = /* @__PURE__ */ new Map();
5469
+ for (const chunk of parentKeyChunks(uniqueKeys, groupCols.length)) for (const [key, value] of await loadChunk(chunk)) results.set(key, value);
5470
+ return parentKeys.map((key) => results.get(joinKeyOf(key, localCols)) ?? (isOne ? null : []));
5103
5471
  }).load(localValues);
5104
5472
  };
5105
5473
  return async (parent, args, context, info) => {
@@ -5204,7 +5572,7 @@ const generateTableOrderCached = (table, innerOrder) => {
5204
5572
  try {
5205
5573
  const columns = visibleColumns(table);
5206
5574
  const columnEntries = Object.entries(columns);
5207
- remapped = Object.fromEntries(columnEntries.map(([columnName, _columnDescription]) => [columnName, { type: innerOrder }]));
5575
+ remapped = Object.fromEntries(columnEntries.filter(([, column]) => isOrderableColumn(column)).map(([columnName]) => [columnName, { type: innerOrder }]));
5208
5576
  if (cacheable) {
5209
5577
  let byInnerOrder = orderMap.get(table);
5210
5578
  if (!byInnerOrder) {
@@ -5247,43 +5615,71 @@ const generateTableSelectTypeFieldsCached = (table, tableName, cacheCtx) => {
5247
5615
  return remapped;
5248
5616
  };
5249
5617
  /**
5618
+ * The to-one relations a table's OrderBy can order through, as `[relationName, targetTableName]`.
5619
+ * To-many relations are left out: "order a parent by its many children" has no single defined
5620
+ * value to sort on. A relation whose name collides with a column is skipped — the column keeps
5621
+ * the name, even one with no ordering of its own, so `extractOrderBy` never reads it as a relation.
5622
+ */
5623
+ const orderRelations = (tableName, relationMap, tables) => {
5624
+ const relations = relationMap?.[tableName];
5625
+ const table = tables?.[tableName];
5626
+ if (!relations || !tables || !table) return [];
5627
+ const columns = visibleColumns(table);
5628
+ return Object.entries(relations).filter(([relationName, relEntry]) => {
5629
+ const relation = relEntry.relation ?? relEntry;
5630
+ return !(relationName in columns) && !!tables[relEntry.targetTableName] && (0, drizzle_orm.is)(relation, drizzle_orm.One) && isFilterableRelation(relation);
5631
+ }).map(([relationName, relEntry]) => [relationName, relEntry.targetTableName]);
5632
+ };
5633
+ /**
5634
+ * Whether a table's OrderBy would have any field: an orderable column of its own, or a to-one
5635
+ * relation to a table that has one. A table of nothing but JSON columns has none, and an input
5636
+ * object with no fields is an invalid schema, so such a table gets no OrderBy and no `orderBy`
5637
+ * argument at all.
5638
+ */
5639
+ const hasOrderFields = (tableName, relationMap, tables, visiting = /* @__PURE__ */ new Set()) => {
5640
+ const table = tables[tableName];
5641
+ if (table && Object.values(visibleColumns(table)).some((column) => isOrderableColumn(column))) return true;
5642
+ if (visiting.has(tableName)) return false;
5643
+ visiting.add(tableName);
5644
+ return orderRelations(tableName, relationMap, tables).some(([, target]) => hasOrderFields(target, relationMap, tables, visiting));
5645
+ };
5646
+ /**
5250
5647
  * Order fields for a table's to-one relations: each takes the target table's own OrderBy
5251
5648
  * input, so a list can be sorted by a related row's column (compiled as a correlated
5252
- * subquery in the ORDER BY — see `extractOrderBy`). To-many relations are left out: "order
5253
- * a parent by its many children" has no single defined value to sort on. A relation whose
5254
- * name collides with a column is skipped — the column keeps the field.
5649
+ * subquery in the ORDER BY — see `extractOrderBy`). A target with no OrderBy of its own is
5650
+ * left out.
5255
5651
  */
5256
- const generateRelationOrderFields = (tableName, cacheCtx, typeNameMapper, columnFields, relationMap, tables) => {
5257
- const relations = relationMap?.[tableName];
5258
- if (!relations || !tables) return {};
5652
+ const generateRelationOrderFields = (tableName, cacheCtx, typeNameMapper, relationMap, tables) => {
5259
5653
  const fields = {};
5260
- for (const [relationName, relEntry] of Object.entries(relations)) {
5261
- if (relationName in columnFields) continue;
5262
- const targetTable = tables[relEntry.targetTableName];
5263
- const relation = relEntry.relation ?? relEntry;
5264
- if (!targetTable || !(0, drizzle_orm.is)(relation, drizzle_orm.One) || !isFilterableRelation(relation)) continue;
5265
- fields[relationName] = {
5266
- type: generateTableOrderTypeCached(targetTable, relEntry.targetTableName, typeNameMapper, cacheCtx, relationMap, tables),
5654
+ for (const [relationName, targetTableName] of orderRelations(tableName, relationMap, tables)) {
5655
+ const type = generateTableOrderTypeCached(tables[targetTableName], targetTableName, typeNameMapper, cacheCtx, relationMap, tables);
5656
+ if (type) fields[relationName] = {
5657
+ type,
5267
5658
  description: `Order by columns of the related ${relationName} row`
5268
5659
  };
5269
5660
  }
5270
5661
  return fields;
5271
5662
  };
5663
+ /**
5664
+ * The table's `<Type>OrderBy` input, or `undefined` when it would have no fields (see
5665
+ * {@link hasOrderFields}); the caller then leaves the `orderBy` argument out.
5666
+ */
5272
5667
  const generateTableOrderTypeCached = (table, tableName, typeNameMapper, cacheCtx, relationMap, tables) => {
5273
5668
  if (cacheCtx.orderTypeCache.has(table)) return cacheCtx.orderTypeCache.get(table);
5669
+ if (!hasOrderFields(tableName, relationMap, {
5670
+ ...tables,
5671
+ [tableName]: table
5672
+ })) return;
5274
5673
  const order = new graphql.GraphQLInputObjectType({
5275
5674
  name: cacheCtx.typeName({
5276
5675
  kind: "orderBy",
5277
5676
  defaultName: `${resolveTypeName(tableName, typeNameMapper)}OrderBy`,
5278
5677
  table: tableName
5279
5678
  }),
5280
- fields: () => {
5281
- const orderColumns = generateTableOrderCached(table, innerOrderType(cacheCtx.typeName));
5282
- return {
5283
- ...orderColumns,
5284
- ...generateRelationOrderFields(tableName, cacheCtx, typeNameMapper, orderColumns, relationMap, tables)
5285
- };
5286
- }
5679
+ fields: () => ({
5680
+ ...generateTableOrderCached(table, innerOrderType(cacheCtx.typeName)),
5681
+ ...generateRelationOrderFields(tableName, cacheCtx, typeNameMapper, relationMap, tables)
5682
+ })
5287
5683
  });
5288
5684
  cacheCtx.orderTypeCache.set(table, order);
5289
5685
  return order;
@@ -5502,7 +5898,7 @@ const generateSelectFields = (tables, tableName, relationMap, fromTableName, fro
5502
5898
  type: new graphql.GraphQLNonNull(new graphql.GraphQLList(new graphql.GraphQLNonNull(relType))),
5503
5899
  args: {
5504
5900
  where: { type: relSelectData.filters },
5505
- orderBy: { type: relSelectData.order },
5901
+ ...relSelectData.order ? { orderBy: { type: relSelectData.order } } : {},
5506
5902
  offset: { type: graphql.GraphQLInt },
5507
5903
  limit: { type: graphql.GraphQLInt },
5508
5904
  ...deletedArg(cacheCtx.softDeleteOf, targetTableName, cacheCtx.typeName),
@@ -5730,6 +6126,7 @@ const resolveBuildConfig = (config, schema) => {
5730
6126
  };
5731
6127
  const transactionsOpt = config?.transactions;
5732
6128
  const transactions = transactionsOpt === void 0 || transactionsOpt === "none" ? void 0 : { timeoutMs: (transactionsOpt === "auto" ? void 0 : transactionsOpt.timeoutMs) ?? 3e4 };
6129
+ 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}.`);
5733
6130
  const limitsConfig = config?.limits;
5734
6131
  if (limitsConfig) {
5735
6132
  const check = (policy, where) => {
@@ -5762,6 +6159,7 @@ const resolveBuildConfig = (config, schema) => {
5762
6159
  for (const [columnName, entry] of Object.entries(orderBy)) {
5763
6160
  if (!columns[columnName]) throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy names '${columnName}', which is not a column of '${tableName}'.`);
5764
6161
  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.`);
6162
+ 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.`);
5765
6163
  const direction = typeof entry === "string" ? entry : entry?.direction;
5766
6164
  if (direction !== "asc" && direction !== "desc") throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy.${columnName} must be 'asc', 'desc', or { direction, priority? }.`);
5767
6165
  const priority = typeof entry === "string" ? void 0 : entry?.priority;
@@ -5820,7 +6218,7 @@ const resolveBuildConfig = (config, schema) => {
5820
6218
  }
5821
6219
  }
5822
6220
  }
5823
- const onWrite = writeHooks.size ? (tableName) => writeHooks.get(tableName) : void 0;
6221
+ const onWrite = [...writeHooks.values()].some(Boolean) ? (tableName) => writeHooks.get(tableName) : void 0;
5824
6222
  const policies = scopeConfig || contextValuesConfig || softDeleteInfos.size || onWrite || defaultOrderBy ? {
5825
6223
  scope: scopeConfig ? (tableName) => scopeConfig[tableName] : void 0,
5826
6224
  contextValues: contextValuesConfig ? (tableName) => contextValuesConfig[tableName] : void 0,
@@ -5911,8 +6309,14 @@ const AGGREGATE_OPS = [
5911
6309
  ];
5912
6310
  /** Ops whose result is a row count: never null, and returned as `Int!` rather than the column's type. */
5913
6311
  const COUNT_OPS = /* @__PURE__ */ new Set(["countNonNull", "countDistinct"]);
6312
+ /**
6313
+ * `avg`, except on MySQL, where AVG over an integer column is a DECIMAL rounded to
6314
+ * `div_precision_increment` (4 by default) digits. Averaging the value as a DOUBLE keeps the
6315
+ * fraction the `Float` output promises, as the other two dialects do.
6316
+ */
6317
+ const avgOf = (col) => columnDialect(col) === "mysql" ? drizzle_orm.sql`avg(cast(${col} as double))`.mapWith(String) : (0, drizzle_orm.avg)(col);
5914
6318
  const OP_FNS = {
5915
- avg: drizzle_orm.avg,
6319
+ avg: avgOf,
5916
6320
  sum: drizzle_orm.sum,
5917
6321
  min: drizzle_orm.min,
5918
6322
  max: drizzle_orm.max,
@@ -5922,6 +6326,18 @@ const OP_FNS = {
5922
6326
  /** Separator for flat select aliases (`avg__price`) — reassembled into nested output by the resolver. */
5923
6327
  const SEP = "__";
5924
6328
  /**
6329
+ * Column types PostgreSQL can test for equality but has no `min`/`max` aggregate for, plus the
6330
+ * SQLite bigint blob, whose bytes compare as text (`max(9, 10)` is 9). They still count
6331
+ * distinct values and group.
6332
+ */
6333
+ const NO_MIN_MAX_COLUMN_TYPES = /* @__PURE__ */ new Set([
6334
+ "PgUUID",
6335
+ "PgMacaddr",
6336
+ "PgMacaddr8",
6337
+ "PgBinaryVector",
6338
+ "SQLiteBigInt"
6339
+ ]);
6340
+ /**
5925
6341
  * Classifies a table's columns for aggregation. avg/sum only make sense on numeric scalars;
5926
6342
  * min/max work on any orderable scalar (numbers, strings, bigints, dates, enums). Booleans,
5927
6343
  * arrays (including array-typed number columns), JSON, buffers, and object-shaped columns
@@ -5953,6 +6369,7 @@ const classifyAggregateColumns = (table, tableName) => {
5953
6369
  if (cached) return cached;
5954
6370
  const numeric = {};
5955
6371
  const orderable = {};
6372
+ const comparable = {};
5956
6373
  const all = {};
5957
6374
  for (const [columnName, column] of Object.entries(visibleColumns(table))) {
5958
6375
  all[columnName] = column;
@@ -5962,7 +6379,11 @@ const classifyAggregateColumns = (table, tableName) => {
5962
6379
  const { type: dataType, constraint } = (0, drizzle_orm.extractExtendedColumnType)(column);
5963
6380
  if (dataType === "boolean" || dataType === "array" || dataType === "custom") continue;
5964
6381
  if (dataType === "object" && constraint !== "date") continue;
5965
- orderable[columnName] = {
6382
+ comparable[columnName] = {
6383
+ column,
6384
+ converted
6385
+ };
6386
+ if (!NO_MIN_MAX_COLUMN_TYPES.has(column.columnType)) orderable[columnName] = {
5966
6387
  column,
5967
6388
  converted
5968
6389
  };
@@ -5971,6 +6392,7 @@ const classifyAggregateColumns = (table, tableName) => {
5971
6392
  const sets = {
5972
6393
  numeric,
5973
6394
  orderable,
6395
+ comparable,
5974
6396
  all
5975
6397
  };
5976
6398
  byName.set(tableName, sets);
@@ -5983,13 +6405,13 @@ const classifyAggregateColumns = (table, tableName) => {
5983
6405
  * and avg/sum of integers overflow Int / produce decimals)
5984
6406
  * - `min` / `max` — per orderable column, the column's own (nullable) scalar type
5985
6407
  * - `countNonNull` — per column, `Int!`: how many matching rows have a non-null value there
5986
- * - `countDistinct` — per orderable column, `Int!`: how many distinct non-null values there are
6408
+ * - `countDistinct` — per comparable column, `Int!`: how many distinct non-null values there are
5987
6409
  * Each wrapper is omitted when no column qualifies for it.
5988
6410
  */
5989
6411
  const generateAggregateTypes = (table, tableName, typeName, cacheCtx) => {
5990
6412
  const cached = cacheCtx?.aggregateTypeCache.get(tableName);
5991
6413
  if (cached) return cached;
5992
- const { numeric, orderable, all } = classifyAggregateColumns(table, tableName);
6414
+ const { numeric, orderable, comparable, all } = classifyAggregateColumns(table, tableName);
5993
6415
  const nameOf = (op) => cacheCtx?.typeName({
5994
6416
  kind: "aggregate",
5995
6417
  defaultName: `${typeName}${capitalize(op)}Aggregate`,
@@ -6010,7 +6432,7 @@ const generateAggregateTypes = (table, tableName, typeName, cacheCtx) => {
6010
6432
  }) };
6011
6433
  const countSets = {
6012
6434
  countNonNull: all,
6013
- countDistinct: orderable
6435
+ countDistinct: comparable
6014
6436
  };
6015
6437
  for (const [op, columns] of Object.entries(countSets)) {
6016
6438
  const columnNames = Object.keys(columns);
@@ -6185,11 +6607,12 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
6185
6607
  }
6186
6608
  return;
6187
6609
  }
6188
- const localColPropNames = joinPairs.map((pair) => pair.localColPropName);
6610
+ const localCols = joinPairs.map((pair) => pair.localCol);
6189
6611
  const foreignCol = joinPairs[0].foreignCol;
6190
6612
  const junction = extractJunctionColumns(relEntry);
6191
6613
  if (junction === null) return;
6192
6614
  const groupCols = junction ? [junction.sourceCol] : joinPairs.map((pair) => pair.foreignCol);
6615
+ const junctionName = junction ? junctionTableName(tables, relEntry) : void 0;
6193
6616
  const groupAliases = groupCols.map((_, i) => `${GROUP_KEY}${i}`);
6194
6617
  const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, joinPairs.map((pair) => pair.localCol), groupCols);
6195
6618
  const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, false);
@@ -6203,7 +6626,7 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
6203
6626
  };
6204
6627
  const resolve = async (parent, args, context, info) => {
6205
6628
  try {
6206
- const { request, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes?.[0], `aggregate:${tableName}::${relationName}`, () => {
6629
+ const { request, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes, `aggregate:${tableName}::${relationName}`, () => {
6207
6630
  const request = parseAggregateRequest(info, target);
6208
6631
  return {
6209
6632
  request,
@@ -6215,23 +6638,26 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
6215
6638
  };
6216
6639
  });
6217
6640
  if (!Object.keys(request.selection).length) return {};
6218
- const localValues = localColPropNames.map((name) => parent[name]);
6641
+ const localValues = parentJoinValues(parent, joinPairs);
6219
6642
  if (localValues.some((value) => value == null)) return assembleAggregateRow({}, request, target);
6220
6643
  const whereArg = args?.where;
6221
6644
  const deleted = args?.deleted;
6222
6645
  const loader = getOrCreateLoader(context, loaderKey, async (parentKeys) => {
6223
6646
  const executor = resolveExecutor(db, context);
6224
- const uniqueKeys = [...new Map(parentKeys.map((key) => [joinKeyOf(key), key])).values()];
6647
+ const uniqueKeys = [...new Map(parentKeys.map((key) => [joinKeyOf(key, localCols), key])).values()];
6225
6648
  const scope = resolveScope(policies, context, filterCtx);
6226
- const whereCondition = withScope(scope, targetTableName, targetTable, (0, drizzle_orm.and)(parentKeyCondition(groupCols, uniqueKeys), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0), deleted ?? defaultDeleted);
6227
6649
  const groupSelection = Object.fromEntries(groupAliases.map((alias, i) => [alias, groupCols[i]]));
6228
- const selected = executor.select({
6229
- ...groupSelection,
6230
- ...request.selection
6231
- }).from(targetTable);
6232
- const rows = await (junction ? selected.innerJoin(junction.table, (0, drizzle_orm.eq)(junction.targetCol, foreignCol)) : selected).where(whereCondition).groupBy(...groupCols);
6233
- const byKey = new Map(rows.map((row) => [joinKeyOf(groupAliases.map((alias) => row[alias])), row]));
6234
- return parentKeys.map((key) => byKey.get(joinKeyOf(key)) ?? {});
6650
+ const byKey = /* @__PURE__ */ new Map();
6651
+ for (const chunk of parentKeyChunks(uniqueKeys, groupCols.length)) {
6652
+ const whereCondition = withScope(scope, targetTableName, targetTable, (0, drizzle_orm.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);
6653
+ const selected = executor.select({
6654
+ ...groupSelection,
6655
+ ...request.selection
6656
+ }).from(targetTable);
6657
+ const rows = await (junction ? selected.innerJoin(junction.table, (0, drizzle_orm.eq)(junction.targetCol, foreignCol)) : selected).where(whereCondition).groupBy(...groupCols);
6658
+ for (const row of rows) byKey.set(joinKeyOf(groupAliases.map((alias) => row[alias]), groupCols), row);
6659
+ }
6660
+ return parentKeys.map((key) => byKey.get(joinKeyOf(key, localCols)) ?? {});
6235
6661
  });
6236
6662
  return assembleAggregateRow(await loader.load(localValues), request, target);
6237
6663
  } catch (e) {
@@ -6257,12 +6683,12 @@ const HAVING_OPS = {
6257
6683
  };
6258
6684
  /**
6259
6685
  * Columns a query can group on. Grouping needs an equality operator, which is the same
6260
- * requirement min/max has, plus booleans — orderable excludes them because a min/max over
6686
+ * requirement countDistinct has, plus booleans — comparable excludes them because a min/max over
6261
6687
  * true/false says nothing, but `GROUP BY isConfirmed` is perfectly ordinary.
6262
6688
  */
6263
6689
  const groupableColumns = (table, tableName) => {
6264
- const { orderable } = classifyAggregateColumns(table, tableName);
6265
- const groupable = { ...orderable };
6690
+ const { comparable } = classifyAggregateColumns(table, tableName);
6691
+ const groupable = { ...comparable };
6266
6692
  for (const [columnName, column] of Object.entries(visibleColumns(table))) {
6267
6693
  if (groupable[columnName]) continue;
6268
6694
  const { type: dataType } = (0, drizzle_orm.extractExtendedColumnType)(column);
@@ -6303,7 +6729,7 @@ const aggregateNumberFilterType = (typeName) => sharedType(typeName, {
6303
6729
  * nothing to compare against.
6304
6730
  */
6305
6731
  const generateHavingInput = (table, tableName, typeName, cacheCtx) => {
6306
- const { numeric, orderable, all } = classifyAggregateColumns(table, tableName);
6732
+ const { numeric, comparable, all } = classifyAggregateColumns(table, tableName);
6307
6733
  const resolveName = cacheCtx?.typeName ?? ((info) => info.defaultName);
6308
6734
  const numberFilter = aggregateNumberFilterType(resolveName);
6309
6735
  const fields = { count: {
@@ -6316,7 +6742,7 @@ const generateHavingInput = (table, tableName, typeName, cacheCtx) => {
6316
6742
  min: numeric,
6317
6743
  max: numeric,
6318
6744
  countNonNull: all,
6319
- countDistinct: orderable
6745
+ countDistinct: comparable
6320
6746
  };
6321
6747
  for (const [op, columns] of Object.entries(opColumns)) {
6322
6748
  const columnNames = Object.keys(columns);
@@ -6381,22 +6807,34 @@ const generateGroupByType = (table, tableName, typeName, cacheCtx) => {
6381
6807
  }
6382
6808
  });
6383
6809
  };
6384
- /** Turns one `${typeName}Having` entry into SQL conditions over the matching aggregate expression. */
6385
- const havingComparisons = (expression, filter) => Object.entries(filter).filter(([op, value]) => value != null && op in HAVING_OPS).map(([op, value]) => HAVING_OPS[op](expression, value));
6810
+ /**
6811
+ * Turns one `${typeName}Having` entry into SQL conditions over the matching aggregate expression.
6812
+ *
6813
+ * The operand is a `Float`, but PostgreSQL types an untyped parameter after what it is compared
6814
+ * with: count and sum of integers are bigint, min/max of an int column int, so `gt: 1.5` would
6815
+ * fail to parse. There the aggregate is compared as `numeric`, which holds every one of them
6816
+ * exactly. MySQL and SQLite already compare numbers across types.
6817
+ */
6818
+ const havingComparisons = (expression, filter, isPg) => {
6819
+ const compared = isPg ? drizzle_orm.sql`cast(${expression} as numeric)` : expression;
6820
+ return Object.entries(filter).filter(([op, value]) => value != null && op in HAVING_OPS).map(([op, value]) => HAVING_OPS[op](compared, value));
6821
+ };
6386
6822
  const buildHavingCondition = (having, columns) => {
6387
6823
  if (!having) return;
6388
6824
  const conditions = [];
6825
+ const firstColumn = Object.values(columns)[0];
6826
+ const isPg = firstColumn ? columnDialect(firstColumn) === "pg" : false;
6389
6827
  for (const [key, value] of Object.entries(having)) {
6390
6828
  if (value == null) continue;
6391
6829
  if (key === "count") {
6392
- conditions.push(...havingComparisons((0, drizzle_orm.count)(), value));
6830
+ conditions.push(...havingComparisons((0, drizzle_orm.count)(), value, isPg));
6393
6831
  continue;
6394
6832
  }
6395
6833
  if (!AGGREGATE_OPS.includes(key)) continue;
6396
6834
  for (const [columnName, filter] of Object.entries(value)) {
6397
6835
  const column = columns[columnName];
6398
6836
  if (!column || filter == null) continue;
6399
- conditions.push(...havingComparisons(OP_FNS[key](column), filter));
6837
+ conditions.push(...havingComparisons(OP_FNS[key](column), filter, isPg));
6400
6838
  }
6401
6839
  }
6402
6840
  return conditions.length ? (0, drizzle_orm.and)(...conditions) : void 0;
@@ -6548,12 +6986,32 @@ const isOptionalOnInsert = (column) => !column.notNull || !!column.hasDefault ||
6548
6986
  * relation could come back without it. A `where` inherited from the reverse relation filters
6549
6987
  * the parent instead, and becomes a per-parent check.
6550
6988
  */
6551
- const ownWherePlan = (relEntry, parentTable, targetTable, localCol) => {
6989
+ const ownWherePlan = (relEntry, parentTable, targetTable, localCol, parentPrimaryKey) => {
6552
6990
  const condition = () => relationOwnCondition(relEntry, parentTable, targetTable, [localCol], [localCol]);
6553
6991
  if (!condition()) return {};
6554
- if (!isRelationFilterReversed(relEntry.relation ?? relEntry)) return { ownCondition: () => condition() };
6555
- return { parentShows: async (executor, key) => (await executor.select({ key: localCol }).from(parentTable).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(localCol, key), condition())).limit(1)).length > 0 };
6992
+ const relation = relEntry.relation ?? relEntry;
6993
+ if (!isRelationFilterReversed(relation)) return { ownCondition: () => condition() };
6994
+ const parentColumns = (0, drizzle_orm.getColumns)(parentTable);
6995
+ const parentKeyColumns = Object.fromEntries(parentPrimaryKey.filter((name) => parentColumns[name]).map((name) => [name, parentColumns[name]]));
6996
+ const keyNames = Object.keys(parentKeyColumns);
6997
+ return {
6998
+ parentShows: async (executor, key) => (await executor.select({ key: localCol }).from(parentTable).where((0, drizzle_orm.and)((0, drizzle_orm.eq)(localCol, key), condition())).limit(1)).length > 0,
6999
+ parentHidden: async (executor, rows) => {
7000
+ if (!rows.length) return false;
7001
+ 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" });
7002
+ const keyOf = (row) => keyNames.map((name) => keyString(row[name])).join("\0");
7003
+ const match = (0, drizzle_orm.or)(...rows.map((row) => (0, drizzle_orm.and)(...keyNames.map((name) => (0, drizzle_orm.eq)(parentColumns[name], row[name])))));
7004
+ const shown = new Set((await executor.select(parentKeyColumns).from(parentTable).where((0, drizzle_orm.and)(match, (0, drizzle_orm.relationsFilterToSQL)(parentTable, relationOwnWhere(relation))))).map(keyOf));
7005
+ return rows.some((row) => !shown.has(keyOf(row)));
7006
+ },
7007
+ parentKeyColumns
7008
+ };
6556
7009
  };
7010
+ /**
7011
+ * A key value in a form that compares equal across the two reads of it — the driver may hand
7012
+ * back a `Date` or a `bigint` that is a different object each time.
7013
+ */
7014
+ const keyString = (value) => value instanceof Date ? `d${value.getTime()}` : value === null || value === void 0 ? "" : `v${String(value)}`;
6557
7015
  /** The property name a column is stored under on its table, matched by identity. */
6558
7016
  const propNameOf = (table, column) => {
6559
7017
  if (!column) return;
@@ -6615,7 +7073,7 @@ const buildThroughPlan = (relationName, relEntry, relation, table, targetTableNa
6615
7073
  targetCol
6616
7074
  },
6617
7075
  targetKeyColumns: { [foreignColPropName]: foreignCol },
6618
- ...ownWherePlan(relEntry, table, targetTable, localCol),
7076
+ ...ownWherePlan(relEntry, table, targetTable, localCol, []),
6619
7077
  allowed: /* @__PURE__ */ new Set([
6620
7078
  "connect",
6621
7079
  "disconnect",
@@ -6694,7 +7152,7 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
6694
7152
  const targetPrimaryKey = resolvePrimaryKeys(targetTable).filter((name) => targetColumns[name]);
6695
7153
  const targetKeyColumns = Object.fromEntries(targetPrimaryKey.map((name) => [name, targetColumns[name]]));
6696
7154
  targetKeyColumns[foreignColPropName] = foreignCol;
6697
- const ownWhere = ownWherePlan(relEntry, table, targetTable, localCol);
7155
+ const ownWhere = ownWherePlan(relEntry, table, targetTable, localCol, resolvePrimaryKeys(table));
6698
7156
  const ops = !fkColumn.notNull ? isOne ? [
6699
7157
  "create",
6700
7158
  "connect",
@@ -7127,6 +7585,16 @@ const createNestedWriteRuntime = (params) => {
7127
7585
  }
7128
7586
  if (supplied(op["connect"])) await link(asArray(op["connect"]), "connect");
7129
7587
  };
7588
+ /**
7589
+ * The value a parent-side `connect` / `disconnect` writes to an existing row's key: `next`,
7590
+ * unless the key points at a row the relation's own `where` hides, which it then keeps.
7591
+ */
7592
+ const keepingHidden = (plan, next) => {
7593
+ if (!plan.ownCondition) return next;
7594
+ const shown = drizzle_orm.sql`select ${plan.foreignCol} from ${plan.targetTable} where ${plan.ownCondition()}`;
7595
+ const replacement = next === null ? drizzle_orm.sql`null` : drizzle_orm.sql`${drizzle_orm.sql.param(next, plan.localCol)}`;
7596
+ return drizzle_orm.sql`case when ${plan.localCol} is null or ${plan.localCol} in (${shown}) then ${replacement} else ${plan.localCol} end`;
7597
+ };
7130
7598
  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, (0, drizzle_orm.and)((0, drizzle_orm.eq)(plan.foreignCol, key), plan.ownCondition?.()))), op, "update");
7131
7599
  return {
7132
7600
  enabled: (tableName) => !!plans[tableName],
@@ -7152,7 +7620,10 @@ const createNestedWriteRuntime = (params) => {
7152
7620
  const allColumns = (0, drizzle_orm.getColumns)(table);
7153
7621
  for (const relationName of Object.keys(ops)) {
7154
7622
  const plan = tablePlans[relationName];
7155
- if (plan?.fkSide === "child" && !(plan.localColPropName in columns) && allColumns[plan.localColPropName]) columns[plan.localColPropName] = allColumns[plan.localColPropName];
7623
+ if (plan && !(plan.localColPropName in columns) && allColumns[plan.localColPropName]) columns[plan.localColPropName] = allColumns[plan.localColPropName];
7624
+ if (plan?.fkSide === "parent" && plan.parentHidden) {
7625
+ for (const [name, column] of Object.entries(plan.parentKeyColumns ?? {})) if (!(name in columns)) columns[name] = column;
7626
+ }
7156
7627
  }
7157
7628
  for (const name of selfTargetingPlan(tablePlans, tableName, ops)?.targetPrimaryKey ?? []) if (!(name in columns) && allColumns[name]) columns[name] = allColumns[name];
7158
7629
  return columns;
@@ -7172,10 +7643,15 @@ const createNestedWriteRuntime = (params) => {
7172
7643
  ...fresh.get(keyOf(row))
7173
7644
  }));
7174
7645
  },
7175
- applyParentSide: async (executor, tableName, ops, request) => {
7646
+ applyParentSide: async (executor, tableName, ops, request, existing = false) => {
7176
7647
  const tablePlans = plans[tableName];
7177
- const patch = {};
7178
- if (!tablePlans) return patch;
7648
+ const result = {
7649
+ values: {},
7650
+ expected: {},
7651
+ existing
7652
+ };
7653
+ const patch = result.values;
7654
+ if (!tablePlans) return result;
7179
7655
  const context = request?.context;
7180
7656
  const scope = resolveScope(policies, context, filterCtx);
7181
7657
  for (const [relationName, op] of Object.entries(ops)) {
@@ -7194,14 +7670,30 @@ const createNestedWriteRuntime = (params) => {
7194
7670
  if (!created) throw drizzleError(`Drizzle-GraphQL Error: 'create' on '${relationName}' inserted no row, so there is nothing to attach.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
7195
7671
  await steps.after();
7196
7672
  patch[plan.localColPropName] = created[plan.foreignColPropName];
7673
+ result.expected[relationName] = created[plan.foreignColPropName];
7197
7674
  } else if (op["connect"] !== void 0) {
7198
7675
  const rows = await executor.select({ [plan.foreignColPropName]: plan.foreignCol }).from(plan.targetTable).where(conditionOf(plan, op["connect"], "connect", scope));
7199
7676
  if (rows.length !== 1) throw connectMatchError(relationName, rows.length);
7200
- patch[plan.localColPropName] = rows[0][plan.foreignColPropName];
7201
- } else if (op["disconnect"] === true) patch[plan.localColPropName] = null;
7677
+ const key = rows[0][plan.foreignColPropName];
7678
+ patch[plan.localColPropName] = existing ? keepingHidden(plan, key) : key;
7679
+ result.expected[relationName] = key;
7680
+ } else if (op["disconnect"] === true) patch[plan.localColPropName] = existing ? keepingHidden(plan, null) : null;
7202
7681
  }
7203
7682
  for (const columnName of Object.keys(contextValues?.(tableName) ?? {})) delete patch[columnName];
7204
- return patch;
7683
+ return result;
7684
+ },
7685
+ checkParentSide: async (executor, tableName, ops, rows, patch) => {
7686
+ const tablePlans = plans[tableName];
7687
+ if (!tablePlans || !rows.length) return;
7688
+ for (const relationName of Object.keys(ops)) {
7689
+ const plan = tablePlans[relationName];
7690
+ if (plan?.fkSide !== "parent") continue;
7691
+ if (relationName in patch.expected) {
7692
+ const expected = keyString(patch.expected[relationName]);
7693
+ 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" });
7694
+ }
7695
+ 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" });
7696
+ }
7205
7697
  },
7206
7698
  applyChildSide: async (executor, tableName, ops, parentRows, request) => {
7207
7699
  const tablePlans = plans[tableName];
@@ -7268,12 +7760,13 @@ var NoParentRow = class extends Error {};
7268
7760
  * `executor` must already be a transaction.
7269
7761
  */
7270
7762
  const writeNestedEntry = async (params) => {
7271
- const { executor, runtime, tableName, ops, write, request } = params;
7763
+ const { executor, runtime, tableName, ops, write, request, existing } = params;
7272
7764
  const guarded = runtime.createsParentSide(tableName, ops);
7273
7765
  const run = async (tx) => {
7274
- const patch = await runtime.applyParentSide(tx, tableName, ops, request);
7275
- const rows = await write(tx, patch);
7766
+ const patch = await runtime.applyParentSide(tx, tableName, ops, request, existing);
7767
+ const rows = await write(tx, patch.values);
7276
7768
  if (guarded && !rows.length) throw new NoParentRow();
7769
+ await runtime.checkParentSide(tx, tableName, ops, rows, patch);
7277
7770
  await runtime.applyChildSide(tx, tableName, ops, rows, request);
7278
7771
  return runtime.refreshed(tx, tableName, ops, rows);
7279
7772
  };
@@ -7335,7 +7828,8 @@ const updateWithNestedOps = async (params) => {
7335
7828
  ...remapValues(columns),
7336
7829
  ...patch
7337
7830
  }),
7338
- request
7831
+ request,
7832
+ existing: true
7339
7833
  }));
7340
7834
  };
7341
7835
  //#endregion
@@ -7488,6 +7982,7 @@ const createSchemaBuilder = (adapter) => {
7488
7982
  const anyTable = (feature) => tableEntries.some(([name]) => featureOf(name)[feature]);
7489
7983
  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?");
7490
7984
  registerScalarOverrides(tables, options);
7985
+ installSqliteBlobJsonDecoders(tables);
7491
7986
  registerEnumConfig(options);
7492
7987
  registerColumnExclusions(tables, options.exclude);
7493
7988
  resetAggregateColumnCache();
@@ -7719,7 +8214,7 @@ const createSchemaBuilder = (adapter) => {
7719
8214
  ...tableFeatures.update ? [updateInput] : [],
7720
8215
  ...built.updateManyInput ? [built.updateManyInput] : [],
7721
8216
  tableFilters,
7722
- tableOrder
8217
+ ...tableOrder ? [tableOrder] : []
7723
8218
  ].forEach((e) => {
7724
8219
  inputs[e.name] = e;
7725
8220
  });
@@ -7849,14 +8344,12 @@ const generateUpsert = (db, tableName, table, baseType, onConflictType, fieldNam
7849
8344
  withTarget: false,
7850
8345
  serverOwned: Object.keys(contextColumns ?? {})
7851
8346
  });
7852
- const requestedNothing = args.onConflict?.action === "NOTHING";
7853
8347
  const runBatch = async (target, { values, plan }) => {
7854
- if (plan.action === "NOTHING" && !requestedNothing) {
8348
+ if (plan.action === "NOTHING") {
7855
8349
  const columns = (0, drizzle_orm.getColumns)(table);
7856
8350
  const keyName = pkNames[0] ?? Object.keys(columns)[0];
7857
8351
  await target.insert(table).values(values).onDuplicateKeyUpdate({ set: { [keyName]: drizzle_orm.sql`${drizzle_orm.sql.identifier(columns[keyName].name)}` } });
7858
- } else if (plan.action === "NOTHING") await target.insert(table).ignore().values(values);
7859
- else await target.insert(table).values(values).onDuplicateKeyUpdate({ set: plan.set });
8352
+ } else await target.insert(table).values(values).onDuplicateKeyUpdate({ set: plan.set });
7860
8353
  };
7861
8354
  if (batches.length === 1) await runBatch(executor, batches[0]);
7862
8355
  else await executor.transaction(async (tx) => {
@@ -7896,7 +8389,7 @@ const generateUpdate = (db, tableName, table, setArgs, filterArgs, fieldName, si
7896
8389
  });
7897
8390
  if (single) await assertSingleMatch(executor, table, filters);
7898
8391
  let query = executor.update(table).set(input);
7899
- if (filters) query = query.where(filters);
8392
+ if (filters) query = query.where(mysqlWriteFilter(table, filters));
7900
8393
  await query;
7901
8394
  await after();
7902
8395
  return isSuccess;
@@ -7911,7 +8404,7 @@ const generateUpdate = (db, tableName, table, setArgs, filterArgs, fieldName, si
7911
8404
  * in order. MySQL has no RETURNING, so — like every other MySQL mutation — the result is
7912
8405
  * `{ isSuccess: true }` rather than the updated rows.
7913
8406
  */
7914
- const generateUpdateMany$2 = (db, tableName, table, updateManyInput, fieldName, filterCtx, txCtx, policies) => writeResolver({
8407
+ const generateUpdateMany$2 = (db, tableName, table, updateManyInput, fieldName, requireWhere, filterCtx, txCtx, policies) => writeResolver({
7915
8408
  db,
7916
8409
  tableName,
7917
8410
  operation: "updateMany",
@@ -7931,13 +8424,20 @@ const generateUpdateMany$2 = (db, tableName, table, updateManyInput, fieldName,
7931
8424
  if (!Object.keys(input).length) throw drizzleError("Unable to update with no values specified!", { code: "DRIZZLE_NO_VALUES" });
7932
8425
  return {
7933
8426
  set: input,
7934
- filters: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope)) : void 0)
8427
+ filters: scopedWhere({
8428
+ scope,
8429
+ tableName,
8430
+ table,
8431
+ where,
8432
+ relationCtx: relationFilterCtx(filterCtx, tableName, scope),
8433
+ required: requireWhere
8434
+ })
7935
8435
  };
7936
8436
  });
7937
8437
  await executor.transaction(async (tx) => {
7938
8438
  for (const entry of entries) {
7939
8439
  let query = tx.update(table).set(entry.set);
7940
- if (entry.filters) query = query.where(entry.filters);
8440
+ if (entry.filters) query = query.where(mysqlWriteFilter(table, entry.filters));
7941
8441
  await query;
7942
8442
  }
7943
8443
  });
@@ -7986,7 +8486,7 @@ const generateDelete = (db, tableName, table, filterArgs, fieldName, single, req
7986
8486
  });
7987
8487
  if (single) await assertSingleMatch(executor, table, filters);
7988
8488
  let query = softDelete && !hard ? executor.update(table).set({ [softDelete.columnName]: restore ? softDelete.writeRestored : softDelete.writeDeleted() }) : executor.delete(table);
7989
- if (filters) query = query.where(filters);
8489
+ if (filters) query = query.where(mysqlWriteFilter(table, filters));
7990
8490
  await query;
7991
8491
  await after();
7992
8492
  return isSuccess;
@@ -7997,6 +8497,34 @@ const generateDelete = (db, tableName, table, filterArgs, fieldName, single, req
7997
8497
  const getTableConfig$2 = memoizeTableConfig(drizzle_orm_mysql_core.getTableConfig);
7998
8498
  /** Primary-key property names for a MySQL table, including table-level composite keys. */
7999
8499
  const mysqlPrimaryKeyPropNames = (table) => getPrimaryKeyPropNamesFromConfig(table, getTableConfig$2);
8500
+ const TABLE_ORIGINAL_NAME = Symbol.for("drizzle:OriginalName");
8501
+ const TABLE_SCHEMA = Symbol.for("drizzle:Schema");
8502
+ /** Whether `chunk` reads `table` anywhere — under an alias, in a subquery, however deep. */
8503
+ const readsTable = (chunk, table) => {
8504
+ if (Array.isArray(chunk)) return chunk.some((part) => readsTable(part, table));
8505
+ if ((0, drizzle_orm.is)(chunk, drizzle_orm.Table)) return chunk[TABLE_ORIGINAL_NAME] === table[TABLE_ORIGINAL_NAME] && chunk[TABLE_SCHEMA] === table[TABLE_SCHEMA];
8506
+ if ((0, drizzle_orm.is)(chunk, drizzle_orm.SQL)) return readsTable(chunk.queryChunks, table);
8507
+ if ((0, drizzle_orm.is)(chunk, drizzle_orm.SQL.Aliased)) return readsTable(chunk.sql, table);
8508
+ if ((0, drizzle_orm.is)(chunk, drizzle_orm.Subquery)) return readsTable(chunk._.sql, table);
8509
+ return false;
8510
+ };
8511
+ /**
8512
+ * The `where` of an UPDATE or DELETE, in a form MySQL accepts. MySQL refuses a statement whose
8513
+ * `where` reads the table it writes (error 1093, "You can't specify target table … for update
8514
+ * in FROM clause") — which a relation filter through a self-referencing relation, or a scope
8515
+ * written as one, does. Such a filter is resolved to the primary keys it matches first, through
8516
+ * a derived table MySQL materializes (`DISTINCT` keeps the optimizer from merging it back in),
8517
+ * and the write matches those keys. Any other filter is returned as it is.
8518
+ */
8519
+ const mysqlWriteFilter = (table, filters) => {
8520
+ if (!filters || !readsTable(filters, table)) return filters;
8521
+ const columns = (0, drizzle_orm.getColumns)(table);
8522
+ const keys = mysqlPrimaryKeyPropNames(table).map((name) => columns[name]);
8523
+ if (!keys.length) return filters;
8524
+ const keyList = drizzle_orm.sql.join(keys.map((column) => drizzle_orm.sql.identifier(column.name)), drizzle_orm.sql`, `);
8525
+ const target = keys.length === 1 ? drizzle_orm.sql`${keys[0]}` : drizzle_orm.sql`(${drizzle_orm.sql.join(keys, drizzle_orm.sql`, `)})`;
8526
+ return drizzle_orm.sql`${target} in (select ${keyList} from (select distinct ${drizzle_orm.sql.join(keys, drizzle_orm.sql`, `)} from ${table} where ${filters}) as ${drizzle_orm.sql.identifier("dgql_write_target")})`;
8527
+ };
8000
8528
  const { prepareBuild, forEachTable, finalizeBuild } = createSchemaBuilder({
8001
8529
  tableClass: drizzle_orm_mysql_core.MySqlTable,
8002
8530
  getTableConfig: getTableConfig$2,
@@ -8050,7 +8578,7 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
8050
8578
  tableFilters,
8051
8579
  cacheCtx
8052
8580
  }) : void 0;
8053
- const updateManyGenerated = updateManyInput ? generateUpdateMany$2(db, tableName, schema[tableName], updateManyInput, updateManyFieldName, filterCtx, mutationTxCtx, policies) : void 0;
8581
+ const updateManyGenerated = updateManyInput ? generateUpdateMany$2(db, tableName, schema[tableName], updateManyInput, updateManyFieldName, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies) : void 0;
8054
8582
  const deleteGenerated = tableFeatures.delete ? generateDelete(db, tableName, schema[tableName], tableFilters, deleteFieldName, false, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies) : void 0;
8055
8583
  const deleteSingleGenerated = tableFeatures.delete ? generateDelete(db, tableName, schema[tableName], tableFilters, deleteSingleFieldName, true, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies) : void 0;
8056
8584
  const restoreGenerated = softDeleteInfo ? generateDelete(db, tableName, schema[tableName], tableFilters, restoreFieldName, false, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies, true) : void 0;
@@ -8067,7 +8595,8 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
8067
8595
  filterCtx,
8068
8596
  txCtx: mutationTxCtx,
8069
8597
  policies,
8070
- returnsRows: false
8598
+ returnsRows: false,
8599
+ writeFilter: (filters) => mysqlWriteFilter(schema[tableName], filters)
8071
8600
  }) : void 0;
8072
8601
  const deleteCountGenerated = tableFeatures.delete && tableFeatures.countMutations ? generateWriteCount({
8073
8602
  db,
@@ -8080,7 +8609,8 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
8080
8609
  filterCtx,
8081
8610
  txCtx: mutationTxCtx,
8082
8611
  policies,
8083
- returnsRows: false
8612
+ returnsRows: false,
8613
+ writeFilter: (filters) => mysqlWriteFilter(schema[tableName], filters)
8084
8614
  }) : void 0;
8085
8615
  const hardDeleteMeta = softDeleteInfo?.hardDelete ? { hardDelete: true } : {};
8086
8616
  const withMeta = (generated, meta) => ({
@@ -8192,7 +8722,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8192
8722
  * one-element list.
8193
8723
  */
8194
8724
  const generateInsert = (build, opts) => {
8195
- const { db, tables, relationMap, typeNameMapper, txCtx, nested, limits, policies, resolveName } = build;
8725
+ const { db, tables, relationMap, typeNameMapper, filterCtx, txCtx, nested, limits, policies, resolveName } = build;
8196
8726
  const { tableName, table, baseType, fieldName, typeName, single, conflictDoNothing = false } = opts;
8197
8727
  const queryArgs = { values: { type: single ? new graphql.GraphQLNonNull(baseType) : new graphql.GraphQLNonNull(new graphql.GraphQLList(new graphql.GraphQLNonNull(baseType))) } };
8198
8728
  const pkNames = primaryKeyPropNames(table);
@@ -8216,6 +8746,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8216
8746
  const input = nestedEntries ? [] : applyContextValuesAll(remapFromGraphQLArrayInput(entries ? entries.map((entry) => entry.columns) : supplied, table), contextColumns, context);
8217
8747
  const { columns, hasRelations, withParams } = mutationSelection(info, {
8218
8748
  relationMap,
8749
+ filterCtx,
8219
8750
  tables,
8220
8751
  tableName,
8221
8752
  typeName,
@@ -8294,6 +8825,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8294
8825
  const input = nestedEntries ? [] : applyContextValuesAll(remapFromGraphQLArrayInput(entries ? entries.map((entry) => entry.columns) : supplied, table), contextColumns, context);
8295
8826
  const { columns, hasRelations, withParams } = mutationSelection(info, {
8296
8827
  relationMap,
8828
+ filterCtx,
8297
8829
  tables,
8298
8830
  tableName,
8299
8831
  typeName,
@@ -8318,24 +8850,25 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8318
8850
  serverOwned: Object.keys(contextColumns ?? {}),
8319
8851
  buildWhere: (where) => extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope))
8320
8852
  });
8321
- const runBatch = async (executor, { values: rows, plan }) => {
8322
- let query = executor.insert(table).values(rows).returning(returning);
8853
+ const batchQuery = (executor, { values: rows, plan }) => {
8854
+ const query = executor.insert(table).values(rows).returning(returning);
8323
8855
  const setWhere = withScope(scope, tableName, table, plan.setWhere);
8324
- query = plan.action === "NOTHING" ? query.onConflictDoNothing(plan.target ? {
8325
- target: plan.target,
8326
- where: plan.targetWhere
8327
- } : void 0) : query.onConflictDoUpdate({
8328
- target: plan.target,
8329
- targetWhere: plan.targetWhere,
8330
- set: plan.set,
8331
- setWhere
8332
- });
8333
- return await query;
8856
+ return applyConflictPlan(query, table, plan, setWhere);
8334
8857
  };
8335
- if (batches.length === 1) return runBatch(target, batches[0]);
8336
- return target.transaction(async (tx) => {
8337
- const rows = [];
8338
- for (const batch of batches) rows.push(...await runBatch(tx, batch));
8858
+ if (batches.length === 1) return await batchQuery(target, batches[0]);
8859
+ return target.transaction((tx) => {
8860
+ const execute = (batch) => {
8861
+ const query = batchQuery(tx, batch);
8862
+ return typeof query.all === "function" ? query.all() : query;
8863
+ };
8864
+ const first = execute(batches[0]);
8865
+ if (typeof first?.then === "function") return (async () => {
8866
+ const rows = [...await first];
8867
+ for (const batch of batches.slice(1)) rows.push(...await execute(batch));
8868
+ return rows;
8869
+ })();
8870
+ const rows = [...first];
8871
+ for (const batch of batches.slice(1)) rows.push(...execute(batch));
8339
8872
  return rows;
8340
8873
  });
8341
8874
  };
@@ -8383,6 +8916,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8383
8916
  await before();
8384
8917
  const { columns, hasRelations, withParams } = mutationSelection(info, {
8385
8918
  relationMap,
8919
+ filterCtx,
8386
8920
  tables,
8387
8921
  tableName,
8388
8922
  typeName,
@@ -8621,7 +9155,8 @@ const createSchemaDataGenerator = (adapter) => {
8621
9155
  table: schema[tableName],
8622
9156
  updateManyInput,
8623
9157
  fieldName: updateManyFieldName,
8624
- typeName
9158
+ typeName,
9159
+ requireWhere: tableFeatures.requireWhere
8625
9160
  }) : void 0;
8626
9161
  const deleteGenerated = tableFeatures.delete ? generateDelete(writeBuild, {
8627
9162
  tableName,
@@ -8876,7 +9411,8 @@ const runUpdateManyBatch = ({ executor, entries, table, returning, nested, table
8876
9411
  tableName,
8877
9412
  ops: entry.ops,
8878
9413
  write,
8879
- request
9414
+ request,
9415
+ existing: true
8880
9416
  }) : await write(tx, {}));
8881
9417
  }
8882
9418
  return results;
@@ -8897,7 +9433,7 @@ const runUpdateManyBatch = ({ executor, entries, table, returning, nested, table
8897
9433
  const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateManyBatch) => {
8898
9434
  return (build, opts) => {
8899
9435
  const { db, tables, relationMap, typeNameMapper, filterCtx, txCtx, nested, limits, policies, resolveName } = build;
8900
- const { tableName, table, updateManyInput, fieldName, typeName } = opts;
9436
+ const { tableName, table, updateManyInput, fieldName, typeName, requireWhere } = opts;
8901
9437
  const queryArgs = { updates: { type: new graphql.GraphQLNonNull(new graphql.GraphQLList(new graphql.GraphQLNonNull(updateManyInput))) } };
8902
9438
  const pkNames = primaryKeyPropNames(table);
8903
9439
  return writeResolver({
@@ -8917,6 +9453,7 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
8917
9453
  const contextColumns = policies?.contextValues?.(tableName);
8918
9454
  const { columns, hasRelations, withParams } = mutationSelection(info, {
8919
9455
  relationMap,
9456
+ filterCtx,
8920
9457
  tables,
8921
9458
  tableName,
8922
9459
  typeName,
@@ -8936,7 +9473,14 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
8936
9473
  return {
8937
9474
  set: input,
8938
9475
  ops,
8939
- filters: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope)) : void 0)
9476
+ filters: scopedWhere({
9477
+ scope,
9478
+ tableName,
9479
+ table,
9480
+ where,
9481
+ relationCtx: relationFilterCtx(filterCtx, tableName, scope),
9482
+ required: requireWhere
9483
+ })
8940
9484
  };
8941
9485
  });
8942
9486
  const anyNested = entries.some((entry) => entry.ops);
@@ -9025,7 +9569,8 @@ const runSqliteUpdateManyBatch = ({ executor, entries, table, returning, anyNest
9025
9569
  tableName,
9026
9570
  ops: entry.ops,
9027
9571
  write,
9028
- request
9572
+ request,
9573
+ existing: true
9029
9574
  }) : write(tx, {});
9030
9575
  };
9031
9576
  return executor.transaction((tx) => {
@@ -9055,6 +9600,7 @@ const sqliteSchemaData = createSchemaDataGenerator({
9055
9600
  preflight: (db, options) => {
9056
9601
  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.");
9057
9602
  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.");
9603
+ 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.");
9058
9604
  },
9059
9605
  generateUpdateMany
9060
9606
  });