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.$project(sel.getFieldsGroupedByResultKey(), table, env) for the column list, then dsl.select(fields).from(table)…​

  • Inline nesting: use jOOQ multiset(select(columns).from(CHILD).where(…​)).as("alias") returning Field<?> (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 throws ClassCastException at runtime.

  • GraphQL-Java delivers ID scalars as String ; a cast to Long (or any numeric PK type) also throws.

  • The one-argument form ignores the column’s registered jOOQ Converter entirely.

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: LookupRows.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.

Column-to-column comparison: ColumnComparison

The companion rule to the one above, on the other axis. Where DSL.val(rawValue, col.getDataType()) covers "bind a value against a column", every comparison of two catalog columns goes through no.sikt.graphitron.render.ColumnComparison and none is spelled inline.

The reason is that the two columns can disagree on their Java type while agreeing on their SQL type. A jOOQ Converter is a client-side type mapping only: it changes what Field.getType() reports and cannot change the column’s SQL type. A consumer whose codegen selects a <forcedType> by column path rather than by type therefore lands the converter on one end of a foreign key and not the other, and the two ends diverge: one stays Field<Short> while the other becomes Field<String>. a.eq(b) then does not compile, and the whole generated module fails with it, even though a = b was always a valid, index-usable predicate.

ColumnComparison reconciles that with coerce, which reinterprets a field’s Java type and leaves the rendered SQL alone. Reach for it, never for cast: cast puts a real SQL CAST around the column and costs the index on it.

Entry point When the right operand is

equality

another aliased table column. Overloaded for the walk’s ColumnRef and the command tier’s CatalogColumn, which carries its bound type as a name and is decoded inside the mint.

equalityAgainstField

some other Field expression typed by a known catalog column, typically a parentInput.field(…) lookup into a VALUES-derived table. The caller names the column the lookup’s DataType was spelled from.

equalityAgainstValue

a bare Java value, so there is nothing for coerce to attach to. This is the two rules composed: bind the value at the DataType of the column it was read from, which makes it a Field rendering through that column’s converter, then coerce that field onto the receiver.

Two properties are worth knowing before touching a call site. A pair whose types agree, or either of whose types is unknown, emits left.eq(right) unchanged, so no approved generated output moves for a schema that compiles today. And the argument is coerced onto the receiver in both directions: neither "raw" nor "referenced" is recoverable from a TypeName, and since neither operand is a bind value the direction is invisible in the emitted SQL, so the rule is chosen for mechanical reviewability rather than for semantics.

A .onKey(Keys.<CONSTANT>) join needs none of this: it names a catalog constant and is type-blind by construction. So is any comparison whose two operands take their type from the same ColumnRef, which is most of the generator’s eq sites and the reason the mint has as few callers as it does.

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 KeyLift.Lifter(LifterRef) verdict, the LifterRef carrying the declaring class + method name. The residue SourceKey derives its Wrap.Row from that lift arm, and the correlation is a ParentCorrelation.OnLiftedSlots (target table + column tuple, single-hop by construction). Each lifted slot folds 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 ParentCorrelation.OnLiftedSlots.columns(), 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) ; the DataType and its registered Converter do 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.