The generator keeps everything it learns in a small relational database called the fact store. This page explains one habit that shapes every table in it: a table is named for what a single row of it says, never for the question somebody wanted answered. The precise rules, and the tests that enforce the enforceable parts, live in The fact model. This page is the guided tour: what the habit looks like, why it feels roomy to work in, and where it pinches.
A database of sentences
When the generator runs, it reads three things: your GraphQL schema files, your database catalog (through the jOOQ classes generated from it), and the Java classes on your classpath. Everything it learns goes into tables in an embedded H2 database.
Each table holds exactly one kind of statement. graphql_field holds "this type declares this
field"; one row per field, keyed by the field’s own coordinates. sql_table holds "the consumer’s
database declares this table". jvm_class holds "this class exists on the declared classpath".
A table is a stack of filled-in copies of its one sentence, and the sentence itself is written on
the table as a SQL comment, so the store describes itself: connect to it, read
INFORMATION_SCHEMA, and every table and column tells you what it means. The generated
schema reference is rendered from those same comments.
Two words from data modeling are worth glossing, because the whole habit hangs on them. The grain of a table is what one row is about: one row per field, one row per foreign-key hop, one row per lint finding. It is the difference between a spreadsheet of sales with one row per receipt and one with one row per day; same subject, different grain, different table. The fact is the statement the row makes about that grain. Name the grain, state the fact, and the table is designed; the name and the comment both fall out of that sentence.
Label the jar by what is in it
Think of the tables as jars in a pantry. The habit says: label each jar with its contents, "raspberry jam, August 2026", never with your plans for it, "for the Sunday scones".
A contents label keeps working when plans change. Anyone can walk into the pantry and cook something you never thought of, because the labels say what is actually there. A plans label goes stale the moment Sunday passes, and there is a quieter problem: you cannot check it. Whether the jar really contains raspberry jam is a fact you can verify by opening it; whether it is truly "for the scones" is knowable only by asking whoever wrote the label.
Relations work the same way. graphitron_table says "the author put @table on this type,
binding it to this database table name". That is a contents label: you can hold it up against the
schema file and check it. A table named for its consumer, say generator_backing_class, would be
a plans label: its meaning depends on what the generator happens to do this month.
The check that costs one sentence
Before a new table exists, its author finishes this sentence out loud: "one row of this table says that …". Two things disqualify an answer. Naming a consumer ("the class the resolver would bind to this type") fails, because the row’s meaning would then move whenever the resolver does. Naming nothing checkable fails too: a fact needs a source you can hold it against, the schema file, the catalog, the classfile.
"This classfile declares this supertype through this clause" passes. "This directive application spelled this argument" passes. The failing sentences usually split, under mild pressure, into two or three passing ones, and that split is the habit doing its work: the question you started from still gets answered, but by a query that joins the facts, not by a table that froze the question. And when a question turns out to be one that several readers keep asking, its query is pushed down into the database as a named view, so the rule is written once and everyone reads the same answer.
A worked example: which table backs a type?
The generator needs to know which database table a GraphQL type is bound to. The tempting design is one table holding the answer: type in, table out, done.
The store instead holds the ingredients and derives the answer:
-
graphitron_tableholds what the author wrote:@tableon a type, with the name they spelled. -
sql_tableholds what the catalog actually declares. -
intent_spelled_tableis a view that answers a small, general question: how does a written table name meet the catalog? The same rule whether the name was written in@table, in a@referencepath, or as a mutation’s target. -
intent_bound_tableis a view one layer up: which catalog table does this type’s binding resolve to, including how many candidates matched.
That last point is where the design pays for itself. The code generator wants exactly one candidate and refuses an ambiguous binding. The language server, asked to complete a table name in an editor, wants every candidate. Both read the same view, because the candidate count is a column rather than a rule buried inside whichever consumer asked first. A single type-to-table answer table could not have served them both without growing a flag, then a mode, then a second copy.
That column has a failing sibling worth seeing beside it, because the two look equally harmless. The count of the candidates is a column. The candidates themselves, glued into one string in a column next to it, are not, and the reason is the question an author actually asks of a list: is the table I care about one of them. A glued string can answer only whether the whole list is exactly the list you named, so membership takes picking the string apart, and every reader picks it apart its own way. The candidates are rows keyed the same way the count is, so a reader joins and asks. Back in the pantry: a label saying how many jars is a fine label, and three jars taped together under one label are no longer three jars.
The history is telling and ordinary: intent_bound_table began as a subquery inside one
consumer’s query, and became a named view the day a second consumer asked the same question.
Facts stayed facts; the question got promoted to a view when it earned it.
What the habit buys
Nothing here is magic; each benefit is a direct consequence of rows meaning something on their own.
You can read the store like prose. Every table and column carries its sentence as a comment.
Debugging a wrong answer starts with SELECT, not with a debugger: query the view, then query
the facts under it, and at each step there is an independent source to compare against.
New capabilities are additions. A new fact lands as a new relation beside the old ones, and a new question lands as a query over them, promoted to a shared view once more than one reader asks it. Existing tables do not change shape to accommodate it, so existing readers do not notice.
Consumers agree by construction. The generator, the language server, and the documentation tools each ask their own queries against the same base tables and shared views. There is no moment where two private copies of the model drift apart, because there are no private copies.
Tests get real oracles. A fact table can be checked against the file, catalog, or classfile it transcribes. A question-shaped table has no independent source; the only available oracle is the old code being replaced, and comparing against that quietly turns yesterday’s bugs into pinned expectations.
Where it pinches
It would be dishonest to sell this as free.
There are more tables than a first sketch would draw. The store holds a couple of hundred relations, and a question that felt like "one table" routinely becomes three facts and a view. Each piece is small and self-describing, but the census is real, and so is the naming effort: the one-sentence check takes actual thought, and it is a habit rather than a gate. The build enforces the surroundings (every relation commented, every relation inside a chartered family with its own prefix), but no test can check that a name was honest. That part is review and culture.
The rule also has sanctioned exceptions and known deviations, written down rather than hidden.
The diagnostic view is deliberately question-shaped: it is a read surface unioning several
families' verdicts for whoever asks "what is wrong", and the roster records it as the exemption.
And the fact-model page names, in the open, the places where today’s store stores a derivation
it should derive. The discipline is a compass, not a purity test; the pages that state the rules
also state where the tree currently falls short of them.
Where to go next
The strict statement of all of this, each rule with its enforcing test named, is
The fact model. The rendered
schema reference shows every relation with its sentence. And
the store is a database you can simply open: a running session can serve it over the PostgreSQL
wire protocol (see StoreConsole in graphitron-model), and psql plus INFORMATION_SCHEMA
make a fine reading room.