Published Build3 min read
Four Inputs, One Row: Why Your Extraction Table Needs a Model ID
A dev.to writeup on extraction schema versioning makes a narrow, load-bearing point: if a row does not record the document, schema, prompt and model that produced it, you cannot migrate it and you cannot explain the day...
Written for builders.See today for builders

What happened
- A row in an extractions table is the output of a function of four inputs: the document, the schema, the prompt and the model. Change any one and later rows are not comparable with earlier ones.
- Unlike a database migration, some changes to an extraction schema cannot be applied to existing rows by any transform.
- Recommended extractions table columns: id; document_id (the source, immutably addressed by content hash, not a path); schema_version (integer, monotonic); prompt_version (integer or a git sha); model_id (the exact string sent, including any date suffix); extracted_at; data jsonb conforming to schema_version; raw_response jsonb, what the model actually returned.
- Without schema_version you cannot write a migration, because you cannot tell which rows need it.
- Without model_id you cannot explain the day your accuracy changed while nothing in your repository did.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
A post on dev.to about schema versioning states the case plainly: a row in an extractions table is the output of a function of four inputs, the document, the schema, the prompt and the model, and changing any one of them means later rows are not comparable with earlier ones [1]. That matters because two of the failure modes it produces are silent, and the source argues that unlike a database migration, some changes to an extraction schema cannot be applied to existing rows by any transform at all [2]. The prescription is a table that carries all four identifiers on every record: document_id as an immutable content hash rather than a path, a monotonic integer schema_version, a prompt_version as an integer or a git sha, and model_id as the exact string sent including any date suffix, alongside extracted_at, the parsed data, and the raw response [3]. Without schema_version you cannot write a migration, because you cannot tell which rows need one [4]. Without model_id you cannot explain the day your accuracy changed while nothing in your repository did [5]. That last case is the one worth dwelling on. According to the source, a provider retiring a snapshot behind an alias is a routine event, and it is invisible unless you wrote the resolved id down [5][6]. It is a version change in the producer with no version change in the code, detectable only if model_id records what the request actually resolved to rather than the alias you asked for [7]. Note the accounting problem: of the four inputs, only schema_version and prompt_version live in your repository, so repository state identifies at most half of what produced a row [8]. The recommended response to an alias shift is the same as for a semantic change, treat records either side of the boundary as different populations until you have compared them [9]. Semantic changes are dangerous precisely because nothing breaks: every old row still validates against the new schema while meaning something different from what it says [10]. The source allows two safe handlings, a new field with a new name, or re-extract everything and refuse to mix versions in one query, and warns that halfway measures produce reports that are quietly wrong for the months straddling the change [11]. Structural changes get database-migration treatment, small numbered forward-only functions applied in sequence [12]. The worked example runs CURRENT = 4 with three steps, a rename of cust to customer_name, a currency enum with an "OTHER" default, and a string total becoming a numeric total_including_tax [13], which means a version 1 record takes three successive functions to reach current [14]. A missing step raises KeyError, which is the intended signal for a gap in the chain [15]. Three operating rules follow: migrate lazily on read and persist the result so one bad record does not take down a 3am batch job [16]; never edit a migration that has run in production, add another [17]; and test each step against a frozen fixture of a real record at that version, checked into the repository, because those fixtures are the only surviving evidence of what version 2 looked like [18]. The cheapest decision in the whole design, the source says, costs a few kilobytes per record: keep the model's complete response, unparsed [19]. It converts a class of additive changes from re-running the model over four million documents into running a script, and the example given is extracting total first, wanting currency later, and finding a total_text of "EUR 1.250,00" already in the response so the backfill is a regex [20]. It also lets you re-derive after a bug in your own transform code without paying for inference twice, which the source calls a more common cause of backfills than schema evolution [21]. Keep the reasoning fields, the quotes, and the fields you discarded [22].
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
A row in an extractions table is the output of a function of four inputs: the document, the schema, the prompt and the model. Change any one and later rows are not comparable with earlier ones.
- [2]
Unlike a database migration, some changes to an extraction schema cannot be applied to existing rows by any transform.
- [3]
Recommended extractions table columns: id; document_id (the source, immutably addressed by content hash, not a path); schema_version (integer, monotonic); prompt_version (integer or a git sha); model_id (the exact string sent, including any date suffix); extracted_at; data jsonb conforming to schema_version; raw_response jsonb, what the model actually returned.
- [4]
Without schema_version you cannot write a migration, because you cannot tell which rows need it.
ReportedView cited source - [5]
Without model_id you cannot explain the day your accuracy changed while nothing in your repository did.
ReportedView cited source - [6]
A provider retiring a snapshot behind an alias is a routine event and it is invisible unless the resolved id was written down.
Sources & coverage · 1 publisher
The reporting this story was synthesized from, earliest first. Every link goes to the original.
- dev.toMultigridAug 12Schema Versioning When Your Extraction Changes
Cited in this coverage: dev.to post 'Schema Versioning When Your Extraction Changes'
Cited in this coverage: dev.to post

