@vantreeseba/drizzle-graphql 6.0.0 → 7.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
@@ -104,6 +104,8 @@ const { schema } = buildSchema(db, {
104
104
  delete: false, // delete<Table> / delete<Table>Single mutations
105
105
  upsert: true, // upsert<Table> / upsert<Table>Single mutations (off by default)
106
106
  nestedWrites: true, // relation fields on the create/update inputs (off by default)
107
+ fieldUpdateOperations: true, // increment/push operations on the update input (off by default)
108
+ countMutations: true, // update<Table>Count / delete<Table>Count mutations (off by default)
107
109
  requireWhere: true, // make `where` non-null on plural update/delete (off by default)
108
110
  },
109
111
  })
@@ -111,8 +113,8 @@ const { schema } = buildSchema(db, {
111
113
 
112
114
  - Any flag left out keeps its default of `true`, so `{ features: { delete: false } }`
113
115
  changes nothing else
114
- - `upsert` and `nestedWrites` are the exceptions: both default to `false`, so the upsert
115
- mutations and the relation fields on the write inputs only exist if you ask for them
116
+ - `upsert`, `nestedWrites`, `fieldUpdateOperations` and `countMutations` are the
117
+ exceptions: all four default to `false`, so they only exist if you ask for them
116
118
  - `groupBy` needs `aggregates`: it reuses those output types, so turning `aggregates` off
117
119
  turns the group-by queries off with it
118
120
  - Turning off `insert` or `update` also drops the input type that only that mutation
@@ -467,7 +469,10 @@ enum. Rows sharing the same combination of those columns collapse to one:
467
469
  - `where` is applied **before** rows are collapsed; `limit` and `offset` are applied
468
470
  **after**, so `limit: 10` returns ten distinct rows
469
471
  - Several columns are treated as one combined key, not as independent ones
470
- - `distinct` is available on list queries only — a single query returns one row either way
472
+ - `distinct` is also available on to-many relation fields, where it means one row per
473
+ distinct value **per parent** — the window is partitioned by the foreign key as well as
474
+ the requested columns
475
+ - `distinct` is not on single queries — those return one row either way
471
476
 
472
477
  It runs as an extra `row_number() over (partition by …)` query that picks the surviving
473
478
  rows' primary keys, after which the main query is narrowed to them — so it needs the same
@@ -536,6 +541,32 @@ field, and passing it back as `after` resumes strictly after that row.
536
541
  - If a table has a real column named `cursor`, the column wins and the meta field is
537
542
  skipped
538
543
 
544
+ To-many relation fields take `after` and `distinct` too, with the same meaning they have on
545
+ a root list of the same table — a relation field is a list of the target table, so it gets
546
+ that table's whole pagination surface:
547
+
548
+ ```graphql
549
+ {
550
+ users {
551
+ id
552
+ posts(orderBy: { createdAt: { direction: desc, priority: 1 } }, limit: 10, after: "…") {
553
+ id
554
+ cursor
555
+ }
556
+ }
557
+ }
558
+ ```
559
+
560
+ Drizzle's `with:` clause cannot express either one — keyset needs a predicate over the
561
+ request's own total order, `distinct` needs a window pass, and a `cursor` has to be computed
562
+ from the raw row. So a relation field that is passed `after` or `distinct`, or that selects
563
+ `cursor`, drops out of the eager fetch and resolves through the request-scoped batch loader
564
+ instead, which implements all three. That is still **one query per relation** across every
565
+ parent in the batch (`distinct` adds its key-selection pass, as on a root list) — never one
566
+ per parent. Nothing about the arguments changes; only how many round-trips the request
567
+ costs.
568
+
569
+
539
570
  ## Upsert
540
571
 
541
572
  `features.upsert` adds a pair of mutations per table that insert rows, or update the ones
@@ -660,6 +691,71 @@ and interleaves awaited statements inside a transaction, which a synchronous SQL
660
691
  cannot do — `buildSchema` rejects both combinations rather than generating something that
661
692
  half-works.
662
693
 
694
+ ## Atomic column updates
695
+
696
+ `Update<Table>Input` sets columns to values, so "increment the view count" is a read followed
697
+ by a write of the number the read returned — two round-trips, and a lost update whenever two
698
+ clients read the same number. `features.fieldUpdateOperations` replaces qualifying columns'
699
+ update fields with an operations input, so the arithmetic happens in the database:
700
+
701
+ ```graphql
702
+ mutation {
703
+ updatePostsSingle(where: { id: { eq: 1 } }, set: { views: { increment: 1 } }) {
704
+ views
705
+ }
706
+ }
707
+ ```
708
+
709
+ compiles to `SET views = views + 1`. The flag changes the update inputs only; insert inputs
710
+ are untouched.
711
+
712
+ - **Numeric columns** (`Int`, `Float`, `BigInt`, `Decimal`) take
713
+ `set` / `increment` / `decrement` / `multiply` / `divide`
714
+ - **Array columns** take `set` / `push`, where `push` appends to the existing array
715
+ - Exactly one operation per column. Passing two, or `{}`, is a `GraphQLError` naming the
716
+ column — graphql-js has no `@oneOf` on the version range this library supports, so it is
717
+ enforced at resolve time rather than by the type
718
+ - `set` is the explicit spelling of the plain assignment the field had before the flag, so
719
+ every existing update has a one-key rewrite: `{ views: 5 }` → `{ views: { set: 5 } }`
720
+ - The input types are named for the **scalar**, not the column — every `Int` column in the
721
+ schema shares one `IntFieldUpdate` — and a column claimed by a `scalarOverrides` entry
722
+ keeps its plain field, since the override decides what that column accepts
723
+
724
+ Two things follow the database's own semantics rather than being guarded:
725
+
726
+ - `increment`/`multiply` on an integer column can overflow, and `divide` on one **rounds**
727
+ the way the dialect rounds (PostgreSQL truncates toward zero; use a `Float` or `Decimal`
728
+ column if you need the fraction)
729
+ - `divide: 0` raises the driver's division-by-zero error, surfaced as a `GraphQLError`
730
+
731
+ ## Row counts instead of rows
732
+
733
+ The plural `update<Table>` / `delete<Table>` mutations always `RETURNING *`, which is wasted
734
+ work — and wasted bandwidth — when a client only wants to know how many rows were touched.
735
+ `features.countMutations` adds a count-returning variant of each:
736
+
737
+ ```graphql
738
+ mutation {
739
+ deletePostsCount(where: { status: { eq: "spam" } })
740
+ updatePostsCount(where: { draft: { eq: true } }, set: { draft: false })
741
+ }
742
+ ```
743
+
744
+ - Both return `Int!` — the number of rows the write affected — and add no new output type
745
+ to the schema
746
+ - Arguments are exactly the plural mutation's, so a client swaps one field name for the
747
+ other; `features.requireWhere` applies to them the same way
748
+ - They follow the feature switch of the write they mirror: `updateCount` needs
749
+ `features.update`, `deleteCount` needs `features.delete`
750
+ - **MySQL** gets them too, and they are arguably most useful there: MySQL's other mutations
751
+ return only `MutationReturn { isSuccess }`, so a row count is strictly more information
752
+ - `update<Table>Count` **rejects** nested writes with a `GraphQLError` — a nested write
753
+ needs the parent rows this mutation deliberately does not read back. Use the plural
754
+ mutation for those
755
+ - The count is read from whichever field the driver reports it in (`rowCount`,
756
+ `affectedRows`, `rowsAffected`, `changes`, `count`); a driver that reports none is a
757
+ `GraphQLError` rather than a silent `0`
758
+
663
759
  ## Single-row update & delete
664
760
 
665
761
  Alongside the plural `update<Table>` / `delete<Table>` mutations, every table gets an
@@ -686,6 +782,17 @@ mutation {
686
782
  - The variants are generated under `features.update` / `features.delete`, share the plural
687
783
  mutations' input types, and appear in `entities.mutations` for custom schemas
688
784
 
785
+ ### Mutation return shapes
786
+
787
+ Every mutation that returns rows returns them non-null, with two exceptions that are
788
+ deliberate rather than accidental:
789
+
790
+ | Mutation | Returns | Why |
791
+ | --- | --- | --- |
792
+ | `create<Table>Single` | `Table!`, or `Table` under `conflictDoNothing` | An insert of one row returns that row. It can only fail to produce one when the `conflictDoNothing` build option swallows a conflict, so it is nullable exactly when that option is set |
793
+ | `update<Table>Many` | `[Table]!` | Each entry's updated rows **in entry order**. An entry whose `where` matched nothing contributes `null` in its slot, so the response lines up with the request positionally — the nullable element is the whole point |
794
+ | everything else | `[Table!]!` / `Table!` / `Table` | — |
795
+
689
796
  The plural mutations still accept a missing `where` (a full-table write) by default. To rule
690
797
  that out at the type level, `features: { requireWhere: true }` makes `where` non-null on
691
798
  `update<Table>` / `delete<Table>` and rejects a `where` with no filters, exactly like the
package/dist/README.md CHANGED
@@ -104,6 +104,8 @@ const { schema } = buildSchema(db, {
104
104
  delete: false, // delete<Table> / delete<Table>Single mutations
105
105
  upsert: true, // upsert<Table> / upsert<Table>Single mutations (off by default)
106
106
  nestedWrites: true, // relation fields on the create/update inputs (off by default)
107
+ fieldUpdateOperations: true, // increment/push operations on the update input (off by default)
108
+ countMutations: true, // update<Table>Count / delete<Table>Count mutations (off by default)
107
109
  requireWhere: true, // make `where` non-null on plural update/delete (off by default)
108
110
  },
109
111
  })
@@ -111,8 +113,8 @@ const { schema } = buildSchema(db, {
111
113
 
112
114
  - Any flag left out keeps its default of `true`, so `{ features: { delete: false } }`
113
115
  changes nothing else
114
- - `upsert` and `nestedWrites` are the exceptions: both default to `false`, so the upsert
115
- mutations and the relation fields on the write inputs only exist if you ask for them
116
+ - `upsert`, `nestedWrites`, `fieldUpdateOperations` and `countMutations` are the
117
+ exceptions: all four default to `false`, so they only exist if you ask for them
116
118
  - `groupBy` needs `aggregates`: it reuses those output types, so turning `aggregates` off
117
119
  turns the group-by queries off with it
118
120
  - Turning off `insert` or `update` also drops the input type that only that mutation
@@ -467,7 +469,10 @@ enum. Rows sharing the same combination of those columns collapse to one:
467
469
  - `where` is applied **before** rows are collapsed; `limit` and `offset` are applied
468
470
  **after**, so `limit: 10` returns ten distinct rows
469
471
  - Several columns are treated as one combined key, not as independent ones
470
- - `distinct` is available on list queries only — a single query returns one row either way
472
+ - `distinct` is also available on to-many relation fields, where it means one row per
473
+ distinct value **per parent** — the window is partitioned by the foreign key as well as
474
+ the requested columns
475
+ - `distinct` is not on single queries — those return one row either way
471
476
 
472
477
  It runs as an extra `row_number() over (partition by …)` query that picks the surviving
473
478
  rows' primary keys, after which the main query is narrowed to them — so it needs the same
@@ -536,6 +541,32 @@ field, and passing it back as `after` resumes strictly after that row.
536
541
  - If a table has a real column named `cursor`, the column wins and the meta field is
537
542
  skipped
538
543
 
544
+ To-many relation fields take `after` and `distinct` too, with the same meaning they have on
545
+ a root list of the same table — a relation field is a list of the target table, so it gets
546
+ that table's whole pagination surface:
547
+
548
+ ```graphql
549
+ {
550
+ users {
551
+ id
552
+ posts(orderBy: { createdAt: { direction: desc, priority: 1 } }, limit: 10, after: "…") {
553
+ id
554
+ cursor
555
+ }
556
+ }
557
+ }
558
+ ```
559
+
560
+ Drizzle's `with:` clause cannot express either one — keyset needs a predicate over the
561
+ request's own total order, `distinct` needs a window pass, and a `cursor` has to be computed
562
+ from the raw row. So a relation field that is passed `after` or `distinct`, or that selects
563
+ `cursor`, drops out of the eager fetch and resolves through the request-scoped batch loader
564
+ instead, which implements all three. That is still **one query per relation** across every
565
+ parent in the batch (`distinct` adds its key-selection pass, as on a root list) — never one
566
+ per parent. Nothing about the arguments changes; only how many round-trips the request
567
+ costs.
568
+
569
+
539
570
  ## Upsert
540
571
 
541
572
  `features.upsert` adds a pair of mutations per table that insert rows, or update the ones
@@ -660,6 +691,71 @@ and interleaves awaited statements inside a transaction, which a synchronous SQL
660
691
  cannot do — `buildSchema` rejects both combinations rather than generating something that
661
692
  half-works.
662
693
 
694
+ ## Atomic column updates
695
+
696
+ `Update<Table>Input` sets columns to values, so "increment the view count" is a read followed
697
+ by a write of the number the read returned — two round-trips, and a lost update whenever two
698
+ clients read the same number. `features.fieldUpdateOperations` replaces qualifying columns'
699
+ update fields with an operations input, so the arithmetic happens in the database:
700
+
701
+ ```graphql
702
+ mutation {
703
+ updatePostsSingle(where: { id: { eq: 1 } }, set: { views: { increment: 1 } }) {
704
+ views
705
+ }
706
+ }
707
+ ```
708
+
709
+ compiles to `SET views = views + 1`. The flag changes the update inputs only; insert inputs
710
+ are untouched.
711
+
712
+ - **Numeric columns** (`Int`, `Float`, `BigInt`, `Decimal`) take
713
+ `set` / `increment` / `decrement` / `multiply` / `divide`
714
+ - **Array columns** take `set` / `push`, where `push` appends to the existing array
715
+ - Exactly one operation per column. Passing two, or `{}`, is a `GraphQLError` naming the
716
+ column — graphql-js has no `@oneOf` on the version range this library supports, so it is
717
+ enforced at resolve time rather than by the type
718
+ - `set` is the explicit spelling of the plain assignment the field had before the flag, so
719
+ every existing update has a one-key rewrite: `{ views: 5 }` → `{ views: { set: 5 } }`
720
+ - The input types are named for the **scalar**, not the column — every `Int` column in the
721
+ schema shares one `IntFieldUpdate` — and a column claimed by a `scalarOverrides` entry
722
+ keeps its plain field, since the override decides what that column accepts
723
+
724
+ Two things follow the database's own semantics rather than being guarded:
725
+
726
+ - `increment`/`multiply` on an integer column can overflow, and `divide` on one **rounds**
727
+ the way the dialect rounds (PostgreSQL truncates toward zero; use a `Float` or `Decimal`
728
+ column if you need the fraction)
729
+ - `divide: 0` raises the driver's division-by-zero error, surfaced as a `GraphQLError`
730
+
731
+ ## Row counts instead of rows
732
+
733
+ The plural `update<Table>` / `delete<Table>` mutations always `RETURNING *`, which is wasted
734
+ work — and wasted bandwidth — when a client only wants to know how many rows were touched.
735
+ `features.countMutations` adds a count-returning variant of each:
736
+
737
+ ```graphql
738
+ mutation {
739
+ deletePostsCount(where: { status: { eq: "spam" } })
740
+ updatePostsCount(where: { draft: { eq: true } }, set: { draft: false })
741
+ }
742
+ ```
743
+
744
+ - Both return `Int!` — the number of rows the write affected — and add no new output type
745
+ to the schema
746
+ - Arguments are exactly the plural mutation's, so a client swaps one field name for the
747
+ other; `features.requireWhere` applies to them the same way
748
+ - They follow the feature switch of the write they mirror: `updateCount` needs
749
+ `features.update`, `deleteCount` needs `features.delete`
750
+ - **MySQL** gets them too, and they are arguably most useful there: MySQL's other mutations
751
+ return only `MutationReturn { isSuccess }`, so a row count is strictly more information
752
+ - `update<Table>Count` **rejects** nested writes with a `GraphQLError` — a nested write
753
+ needs the parent rows this mutation deliberately does not read back. Use the plural
754
+ mutation for those
755
+ - The count is read from whichever field the driver reports it in (`rowCount`,
756
+ `affectedRows`, `rowsAffected`, `changes`, `count`); a driver that reports none is a
757
+ `GraphQLError` rather than a silent `0`
758
+
663
759
  ## Single-row update & delete
664
760
 
665
761
  Alongside the plural `update<Table>` / `delete<Table>` mutations, every table gets an
@@ -686,6 +782,17 @@ mutation {
686
782
  - The variants are generated under `features.update` / `features.delete`, share the plural
687
783
  mutations' input types, and appear in `entities.mutations` for custom schemas
688
784
 
785
+ ### Mutation return shapes
786
+
787
+ Every mutation that returns rows returns them non-null, with two exceptions that are
788
+ deliberate rather than accidental:
789
+
790
+ | Mutation | Returns | Why |
791
+ | --- | --- | --- |
792
+ | `create<Table>Single` | `Table!`, or `Table` under `conflictDoNothing` | An insert of one row returns that row. It can only fail to produce one when the `conflictDoNothing` build option swallows a conflict, so it is nullable exactly when that option is set |
793
+ | `update<Table>Many` | `[Table]!` | Each entry's updated rows **in entry order**. An entry whose `where` matched nothing contributes `null` in its slot, so the response lines up with the request positionally — the nullable element is the whole point |
794
+ | everything else | `[Table!]!` / `Table!` / `Table` | — |
795
+
689
796
  The plural mutations still accept a missing `where` (a full-table write) by default. To rule
690
797
  that out at the type level, `features: { requireWhere: true }` makes `where` non-null on
691
798
  `update<Table>` / `delete<Table>` and rejects a `where` with no filters, exactly like the