<?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>Ci on MongrelDB</title><link>https://www.mongreldb.com/articles/tags/ci/</link><description>Recent content in Ci 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>Sun, 25 Oct 2026 09:00:00 -0500</lastBuildDate><atom:link href="https://www.mongreldb.com/articles/tags/ci/index.xml" rel="self" type="application/rss+xml"/><item><title>Schema Validation in CI, Drift Checks at Deploy</title><link>https://www.mongreldb.com/articles/2026/10/schema-validation-in-ci-drift-checks-at-deploy/</link><pubDate>Sun, 25 Oct 2026 09:00:00 -0500</pubDate><guid>https://www.mongreldb.com/articles/2026/10/schema-validation-in-ci-drift-checks-at-deploy/</guid><description>MongrelDB Kit splits schema safety into two commands: schema validate answers &amp;#39;is this file correct&amp;#39; in CI without a database, and diff answers &amp;#39;does the live database match&amp;#39; at deploy, with generate migration to close the gap.</description><content:encoded><![CDATA[<p>There are two questions hiding inside every schema change, and most migration tooling pretends they are one: &ldquo;is this migration file correct,&rdquo; which is a question about text, and &ldquo;does the live database actually match what the text says,&rdquo; which is a question about reality, and the reason the second question exists at all is that someone, somewhere, always opens a console against production and fixes a column by hand at 11pm, and six weeks later your deploy pipeline is applying migrations to a database that is not the database your migrations were written against. Back in the MySQL days we answered the second question with a cron job that ran <code>mysqldump --no-data</code> on both sides and diffed the output in a loop, which worked about as well as it sounds, because the dump order was never stable and half the diffs were noise about <code>AUTO_INCREMENT</code> counters. The modern equivalent in MongrelDB Kit is two subcommands that know which question they answer, and the discipline is keeping them apart: <code>schema validate</code> runs in CI and never touches a database, <code>diff</code> runs at deploy and never touches your CI, and each one is fast and boring in exactly the way a gate should be.</p>
<h2 id="the-ci-half-schema-validate">The CI half: <code>schema validate</code></h2>
<p>The whole point of the CI gate is that it needs no database, no credentials, and no network, because a check that needs infrastructure is a check people will disable the first time the staging box is down. You point it at the schema JSON file that lives in your repo, and it answers the narrow question of whether the file is internally consistent:</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-bash" data-lang="bash"><span style="display:flex;"><span>mongreldb-kit schema validate schema/schema.json
</span></span><span style="display:flex;"><span><span style="color:#75715e"># OK: schema/schema.json</span>
</span></span></code></pre></div><p>The schema file itself is plain JSON, one entry per table, and every table and every column carries a stable numeric <code>id</code> alongside its name, which is the part that makes everything else in this post possible:</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-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;tables&#34;</span>: [
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;id&#34;</span>: <span style="color:#ae81ff">1</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;users&#34;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&#34;columns&#34;</span>: [
</span></span><span style="display:flex;"><span>        { <span style="color:#f92672">&#34;id&#34;</span>: <span style="color:#ae81ff">1</span>, <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;id&#34;</span>, <span style="color:#f92672">&#34;storage_type&#34;</span>: <span style="color:#e6db74">&#34;int64&#34;</span>, <span style="color:#f92672">&#34;nullable&#34;</span>: <span style="color:#66d9ef">false</span>, <span style="color:#f92672">&#34;primary_key&#34;</span>: <span style="color:#66d9ef">true</span> },
</span></span><span style="display:flex;"><span>        { <span style="color:#f92672">&#34;id&#34;</span>: <span style="color:#ae81ff">2</span>, <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;email&#34;</span>, <span style="color:#f92672">&#34;storage_type&#34;</span>: <span style="color:#e6db74">&#34;text&#34;</span>, <span style="color:#f92672">&#34;nullable&#34;</span>: <span style="color:#66d9ef">false</span> },
</span></span><span style="display:flex;"><span>        { <span style="color:#f92672">&#34;id&#34;</span>: <span style="color:#ae81ff">3</span>, <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;age&#34;</span>, <span style="color:#f92672">&#34;storage_type&#34;</span>: <span style="color:#e6db74">&#34;int64&#34;</span>, <span style="color:#f92672">&#34;nullable&#34;</span>: <span style="color:#66d9ef">true</span>, <span style="color:#f92672">&#34;min&#34;</span>: <span style="color:#ae81ff">0</span>, <span style="color:#f92672">&#34;max&#34;</span>: <span style="color:#ae81ff">150</span> }
</span></span><span style="display:flex;"><span>      ]
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  ]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Validation runs in two passes, and the order is deliberate. The first pass checks the stable IDs, because those IDs are how the engine tells a rename apart from a drop-and-add, and if two tables share an ID or two columns in one table share an ID, every downstream tool will misread the diff, so the validator bails immediately and names the offending ID in the error, something like <code>table &quot;users&quot;: duplicate/reused column id 3 used by &quot;age&quot; and &quot;score&quot;</code>, which is the kind of message you can act on at 6pm on a Friday without opening a debugger. The second pass is the full structural validation, primary keys and index references and foreign key targets, the checks that say the schema describes a database the engine could actually build. Because the whole thing is parse-and-check with no I/O beyond reading one file, it runs in milliseconds and belongs in the same CI step as <code>cargo fmt --check</code> or <code>tsc --noEmit</code>, and the exit code does the gating, so a broken schema fails the PR before anyone wastes review time on it.</p>
<h2 id="the-deploy-half-diff">The deploy half: <code>diff</code></h2>
<p>CI proves the file is correct, and then reality gets a vote, because the database you are about to deploy against has been alive for months and has its own history. The deploy-time question is whether the stored catalog, the schema the engine actually persists inside the database, matches the schema file you are about to start serving from, and that is what <code>diff</code> answers:</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-bash" data-lang="bash"><span style="display:flex;"><span>mongreldb-kit diff schema/schema.json /var/lib/myapp/store.kitdb
</span></span></code></pre></div><p>The output is a line per difference, prefixed by the direction of the change, so <code>+ table events</code> means the code schema has a table the database does not, <code>- column users.age</code> means the database has a column the code dropped, and <code>~ column users.age type: Int64 -&gt; Text</code> means both sides have the column and disagree about what it holds (the diff prints the engine&rsquo;s enum names in <code>CamelCase</code> while the schema file spells them lowercase, same types, two conventions), with the same treatment for unique constraints, foreign keys, and secondary indexes, matched by name so you see which specific constraint drifted. When everything agrees you get exactly one line, <code>no drift</code>, which is the output you want your deploy script to see before it flips traffic. One honest caveat about the wiring: <code>diff</code> exits zero even when it finds drift, because its author treated it as a reporting tool rather than a gate, so your deploy step should check the output for the <code>no drift</code> string or fail on any <code>+</code>/<code>-</code>/<code>~</code> line rather than trusting the exit code, and yes, an <code>--exit-code</code> flag would be a reasonable feature request, but the grep works today and it has the side benefit of printing the drift into your deploy log, which is where you will want it when the gate fires.</p>
<p>This is also where the stable IDs earn their keep a second time, because the diff can detect column-ID reuse between the stored catalog and the code schema, which is the failure mode where someone &ldquo;renames&rdquo; a column by deleting it and adding a new one with a recycled ID, and in a system without stable IDs that mistake is invisible until a query returns the wrong data, while here it shows up as a flagged diff before the deploy goes out.</p>
<h2 id="closing-the-loop-generate-migration">Closing the loop: <code>generate migration</code></h2>
<p>Finding drift is only useful if there is a cheap path back to agreement, and the third subcommand is that path, because it reads the same two sides and emits the migration skeleton that would bring the database up to the code schema:</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-bash" data-lang="bash"><span style="display:flex;"><span>mongreldb-kit generate migration schema/schema.json --from /var/lib/myapp/store.kitdb
</span></span></code></pre></div><p>It opens the database, reads the stored catalog and the applied-migration log, computes the next version number as one past the highest applied version, and prints a migration JSON array with the additive operations the delta implies, <code>create_table</code> for tables the database lacks and <code>add_column</code> for columns it lacks, ready for you to review, rename from its default <code>&quot;generated&quot;</code> name, and commit. The skeleton is deliberately conservative, since generating drops automatically is how tools eat data, so destructive changes stay a human decision and the generator only writes the parts where guessing is safe. From there the ordinary migration commands take over, <code>migrate plan</code> or its alias <code>migrate dry-run</code> to see what would apply, <code>migrate status</code> to see what already has, and <code>migrate apply</code> to run the pending set, which is the same loop we covered when we wrote about content-addressed migration checksums, since the checksum mechanism is what keeps a reviewed migration from being quietly rewritten between CI and deploy.</p>
<h2 id="when-you-need-both-halves-and-when-you-do-not">When you need both halves, and when you do not</h2>
<p>If you are one developer with an embedded database file that ships inside the application, the full two-gate setup is more ceremony than the problem warrants, and running migrations at startup with <code>migrate apply</code> is fine, because the database and the code travel together and drift has nowhere to hide. The split pays for itself the moment there is a second environment, or a second person, or a production database that outlives any single deploy, because that is when &ldquo;the migration is correct&rdquo; and &ldquo;the database matches the migration&rdquo; become different statements with different evidence, and the cheap time to learn they have diverged is in a CI log or a deploy gate, not in a support ticket. Twenty years of watching teams re-learn this says the tooling is never the expensive part; the expensive part is the 11pm console session nobody wrote down, and a <code>diff</code> that prints the truth in four lines is the cheapest insurance against it we know how to build.</p>
]]></content:encoded></item></channel></rss>