Marks a mutation field as fully generated, picks the SQL shape (INSERT, UPDATE, DELETE, UPSERT), and threads the response selection set through RETURNING so writes complete in a single round trip.
SDL signature
directive @mutation(typeName: MutationType!) on FIELD_DEFINITION
enum MutationType { UPDATE, DELETE, INSERT, UPSERT }
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
|
|
(required) |
The SQL shape to emit. |
Canonical example
The tutorial’s three film mutations cover the three most common shapes:
input FilmCreateInput @table(name: "film") {
title: String! @field(name: "title")
languageId: Int! @field(name: "language_id")
}
input FilmUpdateInput @table(name: "film") {
filmId: Int! @field(name: "film_id") @lookupKey
title: String! @field(name: "title")
}
input FilmUpsertInput @table(name: "film") {
filmId: Int! @field(name: "film_id") @lookupKey
title: String! @field(name: "title")
languageId: Int! @field(name: "language_id")
}
type Mutation {
createFilm(in: FilmCreateInput!): Film @mutation(typeName: INSERT)
updateFilm(in: FilmUpdateInput!): Film @mutation(typeName: UPDATE)
upsertFilm(in: FilmUpsertInput!): Film @mutation(typeName: UPSERT)
}
The INSERT shape produces:
INSERT INTO film (title, language_id)
VALUES (?, ?)
RETURNING film.film_id AS "filmId",
film.title AS "title"
The RETURNING projection narrows to the response selection set: a mutation that asks for { filmId title } retrieves only those columns; asking for { filmId title language { name } } adds a JOIN through @reference and the RETURNING is wrapped in a WITH clause so the round-trip count stays at one.
UPDATE uses @lookupKey on the input field to identify the target row; the remaining input fields populate the SET clause. UPSERT adds ON CONFLICT keyed on the @lookupKey columns. DELETE uses @lookupKey and returns the deleted row’s columns through RETURNING.
Payload-returning DELETE
@mutation(typeName: DELETE) supports two payload-carrier shapes. Both echo information about the rows the DML actually removed; neither projects non-primary-key columns from the deleted rows (the row is gone before the response can read it).
Echoing the deleted primary key (recommended)
The simplest payload returns the encoded NodeId of each deleted row:
type SlettRegelverksamlingPayload {
deletedIds: [ID!] # implicit @nodeId; encoder resolves against the input @table's @node
}
extend type Mutation {
slettRegelverksamling(input: [RegelverksamlingDeleteInput!]!): SlettRegelverksamlingPayload
@mutation(typeName: DELETE)
}
The carrier field’s element type must be ID (single DELETE) or [ID!] (bulk DELETE). The list wrapper must be list-of-non-null; [ID] (list-of-nullable) is rejected, because every element of a successful DELETE response is the encoded PK of an actually-deleted row, the slot cannot be null. The encoder is recognised implicitly when the input @table registers a @node. If the input @table is not @node-backed, the carrier is rejected with the same diagnostic as the bare-ID DELETE path; register the input type as @node first. To pin the encoder explicitly (recommended when grep-ability matters), attach @nodeId(typeName: "…") to the carrier field; the directive’s encoder must resolve to the same @table as the mutation’s input. An @nodeId whose encoder resolves to a different table is rejected: you would be returning IDs of a different entity than the one the DML acted on.
The response contains exactly the IDs of rows the DML actually removed.
Projecting the deleted row’s primary key onto an SDL type
The carrier may also return the @table-backed SDL type, but only when every non-nullable field on the type resolves to a primary-key column:
type Regelverksamling @table(name: "regelverksamling") @node {
id: ID! # PK; admits
navn: String # non-PK, nullable; admits, runtime returns null
beskrivelse: String! # non-PK, non-nullable; REJECTS the carrier
}
The classifier inspects every field on the element type and rejects the carrier if any non-nullable field maps to a non-PK column. Nullable non-PK fields admit and always resolve to null at runtime; this is by design — after a DELETE there is no row left to read those columns from. If your SDL type carries non-nullable non-PK fields, prefer the [ID!] shape above, or define a dedicated DeletedRegelverksamling SDL type whose non-nullable fields are PK-only.
@service-resolved fields are not admitted on the element type, nullable or not. The service would receive a PK-only row at runtime and any non-PK source parameter would silently produce null. Use the [ID!] shape and resolve service-backed data on the deleted entity through a sibling lookup if needed. FK-traversing reference fields (@reference paths to a joined target table) are also rejected on DELETE carriers, since the join cannot run after the row is gone.
Single vs bulk
Both shapes work with single and bulk DELETE; cardinality follows the carrier field’s wrapper (ID/Foo for single, [ID!]/[Foo!] for bulk).
What you can’t return
-
Arbitrary non-PK columns of the deleted row. The row is gone;
RETURNINGis narrowed to primary-key columns. If you need the full pre-delete state, snapshot it in your application code before issuing the mutation. -
A bare
@record-backed type. Use a NodeId echo or a@tableprojection.
Constraints
-
The mutation’s input type must carry
@table. The generator fails the build with a "@mutation requires a @table-bound input type" error otherwise; without@tableit has no way to know which table the operation targets. -
UPDATEandDELETErequire at least one@lookupKeyfield on the input type.INSERTrejects@lookupKey(no row exists yet to identify).UPSERTrequires@lookupKeyon the conflict columns. -
UPDATErequires at least one non-@lookupKeyfield on the input type (otherwise theSETclause is empty).UPSERTis exempt: an upsert with no SET-clause fields isINSERT … ON CONFLICT DO NOTHING, which is a legitimate shape. -
@mutationand@serviceare mutually exclusive on the same field. Pick one:@mutationfor fully-generated DB operations,@servicefor handing the operation to custom Java. -
Optional input fields participate in the operation only when the client provides them; absent optional fields are omitted from the column list (
INSERT) or theSETclause (UPDATE/UPSERT).
See also
-
Tutorial page 5: A first mutation walks the
INSERT/UPDATE/UPSERTflow against a running database. -
@lookupKeyidentifies the target row onUPDATE,DELETE, andUPSERT. -
@tableestablishes the input-type binding@mutationrequires. -
@servicefor custom-Java mutations.