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 |
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 |
|
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 |
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 is class-backed: its backing Java/jOOQ class is reflected from the producing field’s return type, not from a directive. |
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
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 |
|---|---|---|
|
|
|
Producer’s reflected return is a Java/jOOQ class (a |
|
|
|
|
|
Interface with |
|
|
Interface without |
|
|
Union type |
|
|
|
|
No generation (error mapping config) |
Input type with |
|
Used in mutation generation. A build warning fires per |
Input type without |
|
No generation (developer-provided class). The input’s backing class is reflected from the parameter type it flows into. |
Input type with |
|
No generation ; column binding resolved per field-usage |
Object type with no |
|
No generation; nested under a parent table-bound or class-backed scope via |
GraphQL |
|
No generation; enum values map to DB strings/ints at column-bind time |
GraphQL |
|
No generation; the consumer’s |
Spec built-in scalar ( |
|
Same registration shape; Java type comes from a closed table (the spec binds these names). |
Non-spec / non-federation |
|
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, 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(permittingQuery/Mutation) is a root field,OnlyChild/Childis a nested field (one or many source objects arriving). Write operations are legal only onRoot.Mutation,NodeResolveonly onRoot.Query,Nestonly on a nested source. The nested arms wrap aSourceShape(Table/Record), the catalog-vs-Java polarity of what arrives atenv.getSource(). -
operation, the verb the field performs: reads (Fetch,Paginate,Lookup,NodeResolve,Nest, and the modeled-but-unpopulatedEntityResolve/Count/Facet), writes (Insert,Upsert,Update,Delete, and the unpopulatedUpdateMatching/DeleteMatching), and the developerServiceCall(the read-vs-write split it once carried is now read off thesourceroot). -
target, the field’s projection endpoint, a wrapper (Single/List) around aTargetShape. The shape carries build-vs-consume:Table/Columnare catalog shapes graphitron builds the SQL for;Record/Fieldare domain shapes graphitron consumes without having built;Connection,Interface,Unionare the container and polymorphic shapes. `Table:Column-
Record:Field` (mirror : reflect). A Relay connection is
Single(Connection(…)), its windowed-read verb thePaginateoperation.
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
Fetchersclass: a synchronous field fetcher, or an asynchronous DataLoader fetcher paired with arows()batch method (called out as DataLoader-backed where it matters). These are theIMPLEMENTED_LEAVES. -
Inline projection ; no per-field method ; the value is projected straight into the parent’s SELECT by
TypeClassGenerator.$fields. These are thePROJECTED_LEAVES. -
Wiring value ; no generated method either ; the field is registered as a
DataFetcher/ColumnFetchervalue throughFetcherEmitter/FetcherRegistrationsEmitter. These areIMPLEMENTED_LEAVESleaves 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
CompositeColumnReferenceFieldis deferred today (STUBBED_VARIANTS).
Query Fields
| Schema Pattern | QueryField Variant |
*Fetchers Generates |
|---|---|---|
Any argument has |
|
Synchronous fetcher + |
|
|
Fetcher method |
Field named |
|
Fetcher method |
Field named |
|
Async DataLoader fetcher dispatching by NodeId |
Return: |
|
Fetcher method |
Return: multi-table interface |
|
Fetcher method(s) ; per-participant polymorphic projection |
Return: union |
|
Fetcher method(s) ; per-participant polymorphic projection |
|
|
Async DataLoader fetcher + |
|
|
Fetcher method |
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.
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 |
|---|---|---|
|
|
Fetcher method ; write + read-back SELECT (the derived re-fetch) |
|
|
Fetcher method ; write + read-back SELECT (the derived re-fetch) |
|
|
Fetcher method ; write + encoded-PK off |
|
|
As above; the write target is named on the field (R457) rather than on the input type. Byte-identical carrier to the |
|
|
Validation error ; build fails. |
|
|
Rejected at classification ; UPSERT generation gated pending R145 |
|
|
Two-step fetcher: per-kind DML chain inside |
|
|
Per-row DML inside one |
|
|
Async DataLoader fetcher + |
|
|
Fetcher method |
Neither |
|
Validation error ; build fails |
Both |
|
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 |
|---|---|---|
|
|
Wiring value ( |
|
|
Inline projection in |
|
|
Wiring value ; materialised by the enclosing |
|
|
Wiring value ( |
|
|
Inline |
|
|
Inline |
|
|
Deferred stub ; rooted-at-parent composite NodeId reference, pending |
Object return type
| Schema Pattern | ChildField Variant |
*Fetchers Generates |
|---|---|---|
|
|
Inlined call in |
|
|
Fetcher method |
|
|
Async DataLoader fetcher + |
|
|
Fetcher method |
Return |
|
Async DataLoader fetcher + |
Return |
|
Inline projection in |
Return |
|
Async DataLoader fetcher + |
Return |
|
Inline projection in |
Return |
|
Fetcher method |
Return multi-table interface |
|
Fetcher method(s) ; per-participant polymorphic projection |
Return union |
|
Fetcher method(s) ; per-participant polymorphic projection |
Return plain object (no |
|
Inline projection ; nested fields fold into the parent SELECT, the object itself wired as an |
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 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 |
|
Wiring value ; property read off the parent record |
Return |
|
Async DataLoader fetcher + |
Return |
|
Async DataLoader fetcher + |
Return |
|
Async DataLoader fetcher + |
Return non-table type |
|
Wiring value ; read off the parent record |
|
|
Passthrough; graphql-java’s default |
|
|
Async DataLoader fetcher + |
|
|
Fetcher method |
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/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 |
|
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 Graphitron Development Principles).
Directives
-
SDL definitions:
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: - Graphitron Development Principles ; the six axioms, their corollaries, and the named enforcement behind each - Rewrite Roadmap ; remaining generator work