Every codebase I have inherited had two files that were always wrong, and they were always the same two files: the seed data script, last touched by someone who left the company, and the hand-written type definitions that describe the database rows, which matched the schema for about a week after they were written and then drifted for years. Both are copies of information that already lives somewhere authoritative, the fixture is a copy of “what rows should exist for a test to be meaningful” and the type file is a copy of the schema, and copies rot, because nothing in the build fails when they fall behind, so the failure shows up later as a test that passes against rows your production table would reject, or a TypeScript interface that happily lets you write a column that no longer exists. When we built the Kit CLI we treated these two as generated artifacts rather than source code, which is the whole trick, because a generated artifact can be regenerated and diffed in review, while a hand-maintained copy can only be remembered, and nobody remembers.
Fixtures are rows, not backups
The fixture commands move table rows in and out of a database as plain JSON, and the shape is exactly what you would write by hand, an object keyed by table name where each value is an array of row objects:
mongreldb-kit fixture create ./store.kitdb customers products > seed.json
{
"customers": [ { "id": 1, "email": "ada@example.com", "name": "Ada", "tier": "free" } ],
"products": [ { "id": 1, "sku": "SKU-1", "name": "Widget", "price_cents": 500 } ]
}
Loading runs the inserts inside a single transaction, which matters more than it sounds, because a fixture that half-applies is worse than one that fails, since a half-applied fixture leaves your test database in a state no test authored and the next failure points at the wrong suspect:
mongreldb-kit fixture load ./store.kitdb ./seed.json
# loaded ./seed.json
Both commands check the named tables against the database’s stored schema before touching any rows, so pointing a fixture at a table that does not exist exits non-zero with table <name> not found in schema rather than silently creating something, and this is why you point them at an application-managed database rather than a bare init’d one, because the schema catalog is what makes the fixture honest. The tradeoff to know about is what fixtures are not: they are a seeding and testing tool, pretty-printed JSON on stdout, not a backup format, and they make no attempt to preserve internal state like sequences or migration history, so if you want disaster recovery you want the engine’s own tooling, and if you want “the same twelve rows in every developer’s test database” you want this.
Two encoding details are worth knowing before you commit a fixture to the repo. Int64 and timestamp_nanos values come out as plain JSON numbers, which is lossless on the round trip because both commands run through the same Rust CLI and serde_json holds an i64 exactly, but a JavaScript tool that parses the file will round anything past 2^53, so hand-edit fixtures in something that respects big integers. Bytes columns have no native JSON type, so the CLI emits them as arrays of byte values, decimals come out as strings, and UUIDs as hex, which keeps every column representable even when it is not pretty. The sequence caveat cuts the other way from what you might expect: the CLI drives the Rust kit’s insert path, where named sequence allocation is independent of manually supplied ids, so loading rows with explicit id values does not advance the sequence, and if the table assigns ids by sequence you should pre-advance it (or reserve a disjoint range for fixtures) after the load, otherwise the next application insert can collide with a row the fixture brought in.
Types fall out of the schema
The second half of the pair is generate types, which reads the same schema JSON the kit already validates and emits language-native definitions on stdout:
mongreldb-kit generate types ./store-schema.json --lang ts > src/db-types.ts
mongreldb-kit generate types ./store-schema.json --lang rust > src/db_types.rs
mongreldb-kit generate types ./store-schema.json --lang python > app/db_types.py
For every table you get three shapes rather than one, and the split is the part that encodes actual schema knowledge: Row has every column with nullable ones widened (| null, Option<…>, Optional[…]), Insert drops the columns you must not supply because they carry a default or are generated, and makes nullable columns optional, and Update makes everything optional for partial patches, so the type system itself stops you from writing a sequence primary key into an insert, which is a bug class that hand-written interfaces almost never exclude because the person writing them was thinking about reads. The mapping is deliberately boring: int64 and timestamp_nanos become bigint in TypeScript, i64 in Rust, int in Python, json becomes unknown or serde_json::Value or Any, bytes becomes Uint8Array or Vec<u8> or bytes, and table names go PascalCase, so order_items comes out as OrderItemsRow:
// Generated by mongreldb-kit. Do not edit.
export interface CustomersRow {
id: bigint;
email: string;
name: string;
tier: string;
created_at: bigint;
}
export interface CustomersInsert {
email: string;
name: string;
}
export interface CustomersUpdate {
id?: bigint;
email?: string;
name?: string;
tier?: string;
created_at?: bigint;
}
The header line is the discipline: generated, do not edit, commit the regenerated file. That sounds like ceremony until the first schema change lands in review with the type diff sitting right next to it, and the reviewer can see that renaming tier to plan touched exactly three interfaces and zero hand-maintained copies, which is the moment the feature pays for itself.
The formats we test with
The reason these two commands exist in the same CLI rather than as separate toys is that we eat the output ourselves: the cross-language conformance suite keeps its fixtures as language-neutral JSON files, schema.json, inserts.json, updates.json, deletes.json, queries.json, and the TypeScript, Rust, and Python runners each load the same files and assert the same results, so the fixture format is not a convenience we built for users, it is the format we already trusted to keep three SDKs honest, and the type generator is what keeps the runners’ row shapes from drifting against it. That is the older lesson wearing new clothes: in 2008 we kept fixtures as SQL dumps and type definitions as a wiki page, and both lied, just slowly; the modern equivalent is not a fancier tool, it is removing the human from the copy path entirely, generating the artifact from the source of truth on every change, and letting the diff tell the reviewer what actually moved.