@vantreeseba/drizzle-graphql 15.1.5 → 16.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -154,7 +154,7 @@ const { schema } = buildSchema(db, {
154
154
  nestedWrites: true, // relation fields on the create/update inputs (off by default)
155
155
  fieldUpdateOperations: true, // increment/push operations on the update input (off by default)
156
156
  countMutations: true, // update<Table>Count / delete<Table>Count mutations (off by default)
157
- requireWhere: true, // make `where` non-null on plural update/delete (off by default)
157
+ requireWhere: true, // require a `where` on plural update/delete/updateMany (off by default)
158
158
  uniqueKeyFilters: true, // a `where` field per compound unique constraint (off by default)
159
159
  },
160
160
  })
@@ -238,7 +238,7 @@ An excluded **column** disappears from every surface at once:
238
238
  | `<Type>OrderBy` | field removed |
239
239
  | `<Type>MinAggregate` / `<Type>MaxAggregate` / … | field removed |
240
240
  | `<Type>DistinctColumn` / `<Type>GroupByColumn` | value removed |
241
- | `<Type>ConflictColumn` (upsert targets) | value removed |
241
+ | `<Type>ConflictTarget` / `<Type>UpdateColumn` (upsert `onConflict`) | value removed |
242
242
 
243
243
  Two details worth stating plainly:
244
244
 
@@ -313,7 +313,9 @@ Two things it deliberately does not do:
313
313
 
314
314
  A relation filter or an ordering through a relation sees exactly the rows the relation field
315
315
  would return. Through a `.through()` relation, the junction table's own `scope` and soft delete
