Conventions for the code the generator emits; the reference companion to the "Generated code is a consumer artifact" axiom in Graphitron Development Principles.
Return types
DataFetchers return Result<Record> ; no DTOs, no TypeMappers. GraphQL-Java traverses records
using the registered field DataFetchers. Exception: Connection fields return ConnectionResult, a
generated carrier wrapping Result<Record> + pagination context.
Selection-aware queries
DataFetchingFieldSelectionSet and SelectedField are threaded through all table method
signatures, structurally committing to selection-aware queries:
-
Top-level queries: call
Type.$fields(sel, table, env)for the column list, thendsl.select(fields).from(table)… -
Inline nesting: use jOOQ
multiset(select(columns).from(CHILD).where(…)).as("alias")returningField<?>(type-erased). Use type erasure at every helper method boundary ; jOOQ generic types compound badly with nesting depth, causing slow compile times. -
@splitQuery: separate DataLoader; parent fetches FK/PK columns, child batches by those keys.
Selection-driven queries produce different SQL per request, preventing cached query-plan reuse.
This is an acceptable trade-off for wide tables with large optional columns; for narrow tables
(≤ 10 columns) where most fields are always requested, TABLE.* is simpler and the dynamic-column
overhead exceeds the benefit.
Error quality
BuildContext.candidateHint(attempt, candidates) sorts candidates by Levenshtein distance. The
Levenshtein-suggestion contract has consolidated onto BuildContext and Rejection (the
rejection-construction sites), with classifier-side callers thinning out as rejections are
produced through the typed sealed-result path. When adding new jOOQ existence checks in the
validator or builder, follow the same pattern ; pass the relevant candidate list from
JooqCatalog to candidateHint, or produce the rejection through Rejection.unknownName(…) so
the candidate list rides on the typed result.
Column value binding: DSL.val(rawValue, col.getDataType())
When emitting code that binds a raw GraphQL input value (from an input map or
env.getArgument(…)) to a specific jOOQ column, always use the two-argument form:
DSL.val(rawValue, table.COL.getDataType())
Do not use the one-argument form with a Java-side cast (DSL.val((JavaType) rawValue)):
-
GraphQL-Java delivers enum values as
String; a Java cast to the jOOQ enum class throwsClassCastExceptionat runtime. -
GraphQL-Java delivers
IDscalars asString; a cast toLong(or any numeric PK type) also throws. -
The one-argument form ignores the column’s registered jOOQ
Converterentirely.
The two-argument form hands rawValue to the column’s DataType and its registered Converter
at bind time. No SQL CAST is rendered; the coercion is purely Java-side, inside jOOQ.
CallSiteExtraction solves a different problem. Its value-coercion strategies exist to produce
a typed Java value for a condition/ordering method parameter ; code paths where a
developer-written method expects the column’s Java type, not a jOOQ Field<T>. For inline jOOQ
DSL expressions (INSERT values(…), UPDATE set(…), DELETE/UPDATE where(…) predicates),
DSL.val(rawValue, col.getDataType()) does the coercion inside jOOQ without any Java-side step,
and no CallSiteExtraction switch is needed.
Precedent: LookupValuesJoinEmitter.addRowBuildingCore (search for DSL.val with two arguments).
For DTO-parent batching ; where the parent’s backing class is a plain POJO or Java record without
a jOOQ FK to the child’s @table ; see DTO-parent batching below; the
lifter contract is how the schema author hands the framework a typed key when the catalog can’t
supply one.
DTO-parent batching
When a record-backed parent’s backing class (reflected from its producer) is not a jOOQ
TableRecord, the catalog cannot supply the FK columns the column-keyed DataLoader path needs to
batch a child @table field. The @sourceRow directive closes that gap: the schema author
supplies a static Java method that lifts a RowN<…> batch key out of the parent DTO, plus the
targetColumns (column names on the child table) the key positions match. The classifier reflects
on the lifter once at build time, validates the per-position column-class match, and produces a
SourceKey whose Reader is SourceRowsCall(lifter) and whose Wrap is Row, carrying a
JoinStep.LiftedHop (target table + slot list, single-hop by construction) in the path and a
LifterRef (declaring class + method name) on the reader. The lifted slots are
JoinSlot.LifterSlot permits, each folding source-side and target-side onto a single ColumnRef
by construction (DataLoader key tuple IS target-column tuple as a type fact, not a prose
precondition). The emitter feeds that into the existing SplitRowsMethodEmitter.buildListMethod
path with no identity branching: target accessors come from HasSlots.slots(), key extraction
from the lifter call. The lifter is the single place where the DTO → key mapping lives; if a
schema author needs a different key, they author a new lifter method, not a new emitter arm.
Helper-locality
Emitted helper methods that bind column references to a specific aliased jOOQ Table instance
always take the Table as a parameter ; never declare it locally. Callers from different paths
(root fetcher, inline subquery, Split-rows method) need to pass distinct aliases for the same
target table; a locally-declared Table forces the wrong alias on every caller but the one the
helper was first written for.
Pattern (canonical example, <fieldName>OrderBy emitted by
TypeFetcherGenerator.buildOrderByHelperMethod):
private static OrderByResult <fieldName>OrderBy(DataFetchingEnvironment env, <Table> table) { ... }
Each call site supplies the alias appropriate to its scope: the root fetcher passes its declared
<entity>Table; a Split-rows method passes its FK-chain terminal alias; an inline subquery passes
its correlated alias. The helper’s column references resolve through the parameter.
The rule does not apply to helpers that are not Table-bound (e.g. cursor encode/decode helpers
operating on Field<?> / SortField<?>).
Cursor encode/decode
The emitter companion to the "Model metadata over parallel type systems" principle: each cursor
column’s jOOQ DataType is already known, so cursor round-tripping goes through the column
metadata rather than a hand-rolled type-tag system (i:, s:, l:).
-
Encode/decode a seek value:
field.getDataType().convert(rawValue); theDataTypeand its registeredConverterdo the coercion, exactly as with `DSL.val’s two-argument form. -
The no-seek case:
DSL.noField(field)where a column position must be present but no seek value applies, rather than a null placeholder or an omitted column that would misalign the tuple.
OrderByResult (pairing List<SortField<?>> with List<Field<?>>) is the carrier these operate
over.