A guide to how GraphQL schema patterns drive Graphitron’s code generation. This document introduces the classification pipeline and the vocabulary needed to read the source code. For variant details and record components, read the Javadoc on each source file listed in the Source Map below.


How Classification Works

GraphitronSchemaBuilder reads the schema once and classifies every type and field into a sealed hierarchy. The generators then operate on these classified models ; they never re-read directives.

GraphQL Schema
      ↓
GraphitronSchemaBuilder  (the only place directives are read)
      ↓
GraphitronSchema
  ├── Map<String, GraphitronType>                      types         (one per GraphQL type)
  ├── Map<FieldCoordinates, GraphitronField>           fields        (one per field)
  ├── Map<String, List<GraphitronField>>               fieldsByType  (derived index)
  ├── Map<String, EntityResolution>                    entitiesByType (federation entity mappings)
  └── List<BuildWarning>                               warnings      (non-fatal advisories)
      ↓
Generators
  ├── TypeFetcherGenerator     →  fetchers.*Fetchers
  ├── TypeClassGenerator       →  types.*
  └── TypeConditionsGenerator  →  conditions.*Conditions

Each sealed variant maps to specific generator output. The sections below show the full directive-pattern → variant → generator output chain.


Classification Vocabulary

Original framing. This section describes the field axes (source context, target type, scope) in their original form. They fold into the three-axis (source, operation, target) model in Field Classification, which the field now carries directly through its source() / operation() / target() accessors. Treat Field Classification as canonical for how a field classifies; the terms below survive as the historical framing and as properties derived from the three axes, not as independent axes.

Two independent classifications describe every field: the source context it is defined on (parent type), and the target type it returns. Both matter because scope transitions are determined by the pair, not by either alone.

Source context

The type on which a field is defined.

Source context Trigger What Graphitron generates

Unmapped

(none ; Query, Mutation)

Entry point. No SQL yet.

Table-mapped

@table

Full SQL generation ; queries, joins, projections.

Result-mapped

producer’s reflected return is a Java/jOOQ class

Runtime wiring only. The type’s backing class is derived by reflection from its producing field (a @service return, a @tableMethod return, or a parent-accessor chain). Graphitron validates types and wires data fetchers, but generates no SQL until a new scope starts.

Target type

The classification of the field’s return type (the element type ; looked through List and Connection wrappers). Encoded as ReturnTypeRef.

Target type ReturnTypeRef variant When it appears

Target table

TableBoundReturnType

Return type has @table (or is a @table + @discriminate interface), or a NestingField inherits the parent’s table context. Carries a fully resolved TableRef.

Target record

ResultReturnType

Return type is class-backed: its backing Java/jOOQ class is reflected from the producing field’s return type, not from a directive.

Target scalar

ScalarReturnType

Scalar, enum, or an unclassified type name (e.g. @nodeId(typeName:) argument types).

Target polymorphic

PolymorphicReturnType

Interfaces/unions spanning multiple tables, and the Relay/Federation built-ins node / _entities.

"Target table" is the pivot concept for scope transitions: every new scope is a query rooted in some target table, driven either by the root entering a table-mapped type or by a record handoff from a result-mapped source into a target-table return.

Scope

A Graphitron scope corresponds to one SQL statement. Fields within a scope contribute to the same query. Scope is determined by the (source context, target type) pair ; independently of @lookupKey, which is orthogonal.

stateDiagram-v2
    direction LR
    [*] --> NoScope
    NoScope --> InScope : enter<br/>(unmapped root field<br/>reaches target-table type)
    InScope --> InScope : split<br/>(@splitQuery on table-mapped source<br/>→ new scope via DataLoader, parent PK key)
    InScope --> InScope : record handoff<br/>(target-table field on result-mapped source<br/>or @service/@tableMethod returning target-table type<br/>→ new scope via DataLoader, parent PK or custom batch key)
    InScope --> Private : exit<br/>(@service field creates private scope<br/>independent of any Graphitron-managed scope)
    Private --> InScope : (private scope ends, caller continues)
    InScope --> [*]
Boundary Trigger

Enter

An unmapped root field reaches a target-table type ; the first scope starts

Split

@splitQuery on a table-mapped source ; a new scope via DataLoader, keyed by the parent’s PK

Record handoff

A target-table field on a result-mapped source, or any user-provided return (@service, @tableMethod) reaching a target-table type ; new scope via DataLoader, keyed by the parent’s PK or a custom batch key

Exit

@service fields create a private scope ; their SQL statement is independent of any Graphitron-managed scope

@lookupKey does not appear in this table on purpose. It shapes the batch (adds the derived target table and the N × M invariant) but does not by itself open or close a scope ; that is always decided by the source/target pair above.

Derived tables

Two kinds of VALUES(…) derived tables built by Graphitron when batching:

  • Derived source table ; built from parent source records. Contains the FK-relevant columns from the parent: the parent’s PK/unique-key columns when the FK is on the child side, or the FK columns themselves when the FK is on the parent side. Used for @splitQuery table fields, user-provided returns (@service, @tableMethod), and mutation read-backs.

  • Derived target table ; built from @lookupKey argument values (from SelectedField.getArguments()). Each argument value (or list element) is one row. Identical for every source in a batch ; all N parents in a batch share the same request arguments, so M (the number of lookup rows) is constant for the entire batch. Base result count is exactly N × M.

