@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/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`
@@ -1406,6 +1514,8 @@ enum. Rows sharing the same combination of those columns collapse to one:
1406
1514
  distinct value **per parent** — the window is partitioned by the foreign key as well as
1407
1515
  the requested columns
1408
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))
1409
1519
 
1410
1520
  It runs as an extra `row_number() over (partition by …)` query that picks the surviving
1411
1521
  rows' primary keys, after which the main query is narrowed to them — so it needs the same
@@ -1422,12 +1532,25 @@ supplies no `orderBy` and the table has no [default ordering](#default-ordering)
1422
1532
  - list queries with `limit` and/or `offset`
1423
1533
  - `<tableName>Single` queries, which are an implicit `limit 1`
1424
1534
  - to-many relation fields with `limit` and/or `offset` (as a tiebreak appended to any
1425
- `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)
1426
1538
 
1427
1539
  An explicit `orderBy` always takes precedence, composite primary keys are ordered by every
1428
1540
  key column, and an unpaginated list query of a table with no `defaults` entry is left
1429
1541
  unordered so no sort is paid for.
1430
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
+
1431
1554
  ## Default ordering
1432
1555
 
1433
1556
  A table usually has one ordering that is right nearly every time it is read — newest first,
@@ -1615,7 +1738,10 @@ mutation {
1615
1738
  exclusion constraint matching" error
1616
1739
  - `target` can be a partial unique index — the `WHERE deleted_at IS NULL` companion of
1617
1740
  [soft delete](#soft-delete), say. Its predicate goes into `ON CONFLICT (…) WHERE …`, so a
1618
- 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`
1619
1745
  - `contextValues` columns are never overwritten on conflict — see
1620
1746
  [columns the server owns](#columns-the-server-owns)
1621
1747
  - `action: NOTHING` inserts nothing and returns nothing for the conflicting row. An `UPDATE`
@@ -1655,7 +1781,9 @@ Dialect differences:
1655
1781
  no predicate, so `<Type>OnConflict` there has only `action` and `update`. Because the
1656
1782
  conflict can be on any unique key, the default overwrite leaves out the primary key as well
1657
1783
  — copying a new row's key over the conflicting row's would move it. `action: NOTHING`
1658
- 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
1659
1787
  mutation
1660
1788
 
1661
1789
  The build-wide `conflictDoNothing` option is deprecated in favour of this: it applies to every
@@ -1728,6 +1856,11 @@ mutation {
1728
1856
  - When the parent write produces no row — an update whose `where` matched nothing, an
1729
1857
  insert or upsert a conflict turned into a no-op — whatever a parent-side `create` inserted
1730
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
1731
1864
  - The returned rows reflect the nested write: on a self-referencing relation
1732
1865
  (`reports: { connect: … }` on the row itself), where a child-side statement rewrites the
1733
1866
  parent row, the parent rows are read back after it
@@ -1740,7 +1873,9 @@ A relation declared with a `where` (`publishedPosts: r.many.posts({ …, where:
1740
1873
  is written through the same way it is read: only the rows it shows.
1741
1874
 
1742
1875
  - `disconnect`, `set`, and the detaching a to-one attach does, reach only rows that satisfy
1743
- 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`
1744
1879
  - `connect` and `set` attach only rows that satisfy it, so the relation shows what was
1745
1880
  attached. A to-one `connect` must match exactly one such row
1746
1881
  - There is no `create`: the inserted row is attached whether or not it satisfies the
@@ -1749,7 +1884,9 @@ is written through the same way it is read: only the rows it shows.
1749
1884
  - A many-to-many `set` unlinks only the links to rows the `where` shows
1750
1885
  - A relation that inherits its `where` from the other side (declared without `from` / `to`)
1751
1886
  filters the parent rather than the target. Writing through it on a parent row it is
1752
- 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
1753
1890
 
1754
1891
  ### The target table's own rules
1755
1892
 
@@ -1945,7 +2082,9 @@ deliberate rather than accidental:
1945
2082
  The plural mutations still accept a missing `where` (a full-table write) by default. To rule
1946
2083
  that out at the type level, `features: { requireWhere: true }` makes `where` non-null on
1947
2084
  `update<Table>` / `delete<Table>` and rejects a `where` with no filters, exactly like the
1948
- 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.
1949
2088
 
1950
2089
  ## Bounding page size
1951
2090
 
@@ -2014,8 +2153,8 @@ So `users(limit: 20) { id posts(limit: 5) { id } }` costs `20 * (1 + 5 * 1)` = 1
2014
2153
  same query without limits costs `10 * (1 + 10 * 1)` = 110.
2015
2154
 
2016
2155
  A [limit policy](#bounding-page-size) feeds into the hint: a field with no `limit` is priced at
2017
- its `defaultLimit` rather than `defaultListSize`, and nothing is priced above `maxLimit`, so the
2018
- 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.
2019
2158
 
2020
2159
  The hints do nothing until you install a complexity rule, so they are generated by default:
2021
2160
 
@@ -2274,7 +2413,13 @@ are untouched.
2274
2413
  does this) — it keys the per-request transaction state
2275
2414
  - `timeoutMs` (default `30000`) rolls back an abandoned transaction, e.g. when the host
2276
2415
  server stopped calling resolvers because of a non-null completion error, instead of
2277
- 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
2278
2423
  - A document that mixes in mutation fields this build did not generate is left alone: their
2279
2424
  completion cannot be tracked, so no transaction is opened
2280
2425
  - It never nests — an executor supplied on the context wins, as below
@@ -2412,6 +2557,10 @@ The payload:
2412
2557
  not have to know which.
2413
2558
  - **Throwing rolls the mutation back.** A hook that can only log is what a logging plugin
2414
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'`.
2415
2564
  - **`rows` is empty** at the `before` position, and on MySQL, whose mutations return no rows.
2416
2565
  The `before` position and `args` still work there.
2417
2566
  - With `nestedWrites`, the hooks of **every table a nested write reaches** fire too —
@@ -2569,7 +2718,9 @@ const finalSchema = addResolversToSchema({
2569
2718
  excludes that relation from `with:` (and from the mutation eager re-fetch).
2570
2719
 
2571
2720
  Opting a relation out does **not** remove its field — it keeps resolving lazily via the
2572
- 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
2573
2724
  many-to-many (`.through()`) relation whose junction has a composite key cannot be loaded
2574
2725
  lazily (`DRIZZLE_UNSUPPORTED_RELATION`); leave it eager-loaded. Table
2575
2726
  and relation names are the Drizzle schema keys (e.g. `Users`, `posts`), matching the keys