@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.js CHANGED
@@ -1,7 +1,7 @@
1
- import { Column, One, SQL, Table, aliasedTable, and, arrayContains, arrayOverlaps, asc, avg, count, countDistinct, desc, eq, extractExtendedColumnType, getColumns, getTableAsAliasSQL, gt, gte, ilike, inArray, is, isNotNull, isNull, like, lt, lte, max, min, ne, not, notIlike, notInArray, notLike, or, relationsFilterToSQL, sql, sum } from "drizzle-orm";
2
- import { MySqlAsyncDatabase, MySqlDate, MySqlDateString, MySqlDateTime, MySqlDateTimeString, MySqlInt, MySqlSerial, MySqlTable, MySqlTimestamp, MySqlTimestampString, getTableConfig } from "drizzle-orm/mysql-core";
1
+ import { Column, One, SQL, Subquery, Table, aliasedTable, and, arrayContains, arrayOverlaps, asc, avg, count, countDistinct, desc, eq, extractExtendedColumnType, getColumns, getTableAsAliasSQL, gt, gte, ilike, inArray, is, isNotNull, isNull, like, lt, lte, max, min, ne, not, notIlike, notInArray, notLike, or, relationsFilterToSQL, sql, sum } from "drizzle-orm";
2
+ import { MySqlAsyncDatabase, MySqlDate, MySqlDateString, MySqlDateTime, MySqlDateTimeString, MySqlEnumColumn, MySqlEnumObjectColumn, MySqlInt, MySqlSerial, MySqlTable, MySqlTimestamp, MySqlTimestampString, getTableConfig } from "drizzle-orm/mysql-core";
3
3
  import { PgAsyncDatabase, PgDate, PgDateString, PgInteger, PgSerial, PgTable, PgTimestamp, PgTimestampString, PgUUID, getTableConfig as getTableConfig$1 } from "drizzle-orm/pg-core";
4
- import { SQLiteAsyncDatabase, SQLiteInteger, SQLiteTable, SQLiteTimestamp, getTableConfig as getTableConfig$2 } from "drizzle-orm/sqlite-core";
4
+ import { SQLiteAsyncDatabase, SQLiteTable, SQLiteTimestamp, getTableConfig as getTableConfig$2 } from "drizzle-orm/sqlite-core";
5
5
  import { GraphQLBoolean, GraphQLEnumType, GraphQLError, GraphQLFloat, GraphQLInputObjectType, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLSchema, GraphQLString, Kind, getArgumentValues, getNamedType } from "graphql";
6
6
  import { GraphQLDateTime, GraphQLJSON, GraphQLUUID } from "graphql-scalars";
7
7
  import pluralize from "pluralize";
@@ -262,7 +262,6 @@ const singularizeMapper = (tableName) => ({
262
262
  });
263
263
  //#endregion
264
264
  //#region src/util/type-converter/index.ts
265
- const allowedNameChars = /^[a-zA-Z0-9_]+$/;
266
265
  /** drizzle's `number` constraints whose values always fit a GraphQL `Int` (32-bit signed). */
267
266
  const smallIntegerConstraints = /* @__PURE__ */ new Set([
268
267
  "int8",
@@ -288,8 +287,15 @@ const declaredEnum = (column) => {
288
287
  schema: typeof declared.schema === "string" ? declared.schema : void 0
289
288
  } : void 0;
290
289
  };
291
- /** `user_status` / `user-status` / `user status` → `UserStatus`; an already-PascalCase name is left alone. */
292
- const pascalize = (input) => input.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => capitalize(part)).join("");
290
+ /**
291
+ * `user_status` / `user-status` / `user status` → `UserStatus`; an already-PascalCase name is left
292
+ * alone. A GraphQL name cannot start with a digit, so a name that would (`2fa_mode`) is prefixed
293
+ * with an underscore: `_2faMode`.
294
+ */
295
+ const pascalize = (input) => {
296
+ const pascal = input.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((part) => capitalize(part)).join("");
297
+ return /^[0-9]/.test(pascal) ? `_${pascal}` : pascal;
298
+ };
293
299
  /**
294
300
  * Identity of the values a GraphQL enum was built from, so a name claimed twice can be told
295
301
  * apart from the same enum reached twice.
@@ -364,7 +370,7 @@ const generateEnumCached = (column, columnName, tableName) => {
364
370
  index,
365
371
  typeName: name
366
372
  });
367
- if (mapped === void 0) return allowedNameChars.test(value) ? value : `Option${index}`;
373
+ if (mapped === void 0) return validEnumValueName(value) ? value : `Option${index}`;
368
374
  if (!validEnumValueName(mapped)) throw new Error(`Drizzle-GraphQL Error: config.enumValueNameMapper named value '${value}' of enum '${name}' (on '${tableName}.${columnName}') '${mapped}', which is not a valid GraphQL enum value name. A name must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ and not be true, false or null.`);
369
375
  return mapped;
370
376
  });
@@ -460,6 +466,27 @@ const sqliteTimestampToDate = (column, value) => new Date(column.mode === "times
460
466
  * mode it uses, for the same reason as {@link isDateOnlyColumn}.
461
467
  */
462
468
  const isDateTimeColumn = (column) => is(column, PgTimestamp) || isPgTimestampStringColumn(column) || is(column, MySqlTimestamp) || is(column, MySqlDateTime) || isMySqlDateTimeStringColumn(column) || isSQLiteTimestampColumn(column);