@condition on lookup fields is allowed. The condition method, however, must preserve the N × M positional contract: each (source, target) pair produced by the derived-table cross join is either kept in full or dropped in full, and no additional rows may be introduced. In practice this means the condition should be a predicate over the pair of rows, not a filter that can change the per-parent result cardinality non-uniformly. Violating the contract desynchronises batch dispatch ; the client receives rows that cannot be reattached to their source. The contract is a developer responsibility, not a build-time check.

Conditions

Kind Purpose Source

Reference condition

How two tables are joined within a scope

@reference directive, carried on the hop’s On axis: FK key → On.ColumnPairs; condition method → On.Predicate

Filter condition

Narrows the result set of the current scope

@condition directive, arguments, cursor

Lookup condition

Filters the (source × target) row pairs produced by a lookup’s derived target table. Must preserve the N × M positional contract ; see Derived tables above.

@condition directive on a field with @lookupKey

Structural properties

Property Effect

@splitQuery

On a table-mapped source, forces a new scope via DataLoader: TableField (no @lookupKey) → SplitTableField; field with @lookupKeySplitLookupTableField. On a result-mapped source it is redundant ; the record handoff already opens a new scope ; and should produce a build warning, not an error.

@lookupKey

Argument values become the derived target table (see Derived tables). Blocks pagination (preserves the N × M result invariant). Without @splitQueryLookupTableField; with @splitQuerySplitLookupTableField. Orthogonal to scope ; see Scope.


Type Classification

