@vantreeseba/drizzle-graphql 15.1.6 → 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,14 +1308,14 @@ 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;
@@ -1305,10 +1366,6 @@ const remapToGraphQLSingleOutput = (queryOutput, tableName, table, relationMap)
1305
1366
  continue;
1306
1367
  }
1307
1368
  const column = columns[key];
1308
- if (value === 0n && column && column.columnType === "SQLiteBigInt" && !column.notNull) {
1309
- delete queryOutput[key];
1310
- continue;
1311
- }
1312
1369
  const remapped = remapToGraphQLCore(key, value, tableName, column, relationMap);
1313
1370
  if (column && remapped !== value) keepRawColumnValue(queryOutput, key, value);
1314
1371
  queryOutput[key] = remapped;
@@ -1328,6 +1385,7 @@ const remapFromGraphQLCore = (value, column, columnName) => {
1328
1385
  const dataType = column.dataType ?? "";
1329
1386
  const columnType = column.columnType ?? "";
1330
1387
  if (isDateTimeColumn(column)) {
1388
+ if (Array.isArray(value)) return value.map((element) => remapFromGraphQLCore(element, column, columnName));
1331
1389
  const isMySql = columnType.startsWith("MySql");
1332
1390
  const isPgString = isPgTimestampStringColumn(column);
1333
1391
  const toIso = isMySql ? mySqlDateTimeTextToIso : isPgString ? pgTimestampTextToIso : void 0;
@@ -1342,41 +1400,29 @@ const remapFromGraphQLCore = (value, column, columnName) => {
1342
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" });
1343
1401
  return day;
1344
1402
  }
1345
- if (dataType.includes("bigint")) try {
1346
- return BigInt(value);
1347
- } catch {
1348
- throw drizzleError(`Field '${columnName}' is not a BigInt!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1349
- }
1350
- if (dataType.includes("json") && column.columnType !== "PgGeometryObject") return value;
1351
- switch (dataType) {
1352
- case "date": {
1353
- const formatted = new Date(value);
1354
- if (Number.isNaN(formatted.getTime())) throw drizzleError(`Field '${columnName}' is not a valid date!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1355
- return formatted;
1356
- }
1357
- case "buffer":
1358
- if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1359
- return Buffer.from(value);
1360
- case "json":
1361
- if (column.columnType === "PgGeometryObject") return value;
1362
- try {
1363
- return JSON.parse(value);
1364
- } catch (e) {
1365
- throw drizzleError(`Invalid JSON in field '${columnName}':\n${e instanceof Error ? e.message : "Unknown error"}`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1366
- }
1367
- case "array":
1368
- if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1369
- 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" });
1370
- return value;
1371
- case "bigint": try {
1403
+ if (dataType.includes("bigint")) {
1404
+ if (Array.isArray(value)) return value.map((element) => remapFromGraphQLCore(element, column, columnName));
1405
+ try {
1372
1406
  return BigInt(value);
1373
- } catch (_error) {
1407
+ } catch {
1374
1408
  throw drizzleError(`Field '${columnName}' is not a BigInt!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1375
1409
  }
1376
- default:
1377
- if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === null) return Object.assign({}, value);
1378
- return value;
1379
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;
1380
1426
  };
1381
1427
  const remapFromGraphQLSingleInput = (queryInput, table, operation = "insert") => {
1382
1428
  const columns = (0, drizzle_orm.getColumns)(table);
@@ -1418,36 +1464,60 @@ const escapeLikeValue = (value) => value.replace(/[\\%_]/g, (char) => `\\${char}
1418
1464
  const safeLikeOps = {
1419
1465
  startsWith: {
1420
1466
  buildPattern: (value) => `${escapeLikeValue(value)}%`,
1467
+ buildGlob: (value) => `${escapeGlobValue(value)}*`,
1421
1468
  insensitive: false
1422
1469
  },
1423
1470
  endsWith: {
1424
1471
  buildPattern: (value) => `%${escapeLikeValue(value)}`,
1472
+ buildGlob: (value) => `*${escapeGlobValue(value)}`,
1425
1473
  insensitive: false
1426
1474
  },
1427
1475
  contains: {
1428
1476
  buildPattern: (value) => `%${escapeLikeValue(value)}%`,
1477
+ buildGlob: (value) => `*${escapeGlobValue(value)}*`,
1429
1478
  insensitive: false
1430
1479
  },
1431
1480
  iStartsWith: {
1432
1481
  buildPattern: (value) => `${escapeLikeValue(value)}%`,
1482
+ buildGlob: (value) => `${escapeGlobValue(value)}*`,
1433
1483
  insensitive: true
1434
1484
  },
1435
1485
  iEndsWith: {
1436
1486
  buildPattern: (value) => `%${escapeLikeValue(value)}`,
1487
+ buildGlob: (value) => `*${escapeGlobValue(value)}`,
1437
1488
  insensitive: true
1438
1489
  },
1439
1490
  iContains: {
1440
1491
  buildPattern: (value) => `%${escapeLikeValue(value)}%`,
1492
+ buildGlob: (value) => `*${escapeGlobValue(value)}*`,
1441
1493
  insensitive: true
1442
1494
  }
1443
1495
  };
1444
1496
  /**
1445
- * `LIKE <pattern> ESCAPE '\'` for a safe string operator. Case-insensitive variants use
1446
- * Postgres's native `ILIKE`; MySQL and SQLite have no `ILIKE`, so they compare `lower()`
1447
- * 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.
1500
+ */
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).
1448
1517
  */
1449
- const safeLikeCondition = (column, pattern, insensitive) => {
1450
- if (!insensitive) return drizzle_orm.sql`${column} like ${pattern} escape ${LIKE_ESCAPE_CHAR}`;
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}`;
1451
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}`;
1452
1522
  };
1453
1523
  /**
@@ -1503,6 +1573,35 @@ const jsonContains = (column, columnName, value) => {
1503
1573
  }
1504
1574
  };
1505
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
+ /**
1506
1605
  * Constant predicates, used where an operator's operand list is empty and the answer is
1507
1606
  * therefore known without touching the column. Written as `1 = 0` / `1 = 1` rather than the
1508
1607
  * `FALSE` / `TRUE` keywords so they compile identically on all three dialects. Built fresh
@@ -1537,7 +1636,8 @@ const NUMERIC_TEXT_PATTERN = "^\\s*-?(\\d+\\.?\\d*|\\.\\d+)([eE][-+]?\\d+)?\\s*$
1537
1636
  * pattern guard.
1538
1637
  * - **SQLite** — `json_extract` returns the value in SQLite's own type, so the reads cast
1539
1638
  * explicitly: without that, SQLite's cross-type ordering puts every string above every
1540
- * 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.
1541
1641
  */
1542
1642
  const jsonPathExprs = (column, path) => {
1543
1643
  switch (columnDialect(column)) {
@@ -1563,10 +1663,11 @@ const jsonPathExprs = (column, path) => {
1563
1663
  }
1564
1664
  default: {
1565
1665
  const extracted = drizzle_orm.sql`json_extract(${column}, ${jsonPathString(path)})`;
1666
+ const type = drizzle_orm.sql`json_type(${column}, ${jsonPathString(path)})`;
1566
1667
  return {
1567
- text: drizzle_orm.sql`cast(${extracted} as text)`,
1568
- number: drizzle_orm.sql`(case when typeof(${extracted}) in ('integer', 'real') then ${extracted} end)`,
1569
- 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)`,
1570
1671
  encodeBoolean: (value) => value ? 1 : 0
1571
1672
  };
1572
1673
  }
@@ -1605,9 +1706,9 @@ const jsonPathCondition = (column, columnName, filter) => {
1605
1706
  continue;
1606
1707
  }
1607
1708
  if (operatorName in safeLikeOps) {
1608
- const { buildPattern, insensitive } = safeLikeOps[operatorName];
1709
+ const op = safeLikeOps[operatorName];
1609
1710
  if (typeof operatorValue !== "string") throw drizzleError(`WHERE ${locator}: operator '${operatorName}' takes a string`, { code: "DRIZZLE_INVALID_FILTER" });
1610
- variants.push(safeLikeCondition(exprs.text, buildPattern(operatorValue), insensitive));
1711
+ variants.push(safeLikeCondition(exprs.text, operatorValue, op, op.insensitive, column));
1611
1712
  continue;
1612
1713
  }
1613
1714
  const comparison = JSON_PATH_COMPARISONS[operatorName];
@@ -1665,20 +1766,25 @@ const extractFiltersColumn = (column, columnName, operators) => {
1665
1766
  for (const [operatorName, operatorValue] of entries) {
1666
1767
  if (operatorValue === null || operatorValue === void 0) continue;
1667
1768
  if (operatorValue === false && FLAG_OPERATORS.has(operatorName)) continue;
1668
- 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) {
1669
1771
  const singleValue = remapFromGraphQLCore(operatorValue, column, columnName);
1670
1772
  variants.push(foldCase ? lowerComparison(column, operatorName, singleValue) : singleValueOps[operatorName](column, singleValue));
1671
1773
  } else if (operatorName in stringValueOps) {
1672
- const insensitivePattern = foldCase && (operatorName === "like" || operatorName === "notLike");
1673
- 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));
1674
1780
  } else if (operatorName === "path" && isJsonColumn(column)) for (const pathFilter of operatorValue) {
1675
1781
  const extracted = jsonPathCondition(column, columnName, pathFilter);
1676
1782
  if (extracted) variants.push(extracted);
1677
1783
  }
1678
1784
  else if (operatorName === "contains" && isJsonColumn(column)) variants.push(jsonContains(column, columnName, operatorValue));
1679
1785
  else if (operatorName in safeLikeOps) {
1680
- const { buildPattern, insensitive: alwaysInsensitive } = safeLikeOps[operatorName];
1681
- variants.push(safeLikeCondition(column, buildPattern(operatorValue), alwaysInsensitive || foldCase));
1786
+ const op = safeLikeOps[operatorName];
1787
+ variants.push(safeLikeCondition(column, operatorValue, op, op.insensitive || foldCase));
1682
1788
  } else if (operatorName in arrayValueOps) {
1683
1789
  if (!operatorValue.length) {
1684
1790
  variants.push(operatorName === "inArray" ? sqlFalse() : sqlTrue());
@@ -1686,13 +1792,13 @@ const extractFiltersColumn = (column, columnName, operators) => {
1686
1792
  }
1687
1793
  const arrayValue = operatorValue.map((val) => remapFromGraphQLCore(val, column, columnName));
1688
1794
  variants.push(foldCase ? lowerMembership(column, arrayValue, operatorName === "notInArray") : arrayValueOps[operatorName](column, arrayValue));
1689
- } 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)]));
1690
1796
  else if (operatorName in arrayMembershipOps) {
1691
1797
  if (!operatorValue.length) {
1692
1798
  variants.push(operatorName === "hasSome" ? sqlFalse() : sqlTrue());
1693
1799
  continue;
1694
1800
  }
1695
- variants.push(arrayMembershipOps[operatorName](column, operatorValue));
1801
+ variants.push(arrayMembershipOps[operatorName](column, operatorValue.map((element) => remapFromGraphQLCore(element, column, columnName))));
1696
1802
  } else if (operatorName === "isEmpty") variants.push(drizzle_orm.sql`cardinality(${column}) = 0`);
1697
1803
  else if (operatorName in nullableOps) variants.push(nullableOps[operatorName](column));
1698
1804
  else throw drizzleError(`WHERE ${columnName}: Unknown operator: ${operatorName}`, { code: "DRIZZLE_INVALID_FILTER" });
@@ -2540,6 +2646,32 @@ const withScope = (scope, tableName, table, condition, mode) => {
2540
2646
  const predicate = scope?.on(tableName, table, mode);
2541
2647
  return predicate ? (0, drizzle_orm.and)(condition, predicate) : condition;
2542
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;
2543
2675
  /** Binds a build's {@link TablePolicies} to its relation context, once, at schema-build time. */
2544
2676
  const bindPolicies = (policies, filterCtx) => policies?.scope || policies?.contextValues || policies?.softDelete || policies?.onWrite || policies?.defaultOrderBy ? {
2545
2677
  scope: policies.scope || policies.softDelete ? (context) => resolveScope(policies, context, filterCtx) : void 0,
@@ -2590,7 +2722,7 @@ const deletedArg = (softDelete, tableName, resolveName) => softDelete?.(tableNam
2590
2722
  const selectArrayArgs = (orderArgs, filterArgs, distinctEnum, softDelete, tableName, resolveName) => ({
2591
2723
  offset: { type: graphql.GraphQLInt },
2592
2724
  limit: { type: graphql.GraphQLInt },
2593
- orderBy: { type: orderArgs },
2725
+ ...orderArgs ? { orderBy: { type: orderArgs } } : {},
2594
2726
  where: { type: filterArgs },
2595
2727
  after: {
2596
2728
  type: graphql.GraphQLString,
@@ -2602,7 +2734,7 @@ const selectArrayArgs = (orderArgs, filterArgs, distinctEnum, softDelete, tableN
2602
2734
  /** GraphQL argument map for a single-row select field (no `limit`). */
2603
2735
  const selectSingleArgs = (orderArgs, filterArgs, softDelete, tableName, resolveName) => ({
2604
2736
  offset: { type: graphql.GraphQLInt },
2605
- orderBy: { type: orderArgs },
2737
+ ...orderArgs ? { orderBy: { type: orderArgs } } : {},
2606
2738
  where: { type: filterArgs },
2607
2739
  ...deletedArg(softDelete, tableName, resolveName)
2608
2740
  });
@@ -2652,7 +2784,7 @@ const generateDistinctEnum = (table, typeName, resolveName, tableKey) => {
2652
2784
  defaultName,
2653
2785
  table: tableKey,
2654
2786
  operation: "distinct"
2655
- }) ?? 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));
2656
2788
  };
2657
2789
  //#endregion
2658
2790
  //#region src/util/builders/common/conflict.ts
@@ -2822,6 +2954,44 @@ const resolveConflictPlan = (params) => {
2822
2954
  plan
2823
2955
  }));
2824
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
+ };
2825
2995
  /** `excluded.<column>` — PostgreSQL and SQLite name the rejected row this way. */
2826
2996
  const excludedColumnRef = (columnName) => drizzle_orm.sql`excluded.${drizzle_orm.sql.identifier(columnName)}`;
2827
2997
  /** `values(<column>)` — MySQL's equivalent inside ON DUPLICATE KEY UPDATE. */
@@ -3103,6 +3273,21 @@ const decodeCursor = (after, entries) => {
3103
3273
  return payload.v.map(decodeCursorValue);
3104
3274
  };
3105
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
+ /**
3106
3291
  * The keyset predicate selecting rows strictly after the cursor position in the given total
3107
3292
  * order — the expanded lexicographic form
3108
3293
  * `after(k0) OR (k0 = v0 AND after(k1)) OR (k0 = v0 AND k1 = v1 AND after(k2)) …`,
@@ -3127,7 +3312,7 @@ const buildCursorCondition = (table, entries, values, nullOrdering) => {
3127
3312
  let strictlyAfter;
3128
3313
  if (value === null || value === void 0) strictlyAfter = nullsSortLast ? void 0 : (0, drizzle_orm.isNotNull)(col);
3129
3314
  else {
3130
- const comparison = direction === "asc" ? (0, drizzle_orm.gt)(col, value) : (0, drizzle_orm.lt)(col, value);
3315
+ const comparison = strictComparison(col, direction, value);
3131
3316
  strictlyAfter = nullsSortLast ? (0, drizzle_orm.or)(comparison, (0, drizzle_orm.isNull)(col)) : comparison;
3132
3317
  }
3133
3318
  if (strictlyAfter) disjuncts.push(equalities.length ? (0, drizzle_orm.and)(...equalities, strictlyAfter) : strictlyAfter);
@@ -3330,6 +3515,115 @@ const getUniqueTargets = (table, getTableConfig) => {
3330
3515
  */
3331
3516
  const getUniqueColumnSets = (table, getTableConfig, options = {}) => getUniqueTargets(table, getTableConfig).filter((target) => options.partial || !target.where).map((target) => target.columns);
3332
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
3333
3627
  //#region src/util/builders/common/distinct.ts
3334
3628
  /**
3335
3629
  * Alias the parent key comes back under when the pass runs through a junction table. See
@@ -3377,13 +3671,15 @@ const selectDistinctKeys = async (params) => {
3377
3671
  ...junction ? { [DISTINCT_PARENT]: sub[DISTINCT_PARENT] } : {}
3378
3672
  }).from(sub).where((0, drizzle_orm.eq)(sub[DISTINCT_RN], 1)).orderBy(...outerOrder);
3379
3673
  if (offset) query = query.offset(offset);
3380
- 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);
3381
3676
  return await query;
3382
3677
  };
3383
3678
  /**
3384
3679
  * Condition matching exactly the rows identified by `keys` — an `IN (…)` for a single-column
3385
- * primary key, an `OR` of per-row equality for a composite one. `table` may be the aliased
3386
- * 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.
3387
3683
  */
3388
3684
  const primaryKeyRestriction = (table, pkNames, keys) => {
3389
3685
  const cols = (0, drizzle_orm.getColumns)(table);
@@ -3391,7 +3687,7 @@ const primaryKeyRestriction = (table, pkNames, keys) => {
3391
3687
  const name = pkNames[0];
3392
3688
  return (0, drizzle_orm.inArray)(cols[name], keys.map((key) => key[name]));
3393
3689
  }
3394
- 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])))));
3395
3691
  };
3396
3692
  //#endregion
3397
3693
  //#region src/util/builders/common/docs.ts
@@ -3537,99 +3833,6 @@ const innerOrderType = (typeName) => sharedType(typeName, {
3537
3833
  }
3538
3834
  }));
3539
3835
  //#endregion
3540
- //#region src/util/builders/common/limits.ts
3541
- /**
3542
- * Substitutes a table's default ordering for an absent `orderBy`.
3543
- *
3544
- * Applied where the arguments are first read, so everything downstream — the cursor tuple, a
3545
- * `distinct` pass, the plain-select fallback — sees one effective ordering and cannot
3546
- * disagree about it. Only a missing argument is replaced: `orderBy: {}` is a request for no
3547
- * ordering, and stays one.
3548
- */
3549
- const withDefaultOrderBy = (args, tableName, defaults) => {
3550
- if (!defaults || args?.orderBy != null) return args;
3551
- const fallback = defaults(tableName);
3552
- return fallback ? {
3553
- ...args,
3554
- orderBy: fallback
3555
- } : args;
3556
- };
3557
- /**
3558
- * Rejects a negative `limit` or `offset`. Neither is a page size or a position, and the
3559
- * dialects disagree on what one means: SQLite reads `LIMIT -1` as "no limit" — which would walk
3560
- * straight past `maxLimit` — while PostgreSQL and MySQL fail with a raw database error. Applied
3561
- * whether or not the table has a limit policy.
3562
- */
3563
- const assertPagingNotNegative = (paging, errorCtx) => {
3564
- for (const name of ["limit", "offset"]) {
3565
- const value = paging[name];
3566
- if (value != null && value < 0) throw drizzleError(`'${name}' must not be negative, got ${value}.`, {
3567
- code: "DRIZZLE_INVALID_INPUT_VALUE",
3568
- ...errorCtx
3569
- });
3570
- }
3571
- };
3572
- /**
3573
- * The `limit` a query actually runs with, given the policy for the table it reads.
3574
- *
3575
- * A request that passes no `limit` falls back to `defaultLimit`, or to `maxLimit` when only
3576
- * that is configured — an absent limit means every row, which is above any maximum. A limit
3577
- * the client did ask for is rejected when it exceeds `maxLimit`, since silently truncating a
3578
- * page tells a paginating client it has reached the end when it has not; `clampToMax` opts
3579
- * into the truncation instead. A limit the *policy* supplied is capped rather than rejected —
3580
- * a misconfigured `defaultLimit` above `maxLimit` is the operator's problem, not something to
3581
- * fail a client's query over.
3582
- */
3583
- const applyLimitPolicy = (limit, policy, errorCtx) => {
3584
- assertPagingNotNegative({ limit }, errorCtx);
3585
- if (!policy) return limit ?? void 0;
3586
- const { defaultLimit, maxLimit, clampToMax } = policy;
3587
- if (limit == null) {
3588
- const fallback = defaultLimit ?? maxLimit;
3589
- if (fallback === void 0) return;
3590
- return maxLimit === void 0 ? fallback : Math.min(fallback, maxLimit);
3591
- }
3592
- if (maxLimit !== void 0 && limit > maxLimit) {
3593
- if (clampToMax) return maxLimit;
3594
- throw drizzleError(`'limit' of ${limit} exceeds the maximum of ${maxLimit}.`, {
3595
- code: "DRIZZLE_LIMIT_EXCEEDED",
3596
- ...errorCtx
3597
- });
3598
- }
3599
- return limit;
3600
- };
3601
- /** Merges a per-table override over the global policy, or `undefined` when neither bounds anything. */
3602
- const resolveLimitPolicy = (global, table) => {
3603
- const defaultLimit = table?.defaultLimit ?? global?.defaultLimit;
3604
- const maxLimit = table?.maxLimit ?? global?.maxLimit;
3605
- if (defaultLimit === void 0 && maxLimit === void 0) return;
3606
- return {
3607
- defaultLimit,
3608
- maxLimit,
3609
- clampToMax: table?.clampToMax ?? global?.clampToMax ?? false
3610
- };
3611
- };
3612
- /**
3613
- * A paginated field costs its page size times whatever one row of it costs, so `users(limit: 100)
3614
- * { posts(limit: 10) { id } }` is charged for the thousand rows it can return rather than the two
3615
- * fields it mentions. `childComplexity` floors at 1 so a row is never free.
3616
- *
3617
- * The page size is the *effective* one: a request without `limit` is priced at the policy's
3618
- * `defaultLimit` rather than the estimator's guess, and no request is priced above `maxLimit`,
3619
- * so the cost a query is charged matches the rows it can actually pull.
3620
- */
3621
- const listFieldComplexity = (options, policy) => ({ args, childComplexity }) => {
3622
- const limit = args["limit"];
3623
- let rows = (typeof limit === "number" && limit > 0 ? limit : void 0) ?? policy?.defaultLimit ?? options.defaultListSize;
3624
- if (policy?.maxLimit !== void 0) rows = Math.min(rows, policy.maxLimit);
3625
- return rows * Math.max(childComplexity, 1);
3626
- };
3627
- /**
3628
- * An aggregate returns a single row but reads however many match, so its cost tracks the scan
3629
- * rather than the response.
3630
- */
3631
- const aggregateFieldComplexity = (options) => ({ childComplexity }) => options.aggregateCost + childComplexity;
3632
- //#endregion
3633
3836
  //#region src/util/parse-resolve-info.ts
3634
3837
  /** Unwraps `[T]!`-style wrappers down to the named type. `getNamedType` without the realm tie. */
3635
3838
  const namedTypeOf = (type) => {
@@ -3793,14 +3996,10 @@ const NUMERIC_SCALARS = /* @__PURE__ */ new Set([
3793
3996
  ]);
3794
3997
  /**
3795
3998
  * Column types that convert to a GraphQL list without being a list *in the database* —
3796
- * a bytea reads as `[Int]`, a vector and a geometry point as `[Float]` — so appending to
3797
- * 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.
3798
4001
  */
3799
- const NON_LIST_ARRAY_COLUMNS = /* @__PURE__ */ new Set([
3800
- "PgVector",
3801
- "PgGeometry",
3802
- "PgBytea"
3803
- ]);
4002
+ const isNonListArrayColumn = (column) => isFloatTupleColumn(column) || isBufferColumn(column);
3804
4003
  const kindCache = /* @__PURE__ */ new WeakMap();
3805
4004
  /**
3806
4005
  * Whether a column takes an operations input, and which one.
@@ -3817,7 +4016,7 @@ const fieldUpdateKind = (column, columnName, tableName) => {
3817
4016
  if (!getColumnScalarOverride(column, true)) {
3818
4017
  const { type } = drizzleColumnToGraphQLType(column, columnName, tableName, true, false, true);
3819
4018
  if (NUMERIC_SCALARS.has(type)) kind = "numeric";
3820
- 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";
3821
4020
  }
3822
4021
  kindCache.set(column, kind);
3823
4022
  return kind;
@@ -4088,6 +4287,16 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
4088
4287
  if (!relFieldSelection) continue;
4089
4288
  const eagerArgs = field.args;
4090
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
+ }
4091
4300
  const columns = extractSelectedColumnsFromTree(relFieldSelection, tables[targetTableName], {
4092
4301
  tableName: targetTableName,
4093
4302
  relationMap,
@@ -4113,9 +4322,11 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
4113
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;
4114
4323
  const hasPagination = offset != null || limit != null;
4115
4324
  const pkNames = targetPkNames ?? [];
4116
- 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;
4117
4328
  thisRecord.offset = offset;
4118
- thisRecord.limit = limit;
4329
+ thisRecord.limit = offsetOnlyLimit(tables[targetTableName], limit, offset);
4119
4330
  thisRecord.with = extractRelationsParams(relationMap, tables, targetTableName, field, relTypeName, options);
4120
4331
  args[relName] = omitUndefinedKeys(thisRecord);
4121
4332
  }
@@ -4206,7 +4417,7 @@ const runRelationalSelect = async (opts) => {
4206
4417
  const preciseProps = Object.keys(cursorPreciseExtras(table, cursorEntries));
4207
4418
  if (preciseProps.length) params.extras = Object.fromEntries(preciseProps.map((prop) => [prop, (aliasedTable) => cursorPreciseExtras(aliasedTable, cursorEntries)[prop]]));
4208
4419
  }
4209
- params.limit = distinctKeys ? void 0 : opts.limit;
4420
+ params.limit = distinctKeys ? void 0 : offsetOnlyLimit(table, opts.limit, offset);
4210
4421
  const result = await queryBase.findMany(omitUndefinedKeys(params));
4211
4422
  if (cursorEntries && cursorSelected) attachRowCursors(result, cursorEntries);
4212
4423
  return remapToGraphQLArrayOutput(result, tableName, table, relationMap);
@@ -4263,6 +4474,8 @@ const eagerLoadMutationRelations = async (db, tableName, rows, pkNames, withPara
4263
4474
  }
4264
4475
  return rows;
4265
4476
  };
4477
+ /** The longest delay `setTimeout` honours; anything above, Infinity included, fires after 1 ms. */
4478
+ const MAX_TIMER_MS = 2 ** 31 - 1;
4266
4479
  const createMutationTxCtx = (options) => options ? {
4267
4480
  timeoutMs: options.timeoutMs,
4268
4481
  fieldNames: /* @__PURE__ */ new Set(),
@@ -4289,28 +4502,28 @@ const includedByDirectives = (node, variables) => {
4289
4502
  * this build did not generate — its completion cannot be tracked, so the caller must not
4290
4503
  * open a transaction it might never be able to close.
4291
4504
  */
4292
- const collectRootMutationKeys = (selectionSet, fragments, variables, known, keys, visitedFragments) => {
4505
+ const collectRootMutationKeys = (selectionSet, fragments, variables, known, keys, visitedFragments, argumentsCoerce) => {
4293
4506
  for (const selection of selectionSet.selections) {
4294
4507
  if (!includedByDirectives(selection, variables)) continue;
4295
4508
  if (selection.kind === "Field") {
4296
4509
  const name = selection.name.value;
4297
4510
  if (name === "__typename") continue;
4298
4511
  if (!known.has(name)) return false;
4299
- keys.add(selection.alias?.value ?? name);
4512
+ if (argumentsCoerce(selection)) keys.add(selection.alias?.value ?? name);
4300
4513
  } else if (selection.kind === "InlineFragment") {
4301
- if (!collectRootMutationKeys(selection.selectionSet, fragments, variables, known, keys, visitedFragments)) return false;
4514
+ if (!collectRootMutationKeys(selection.selectionSet, fragments, variables, known, keys, visitedFragments, argumentsCoerce)) return false;
4302
4515
  } else if (selection.kind === "FragmentSpread") {
4303
4516
  const fragmentName = selection.name.value;
4304
4517
  if (visitedFragments.has(fragmentName)) continue;
4305
4518
  visitedFragments.add(fragmentName);
4306
4519
  const fragment = fragments?.[fragmentName];
4307
4520
  if (!fragment) return false;
4308
- if (!collectRootMutationKeys(fragment.selectionSet, fragments, variables, known, keys, visitedFragments)) return false;
4521
+ if (!collectRootMutationKeys(fragment.selectionSet, fragments, variables, known, keys, visitedFragments, argumentsCoerce)) return false;
4309
4522
  }
4310
4523
  }
4311
4524
  return true;
4312
4525
  };
4313
- 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" });
4314
4527
  /** Opens the request-wide transaction and wires up latch, inactivity timer and cleanup. */
4315
4528
  const openSharedTx = (db, context, txCtx, stateKey, expected) => {
4316
4529
  let release;
@@ -4345,6 +4558,7 @@ const openSharedTx = (db, context, txCtx, stateKey, expected) => {
4345
4558
  completed: 0,
4346
4559
  failure: void 0,
4347
4560
  aborted: false,
4561
+ timedOut: false,
4348
4562
  done: false,
4349
4563
  ready,
4350
4564
  settled,
@@ -4359,8 +4573,9 @@ const openSharedTx = (db, context, txCtx, stateKey, expected) => {
4359
4573
  suspendTimer: clearTimer,
4360
4574
  bumpTimer: () => {
4361
4575
  clearTimer();
4362
- if (state.done) return;
4576
+ if (state.done || txCtx.timeoutMs === Number.POSITIVE_INFINITY) return;
4363
4577
  timer = setTimeout(() => {
4578
+ if (!state.aborted) state.timedOut = true;
4364
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" }));
4365
4580
  }, txCtx.timeoutMs);
4366
4581
  timer.unref?.();
@@ -4379,15 +4594,15 @@ const openSharedTx = (db, context, txCtx, stateKey, expected) => {
4379
4594
  state.bumpTimer();
4380
4595
  return state;
4381
4596
  };
4382
- const runOnSharedTx = async (state, body) => {
4597
+ const runOnSharedTx = async (state, timeoutMs, body) => {
4383
4598
  if (state.aborted) {
4384
4599
  await state.settled.catch(() => {});
4385
- throw abortedBatchError();
4600
+ throw abortedBatchError(state, timeoutMs);
4386
4601
  }
4387
4602
  state.suspendTimer();
4388
4603
  const tx = await state.ready;
4389
4604
  try {
4390
- if (state.aborted) throw abortedBatchError();
4605
+ if (state.aborted) throw abortedBatchError(state, timeoutMs);
4391
4606
  const result = await body(tx);
4392
4607
  state.completed += 1;
4393
4608
  if (state.completed >= state.expected) {
@@ -4420,14 +4635,24 @@ const runMutation = async (db, context, info, txCtx, body, forceTx) => {
4420
4635
  if (contextIsObject && context[drizzleExecutorKey]) return body(context[drizzleExecutorKey]);
4421
4636
  if (!txCtx || info.operation.operation !== "mutation") return forceTx ? db.transaction((tx) => body(tx)) : body(db);
4422
4637
  const keys = /* @__PURE__ */ new Set();
4423
- 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);
4424
4649
  txCtx.states.set(stateKey, state);
4425
4650
  }
4426
4651
  if (state.kind === "passthrough") {
4427
4652
  const executor = resolveExecutor(db, context);
4428
4653
  return forceTx && executor === db ? db.transaction((tx) => body(tx)) : body(executor);
4429
4654
  }
4430
- return runOnSharedTx(state, body);
4655
+ return runOnSharedTx(state, txCtx.timeoutMs, body);
4431
4656
  };
4432
4657
  //#endregion
4433
4658
  //#region src/util/builders/common/write-hooks.ts
@@ -4581,10 +4806,11 @@ const hardDeleteArg = {
4581
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."
4582
4807
  };
4583
4808
  const prepareMutationRelationColumns = (params) => {
4584
- const { relationMap, tables, tableName, typeNameMapper, table, pkNames, parsedInfo, resolveName } = params;
4809
+ const { relationMap, filterCtx, tables, tableName, typeNameMapper, table, pkNames, parsedInfo, resolveName } = params;
4585
4810
  const typeName = resolveObjectTypeName(tableName, typeNameMapper, resolveName);
4586
4811
  const withParams = relationMap[tableName] ? extractRelationsParams(relationMap, tables, tableName, parsedInfo, typeName, {
4587
4812
  typeNameMapper,
4813
+ filterCtx,
4588
4814
  limits: params.limits,
4589
4815
  scope: params.scope,
4590
4816
  defaultOrderBy: params.defaultOrderBy,
@@ -4594,7 +4820,8 @@ const prepareMutationRelationColumns = (params) => {
4594
4820
  const baseColumns = extractSelectedColumnsFromTreeSQLFormat(parsedInfo.fieldsByTypeName[typeName], table, {
4595
4821
  tableName,
4596
4822
  relationMap,
4597
- tables
4823
+ tables,
4824
+ allRelations: filterCtx?.relationMap
4598
4825
  });
4599
4826
  return {
4600
4827
  columns: hasRelations ? withPrimaryKeyColumns(baseColumns, table, pkNames) : baseColumns,
@@ -4730,7 +4957,7 @@ const assertSingleMatch = async (executor, table, filters) => {
4730
4957
  * MySQL (no `RETURNING`; its hooks see `rows: []` as on every MySQL mutation), skips the
4731
4958
  * `RETURNING` and reads the count off the driver's result via {@link rowsAffected}.
4732
4959
  */
4733
- 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 }) => {
4734
4961
  const softDelete = kind === "delete" ? policies?.softDelete?.(tableName) : void 0;
4735
4962
  const canHardDelete = softDelete?.hardDelete === true;
4736
4963
  const queryArgs = {
@@ -4773,7 +5000,7 @@ const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, f
4773
5000
  required: requireWhere,
4774
5001
  deleted: hard ? "INCLUDE" : void 0
4775
5002
  });
4776
- if (filters) query = query.where(filters);
5003
+ if (filters) query = query.where(writeFilter ? writeFilter(filters) : filters);
4777
5004
  if (!readsRows) {
4778
5005
  const result = await query;
4779
5006
  await after();
@@ -4833,27 +5060,40 @@ const getOrCreateLoader = (context, key, batchFn) => {
4833
5060
  if (!loaders.has(key)) loaders.set(key, new BatchLoader(batchFn));
4834
5061
  return loaders.get(key);
4835
5062
  };
5063
+ const requestCacheNode = () => ({
5064
+ children: /* @__PURE__ */ new WeakMap(),
5065
+ entries: /* @__PURE__ */ new Map()
5066
+ });
4836
5067
  /**
4837
5068
  * Memoizes a value that a resolver derives from its own field — its args and its selection —
4838
5069
  * for the length of one request. Resolvers run once per parent row, so anything derived only
4839
5070
  * from the field is otherwise recomputed for every row of a batch.
4840
5071
  *
4841
- * The cache lives on the context (weakly keyed by the AST node the value is derived from, so a
4842
- * document cached across requests never carries values between them) and `key` separates fields
4843
- * that share a node: one fragment selection can resolve for more than one parent type. Without a
4844
- * 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.
4845
5081
  *
4846
5082
  * `compute` must not read the parent row.
4847
5083
  */
4848
- const getOrCreateRequestValue = (context, node, key, compute) => {
4849
- if (!context || typeof context !== "object" || !node || typeof node !== "object") return compute();
4850
- if (!context[DRIZZLE_REQUEST_CACHE_KEY]) context[DRIZZLE_REQUEST_CACHE_KEY] = /* @__PURE__ */ new WeakMap();
4851
- const cache = context[DRIZZLE_REQUEST_CACHE_KEY];
4852
- let entries = cache.get(node);
4853
- if (!entries) {
4854
- entries = /* @__PURE__ */ new Map();
4855
- 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;
4856
5095
  }
5096
+ const entries = level.entries;
4857
5097
  if (entries.has(key)) return entries.get(key);
4858
5098
  const value = compute();
4859
5099
  entries.set(key, value);
@@ -5088,6 +5328,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
5088
5328
  const junction = extractJunctionColumns(relEntry);
5089
5329
  const groupCols = junction ? [junction.sourceCol] : joinPairs.map((pair) => pair.foreignCol);
5090
5330
  const groupKeys = junction ? [PARENT_KEY] : joinPairs.map((pair) => pair.foreignColPropName);
5331
+ const junctionName = junction ? junctionTableName(tables, relEntry) : void 0;
5091
5332
  const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, joinPairs.map((pair) => pair.localCol), groupCols);
5092
5333
  const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, isOne && relEntry.relation?.optional === false);
5093
5334
  const limitPolicy = isOne ? void 0 : limits?.(targetTableName);
@@ -5132,7 +5373,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
5132
5373
  const localValues = parentJoinValues(parent, joinPairs);
5133
5374
  if (localValues.some((value) => value == null)) return isOne ? null : [];
5134
5375
  const { where: whereArg, limit: requestedLimit, offset, after, deleted } = args ?? {};
5135
- 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}`, () => {
5136
5377
  const orderByArg = isOne ? args?.orderBy : withDefaultOrderBy(args ?? {}, targetTableName, policies?.defaultOrderBy).orderBy;
5137
5378
  const limit = applyLimitPolicy(requestedLimit, limitPolicy, errorCtx);
5138
5379
  assertPagingNotNegative({ offset }, errorCtx);
@@ -5176,7 +5417,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
5176
5417
  const uniqueKeys = [...new Map(parentKeys.map((key) => [joinKeyOf(key, localCols), key])).values()];
5177
5418
  const scope = resolveScope(policies, context, filterCtx);
5178
5419
  const loadChunk = async (chunkKeys) => {
5179
- let whereCondition = withScope(scope, targetTableName, targetTable, (0, drizzle_orm.and)(parentKeyCondition(groupCols, chunkKeys), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0, cursorValues ? buildCursorCondition(targetTable, cursorEntries, cursorValues, nullOrdering) : void 0), deleted ?? defaultDeleted);
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);
5180
5421
  if (distinct) {
5181
5422
  const keys = await selectDistinctKeys({
5182
5423
  db: executor,
@@ -5194,7 +5435,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
5194
5435
  } : void 0
5195
5436
  });
5196
5437
  if (!keys.length) return /* @__PURE__ */ new Map();
5197
- 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));
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));
5198
5439
  }
5199
5440
  const selection = junction ? {
5200
5441
  ...columns ?? (0, drizzle_orm.getColumns)(targetTable),
@@ -5331,7 +5572,7 @@ const generateTableOrderCached = (table, innerOrder) => {
5331
5572
  try {
5332
5573
  const columns = visibleColumns(table);
5333
5574
  const columnEntries = Object.entries(columns);
5334
- remapped = Object.fromEntries(columnEntries.map(([columnName, _columnDescription]) => [columnName, { type: innerOrder }]));
5575
+ remapped = Object.fromEntries(columnEntries.filter(([, column]) => isOrderableColumn(column)).map(([columnName]) => [columnName, { type: innerOrder }]));
5335
5576
  if (cacheable) {
5336
5577
  let byInnerOrder = orderMap.get(table);
5337
5578
  if (!byInnerOrder) {
@@ -5374,43 +5615,71 @@ const generateTableSelectTypeFieldsCached = (table, tableName, cacheCtx) => {
5374
5615
  return remapped;
5375
5616
  };
5376
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
+ /**
5377
5647
  * Order fields for a table's to-one relations: each takes the target table's own OrderBy
5378
5648
  * input, so a list can be sorted by a related row's column (compiled as a correlated
5379
- * subquery in the ORDER BY — see `extractOrderBy`). To-many relations are left out: "order
5380
- * a parent by its many children" has no single defined value to sort on. A relation whose
5381
- * 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.
5382
5651
  */
5383
- const generateRelationOrderFields = (tableName, cacheCtx, typeNameMapper, columnFields, relationMap, tables) => {
5384
- const relations = relationMap?.[tableName];
5385
- if (!relations || !tables) return {};
5652
+ const generateRelationOrderFields = (tableName, cacheCtx, typeNameMapper, relationMap, tables) => {
5386
5653
  const fields = {};
5387
- for (const [relationName, relEntry] of Object.entries(relations)) {
5388
- if (relationName in columnFields) continue;
5389
- const targetTable = tables[relEntry.targetTableName];
5390
- const relation = relEntry.relation ?? relEntry;
5391
- if (!targetTable || !(0, drizzle_orm.is)(relation, drizzle_orm.One) || !isFilterableRelation(relation)) continue;
5392
- fields[relationName] = {
5393
- 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,
5394
5658
  description: `Order by columns of the related ${relationName} row`
5395
5659
  };
5396
5660
  }
5397
5661
  return fields;
5398
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
+ */
5399
5667
  const generateTableOrderTypeCached = (table, tableName, typeNameMapper, cacheCtx, relationMap, tables) => {
5400
5668
  if (cacheCtx.orderTypeCache.has(table)) return cacheCtx.orderTypeCache.get(table);
5669
+ if (!hasOrderFields(tableName, relationMap, {
5670
+ ...tables,
5671
+ [tableName]: table
5672
+ })) return;
5401
5673
  const order = new graphql.GraphQLInputObjectType({
5402
5674
  name: cacheCtx.typeName({
5403
5675
  kind: "orderBy",
5404
5676
  defaultName: `${resolveTypeName(tableName, typeNameMapper)}OrderBy`,
5405
5677
  table: tableName
5406
5678
  }),
5407
- fields: () => {
5408
- const orderColumns = generateTableOrderCached(table, innerOrderType(cacheCtx.typeName));
5409
- return {
5410
- ...orderColumns,
5411
- ...generateRelationOrderFields(tableName, cacheCtx, typeNameMapper, orderColumns, relationMap, tables)
5412
- };
5413
- }
5679
+ fields: () => ({
5680
+ ...generateTableOrderCached(table, innerOrderType(cacheCtx.typeName)),
5681
+ ...generateRelationOrderFields(tableName, cacheCtx, typeNameMapper, relationMap, tables)
5682
+ })
5414
5683
  });
5415
5684
  cacheCtx.orderTypeCache.set(table, order);
5416
5685
  return order;
@@ -5629,7 +5898,7 @@ const generateSelectFields = (tables, tableName, relationMap, fromTableName, fro
5629
5898
  type: new graphql.GraphQLNonNull(new graphql.GraphQLList(new graphql.GraphQLNonNull(relType))),
5630
5899
  args: {
5631
5900
  where: { type: relSelectData.filters },
5632
- orderBy: { type: relSelectData.order },
5901
+ ...relSelectData.order ? { orderBy: { type: relSelectData.order } } : {},
5633
5902
  offset: { type: graphql.GraphQLInt },
5634
5903
  limit: { type: graphql.GraphQLInt },
5635
5904
  ...deletedArg(cacheCtx.softDeleteOf, targetTableName, cacheCtx.typeName),
@@ -5857,6 +6126,7 @@ const resolveBuildConfig = (config, schema) => {
5857
6126
  };
5858
6127
  const transactionsOpt = config?.transactions;
5859
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}.`);
5860
6130
  const limitsConfig = config?.limits;
5861
6131
  if (limitsConfig) {
5862
6132
  const check = (policy, where) => {
@@ -5889,6 +6159,7 @@ const resolveBuildConfig = (config, schema) => {
5889
6159
  for (const [columnName, entry] of Object.entries(orderBy)) {
5890
6160
  if (!columns[columnName]) throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy names '${columnName}', which is not a column of '${tableName}'.`);
5891
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.`);
5892
6163
  const direction = typeof entry === "string" ? entry : entry?.direction;
5893
6164
  if (direction !== "asc" && direction !== "desc") throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy.${columnName} must be 'asc', 'desc', or { direction, priority? }.`);
5894
6165
  const priority = typeof entry === "string" ? void 0 : entry?.priority;
@@ -5947,7 +6218,7 @@ const resolveBuildConfig = (config, schema) => {
5947
6218
  }
5948
6219
  }
5949
6220
  }
5950
- const onWrite = writeHooks.size ? (tableName) => writeHooks.get(tableName) : void 0;
6221
+ const onWrite = [...writeHooks.values()].some(Boolean) ? (tableName) => writeHooks.get(tableName) : void 0;
5951
6222
  const policies = scopeConfig || contextValuesConfig || softDeleteInfos.size || onWrite || defaultOrderBy ? {
5952
6223
  scope: scopeConfig ? (tableName) => scopeConfig[tableName] : void 0,
5953
6224
  contextValues: contextValuesConfig ? (tableName) => contextValuesConfig[tableName] : void 0,
@@ -6038,8 +6309,14 @@ const AGGREGATE_OPS = [
6038
6309
  ];
6039
6310
  /** Ops whose result is a row count: never null, and returned as `Int!` rather than the column's type. */
6040
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);
6041
6318
  const OP_FNS = {
6042
- avg: drizzle_orm.avg,
6319
+ avg: avgOf,
6043
6320
  sum: drizzle_orm.sum,
6044
6321
  min: drizzle_orm.min,
6045
6322
  max: drizzle_orm.max,
@@ -6049,6 +6326,18 @@ const OP_FNS = {
6049
6326
  /** Separator for flat select aliases (`avg__price`) — reassembled into nested output by the resolver. */
6050
6327
  const SEP = "__";
6051
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
+ /**
6052
6341
  * Classifies a table's columns for aggregation. avg/sum only make sense on numeric scalars;
6053
6342
  * min/max work on any orderable scalar (numbers, strings, bigints, dates, enums). Booleans,
6054
6343
  * arrays (including array-typed number columns), JSON, buffers, and object-shaped columns
@@ -6080,6 +6369,7 @@ const classifyAggregateColumns = (table, tableName) => {
6080
6369
  if (cached) return cached;
6081
6370
  const numeric = {};
6082
6371
  const orderable = {};
6372
+ const comparable = {};
6083
6373
  const all = {};
6084
6374
  for (const [columnName, column] of Object.entries(visibleColumns(table))) {
6085
6375
  all[columnName] = column;
@@ -6089,7 +6379,11 @@ const classifyAggregateColumns = (table, tableName) => {
6089
6379
  const { type: dataType, constraint } = (0, drizzle_orm.extractExtendedColumnType)(column);
6090
6380
  if (dataType === "boolean" || dataType === "array" || dataType === "custom") continue;
6091
6381
  if (dataType === "object" && constraint !== "date") continue;
6092
- orderable[columnName] = {
6382
+ comparable[columnName] = {
6383
+ column,
6384
+ converted
6385
+ };
6386
+ if (!NO_MIN_MAX_COLUMN_TYPES.has(column.columnType)) orderable[columnName] = {
6093
6387
  column,
6094
6388
  converted
6095
6389
  };
@@ -6098,6 +6392,7 @@ const classifyAggregateColumns = (table, tableName) => {
6098
6392
  const sets = {
6099
6393
  numeric,
6100
6394
  orderable,
6395
+ comparable,
6101
6396
  all
6102
6397
  };
6103
6398
  byName.set(tableName, sets);
@@ -6110,13 +6405,13 @@ const classifyAggregateColumns = (table, tableName) => {
6110
6405
  * and avg/sum of integers overflow Int / produce decimals)
6111
6406
  * - `min` / `max` — per orderable column, the column's own (nullable) scalar type
6112
6407
  * - `countNonNull` — per column, `Int!`: how many matching rows have a non-null value there
6113
- * - `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
6114
6409
  * Each wrapper is omitted when no column qualifies for it.
6115
6410
  */
6116
6411
  const generateAggregateTypes = (table, tableName, typeName, cacheCtx) => {
6117
6412
  const cached = cacheCtx?.aggregateTypeCache.get(tableName);
6118
6413
  if (cached) return cached;
6119
- const { numeric, orderable, all } = classifyAggregateColumns(table, tableName);
6414
+ const { numeric, orderable, comparable, all } = classifyAggregateColumns(table, tableName);
6120
6415
  const nameOf = (op) => cacheCtx?.typeName({
6121
6416
  kind: "aggregate",
6122
6417
  defaultName: `${typeName}${capitalize(op)}Aggregate`,
@@ -6137,7 +6432,7 @@ const generateAggregateTypes = (table, tableName, typeName, cacheCtx) => {
6137
6432
  }) };
6138
6433
  const countSets = {
6139
6434
  countNonNull: all,
6140
- countDistinct: orderable
6435
+ countDistinct: comparable
6141
6436
  };
6142
6437
  for (const [op, columns] of Object.entries(countSets)) {
6143
6438
  const columnNames = Object.keys(columns);
@@ -6317,6 +6612,7 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
6317
6612
  const junction = extractJunctionColumns(relEntry);
6318
6613
  if (junction === null) return;
6319
6614
  const groupCols = junction ? [junction.sourceCol] : joinPairs.map((pair) => pair.foreignCol);
6615
+ const junctionName = junction ? junctionTableName(tables, relEntry) : void 0;
6320
6616
  const groupAliases = groupCols.map((_, i) => `${GROUP_KEY}${i}`);
6321
6617
  const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, joinPairs.map((pair) => pair.localCol), groupCols);
6322
6618
  const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, false);
@@ -6330,7 +6626,7 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
6330
6626
  };
6331
6627
  const resolve = async (parent, args, context, info) => {
6332
6628
  try {
6333
- const { request, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes?.[0], `aggregate:${tableName}::${relationName}`, () => {
6629
+ const { request, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes, `aggregate:${tableName}::${relationName}`, () => {
6334
6630
  const request = parseAggregateRequest(info, target);
6335
6631
  return {
6336
6632
  request,
@@ -6353,7 +6649,7 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
6353
6649
  const groupSelection = Object.fromEntries(groupAliases.map((alias, i) => [alias, groupCols[i]]));
6354
6650
  const byKey = /* @__PURE__ */ new Map();
6355
6651
  for (const chunk of parentKeyChunks(uniqueKeys, groupCols.length)) {
6356
- const whereCondition = withScope(scope, targetTableName, targetTable, (0, drizzle_orm.and)(parentKeyCondition(groupCols, chunk), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0), deleted ?? defaultDeleted);
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);
6357
6653
  const selected = executor.select({
6358
6654
  ...groupSelection,
6359
6655
  ...request.selection
@@ -6387,12 +6683,12 @@ const HAVING_OPS = {
6387
6683
  };
6388
6684
  /**
6389
6685
  * Columns a query can group on. Grouping needs an equality operator, which is the same
6390
- * 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
6391
6687
  * true/false says nothing, but `GROUP BY isConfirmed` is perfectly ordinary.
6392
6688
  */
6393
6689
  const groupableColumns = (table, tableName) => {
6394
- const { orderable } = classifyAggregateColumns(table, tableName);
6395
- const groupable = { ...orderable };
6690
+ const { comparable } = classifyAggregateColumns(table, tableName);
6691
+ const groupable = { ...comparable };
6396
6692
  for (const [columnName, column] of Object.entries(visibleColumns(table))) {
6397
6693
  if (groupable[columnName]) continue;
6398
6694
  const { type: dataType } = (0, drizzle_orm.extractExtendedColumnType)(column);
@@ -6433,7 +6729,7 @@ const aggregateNumberFilterType = (typeName) => sharedType(typeName, {
6433
6729
  * nothing to compare against.
6434
6730
  */
6435
6731
  const generateHavingInput = (table, tableName, typeName, cacheCtx) => {
6436
- const { numeric, orderable, all } = classifyAggregateColumns(table, tableName);
6732
+ const { numeric, comparable, all } = classifyAggregateColumns(table, tableName);
6437
6733
  const resolveName = cacheCtx?.typeName ?? ((info) => info.defaultName);
6438
6734
  const numberFilter = aggregateNumberFilterType(resolveName);
6439
6735
  const fields = { count: {
@@ -6446,7 +6742,7 @@ const generateHavingInput = (table, tableName, typeName, cacheCtx) => {
6446
6742
  min: numeric,
6447
6743
  max: numeric,
6448
6744
  countNonNull: all,
6449
- countDistinct: orderable
6745
+ countDistinct: comparable
6450
6746
  };
6451
6747
  for (const [op, columns] of Object.entries(opColumns)) {
6452
6748
  const columnNames = Object.keys(columns);
@@ -6511,22 +6807,34 @@ const generateGroupByType = (table, tableName, typeName, cacheCtx) => {
6511
6807
  }
6512
6808
  });
6513
6809
  };
6514
- /** Turns one `${typeName}Having` entry into SQL conditions over the matching aggregate expression. */
6515
- 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
+ };
6516
6822
  const buildHavingCondition = (having, columns) => {
6517
6823
  if (!having) return;
6518
6824
  const conditions = [];
6825
+ const firstColumn = Object.values(columns)[0];
6826
+ const isPg = firstColumn ? columnDialect(firstColumn) === "pg" : false;
6519
6827
  for (const [key, value] of Object.entries(having)) {
6520
6828
  if (value == null) continue;
6521
6829
  if (key === "count") {
6522
- conditions.push(...havingComparisons((0, drizzle_orm.count)(), value));
6830
+ conditions.push(...havingComparisons((0, drizzle_orm.count)(), value, isPg));
6523
6831
  continue;
6524
6832
  }
6525
6833
  if (!AGGREGATE_OPS.includes(key)) continue;
6526
6834
  for (const [columnName, filter] of Object.entries(value)) {
6527
6835
  const column = columns[columnName];
6528
6836
  if (!column || filter == null) continue;
6529
- conditions.push(...havingComparisons(OP_FNS[key](column), filter));
6837
+ conditions.push(...havingComparisons(OP_FNS[key](column), filter, isPg));
6530
6838
  }
6531
6839
  }
6532
6840
  return conditions.length ? (0, drizzle_orm.and)(...conditions) : void 0;
@@ -6678,12 +6986,32 @@ const isOptionalOnInsert = (column) => !column.notNull || !!column.hasDefault ||
6678
6986
  * relation could come back without it. A `where` inherited from the reverse relation filters
6679
6987
  * the parent instead, and becomes a per-parent check.
6680
6988
  */
6681
- const ownWherePlan = (relEntry, parentTable, targetTable, localCol) => {
6989
+ const ownWherePlan = (relEntry, parentTable, targetTable, localCol, parentPrimaryKey) => {
6682
6990
  const condition = () => relationOwnCondition(relEntry, parentTable, targetTable, [localCol], [localCol]);
6683
6991
  if (!condition()) return {};
6684
- if (!isRelationFilterReversed(relEntry.relation ?? relEntry)) return { ownCondition: () => condition() };
6685
- 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
+ };
6686
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)}`;
6687
7015
  /** The property name a column is stored under on its table, matched by identity. */
6688
7016
  const propNameOf = (table, column) => {
6689
7017
  if (!column) return;
@@ -6745,7 +7073,7 @@ const buildThroughPlan = (relationName, relEntry, relation, table, targetTableNa
6745
7073
  targetCol
6746
7074
  },
6747
7075
  targetKeyColumns: { [foreignColPropName]: foreignCol },
6748
- ...ownWherePlan(relEntry, table, targetTable, localCol),
7076
+ ...ownWherePlan(relEntry, table, targetTable, localCol, []),
6749
7077
  allowed: /* @__PURE__ */ new Set([
6750
7078
  "connect",
6751
7079
  "disconnect",
@@ -6824,7 +7152,7 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
6824
7152
  const targetPrimaryKey = resolvePrimaryKeys(targetTable).filter((name) => targetColumns[name]);
6825
7153
  const targetKeyColumns = Object.fromEntries(targetPrimaryKey.map((name) => [name, targetColumns[name]]));
6826
7154
  targetKeyColumns[foreignColPropName] = foreignCol;
6827
- const ownWhere = ownWherePlan(relEntry, table, targetTable, localCol);
7155
+ const ownWhere = ownWherePlan(relEntry, table, targetTable, localCol, resolvePrimaryKeys(table));
6828
7156
  const ops = !fkColumn.notNull ? isOne ? [
6829
7157
  "create",
6830
7158
  "connect",
@@ -7257,6 +7585,16 @@ const createNestedWriteRuntime = (params) => {
7257
7585
  }
7258
7586
  if (supplied(op["connect"])) await link(asArray(op["connect"]), "connect");
7259
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
+ };
7260
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");
7261
7599
  return {
7262
7600
  enabled: (tableName) => !!plans[tableName],
@@ -7282,7 +7620,10 @@ const createNestedWriteRuntime = (params) => {
7282
7620
  const allColumns = (0, drizzle_orm.getColumns)(table);
7283
7621
  for (const relationName of Object.keys(ops)) {
7284
7622
  const plan = tablePlans[relationName];
7285
- 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
+ }
7286
7627
  }
7287
7628
  for (const name of selfTargetingPlan(tablePlans, tableName, ops)?.targetPrimaryKey ?? []) if (!(name in columns) && allColumns[name]) columns[name] = allColumns[name];
7288
7629
  return columns;
@@ -7302,10 +7643,15 @@ const createNestedWriteRuntime = (params) => {
7302
7643
  ...fresh.get(keyOf(row))
7303
7644
  }));
7304
7645
  },
7305
- applyParentSide: async (executor, tableName, ops, request) => {
7646
+ applyParentSide: async (executor, tableName, ops, request, existing = false) => {
7306
7647
  const tablePlans = plans[tableName];
7307
- const patch = {};
7308
- if (!tablePlans) return patch;
7648
+ const result = {
7649
+ values: {},
7650
+ expected: {},
7651
+ existing
7652
+ };
7653
+ const patch = result.values;
7654
+ if (!tablePlans) return result;
7309
7655
  const context = request?.context;
7310
7656
  const scope = resolveScope(policies, context, filterCtx);
7311
7657
  for (const [relationName, op] of Object.entries(ops)) {
@@ -7324,14 +7670,30 @@ const createNestedWriteRuntime = (params) => {
7324
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" });
7325
7671
  await steps.after();
7326
7672
  patch[plan.localColPropName] = created[plan.foreignColPropName];
7673
+ result.expected[relationName] = created[plan.foreignColPropName];
7327
7674
  } else if (op["connect"] !== void 0) {
7328
7675
  const rows = await executor.select({ [plan.foreignColPropName]: plan.foreignCol }).from(plan.targetTable).where(conditionOf(plan, op["connect"], "connect", scope));
7329
7676
  if (rows.length !== 1) throw connectMatchError(relationName, rows.length);
7330
- patch[plan.localColPropName] = rows[0][plan.foreignColPropName];
7331
- } 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;
7332
7681
  }
7333
7682
  for (const columnName of Object.keys(contextValues?.(tableName) ?? {})) delete patch[columnName];
7334
- 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
+ }
7335
7697
  },
7336
7698
  applyChildSide: async (executor, tableName, ops, parentRows, request) => {
7337
7699
  const tablePlans = plans[tableName];
@@ -7398,12 +7760,13 @@ var NoParentRow = class extends Error {};
7398
7760
  * `executor` must already be a transaction.
7399
7761
  */
7400
7762
  const writeNestedEntry = async (params) => {
7401
- const { executor, runtime, tableName, ops, write, request } = params;
7763
+ const { executor, runtime, tableName, ops, write, request, existing } = params;
7402
7764
  const guarded = runtime.createsParentSide(tableName, ops);
7403
7765
  const run = async (tx) => {
7404
- const patch = await runtime.applyParentSide(tx, tableName, ops, request);
7405
- 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);
7406
7768
  if (guarded && !rows.length) throw new NoParentRow();
7769
+ await runtime.checkParentSide(tx, tableName, ops, rows, patch);
7407
7770
  await runtime.applyChildSide(tx, tableName, ops, rows, request);
7408
7771
  return runtime.refreshed(tx, tableName, ops, rows);
7409
7772
  };
@@ -7465,7 +7828,8 @@ const updateWithNestedOps = async (params) => {
7465
7828
  ...remapValues(columns),
7466
7829
  ...patch
7467
7830
  }),
7468
- request
7831
+ request,
7832
+ existing: true
7469
7833
  }));
7470
7834
  };
7471
7835
  //#endregion
@@ -7618,6 +7982,7 @@ const createSchemaBuilder = (adapter) => {
7618
7982
  const anyTable = (feature) => tableEntries.some(([name]) => featureOf(name)[feature]);
7619
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?");
7620
7984
  registerScalarOverrides(tables, options);
7985
+ installSqliteBlobJsonDecoders(tables);
7621
7986
  registerEnumConfig(options);
7622
7987
  registerColumnExclusions(tables, options.exclude);
7623
7988
  resetAggregateColumnCache();
@@ -7849,7 +8214,7 @@ const createSchemaBuilder = (adapter) => {
7849
8214
  ...tableFeatures.update ? [updateInput] : [],
7850
8215
  ...built.updateManyInput ? [built.updateManyInput] : [],
7851
8216
  tableFilters,
7852
- tableOrder
8217
+ ...tableOrder ? [tableOrder] : []
7853
8218
  ].forEach((e) => {
7854
8219
  inputs[e.name] = e;
7855
8220
  });
@@ -7979,14 +8344,12 @@ const generateUpsert = (db, tableName, table, baseType, onConflictType, fieldNam
7979
8344
  withTarget: false,
7980
8345
  serverOwned: Object.keys(contextColumns ?? {})
7981
8346
  });
7982
- const requestedNothing = args.onConflict?.action === "NOTHING";
7983
8347
  const runBatch = async (target, { values, plan }) => {
7984
- if (plan.action === "NOTHING" && !requestedNothing) {
8348
+ if (plan.action === "NOTHING") {
7985
8349
  const columns = (0, drizzle_orm.getColumns)(table);
7986
8350
  const keyName = pkNames[0] ?? Object.keys(columns)[0];
7987
8351
  await target.insert(table).values(values).onDuplicateKeyUpdate({ set: { [keyName]: drizzle_orm.sql`${drizzle_orm.sql.identifier(columns[keyName].name)}` } });
7988
- } else if (plan.action === "NOTHING") await target.insert(table).ignore().values(values);
7989
- else await target.insert(table).values(values).onDuplicateKeyUpdate({ set: plan.set });
8352
+ } else await target.insert(table).values(values).onDuplicateKeyUpdate({ set: plan.set });
7990
8353
  };
7991
8354
  if (batches.length === 1) await runBatch(executor, batches[0]);
7992
8355
  else await executor.transaction(async (tx) => {
@@ -8026,7 +8389,7 @@ const generateUpdate = (db, tableName, table, setArgs, filterArgs, fieldName, si
8026
8389
  });
8027
8390
  if (single) await assertSingleMatch(executor, table, filters);
8028
8391
  let query = executor.update(table).set(input);
8029
- if (filters) query = query.where(filters);
8392
+ if (filters) query = query.where(mysqlWriteFilter(table, filters));
8030
8393
  await query;
8031
8394
  await after();
8032
8395
  return isSuccess;
@@ -8041,7 +8404,7 @@ const generateUpdate = (db, tableName, table, setArgs, filterArgs, fieldName, si
8041
8404
  * in order. MySQL has no RETURNING, so — like every other MySQL mutation — the result is
8042
8405
  * `{ isSuccess: true }` rather than the updated rows.
8043
8406
  */
8044
- 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({
8045
8408
  db,
8046
8409
  tableName,
8047
8410
  operation: "updateMany",
@@ -8061,13 +8424,20 @@ const generateUpdateMany$2 = (db, tableName, table, updateManyInput, fieldName,
8061
8424
  if (!Object.keys(input).length) throw drizzleError("Unable to update with no values specified!", { code: "DRIZZLE_NO_VALUES" });
8062
8425
  return {
8063
8426
  set: input,
8064
- 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
+ })
8065
8435
  };
8066
8436
  });
8067
8437
  await executor.transaction(async (tx) => {
8068
8438
  for (const entry of entries) {
8069
8439
  let query = tx.update(table).set(entry.set);
8070
- if (entry.filters) query = query.where(entry.filters);
8440
+ if (entry.filters) query = query.where(mysqlWriteFilter(table, entry.filters));
8071
8441
  await query;
8072
8442
  }
8073
8443
  });
@@ -8116,7 +8486,7 @@ const generateDelete = (db, tableName, table, filterArgs, fieldName, single, req
8116
8486
  });
8117
8487
  if (single) await assertSingleMatch(executor, table, filters);
8118
8488
  let query = softDelete && !hard ? executor.update(table).set({ [softDelete.columnName]: restore ? softDelete.writeRestored : softDelete.writeDeleted() }) : executor.delete(table);
8119
- if (filters) query = query.where(filters);
8489
+ if (filters) query = query.where(mysqlWriteFilter(table, filters));
8120
8490
  await query;
8121
8491
  await after();
8122
8492
  return isSuccess;
@@ -8127,6 +8497,34 @@ const generateDelete = (db, tableName, table, filterArgs, fieldName, single, req
8127
8497
  const getTableConfig$2 = memoizeTableConfig(drizzle_orm_mysql_core.getTableConfig);
8128
8498
  /** Primary-key property names for a MySQL table, including table-level composite keys. */
8129
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
+ };
8130
8528
  const { prepareBuild, forEachTable, finalizeBuild } = createSchemaBuilder({
8131
8529
  tableClass: drizzle_orm_mysql_core.MySqlTable,
8132
8530
  getTableConfig: getTableConfig$2,
@@ -8180,7 +8578,7 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
8180
8578
  tableFilters,
8181
8579
  cacheCtx
8182
8580
  }) : void 0;
8183
- 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;
8184
8582
  const deleteGenerated = tableFeatures.delete ? generateDelete(db, tableName, schema[tableName], tableFilters, deleteFieldName, false, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies) : void 0;
8185
8583
  const deleteSingleGenerated = tableFeatures.delete ? generateDelete(db, tableName, schema[tableName], tableFilters, deleteSingleFieldName, true, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies) : void 0;
8186
8584
  const restoreGenerated = softDeleteInfo ? generateDelete(db, tableName, schema[tableName], tableFilters, restoreFieldName, false, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies, true) : void 0;
@@ -8197,7 +8595,8 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
8197
8595
  filterCtx,
8198
8596
  txCtx: mutationTxCtx,
8199
8597
  policies,
8200
- returnsRows: false
8598
+ returnsRows: false,
8599
+ writeFilter: (filters) => mysqlWriteFilter(schema[tableName], filters)
8201
8600
  }) : void 0;
8202
8601
  const deleteCountGenerated = tableFeatures.delete && tableFeatures.countMutations ? generateWriteCount({
8203
8602
  db,
@@ -8210,7 +8609,8 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
8210
8609
  filterCtx,
8211
8610
  txCtx: mutationTxCtx,
8212
8611
  policies,
8213
- returnsRows: false
8612
+ returnsRows: false,
8613
+ writeFilter: (filters) => mysqlWriteFilter(schema[tableName], filters)
8214
8614
  }) : void 0;
8215
8615
  const hardDeleteMeta = softDeleteInfo?.hardDelete ? { hardDelete: true } : {};
8216
8616
  const withMeta = (generated, meta) => ({
@@ -8322,7 +8722,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8322
8722
  * one-element list.
8323
8723
  */
8324
8724
  const generateInsert = (build, opts) => {
8325
- const { db, tables, relationMap, typeNameMapper, txCtx, nested, limits, policies, resolveName } = build;
8725
+ const { db, tables, relationMap, typeNameMapper, filterCtx, txCtx, nested, limits, policies, resolveName } = build;
8326
8726
  const { tableName, table, baseType, fieldName, typeName, single, conflictDoNothing = false } = opts;
8327
8727
  const queryArgs = { values: { type: single ? new graphql.GraphQLNonNull(baseType) : new graphql.GraphQLNonNull(new graphql.GraphQLList(new graphql.GraphQLNonNull(baseType))) } };
8328
8728
  const pkNames = primaryKeyPropNames(table);
@@ -8346,6 +8746,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8346
8746
  const input = nestedEntries ? [] : applyContextValuesAll(remapFromGraphQLArrayInput(entries ? entries.map((entry) => entry.columns) : supplied, table), contextColumns, context);
8347
8747
  const { columns, hasRelations, withParams } = mutationSelection(info, {
8348
8748
  relationMap,
8749
+ filterCtx,
8349
8750
  tables,
8350
8751
  tableName,
8351
8752
  typeName,
@@ -8424,6 +8825,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8424
8825
  const input = nestedEntries ? [] : applyContextValuesAll(remapFromGraphQLArrayInput(entries ? entries.map((entry) => entry.columns) : supplied, table), contextColumns, context);
8425
8826
  const { columns, hasRelations, withParams } = mutationSelection(info, {
8426
8827
  relationMap,
8828
+ filterCtx,
8427
8829
  tables,
8428
8830
  tableName,
8429
8831
  typeName,
@@ -8449,18 +8851,9 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8449
8851
  buildWhere: (where) => extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope))
8450
8852
  });
8451
8853
  const batchQuery = (executor, { values: rows, plan }) => {
8452
- let query = executor.insert(table).values(rows).returning(returning);
8854
+ const query = executor.insert(table).values(rows).returning(returning);
8453
8855
  const setWhere = withScope(scope, tableName, table, plan.setWhere);
8454
- query = plan.action === "NOTHING" ? query.onConflictDoNothing(plan.target ? {
8455
- target: plan.target,
8456
- where: plan.targetWhere
8457
- } : void 0) : query.onConflictDoUpdate({
8458
- target: plan.target,
8459
- targetWhere: plan.targetWhere,
8460
- set: plan.set,
8461
- setWhere
8462
- });
8463
- return query;
8856
+ return applyConflictPlan(query, table, plan, setWhere);
8464
8857
  };
8465
8858
  if (batches.length === 1) return await batchQuery(target, batches[0]);
8466
8859
  return target.transaction((tx) => {
@@ -8523,6 +8916,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8523
8916
  await before();
8524
8917
  const { columns, hasRelations, withParams } = mutationSelection(info, {
8525
8918
  relationMap,
8919
+ filterCtx,
8526
8920
  tables,
8527
8921
  tableName,
8528
8922
  typeName,
@@ -8761,7 +9155,8 @@ const createSchemaDataGenerator = (adapter) => {
8761
9155
  table: schema[tableName],
8762
9156
  updateManyInput,
8763
9157
  fieldName: updateManyFieldName,
8764
- typeName
9158
+ typeName,
9159
+ requireWhere: tableFeatures.requireWhere
8765
9160
  }) : void 0;
8766
9161
  const deleteGenerated = tableFeatures.delete ? generateDelete(writeBuild, {
8767
9162
  tableName,
@@ -9016,7 +9411,8 @@ const runUpdateManyBatch = ({ executor, entries, table, returning, nested, table
9016
9411
  tableName,
9017
9412
  ops: entry.ops,
9018
9413
  write,
9019
- request
9414
+ request,
9415
+ existing: true
9020
9416
  }) : await write(tx, {}));
9021
9417
  }
9022
9418
  return results;
@@ -9037,7 +9433,7 @@ const runUpdateManyBatch = ({ executor, entries, table, returning, nested, table
9037
9433
  const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateManyBatch) => {
9038
9434
  return (build, opts) => {
9039
9435
  const { db, tables, relationMap, typeNameMapper, filterCtx, txCtx, nested, limits, policies, resolveName } = build;
9040
- const { tableName, table, updateManyInput, fieldName, typeName } = opts;
9436
+ const { tableName, table, updateManyInput, fieldName, typeName, requireWhere } = opts;
9041
9437
  const queryArgs = { updates: { type: new graphql.GraphQLNonNull(new graphql.GraphQLList(new graphql.GraphQLNonNull(updateManyInput))) } };
9042
9438
  const pkNames = primaryKeyPropNames(table);
9043
9439
  return writeResolver({
@@ -9057,6 +9453,7 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
9057
9453
  const contextColumns = policies?.contextValues?.(tableName);
9058
9454
  const { columns, hasRelations, withParams } = mutationSelection(info, {
9059
9455
  relationMap,
9456
+ filterCtx,
9060
9457
  tables,
9061
9458
  tableName,
9062
9459
  typeName,
@@ -9076,7 +9473,14 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
9076
9473
  return {
9077
9474
  set: input,
9078
9475
  ops,
9079
- 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
+ })
9080
9484
  };
9081
9485
  });
9082
9486
  const anyNested = entries.some((entry) => entry.ops);
@@ -9165,7 +9569,8 @@ const runSqliteUpdateManyBatch = ({ executor, entries, table, returning, anyNest
9165
9569
  tableName,
9166
9570
  ops: entry.ops,
9167
9571
  write,
9168
- request
9572
+ request,
9573
+ existing: true
9169
9574
  }) : write(tx, {});
9170
9575
  };
9171
9576
  return executor.transaction((tx) => {
@@ -9195,6 +9600,7 @@ const sqliteSchemaData = createSchemaDataGenerator({
9195
9600
  preflight: (db, options) => {
9196
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.");
9197
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.");
9198
9604
  },
9199
9605
  generateUpdateMany
9200
9606
  });