316
- apply as well. A scope that returns a filter object is the server's own predicate, so the
316
+ apply as well — to the relation field, its `<relation>Aggregate` and a relation filter alike,
317
+ so a link row the caller cannot read links nothing on any of them (see
318
+ [junction rows](#junction-rows)). A scope that returns a filter object is the server's own predicate, so the
317
319
  relations it reaches through are read without their targets' scopes.
318
320
 
319
321
  Returning `undefined` means *no restriction for this context*, so one hook can let an
@@ -566,6 +568,27 @@ field shows. They take no `deleted` argument of their own.
566
568
  `scope` changes reads only. A nested `connect` / `disconnect` / `set` still refuses to attach a
567
569
  marked row under either setting, and the table's own writes are unaffected.
568
570
 
571
+ #### Junction rows
572
+
573
+ A `.through()` relation links only through the junction rows the junction table's own `scope`
574
+ and soft delete keep, on every path that reads it: the relation field (eager or batched,
575
+ paginated or `distinct`), `<relation>Aggregate`, and a relation filter. A marked junction row is
576
+ a removed link, so the junction is always read at `EXCLUDE`:
577
+
578
+ - The relation's `deleted` argument is about the **target** rows. `tags(deleted: INCLUDE)`
579
+ brings back marked tags that are still linked, never a tag whose link was marked.
580
+ - The junction's own `softDelete.scope` only decides what the junction table's own fields
581
+ return. `'root'` keeps a retired row rendering where a relation points *at* it; a relation
582
+ passes *through* a junction row and never renders it, so there is nothing for `'root'` to
583
+ keep, and a marked link stays hidden under either setting.
584
+
585
+ Drizzle's `with:` joins the junction itself and offers no way to add a condition to it, so a
586
+ relation whose junction has a `scope` or soft delete is loaded by its field resolver's batch
587
+ loader rather than with its parent — still one query per relation per request, not one per
588
+ parent. A junction joined on more than one column per side cannot be loaded that way, so
589
+ selecting such a relation while its junction has a policy fails with
590
+ `DRIZZLE_UNSUPPORTED_RELATION` instead of returning links the policy hides.
591
+
569
592
  ### Purging a row
570
593
 
571
594
  Soft delete and purge are not alternatives — emptying the trash, reclaiming a unique key and
@@ -681,6 +704,36 @@ mutation {
681
704
  `2026-09-15T10:00:00.000Z` — and filter operands on such a column are floored the same way,
682
705
  so `eq` with the value you wrote finds the row you wrote it to
683
706
 
707
+ Some columns map to built-in GraphQL types rather than a custom scalar:
708
+
709
+ | Drizzle column | GraphQL type | Transported as |
710
+ | --- | --- | --- |
711
+ | SQLite `integer()` (number mode) | `Float` | a JS number, exact up to 2^53 |
712
+ | Postgres `bytea`, SQLite `blob({ mode: 'buffer' })` | `[Int!]` | the bytes, each 0–255 |
713
+ | Postgres `point` / `line` (tuple mode), PostGIS `geometry` (tuple mode), `vector`, `halfvec` | `[Float!]` | the tuple or vector |
714
+ | Postgres `point({ mode: 'xy' })`, `line({ mode: 'abc' })` | `JSON` | the object, `{ x, y }` / `{ a, b, c }` |
715
+
716
+ - **SQLite `integer()`** holds any 64-bit integer, a rowid primary key included, so it is a
717
+ `Float`: GraphQL's `Int` is 32-bit and cannot serialize anything past 2^31. Drizzle reads
718
+ the column as a JS number, so values are exact up to `Number.MAX_SAFE_INTEGER`; declare it
719
+ `blob({ mode: 'bigint' })` (a `BigInt`) for more. Its `'boolean'` and `'timestamp'` modes
720
+ keep `Boolean` and `DateTime`
721
+ - A **tuple** must have its type's length — 2 for a point, 3 for a line — or the write is
722
+ rejected
723
+ - An **array column** (`bigint().array()`, `timestamp().array()`, …) is a list of its element
724
+ type, and each element is converted as a single value would be, in writes and in the `has` /
725
+ `hasSome` / `hasEvery` operands alike
726
+
727
+ > **Upgrading — SQLite `integer()` columns.** A SQLite `integer()` column in number mode (the
728
+ > default) used to be `Int`, and reading a value past 2^31 failed with "Int cannot represent
729
+ > non 32-bit signed integer value". It is now `Float` in every position: the object field, the
730
+ > create / update inputs, primary-key and unique-key arguments, `FloatFilter` operands, aggregate
731
+ > `min` / `max` and group keys, and `FloatFieldUpdate`. Variables typed `Int` for these fields
732
+ > become `Float` (an `Int` variable is a validation error in a `Float` position); literals and
733
+ > the values themselves are unchanged. `integer({ mode: 'boolean' })` and the timestamp modes are
734
+ > not affected. A SQLite `blob({ mode: 'buffer' })` column, which used to be `JSON` and could
735
+ > not be written, is now `[Int!]`.
736
+
684
737
  The scalars are exported if you need them in a hand-written schema:
685
738
 
686
739
  ```Typescript
@@ -797,11 +850,16 @@ type Posts {
797
850
 
798
851
  - One type, one filter input (`StatusEnumFilter`), so a client variable typed `StatusEnum!`
799
852
  is passable to either table
800
- - The declared name is PascalCased and suffixed: `user_status` → `UserStatusEnum`
853
+ - The declared name is PascalCased and suffixed: `user_status` → `UserStatusEnum`. A name
854
+ that would start with a digit gets a leading underscore: `2fa_mode` → `_2faModeEnum`
801
855
  - A column that lists its values inline (`text('tier', { enum: ['free', 'paid'] })`) has no
802
856
  declared name to share, so it keeps a per-column type — `UsersTierEnum`
803
857
  - Two different enums that would land on the same GraphQL name (`status` in two Postgres
804
858
  schemas) fail the build rather than silently sharing a type
859
+ - Ordering by an enum column, its `min` / `max`, and cursor pages over it follow the
860
+ database's own rule. PostgreSQL and MySQL sort an enum by declaration order (`active` before
861
+ `archived` because it is declared first). SQLite has no enum type — the column is `text`
862
+ with the values checked by Drizzle — so there it sorts alphabetically
805
863
 
806
864
  `enumNameMapper` overrides the name. Returning `undefined` keeps the default, so a mapper
807
865
  only has to handle the cases it cares about:
@@ -831,8 +889,10 @@ lists is the build-time error above.
831
889
  ### Enum value names
832
890
 
833
891
  A value's GraphQL name is the stored value itself, or `Option<index>` when the stored value
834
- is not a usable GraphQL name. `enumValueNameMapper` renames the values — most often to follow
835
- GraphQL's SCREAMING_CASE convention while the database keeps its lowercase values:
892
+ is not a usable GraphQL name — one with a character outside `[_a-zA-Z0-9]` (`hot-fix`), one
893
+ that starts with a digit (`2xl`), or `true` / `false` / `null`. `enumValueNameMapper` renames
894
+ the values — most often to follow GraphQL's SCREAMING_CASE convention while the database keeps
895
+ its lowercase values:
836
896
 
837
897
  ```Typescript
838
898
  export const Runs = pgTable('runs', {
@@ -1052,6 +1112,27 @@ by hand. Under `insensitive` the operand stays a literal and stays a bound param
1052
1112
  from the numeric, uuid, boolean, timestamp and enum filters (see
1053
1113
  [Which operators a column gets](#which-operators-a-column-gets))
1054
1114
 
1115
+ **On MySQL, plain comparisons may already ignore case.** MySQL compares strings under the
1116
+ column's collation, and its default (`utf8mb4_0900_ai_ci`, like every `*_ai_ci` collation) is
1117
+ case- **and** accent-insensitive. So without `insensitive`, `eq: "dan"` matches `Dan` and `dän`
1118
+ there, and the same holds for `ne`, the ordering comparisons, `inArray`, `orderBy`, `min` /
1119
+ `max`, `distinct`, `groupBy` and the `like` / `startsWith` / `endsWith` / `contains` operators.
1120
+ PostgreSQL compares exactly. That is the database's configured behaviour, not something this
1121
+ library adds: give a column a binary or `*_as_cs` collation (`varchar(…) COLLATE utf8mb4_bin`)
1122
+ where it has to compare exactly.
1123
+
1124
+ ### Case sensitivity of the pattern operators on SQLite
1125
+
1126
+ `like` / `notLike` and the safe `startsWith` / `endsWith` / `contains` are case-sensitive, as
1127
+ they are on PostgreSQL: `contains: "App"` matches `Apple` and not `apple`. SQLite's own `LIKE`
1128
+ folds ASCII case, so on SQLite they compile to `GLOB` instead, with `%` / `_` translated to its
1129
+ `*` / `?` and every other wildcard in the operand matched literally. The `i`-prefixed operators
1130
+ and `insensitive: true` fold case on SQLite as everywhere else.
1131
+
1132
+ > **Upgrading — SQLite.** These operators used to compile to `LIKE` on SQLite and so matched
1133
+ > case-insensitively. Queries that relied on that should use the `i`-prefixed operator or
1134
+ > `insensitive: true`.
1135
+
1055
1136
  ## Which operators a column gets
1056
1137
 
1057
1138
  A filter input carries the operators that can express a question about that column type, and
@@ -1068,7 +1149,8 @@ no others. The set follows the column's data type, never its name:
1068
1149
 
1069
1150
  The pattern operators are `like` / `notLike` / `ilike` / `notIlike`, the safe
1070
1151
  `startsWith` / `endsWith` / `contains` and their `i`-prefixed forms, plus the `insensitive`
1071
- flag that modifies them.
1152
+ flag that modifies them. MySQL and SQLite have no `ILIKE`, so there `ilike` / `notIlike`
1153
+ compile to `lower(column) LIKE lower(pattern)`, the same form `insensitive` uses.
1072
1154
 
1073
1155
  An operand of `null` leaves its operator out, as though it had not been sent. `false` is an
1074
1156
  ordinary value: `active: { eq: false }` matches the rows that are false, `ne: false` the rows
@@ -1211,6 +1293,23 @@ each one — Postgres errors outright on a bad `::numeric`, MySQL quietly casts
1211
1293
  string to `0`, and SQLite's cross-type ordering would otherwise sort every string above every
1212
1294
  number.
1213
1295
 
1296
+ SQLite has no boolean type, and its `json_extract` hands a JSON `true` / `false` back as `1` /
1297
+ `0`, so its path reads check `json_type` as well: `eq: true` matches only a JSON `true`, never
1298
+ the number `1`, a number operand never matches a boolean, and a text read sees `"true"` /
1299
+ `"false"`, as PostgreSQL's does.
1300
+
1301
+ **Whole-document `eq` / `ne`** compare documents, not their text. Object key order and
1302
+ whitespace do not matter; array order does, and so does the difference between `1` and `true`.
1303
+ PostgreSQL compares as `jsonb` (so a plain `json` column, which has no `=` of its own, works
1304
+ too), MySQL against `CAST(? AS JSON)`, and SQLite by matching the two documents' nodes both ways.
1305
+ Like `=` / `<>` on any column, neither matches a row whose column is SQL `NULL`.
1306
+
1307
+ **JSON columns cannot be ordered.** A JSON document has no meaningful order — PostgreSQL's
1308
+ `json` has none at all, and MySQL's JSON ordering is not one a cursor can resume from — so
1309
+ JSON columns are left out of `<Type>OrderBy` (and so out of cursor pagination and every relation
1310
+ `orderBy`) and out of `<Type>DistinctColumn`, and naming one in `defaults.<table>.orderBy` fails
1311
+ the build. Order by a generated column or a path you extract yourself instead.
1312
+
1214
1313
  Note that `contains` means different things at the two levels, because a path names a scalar:
1215
1314
  on the column it is structural JSON containment (Postgres `@>` / MySQL `JSON_CONTAINS`),
1216
1315
  inside a `path` filter it is substring matching on the extracted value.
@@ -1252,13 +1351,19 @@ following the same naming rules as the other generated queries:
1252
1351
  ```
1253
1352
 
1254
1353
  - `count` — number of matching rows (`Int!`)
1255
- - `avg` / `sum` — one nullable `Float` field per numeric column
1354
+ - `avg` / `sum` — one nullable `Float` field per numeric column. On MySQL `avg` is computed
1355
+ as a `DOUBLE`, since MySQL's own `AVG` over an integer column is a `DECIMAL` rounded to four
1356
+ places
1256
1357
  - `min` / `max` — one field per orderable column (numbers, strings, enums, dates, bigints),
1257
- typed exactly like that column is in the table's own type
1358
+ typed exactly like that column is in the table's own type. PostgreSQL `uuid`, `macaddr`,
1359
+ `macaddr8` and `bit` columns are left out, because PostgreSQL has no `min` / `max` for them,
1360
+ and so is a SQLite `blob({ mode: 'bigint' })` column, whose bytes compare as text (`9` above
1361
+ `10`)
1258
1362
  - `countNonNull` — `Int!` per column: how many matching rows have a non-null value there.
1259
1363
  Every column qualifies, since `count(col)` is valid whatever the type
1260
1364
  - `countDistinct` — `Int!` per column: how many distinct non-null values there are. Limited to
1261
- the same columns as `min` / `max`, because counting distinct values needs an equality operator
1365
+ the columns with an equality operator: the `min` / `max` columns plus the ones listed above
1366
+ that compare but have no `min` / `max`
1262
1367
 
1263
1368
  Columns that have no meaningful ordering — booleans, arrays, JSON, buffers, and geometry —
1264
1369
  are left out of these types, and the `avg` / `sum` / `min` / `max` fields themselves are
@@ -1299,7 +1404,7 @@ columns you group by.
1299
1404
  ```
1300
1405
 
1301
1406
  - `groupBy` is required and takes one or more values of the table's `<Type>GroupByColumn`
1302
- enum. It holds the orderable columns plus booleans — anything the database can group on.
1407
+ enum. It holds the `countDistinct` columns plus booleans — anything the database can group on.
1303
1408
  An empty list is an error, and repeated columns are ignored
1304
1409
  - `group` is a `<Type>GroupKeys!` object with one nullable field per groupable column, typed
1305
1410
  like the table's own column. Columns the query did not group by come back as `null`, which
@@ -1328,6 +1433,9 @@ columns you group by.
1328
1433
  filter per numeric column (the comparison is numeric, so a `max` over a text or date
1329
1434
  column has no `having` entry even though the aggregate type returns it),
1330
1435
  `countNonNull` one per column, and `countDistinct` one per column `countDistinct` exposes
1436
+ - The operand is a `Float` whatever the aggregate's own type, so `count: { gt: 1.5 }` works:
1437
+ on PostgreSQL, where an integer aggregate would otherwise give the parameter its integer
1438
+ type, the aggregate is compared as `numeric`
1331
1439
  - Every entry in a `having` is ANDed together
1332
1440
  - A group filter does not need to be in the selection — `having: { count: { gt: 3 } }` works
1333
1441
  whether or not you asked for `count`
@@ -1375,7 +1483,10 @@ can count or summarise related rows without fetching them:
1375
1483
  All parents in a selection are aggregated with a single
1376
1484
  `SELECT <fk>, … WHERE <fk> IN (…) GROUP BY <fk>` per request, so `postsAggregate` on a list of
1377
1485
  users is one extra query, not one per user. Differently-aliased selections with different
1378
- `where` arguments are batched separately.
1486
+ `where` arguments are batched separately. A batch whose parent keys would bind more than
1487
+ 10,000 parameters (10,000 parents on a single-column key, 5,000 on a two-column one) is split
1488
+ into several such queries, which keeps it within SQLite's 32,766- and Postgres/MySQL's
1489
+ 65,535-parameter limits; the batched relation fields split the same way.
1379
1490
 
1380
1491
  ## Distinct
1381
1492
 
@@ -1403,6 +1514,8 @@ enum. Rows sharing the same combination of those columns collapse to one:
1403
1514
  distinct value **per parent** — the window is partitioned by the foreign key as well as
1404
1515
  the requested columns
1405
1516
  - `distinct` is not on single queries — those return one row either way
1517
+ - JSON columns are not in `<Type>DistinctColumn` (see
1518
+ [Filtering inside JSON documents](#filtering-inside-json-documents))
1406
1519
 
1407
1520
  It runs as an extra `row_number() over (partition by …)` query that picks the surviving
1408
1521
  rows' primary keys, after which the main query is narrowed to them — so it needs the same
@@ -1419,12 +1532,25 @@ supplies no `orderBy` and the table has no [default ordering](#default-ordering)
1419
1532
  - list queries with `limit` and/or `offset`
1420
1533
  - `<tableName>Single` queries, which are an implicit `limit 1`
1421
1534
  - to-many relation fields with `limit` and/or `offset` (as a tiebreak appended to any
1422
- `orderBy` you do supply, so per-parent slices are deterministic)
1535
+ `orderBy` you do supply or [default ordering](#default-ordering) the target has — and under
1536
+ `orderBy: {}` too — so per-parent slices are deterministic and the eager and lazy paths
1537
+ return the same slice)
1423
1538
 
1424
1539
  An explicit `orderBy` always takes precedence, composite primary keys are ordered by every
1425
1540
  key column, and an unpaginated list query of a table with no `defaults` entry is left
1426
1541
  unordered so no sort is paid for.
1427
1542
 
1543
+ `offset` works without `limit` on every dialect. SQLite and MySQL only accept `OFFSET` after a
1544
+ `LIMIT`, so there an offset-only page is sent with a limit meaning "no limit" (`-1` on SQLite,
1545
+ 2^53 - 1 on MySQL).
1546
+
1547
+ `<Type>OrderBy` offers every column that has a meaningful order. JSON columns (see
1548
+ [Filtering inside JSON documents](#filtering-inside-json-documents)) and SQLite
1549
+ `blob({ mode: 'bigint' })` columns, whose bytes sort as text, are left out. A table left with
1550
+ nothing to order by — no such column, and no to-one relation to a table that has one — gets no
1551
+ `<Type>OrderBy` and no `orderBy` argument, on its lists or on relation fields pointing at it, and
1552
+ likewise no `<Type>DistinctColumn` or `distinct` argument when no column qualifies for that.
1553
+
1428
1554
  ## Default ordering
1429
1555
 
1430
1556
  A table usually has one ordering that is right nearly every time it is read — newest first,
@@ -1604,14 +1730,18 @@ mutation {
1604
1730
  out the rest of the row. Listing an unsupplied column in `update` is an error rather than
1605
1731
  a silent no-op
1606
1732
  - That holds per row: a batch whose rows supply different columns is split into one
1607
- statement per run of consecutive rows that write the same columns, run in one transaction,
1608
- so a row that left a column out keeps it. The rows come back in the order they were sent
1733
+ statement per run of consecutive rows that write the same columns, run in one transaction
1734
+ (on a synchronous SQLite driver such as better-sqlite3 too), so a row that left a column out
1735
+ keeps it. The rows come back in the order they were sent
1609
1736
  - `target` must match one of the table's unique constraints exactly; anything else is
1610
1737
  rejected with the list of valid targets, instead of the database's opaque "no unique or
1611
1738
  exclusion constraint matching" error
1612
1739
  - `target` can be a partial unique index — the `WHERE deleted_at IS NULL` companion of
1613
1740
  [soft delete](#soft-delete), say. Its predicate goes into `ON CONFLICT (…) WHERE …`, so a
1614
- conflict is detected only against the rows the index covers
1741
+ conflict is detected only against the rows the index covers. On SQLite the predicate is
1742
+ written with its values inline (SQLite matches it to the index as written, so
1743
+ `status = 'active'` works), and `action: NOTHING` on such a target is spelled as an update
1744
+ that matches no row, since SQLite cannot parse the predicate before `DO NOTHING`
1615
1745
  - `contextValues` columns are never overwritten on conflict — see
1616
1746
  [columns the server owns](#columns-the-server-owns)
1617
1747
  - `action: NOTHING` inserts nothing and returns nothing for the conflicting row. An `UPDATE`
@@ -1651,7 +1781,9 @@ Dialect differences:
1651
1781
  no predicate, so `<Type>OnConflict` there has only `action` and `update`. Because the
1652
1782
  conflict can be on any unique key, the default overwrite leaves out the primary key as well
1653
1783
  — copying a new row's key over the conflicting row's would move it. `action: NOTHING`
1654
- becomes `INSERT IGNORE`, and the mutations return `MutationReturn` like every other MySQL
1784
+ becomes a no-op `ON DUPLICATE KEY UPDATE <key> = <key>` rather than `INSERT IGNORE`, so
1785
+ only a duplicate key is skipped: a value too long for its column or a foreign-key violation
1786
+ still fails the write. The mutations return `MutationReturn` like every other MySQL
1655
1787
  mutation
1656
1788
 
1657
1789
  The build-wide `conflictDoNothing` option is deprecated in favour of this: it applies to every
@@ -1724,6 +1856,11 @@ mutation {
1724
1856
  - When the parent write produces no row — an update whose `where` matched nothing, an
1725
1857
  insert or upsert a conflict turned into a no-op — whatever a parent-side `create` inserted
1726
1858
  for it is rolled back too, so no orphan is left. In a batch only that entry is undone
1859
+ - An upsert whose conflict branch does not write the parent's join column (an
1860
+ `onConflict.update` that leaves it out) would update the existing row without attaching
1861
+ what a parent-side `create` or `connect` produced. That fails with
1862
+ `DRIZZLE_NESTED_WRITE_INVALID` and rolls the write back; list the column in
1863
+ `onConflict.update`, or leave `update` unset so every supplied column is written
1727
1864
  - The returned rows reflect the nested write: on a self-referencing relation
1728
1865
  (`reports: { connect: … }` on the row itself), where a child-side statement rewrites the
1729
1866
  parent row, the parent rows are read back after it
@@ -1736,7 +1873,9 @@ A relation declared with a `where` (`publishedPosts: r.many.posts({ …, where:
1736
1873
  is written through the same way it is read: only the rows it shows.
1737
1874
 
1738
1875
  - `disconnect`, `set`, and the detaching a to-one attach does, reach only rows that satisfy
1739
- the `where` — `publishedPosts: { set: [] }` leaves the drafts attached
1876
+ the `where` — `publishedPosts: { set: [] }` leaves the drafts attached. On a to-one whose
1877
+ key is on the parent, `disconnect` leaves a key that points at a row the `where` hides,
1878
+ and a `connect` that would replace such a key fails with `DRIZZLE_NESTED_WRITE_INVALID`
1740
1879
  - `connect` and `set` attach only rows that satisfy it, so the relation shows what was
1741
1880
  attached. A to-one `connect` must match exactly one such row
1742
1881
  - There is no `create`: the inserted row is attached whether or not it satisfies the
@@ -1745,7 +1884,9 @@ is written through the same way it is read: only the rows it shows.
1745
1884
  - A many-to-many `set` unlinks only the links to rows the `where` shows
1746
1885
  - A relation that inherits its `where` from the other side (declared without `from` / `to`)
1747
1886
  filters the parent rather than the target. Writing through it on a parent row it is
1748
- hidden for fails with `DRIZZLE_NESTED_WRITE_INVALID`
1887
+ hidden for fails with `DRIZZLE_NESTED_WRITE_INVALID`. When the key is on the parent
1888
+ (`connect`, `create` or `disconnect` on a to-one), the row is judged as the write leaves
1889
+ it, so a write that also makes the row visible goes through
1749
1890
 
1750
1891
  ### The target table's own rules
1751
1892
 
@@ -1941,7 +2082,9 @@ deliberate rather than accidental:
1941
2082
  The plural mutations still accept a missing `where` (a full-table write) by default. To rule
1942
2083
  that out at the type level, `features: { requireWhere: true }` makes `where` non-null on
1943
2084
  `update<Table>` / `delete<Table>` and rejects a `where` with no filters, exactly like the
1944
- Single variants. It defaults to `false` for backwards compatibility.
2085
+ Single variants. It also covers `update<Table>Many`: an entry without a `where`, or with one
2086
+ that has no filters, rejects the whole batch with `DRIZZLE_WHERE_REQUIRED` before any entry
2087
+ runs. It defaults to `false` for backwards compatibility.
1945
2088
 
1946
2089
  ## Bounding page size
1947
2090
 
@@ -2010,8 +2153,8 @@ So `users(limit: 20) { id posts(limit: 5) { id } }` costs `20 * (1 + 5 * 1)` = 1
2010
2153
  same query without limits costs `10 * (1 + 10 * 1)` = 110.
2011
2154
 
2012
2155
  A [limit policy](#bounding-page-size) feeds into the hint: a field with no `limit` is priced at
2013
- its `defaultLimit` rather than `defaultListSize`, and nothing is priced above `maxLimit`, so the
2014
- cost matches the rows the query can actually pull.
2156
+ its `defaultLimit` (or at `maxLimit` when the policy sets only that) rather than `defaultListSize`,
2157
+ and nothing is priced above `maxLimit`, so the cost matches the rows the query can actually pull.
2015
2158
 
2016
2159
  The hints do nothing until you install a complexity rule, so they are generated by default:
2017
2160
 
@@ -2270,7 +2413,13 @@ are untouched.
2270
2413
  does this) — it keys the per-request transaction state
2271
2414
  - `timeoutMs` (default `30000`) rolls back an abandoned transaction, e.g. when the host
2272
2415
  server stopped calling resolvers because of a non-null completion error, instead of
2273
- leaking the connection
2416
+ leaking the connection. It must be a positive integer up to `2147483647` (the longest
2417
+ delay `setTimeout` honours), or `Infinity` to turn the timer off; `buildSchema` throws for
2418
+ anything else. With the timer off an abandoned transaction keeps its connection, so prefer
2419
+ a finite value. Fields after a timeout fail with `DRIZZLE_TRANSACTION_ABORTED`, naming the
2420
+ timeout as the cause
2421
+ - A root field whose arguments fail to coerce is answered with an error without its resolver
2422
+ running, so it is not waited for: the transaction commits once the other fields complete
2274
2423
  - A document that mixes in mutation fields this build did not generate is left alone: their
2275
2424
  completion cannot be tracked, so no transaction is opened
2276
2425
  - It never nests — an executor supplied on the context wins, as below
@@ -2408,6 +2557,10 @@ The payload:
2408
2557
  not have to know which.
2409
2558
  - **Throwing rolls the mutation back.** A hook that can only log is what a logging plugin
2410
2559
  already does.
2560
+ - **SQLite needs an asynchronous driver** (libsql, D1). A synchronous one (better-sqlite3,
2561
+ `node:sqlite`) commits before an awaited hook runs, so a throw could not roll anything
2562
+ back; `buildSchema` throws for `onWrite` on one, as it does for `nestedWrites` and
2563
+ `transactions: 'auto'`.
2411
2564
  - **`rows` is empty** at the `before` position, and on MySQL, whose mutations return no rows.
2412
2565
  The `before` position and `args` still work there.
2413
2566
  - With `nestedWrites`, the hooks of **every table a nested write reaches** fire too —
@@ -2486,7 +2639,15 @@ Generated schemas resolve nested relations without N+1 query explosions:
2486
2639
  `ROW_NUMBER() OVER (PARTITION BY …)` window function — still one query, not one per
2487
2640
  parent. A relation joined on several columns (`from: [a, b], to: [x, y]`) is batched,
2488
2641
  grouped and partitioned by all of them, matching the parents with
2489
- `(a = … AND b = …) OR …` instead of `IN (…)`.
2642
+ `(a = … AND b = …) OR …` instead of `IN (…)`, nested as a balanced tree so a batch of
2643
+ thousands of parents stays within SQLite's expression-depth limit. A batch over 10,000 key
2644
+ parameters runs as several queries, each parent read whole by one of them. The parent's join values are bound as the
2645
+ column holds them, not in their GraphQL output form, so a relation joined on a timestamp,
2646
+ `date`, `bigint` or decimal column works on this path and in `<relation>Aggregate` too. A
2647
+ parent row this library remapped keeps its original values for that; a parent your own
2648
+ resolver built in GraphQL form (an ISO string for a timestamp, say) has them mapped back the
2649
+ way an input value would be, which cuts a `'string'`-mode microsecond timestamp to
2650
+ milliseconds — return the row as drizzle read it to keep the full precision.
2490
2651
 
2491
2652
  ```Typescript
2492
2653
  // entities.fieldResolvers is keyed by table name, then relation name
@@ -2557,7 +2718,9 @@ const finalSchema = addResolversToSchema({
2557
2718
  excludes that relation from `with:` (and from the mutation eager re-fetch).
2558
2719
 
2559
2720
  Opting a relation out does **not** remove its field — it keeps resolving lazily via the
2560
- request-scoped batch loader, so the field still works even before you override it. A
2721
+ request-scoped batch loader, so the field still works even before you override it — in a
2722
+ mutation payload as well, where the write returns the join column the relation correlates on
2723
+ even though the selection never named it. A
2561
2724
  many-to-many (`.through()`) relation whose junction has a composite key cannot be loaded
2562
2725
  lazily (`DRIZZLE_UNSUPPORTED_RELATION`); leave it eager-loaded. Table
2563
2726
  and relation names are the Drizzle schema keys (e.g. `Users`, `posts`), matching the keys