469
+ /**
470
+ * Whether the column holds raw bytes: Postgres `bytea` and SQLite `blob({ mode: 'buffer' })`,
471
+ * both `object buffer` in drizzle v1. Their values are `[Int!]` lists of the bytes.
472
+ */
473
+ const isBufferColumn = (column) => (column.dataType ?? "").endsWith(" buffer");
474
+ /**
475
+ * Whether the column is a float tuple or vector: drizzle v1 gives every such column an
476
+ * `array …` data type — `point` / `line` in tuple mode (`array point`, `array line`), PostGIS
477
+ * `geometry` in tuple mode, pgvector `vector` and `halfvec`. Each value is one list of numbers,
478
+ * typed `[Float!]`, not a database array of elements.
479
+ */
480
+ const isFloatTupleColumn = (column) => (column.dataType ?? "").startsWith("array ");
481
+ /** The fixed length of a float tuple column's values, or `undefined` for a vector. */
482
+ const floatTupleLength = (column) => {
483
+ switch (column.dataType) {
484
+ case "array point":
485
+ case "array geometry": return 2;
486
+ case "array line": return 3;
487
+ default: return;
488
+ }
489
+ };
463
490
  const columnToGraphQLCore = (column, columnName, tableName, isInput) => {
464
491
  const dimensions = column.dimensions;
465
492
  if (dimensions !== void 0 && dimensions > 0) {
@@ -497,7 +524,7 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
497
524
  return column.columnType === "PgGeometryObject" ? {
498
525
  type: isInput ? geoXyInputType : geoXyType,
499
526
  typeLabel: "Geometry points XY"
500
- } : column.columnType === "PgBytea" ? {
527
+ } : isBufferColumn(column) ? {
501
528
  type: new GraphQLList(new GraphQLNonNull(GraphQLInt)),
502
529
  typeLabel: "Buffer"
503
530
  } : {
@@ -530,7 +557,7 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
530
557
  type: GraphQLBigIntString,
531
558
  typeLabel: "BigInt"
532
559
  };
533
- case "number": return constraint !== void 0 && smallIntegerConstraints.has(constraint) || is(column, PgInteger) || is(column, PgSerial) || is(column, MySqlInt) || is(column, MySqlSerial) || is(column, SQLiteInteger) ? {
560
+ case "number": return constraint !== void 0 && smallIntegerConstraints.has(constraint) || is(column, PgInteger) || is(column, PgSerial) || is(column, MySqlInt) || is(column, MySqlSerial) ? {
534
561
  type: GraphQLInt,
535
562
  typeLabel: "Integer"
536
563
  } : {
@@ -538,18 +565,10 @@ const columnBaseToGraphQLCore = (column, columnName, tableName, isInput) => {
538
565
  typeLabel: "Float"
539
566
  };
540
567
  case "array": {
541
- if (column.columnType === "PgVector") return {
542
- type: new GraphQLList(new GraphQLNonNull(GraphQLFloat)),
543
- typeLabel: "Array<Float>"
544
- };
545
- if (column.columnType === "PgGeometry") return {
546
- type: new GraphQLList(new GraphQLNonNull(GraphQLFloat)),
547
- typeLabel: "Tuple<[Float, Float]>"
548
- };
549
- const innerType = columnToGraphQLCore(column.baseColumn, columnName, tableName, isInput);
568
+ const length = floatTupleLength(column);
550
569
  return {
551
- type: new GraphQLList(new GraphQLNonNull(innerType.type)),
552
- typeLabel: `Array<${innerType.typeLabel}>`
570
+ type: new GraphQLList(new GraphQLNonNull(GraphQLFloat)),
571
+ typeLabel: length === void 0 ? "Array<Float>" : `Tuple<[${Array(length).fill("Float").join(", ")}]>`
553
572
  };
554
573
  }
555
574
  default: throw new Error(`Drizzle-GraphQL Error: Type ${column.dataType} is not implemented!`);
@@ -691,6 +710,18 @@ const columnDialect = (column) => {
691
710
  */
692
711
  const isJsonColumn = (column) => (column.dataType ?? "").includes("json") && column.columnType !== "PgGeometryObject";
693
712
  /**
713
+ * SQLite `blob({ mode: 'bigint' })`: the bigint is stored as the bytes of its decimal text, and
714
+ * SQLite compares blobs byte by byte, so `9` sorts above `10`. Equality still holds, so it can be
715
+ * grouped and made distinct on, but not ordered or given a min/max.
716
+ */
717
+ const isSqliteBlobBigIntColumn = (column) => column.columnType === "SQLiteBigInt";
718
+ /**
719
+ * Whether `orderBy` (and so cursor pagination) offers a column. JSON has no meaningful order:
720
+ * PostgreSQL `json` cannot be ordered at all, and MySQL's JSON ordering disagrees with any
721
+ * keyset comparison a cursor could make. A SQLite blob bigint orders as text.
722
+ */
723
+ const isOrderableColumn = (column) => !isJsonColumn(column) && !isSqliteBlobBigIntColumn(column);
724
+ /**
694
725
  * Maps a Drizzle column to the generic filter type to use.
695
726
  *
696
727
  * Selection is keyed on the column's data type, never its name: a text column named
@@ -1197,6 +1228,36 @@ const pgTimestampTextToIso = (value) => {
1197
1228
  const utc = (/* @__PURE__ */ new Date(wallMs - offsetSeconds * 1e3)).toISOString();
1198
1229
  return utc.length === 24 ? `${utc.slice(0, 19)}${fraction}Z` : value;
1199
1230
  };
1231
+ /** Marks a decoder installed by {@link installSqliteBlobJsonDecoders}, so a rebuild never wraps it twice. */
1232
+ const BLOB_JSON_DECODER = Symbol.for("drizzle-graphql.sqliteBlobJsonDecoder");
1233
+ /**
1234
+ * Gives SQLite's blob columns a decoder for the relational query builder's JSON path, so a NULL
1235
+ * there reads back as null.
1236
+ *
1237
+ * Inside a nested relation drizzle selects a blob as `hex(column)` (JSON cannot hold a blob), and
1238
+ * `hex(NULL)` is `''`. drizzle then decodes that `''` with the column's ordinary decoder: a
1239
+ * `blob({ mode: 'bigint' })` reads it as `0n` — indistinguishable from a stored 0 — and a
1240
+ * `blob({ mode: 'json' })` throws on it. drizzle consults a column's `mapFromJsonValue`, when it
1241
+ * has one, instead of `mapFromDriverValue` for exactly this path, so one is installed here that
1242
+ * reads `''` as NULL and hands everything else to the column's own decoder.
1243
+ *
1244
+ * `''` is never a stored bigint or JSON blob, so for those it can only be NULL. For a buffer
1245
+ * blob it is also how an empty blob arrives, so it is read as NULL only when the column is
1246
+ * nullable — a `notNull` column cannot hold the NULL, so there it is the empty blob.
1247
+ */
1248
+ const installSqliteBlobJsonDecoders = (tables) => {
1249
+ for (const table of Object.values(tables)) for (const column of Object.values(getColumns(table))) {
1250
+ const columnType = column.columnType ?? "";
1251
+ const isBuffer = columnType === "SQLiteBlobBuffer";
1252
+ if (columnType !== "SQLiteBigInt" && columnType !== "SQLiteBlobJson" && !isBuffer) continue;
1253
+ const existing = column.mapFromJsonValue;
1254
+ if (existing && !existing[BLOB_JSON_DECODER]) continue;
1255
+ const emptyIsNull = !isBuffer || !column.notNull;
1256
+ const decoder = (value) => value === "" && emptyIsNull ? null : column.mapFromDriverValue(value);
1257
+ decoder[BLOB_JSON_DECODER] = true;
1258
+ column.mapFromJsonValue = decoder;
1259
+ }
1260
+ };
1200
1261
  const remapToGraphQLCore = (key, value, tableName, column, relationMap) => {
1201
1262
  if (Array.isArray(value)) {
1202
1263
  const relations = relationMap?.[tableName];
@@ -1223,14 +1284,14 @@ const remapToGraphQLCore = (key, value, tableName, column, relationMap) => {
1223
1284
  if (value instanceof Date) return value.toISOString();
1224
1285
  if (typeof value === "string" && isMySqlDateTimeStringColumn(column)) return mySqlDateTimeTextToIso(value);
1225
1286
  if (typeof value === "string" && isPgTimestampStringColumn(column)) return pgTimestampTextToIso(value);
1226
- if (value instanceof Buffer) return Array.from(value);
1287
+ if (value instanceof Uint8Array) return Array.from(value);
1227
1288
  if (typeof value === "bigint") return value.toString();
1228
1289
  if (Array.isArray(value)) {
1229
- if (column.columnType === "PgGeometry" || column.columnType === "PgVector") return value;
1290
+ if (isFloatTupleColumn(column)) return value;
1230
1291
  return value.map((arrVal) => remapToGraphQLCore(key, arrVal, tableName, column, relationMap));
1231
1292
  }
1232
1293
  if (typeof value === "object" && value !== null) {
1233
- if (column.columnType === "PgGeometryObject") return value;
1294
+ if ((column.dataType ?? "").startsWith("object")) return value;
1234
1295
  return JSON.stringify(value);
1235
1296
  }
1236
1297
  return value;
@@ -1281,10 +1342,6 @@ const remapToGraphQLSingleOutput = (queryOutput, tableName, table, relationMap)
1281
1342
  continue;
1282
1343
  }
1283
1344
  const column = columns[key];
1284
- if (value === 0n && column && column.columnType === "SQLiteBigInt" && !column.notNull) {
1285
- delete queryOutput[key];
1286
- continue;
1287
- }
1288
1345
  const remapped = remapToGraphQLCore(key, value, tableName, column, relationMap);
1289
1346
  if (column && remapped !== value) keepRawColumnValue(queryOutput, key, value);
1290
1347
  queryOutput[key] = remapped;
@@ -1304,6 +1361,7 @@ const remapFromGraphQLCore = (value, column, columnName) => {
1304
1361
  const dataType = column.dataType ?? "";
1305
1362
  const columnType = column.columnType ?? "";
1306
1363
  if (isDateTimeColumn(column)) {
1364
+ if (Array.isArray(value)) return value.map((element) => remapFromGraphQLCore(element, column, columnName));
1307
1365
  const isMySql = columnType.startsWith("MySql");
1308
1366
  const isPgString = isPgTimestampStringColumn(column);
1309
1367
  const toIso = isMySql ? mySqlDateTimeTextToIso : isPgString ? pgTimestampTextToIso : void 0;
@@ -1318,41 +1376,29 @@ const remapFromGraphQLCore = (value, column, columnName) => {
1318
1376
  if (typeof day !== "string" || !isCalendarDate(day)) throw drizzleError(`Field '${columnName}' is not a valid date! Expected a calendar date as YYYY-MM-DD.`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1319
1377
  return day;
1320
1378
  }
1321
- if (dataType.includes("bigint")) try {
1322
- return BigInt(value);
1323
- } catch {
1324
- throw drizzleError(`Field '${columnName}' is not a BigInt!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1325
- }
1326
- if (dataType.includes("json") && column.columnType !== "PgGeometryObject") return value;
1327
- switch (dataType) {
1328
- case "date": {
1329
- const formatted = new Date(value);
1330
- if (Number.isNaN(formatted.getTime())) throw drizzleError(`Field '${columnName}' is not a valid date!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1331
- return formatted;
1332
- }
1333
- case "buffer":
1334
- if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1335
- return Buffer.from(value);
1336
- case "json":
1337
- if (column.columnType === "PgGeometryObject") return value;
1338
- try {
1339
- return JSON.parse(value);
1340
- } catch (e) {
1341
- throw drizzleError(`Invalid JSON in field '${columnName}':\n${e instanceof Error ? e.message : "Unknown error"}`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1342
- }
1343
- case "array":
1344
- if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1345
- 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" });
1346
- return value;
1347
- case "bigint": try {
1379
+ if (dataType.includes("bigint")) {
1380
+ if (Array.isArray(value)) return value.map((element) => remapFromGraphQLCore(element, column, columnName));
1381
+ try {
1348
1382
  return BigInt(value);
1349
- } catch (_error) {
1383
+ } catch {
1350
1384
  throw drizzleError(`Field '${columnName}' is not a BigInt!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1351
1385
  }
1352
- default:
1353
- if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === null) return Object.assign({}, value);
1354
- return value;
1355
1386
  }
1387
+ if (isJsonColumn(column)) return value;
1388
+ if (isBufferColumn(column)) {
1389
+ if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1390
+ if (column.dimensions > 0 && value.every((element) => element === null || Array.isArray(element))) return value.map((element) => remapFromGraphQLCore(element, column, columnName));
1391
+ if (!value.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255)) throw drizzleError(`Field '${columnName}' is not a list of bytes! Expected integers from 0 to 255.`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1392
+ return Buffer.from(value);
1393
+ }
1394
+ if (isFloatTupleColumn(column)) {
1395
+ if (!Array.isArray(value)) throw drizzleError(`Field '${columnName}' is not an array!`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1396
+ const length = floatTupleLength(column);
1397
+ if (length !== void 0 && value.length !== length) throw drizzleError(`Invalid float tuple in field '${columnName}': expected array with length of ${length}, received ${value.length}`, { code: "DRIZZLE_INVALID_INPUT_VALUE" });
1398
+ return value;
1399
+ }
1400
+ if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === null) return Object.assign({}, value);
1401
+ return value;
1356
1402
  };
1357
1403
  const remapFromGraphQLSingleInput = (queryInput, table, operation = "insert") => {
1358
1404
  const columns = getColumns(table);
@@ -1394,36 +1440,60 @@ const escapeLikeValue = (value) => value.replace(/[\\%_]/g, (char) => `\\${char}
1394
1440
  const safeLikeOps = {
1395
1441
  startsWith: {
1396
1442
  buildPattern: (value) => `${escapeLikeValue(value)}%`,
1443
+ buildGlob: (value) => `${escapeGlobValue(value)}*`,
1397
1444
  insensitive: false
1398
1445
  },
1399
1446
  endsWith: {
1400
1447
  buildPattern: (value) => `%${escapeLikeValue(value)}`,
1448
+ buildGlob: (value) => `*${escapeGlobValue(value)}`,
1401
1449
  insensitive: false
1402
1450
  },
1403
1451
  contains: {
1404
1452
  buildPattern: (value) => `%${escapeLikeValue(value)}%`,
1453
+ buildGlob: (value) => `*${escapeGlobValue(value)}*`,
1405
1454
  insensitive: false
1406
1455
  },
1407
1456
  iStartsWith: {
1408
1457
  buildPattern: (value) => `${escapeLikeValue(value)}%`,
1458
+ buildGlob: (value) => `${escapeGlobValue(value)}*`,
1409
1459
  insensitive: true
1410
1460
  },
1411
1461
  iEndsWith: {
1412
1462
  buildPattern: (value) => `%${escapeLikeValue(value)}`,
1463
+ buildGlob: (value) => `*${escapeGlobValue(value)}`,
1413
1464
  insensitive: true
1414
1465
  },
1415
1466
  iContains: {
1416
1467
  buildPattern: (value) => `%${escapeLikeValue(value)}%`,
1468
+ buildGlob: (value) => `*${escapeGlobValue(value)}*`,
1417
1469
  insensitive: true
1418
1470
  }
1419
1471
  };
1420
1472
  /**
1421
- * `LIKE <pattern> ESCAPE '\'` for a safe string operator. Case-insensitive variants use
1422
- * Postgres's native `ILIKE`; MySQL and SQLite have no `ILIKE`, so they compare `lower()`
1423
- * on both sides instead (mirroring how the raw `ilike` operator is Postgres-only).
1473
+ * Escapes SQLite's `GLOB` wildcards (`*`, `?`, `[`) in a literal search term by wrapping each in
1474
+ * a one-character class, so the term only ever matches literally inside a GLOB pattern. GLOB
1475
+ * has no escape character; `]` outside a class and everything else is already literal.
1476
+ */
1477
+ const escapeGlobValue = (value) => value.replace(/[*?[]/g, (char) => `[${char}]`);
1478
+ /**
1479
+ * A caller-written LIKE pattern as the GLOB pattern matching the same strings, for SQLite's
1480
+ * case-sensitive `like` / `notLike`: `%` → `*`, `_` → `?`, and GLOB's own wildcards escaped.
1481
+ * The raw operators bind no `ESCAPE` clause, so `\` is an ordinary character in both.
1482
+ */
1483
+ const likePatternToGlob = (pattern) => pattern.replace(/[%_*?[]/g, (char) => char === "%" ? "*" : char === "_" ? "?" : `[${char}]`);
1484
+ /**
1485
+ * The safe string operator's predicate. Case-insensitive variants use Postgres's native
1486
+ * `ILIKE`; MySQL and SQLite have no `ILIKE`, so they compare `lower()` on both sides instead
1487
+ * (mirroring how the raw `ilike` operator is Postgres-only).
1488
+ *
1489
+ * The case-sensitive variants are `LIKE <pattern> ESCAPE '\'`, except on SQLite, whose `LIKE`
1490
+ * folds ASCII case — `contains: "App"` would match `apple`. There they are `GLOB`, which is
1491
+ * case-sensitive, as `LIKE` is on Postgres. `dialectColumn` names the column the dialect is read
1492
+ * from when `column` is an expression over it (a JSON path read).
1424
1493
  */
1425
- const safeLikeCondition = (column, pattern, insensitive) => {
1426
- if (!insensitive) return sql`${column} like ${pattern} escape ${LIKE_ESCAPE_CHAR}`;
1494
+ const safeLikeCondition = (column, value, op, insensitive, dialectColumn = column) => {
1495
+ const pattern = op.buildPattern(value);
1496
+ if (!insensitive) return columnDialect(dialectColumn) === "sqlite" ? sql`${column} glob ${op.buildGlob(value)}` : sql`${column} like ${pattern} escape ${LIKE_ESCAPE_CHAR}`;
1427
1497
  return (column.columnType ?? "").startsWith("Pg") ? sql`${column} ilike ${pattern} escape ${LIKE_ESCAPE_CHAR}` : sql`lower(${column}) like ${pattern.toLowerCase()} escape ${LIKE_ESCAPE_CHAR}`;
1428
1498
  };
1429
1499
  /**
@@ -1479,6 +1549,35 @@ const jsonContains = (column, columnName, value) => {
1479
1549
  }
1480
1550
  };
1481
1551
  /**
1552
+ * The nodes of a SQLite JSON document as `(path, kind, value)` rows, for a structural
1553
+ * comparison. Integers and reals share one kind so `1` and `1.0` are the same number, as they
1554
+ * are on the other two dialects.
1555
+ */
1556
+ const sqliteJsonNodes = (document) => sql`select fullkey, case when type in ('integer', 'real') then 'number' else type end, atom from json_tree(${document})`;
1557
+ /**
1558
+ * Whole-document `eq` / `ne` on a JSON column, comparing documents rather than their text:
1559
+ * - Postgres: as `jsonb` (a plain `json` column has no `=` operator at all)
1560
+ * - MySQL: against `CAST(? AS JSON)`; a bare string parameter would be a JSON string scalar
1561
+ * - SQLite: the two documents' node sets must match both ways, so object key order and
1562
+ * whitespace do not matter, while array order still does
1563
+ * A SQL NULL column matches neither, as with `=` / `<>` on any other column.
1564
+ */
1565
+ const jsonEquality = (column, columnName, value, negated) => {
1566
+ const serialized = JSON.stringify(value);
1567
+ switch (columnDialect(column)) {
1568
+ case "pg": {
1569
+ const left = column.columnType === "PgJson" ? sql`${column}::jsonb` : sql`${column}`;
1570
+ return negated ? sql`${left} <> ${serialized}::jsonb` : sql`${left} = ${serialized}::jsonb`;
1571
+ }
1572
+ case "mysql": return negated ? sql`${column} <> cast(${serialized} as json)` : sql`${column} = cast(${serialized} as json)`;
1573
+ case "sqlite": {
1574
+ const equal = sql`(not exists (${sqliteJsonNodes(column)} except ${sqliteJsonNodes(serialized)}) and not exists (${sqliteJsonNodes(serialized)} except ${sqliteJsonNodes(column)}))`;
1575
+ return negated ? sql`(${column} is not null and not ${equal})` : sql`(${column} is not null and ${equal})`;
1576
+ }
1577
+ default: throw drizzleError(`WHERE ${columnName}: JSON equality is not supported for this dialect!`, { code: "DRIZZLE_INVALID_FILTER" });
1578
+ }
1579
+ };
1580
+ /**
1482
1581
  * Constant predicates, used where an operator's operand list is empty and the answer is
1483
1582
  * therefore known without touching the column. Written as `1 = 0` / `1 = 1` rather than the
1484
1583
  * `FALSE` / `TRUE` keywords so they compile identically on all three dialects. Built fresh
@@ -1513,7 +1612,8 @@ const NUMERIC_TEXT_PATTERN = "^\\s*-?(\\d+\\.?\\d*|\\.\\d+)([eE][-+]?\\d+)?\\s*$
1513
1612
  * pattern guard.
1514
1613
  * - **SQLite** — `json_extract` returns the value in SQLite's own type, so the reads cast
1515
1614
  * explicitly: without that, SQLite's cross-type ordering puts every string above every
1516
- * number and `'admin' > 0` would be true.
1615
+ * number and `'admin' > 0` would be true. A JSON boolean comes back as 1 / 0, so every read
1616
+ * also checks `json_type` to keep booleans and numbers apart.
1517
1617
  */
1518
1618
  const jsonPathExprs = (column, path) => {
1519
1619
  switch (columnDialect(column)) {
@@ -1539,10 +1639,11 @@ const jsonPathExprs = (column, path) => {
1539
1639
  }
1540
1640
  default: {
1541
1641
  const extracted = sql`json_extract(${column}, ${jsonPathString(path)})`;
1642
+ const type = sql`json_type(${column}, ${jsonPathString(path)})`;
1542
1643
  return {
1543
- text: sql`cast(${extracted} as text)`,
1544
- number: sql`(case when typeof(${extracted}) in ('integer', 'real') then ${extracted} end)`,
1545
- boolean: extracted,
1644
+ text: sql`(case ${type} when 'true' then 'true' when 'false' then 'false' else cast(${extracted} as text) end)`,
1645
+ number: sql`(case when ${type} in ('integer', 'real') then ${extracted} end)`,
1646
+ boolean: sql`(case when ${type} in ('true', 'false') then ${extracted} end)`,
1546
1647
  encodeBoolean: (value) => value ? 1 : 0
1547
1648
  };
1548
1649
  }
@@ -1581,9 +1682,9 @@ const jsonPathCondition = (column, columnName, filter) => {
1581
1682
  continue;
1582
1683
  }
1583
1684
  if (operatorName in safeLikeOps) {
1584
- const { buildPattern, insensitive } = safeLikeOps[operatorName];
1685
+ const op = safeLikeOps[operatorName];
1585
1686
  if (typeof operatorValue !== "string") throw drizzleError(`WHERE ${locator}: operator '${operatorName}' takes a string`, { code: "DRIZZLE_INVALID_FILTER" });
1586
- variants.push(safeLikeCondition(exprs.text, buildPattern(operatorValue), insensitive));
1687
+ variants.push(safeLikeCondition(exprs.text, operatorValue, op, op.insensitive, column));
1587
1688
  continue;
1588
1689
  }
1589
1690
  const comparison = JSON_PATH_COMPARISONS[operatorName];
@@ -1641,20 +1742,25 @@ const extractFiltersColumn = (column, columnName, operators) => {
1641
1742
  for (const [operatorName, operatorValue] of entries) {
1642
1743
  if (operatorValue === null || operatorValue === void 0) continue;
1643
1744
  if (operatorValue === false && FLAG_OPERATORS.has(operatorName)) continue;
1644
- if (operatorName in singleValueOps) {
1745
+ if ((operatorName === "eq" || operatorName === "ne") && isJsonColumn(column)) variants.push(jsonEquality(column, columnName, operatorValue, operatorName === "ne"));
1746
+ else if (operatorName in singleValueOps) {
1645
1747
  const singleValue = remapFromGraphQLCore(operatorValue, column, columnName);
1646
1748
  variants.push(foldCase ? lowerComparison(column, operatorName, singleValue) : singleValueOps[operatorName](column, singleValue));
1647
1749
  } else if (operatorName in stringValueOps) {
1648
- const insensitivePattern = foldCase && (operatorName === "like" || operatorName === "notLike");
1649
- variants.push(insensitivePattern ? insensitiveLike(column, operatorValue, operatorName === "notLike") : stringValueOps[operatorName](column, operatorValue));
1750
+ const isLike = operatorName === "like" || operatorName === "notLike";
1751
+ if (isLike && foldCase || (operatorName === "ilike" || operatorName === "notIlike") && columnDialect(column) !== "pg") variants.push(insensitiveLike(column, operatorValue, operatorName === "notLike" || operatorName === "notIlike"));
1752
+ else if (isLike && columnDialect(column) === "sqlite") {
1753
+ const glob = likePatternToGlob(operatorValue);
1754
+ variants.push(operatorName === "like" ? sql`${column} glob ${glob}` : sql`${column} not glob ${glob}`);
1755
+ } else variants.push(stringValueOps[operatorName](column, operatorValue));
1650
1756
  } else if (operatorName === "path" && isJsonColumn(column)) for (const pathFilter of operatorValue) {
1651
1757
  const extracted = jsonPathCondition(column, columnName, pathFilter);
1652
1758
  if (extracted) variants.push(extracted);
1653
1759
  }
1654
1760
  else if (operatorName === "contains" && isJsonColumn(column)) variants.push(jsonContains(column, columnName, operatorValue));
1655
1761
  else if (operatorName in safeLikeOps) {
1656
- const { buildPattern, insensitive: alwaysInsensitive } = safeLikeOps[operatorName];
1657
- variants.push(safeLikeCondition(column, buildPattern(operatorValue), alwaysInsensitive || foldCase));
1762
+ const op = safeLikeOps[operatorName];
1763
+ variants.push(safeLikeCondition(column, operatorValue, op, op.insensitive || foldCase));
1658
1764
  } else if (operatorName in arrayValueOps) {
1659
1765
  if (!operatorValue.length) {
1660
1766
  variants.push(operatorName === "inArray" ? sqlFalse() : sqlTrue());
@@ -1662,13 +1768,13 @@ const extractFiltersColumn = (column, columnName, operators) => {
1662
1768
  }
1663
1769
  const arrayValue = operatorValue.map((val) => remapFromGraphQLCore(val, column, columnName));
1664
1770
  variants.push(foldCase ? lowerMembership(column, arrayValue, operatorName === "notInArray") : arrayValueOps[operatorName](column, arrayValue));
1665
- } else if (operatorName === "has") variants.push(arrayContains(column, [operatorValue]));
1771
+ } else if (operatorName === "has") variants.push(arrayContains(column, [remapFromGraphQLCore(operatorValue, column, columnName)]));
1666
1772
  else if (operatorName in arrayMembershipOps) {
1667
1773
  if (!operatorValue.length) {
1668
1774
  variants.push(operatorName === "hasSome" ? sqlFalse() : sqlTrue());
1669
1775
  continue;
1670
1776
  }
1671
- variants.push(arrayMembershipOps[operatorName](column, operatorValue));
1777
+ variants.push(arrayMembershipOps[operatorName](column, operatorValue.map((element) => remapFromGraphQLCore(element, column, columnName))));
1672
1778
  } else if (operatorName === "isEmpty") variants.push(sql`cardinality(${column}) = 0`);
1673
1779
  else if (operatorName in nullableOps) variants.push(nullableOps[operatorName](column));
1674
1780
  else throw drizzleError(`WHERE ${columnName}: Unknown operator: ${operatorName}`, { code: "DRIZZLE_INVALID_FILTER" });
@@ -2516,6 +2622,32 @@ const withScope = (scope, tableName, table, condition, mode) => {
2516
2622
  const predicate = scope?.on(tableName, table, mode);
2517
2623
  return predicate ? and(condition, predicate) : condition;
2518
2624
  };
2625
+ /**
2626
+ * The junction table a `.through()` relation links through, by its schema key — the key its
2627
+ * `scope` and `softDelete` are configured under. `undefined` for a relation with no junction,
2628
+ * or a junction that is not one of the build's tables.
2629
+ */
2630
+ const junctionTableName = (tables, relEntry) => {
2631
+ const throughTable = (relEntry?.relation ?? relEntry)?.throughTable;
2632
+ return throughTable ? Object.keys(tables).find((name) => tables[name] === throughTable) : void 0;
2633
+ };
2634
+ /**
2635
+ * Whether a `.through()` relation's junction restricts which links exist for this request —
2636
+ * a `scope` on the junction table, or a soft delete it declares.
2637
+ */
2638
+ const hasJunctionPolicy = (scope, junctionName) => !!junctionName && !!scope?.has(junctionName, "EXCLUDE");
2639
+ /**
2640
+ * The junction's own row policy, compiled against the junction table as the query names it.
2641
+ * A `.through()` relation links only through the junction rows this keeps, on every path that
2642
+ * reads the relation — the relation field, `<relation>Aggregate`, and a relation filter — so
2643
+ * they all agree on which rows are related.
2644
+ *
2645
+ * The junction is always read at `EXCLUDE`: a marked junction row is a removed link. The
2646
+ * relation's `deleted` argument is about the *target* rows, and the junction's own
2647
+ * `softDelete.scope` only governs reads of the junction table's own fields — a link row has no
2648
+ * field of its own on the relation that `'root'` could keep rendering.
2649
+ */
2650
+ const junctionPolicy = (scope, junctionName, table) => hasJunctionPolicy(scope, junctionName) ? scope.on(junctionName, table, "EXCLUDE") : void 0;
2519
2651
  /** Binds a build's {@link TablePolicies} to its relation context, once, at schema-build time. */
2520
2652
  const bindPolicies = (policies, filterCtx) => policies?.scope || policies?.contextValues || policies?.softDelete || policies?.onWrite || policies?.defaultOrderBy ? {
2521
2653
  scope: policies.scope || policies.softDelete ? (context) => resolveScope(policies, context, filterCtx) : void 0,
@@ -2566,7 +2698,7 @@ const deletedArg = (softDelete, tableName, resolveName) => softDelete?.(tableNam
2566
2698
  const selectArrayArgs = (orderArgs, filterArgs, distinctEnum, softDelete, tableName, resolveName) => ({
2567
2699
  offset: { type: GraphQLInt },
2568
2700
  limit: { type: GraphQLInt },
2569
- orderBy: { type: orderArgs },
2701
+ ...orderArgs ? { orderBy: { type: orderArgs } } : {},
2570
2702
  where: { type: filterArgs },
2571
2703
  after: {
2572
2704
  type: GraphQLString,
@@ -2578,7 +2710,7 @@ const selectArrayArgs = (orderArgs, filterArgs, distinctEnum, softDelete, tableN
2578
2710
  /** GraphQL argument map for a single-row select field (no `limit`). */
2579
2711
  const selectSingleArgs = (orderArgs, filterArgs, softDelete, tableName, resolveName) => ({
2580
2712
  offset: { type: GraphQLInt },
2581
- orderBy: { type: orderArgs },
2713
+ ...orderArgs ? { orderBy: { type: orderArgs } } : {},
2582
2714
  where: { type: filterArgs },
2583
2715
  ...deletedArg(softDelete, tableName, resolveName)
2584
2716
  });
@@ -2628,7 +2760,7 @@ const generateDistinctEnum = (table, typeName, resolveName, tableKey) => {
2628
2760
  defaultName,
2629
2761
  table: tableKey,
2630
2762
  operation: "distinct"
2631
- }) ?? defaultName, `Columns of ${typeName} that a query can be made distinct on`);
2763
+ }) ?? defaultName, `Columns of ${typeName} that a query can be made distinct on`, (column) => !isJsonColumn(column));
2632
2764
  };
2633
2765
  //#endregion
2634
2766
  //#region src/util/builders/common/conflict.ts
@@ -2798,6 +2930,44 @@ const resolveConflictPlan = (params) => {
2798
2930
  plan
2799
2931
  }));
2800
2932
  };
2933
+ /**
2934
+ * Adds a {@link ConflictPlan}'s clause to a PostgreSQL or SQLite insert.
2935
+ *
2936
+ * SQLite needs two things drizzle's builder does not give it when the target is a partial
2937
+ * unique index:
2938
+ *
2939
+ * - The index predicate written literally. SQLite matches a conflict target to an index by
2940
+ * comparing the predicates as written, and a bound `?` never equals `'active'`, so a
2941
+ * predicate that compares to a value would never find its index. Its params are inlined.
2942
+ * - A `DO NOTHING` it can parse. Drizzle writes the predicate after `do nothing`, which is a
2943
+ * syntax error, so the action is spelled as an update that matches no row instead:
2944
+ * `ON CONFLICT (…) WHERE … DO UPDATE SET col = col WHERE 0 = 1`. A row that is not updated is
2945
+ * not returned, which is what `DO NOTHING` does too.
2946
+ */
2947
+ const applyConflictPlan = (query, table, plan, setWhere) => {
2948
+ const sqlite = is(table, SQLiteTable);
2949
+ const targetWhere = sqlite && plan.targetWhere ? sql`${plan.targetWhere}`.inlineParams() : plan.targetWhere;
2950
+ if (plan.action === "UPDATE") return query.onConflictDoUpdate({
2951
+ target: plan.target,
2952
+ targetWhere,
2953
+ set: plan.set,
2954
+ setWhere
2955
+ });
2956
+ if (!plan.target) return query.onConflictDoNothing();
2957
+ if (sqlite && targetWhere) {
2958
+ const [key] = Object.entries(getColumns(table)).find(([, column]) => column === plan.target[0]);
2959
+ return query.onConflictDoUpdate({
2960
+ target: plan.target,
2961
+ targetWhere,
2962
+ set: { [key]: sql`${plan.target[0]}` },
2963
+ setWhere: sql`0 = 1`
2964
+ });
2965
+ }
2966
+ return query.onConflictDoNothing({
2967
+ target: plan.target,
2968
+ where: targetWhere
2969
+ });
2970
+ };
2801
2971
  /** `excluded.<column>` — PostgreSQL and SQLite name the rejected row this way. */
2802
2972
  const excludedColumnRef = (columnName) => sql`excluded.${sql.identifier(columnName)}`;
2803
2973
  /** `values(<column>)` — MySQL's equivalent inside ON DUPLICATE KEY UPDATE. */
@@ -3079,6 +3249,21 @@ const decodeCursor = (after, entries) => {
3079
3249
  return payload.v.map(decodeCursorValue);
3080
3250
  };
3081
3251
  /**
3252
+ * `col > value` (asc) or `col < value` (desc), compared by the rule the ORDER BY sorts by.
3253
+ * MySQL sorts an ENUM by its declaration index but compares it with a string as a string, so
3254
+ * there the comparison is between `col + 0` (the 1-based index) and the value's own index.
3255
+ */
3256
+ const strictComparison = (col, direction, value) => {
3257
+ if (is(col, MySqlEnumColumn) || is(col, MySqlEnumObjectColumn)) {
3258
+ const index = col.enumValues?.indexOf(value) ?? -1;
3259
+ if (index >= 0) {
3260
+ const position = sql`(${col} + 0)`;
3261
+ return direction === "asc" ? sql`${position} > ${index + 1}` : sql`${position} < ${index + 1}`;
3262
+ }
3263
+ }
3264
+ return direction === "asc" ? gt(col, value) : lt(col, value);
3265
+ };
3266
+ /**
3082
3267
  * The keyset predicate selecting rows strictly after the cursor position in the given total
3083
3268
  * order — the expanded lexicographic form
3084
3269
  * `after(k0) OR (k0 = v0 AND after(k1)) OR (k0 = v0 AND k1 = v1 AND after(k2)) …`,
@@ -3103,7 +3288,7 @@ const buildCursorCondition = (table, entries, values, nullOrdering) => {
3103
3288
  let strictlyAfter;
3104
3289
  if (value === null || value === void 0) strictlyAfter = nullsSortLast ? void 0 : isNotNull(col);
3105
3290
  else {
3106
- const comparison = direction === "asc" ? gt(col, value) : lt(col, value);
3291
+ const comparison = strictComparison(col, direction, value);
3107
3292
  strictlyAfter = nullsSortLast ? or(comparison, isNull(col)) : comparison;
3108
3293
  }
3109
3294
  if (strictlyAfter) disjuncts.push(equalities.length ? and(...equalities, strictlyAfter) : strictlyAfter);
@@ -3306,6 +3491,115 @@ const getUniqueTargets = (table, getTableConfig) => {
3306
3491
  */
3307
3492
  const getUniqueColumnSets = (table, getTableConfig, options = {}) => getUniqueTargets(table, getTableConfig).filter((target) => options.partial || !target.where).map((target) => target.columns);
3308
3493
  //#endregion
3494
+ //#region src/util/builders/common/limits.ts
3495
+ /**
3496
+ * The `limit` to send with an `offset` the request gave without one. SQLite and MySQL accept
3497
+ * `OFFSET` only after a `LIMIT`, so `offset` alone has to carry a limit that means "all of
3498
+ * them": SQLite reads `LIMIT -1` that way, and on MySQL it is the largest integer every driver
3499
+ * binds exactly (MySQL's own documented 2^64-1 does not fit a JS number). PostgreSQL takes
3500
+ * `OFFSET` alone, so the limit is left as it was.
3501
+ */
3502
+ const offsetOnlyLimit = (table, limit, offset) => {
3503
+ if (limit != null || !offset) return limit;
3504
+ const firstColumn = Object.values(getColumns(table))[0];
3505
+ switch (firstColumn ? columnDialect(firstColumn) : void 0) {
3506
+ case "sqlite": return -1;
3507
+ case "mysql": return Number.MAX_SAFE_INTEGER;
3508
+ default: return limit;
3509
+ }
3510
+ };
3511
+ /**
3512
+ * Substitutes a table's default ordering for an absent `orderBy`.
3513
+ *
3514
+ * Applied where the arguments are first read, so everything downstream — the cursor tuple, a
3515
+ * `distinct` pass, the plain-select fallback — sees one effective ordering and cannot
3516
+ * disagree about it. Only a missing argument is replaced: `orderBy: {}` is a request for no
3517
+ * ordering, and stays one.
3518
+ */
3519
+ const withDefaultOrderBy = (args, tableName, defaults) => {
3520
+ if (!defaults || args?.orderBy != null) return args;
3521
+ const fallback = defaults(tableName);
3522
+ return fallback ? {
3523
+ ...args,
3524
+ orderBy: fallback
3525
+ } : args;
3526
+ };
3527
+ /**
3528
+ * Rejects a negative `limit` or `offset`. Neither is a page size or a position, and the
3529
+ * dialects disagree on what one means: SQLite reads `LIMIT -1` as "no limit" — which would walk
3530
+ * straight past `maxLimit` — while PostgreSQL and MySQL fail with a raw database error. Applied
3531
+ * whether or not the table has a limit policy.
3532
+ */
3533
+ const assertPagingNotNegative = (paging, errorCtx) => {
3534
+ for (const name of ["limit", "offset"]) {
3535
+ const value = paging[name];
3536
+ if (value != null && value < 0) throw drizzleError(`'${name}' must not be negative, got ${value}.`, {
3537
+ code: "DRIZZLE_INVALID_INPUT_VALUE",
3538
+ ...errorCtx
3539
+ });
3540
+ }
3541
+ };
3542
+ /**
3543
+ * The `limit` a query actually runs with, given the policy for the table it reads.
3544
+ *
3545
+ * A request that passes no `limit` falls back to `defaultLimit`, or to `maxLimit` when only
3546
+ * that is configured — an absent limit means every row, which is above any maximum. A limit
3547
+ * the client did ask for is rejected when it exceeds `maxLimit`, since silently truncating a
3548
+ * page tells a paginating client it has reached the end when it has not; `clampToMax` opts
3549
+ * into the truncation instead. A limit the *policy* supplied is capped rather than rejected —
3550
+ * a misconfigured `defaultLimit` above `maxLimit` is the operator's problem, not something to
3551
+ * fail a client's query over.
3552
+ */
3553
+ const applyLimitPolicy = (limit, policy, errorCtx) => {
3554
+ assertPagingNotNegative({ limit }, errorCtx);
3555
+ if (!policy) return limit ?? void 0;
3556
+ const { defaultLimit, maxLimit, clampToMax } = policy;
3557
+ if (limit == null) {
3558
+ const fallback = defaultLimit ?? maxLimit;
3559
+ if (fallback === void 0) return;
3560
+ return maxLimit === void 0 ? fallback : Math.min(fallback, maxLimit);
3561
+ }
3562
+ if (maxLimit !== void 0 && limit > maxLimit) {
3563
+ if (clampToMax) return maxLimit;
3564
+ throw drizzleError(`'limit' of ${limit} exceeds the maximum of ${maxLimit}.`, {
3565
+ code: "DRIZZLE_LIMIT_EXCEEDED",
3566
+ ...errorCtx
3567
+ });
3568
+ }
3569
+ return limit;
3570
+ };
3571
+ /** Merges a per-table override over the global policy, or `undefined` when neither bounds anything. */
3572
+ const resolveLimitPolicy = (global, table) => {
3573
+ const defaultLimit = table?.defaultLimit ?? global?.defaultLimit;
3574
+ const maxLimit = table?.maxLimit ?? global?.maxLimit;
3575
+ if (defaultLimit === void 0 && maxLimit === void 0) return;
3576
+ return {
3577
+ defaultLimit,
3578
+ maxLimit,
3579
+ clampToMax: table?.clampToMax ?? global?.clampToMax ?? false
3580
+ };
3581
+ };
3582
+ /**
3583
+ * A paginated field costs its page size times whatever one row of it costs, so `users(limit: 100)
3584
+ * { posts(limit: 10) { id } }` is charged for the thousand rows it can return rather than the two
3585
+ * fields it mentions. `childComplexity` floors at 1 so a row is never free.
3586
+ *
3587
+ * The page size is the *effective* one: a request without `limit` is priced at the policy's
3588
+ * `defaultLimit` (or its `maxLimit` when that is all it sets) rather than the estimator's guess, and no request is priced above `maxLimit`,
3589
+ * so the cost a query is charged matches the rows it can actually pull.
3590
+ */
3591
+ const listFieldComplexity = (options, policy) => ({ args, childComplexity }) => {
3592
+ const limit = args["limit"];
3593
+ let rows = (typeof limit === "number" && limit > 0 ? limit : void 0) ?? policy?.defaultLimit ?? policy?.maxLimit ?? options.defaultListSize;
3594
+ if (policy?.maxLimit !== void 0) rows = Math.min(rows, policy.maxLimit);
3595
+ return rows * Math.max(childComplexity, 1);
3596
+ };
3597
+ /**
3598
+ * An aggregate returns a single row but reads however many match, so its cost tracks the scan
3599
+ * rather than the response.
3600
+ */
3601
+ const aggregateFieldComplexity = (options) => ({ childComplexity }) => options.aggregateCost + childComplexity;
3602
+ //#endregion
3309
3603
  //#region src/util/builders/common/distinct.ts
3310
3604
  /**
3311
3605
  * Alias the parent key comes back under when the pass runs through a junction table. See
@@ -3353,13 +3647,15 @@ const selectDistinctKeys = async (params) => {
3353
3647
  ...junction ? { [DISTINCT_PARENT]: sub[DISTINCT_PARENT] } : {}
3354
3648
  }).from(sub).where(eq(sub[DISTINCT_RN], 1)).orderBy(...outerOrder);
3355
3649
  if (offset) query = query.offset(offset);
3356
- if (limit != null) query = query.limit(limit);
3650
+ const effectiveLimit = offsetOnlyLimit(table, limit, offset);
3651
+ if (effectiveLimit != null) query = query.limit(effectiveLimit < 0 ? sql`${effectiveLimit}` : effectiveLimit);
3357
3652
  return await query;
3358
3653
  };
3359
3654
  /**
3360
3655
  * Condition matching exactly the rows identified by `keys` — an `IN (…)` for a single-column
3361
- * primary key, an `OR` of per-row equality for a composite one. `table` may be the aliased
3362
- * RQB proxy.
3656
+ * primary key, an `OR` of per-row equality for a composite one — nested as a balanced tree, so
3657
+ * thousands of keys stay within SQLite's expression-depth limit. `table` may be the aliased RQB
3658
+ * proxy.
3363
3659
  */
3364
3660
  const primaryKeyRestriction = (table, pkNames, keys) => {
3365
3661
  const cols = getColumns(table);
@@ -3367,7 +3663,7 @@ const primaryKeyRestriction = (table, pkNames, keys) => {
3367
3663
  const name = pkNames[0];
3368
3664
  return inArray(cols[name], keys.map((key) => key[name]));
3369
3665
  }
3370
- return or(...keys.map((key) => and(...pkNames.map((name) => eq(cols[name], key[name])))));
3666
+ return balancedOr(keys.map((key) => and(...pkNames.map((name) => eq(cols[name], key[name])))));
3371
3667
  };
3372
3668
  //#endregion
3373
3669
  //#region src/util/builders/common/docs.ts
@@ -3513,99 +3809,6 @@ const innerOrderType = (typeName) => sharedType(typeName, {
3513
3809
  }
3514
3810
  }));
3515
3811
  //#endregion
3516
- //#region src/util/builders/common/limits.ts
3517
- /**
3518
- * Substitutes a table's default ordering for an absent `orderBy`.
3519
- *
3520
- * Applied where the arguments are first read, so everything downstream — the cursor tuple, a
3521
- * `distinct` pass, the plain-select fallback — sees one effective ordering and cannot
3522
- * disagree about it. Only a missing argument is replaced: `orderBy: {}` is a request for no
3523
- * ordering, and stays one.
3524
- */
3525
- const withDefaultOrderBy = (args, tableName, defaults) => {
3526
- if (!defaults || args?.orderBy != null) return args;
3527
- const fallback = defaults(tableName);
3528
- return fallback ? {
3529
- ...args,
3530
- orderBy: fallback
3531
- } : args;
3532
- };
3533
- /**
3534
- * Rejects a negative `limit` or `offset`. Neither is a page size or a position, and the
3535
- * dialects disagree on what one means: SQLite reads `LIMIT -1` as "no limit" — which would walk
3536
- * straight past `maxLimit` — while PostgreSQL and MySQL fail with a raw database error. Applied
3537
- * whether or not the table has a limit policy.
3538
- */
3539
- const assertPagingNotNegative = (paging, errorCtx) => {
3540
- for (const name of ["limit", "offset"]) {
3541
- const value = paging[name];
3542
- if (value != null && value < 0) throw drizzleError(`'${name}' must not be negative, got ${value}.`, {
3543
- code: "DRIZZLE_INVALID_INPUT_VALUE",
3544
- ...errorCtx
3545
- });
3546
- }
3547
- };
3548
- /**
3549
- * The `limit` a query actually runs with, given the policy for the table it reads.
3550
- *
3551
- * A request that passes no `limit` falls back to `defaultLimit`, or to `maxLimit` when only
3552
- * that is configured — an absent limit means every row, which is above any maximum. A limit
3553
- * the client did ask for is rejected when it exceeds `maxLimit`, since silently truncating a
3554
- * page tells a paginating client it has reached the end when it has not; `clampToMax` opts
3555
- * into the truncation instead. A limit the *policy* supplied is capped rather than rejected —
3556
- * a misconfigured `defaultLimit` above `maxLimit` is the operator's problem, not something to
3557
- * fail a client's query over.
3558
- */
3559
- const applyLimitPolicy = (limit, policy, errorCtx) => {
3560
- assertPagingNotNegative({ limit }, errorCtx);
3561
- if (!policy) return limit ?? void 0;
3562
- const { defaultLimit, maxLimit, clampToMax } = policy;
3563
- if (limit == null) {
3564
- const fallback = defaultLimit ?? maxLimit;
3565
- if (fallback === void 0) return;
3566
- return maxLimit === void 0 ? fallback : Math.min(fallback, maxLimit);
3567
- }
3568
- if (maxLimit !== void 0 && limit > maxLimit) {
3569
- if (clampToMax) return maxLimit;
3570
- throw drizzleError(`'limit' of ${limit} exceeds the maximum of ${maxLimit}.`, {
3571
- code: "DRIZZLE_LIMIT_EXCEEDED",
3572
- ...errorCtx
3573
- });
3574
- }
3575
- return limit;
3576
- };
3577
- /** Merges a per-table override over the global policy, or `undefined` when neither bounds anything. */
3578
- const resolveLimitPolicy = (global, table) => {
3579
- const defaultLimit = table?.defaultLimit ?? global?.defaultLimit;
3580
- const maxLimit = table?.maxLimit ?? global?.maxLimit;
3581
- if (defaultLimit === void 0 && maxLimit === void 0) return;
3582
- return {
3583
- defaultLimit,
3584
- maxLimit,
3585
- clampToMax: table?.clampToMax ?? global?.clampToMax ?? false
3586
- };
3587
- };
3588
- /**
3589
- * A paginated field costs its page size times whatever one row of it costs, so `users(limit: 100)
3590
- * { posts(limit: 10) { id } }` is charged for the thousand rows it can return rather than the two
3591
- * fields it mentions. `childComplexity` floors at 1 so a row is never free.
3592
- *
3593
- * The page size is the *effective* one: a request without `limit` is priced at the policy's
3594
- * `defaultLimit` rather than the estimator's guess, and no request is priced above `maxLimit`,
3595
- * so the cost a query is charged matches the rows it can actually pull.
3596
- */
3597
- const listFieldComplexity = (options, policy) => ({ args, childComplexity }) => {
3598
- const limit = args["limit"];
3599
- let rows = (typeof limit === "number" && limit > 0 ? limit : void 0) ?? policy?.defaultLimit ?? options.defaultListSize;
3600
- if (policy?.maxLimit !== void 0) rows = Math.min(rows, policy.maxLimit);
3601
- return rows * Math.max(childComplexity, 1);
3602
- };
3603
- /**
3604
- * An aggregate returns a single row but reads however many match, so its cost tracks the scan
3605
- * rather than the response.
3606
- */
3607
- const aggregateFieldComplexity = (options) => ({ childComplexity }) => options.aggregateCost + childComplexity;
3608
- //#endregion
3609
3812
  //#region src/util/parse-resolve-info.ts
3610
3813
  /** Unwraps `[T]!`-style wrappers down to the named type. `getNamedType` without the realm tie. */
3611
3814
  const namedTypeOf = (type) => {
@@ -3769,14 +3972,10 @@ const NUMERIC_SCALARS = /* @__PURE__ */ new Set([
3769
3972
  ]);
3770
3973
  /**
3771
3974
  * Column types that convert to a GraphQL list without being a list *in the database* —
3772
- * a bytea reads as `[Int]`, a vector and a geometry point as `[Float]` — so appending to
3773
- * them is not the operation `push` means.
3975
+ * a bytea or SQLite buffer blob reads as `[Int]`, a vector, a point or a line as `[Float]` —
3976
+ * so appending to them is not the operation `push` means.
3774
3977
  */
3775
- const NON_LIST_ARRAY_COLUMNS = /* @__PURE__ */ new Set([
3776
- "PgVector",
3777
- "PgGeometry",
3778
- "PgBytea"
3779
- ]);
3978
+ const isNonListArrayColumn = (column) => isFloatTupleColumn(column) || isBufferColumn(column);
3780
3979
  const kindCache = /* @__PURE__ */ new WeakMap();
3781
3980
  /**
3782
3981
  * Whether a column takes an operations input, and which one.
@@ -3793,7 +3992,7 @@ const fieldUpdateKind = (column, columnName, tableName) => {
3793
3992
  if (!getColumnScalarOverride(column, true)) {
3794
3993
  const { type } = drizzleColumnToGraphQLType(column, columnName, tableName, true, false, true);
3795
3994
  if (NUMERIC_SCALARS.has(type)) kind = "numeric";
3796
- else if (type instanceof GraphQLList && !NON_LIST_ARRAY_COLUMNS.has(column.columnType)) kind = "list";
3995
+ else if (type instanceof GraphQLList && !isNonListArrayColumn(column)) kind = "list";
3797
3996
  }
3798
3997
  kindCache.set(column, kind);
3799
3998
  return kind;
@@ -4064,6 +4263,16 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
4064
4263
  if (!relFieldSelection) continue;
4065
4264
  const eagerArgs = field.args;
4066
4265
  if (!is(relEntry.relation ?? relEntry, One) && (eagerArgs?.after != null || !!(eagerArgs?.distinct)?.length || isCursorFieldSelected(relFieldSelection, tables[targetTableName]))) continue;
4266
+ const junctionName = junctionTableName(tables, relEntry);
4267
+ if (hasJunctionPolicy(scope, junctionName)) {
4268
+ if (!extractJunctionColumns(relEntry)) throw drizzleError(`Relation '${tableName}.${relName}' goes through '${junctionName}', which has a scope or soft delete, over a composite key. Its links can only be filtered when the junction joins on one column per side.`, {
4269
+ code: "DRIZZLE_UNSUPPORTED_RELATION",
4270
+ table: tableName,
4271
+ operation: "relation",
4272
+ relation: relName
4273
+ });
4274
+ continue;
4275
+ }
4067
4276
  const columns = extractSelectedColumnsFromTree(relFieldSelection, tables[targetTableName], {
4068
4277
  tableName: targetTableName,
4069
4278
  relationMap,
@@ -4089,9 +4298,11 @@ const extractRelationsParams = (relationMap, tables, tableName, info, typeName,
4089
4298
  thisRecord.where = relWhere || scope?.has(targetTableName, relDeleted) ? { RAW: (aliasedTable) => withScope(scope, targetTableName, aliasedTable, relWhere ? extractFilters(aliasedTable, relName, relWhere, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0, relDeleted) } : void 0;
4090
4299
  const hasPagination = offset != null || limit != null;
4091
4300
  const pkNames = targetPkNames ?? [];
4092
- thisRecord.orderBy = relationArgs?.orderBy ? (aliasedTable) => extractOrderBy(aliasedTable, relationArgs.orderBy, relationFilterCtx(filterCtx, targetTableName, scope), relWhere) : hasPagination && pkNames.length ? (aliasedTable) => primaryKeyOrderExprs(aliasedTable, pkNames) : void 0;
4301
+ const tiebreak = hasPagination && pkNames.length > 0;
4302
+ const requestOrder = relationArgs?.orderBy;
4303
+ thisRecord.orderBy = requestOrder || tiebreak ? (aliasedTable) => [...requestOrder ? extractOrderBy(aliasedTable, requestOrder, relationFilterCtx(filterCtx, targetTableName, scope), relWhere) : [], ...tiebreak ? primaryKeyOrderExprs(aliasedTable, pkNames) : []] : void 0;
4093
4304
  thisRecord.offset = offset;
4094
- thisRecord.limit = limit;
4305
+ thisRecord.limit = offsetOnlyLimit(tables[targetTableName], limit, offset);
4095
4306
  thisRecord.with = extractRelationsParams(relationMap, tables, targetTableName, field, relTypeName, options);
4096
4307
  args[relName] = omitUndefinedKeys(thisRecord);
4097
4308
  }
@@ -4182,7 +4393,7 @@ const runRelationalSelect = async (opts) => {
4182
4393
  const preciseProps = Object.keys(cursorPreciseExtras(table, cursorEntries));
4183
4394
  if (preciseProps.length) params.extras = Object.fromEntries(preciseProps.map((prop) => [prop, (aliasedTable) => cursorPreciseExtras(aliasedTable, cursorEntries)[prop]]));
4184
4395
  }
4185
- params.limit = distinctKeys ? void 0 : opts.limit;
4396
+ params.limit = distinctKeys ? void 0 : offsetOnlyLimit(table, opts.limit, offset);
4186
4397
  const result = await queryBase.findMany(omitUndefinedKeys(params));
4187
4398
  if (cursorEntries && cursorSelected) attachRowCursors(result, cursorEntries);
4188
4399
  return remapToGraphQLArrayOutput(result, tableName, table, relationMap);
@@ -4239,6 +4450,8 @@ const eagerLoadMutationRelations = async (db, tableName, rows, pkNames, withPara
4239
4450
  }
4240
4451
  return rows;
4241
4452
  };
4453
+ /** The longest delay `setTimeout` honours; anything above, Infinity included, fires after 1 ms. */
4454
+ const MAX_TIMER_MS = 2 ** 31 - 1;
4242
4455
  const createMutationTxCtx = (options) => options ? {
4243
4456
  timeoutMs: options.timeoutMs,
4244
4457
  fieldNames: /* @__PURE__ */ new Set(),
@@ -4265,28 +4478,28 @@ const includedByDirectives = (node, variables) => {
4265
4478
  * this build did not generate — its completion cannot be tracked, so the caller must not
4266
4479
  * open a transaction it might never be able to close.
4267
4480
  */
4268
- const collectRootMutationKeys = (selectionSet, fragments, variables, known, keys, visitedFragments) => {
4481
+ const collectRootMutationKeys = (selectionSet, fragments, variables, known, keys, visitedFragments, argumentsCoerce) => {
4269
4482
  for (const selection of selectionSet.selections) {
4270
4483
  if (!includedByDirectives(selection, variables)) continue;
4271
4484
  if (selection.kind === "Field") {
4272
4485
  const name = selection.name.value;
4273
4486
  if (name === "__typename") continue;
4274
4487
  if (!known.has(name)) return false;
4275
- keys.add(selection.alias?.value ?? name);
4488
+ if (argumentsCoerce(selection)) keys.add(selection.alias?.value ?? name);
4276
4489
  } else if (selection.kind === "InlineFragment") {
4277
- if (!collectRootMutationKeys(selection.selectionSet, fragments, variables, known, keys, visitedFragments)) return false;
4490
+ if (!collectRootMutationKeys(selection.selectionSet, fragments, variables, known, keys, visitedFragments, argumentsCoerce)) return false;
4278
4491
  } else if (selection.kind === "FragmentSpread") {
4279
4492
  const fragmentName = selection.name.value;
4280
4493
  if (visitedFragments.has(fragmentName)) continue;
4281
4494
  visitedFragments.add(fragmentName);
4282
4495
  const fragment = fragments?.[fragmentName];
4283
4496
  if (!fragment) return false;
4284
- if (!collectRootMutationKeys(fragment.selectionSet, fragments, variables, known, keys, visitedFragments)) return false;
4497
+ if (!collectRootMutationKeys(fragment.selectionSet, fragments, variables, known, keys, visitedFragments, argumentsCoerce)) return false;
4285
4498
  }
4286
4499
  }
4287
4500
  return true;
4288
4501
  };
4289
- const abortedBatchError = () => drizzleError("Drizzle-GraphQL Error: Mutation was not executed because an earlier mutation in the same request failed and the shared transaction was rolled back.", { code: "DRIZZLE_TRANSACTION_ABORTED" });
4502
+ const abortedBatchError = (state, timeoutMs) => drizzleError(state.timedOut ? `Drizzle-GraphQL Error: Mutation was not executed because the shared transaction timed out after ${timeoutMs}ms waiting for the remaining mutation fields and was rolled back.` : "Drizzle-GraphQL Error: Mutation was not executed because an earlier mutation in the same request failed and the shared transaction was rolled back.", { code: "DRIZZLE_TRANSACTION_ABORTED" });
4290
4503
  /** Opens the request-wide transaction and wires up latch, inactivity timer and cleanup. */
4291
4504
  const openSharedTx = (db, context, txCtx, stateKey, expected) => {
4292
4505
  let release;
@@ -4321,6 +4534,7 @@ const openSharedTx = (db, context, txCtx, stateKey, expected) => {
4321
4534
  completed: 0,
4322
4535
  failure: void 0,
4323
4536
  aborted: false,
4537
+ timedOut: false,
4324
4538
  done: false,
4325
4539
  ready,
4326
4540
  settled,
@@ -4335,8 +4549,9 @@ const openSharedTx = (db, context, txCtx, stateKey, expected) => {
4335
4549
  suspendTimer: clearTimer,
4336
4550
  bumpTimer: () => {
4337
4551
  clearTimer();
4338
- if (state.done) return;
4552
+ if (state.done || txCtx.timeoutMs === Number.POSITIVE_INFINITY) return;
4339
4553
  timer = setTimeout(() => {
4554
+ if (!state.aborted) state.timedOut = true;
4340
4555
  state.abort(drizzleError(`Drizzle-GraphQL Error: Shared mutation transaction timed out after ${txCtx.timeoutMs}ms waiting for the remaining mutation fields; rolling back.`, { code: "DRIZZLE_TRANSACTION_ABORTED" }));
4341
4556
  }, txCtx.timeoutMs);
4342
4557
  timer.unref?.();
@@ -4355,15 +4570,15 @@ const openSharedTx = (db, context, txCtx, stateKey, expected) => {
4355
4570
  state.bumpTimer();
4356
4571
  return state;
4357
4572
  };
4358
- const runOnSharedTx = async (state, body) => {
4573
+ const runOnSharedTx = async (state, timeoutMs, body) => {
4359
4574
  if (state.aborted) {
4360
4575
  await state.settled.catch(() => {});
4361
- throw abortedBatchError();
4576
+ throw abortedBatchError(state, timeoutMs);
4362
4577
  }
4363
4578
  state.suspendTimer();
4364
4579
  const tx = await state.ready;
4365
4580
  try {
4366
- if (state.aborted) throw abortedBatchError();
4581
+ if (state.aborted) throw abortedBatchError(state, timeoutMs);
4367
4582
  const result = await body(tx);
4368
4583
  state.completed += 1;
4369
4584
  if (state.completed >= state.expected) {
@@ -4396,14 +4611,24 @@ const runMutation = async (db, context, info, txCtx, body, forceTx) => {
4396
4611
  if (contextIsObject && context[drizzleExecutorKey]) return body(context[drizzleExecutorKey]);
4397
4612
  if (!txCtx || info.operation.operation !== "mutation") return forceTx ? db.transaction((tx) => body(tx)) : body(db);
4398
4613
  const keys = /* @__PURE__ */ new Set();
4399
- state = !collectRootMutationKeys(info.operation.selectionSet, info.fragments, info.variableValues, txCtx.fieldNames, keys, /* @__PURE__ */ new Set()) || keys.size <= 1 ? { kind: "passthrough" } : openSharedTx(db, context, txCtx, stateKey, keys.size);
4614
+ const mutationFields = info.schema.getMutationType()?.getFields() ?? {};
4615
+ state = !collectRootMutationKeys(info.operation.selectionSet, info.fragments, info.variableValues, txCtx.fieldNames, keys, /* @__PURE__ */ new Set(), (node) => {
4616
+ const fieldDef = mutationFields[node.name.value];
4617
+ if (!fieldDef) return false;
4618
+ try {
4619
+ getArgumentValues(fieldDef, node, info.variableValues);
4620
+ return true;
4621
+ } catch {
4622
+ return false;
4623
+ }
4624
+ }) || keys.size <= 1 ? { kind: "passthrough" } : openSharedTx(db, context, txCtx, stateKey, keys.size);
4400
4625
  txCtx.states.set(stateKey, state);
4401
4626
  }
4402
4627
  if (state.kind === "passthrough") {
4403
4628
  const executor = resolveExecutor(db, context);
4404
4629
  return forceTx && executor === db ? db.transaction((tx) => body(tx)) : body(executor);
4405
4630
  }
4406
- return runOnSharedTx(state, body);
4631
+ return runOnSharedTx(state, txCtx.timeoutMs, body);
4407
4632
  };
4408
4633
  //#endregion
4409
4634
  //#region src/util/builders/common/write-hooks.ts
@@ -4557,10 +4782,11 @@ const hardDeleteArg = {
4557
4782
  description: "Remove the matched rows instead of marking them deleted. Reads at `INCLUDE`, so it reaches rows that are already marked; a `scope` still confines what it can reach."
4558
4783
  };
4559
4784
  const prepareMutationRelationColumns = (params) => {
4560
- const { relationMap, tables, tableName, typeNameMapper, table, pkNames, parsedInfo, resolveName } = params;
4785
+ const { relationMap, filterCtx, tables, tableName, typeNameMapper, table, pkNames, parsedInfo, resolveName } = params;
4561
4786
  const typeName = resolveObjectTypeName(tableName, typeNameMapper, resolveName);
4562
4787
  const withParams = relationMap[tableName] ? extractRelationsParams(relationMap, tables, tableName, parsedInfo, typeName, {
4563
4788
  typeNameMapper,
4789
+ filterCtx,
4564
4790
  limits: params.limits,
4565
4791
  scope: params.scope,
4566
4792
  defaultOrderBy: params.defaultOrderBy,
@@ -4570,7 +4796,8 @@ const prepareMutationRelationColumns = (params) => {
4570
4796
  const baseColumns = extractSelectedColumnsFromTreeSQLFormat(parsedInfo.fieldsByTypeName[typeName], table, {
4571
4797
  tableName,
4572
4798
  relationMap,
4573
- tables
4799
+ tables,
4800
+ allRelations: filterCtx?.relationMap
4574
4801
  });
4575
4802
  return {
4576
4803
  columns: hasRelations ? withPrimaryKeyColumns(baseColumns, table, pkNames) : baseColumns,
@@ -4706,7 +4933,7 @@ const assertSingleMatch = async (executor, table, filters) => {
4706
4933
  * MySQL (no `RETURNING`; its hooks see `rows: []` as on every MySQL mutation), skips the
4707
4934
  * `RETURNING` and reads the count off the driver's result via {@link rowsAffected}.
4708
4935
  */
4709
- const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, fieldName, requireWhere, filterCtx, txCtx, nested, policies, pkNames = [], returnsRows }) => {
4936
+ const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, fieldName, requireWhere, filterCtx, txCtx, nested, policies, pkNames = [], returnsRows, writeFilter }) => {
4710
4937
  const softDelete = kind === "delete" ? policies?.softDelete?.(tableName) : void 0;
4711
4938
  const canHardDelete = softDelete?.hardDelete === true;
4712
4939
  const queryArgs = {
@@ -4749,7 +4976,7 @@ const generateWriteCount = ({ db, tableName, table, kind, setArgs, filterArgs, f
4749
4976
  required: requireWhere,
4750
4977
  deleted: hard ? "INCLUDE" : void 0
4751
4978
  });
4752
- if (filters) query = query.where(filters);
4979
+ if (filters) query = query.where(writeFilter ? writeFilter(filters) : filters);
4753
4980
  if (!readsRows) {
4754
4981
  const result = await query;
4755
4982
  await after();
@@ -4809,27 +5036,40 @@ const getOrCreateLoader = (context, key, batchFn) => {
4809
5036
  if (!loaders.has(key)) loaders.set(key, new BatchLoader(batchFn));
4810
5037
  return loaders.get(key);
4811
5038
  };
5039
+ const requestCacheNode = () => ({
5040
+ children: /* @__PURE__ */ new WeakMap(),
5041
+ entries: /* @__PURE__ */ new Map()
5042
+ });
4812
5043
  /**
4813
5044
  * Memoizes a value that a resolver derives from its own field — its args and its selection —
4814
5045
  * for the length of one request. Resolvers run once per parent row, so anything derived only
4815
5046
  * from the field is otherwise recomputed for every row of a batch.
4816
5047
  *
4817
- * The cache lives on the context (weakly keyed by the AST node the value is derived from, so a
4818
- * document cached across requests never carries values between them) and `key` separates fields
4819
- * that share a node: one fragment selection can resolve for more than one parent type. Without a
4820
- * context object there is nowhere request-scoped to cache, so the value is simply computed.
5048
+ * The value is keyed by every AST node of the field — `info.fieldNodes`, the nodes graphql-js
5049
+ * merged into this one response field — not only the first. A fragment's field node is the first
5050
+ * node wherever the fragment is spread, but the merged selection differs when a sibling selection
5051
+ * at one spread site adds to it, so keying on one node would serve one site the other's selection.
5052
+ * The nodes are matched by identity, in order, through a chain of weak maps, so a document cached
5053
+ * across requests never carries values between them (the root lives on the context), and an array
5054
+ * graphql rebuilds per call still hits. `key` separates fields that share nodes: one fragment
5055
+ * selection can resolve for more than one parent type. Without a context object, or without
5056
+ * nodes, there is nowhere request-scoped to cache, so the value is simply computed.
4821
5057
  *
4822
5058
  * `compute` must not read the parent row.
4823
5059
  */
4824
- const getOrCreateRequestValue = (context, node, key, compute) => {
4825
- if (!context || typeof context !== "object" || !node || typeof node !== "object") return compute();
4826
- if (!context[DRIZZLE_REQUEST_CACHE_KEY]) context[DRIZZLE_REQUEST_CACHE_KEY] = /* @__PURE__ */ new WeakMap();
4827
- const cache = context[DRIZZLE_REQUEST_CACHE_KEY];
4828
- let entries = cache.get(node);
4829
- if (!entries) {
4830
- entries = /* @__PURE__ */ new Map();
4831
- cache.set(node, entries);
5060
+ const getOrCreateRequestValue = (context, nodes, key, compute) => {
5061
+ if (!context || typeof context !== "object" || !Array.isArray(nodes) || !nodes.length || nodes.some((node) => !node || typeof node !== "object")) return compute();
5062
+ if (!context[DRIZZLE_REQUEST_CACHE_KEY]) context[DRIZZLE_REQUEST_CACHE_KEY] = requestCacheNode();
5063
+ let level = context[DRIZZLE_REQUEST_CACHE_KEY];
5064
+ for (const node of nodes) {
5065
+ let next = level.children.get(node);
5066
+ if (!next) {
5067
+ next = requestCacheNode();
5068
+ level.children.set(node, next);
5069
+ }
5070
+ level = next;
4832
5071
  }
5072
+ const entries = level.entries;
4833
5073
  if (entries.has(key)) return entries.get(key);
4834
5074
  const value = compute();
4835
5075
  entries.set(key, value);
@@ -5064,6 +5304,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
5064
5304
  const junction = extractJunctionColumns(relEntry);
5065
5305
  const groupCols = junction ? [junction.sourceCol] : joinPairs.map((pair) => pair.foreignCol);
5066
5306
  const groupKeys = junction ? [PARENT_KEY] : joinPairs.map((pair) => pair.foreignColPropName);
5307
+ const junctionName = junction ? junctionTableName(tables, relEntry) : void 0;
5067
5308
  const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, joinPairs.map((pair) => pair.localCol), groupCols);
5068
5309
  const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, isOne && relEntry.relation?.optional === false);
5069
5310
  const limitPolicy = isOne ? void 0 : limits?.(targetTableName);
@@ -5108,7 +5349,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
5108
5349
  const localValues = parentJoinValues(parent, joinPairs);
5109
5350
  if (localValues.some((value) => value == null)) return isOne ? null : [];
5110
5351
  const { where: whereArg, limit: requestedLimit, offset, after, deleted } = args ?? {};
5111
- const { orderByArg, limit, distinct, cursorEntries, cursorValues, columns, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes?.[0], `relation:${tableName}::${relationName}`, () => {
5352
+ const { orderByArg, limit, distinct, cursorEntries, cursorValues, columns, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes, `relation:${tableName}::${relationName}`, () => {
5112
5353
  const orderByArg = isOne ? args?.orderBy : withDefaultOrderBy(args ?? {}, targetTableName, policies?.defaultOrderBy).orderBy;
5113
5354
  const limit = applyLimitPolicy(requestedLimit, limitPolicy, errorCtx);
5114
5355
  assertPagingNotNegative({ offset }, errorCtx);
@@ -5152,7 +5393,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
5152
5393
  const uniqueKeys = [...new Map(parentKeys.map((key) => [joinKeyOf(key, localCols), key])).values()];
5153
5394
  const scope = resolveScope(policies, context, filterCtx);
5154
5395
  const loadChunk = async (chunkKeys) => {
5155
- let whereCondition = withScope(scope, targetTableName, targetTable, 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);
5396
+ let whereCondition = withScope(scope, targetTableName, targetTable, and(parentKeyCondition(groupCols, chunkKeys), ownCondition, junction ? junctionPolicy(scope, junctionName, junction.table) : void 0, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0, cursorValues ? buildCursorCondition(targetTable, cursorEntries, cursorValues, nullOrdering) : void 0), deleted ?? defaultDeleted);
5156
5397
  if (distinct) {
5157
5398
  const keys = await selectDistinctKeys({
5158
5399
  db: executor,
@@ -5170,7 +5411,7 @@ const createRelationResolverFactory = (db, tables, nullOrdering, filterCtx, limi
5170
5411
  } : void 0
5171
5412
  });
5172
5413
  if (!keys.length) return /* @__PURE__ */ new Map();
5173
- whereCondition = and(whereCondition, junction ? or(...keys.map((key) => and(eq(junction.sourceCol, key[DISTINCT_PARENT]), primaryKeyRestriction(targetTable, targetPkNames, [key])))) : primaryKeyRestriction(targetTable, targetPkNames, keys));
5414
+ whereCondition = and(whereCondition, junction ? balancedOr(keys.map((key) => and(eq(junction.sourceCol, key[DISTINCT_PARENT]), primaryKeyRestriction(targetTable, targetPkNames, [key])))) : primaryKeyRestriction(targetTable, targetPkNames, keys));
5174
5415
  }
5175
5416
  const selection = junction ? {
5176
5417
  ...columns ?? getColumns(targetTable),
@@ -5307,7 +5548,7 @@ const generateTableOrderCached = (table, innerOrder) => {
5307
5548
  try {
5308
5549
  const columns = visibleColumns(table);
5309
5550
  const columnEntries = Object.entries(columns);
5310
- remapped = Object.fromEntries(columnEntries.map(([columnName, _columnDescription]) => [columnName, { type: innerOrder }]));
5551
+ remapped = Object.fromEntries(columnEntries.filter(([, column]) => isOrderableColumn(column)).map(([columnName]) => [columnName, { type: innerOrder }]));
5311
5552
  if (cacheable) {
5312
5553
  let byInnerOrder = orderMap.get(table);
5313
5554
  if (!byInnerOrder) {
@@ -5350,43 +5591,71 @@ const generateTableSelectTypeFieldsCached = (table, tableName, cacheCtx) => {
5350
5591
  return remapped;
5351
5592
  };
5352
5593
  /**
5594
+ * The to-one relations a table's OrderBy can order through, as `[relationName, targetTableName]`.
5595
+ * To-many relations are left out: "order a parent by its many children" has no single defined
5596
+ * value to sort on. A relation whose name collides with a column is skipped — the column keeps
5597
+ * the name, even one with no ordering of its own, so `extractOrderBy` never reads it as a relation.
5598
+ */
5599
+ const orderRelations = (tableName, relationMap, tables) => {
5600
+ const relations = relationMap?.[tableName];
5601
+ const table = tables?.[tableName];
5602
+ if (!relations || !tables || !table) return [];
5603
+ const columns = visibleColumns(table);
5604
+ return Object.entries(relations).filter(([relationName, relEntry]) => {
5605
+ const relation = relEntry.relation ?? relEntry;
5606
+ return !(relationName in columns) && !!tables[relEntry.targetTableName] && is(relation, One) && isFilterableRelation(relation);
5607
+ }).map(([relationName, relEntry]) => [relationName, relEntry.targetTableName]);
5608
+ };
5609
+ /**
5610
+ * Whether a table's OrderBy would have any field: an orderable column of its own, or a to-one
5611
+ * relation to a table that has one. A table of nothing but JSON columns has none, and an input
5612
+ * object with no fields is an invalid schema, so such a table gets no OrderBy and no `orderBy`
5613
+ * argument at all.
5614
+ */
5615
+ const hasOrderFields = (tableName, relationMap, tables, visiting = /* @__PURE__ */ new Set()) => {
5616
+ const table = tables[tableName];
5617
+ if (table && Object.values(visibleColumns(table)).some((column) => isOrderableColumn(column))) return true;
5618
+ if (visiting.has(tableName)) return false;
5619
+ visiting.add(tableName);
5620
+ return orderRelations(tableName, relationMap, tables).some(([, target]) => hasOrderFields(target, relationMap, tables, visiting));
5621
+ };
5622
+ /**
5353
5623
  * Order fields for a table's to-one relations: each takes the target table's own OrderBy
5354
5624
  * input, so a list can be sorted by a related row's column (compiled as a correlated
5355
- * subquery in the ORDER BY — see `extractOrderBy`). To-many relations are left out: "order
5356
- * a parent by its many children" has no single defined value to sort on. A relation whose
5357
- * name collides with a column is skipped — the column keeps the field.
5625
+ * subquery in the ORDER BY — see `extractOrderBy`). A target with no OrderBy of its own is
5626
+ * left out.
5358
5627
  */
5359
- const generateRelationOrderFields = (tableName, cacheCtx, typeNameMapper, columnFields, relationMap, tables) => {
5360
- const relations = relationMap?.[tableName];
5361
- if (!relations || !tables) return {};
5628
+ const generateRelationOrderFields = (tableName, cacheCtx, typeNameMapper, relationMap, tables) => {
5362
5629
  const fields = {};
5363
- for (const [relationName, relEntry] of Object.entries(relations)) {
5364
- if (relationName in columnFields) continue;
5365
- const targetTable = tables[relEntry.targetTableName];
5366
- const relation = relEntry.relation ?? relEntry;
5367
- if (!targetTable || !is(relation, One) || !isFilterableRelation(relation)) continue;
5368
- fields[relationName] = {
5369
- type: generateTableOrderTypeCached(targetTable, relEntry.targetTableName, typeNameMapper, cacheCtx, relationMap, tables),
5630
+ for (const [relationName, targetTableName] of orderRelations(tableName, relationMap, tables)) {
5631
+ const type = generateTableOrderTypeCached(tables[targetTableName], targetTableName, typeNameMapper, cacheCtx, relationMap, tables);
5632
+ if (type) fields[relationName] = {
5633
+ type,
5370
5634
  description: `Order by columns of the related ${relationName} row`
5371
5635
  };
5372
5636
  }
5373
5637
  return fields;
5374
5638
  };
5639
+ /**
5640
+ * The table's `<Type>OrderBy` input, or `undefined` when it would have no fields (see
5641
+ * {@link hasOrderFields}); the caller then leaves the `orderBy` argument out.
5642
+ */
5375
5643
  const generateTableOrderTypeCached = (table, tableName, typeNameMapper, cacheCtx, relationMap, tables) => {
5376
5644
  if (cacheCtx.orderTypeCache.has(table)) return cacheCtx.orderTypeCache.get(table);
5645
+ if (!hasOrderFields(tableName, relationMap, {
5646
+ ...tables,
5647
+ [tableName]: table
5648
+ })) return;
5377
5649
  const order = new GraphQLInputObjectType({
5378
5650
  name: cacheCtx.typeName({
5379
5651
  kind: "orderBy",
5380
5652
  defaultName: `${resolveTypeName(tableName, typeNameMapper)}OrderBy`,
5381
5653
  table: tableName
5382
5654
  }),
5383
- fields: () => {
5384
- const orderColumns = generateTableOrderCached(table, innerOrderType(cacheCtx.typeName));
5385
- return {
5386
- ...orderColumns,
5387
- ...generateRelationOrderFields(tableName, cacheCtx, typeNameMapper, orderColumns, relationMap, tables)
5388
- };
5389
- }
5655
+ fields: () => ({
5656
+ ...generateTableOrderCached(table, innerOrderType(cacheCtx.typeName)),
5657
+ ...generateRelationOrderFields(tableName, cacheCtx, typeNameMapper, relationMap, tables)
5658
+ })
5390
5659
  });
5391
5660
  cacheCtx.orderTypeCache.set(table, order);
5392
5661
  return order;
@@ -5605,7 +5874,7 @@ const generateSelectFields = (tables, tableName, relationMap, fromTableName, fro
5605
5874
  type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(relType))),
5606
5875
  args: {
5607
5876
  where: { type: relSelectData.filters },
5608
- orderBy: { type: relSelectData.order },
5877
+ ...relSelectData.order ? { orderBy: { type: relSelectData.order } } : {},
5609
5878
  offset: { type: GraphQLInt },
5610
5879
  limit: { type: GraphQLInt },
5611
5880
  ...deletedArg(cacheCtx.softDeleteOf, targetTableName, cacheCtx.typeName),
@@ -5833,6 +6102,7 @@ const resolveBuildConfig = (config, schema) => {
5833
6102
  };
5834
6103
  const transactionsOpt = config?.transactions;
5835
6104
  const transactions = transactionsOpt === void 0 || transactionsOpt === "none" ? void 0 : { timeoutMs: (transactionsOpt === "auto" ? void 0 : transactionsOpt.timeoutMs) ?? 3e4 };
6105
+ if (transactions && transactions.timeoutMs !== Number.POSITIVE_INFINITY && !(Number.isInteger(transactions.timeoutMs) && transactions.timeoutMs >= 1 && transactions.timeoutMs <= MAX_TIMER_MS)) throw new Error(`Drizzle-GraphQL Error: config.transactions.timeoutMs must be a positive integer no greater than ${MAX_TIMER_MS}, or Infinity for no timeout; got ${transactions.timeoutMs}.`);
5836
6106
  const limitsConfig = config?.limits;
5837
6107
  if (limitsConfig) {
5838
6108
  const check = (policy, where) => {
@@ -5865,6 +6135,7 @@ const resolveBuildConfig = (config, schema) => {
5865
6135
  for (const [columnName, entry] of Object.entries(orderBy)) {
5866
6136
  if (!columns[columnName]) throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy names '${columnName}', which is not a column of '${tableName}'.`);
5867
6137
  if (config?.exclude?.columns?.[tableName]?.includes(columnName)) throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy names '${columnName}', which config.exclude.columns hides. A default ordering is encoded into every row's cursor, so it can only use columns the schema exposes.`);
6138
+ if (!isOrderableColumn(columns[columnName])) throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy names '${columnName}', which has no meaningful ordering (a JSON column, or a SQLite blob bigint), so the generated orderBy does not offer it either.`);
5868
6139
  const direction = typeof entry === "string" ? entry : entry?.direction;
5869
6140
  if (direction !== "asc" && direction !== "desc") throw new Error(`Drizzle-GraphQL Error: config.defaults.${tableName}.orderBy.${columnName} must be 'asc', 'desc', or { direction, priority? }.`);
5870
6141
  const priority = typeof entry === "string" ? void 0 : entry?.priority;
@@ -5923,7 +6194,7 @@ const resolveBuildConfig = (config, schema) => {
5923
6194
  }
5924
6195
  }
5925
6196
  }
5926
- const onWrite = writeHooks.size ? (tableName) => writeHooks.get(tableName) : void 0;
6197
+ const onWrite = [...writeHooks.values()].some(Boolean) ? (tableName) => writeHooks.get(tableName) : void 0;
5927
6198
  const policies = scopeConfig || contextValuesConfig || softDeleteInfos.size || onWrite || defaultOrderBy ? {
5928
6199
  scope: scopeConfig ? (tableName) => scopeConfig[tableName] : void 0,
5929
6200
  contextValues: contextValuesConfig ? (tableName) => contextValuesConfig[tableName] : void 0,
@@ -6014,8 +6285,14 @@ const AGGREGATE_OPS = [
6014
6285
  ];
6015
6286
  /** Ops whose result is a row count: never null, and returned as `Int!` rather than the column's type. */
6016
6287
  const COUNT_OPS = /* @__PURE__ */ new Set(["countNonNull", "countDistinct"]);
6288
+ /**
6289
+ * `avg`, except on MySQL, where AVG over an integer column is a DECIMAL rounded to
6290
+ * `div_precision_increment` (4 by default) digits. Averaging the value as a DOUBLE keeps the
6291
+ * fraction the `Float` output promises, as the other two dialects do.
6292
+ */
6293
+ const avgOf = (col) => columnDialect(col) === "mysql" ? sql`avg(cast(${col} as double))`.mapWith(String) : avg(col);
6017
6294
  const OP_FNS = {
6018
- avg,
6295
+ avg: avgOf,
6019
6296
  sum,
6020
6297
  min,
6021
6298
  max,
@@ -6025,6 +6302,18 @@ const OP_FNS = {
6025
6302
  /** Separator for flat select aliases (`avg__price`) — reassembled into nested output by the resolver. */
6026
6303
  const SEP = "__";
6027
6304
  /**
6305
+ * Column types PostgreSQL can test for equality but has no `min`/`max` aggregate for, plus the
6306
+ * SQLite bigint blob, whose bytes compare as text (`max(9, 10)` is 9). They still count
6307
+ * distinct values and group.
6308
+ */
6309
+ const NO_MIN_MAX_COLUMN_TYPES = /* @__PURE__ */ new Set([
6310
+ "PgUUID",
6311
+ "PgMacaddr",
6312
+ "PgMacaddr8",
6313
+ "PgBinaryVector",
6314
+ "SQLiteBigInt"
6315
+ ]);
6316
+ /**
6028
6317
  * Classifies a table's columns for aggregation. avg/sum only make sense on numeric scalars;
6029
6318
  * min/max work on any orderable scalar (numbers, strings, bigints, dates, enums). Booleans,
6030
6319
  * arrays (including array-typed number columns), JSON, buffers, and object-shaped columns
@@ -6056,6 +6345,7 @@ const classifyAggregateColumns = (table, tableName) => {
6056
6345
  if (cached) return cached;
6057
6346
  const numeric = {};
6058
6347
  const orderable = {};
6348
+ const comparable = {};
6059
6349
  const all = {};
6060
6350
  for (const [columnName, column] of Object.entries(visibleColumns(table))) {
6061
6351
  all[columnName] = column;
@@ -6065,7 +6355,11 @@ const classifyAggregateColumns = (table, tableName) => {
6065
6355
  const { type: dataType, constraint } = extractExtendedColumnType(column);
6066
6356
  if (dataType === "boolean" || dataType === "array" || dataType === "custom") continue;
6067
6357
  if (dataType === "object" && constraint !== "date") continue;
6068
- orderable[columnName] = {
6358
+ comparable[columnName] = {
6359
+ column,
6360
+ converted
6361
+ };
6362
+ if (!NO_MIN_MAX_COLUMN_TYPES.has(column.columnType)) orderable[columnName] = {
6069
6363
  column,
6070
6364
  converted
6071
6365
  };
@@ -6074,6 +6368,7 @@ const classifyAggregateColumns = (table, tableName) => {
6074
6368
  const sets = {
6075
6369
  numeric,
6076
6370
  orderable,
6371
+ comparable,
6077
6372
  all
6078
6373
  };
6079
6374
  byName.set(tableName, sets);
@@ -6086,13 +6381,13 @@ const classifyAggregateColumns = (table, tableName) => {
6086
6381
  * and avg/sum of integers overflow Int / produce decimals)
6087
6382
  * - `min` / `max` — per orderable column, the column's own (nullable) scalar type
6088
6383
  * - `countNonNull` — per column, `Int!`: how many matching rows have a non-null value there
6089
- * - `countDistinct` — per orderable column, `Int!`: how many distinct non-null values there are
6384
+ * - `countDistinct` — per comparable column, `Int!`: how many distinct non-null values there are
6090
6385
  * Each wrapper is omitted when no column qualifies for it.
6091
6386
  */
6092
6387
  const generateAggregateTypes = (table, tableName, typeName, cacheCtx) => {
6093
6388
  const cached = cacheCtx?.aggregateTypeCache.get(tableName);
6094
6389
  if (cached) return cached;
6095
- const { numeric, orderable, all } = classifyAggregateColumns(table, tableName);
6390
+ const { numeric, orderable, comparable, all } = classifyAggregateColumns(table, tableName);
6096
6391
  const nameOf = (op) => cacheCtx?.typeName({
6097
6392
  kind: "aggregate",
6098
6393
  defaultName: `${typeName}${capitalize(op)}Aggregate`,
@@ -6113,7 +6408,7 @@ const generateAggregateTypes = (table, tableName, typeName, cacheCtx) => {
6113
6408
  }) };
6114
6409
  const countSets = {
6115
6410
  countNonNull: all,
6116
- countDistinct: orderable
6411
+ countDistinct: comparable
6117
6412
  };
6118
6413
  for (const [op, columns] of Object.entries(countSets)) {
6119
6414
  const columnNames = Object.keys(columns);
@@ -6293,6 +6588,7 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
6293
6588
  const junction = extractJunctionColumns(relEntry);
6294
6589
  if (junction === null) return;
6295
6590
  const groupCols = junction ? [junction.sourceCol] : joinPairs.map((pair) => pair.foreignCol);
6591
+ const junctionName = junction ? junctionTableName(tables, relEntry) : void 0;
6296
6592
  const groupAliases = groupCols.map((_, i) => `${GROUP_KEY}${i}`);
6297
6593
  const ownCondition = relationOwnCondition(relEntry, parentTable, targetTable, joinPairs.map((pair) => pair.localCol), groupCols);
6298
6594
  const defaultDeleted = relationDeletedDefault(policies?.softDelete, targetTableName, false);
@@ -6306,7 +6602,7 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
6306
6602
  };
6307
6603
  const resolve = async (parent, args, context, info) => {
6308
6604
  try {
6309
- const { request, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes?.[0], `aggregate:${tableName}::${relationName}`, () => {
6605
+ const { request, loaderKey } = getOrCreateRequestValue(context, info?.fieldNodes, `aggregate:${tableName}::${relationName}`, () => {
6310
6606
  const request = parseAggregateRequest(info, target);
6311
6607
  return {
6312
6608
  request,
@@ -6329,7 +6625,7 @@ const createRelationAggregateFactory = (db, tables, cacheCtx, typeNameMapper, fi
6329
6625
  const groupSelection = Object.fromEntries(groupAliases.map((alias, i) => [alias, groupCols[i]]));
6330
6626
  const byKey = /* @__PURE__ */ new Map();
6331
6627
  for (const chunk of parentKeyChunks(uniqueKeys, groupCols.length)) {
6332
- const whereCondition = withScope(scope, targetTableName, targetTable, and(parentKeyCondition(groupCols, chunk), ownCondition, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0), deleted ?? defaultDeleted);
6628
+ const whereCondition = withScope(scope, targetTableName, targetTable, and(parentKeyCondition(groupCols, chunk), ownCondition, junction ? junctionPolicy(scope, junctionName, junction.table) : void 0, whereArg ? extractFilters(targetTable, targetTableName, whereArg, relationFilterCtx(filterCtx, targetTableName, scope)) : void 0), deleted ?? defaultDeleted);
6333
6629
  const selected = executor.select({
6334
6630
  ...groupSelection,
6335
6631
  ...request.selection
@@ -6363,12 +6659,12 @@ const HAVING_OPS = {
6363
6659
  };
6364
6660
  /**
6365
6661
  * Columns a query can group on. Grouping needs an equality operator, which is the same
6366
- * requirement min/max has, plus booleans — orderable excludes them because a min/max over
6662
+ * requirement countDistinct has, plus booleans — comparable excludes them because a min/max over
6367
6663
  * true/false says nothing, but `GROUP BY isConfirmed` is perfectly ordinary.
6368
6664
  */
6369
6665
  const groupableColumns = (table, tableName) => {
6370
- const { orderable } = classifyAggregateColumns(table, tableName);
6371
- const groupable = { ...orderable };
6666
+ const { comparable } = classifyAggregateColumns(table, tableName);
6667
+ const groupable = { ...comparable };
6372
6668
  for (const [columnName, column] of Object.entries(visibleColumns(table))) {
6373
6669
  if (groupable[columnName]) continue;
6374
6670
  const { type: dataType } = extractExtendedColumnType(column);
@@ -6409,7 +6705,7 @@ const aggregateNumberFilterType = (typeName) => sharedType(typeName, {
6409
6705
  * nothing to compare against.
6410
6706
  */
6411
6707
  const generateHavingInput = (table, tableName, typeName, cacheCtx) => {
6412
- const { numeric, orderable, all } = classifyAggregateColumns(table, tableName);
6708
+ const { numeric, comparable, all } = classifyAggregateColumns(table, tableName);
6413
6709
  const resolveName = cacheCtx?.typeName ?? ((info) => info.defaultName);
6414
6710
  const numberFilter = aggregateNumberFilterType(resolveName);
6415
6711
  const fields = { count: {
@@ -6422,7 +6718,7 @@ const generateHavingInput = (table, tableName, typeName, cacheCtx) => {
6422
6718
  min: numeric,
6423
6719
  max: numeric,
6424
6720
  countNonNull: all,
6425
- countDistinct: orderable
6721
+ countDistinct: comparable
6426
6722
  };
6427
6723
  for (const [op, columns] of Object.entries(opColumns)) {
6428
6724
  const columnNames = Object.keys(columns);
@@ -6487,22 +6783,34 @@ const generateGroupByType = (table, tableName, typeName, cacheCtx) => {
6487
6783
  }
6488
6784
  });
6489
6785
  };
6490
- /** Turns one `${typeName}Having` entry into SQL conditions over the matching aggregate expression. */
6491
- const havingComparisons = (expression, filter) => Object.entries(filter).filter(([op, value]) => value != null && op in HAVING_OPS).map(([op, value]) => HAVING_OPS[op](expression, value));
6786
+ /**
6787
+ * Turns one `${typeName}Having` entry into SQL conditions over the matching aggregate expression.
6788
+ *
6789
+ * The operand is a `Float`, but PostgreSQL types an untyped parameter after what it is compared
6790
+ * with: count and sum of integers are bigint, min/max of an int column int, so `gt: 1.5` would
6791
+ * fail to parse. There the aggregate is compared as `numeric`, which holds every one of them
6792
+ * exactly. MySQL and SQLite already compare numbers across types.
6793
+ */
6794
+ const havingComparisons = (expression, filter, isPg) => {
6795
+ const compared = isPg ? sql`cast(${expression} as numeric)` : expression;
6796
+ return Object.entries(filter).filter(([op, value]) => value != null && op in HAVING_OPS).map(([op, value]) => HAVING_OPS[op](compared, value));
6797
+ };
6492
6798
  const buildHavingCondition = (having, columns) => {
6493
6799
  if (!having) return;
6494
6800
  const conditions = [];
6801
+ const firstColumn = Object.values(columns)[0];
6802
+ const isPg = firstColumn ? columnDialect(firstColumn) === "pg" : false;
6495
6803
  for (const [key, value] of Object.entries(having)) {
6496
6804
  if (value == null) continue;
6497
6805
  if (key === "count") {
6498
- conditions.push(...havingComparisons(count(), value));
6806
+ conditions.push(...havingComparisons(count(), value, isPg));
6499
6807
  continue;
6500
6808
  }
6501
6809
  if (!AGGREGATE_OPS.includes(key)) continue;
6502
6810
  for (const [columnName, filter] of Object.entries(value)) {
6503
6811
  const column = columns[columnName];
6504
6812
  if (!column || filter == null) continue;
6505
- conditions.push(...havingComparisons(OP_FNS[key](column), filter));
6813
+ conditions.push(...havingComparisons(OP_FNS[key](column), filter, isPg));
6506
6814
  }
6507
6815
  }
6508
6816
  return conditions.length ? and(...conditions) : void 0;
@@ -6654,12 +6962,32 @@ const isOptionalOnInsert = (column) => !column.notNull || !!column.hasDefault ||
6654
6962
  * relation could come back without it. A `where` inherited from the reverse relation filters
6655
6963
  * the parent instead, and becomes a per-parent check.
6656
6964
  */
6657
- const ownWherePlan = (relEntry, parentTable, targetTable, localCol) => {
6965
+ const ownWherePlan = (relEntry, parentTable, targetTable, localCol, parentPrimaryKey) => {
6658
6966
  const condition = () => relationOwnCondition(relEntry, parentTable, targetTable, [localCol], [localCol]);
6659
6967
  if (!condition()) return {};
6660
- if (!isRelationFilterReversed(relEntry.relation ?? relEntry)) return { ownCondition: () => condition() };
6661
- return { parentShows: async (executor, key) => (await executor.select({ key: localCol }).from(parentTable).where(and(eq(localCol, key), condition())).limit(1)).length > 0 };
6968
+ const relation = relEntry.relation ?? relEntry;
6969
+ if (!isRelationFilterReversed(relation)) return { ownCondition: () => condition() };
6970
+ const parentColumns = getColumns(parentTable);
6971
+ const parentKeyColumns = Object.fromEntries(parentPrimaryKey.filter((name) => parentColumns[name]).map((name) => [name, parentColumns[name]]));
6972
+ const keyNames = Object.keys(parentKeyColumns);
6973
+ return {
6974
+ parentShows: async (executor, key) => (await executor.select({ key: localCol }).from(parentTable).where(and(eq(localCol, key), condition())).limit(1)).length > 0,
6975
+ parentHidden: async (executor, rows) => {
6976
+ if (!rows.length) return false;
6977
+ if (!keyNames.length || !rows.every((row) => keyNames.every((name) => name in row))) throw drizzleError(`Drizzle-GraphQL Error: cannot check the \`where\` of this relation: the written row cannot be identified by a primary key.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
6978
+ const keyOf = (row) => keyNames.map((name) => keyString(row[name])).join("\0");
6979
+ const match = or(...rows.map((row) => and(...keyNames.map((name) => eq(parentColumns[name], row[name])))));
6980
+ const shown = new Set((await executor.select(parentKeyColumns).from(parentTable).where(and(match, relationsFilterToSQL(parentTable, relationOwnWhere(relation))))).map(keyOf));
6981
+ return rows.some((row) => !shown.has(keyOf(row)));
6982
+ },
6983
+ parentKeyColumns
6984
+ };
6662
6985
  };
6986
+ /**
6987
+ * A key value in a form that compares equal across the two reads of it — the driver may hand
6988
+ * back a `Date` or a `bigint` that is a different object each time.
6989
+ */
6990
+ const keyString = (value) => value instanceof Date ? `d${value.getTime()}` : value === null || value === void 0 ? "" : `v${String(value)}`;
6663
6991
  /** The property name a column is stored under on its table, matched by identity. */
6664
6992
  const propNameOf = (table, column) => {
6665
6993
  if (!column) return;
@@ -6721,7 +7049,7 @@ const buildThroughPlan = (relationName, relEntry, relation, table, targetTableNa
6721
7049
  targetCol
6722
7050
  },
6723
7051
  targetKeyColumns: { [foreignColPropName]: foreignCol },
6724
- ...ownWherePlan(relEntry, table, targetTable, localCol),
7052
+ ...ownWherePlan(relEntry, table, targetTable, localCol, []),
6725
7053
  allowed: /* @__PURE__ */ new Set([
6726
7054
  "connect",
6727
7055
  "disconnect",
@@ -6800,7 +7128,7 @@ const buildNestedWritePlans = (tables, relationMap, resolveUniqueSets, resolvePr
6800
7128
  const targetPrimaryKey = resolvePrimaryKeys(targetTable).filter((name) => targetColumns[name]);
6801
7129
  const targetKeyColumns = Object.fromEntries(targetPrimaryKey.map((name) => [name, targetColumns[name]]));
6802
7130
  targetKeyColumns[foreignColPropName] = foreignCol;
6803
- const ownWhere = ownWherePlan(relEntry, table, targetTable, localCol);
7131
+ const ownWhere = ownWherePlan(relEntry, table, targetTable, localCol, resolvePrimaryKeys(table));
6804
7132
  const ops = !fkColumn.notNull ? isOne ? [
6805
7133
  "create",
6806
7134
  "connect",
@@ -7233,6 +7561,16 @@ const createNestedWriteRuntime = (params) => {
7233
7561
  }
7234
7562
  if (supplied(op["connect"])) await link(asArray(op["connect"]), "connect");
7235
7563
  };
7564
+ /**
7565
+ * The value a parent-side `connect` / `disconnect` writes to an existing row's key: `next`,
7566
+ * unless the key points at a row the relation's own `where` hides, which it then keeps.
7567
+ */
7568
+ const keepingHidden = (plan, next) => {
7569
+ if (!plan.ownCondition) return next;
7570
+ const shown = sql`select ${plan.foreignCol} from ${plan.targetTable} where ${plan.ownCondition()}`;
7571
+ const replacement = next === null ? sql`null` : sql`${sql.param(next, plan.localCol)}`;
7572
+ return sql`case when ${plan.localCol} is null or ${plan.localCol} in (${shown}) then ${replacement} else ${plan.localCol} end`;
7573
+ };
7236
7574
  const detachAll = (executor, plan, key, scope, steps, op) => steps.run(executor.update(plan.targetTable).set({ [plan.foreignColPropName]: null }).where(withScope(scope, plan.targetTableName, plan.targetTable, and(eq(plan.foreignCol, key), plan.ownCondition?.()))), op, "update");
7237
7575
  return {
7238
7576
  enabled: (tableName) => !!plans[tableName],
@@ -7258,7 +7596,10 @@ const createNestedWriteRuntime = (params) => {
7258
7596
  const allColumns = getColumns(table);
7259
7597
  for (const relationName of Object.keys(ops)) {
7260
7598
  const plan = tablePlans[relationName];
7261
- if (plan?.fkSide === "child" && !(plan.localColPropName in columns) && allColumns[plan.localColPropName]) columns[plan.localColPropName] = allColumns[plan.localColPropName];
7599
+ if (plan && !(plan.localColPropName in columns) && allColumns[plan.localColPropName]) columns[plan.localColPropName] = allColumns[plan.localColPropName];
7600
+ if (plan?.fkSide === "parent" && plan.parentHidden) {
7601
+ for (const [name, column] of Object.entries(plan.parentKeyColumns ?? {})) if (!(name in columns)) columns[name] = column;
7602
+ }
7262
7603
  }
7263
7604
  for (const name of selfTargetingPlan(tablePlans, tableName, ops)?.targetPrimaryKey ?? []) if (!(name in columns) && allColumns[name]) columns[name] = allColumns[name];
7264
7605
  return columns;
@@ -7278,10 +7619,15 @@ const createNestedWriteRuntime = (params) => {
7278
7619
  ...fresh.get(keyOf(row))
7279
7620
  }));
7280
7621
  },
7281
- applyParentSide: async (executor, tableName, ops, request) => {
7622
+ applyParentSide: async (executor, tableName, ops, request, existing = false) => {
7282
7623
  const tablePlans = plans[tableName];
7283
- const patch = {};
7284
- if (!tablePlans) return patch;
7624
+ const result = {
7625
+ values: {},
7626
+ expected: {},
7627
+ existing
7628
+ };
7629
+ const patch = result.values;
7630
+ if (!tablePlans) return result;
7285
7631
  const context = request?.context;
7286
7632
  const scope = resolveScope(policies, context, filterCtx);
7287
7633
  for (const [relationName, op] of Object.entries(ops)) {
@@ -7300,14 +7646,30 @@ const createNestedWriteRuntime = (params) => {
7300
7646
  if (!created) throw drizzleError(`Drizzle-GraphQL Error: 'create' on '${relationName}' inserted no row, so there is nothing to attach.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
7301
7647
  await steps.after();
7302
7648
  patch[plan.localColPropName] = created[plan.foreignColPropName];
7649
+ result.expected[relationName] = created[plan.foreignColPropName];
7303
7650
  } else if (op["connect"] !== void 0) {
7304
7651
  const rows = await executor.select({ [plan.foreignColPropName]: plan.foreignCol }).from(plan.targetTable).where(conditionOf(plan, op["connect"], "connect", scope));
7305
7652
  if (rows.length !== 1) throw connectMatchError(relationName, rows.length);
7306
- patch[plan.localColPropName] = rows[0][plan.foreignColPropName];
7307
- } else if (op["disconnect"] === true) patch[plan.localColPropName] = null;
7653
+ const key = rows[0][plan.foreignColPropName];
7654
+ patch[plan.localColPropName] = existing ? keepingHidden(plan, key) : key;
7655
+ result.expected[relationName] = key;
7656
+ } else if (op["disconnect"] === true) patch[plan.localColPropName] = existing ? keepingHidden(plan, null) : null;
7308
7657
  }
7309
7658
  for (const columnName of Object.keys(contextValues?.(tableName) ?? {})) delete patch[columnName];
7310
- return patch;
7659
+ return result;
7660
+ },
7661
+ checkParentSide: async (executor, tableName, ops, rows, patch) => {
7662
+ const tablePlans = plans[tableName];
7663
+ if (!tablePlans || !rows.length) return;
7664
+ for (const relationName of Object.keys(ops)) {
7665
+ const plan = tablePlans[relationName];
7666
+ if (plan?.fkSide !== "parent") continue;
7667
+ if (relationName in patch.expected) {
7668
+ const expected = keyString(patch.expected[relationName]);
7669
+ if (rows.some((row) => keyString(row[plan.localColPropName]) !== expected)) throw drizzleError(patch.existing ? `Drizzle-GraphQL Error: cannot connect through '${relationName}': the row it holds now is one its \`where\` hides, which a write through it cannot detach.` : `Drizzle-GraphQL Error: cannot write through '${relationName}': the ${tableName} row was not written with it — an upsert whose conflict branch did not write '${plan.localColPropName}'. Include it in onConflict.update, or leave update unset.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
7670
+ }
7671
+ if (plan.parentHidden && await plan.parentHidden(executor, rows)) throw drizzleError(`Drizzle-GraphQL Error: cannot write through '${relationName}': its \`where\` hides the relation on this ${tableName} row.`, { code: "DRIZZLE_NESTED_WRITE_INVALID" });
7672
+ }
7311
7673
  },
7312
7674
  applyChildSide: async (executor, tableName, ops, parentRows, request) => {
7313
7675
  const tablePlans = plans[tableName];
@@ -7374,12 +7736,13 @@ var NoParentRow = class extends Error {};
7374
7736
  * `executor` must already be a transaction.
7375
7737
  */
7376
7738
  const writeNestedEntry = async (params) => {
7377
- const { executor, runtime, tableName, ops, write, request } = params;
7739
+ const { executor, runtime, tableName, ops, write, request, existing } = params;
7378
7740
  const guarded = runtime.createsParentSide(tableName, ops);
7379
7741
  const run = async (tx) => {
7380
- const patch = await runtime.applyParentSide(tx, tableName, ops, request);
7381
- const rows = await write(tx, patch);
7742
+ const patch = await runtime.applyParentSide(tx, tableName, ops, request, existing);
7743
+ const rows = await write(tx, patch.values);
7382
7744
  if (guarded && !rows.length) throw new NoParentRow();
7745
+ await runtime.checkParentSide(tx, tableName, ops, rows, patch);
7383
7746
  await runtime.applyChildSide(tx, tableName, ops, rows, request);
7384
7747
  return runtime.refreshed(tx, tableName, ops, rows);
7385
7748
  };
@@ -7441,7 +7804,8 @@ const updateWithNestedOps = async (params) => {
7441
7804
  ...remapValues(columns),
7442
7805
  ...patch
7443
7806
  }),
7444
- request
7807
+ request,
7808
+ existing: true
7445
7809
  }));
7446
7810
  };
7447
7811
  //#endregion
@@ -7594,6 +7958,7 @@ const createSchemaBuilder = (adapter) => {
7594
7958
  const anyTable = (feature) => tableEntries.some(([name]) => featureOf(name)[feature]);
7595
7959
  if (!tableEntries.length) throw new Error("Drizzle-GraphQL Error: No tables detected in Drizzle-ORM's database instance. Did you forget to pass schema to drizzle constructor?");
7596
7960
  registerScalarOverrides(tables, options);
7961
+ installSqliteBlobJsonDecoders(tables);
7597
7962
  registerEnumConfig(options);
7598
7963
  registerColumnExclusions(tables, options.exclude);
7599
7964
  resetAggregateColumnCache();
@@ -7825,7 +8190,7 @@ const createSchemaBuilder = (adapter) => {
7825
8190
  ...tableFeatures.update ? [updateInput] : [],
7826
8191
  ...built.updateManyInput ? [built.updateManyInput] : [],
7827
8192
  tableFilters,
7828
- tableOrder
8193
+ ...tableOrder ? [tableOrder] : []
7829
8194
  ].forEach((e) => {
7830
8195
  inputs[e.name] = e;
7831
8196
  });
@@ -7955,14 +8320,12 @@ const generateUpsert = (db, tableName, table, baseType, onConflictType, fieldNam
7955
8320
  withTarget: false,
7956
8321
  serverOwned: Object.keys(contextColumns ?? {})
7957
8322
  });
7958
- const requestedNothing = args.onConflict?.action === "NOTHING";
7959
8323
  const runBatch = async (target, { values, plan }) => {
7960
- if (plan.action === "NOTHING" && !requestedNothing) {
8324
+ if (plan.action === "NOTHING") {
7961
8325
  const columns = getColumns(table);
7962
8326
  const keyName = pkNames[0] ?? Object.keys(columns)[0];
7963
8327
  await target.insert(table).values(values).onDuplicateKeyUpdate({ set: { [keyName]: sql`${sql.identifier(columns[keyName].name)}` } });
7964
- } else if (plan.action === "NOTHING") await target.insert(table).ignore().values(values);
7965
- else await target.insert(table).values(values).onDuplicateKeyUpdate({ set: plan.set });
8328
+ } else await target.insert(table).values(values).onDuplicateKeyUpdate({ set: plan.set });
7966
8329
  };
7967
8330
  if (batches.length === 1) await runBatch(executor, batches[0]);
7968
8331
  else await executor.transaction(async (tx) => {
@@ -8002,7 +8365,7 @@ const generateUpdate = (db, tableName, table, setArgs, filterArgs, fieldName, si
8002
8365
  });
8003
8366
  if (single) await assertSingleMatch(executor, table, filters);
8004
8367
  let query = executor.update(table).set(input);
8005
- if (filters) query = query.where(filters);
8368
+ if (filters) query = query.where(mysqlWriteFilter(table, filters));
8006
8369
  await query;
8007
8370
  await after();
8008
8371
  return isSuccess;
@@ -8017,7 +8380,7 @@ const generateUpdate = (db, tableName, table, setArgs, filterArgs, fieldName, si
8017
8380
  * in order. MySQL has no RETURNING, so — like every other MySQL mutation — the result is
8018
8381
  * `{ isSuccess: true }` rather than the updated rows.
8019
8382
  */
8020
- const generateUpdateMany$2 = (db, tableName, table, updateManyInput, fieldName, filterCtx, txCtx, policies) => writeResolver({
8383
+ const generateUpdateMany$2 = (db, tableName, table, updateManyInput, fieldName, requireWhere, filterCtx, txCtx, policies) => writeResolver({
8021
8384
  db,
8022
8385
  tableName,
8023
8386
  operation: "updateMany",
@@ -8037,13 +8400,20 @@ const generateUpdateMany$2 = (db, tableName, table, updateManyInput, fieldName,
8037
8400
  if (!Object.keys(input).length) throw drizzleError("Unable to update with no values specified!", { code: "DRIZZLE_NO_VALUES" });
8038
8401
  return {
8039
8402
  set: input,
8040
- filters: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope)) : void 0)
8403
+ filters: scopedWhere({
8404
+ scope,
8405
+ tableName,
8406
+ table,
8407
+ where,
8408
+ relationCtx: relationFilterCtx(filterCtx, tableName, scope),
8409
+ required: requireWhere
8410
+ })
8041
8411
  };
8042
8412
  });
8043
8413
  await executor.transaction(async (tx) => {
8044
8414
  for (const entry of entries) {
8045
8415
  let query = tx.update(table).set(entry.set);
8046
- if (entry.filters) query = query.where(entry.filters);
8416
+ if (entry.filters) query = query.where(mysqlWriteFilter(table, entry.filters));
8047
8417
  await query;
8048
8418
  }
8049
8419
  });
@@ -8092,7 +8462,7 @@ const generateDelete = (db, tableName, table, filterArgs, fieldName, single, req
8092
8462
  });
8093
8463
  if (single) await assertSingleMatch(executor, table, filters);
8094
8464
  let query = softDelete && !hard ? executor.update(table).set({ [softDelete.columnName]: restore ? softDelete.writeRestored : softDelete.writeDeleted() }) : executor.delete(table);
8095
- if (filters) query = query.where(filters);
8465
+ if (filters) query = query.where(mysqlWriteFilter(table, filters));
8096
8466
  await query;
8097
8467
  await after();
8098
8468
  return isSuccess;
@@ -8103,6 +8473,34 @@ const generateDelete = (db, tableName, table, filterArgs, fieldName, single, req
8103
8473
  const getTableConfig$5 = memoizeTableConfig(getTableConfig);
8104
8474
  /** Primary-key property names for a MySQL table, including table-level composite keys. */
8105
8475
  const mysqlPrimaryKeyPropNames = (table) => getPrimaryKeyPropNamesFromConfig(table, getTableConfig$5);
8476
+ const TABLE_ORIGINAL_NAME = Symbol.for("drizzle:OriginalName");
8477
+ const TABLE_SCHEMA = Symbol.for("drizzle:Schema");
8478
+ /** Whether `chunk` reads `table` anywhere — under an alias, in a subquery, however deep. */
8479
+ const readsTable = (chunk, table) => {
8480
+ if (Array.isArray(chunk)) return chunk.some((part) => readsTable(part, table));
8481
+ if (is(chunk, Table)) return chunk[TABLE_ORIGINAL_NAME] === table[TABLE_ORIGINAL_NAME] && chunk[TABLE_SCHEMA] === table[TABLE_SCHEMA];
8482
+ if (is(chunk, SQL)) return readsTable(chunk.queryChunks, table);
8483
+ if (is(chunk, SQL.Aliased)) return readsTable(chunk.sql, table);
8484
+ if (is(chunk, Subquery)) return readsTable(chunk._.sql, table);
8485
+ return false;
8486
+ };
8487
+ /**
8488
+ * The `where` of an UPDATE or DELETE, in a form MySQL accepts. MySQL refuses a statement whose
8489
+ * `where` reads the table it writes (error 1093, "You can't specify target table … for update
8490
+ * in FROM clause") — which a relation filter through a self-referencing relation, or a scope
8491
+ * written as one, does. Such a filter is resolved to the primary keys it matches first, through
8492
+ * a derived table MySQL materializes (`DISTINCT` keeps the optimizer from merging it back in),
8493
+ * and the write matches those keys. Any other filter is returned as it is.
8494
+ */
8495
+ const mysqlWriteFilter = (table, filters) => {
8496
+ if (!filters || !readsTable(filters, table)) return filters;
8497
+ const columns = getColumns(table);
8498
+ const keys = mysqlPrimaryKeyPropNames(table).map((name) => columns[name]);
8499
+ if (!keys.length) return filters;
8500
+ const keyList = sql.join(keys.map((column) => sql.identifier(column.name)), sql`, `);
8501
+ const target = keys.length === 1 ? sql`${keys[0]}` : sql`(${sql.join(keys, sql`, `)})`;
8502
+ return sql`${target} in (select ${keyList} from (select distinct ${sql.join(keys, sql`, `)} from ${table} where ${filters}) as ${sql.identifier("dgql_write_target")})`;
8503
+ };
8106
8504
  const { prepareBuild, forEachTable, finalizeBuild } = createSchemaBuilder({
8107
8505
  tableClass: MySqlTable,
8108
8506
  getTableConfig: getTableConfig$5,
@@ -8156,7 +8554,7 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
8156
8554
  tableFilters,
8157
8555
  cacheCtx
8158
8556
  }) : void 0;
8159
- const updateManyGenerated = updateManyInput ? generateUpdateMany$2(db, tableName, schema[tableName], updateManyInput, updateManyFieldName, filterCtx, mutationTxCtx, policies) : void 0;
8557
+ const updateManyGenerated = updateManyInput ? generateUpdateMany$2(db, tableName, schema[tableName], updateManyInput, updateManyFieldName, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies) : void 0;
8160
8558
  const deleteGenerated = tableFeatures.delete ? generateDelete(db, tableName, schema[tableName], tableFilters, deleteFieldName, false, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies) : void 0;
8161
8559
  const deleteSingleGenerated = tableFeatures.delete ? generateDelete(db, tableName, schema[tableName], tableFilters, deleteSingleFieldName, true, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies) : void 0;
8162
8560
  const restoreGenerated = softDeleteInfo ? generateDelete(db, tableName, schema[tableName], tableFilters, restoreFieldName, false, tableFeatures.requireWhere, filterCtx, mutationTxCtx, policies, true) : void 0;
@@ -8173,7 +8571,8 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
8173
8571
  filterCtx,
8174
8572
  txCtx: mutationTxCtx,
8175
8573
  policies,
8176
- returnsRows: false
8574
+ returnsRows: false,
8575
+ writeFilter: (filters) => mysqlWriteFilter(schema[tableName], filters)
8177
8576
  }) : void 0;
8178
8577
  const deleteCountGenerated = tableFeatures.delete && tableFeatures.countMutations ? generateWriteCount({
8179
8578
  db,
@@ -8186,7 +8585,8 @@ const generateSchemaData$2 = (db, schema, relations, options) => {
8186
8585
  filterCtx,
8187
8586
  txCtx: mutationTxCtx,
8188
8587
  policies,
8189
- returnsRows: false
8588
+ returnsRows: false,
8589
+ writeFilter: (filters) => mysqlWriteFilter(schema[tableName], filters)
8190
8590
  }) : void 0;
8191
8591
  const hardDeleteMeta = softDeleteInfo?.hardDelete ? { hardDelete: true } : {};
8192
8592
  const withMeta = (generated, meta) => ({
@@ -8298,7 +8698,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8298
8698
  * one-element list.
8299
8699
  */
8300
8700
  const generateInsert = (build, opts) => {
8301
- const { db, tables, relationMap, typeNameMapper, txCtx, nested, limits, policies, resolveName } = build;
8701
+ const { db, tables, relationMap, typeNameMapper, filterCtx, txCtx, nested, limits, policies, resolveName } = build;
8302
8702
  const { tableName, table, baseType, fieldName, typeName, single, conflictDoNothing = false } = opts;
8303
8703
  const queryArgs = { values: { type: single ? new GraphQLNonNull(baseType) : new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(baseType))) } };
8304
8704
  const pkNames = primaryKeyPropNames(table);
@@ -8322,6 +8722,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8322
8722
  const input = nestedEntries ? [] : applyContextValuesAll(remapFromGraphQLArrayInput(entries ? entries.map((entry) => entry.columns) : supplied, table), contextColumns, context);
8323
8723
  const { columns, hasRelations, withParams } = mutationSelection(info, {
8324
8724
  relationMap,
8725
+ filterCtx,
8325
8726
  tables,
8326
8727
  tableName,
8327
8728
  typeName,
@@ -8400,6 +8801,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8400
8801
  const input = nestedEntries ? [] : applyContextValuesAll(remapFromGraphQLArrayInput(entries ? entries.map((entry) => entry.columns) : supplied, table), contextColumns, context);
8401
8802
  const { columns, hasRelations, withParams } = mutationSelection(info, {
8402
8803
  relationMap,
8804
+ filterCtx,
8403
8805
  tables,
8404
8806
  tableName,
8405
8807
  typeName,
@@ -8425,18 +8827,9 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8425
8827
  buildWhere: (where) => extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope))
8426
8828
  });
8427
8829
  const batchQuery = (executor, { values: rows, plan }) => {
8428
- let query = executor.insert(table).values(rows).returning(returning);
8830
+ const query = executor.insert(table).values(rows).returning(returning);
8429
8831
  const setWhere = withScope(scope, tableName, table, plan.setWhere);
8430
- query = plan.action === "NOTHING" ? query.onConflictDoNothing(plan.target ? {
8431
- target: plan.target,
8432
- where: plan.targetWhere
8433
- } : void 0) : query.onConflictDoUpdate({
8434
- target: plan.target,
8435
- targetWhere: plan.targetWhere,
8436
- set: plan.set,
8437
- setWhere
8438
- });
8439
- return query;
8832
+ return applyConflictPlan(query, table, plan, setWhere);
8440
8833
  };
8441
8834
  if (batches.length === 1) return await batchQuery(target, batches[0]);
8442
8835
  return target.transaction((tx) => {
@@ -8499,6 +8892,7 @@ const buildWriteResolvers = (primaryKeyPropNames) => {
8499
8892
  await before();
8500
8893
  const { columns, hasRelations, withParams } = mutationSelection(info, {
8501
8894
  relationMap,
8895
+ filterCtx,
8502
8896
  tables,
8503
8897
  tableName,
8504
8898
  typeName,
@@ -8737,7 +9131,8 @@ const createSchemaDataGenerator = (adapter) => {
8737
9131
  table: schema[tableName],
8738
9132
  updateManyInput,
8739
9133
  fieldName: updateManyFieldName,
8740
- typeName
9134
+ typeName,
9135
+ requireWhere: tableFeatures.requireWhere
8741
9136
  }) : void 0;
8742
9137
  const deleteGenerated = tableFeatures.delete ? generateDelete(writeBuild, {
8743
9138
  tableName,
@@ -8992,7 +9387,8 @@ const runUpdateManyBatch = ({ executor, entries, table, returning, nested, table
8992
9387
  tableName,
8993
9388
  ops: entry.ops,
8994
9389
  write,
8995
- request
9390
+ request,
9391
+ existing: true
8996
9392
  }) : await write(tx, {}));
8997
9393
  }
8998
9394
  return results;
@@ -9013,7 +9409,7 @@ const runUpdateManyBatch = ({ executor, entries, table, returning, nested, table
9013
9409
  const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateManyBatch) => {
9014
9410
  return (build, opts) => {
9015
9411
  const { db, tables, relationMap, typeNameMapper, filterCtx, txCtx, nested, limits, policies, resolveName } = build;
9016
- const { tableName, table, updateManyInput, fieldName, typeName } = opts;
9412
+ const { tableName, table, updateManyInput, fieldName, typeName, requireWhere } = opts;
9017
9413
  const queryArgs = { updates: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(updateManyInput))) } };
9018
9414
  const pkNames = primaryKeyPropNames(table);
9019
9415
  return writeResolver({
@@ -9033,6 +9429,7 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
9033
9429
  const contextColumns = policies?.contextValues?.(tableName);
9034
9430
  const { columns, hasRelations, withParams } = mutationSelection(info, {
9035
9431
  relationMap,
9432
+ filterCtx,
9036
9433
  tables,
9037
9434
  tableName,
9038
9435
  typeName,
@@ -9052,7 +9449,14 @@ const createUpdateManyGenerator = (primaryKeyPropNames, runBatch = runUpdateMany
9052
9449
  return {
9053
9450
  set: input,
9054
9451
  ops,
9055
- filters: withScope(scope, tableName, table, where ? extractFilters(table, tableName, where, relationFilterCtx(filterCtx, tableName, scope)) : void 0)
9452
+ filters: scopedWhere({
9453
+ scope,
9454
+ tableName,
9455
+ table,
9456
+ where,
9457
+ relationCtx: relationFilterCtx(filterCtx, tableName, scope),
9458
+ required: requireWhere
9459
+ })
9056
9460
  };
9057
9461
  });
9058
9462
  const anyNested = entries.some((entry) => entry.ops);
@@ -9141,7 +9545,8 @@ const runSqliteUpdateManyBatch = ({ executor, entries, table, returning, anyNest
9141
9545
  tableName,
9142
9546
  ops: entry.ops,
9143
9547
  write,
9144
- request
9548
+ request,
9549
+ existing: true
9145
9550
  }) : write(tx, {});
9146
9551
  };
9147
9552
  return executor.transaction((tx) => {
@@ -9171,6 +9576,7 @@ const sqliteSchemaData = createSchemaDataGenerator({
9171
9576
  preflight: (db, options) => {
9172
9577
  if (options.features.nestedWrites && db.resultKind === "sync") throw new Error("Drizzle-GraphQL Error: features.nestedWrites requires an asynchronous SQLite driver (e.g. libsql). Synchronous drivers cannot run the multi-statement transaction a nested write needs.");
9173
9578
  if (options.transactions && db.resultKind === "sync") throw new Error("Drizzle-GraphQL Error: transactions: 'auto' requires an asynchronous SQLite driver (e.g. libsql). Synchronous drivers cannot hold a transaction open across resolvers.");
9579
+ if (options.policies?.onWrite && db.resultKind === "sync") throw new Error("Drizzle-GraphQL Error: onWrite requires an asynchronous SQLite driver (e.g. libsql). Synchronous drivers commit before an awaited hook runs, so a throwing hook could not roll the mutation back.");
9174
9580
  },
9175
9581
  generateUpdateMany
9176
9582
  });