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
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 | Directive | What Graphitron generates |
|---|---|---|
Unmapped |
(none ; Query, Mutation) |
Entry point. No SQL yet. |
Table-mapped |
|
Full SQL generation ; queries, joins, projections. |
Result-mapped |
|
Runtime wiring only. 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 |
|
Return type has |
Target record |
|
Return type has |
Target scalar |
|
Scalar, enum, or an unclassified type name (e.g. |
Target polymorphic |
|
Interfaces/unions spanning multiple tables, and the Relay/Federation built-ins |
"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 |
|
Record handoff |
A target-table field on a result-mapped source, or any user-provided return ( |
Exit |
|
@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
@splitQuerytable fields, user-provided returns (@service,@tableMethod), and mutation read-backs. -
Derived target table ; built from
@lookupKeyargument values (fromSelectedField.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 |
|
Filter condition |
Narrows the result set of the current scope |
|
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. |
|
Structural properties
| Property | Effect |
|---|---|
|
On a table-mapped source, forces a new scope via DataLoader: |
|
Argument values become the derived target table (see Derived tables). Blocks pagination (preserves the N × M result invariant). Without |
Type Classification
| Directive Pattern on Type | GraphitronType Variant |
Generator Output |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Interface with |
|
|
Interface without |
|
|
Union type |
|
|
|
|
No generation (error mapping config) |
Input type with |
|
Used in mutation generation |
Input type with |
|
On input types, |
Input type without |
|
No generation (developer-provided class) |
Input type with |
|
No generation ; column binding resolved per field-usage |
Object type with no |
|
No generation; nested under a parent table-bound or result-bound scope via |
GraphQL |
|
No generation; enum values map to DB strings/ints at column-bind time |
GraphQL |
|
No generation; the consumer’s |
GraphQL |
|
Same as above, but the constant FQN is read from |
Spec built-in scalar ( |
|
Same registration shape; Java type comes from a closed table (the spec binds these names). |
GraphQL |
|
Validation error pointing at |
Generated |
|
Pagination wrapper; pairs with |
Generated |
|
Edge wrapper carrying |
Generated |
|
Connection page-info wrapper |
Conflicting or unresolvable directives |
|
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, 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
Fields are classified separately for root types (Query/Mutation) and nested types.
Query Fields
| Schema Pattern | QueryField Variant |
*Fetchers Generates |
|---|---|---|
Any argument has |
|
Async DataLoader fetcher + sync |
|
|
Field method stub |
Field named |
|
Field method stub |
Field named |
|
Async DataLoader fetcher dispatching by NodeId |
Return: |
|
Field method stub |
Return: multi-table interface |
|
Field method stub |
Return: union |
|
Field method stub |
|
|
Async DataLoader fetcher + |
|
|
Field method stub |
Return: |
|
Full fetcher ; condition call + orderBy build + inline DSL chain ( |
Anything else |
|
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.
Mutation Fields
| Schema Pattern | MutationField Variant |
*Fetchers Generates |
|---|---|---|
|
|
Field method stub |
|
|
Field method stub |
|
|
Field method stub |
|
|
Field method stub |
|
|
Two-step fetcher: per-kind DML chain inside |
|
|
Per-row DML inside one |
|
|
Async DataLoader fetcher + |
|
|
Field method stub |
Neither |
|
Validation error ; build fails |
Both |
|
Validation error ; build fails |
The four Mutation*TableField permits are guaranteed never to carry a @record return: every @record-returning DML mutation 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 |
|---|---|---|
|
|
Column method in |
|
|
Column method in |
|
|
Column method in |
|
|
Column method in |
|
|
As above, against the FK-resolved target column |
|
|
Column method in |
|
|
As above, against the FK-resolved target columns |
Object return type
| Schema Pattern | ChildField Variant |
*Fetchers Generates |
|---|---|---|
|
|
Inlined call in |
|
|
Field method stub |
|
|
Async DataLoader fetcher + |
|
|
Field method stub |
Return |
|
Async DataLoader fetcher + |
Return |
|
Field method stub (correlated subquery ; G5 pending) |
Return |
|
Async DataLoader fetcher + |
Return |
|
Field method stub (correlated subquery ; G5 pending) |
Return |
|
Field method stub |
Return multi-table interface |
|
Field method stub |
Return union |
|
Field method stub |
Return plain object (no |
|
Field method stub (inherits parent table context) |
Constructor-mapped field |
|
Field method stub |
Conflicting directives |
|
Validation error; build fails |
|
|
Validation error; the directive is no longer supported ( |
|
|
Validation error; the directive is no longer supported ( |
Child Fields (on @record parent)
| Schema Pattern | ChildField Variant |
*Fetchers Generates |
|---|---|---|
Scalar/enum with |
|
Column method in |
Return |
|
Async DataLoader fetcher + |
Return |
|
Async DataLoader fetcher + |
Return |
|
Async DataLoader fetcher + |
Return non-table type |
|
Column method in |
|
|
Passthrough; graphql-java’s default |
|
|
Async DataLoader fetcher + |
|
|
Field method stub |
Input Fields (on @table input parent)
| Schema Pattern | InputField Variant |
Used for |
|---|---|---|
|
|
Maps input argument to a table column |
|
|
Maps input argument to a FK column |
|
|
Decodes a composite-PK NodeId into the input’s PK columns (carries |
|
|
Decodes a composite-PK NodeId into FK columns referencing the typeName target |
Return plain object (no |
|
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 |
|---|---|---|---|---|
|
No ; synchronous |
Derived target only |
Allowed ; must preserve N × M contract† |
Never ; result count = M exactly |
|
No ; correlated subquery |
Derived target + correlated parent join |
Allowed ; must preserve N × M contract† |
Never |
|
Yes |
Derived source only |
Allowed |
Allowed |
|
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 |
|
Field returns |
|
GraphQL enum on |
Enum-to-DB string/int mapping |
Source Map
All source lives under graphitron-rewrite/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 |
|
Sealed interface ; all type variants and their record components |
Field hierarchy |
|
Sealed interface ; |
SQL-generating marker |
|
Orthogonal interface ; |
Method-backed marker |
|
Orthogonal interface ; |
DataLoader marker |
|
Orthogonal interface ; |
Table reference |
|
Resolved jOOQ table with PK columns |
Column reference |
|
Resolved column with Java type |
Return type |
|
|
Cardinality |
|
|
Join path |
|
|
WHERE filters |
|
|
Service methods |
|
Resolved Java method with |
DataLoader keys |
|
|
Ordering |
|
|
Pagination |
|
Relay cursor arguments |
Argument extraction |
|
Five direct strategies ( |
Input field hierarchy |
|
Sealed interface ; |
Condition params |
|
Call-site vs body-generation views |
Builders (root package)
| Component | File | Responsibility |
|---|---|---|
Entry point |
|
Sole directive-reading boundary ; assembles |
Type classification |
|
Two-pass: classify types, then enrich interfaces/unions with participants |
Field classification |
|
Classifies fields based on parent type, directives, and return type |
jOOQ lookups |
|
Lazy wrapper around jOOQ |
Service reflection |
|
Reflects |
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 |
|
|
Schema emission ( |
|
The runtime SDL classes graphql-java consumes: per-type wiring entries, applied-directive emission, registration glue |
Error-handling emission ( |
|
The typed-error classes service payloads project errors into; mappings constant + router that selects per-exception |
Runtime helpers ( |
|
Per-app emitted runtime classes ( |
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 Rewrite Design Principles).
Directives
-
SDL definitions:
graphitron-rewrite/graphitron/src/main/resources/no/sikt/graphitron/rewrite/schema/directives.graphqls.RewriteSchemaLoaderauto-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: - Rewrite Design Principles ; architectural and technical principles for the rewrite pipeline - Rewrite Roadmap ; remaining generator work