<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Tooling on MongrelDB</title><link>https://www.mongreldb.com/articles/tags/tooling/</link><description>Recent content in Tooling on MongrelDB</description><image><title>MongrelDB</title><url>https://www.mongreldb.com/assets/og-mongreldb.png</url><link>https://www.mongreldb.com/assets/og-mongreldb.png</link></image><generator>Hugo</generator><language>en-US</language><lastBuildDate>Wed, 28 Oct 2026 09:00:00 -0500</lastBuildDate><atom:link href="https://www.mongreldb.com/articles/tags/tooling/index.xml" rel="self" type="application/rss+xml"/><item><title>Fixture Import and Export, Plus Type Generation</title><link>https://www.mongreldb.com/articles/2026/10/fixture-import-export-and-type-generation/</link><pubDate>Wed, 28 Oct 2026 09:00:00 -0500</pubDate><guid>https://www.mongreldb.com/articles/2026/10/fixture-import-export-and-type-generation/</guid><description>MongrelDB Kit ships fixture create/load for moving rows in and out of a database as JSON, and generate types for emitting TypeScript, Rust, and Python row definitions from the schema, so the two artifacts that rot fastest stop being hand-maintained copies.</description><content:encoded><![CDATA[<p>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 &ldquo;what rows should exist for a test to be meaningful&rdquo; 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.</p>
<h2 id="fixtures-are-rows-not-backups">Fixtures are rows, not backups</h2>
<p>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:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>mongreldb-kit fixture create ./store.kitdb customers products &gt; seed.json
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;customers&#34;</span>: [ { <span style="color:#f92672">&#34;id&#34;</span>: <span style="color:#ae81ff">1</span>, <span style="color:#f92672">&#34;email&#34;</span>: <span style="color:#e6db74">&#34;ada@example.com&#34;</span>, <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Ada&#34;</span>, <span style="color:#f92672">&#34;tier&#34;</span>: <span style="color:#e6db74">&#34;free&#34;</span> } ],
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;products&#34;</span>: [ { <span style="color:#f92672">&#34;id&#34;</span>: <span style="color:#ae81ff">1</span>, <span style="color:#f92672">&#34;sku&#34;</span>: <span style="color:#e6db74">&#34;SKU-1&#34;</span>, <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Widget&#34;</span>, <span style="color:#f92672">&#34;price_cents&#34;</span>: <span style="color:#ae81ff">500</span> } ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>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:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>mongreldb-kit fixture load ./store.kitdb ./seed.json
</span></span><span style="display:flex;"><span><span style="color:#75715e"># loaded ./seed.json</span>
</span></span></code></pre></div><p>Both commands check the named tables against the database&rsquo;s stored schema before touching any rows, so pointing a fixture at a table that does not exist exits non-zero with <code>table &lt;name&gt; not found in schema</code> rather than silently creating something, and this is why you point them at an application-managed database rather than a bare <code>init</code>&rsquo;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&rsquo;s own tooling, and if you want &ldquo;the same twelve rows in every developer&rsquo;s test database&rdquo; you want this.</p>
<p>Two encoding details are worth knowing before you commit a fixture to the repo. Int64 and <code>timestamp_nanos</code> values come out as plain JSON numbers, which is lossless on the round trip because both commands run through the same Rust CLI and <code>serde_json</code> 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&rsquo;s insert path, where named sequence allocation is independent of manually supplied ids, so loading rows with explicit <code>id</code> 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.</p>
<h2 id="types-fall-out-of-the-schema">Types fall out of the schema</h2>
<p>The second half of the pair is <code>generate types</code>, which reads the same schema JSON the kit already validates and emits language-native definitions on stdout:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-sh" data-lang="sh"><span style="display:flex;"><span>mongreldb-kit generate types ./store-schema.json --lang ts     &gt; src/db-types.ts
</span></span><span style="display:flex;"><span>mongreldb-kit generate types ./store-schema.json --lang rust   &gt; src/db_types.rs
</span></span><span style="display:flex;"><span>mongreldb-kit generate types ./store-schema.json --lang python &gt; app/db_types.py
</span></span></code></pre></div><p>For every table you get three shapes rather than one, and the split is the part that encodes actual schema knowledge: <code>Row</code> has every column with nullable ones widened (<code>| null</code>, <code>Option&lt;…&gt;</code>, <code>Optional[…]</code>), <code>Insert</code> drops the columns you must not supply because they carry a default or are generated, and makes nullable columns optional, and <code>Update</code> 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: <code>int64</code> and <code>timestamp_nanos</code> become <code>bigint</code> in TypeScript, <code>i64</code> in Rust, <code>int</code> in Python, <code>json</code> becomes <code>unknown</code> or <code>serde_json::Value</code> or <code>Any</code>, <code>bytes</code> becomes <code>Uint8Array</code> or <code>Vec&lt;u8&gt;</code> or <code>bytes</code>, and table names go PascalCase, so <code>order_items</code> comes out as <code>OrderItemsRow</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#75715e">// Generated by mongreldb-kit. Do not edit.
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">CustomersRow</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">bigint</span>;
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">email</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">name</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">tier</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">created_at</span>: <span style="color:#66d9ef">bigint</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">CustomersInsert</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">email</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">name</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">CustomersUpdate</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">id?</span>: <span style="color:#66d9ef">bigint</span>;
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">email?</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">name?</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">tier?</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">created_at?</span>: <span style="color:#66d9ef">bigint</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>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 <code>tier</code> to <code>plan</code> touched exactly three interfaces and zero hand-maintained copies, which is the moment the feature pays for itself.</p>
<h2 id="the-formats-we-test-with">The formats we test with</h2>
<p>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, <code>schema.json</code>, <code>inserts.json</code>, <code>updates.json</code>, <code>deletes.json</code>, <code>queries.json</code>, 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&rsquo; 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.</p>
]]></content:encoded></item></channel></rss>