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

@table

Full SQL generation ; queries, joins, projections.

Result-mapped

@record

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

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 has @record.

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: FK key → FkJoin; condition method → ConditionJoin

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

Directive Pattern on Type GraphitronType Variant Generator Output

@table (no @node, no @discriminate)

TableType

Fetchers class + class

@table + @node

NodeType

Fetchers class + class (with Relay ID handling)

@record

ResultType*

*Fetchers class only (no SQL scope of its own)

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

TableInputType

Used in mutation generation

Input type with @table + @record

InputType* (via @record)

On input types, @record dominates @table. When both are present, @table is ignored and the input classifies as if only @record were declared; a build warning names the shadowed directive. Clean up by removing @table from the input declaration ; the warning disappears naturally.

Input type without @table

InputType*

No generation (developer-provided class)

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 @record

PlainObjectType

No generation; nested under a parent table-bound or result-bound 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.

GraphQL scalar type matching the extended-scalars convention table (no directive, graphql-java-extended-scalars on the classpath)

ScalarType

Same as above, but the constant FQN is read from ScalarTypeResolver’s 30-entry convention table (`scalar BigDecimalExtendedScalars.GraphQLBigDecimal, etc.).

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).

GraphQL scalar type with no directive and no convention-table entry

UnclassifiedType

Validation error pointing at @scalarType(scalar:) or extended-scalars as the 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, 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 @lookupKey

QueryLookupTableField

Async DataLoader fetcher + sync lookup*() rows method

@tableMethod

QueryTableMethodTableField

Field method stub

Field named node (Relay)

QueryNodeField

Field method stub

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

QueryNodesField

Async DataLoader fetcher dispatching by NodeId

Return: @table+@discriminate interface

QueryTableInterfaceField

Field method stub

Return: multi-table interface

QueryInterfaceField

Field method stub

Return: union

QueryUnionField

Field method stub

@service, return @table type

QueryServiceTableField

Async DataLoader fetcher + rows*() method

@service, return non-table type

QueryServiceRecordField

Field method stub

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.

Mutation Fields

Schema Pattern MutationField Variant *Fetchers Generates

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

MutationInsertTableField

Field method stub

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

MutationUpdateTableField

Field method stub

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

MutationDeleteTableField

Field method stub

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

MutationUpsertTableField

Field method stub

@mutation(typeName: INSERT|UPDATE|DELETE|UPSERT), returning a single-record @record(record:{className:…​})-or-bare-SDL carrier (one recognized data field, optional errors-shaped field)

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

Field method stub

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 @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

@field(name:) or matching column name

ColumnField (compaction = Direct)

Column method in wiring()

@reference on scalar

ColumnReferenceField (compaction = Direct)

Column method in wiring()

@reference on scalar declared on a TableInterfaceType participant

ParticipantColumnReferenceField

Column method in wiring(); picks up the participant-side FK rather than the interface-side resolver

@nodeId (single-column PK), no typeName

ColumnField (compaction = NodeIdEncodeKeys)

Column method in wiring(); the projected column is wrapped in the per-Node encode<TypeName> helper

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

ColumnReferenceField (compaction = NodeIdEncodeKeys)

As above, against the FK-resolved target column

@nodeId (composite PK), no typeName

CompositeColumnField

Column method in wiring(); projects RowN<…​> of the parent’s PK columns and wraps in encode<TypeName>

@nodeId(typeName:), composite PK target

CompositeColumnReferenceField

As above, against the FK-resolved target columns

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

Field method stub

@service, return @table

ServiceTableField

Async DataLoader fetcher + rows*() method

@service, return non-table

ServiceRecordField

Field method stub

Return @table, @splitQuery + @lookupKey

SplitLookupTableField

Async DataLoader fetcher + rows*() method

Return @table, @lookupKey (no split)

LookupTableField

Field method stub (correlated subquery ; G5 pending)

Return @table, @splitQuery

SplitTableField

Async DataLoader fetcher + rows*() method

Return @table (default)

TableField

Field method stub (correlated subquery ; G5 pending)

Return @table+@discriminate interface

TableInterfaceField

Field method stub

Return multi-table interface

InterfaceField

Field method stub

Return union

UnionField

Field method stub

Return plain object (no @table)

NestingField

Field method stub (inherits parent table context)

Constructor-mapped field

ConstructorField

Field method stub

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 @record parent)

Schema Pattern ChildField Variant *Fetchers Generates

Scalar/enum with @field

PropertyField

Column method in wiring()

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 PojoResultType 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

Column method in wiring()

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

Field method stub

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-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

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

FkJoin / ConditionJoin / 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 — 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

Five direct strategies (Direct / EnumValueOf / TextMapLookup / ContextArg / JooqConvert) plus two sealed sub-groupers: NestedInputField (descends an input-object graph for @condition on INPUT_FIELD_DEFINITION) and NodeIdDecodeKeys (SkipMismatchedElement / ThrowOnMismatch; arity-N NodeId decode at the call site)

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 Rewrite Design Principles).

Directives

  • SDL definitions: graphitron-rewrite/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: - Rewrite Design Principles ; architectural and technical principles for the rewrite pipeline - Rewrite Roadmap ; remaining generator work