Prefix: store_.
Every run of the generator leaves a record of itself here: which module it captured, which files it read, what configuration it held in hand, and where it wrote. The rows answer bookkeeping questions rather than schema questions: what was this store built from, and which graph do these facts belong to. When two runs disagree, this family is where you look up what each of them actually saw.
Where to start
- store_graph
-
A graph the store holds: the anchor of the graph_name partition dimension, one row per module ever captured into this store.
- store_graph_source
-
The membership relation store_graph’s comment defers to: which sources are the joining graph’s.
- store_stamp
-
What this store was built from.
How this family meets the others
graphql_-
45 foreign keys from that family’s rows into this one.
graphitron_-
94 foreign keys from that family’s rows into this one.
sql_-
4 foreign keys from that family’s rows into this one.
javac_-
1 foreign key from that family’s rows into this one.
code_-
3 foreign keys from that family’s rows into this one.
intent_-
4 foreign keys from that family’s rows into this one.
rejection_-
1 foreign key from that family’s rows into this one.
lint_-
1 foreign key from that family’s rows into this one.
build_warning_-
1 foreign key from that family’s rows into this one.
Why the name is right
The store’s own record: what it read, what it was built from, and which graphs it holds. Its rows are never a reading of the consumer’s schema, database or classpath, which is what the transcription families are named for; the graph recipe rows are configuration the run held in hand, which is what keeps them in this family rather than making them a family of their own.
store_class_file
One class file the classpath reading found, at the entry it was found under and the path it sits at: the unit a reading opens, and therefore the unit it can decline to open again. For example no/sikt/example/FilmService.class under a consumer’s target/classes is one row, and a file a later compile removed is no row at all.
Primary key: (source_name, file_path). Foreign key: (source_name) references store_source (source_name).
source_name(CHARACTER VARYING, not null)-
the classpath entry the file was found under; the key’s leading dimension and what it is swept with, an entry that leaves taking its files by cascade
file_path(CHARACTER VARYING, not null)-
where the file sits under that entry, forward-slashed on every platform so a store written on one machine and compared on another does not disagree about files neither has changed. With the entry above, the grain. Not the class name with dots for slashes: what a file declares is what reading it said, which code_class states
byte_size(BIGINT, nullable)-
the file’s length when it was last read; half of what a later reading compares to decide whether to open it again. Null where it could not be established, which reads as changed
mtime(TIMESTAMP, nullable)-
when the file was last written, as the filesystem reports it; the other half of the comparison. Trustworthy here because this is build output and a compiler wrote it, which is not a claim to make about a file a person edits
touched_at(TIMESTAMP, not null)-
the reading that last found this file; what the sweep compares against, so a file the reading no longer finds is deleted and takes what was read out of it with it
store_graph
A graph the store holds: the anchor of the graph_name partition dimension, one row per module ever captured into this store. Two discriminators keep the DDL’s FK conventions readable against this relation. First, why the SDL roots carry an FK here while the SDL-to-store_source FK was declined: the graph is ambient before the walk begins and NOT NULL on every row, while the source rows are a summary collected last and nullable at schema-level sites, so the FK doctrine admits one and not the other. Second: any derivation joining an SDL fact to a catalog or classpath fact (graphitron_service_entry’s class name against code_class, graphitron_table_entry’s table reference against sql_table) is underdetermined in a shared store until a membership relation says which sources are the joining graph’s; store_graph_source below is that relation, landed with its first consumer (the inferred claim view), and such a join scopes its catalog side through it.
Primary key: (graph_name).
graph_name(CHARACTER VARYING, not null)-
the graph’s configured name (the Maven <graphName> parameter, defaulting to the module’s artifactId); the value every partitioned key leads with
base_dir(CHARACTER VARYING, not null)-
the capturing run’s base directory, absolute and normalized. NOT NULL and deliberately not grouped with the nullable build-identity pair: every run has a directory (RunContext requires basedir of every caller) even when it has no build file, and this column is what the ownership check reads when a run’s graph_name is already recorded against a different directory
build_file_path(CHARACTER VARYING, nullable)-
the module’s build file (its pom), absolute and normalized; NULL on a programmatic run with no build file
build_file_stamp(CHARACTER VARYING, nullable)-
content hash of the build file, spelled <scheme>:<hex> as SourceStamp writes it and always the sha256: scheme, a build file being read rather than resolved; the graph’s build identity: the remembered recipe is trusted only while the build file still hashes to this, and a mismatch marks the recipe possibly stale until the module’s own next build repairs it
last_captured(TIMESTAMP, not null)-
when this graph’s own run last captured it; the age half of the age/currency distinction, and the bookkeeping a future eviction surface reads
store_graph_lint_disabled_rule
The <lint><disabledRules> half, one row per silenced rule id. Decomposed rather than rendered: a rendered block would be a string a later reader has to re-parse, which is the shape the recipe’s source names exist to remove, and permitting a rendered form per parameter would reintroduce the untyped default door. The two <lint> halves are a genuine conjunction (LintConfig is a plain record of both) but they are not the same shape, which is why they are two relations rather than one discriminated one: this half is a Set and takes no ordinal, its sibling is a List and takes one, and forcing them together would need a nullable ordinal.
Primary key: (graph_name, rule_id). Foreign key: (graph_name) references store_graph (graph_name).
graph_name(CHARACTER VARYING, not null)-
the owning graph’s partition, anchored by store_graph
rule_id(CHARACTER VARYING, not null)-
the disabled rule’s id as configured; the value is the key, there being no position to record. An ordinal here would record the JVM’s iteration order over a Set and call it a position
touched_at(TIMESTAMP, not null)-
when the reading that wrote this row ran. A run transcribes the whole of its configuration, so the reading finishes by deleting this graph’s rows carrying a different instant: those are the parameters the run no longer had, which an upsert cannot find because there is no incoming row to match. NOT NULL is what makes that total, a row with no instant being one no reading claims and no sweep reaches. Keyed by the rule id, so a rule the author re-enabled leaves a row this reading did not touch rather than a row it overwrote
store_graph_lint_excluded_type
The <lint><excludedTypes> half, one row per type-name glob excluded from the SDL lint engine. Ordinal-keyed because the configured value is a List and its order is the author’s, which is the grain rule’s "an ordinal only where the source is genuinely ordered".
Primary key: (graph_name, ordinal). Foreign key: (graph_name) references store_graph (graph_name).
graph_name(CHARACTER VARYING, not null)-
the owning graph’s partition, anchored by store_graph
ordinal(INTEGER, not null)-
position in the configured list, document order
type_pattern(CHARACTER VARYING, not null)-
the type-name glob as configured
touched_at(TIMESTAMP, not null)-
when the reading that wrote this row ran. A run transcribes the whole of its configuration, so the reading finishes by deleting this graph’s rows carrying a different instant: those are the parameters the run no longer had, which an upsert cannot find because there is no incoming row to match. NOT NULL is what makes that total, a row with no instant being one no reading claims and no sweep reaches
store_graph_output
Where a generating run wrote: the three output coordinates travelling together, because they are present together on any generating run and jointly answer one question. A validate-only run has no row rather than a row carrying the inert package sentinel the validate goal substitutes to satisfy the context’s non-null contract: that sentinel is the run’s own admission that it had no output coordinates, and transcribing it would mint the derived fact that can disagree.
Primary key: (graph_name). Foreign key: (graph_name) references store_graph (graph_name).
graph_name(CHARACTER VARYING, not null)-
the owning graph’s partition, anchored by store_graph
output_package(CHARACTER VARYING, not null)-
the root Java package generation wrote under, from <outputPackage>
jooq_package(CHARACTER VARYING, not null)-
the root Java package of the consumer’s jOOQ-generated catalog, from <jooqPackage>; what every @table and @field was resolved against
output_directory(CHARACTER VARYING, not null)-
the directory generation wrote sources into, absolute and normalized, from <outputDirectory> resolved against the base directory
touched_at(TIMESTAMP, not null)-
when the reading that wrote this row ran. A run transcribes the whole of its configuration, so the reading finishes by deleting this graph’s rows carrying a different instant: those are the parameters the run no longer had, which an upsert cannot find because there is no incoming row to match. NOT NULL is what makes that total, a row with no instant being one no reading claims and no sweep reaches. A validate-only run writes no row here, so a module that generated once and now only validates loses its coordinates rather than keeping the last generating run’s
store_graph_schema_extension
The recipe’s effective schema-file-extension filter, one row per accepted extension. A per-run set rather than a per-binding one, which is why it sits beside the bindings rather than under them.
Primary key: (graph_name, ordinal). Foreign key: (graph_name) references store_graph (graph_name).
graph_name(CHARACTER VARYING, not null)-
the owning graph’s partition, anchored by store_graph
ordinal(INTEGER, not null)-
stable position in the resolved set, for faithful replay
extension(CHARACTER VARYING, not null)-
an accepted schema-file extension including the leading dot, as configured
touched_at(TIMESTAMP, not null)-
when the reading that wrote this row ran. A run transcribes the whole of its configuration, so the reading finishes by deleting this graph’s rows carrying a different instant: those are the parameters the run no longer had, which an upsert cannot find because there is no incoming row to match. NOT NULL is what makes that total, a row with no instant being one no reading claims and no sweep reaches
store_graph_schema_input
The graph’s SDL recipe, one row per resolved recipe entry: configuration the run held in hand, written fresh by every run, never a derivation over captured rows. It records what the read-set never can: how to find the graph’s schema files, including ones that do not exist yet, so a currency check can re-expand the globs over base_dir without building the module. One discriminated relation rather than one per kind, because the ordinal is the recipe’s spine and splitting the relations would shatter the one ordering key. A reader of another graph’s recipe rows is maintenance machinery and counts as such exactly while it writes no conclusions outside the store_ family; these rows never join the cross-graph consumer read surface, whose enumeration axis is store_graph and store_graph_supergraph and whose payload axis is the SDL-derived families only.
Primary key: (graph_name, ordinal). Foreign key: (graph_name) references store_graph (graph_name). Check: "KIND" IN('pattern', 'file', 'named').
graph_name(CHARACTER VARYING, not null)-
the owning graph’s partition, anchored by store_graph
ordinal(INTEGER, not null)-
entry position in the resolved configuration, document order
kind(CHARACTER VARYING, not null)-
a closed taxonomy of what entry_value holds: a glob pattern a build resolved, a literal file a programmatic caller handed over, or a literal bare label. The two literal kinds transcribe which door the entry’s source came through, so a replay recovers the arm from the row instead of re-asking the filesystem a question about a stored string
entry_value(CHARACTER VARYING, not null)-
the entry as configured: an include pattern in the recipe’s one glob dialect (SchemaRecipe owns the expansion) when kind is pattern, and the source’s canonical rendering when it is file or named
tag(CHARACTER VARYING, nullable)-
the entry’s tag, when configured; not optional fidelity: the generator’s emitted schema carries the tag applications and the capture’s assembly judges them, so a replay without it would judge a different schema than the graph’s own build. No relation transcribes the applications themselves
description_note(CHARACTER VARYING, nullable)-
the entry’s description note, when configured; kept for the same replay-fidelity reason as tag
touched_at(TIMESTAMP, not null)-
when the reading that wrote this row ran. A run transcribes the whole of its configuration, so the reading finishes by deleting this graph’s rows carrying a different instant: those are the parameters the run no longer had, which an upsert cannot find because there is no incoming row to match. NOT NULL is what makes that total, a row with no instant being one no reading claims and no sweep reaches. A recipe that lost an entry is the case to read it against: the surviving entries upsert over the leading ordinals and the trailing ones are what the sweep collects
store_graph_session_mount
The <sessionState> <mount> reference: the consumer’s static Java method that mounts identity on each acquired connection, as authored. Row presence is the fact, per the family’s absence rule: no row means no identity is mounted. The primary key on graph_name alone makes "at most one mount per graph" structural. Only the authored string lands here; the reflected signature is a build-time model fact, never stored back into this provenance family.
Primary key: (graph_name). Foreign key: (graph_name) references store_graph (graph_name).
graph_name(CHARACTER VARYING, not null)-
the configuring graph’s partition, anchored by store_graph
mount_method(CHARACTER VARYING, not null)-
the mounting method as authored, fqcn#method, from <mount>
touched_at(TIMESTAMP, not null)-
when the reading that wrote this row ran. A run transcribes the whole of its configuration, so the reading finishes by deleting this graph’s rows carrying a different instant: those are the parameters the run no longer had, which an upsert cannot find because there is no incoming row to match. NOT NULL is what makes that total, a row with no instant being one no reading claims and no sweep reaches
store_graph_session_unmount
The optional <unmount> reference beside the mount. Row presence is the fact: no row means the supported mount-only configuration (the next request’s mount overwrites wholesale), which the reconciler admits without ceremony, and the foreign key to store_graph_session_mount is the "unmount without mount is a defect" rule made structural.
Primary key: (graph_name). Foreign key: (graph_name) references store_graph_session_mount (graph_name).
graph_name(CHARACTER VARYING, not null)-
the configuring graph’s partition, anchored by store_graph_session_mount
unmount_method(CHARACTER VARYING, not null)-
the unmounting method as authored, fqcn#method, from <unmount>
touched_at(TIMESTAMP, not null)-
when the reading that wrote this row ran. A run transcribes the whole of its configuration, so the reading finishes by deleting this graph’s rows carrying a different instant: those are the parameters the run no longer had, which an upsert cannot find because there is no incoming row to match. NOT NULL is what makes that total, a row with no instant being one no reading claims and no sweep reaches. Swept before the mount beside it, which is the foreign key deciding the order rather than a rule this family states twice
store_graph_source
The membership relation store_graph’s comment defers to: which sources are the joining graph’s. One row per source the graph’s run actually read, every kind alike (schema files, jOOQ schema packages, classpath entries), because kind is an axis on store_source and a kind-filtered membership would make completeness a function of which consumers had shipped, leaving a reader unable to tell "not this graph’s" from "kind not captured yet". What the run read, not configuration: the recipe rows above hold patterns the run held in hand, including files that do not exist yet, while a row here names a source the walk met. Graph-keyed, so a capture writes exactly its own graph’s rows: it claims each source it read and deletes the claims it no longer holds, rather than clearing and rewriting, the stamp being a record of what this graph last transcribed that a clear would throw away. Any derivation joining a graph-keyed fact to a source-keyed one (the column-match claim view is the first) scopes its catalog side through this relation, which is what keeps one graph’s resolution from seeing a sibling module’s tables in a shared store.
Primary key: (graph_name, source_name). Foreign key: (graph_name) references store_graph (graph_name). Foreign key: (source_name) references store_source (source_name).
graph_name(CHARACTER VARYING, not null)-
the member graph, anchored by store_graph
source_name(CHARACTER VARYING, not null)-
a source the graph’s run read, anchored by store_source; the scan’s hand-built stand-ins record against the empty source name like their class rows do
stamp(CHARACTER VARYING, nullable)-
what this graph’s rows of this source were derived from, or null where it holds a claim but no transcription it may trust. Per graph rather than per source: the stamp on store_source says what the file last hashed to for whoever read it, which answers nothing about whether a given graph has transcribed those bytes, so only this column can tell a reading that a scope is already current and may be left alone
read_at(TIMESTAMP, nullable)-
when this graph last transcribed this source, which is the instant its rows of it carry. The stamp says what the bytes were and this says when they were read, and a reading deciding whether it may leave a scope alone needs both. Not advanced by a reading that skipped the source: the rows still date from the reading that wrote them, and a column claiming otherwise would describe a transcription that never ran
store_graph_supergraph
Which supergraph a graph declared itself a subgraph of: the graph’s own declaration of its <supergraph> parameter, minted and cleared by the graph’s own run like every other graph-keyed row. What it asserts is grouping, not federation. Declaring membership does not make a graph federated and is not policed against the SDL’s opt-in, which graphitron_link_entry already records as a predicate over the @link url; the grouping is deliberately usable before any federation SDL lands, since a subgraph under development may declare its home before its first @key is written. Only graphs with a declared supergraph are registered, so the row’s presence is the fact and a standalone graph has no row; a nullable column on the anchor would be the field every construction site may leave null, which this store spells structurally instead. Three absences collapse deliberately, because every reader’s safe answer is the same "not a peer": a graph whose author declared nothing, a programmatic run that was never asked, and a graph whose anchor a diagnostics preamble minted before capture ran. Deliberately not a supergraph entity relation of its own beside store_graph: no single run would mint or may clear such a row, and StoreRefresh derives the ownership-scoped clear set from the presence of a graph_name column, so the supergraph exists here as a value graphs declare and never as an entity anything owns. Single-valued by the (graph_name) key; if federation practice’s multi-supergraph publication ever has to be admitted, the widening is the key growing to (graph_name, supergraph_name), which costs a store-stamp roll rather than a data migration. This relation and store_graph are the whole of the cross-graph consumer read surface’s enumeration axis; nothing else configuration-shaped joins it, and what a surface reads about a peer stays SDL-derived.
Primary key: (graph_name). Foreign key: (graph_name) references store_graph (graph_name).
graph_name(CHARACTER VARYING, not null)-
the declaring graph’s partition, anchored by store_graph; also the key, which is where the single-valued claim is enforced structurally
supergraph_name(CHARACTER VARYING, not null)-
the declared supergraph’s name, as the <supergraph> parameter spelled it, with an empty element collapsed to absent by the decode rather than stored blank. Paired with graph_name it is the store’s rendering of the addressing federation already uses, which is why <graphName>'s own documentation speaks of the subgraph’s published name. A graph’s peers are the graphs this relation joins to over this column, a self-join between non-null values, so two standalone graphs never group by accident and two supergraphs in one workspace store coexist mutually invisible
touched_at(TIMESTAMP, not null)-
when the reading that wrote this row ran. A run transcribes the whole of its configuration, so the reading finishes by deleting this graph’s rows carrying a different instant: those are the parameters the run no longer had, which an upsert cannot find because there is no incoming row to match. NOT NULL is what makes that total, a row with no instant being one no reading claims and no sweep reaches. A graph that stopped declaring a supergraph writes no row, so the sweep is the whole of how it stops being a peer
store_graph_tenant_column
The database-per-tenant column declaration, from <tenantColumn>. Single-valued, optional and run-owned, so its own graph-keyed relation whose row presence is the fact; a single-tenant build has no row. Deliberately not a column beside store_graph’s base_dir and last_captured, even though it is single-valued and run-owned too: it is generation payload rather than a fact about how the partition groups or where it lives, and beside the anchor it would be the first brick of the key-value bag.
Primary key: (graph_name). Foreign key: (graph_name) references store_graph (graph_name).
graph_name(CHARACTER VARYING, not null)-
the declaring graph’s partition, anchored by store_graph
column_name(CHARACTER VARYING, not null)-
the column name as configured; matched against catalog columns the way column lookups match, Java name first then SQL name, both case-insensitively
touched_at(TIMESTAMP, not null)-
when the reading that wrote this row ran. A run transcribes the whole of its configuration, so the reading finishes by deleting this graph’s rows carrying a different instant: those are the parameters the run no longer had, which an upsert cannot find because there is no incoming row to match. NOT NULL is what makes that total, a row with no instant being one no reading claims and no sweep reaches
store_source
A source the store read, store-global rather than graph-keyed: it can say what a file hashed to, never which graph read it. Every base relation is partitionable by the source that produced it: a refresh deletes exactly the rows one source wrote and re-walks it, so a relation unreachable from a source row is one the store can only ever discard wholesale.
Primary key: (source_name). Check: "SOURCE_KIND" IN('SCHEMA_FILE', 'DIRECTORY', 'JAR', 'JOOQ_SCHEMA'). Check: ("ORIGIN" IN('PROJECT', 'REACTOR', 'SIBLING', 'DECLARED', 'TRANSITIVE'))
OR ("ORIGIN" IS NULL). Check: ("ORIGIN" <> 'PROJECT')
OR ("COORDINATE" IS NULL).
source_name(CHARACTER VARYING, not null)-
the schema file path, the classpath entry path, or the generated package a jOOQ schema lives in, as the reader spelled it
source_kind(CHARACTER VARYING, not null)-
a closed taxonomy: a schema file, a directory root, a jar, or a generated jOOQ schema package. The last names jOOQ deliberately, unlike the sql_ family: a family is named for whose vocabulary its rows are written in and jOOQ owns none of SQL’s, but a source is named for what it is, and a generated package is jOOQ’s artefact
origin(CHARACTER VARYING, nullable)-
how this entry reached the compile classpath, which is the decision the build already took and used to drop: PROJECT for the module’s own build output, REACTOR for another module of the same build this module declares a dependency on, SIBLING for a reactor module’s output this module declares no dependency on, DECLARED for a coordinate the pom names that the build does not produce, TRANSITIVE for one a declared dependency dragged in. REACTOR and DECLARED are one fact apart and the distinction is the point of both: declaring a dependency is what makes naming it legitimate, which the two share, while being the build’s own code is what a reader scoping to the consumer wants, which only the first has. Collapsing them, as this column once did, leaves such a reader unable to tell a module of its own build from a third-party jar. Nullable, and on two populations rather than one: a schema file reaches the run by configuration and is on no classpath, and a classpath row the retiring walk wrote through ClasspathSources.upsert carries none either, that seam holding a path and a kind and never the decision. So absence is not-recorded and never not-classified, and a reader scoping to the reactor must not read a null as an answer. The column a reader scopes by when it wants the reactor rather than the world, and the axis a ranking over classpath contents sorts on, a consumer’s own exception outranking a transport library’s; recorded rather than re-derived because nothing downstream of the plugin can recover it from a path
coordinate(CHARACTER VARYING, nullable)-
the entry’s groupId:artifactId, or the module name for a reactor sibling found by convention; NULL for the project’s own output, which is what the origin beside it already says, and NULL on a schema file. Here for the same reason the refusals carry it: "declare a dependency on module X" needs a name no target/classes path supplies
stamp(CHARACTER VARYING, nullable)-
the source’s content identity, spelled <scheme>:<hex> by SourceStamp, so an unchanged source is read once and a currency check can re-hash a cold graph’s files without building its module. Two schemes: sha256: is a hash of bytes this store read, and sha1: is an identity the resolver already established for an artifact it downloaded, supplied by whoever resolved the classpath and never recomputed here, which is what lets a jar whose coordinate cannot be republished under new bytes go unhashed. The tag is load-bearing rather than decorative, comparison being string equality: two schemes never compare equal, so a source whose scheme changed reads as changed and is re-walked once. Schema files are stamped at capture time (one file re-read per schema file, priced against exactly that reader); NULL where nothing resolves to a regular file to hash: a directory root changes on every compile, the bundled directives.graphqls is a resource name, a programmatic caller may hand a bare name, and a jOOQ schema is a package spread across the classpath whose walk is cheap enough not to need one. Also NULL while the source’s rows are being written, and set only once they are all in, so a run that dies mid-load leaves a partition that is re-walked rather than one that claims to be complete
mtime(TIMESTAMP, nullable)-
the filesystem’s last-modified time for this source, read when the source was read. Not a currency signal and deliberately not one: stamp answers whether the bytes changed, and this column exists because a modification time answers a question a hash cannot, which of two sources was written later. That is what orders candidates when several documents declare one coordinate, so a report can say which declaration is the incumbent and which is the incursion rather than naming whichever the reader happened to walk first. Null where the source is not a regular file, a generated jOOQ package and a programmatic label having no modification time to read.
last_seen(TIMESTAMP, not null)-
when a run last named this source in its input set; the age half of the age/currency distinction, and the bookkeeping a future eviction surface reads
read_at(TIMESTAMP, nullable)-
when the content behind this row’s stamp began being read; the currency half of the distinction last_seen names the age half of, and never the same question. last_seen moves when a run names the source in its input set, whether or not it opened it; this moves only where a pass read the bytes, and it moves for every source a pass verified rather than only the ones it rewrote, a source hashed and found equal to its stamp having been read as surely as one that was rewritten. Taken before the read rather than at the write, which is the whole of the concurrency argument: a change landing between the read and the commit is later than this value and is correctly read as a change, where a value taken at the write would swallow that window in silence. NULL reads as never, the conservative answer for a row that exists before any pass read its content. What makes the column mean anything is a comparison against a reading process’s own two instants: when it began watching this source’s corpus, and when it last saw this source move. A value below either says the row is not known to be current, so a value written while nothing was watching buys nothing, and one written by another process while we were watching counts as ours
store_stamp
What this store was built from. At most one row, stated structurally. A persisted store is never state of record: this row decides whether an existing file is intelligible at all. The same stamp names the store’s directory segment, so a mismatching file is normally never even opened; meeting one here means it was moved or damaged by hand, and the store falls back to an in-memory one and leaves the file alone.
Primary key: (singleton). Check: "SINGLETON" = 'X'.
singleton(CHARACTER, not null)-
always 'X'; the CHECK plus the primary key is how a relation says "at most one row" in SQL
ddl_hash(CHARACTER VARYING, not null)-
hash of the DDL resource the store was created from, so any schema edit at all invalidates a persisted file
generator_version(CHARACTER VARYING, not null)-
the capturing generator’s implementation version, or a placeholder where the manifest declares none. It invalidates a persisted file across releases; within one, a capture change that alters row content without touching the DDL is not caught here, and deleting the store’s cache directory is the remedy