The worked examples in this section render directly from the classification test corpus (ClassifiedCorpus in graphitron’s test sources): the SDL shown is the live fixture the classifier runs against, projected through a documented query by the query-as-view renderer, with the test-only `@classified / @classifiedType assertion directives stripped. The ClassifiedDocTest build guard fails if this page ever drifts from what the corpus renders, so a displayed example is always one the classifier actually produces the stated verdict for. The reference table that follows enumerates the verdicts not yet migrated to a worked example.

@table type → TableType

A type carrying @table(name:) (without @node or @discriminate) classifies as TableType, the pivot for SQL generation. A root field returning it enters a new query scope rooted in that table, and a scalar child whose name matches a column projects inline.

type Query {
  "A single film, fetched by primary key."
  film: Film
}

type Film @table(name: "film") {
  "The film's display title."
  title: String
}

Film classifies as TableType (full SQL generation: queries, joins, projections); Query.film enters the scope; Film.title projects an inline column. Asserted by the catalog corpus example via @classifiedType(as: TableType). The field descriptions come from # …​ comments authored on the example’s projection query, rendered as SDL descriptions by QueryViewRenderer.

Reference table

Classification Trigger on Type GraphitronType Variant Generator Output

@table + @node

NodeType

Fetchers class + class (with Relay ID handling)

Producer’s reflected return is a Java/jOOQ class (a @service return, a @tableMethod return, or a parent-accessor chain)

ResultType*

*Fetchers class only (no SQL scope of its own). The backing class is reflection-derived, not declared.

Query or Mutation root type

RootType

*Fetchers class only

Interface with @table + @discriminate

TableInterfaceType

*Fetchers class

Interface without @table (multi-table)

InterfaceType

*Fetchers class

Union type

UnionType

*Fetchers class

@error

ErrorType

No generation (error mapping config)

Input type with @table (deprecated on input; see docs/manual/reference/directives/table.adoc)

TableInputType

Used in mutation generation. A build warning fires per @table-on-input usage, except encoded-ID / scalar-return INSERT/UPSERT. A DELETE-consumed input warns and names @mutation(table:) as the replacement (R457).

Input type without @table (the @service / @condition / @tableMethod parameter the input flows into is reflected as a developer class)

InputType*

No generation (developer-provided class). The input’s backing class is reflected from the parameter type it flows into.

Input type with @table used on fields with conflicting return tables

PojoInputType (unbound)

No generation ; column binding resolved per field-usage

Object type with no @table and no producer binding

NestingType

No generation; nested under a parent table-bound or class-backed scope via NestingField

GraphQL enum type

EnumType

No generation; enum values map to DB strings/ints at column-bind time

GraphQL scalar type with @scalarType(scalar:)

ScalarType

No generation; the consumer’s public static final GraphQLScalarType constant is registered on the synthesized schema via .additionalType(…​). Graphitron reflects on the constant’s Coercing<I, O> to recover the Java type used for input-record components, service params, and Field<X> projections.

Spec built-in scalar (Int / Float / String / Boolean / ID)

ScalarType

Same registration shape; Java type comes from a closed table (the spec binds these names). @scalarType on a spec built-in is a hard validation error (directive conflict).

Non-spec / non-federation scalar with no @scalarType

UnclassifiedType

Validation error pointing at @scalarType(scalar:) as the single fix; no silent fallback to Object.

Generated *Connection type (from @asConnection transform)

ConnectionType

Pagination wrapper; pairs with EdgeType and PageInfoType below

Generated *Edge type

EdgeType

Edge wrapper carrying node + cursor for the connection

Generated PageInfo type

PageInfoType

Connection page-info wrapper

Conflicting or unresolvable directives

UnclassifiedType

Validation error ; build fails

Intermediate sealed interfaces (not shown in the table ; grouping nodes in the hierarchy): - TableBackedType ; groups TableType, NodeType, TableInterfaceType. Builders switch on this to detect table-mapped types. - ResultType is itself a sealed sub-interface with four concrete variants: JavaRecordType, Backed (the sole concrete leaf of the intermediate PojoResultType), JooqRecordType, JooqTableRecordType ; reflecting how the result class is represented in Java. - InputType is itself a sealed sub-interface with four concrete variants: JavaRecordInputType, PojoInputType, JooqRecordInputType, JooqTableRecordInputType ; same split by Java representation. PojoInputType is also used when an input type appears as an argument on fields with different return tables ; it is classified as unbound rather than failing.


Field Classification

A field’s classification factors into three asserted axes plus a derived layer. The worked examples below render the dimensional form from the corpus; the leaf-name tables that follow are a curated reference, enumerating each sealed variant and the generator output it drives. The corpus, not these tables, is the coverage source of truth: VariantCoverageTest guarantees every output-field leaf is demonstrated by a corpus fixture, so a variant absent from a table below is still tested, just not featured here in prose.

A field is an edge: it arrives into a source, performs an operation, and projects a target. The three asserted axes are:

  • source, the field’s arrival endpoint, a wrapper around a shape. The wrapper arm is position and the legality gate: Root (permitting Query / Mutation) is a root field, OnlyChild / Child is a nested field (one or many source objects arriving). Write operations are legal only on Root.Mutation, NodeResolve only on Root.Query, Nest only on a nested source. The nested arms wrap a SourceShape (Table / Record), the catalog-vs-Java polarity of what arrives at env.getSource().

  • operation, the verb the field performs: reads (Fetch, Paginate, Lookup, NodeResolve, Nest, and the modeled-but-unpopulated EntityResolve / Count / Facet), writes (Insert, Upsert, Update, Delete, and the unpopulated UpdateMatching / DeleteMatching), and the developer ServiceCall (the read-vs-write split it once carried is now read off the source root).

  • target, the field’s projection endpoint, a wrapper (Single / List) around a TargetShape. The shape carries build-vs-consume: Table / Column are catalog shapes graphitron builds the SQL for; Record / Field are domain shapes graphitron consumes without having built; Connection,

    Interface, Union are the container and polymorphic shapes. `Table:Column

    Record:Field` (mirror : reflect). A Relay connection is Single(Connection(…​)), its windowed-read verb the Paginate operation.

The derived layer is computed from those three plus the field’s slots and schema position, never asserted: FetchRelated (a Fetch reaching a related entity over a join-path), re-fetch (a service/DML producer yielding a Table shape, forcing a re-projection), new-query (@splitQuery / polymorphic / record-handoff opening a fresh keyed query), and polarity (mutating-or-not, from the source root and the write operations). The governing principle is assert what nothing else carries; derive what another axis or slot already forces. The subsections below sound out the derived layer and each axis in turn.

The derived layer: same verdict, different mechanism

Two fields can share an identical (source, operation, target) verdict yet emit different SQL, because the fetcher/loader mechanism is derived, not asserted. The cleanest illustration is the new-query derivation: a @table child reachable by a foreign key from a @table parent inlines as a correlated subquery folded into the parent’s SELECT, while adding @splitQuery opens a new keyed query dispatched through a DataLoader. Both classify identically (source = Child(Table), operation = Fetch, target = Single(Table)); only the derived new-query layer differs, forced by the @splitQuery slot.

type Query {
  city: City
}

type City @table(name: "city") {
  country: Country
  countrySplit: Country @splitQuery
}

type Country @table(name: "country") {
  name: String @field(name: "country")
}

City.country and City.countrySplit return the same Country over the same city → country foreign key and carry the same Child / Fetch / Table verdict; @splitQuery flips only the derived new-query layer, not an asserted axis. Asserted by the child-table corpus example.

The record-handoff boundary

@splitQuery is not the only trigger for the new-query derivation. A @table child reached by a foreign key inlines under a @table parent, but the same FK-reached child re-queries under a record-backed parent, because the record handoff has already opened a new DataLoader-backed scope that the correlated subquery cannot fold back into. The parent’s table-ness, not a directive, forces the derived re-query here; the asserted verdict is unchanged.

type Query {
  film: Film
}

type Film @table(name: "film") {
  language: Language @reference(path: [{key : "film_language_id_fkey"}])
  details: FilmDetails
}

type Language @table(name: "language") {
  name: String
}

type FilmDetails {
  language: Language @reference(path: [{key : "film_language_id_fkey"}])
}

Film.language (TableField) and FilmDetails.language (RecordTableField) return the same Language over the same film_language_id_fkey foreign key and carry the same Fetch / Table operation and target; they differ only on the source shape (Child(Table) vs Child(Record)), and the record handoff forces a derived keyed re-query under FilmDetails, not a different asserted axis. Asserted by the record-table corpus example.

target shape: build-vs-consume (Column vs. Field)

The target shape records what domain object the value is, and with it whether graphitron builds the SQL (catalog: Table / Column) or consumes a value it did not build (domain: Record / Field). Two scalar flavors hinge on the parent’s table-ness: a scalar under a @table parent projects a Column (a real database column graphitron projects), while under a record-backed parent (a plain object with no @table, here produced as a service method’s return type) a scalar projects a Field (a POJO property read off a record graphitron only reflects). A nested non-table object under a record parent is the object flavor of the same Field shape. All three are Fetch; the parent’s table-ness moves the source shape (Table vs Record) and with it the target shape across the build-vs-consume line.

type Query {
  film: Film
}

type Film @table(name: "film") {
  title: String
  details: FilmDetails
}

type FilmDetails {
  stats: FilmStats
}

type FilmStats {
  count: Int
}

Film.title classifies with target shape Column (title is a column of the film table graphitron builds the projection for). FilmStats.count, a scalar under the record-backed FilmStats, classifies with target shape Field (a property graphitron reflects). FilmDetails.stats, a nested non-table object under the record-backed FilmDetails, also classifies with target shape Field (its object flavor). All hold operation = Fetch; the two record-backed cases also carry source = Child(Record). Asserted by the mapping corpus example.

Polymorphic fields: interfaces, unions, and Relay nodes

A field returning an interface, a union, or a Relay Node is catalog-bound over the participant types: classification resolves the polymorphic type to its participant @table rows and projects each branch from the catalog. The target shape is therefore Interface / Union (the projection lands on participant table rows), with the participant set carried as a derived slot rather than as a distinct shape value. The operation is Fetch for interface and union fields (root or child) and NodeResolve for the Relay node / nodes roots; the new keyed query a plain-interface or union field opens (InterfaceField / UnionField, and the polymorphic roots QueryInterfaceField / QueryUnionField / QueryNodeField / QueryNodesField) is a derived new-query, not an asserted axis. The one structural difference is a @table+@discriminate interface child (TableInterfaceField): it is foreign-key-correlatable from the parent, so it inlines rather than opening a new query, but its asserted Child / Fetch / Table verdict is the same.

type Query {
  customer: Customer
}

type Customer @table(name: "customer") {
  address: Named
}

interface Named {
  name: String
}

Customer.address returns the plain interface Named (implemented by the @table type Address), so it classifies with source = Child, operation = Fetch, target shape Interface (InterfaceField): a derived new keyed query projects the participant table, with Address recorded as the participant slot. Asserted by the interface corpus example.

A union child follows the same dimensional rule. The selection descends into each participant through an inline fragment (…​ on Film, …​ on Actor), and classification resolves the union to its participant @table rows:

type Query {
  filmActor: FilmActor
}

type FilmActor @table(name: "film_actor") {
  related: FilmOrActor
}

type Film @table(name: "film") {
  title: String
}

type Actor @table(name: "actor") {
  firstName: String @field(name: "FIRST_NAME")
}

union FilmOrActor = Film | Actor

FilmActor.related returns the union FilmOrActor, so it classifies with source = Child, operation = Fetch, target shape Union (UnionField): a derived new keyed query projects whichever participant table a row resolves to, with Film and Actor recorded as the participant slots. Asserted by the union corpus example.

The @table+@discriminate interface child (TableInterfaceField, inline, target shape Table) and the Relay Node root (QueryNodeField, operation = NodeResolve, target shape Interface) are further leaves on this same polymorphic rule; they are asserted corpus-only (table-interface, relay-node).

A discriminated interface may also be a joined-table (class-table) inheritance: each concrete type declares its own detail @table distinct from the discriminated base, with its inherited (base-resident) fields carrying a @reference back to the base and its own columns living on the detail table. Such a participant classifies as a ParticipantRef.JoinedTableBound carrying the resolved child-to-parent hop (rather than a single-table TableBound); its inherited field is a ColumnReferenceField resolved on the base and its own field a plain ColumnField on the detail table. The interface fetcher selects from the base and emits a discriminator-gated LEFT JOIN to each participant’s detail table, projecting the shared fields off the base and each participant’s detail-exclusive columns off its detail alias; the same concrete type is independently queryable on its own, resolving its inherited fields through the parent reference. The child-to-parent join must be PK=FK (the detail table’s foreign-key columns to the base are its own primary key, single-column or composite), which keeps the base-to-detail join single-valued. Asserted by the joined-table-interface corpus example.

Reading the generator-output column

The "`*Fetchers` Generates" column in the tables below names which of four emission paths a variant drives. The four are an exhaustive, disjoint partition of every field leaf, enforced by GeneratorCoverageTest.everyGraphitronFieldLeafHasAKnownDispatchStatus, so a new leaf cannot be added without declaring its path:

  • Fetcher method ; a real method on the generated Fetchers class: a synchronous field fetcher, or an asynchronous DataLoader fetcher paired with a rows() batch method (called out as DataLoader-backed where it matters). These are the IMPLEMENTED_LEAVES.

  • Inline projection ; no per-field method ; the value is projected straight into the parent’s SELECT by TypeClassGenerator.$fields. These are the PROJECTED_LEAVES.

  • Wiring value ; no generated method either ; the field is registered as a DataFetcher / ColumnFetcher value through FetcherEmitter / FetcherRegistrationsEmitter. These are IMPLEMENTED_LEAVES leaves with an empty dispatch arm.

  • Deferred stub ; a throwing stub method standing in for generation that is not built yet, paired with a build-time validator rejection. Only CompositeColumnReferenceField is deferred today (STUBBED_VARIANTS).

Query Fields

Schema Pattern QueryField Variant *Fetchers Generates

Any argument has @lookupKey

QueryLookupTableField

Synchronous fetcher + lookup*() rows method (+ VALUES input-rows helper)

@tableMethod

QueryTableMethodTableField

Fetcher method

Field named node (Relay)

QueryNodeField

Fetcher method

Field named nodes (Relay; auto-emitted, batched-by-id)

QueryNodesField

Async DataLoader fetcher dispatching by NodeId

Return: @table+@discriminate interface

QueryTableInterfaceField

Fetcher method

Return: multi-table interface

QueryInterfaceField

Fetcher method(s) ; per-participant polymorphic projection

Return: union

QueryUnionField

Fetcher method(s) ; per-participant polymorphic projection

@service, return @table type

QueryServiceTableField

Async DataLoader fetcher + rows*() method

@service, return non-table type

QueryServiceRecordField

Fetcher method

Return: @table type (default)

QueryTableField

Full fetcher ; condition call + orderBy build + inline DSL chain (dsl.select(Type.$fields(…​)).from(table)…​)

Anything else

UnclassifiedField**

Validation error ; build fails

The federation _entities field is not modelled as a QueryField permit; it is resolved by federation-graphql-java-support directly and dispatched through the generated EntityFetcherDispatch runtime helper. The corresponding entity-resolution metadata is carried in GraphitronSchema.entitiesByType.

A @service field’s input parameter may itself be a generated jOOQ TableRecord (singular or List<…>), distinct from the field’s return type. When the parameter’s SDL input type classifies as JooqTableRecordInputType, the call site binds it on the column axis rather than instantiating a Java bean: each plain input field names a column through @field(name:), and an optional @nodeId field decodes the record’s scalar key. The parameter is materialised by a generated create<Record> (singular) / create<Record>List (list) helper on the *Fetchers class (the CallSiteExtraction.JooqRecord binding), which loads the columns through record.fromArray(…, Tables.<T>.<col>…) and decodes the identity through NodeIdEncoder.decodeValues. The binding is coordinate-agnostic: it applies identically whether the @service field is a root field or a @table-parent child field, sharing one helper.

Mutation Fields

A mutation whose @mutation(typeName:) writes the catalog and returns a @table type classifies with source = Mutation, the write verb as its operation (Insert / Update / Delete), and target shape Table. The write produces the affected row, then a follow-up SELECT re-projects it through the catalog; that read-back is the derived re-fetch (a write producer yielding a Table shape), not a separate asserted axis. The mutation’s input object is part of the rendered closure, so the excerpt shows the @table-bound input the write consumes:

type Mutation {
  createFilm(in: FilmInput!): Film @mutation(typeName: INSERT)
}

type Film @table(name: "film") {
  title: String
}

input FilmInput @table(name: "film") {
  title: String
}

Mutation.createFilm inserts a film row from FilmInput and projects the inserted row as the @table type Film: source = Mutation, operation = Insert, target shape Table (MutationInsertTableField), the read-back being the derived re-fetch. Asserted by the dml corpus example.

Schema Pattern MutationField Variant *Fetchers Generates

@mutation(typeName: INSERT), returning ID or a @table type

MutationInsertTableField

Fetcher method ; write + read-back SELECT (the derived re-fetch)

@mutation(typeName: UPDATE), returning ID or a @table type

MutationUpdateTableField

Fetcher method ; write + read-back SELECT (the derived re-fetch)

@mutation(typeName: DELETE), returning ID, write target from the input’s @table

MutationDeleteTableField

Fetcher method ; write + encoded-PK off RETURNING (no read-back ; the row is gone, target shape Column)

@mutation(typeName: DELETE, table: "…"), returning ID, input carrying no @table

MutationDeleteTableField

As above; the write target is named on the field (R457) rather than on the input type. Byte-identical carrier to the @table-on-input form

@mutation(typeName: INSERT|UPDATE|UPSERT, table: "…") (table: on a non-DELETE verb)

UnclassifiedField (typed MutationTableArgError.UnsupportedVerb)

Validation error ; build fails. table: is wired for DELETE only

@mutation(typeName: UPSERT), returning ID or a @table type

MutationUpsertTableField

Rejected at classification ; UPSERT generation gated pending R145

@mutation(typeName: INSERT|UPDATE|DELETE|UPSERT), returning a single-record class-backed carrier (one recognized data field, optional errors-shaped field; the carrier’s backing class is reflected from the producing field, not declared)

MutationDmlRecordField

Two-step fetcher: per-kind DML chain inside transactionResult with PK-only RETURNING, plus a follow-up data-field SELECT outside the transaction

@mutation(typeName: INSERT|UPDATE) with bulk @table input, returning a single-record carrier with a list-shaped @table-element data field

MutationBulkDmlRecordField

Per-row DML inside one transactionResult, accumulating a typed Result<RecordN<PK>> in input order

@service, return @table type

MutationServiceTableField

Async DataLoader fetcher + rows*() method

@service, return non-table type

MutationServiceRecordField

Fetcher method

Neither @service nor @mutation

UnclassifiedField**

Validation error ; build fails

Both @service and @mutation

UnclassifiedField**

Validation error ; build fails

The four Mutation*TableField permits are guaranteed never to carry a class-backed (record-shaped) return: every DML mutation whose return type reflects to a record carrier routes through the DML-carrier permits (MutationDmlRecordField / MutationBulkDmlRecordField) via BuildContext.scanStructuralDmlPayload. The narrowness is enforced structurally by the routing in FieldBuilder.classifyMutationField, not by an Invariant the validator restates.

Child Fields (on @table parent)

Scalar / Enum return type

Schema Pattern ChildField Variant *Fetchers Generates

@field(name:) or matching column name

ColumnField (compaction = Direct)

Wiring value (ColumnFetcher)

@reference on scalar

ColumnReferenceField (compaction = Direct)

Inline projection in $fields + ColumnFetcher wiring value

@reference on scalar declared on a TableInterfaceType participant

ParticipantColumnReferenceField

Wiring value ; materialised by the enclosing TableInterfaceField fetcher’s conditional LEFT JOIN and read back via FetcherEmitter (participant-side FK, not the interface-side resolver)

@nodeId (single-column PK), no typeName

ColumnField (compaction = NodeIdEncodeKeys)

Wiring value (ColumnFetcher); the projected column is wrapped in the per-Node encode<TypeName> helper

@nodeId(typeName:), single-column PK target

ColumnReferenceField (compaction = NodeIdEncodeKeys)

Inline $fields projection against the FK-resolved target column, wrapped in encode<TypeName>

@nodeId (composite PK), no typeName

CompositeColumnField

Inline $fields projection of RowN<…​> of the parent’s PK columns, wrapped in encode<TypeName>

@nodeId(typeName:), composite PK target

CompositeColumnReferenceField

Deferred stub ; rooted-at-parent composite NodeId reference, pending nodeidreferencefield-join-projection-form

Object return type

Schema Pattern ChildField Variant *Fetchers Generates

@externalField(reference: {className, method})

ComputedField

Inlined call in $fields() (<Class>.<method>(table).as("<name>")) + ColumnFetcher registered by alias in wiring(). Method must be public static Field<X> name(<ParentTable> table).

@tableMethod

TableMethodField

Fetcher method

@service, return @table

ServiceTableField

Async DataLoader fetcher + rows*() method

@service, return non-table

ServiceRecordField

Fetcher method

Return @table, @splitQuery + @lookupKey

SplitLookupTableField

Async DataLoader fetcher + rows*() method

Return @table, @lookupKey (no split)

LookupTableField

Inline projection in $fields ; correlated subquery via DSL.multiset + VALUES/JOIN lookup keyset

Return @table, @splitQuery

SplitTableField

Async DataLoader fetcher + rows*() method

Return @table (default)

TableField

Inline projection in $fields ; correlated subquery via DSL.multiset (single cardinality adds .limit(1) + a read-side unwrap lambda)

Return @table+@discriminate interface

TableInterfaceField

Fetcher method

Return multi-table interface

InterfaceField

Fetcher method(s) ; per-participant polymorphic projection

Return union

UnionField

Fetcher method(s) ; per-participant polymorphic projection

Return plain object (no @table)

NestingField

Inline projection ; nested fields fold into the parent SELECT, the object itself wired as an env.getSource() passthrough (inherits parent table context)

Conflicting directives

UnclassifiedField**

Validation error; build fails

@notGenerated

UnclassifiedField**

Validation error; the directive is no longer supported (INVALID_SCHEMA rejection). Remove it; fields must be fully described by the schema.

@multitableReference

UnclassifiedField**

Validation error; the directive is no longer supported (INVALID_SCHEMA rejection). Remove it; the rewrite generates multi-table interface dispatch from @discriminate / @discriminator.

Child Fields (on a class-backed parent)

The parent here is a type whose backing class is reflection-derived from its producer (a @service / @tableMethod return, or a parent-accessor chain), not declared via a directive. The Record* variant names below are reflection-derived classifications, kept stable from earlier naming.

Schema Pattern ChildField Variant *Fetchers Generates

Scalar/enum with @field

PropertyField

Wiring value ; property read off the parent record

Return @table, @lookupKey (catalog FK from parent’s jOOQ TableRecord)

RecordLookupTableField (SourceKey.Reader.ColumnRead)

Async DataLoader fetcher + rows*() method

Return @table (default; catalog FK from parent’s jOOQ TableRecord)

RecordTableField (SourceKey.Reader.ColumnRead)

Async DataLoader fetcher + rows*() method

Return @table with @sourceRow (parent is Backed or JavaRecordType; no catalog FK required)

RecordTableField / RecordLookupTableField (SourceKey.Reader.SourceRowsCall)

Async DataLoader fetcher + rows*() method; the developer-supplied static lifter populates the batched VALUES rows

Return non-table type

RecordField

Wiring value ; read off the parent record

errors: slot on a service payload (one entry per @error mapping)

ErrorsField

Passthrough; graphql-java’s default PropertyDataFetcher reads the list the carrier’s try/catch wrapper or the service-method body produced

@service, return @table

ServiceTableField

Async DataLoader fetcher + rows*() method

@service, return non-table

ServiceRecordField

Fetcher method

Input Fields (on @table input parent)

Schema Pattern InputField Variant Used for

@field(name:) or matching column name

InputField.ColumnField

Maps input argument to a table column

@reference on input scalar

InputField.ColumnReferenceField

Maps input argument to a FK column

@nodeId on input scalar, composite PK

InputField.CompositeColumnField

Decodes a composite-PK NodeId into the input’s PK columns (carries NodeIdDecodeKeys extraction)

@nodeId(typeName:) on input scalar, composite PK

InputField.CompositeColumnReferenceField

Decodes a composite-PK NodeId into FK columns referencing the typeName target

Return plain object (no @table)

InputField.NestingField

Expands nested input fields inline against parent table

InputField is a separate top-level sub-hierarchy of GraphitronField, alongside RootField and ChildField. It classifies fields on TableInputType for mutation input processing.

*\* UnclassifiedField is a direct permit of GraphitronField; it is not nested under QueryField, MutationField, or ChildField. It is listed in the tables above for completeness, but structurally it sits at the top level of the sealed hierarchy. TableTargetField is an intermediate sealed sub-interface of ChildField grouping all 8 SQL-generating child field variants (TableField, SplitTableField, LookupTableField, SplitLookupTableField, TableInterfaceField, ServiceTableField, RecordTableField, RecordLookupTableField).

DataLoader-backed field categories

Four categories of DataLoader-backed child field, distinguished by @splitQuery and @lookupKey:

Category DataLoader Derived tables @condition / non-@lookupKey args Pagination

QueryLookupTableField (root lookup, no @splitQuery)

No ; synchronous

Derived target only

Allowed ; must preserve N × M contract†

Never ; result count = M exactly

LookupTableField (@lookupKey, no @splitQuery, table-mapped parent)

No ; correlated subquery

Derived target + correlated parent join

Allowed ; must preserve N × M contract†

Never

SplitTableField (@splitQuery, no @lookupKey)

Yes

Derived source only

Allowed

Allowed

SplitLookupTableField (@splitQuery + @lookupKey, result-mapped parent)

Yes

Both

Allowed ; must preserve N × M contract†

Never ; result count = N × M

† See Derived tables for the contract definition.


Implicit Classification Rules

Not all classification requires directives. The builder also classifies based on:

Schema Pattern Classification Effect

Field name matches column name (on @table type)

ColumnField ; direct column mapping

Field returns *Connection type (from @asConnection transform)

FieldWrapper.Connection ; pagination logic

GraphQL enum on @table field

Enum-to-DB string/int mapping


Source Map

All source lives under graphitron/src/main/java/no/sikt/graphitron/rewrite/. Each file has Javadoc documenting its variants and record components.

Model (model/)

Concept File What to look for

Type hierarchy

GraphitronType.java

Sealed interface ; all type variants and their record components

Field hierarchy

GraphitronField.java

Sealed interface ; RootField/ChildField/InputField sub-hierarchies

SQL-generating marker

SqlGeneratingField.java

Orthogonal interface ; returnType(), filters(), orderBy(), pagination()

Method-backed marker

MethodBackedField.java

Orthogonal interface ; method() returning MethodRef

DataLoader marker

BatchKeyField.java

Orthogonal interface ; sourceKey(), loaderRegistration(), rowsMethodName()

Table reference

TableRef.java

Resolved jOOQ table with PK columns

Column reference

ColumnRef.java

Resolved column with Java type

Return type

ReturnTypeRef.java

TableBound / Result / Scalar / Polymorphic

Cardinality

FieldWrapper.java

Single / List / Connection

Join path

JoinStep.java

Hop (a TableExpr target × an On: ColumnPairs / Predicate) / LiftedHop

WHERE filters

WhereFilter.java

GeneratedConditionFilter / ConditionFilter ; call-site contract

Service methods

MethodRef.java

Resolved Java method with ParamSource variants

DataLoader keys

SourceKey.java, LoaderRegistration.java

SourceKey carries the source-side dispatch axis (sealed WrapRow / Record / TableRecord — sealed ReaderColumnRead / AccessorCall / SourceRowsCall / ServiceTableRecord / ServiceUntypedRecord / ResultRowWalk / ProducedRecordRead — plus a Cardinality enum). LoaderRegistration carries DataLoader identity (ContainerPOSITIONAL_LIST / MAPPED_SETDispatchLOAD_ONE / LOAD_MANY).

Ordering

OrderBySpec.java

Fixed / Argument / None

Pagination

PaginationSpec.java

Relay cursor arguments

Argument extraction

CallSiteExtraction.java

Direct strategies (Direct / EnumValueOf / ContextArg / JooqConvert) plus sealed sub-shapes: NestedInputField (descends an input-object graph for @condition on INPUT_FIELD_DEFINITION), NodeIdDecodeKeys (SkipMismatchedElement / ThrowOnMismatch; arity-N NodeId decode at the call site), NodeIdDecodeRecord (NodeId decode into a jOOQ-record member), InputBean (service input-bean instantiation), and JooqRecord (jOOQ record construction from input fields)

Input field hierarchy

InputField.java

Sealed interface ; ColumnField / ColumnReferenceField / CompositeColumnField / CompositeColumnReferenceField / NestingField for mutation inputs

Condition params

CallParam.java, BodyParam.java

Call-site vs body-generation views

Builders (root package)

Component File Responsibility

Entry point

GraphitronSchemaBuilder.java

Sole directive-reading boundary ; assembles GraphitronSchema

Type classification

TypeBuilder.java

Two-pass: classify types, then enrich interfaces/unions with participants

Field classification

FieldBuilder.java

Classifies fields based on parent type, directives, and return type

jOOQ lookups

JooqCatalog.java

Lazy wrapper around jOOQ Catalog ; tables, columns, FKs, indexes, PKs

Service reflection

ServiceCatalog.java

Reflects @service/@tableMethod Java methods into MethodRef

Generators (generators/)

Four families, by what they emit. The Generators package holds emitter helpers (SplitRowsMethodEmitter, InlineLookupTableFieldEmitter, LookupValuesJoinEmitter, JoinPathEmitter, MultiTablePolymorphicEmitter, ArgCallEmitter, FetcherEmitter, HandleMethodBody, SelectMethodBody, ValuesJoinRowBuilder, etc.) alongside the entry-point generators below ; helpers are reached from inside a generator and don’t appear in this table.

Family Generators What lands

Fetcher emission

TypeFetcherGenerator, TypeClassGenerator, TypeConditionsGenerator

fetchers.Fetchers (wiring + field fetcher/rows methods); types. ($fields(sel, table, env) projection); conditions.*Conditions (pure-function WHERE predicates)

Schema emission (schema/)

ObjectTypeGenerator, InputTypeGenerator, EnumTypeGenerator, GraphitronFacadeGenerator, GraphitronSchemaClassGenerator, FetcherRegistrationsEmitter, DirectiveDefinitionEmitter, AppliedDirectiveEmitter, GraphQLValueEmitter, InputDirectiveInputTypes

The runtime SDL classes graphql-java consumes: per-type wiring entries, applied-directive emission, registration glue

Error-handling emission (schema/)

ConstraintViolationsClassGenerator, ErrorMappingsClassGenerator, ErrorRouterClassGenerator

The typed-error classes service payloads project errors into; mappings constant + router that selects per-exception

Runtime helpers (util/)

ColumnFetcherClassGenerator, ConnectionResultClassGenerator, ConnectionHelperClassGenerator, GraphitronValuesClassGenerator, EntityFetcherDispatchClassGenerator, GraphitronContextInterfaceGenerator, NodeIdEncoderClassGenerator, OrderByResultClassGenerator, QueryNodeFetcherClassGenerator

Per-app emitted runtime classes (ColumnFetcher, ConnectionResult, ConnectionHelper, GraphitronValues, EntityFetcherDispatch, GraphitronContext, NodeIdEncoder, OrderByResult, QueryNodeFetcher) — the runtime extension points graphql-java and the federation library plug into

GeneratorUtils (root of generators/) holds shared building blocks reached from any family ; ResolvedTableNames, key-type construction, constants. QueryConditionsGenerator is part of fetcher emission and emits the (env, …​) → typed-args adapter that fronts user-written *Conditions composers (see the "Wire boundaries are typed adapter / composer pairs" principle in Graphitron Development Principles).

Directives

  • SDL definitions: graphitron/src/main/resources/no/sikt/graphitron/rewrite/schema/directives.graphqls. RewriteSchemaLoader auto-injects this file into the merged schema.

  • Directive reference with examples: see the directive reference under docs/manual/reference/directives/ (rendered at the published manual). For the contributor-facing chapter consolidating runtime extension points and the typed-rejection narrative, see Runtime Extension Points and Typed rejection; this Source Map is the canonical reference for what each generator emits.


See also: - Graphitron Development Principles ; the six axioms, their corollaries, and the named enforcement behind each - Rewrite Roadmap ; remaining generator work