<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tushar Aggarwal</title>
    <description>The latest articles on DEV Community by Tushar Aggarwal (@aggtushar123).</description>
    <link>https://dev.to/aggtushar123</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4120376%2Fb1c85c65-2865-4e47-b218-b8c87208c12c.jpg</url>
      <title>DEV Community: Tushar Aggarwal</title>
      <link>https://dev.to/aggtushar123</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aggtushar123"/>
    <language>en</language>
    <item>
      <title>The bug where every check passed and the data was still wrong</title>
      <dc:creator>Tushar Aggarwal</dc:creator>
      <pubDate>Fri, 11 Sep 2026 07:14:30 +0000</pubDate>
      <link>https://dev.to/aggtushar123/the-bug-where-every-check-passed-and-the-data-was-still-wrong-5fo2</link>
      <guid>https://dev.to/aggtushar123/the-bug-where-every-check-passed-and-the-data-was-still-wrong-5fo2</guid>
      <description>&lt;p&gt;I'm building [&lt;a href="https://github.com/aggtushar123/mongopg-migrate" rel="noopener noreferrer"&gt;mongopg-migrate&lt;/a&gt;], a tool that migrates MongoDB collections onto an &lt;em&gt;existing&lt;/em&gt; Postgres schema you already designed. It's alpha &lt;code&gt;pip install mongopg-migrate&lt;/code&gt;, currently v0.2.0. This is a bug from it, the worst one I've hit so far, because every safeguard the tool has fired correctly, and the migration still came out wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The tool supports nested arrays (&lt;code&gt;explode:&lt;/code&gt;): a Mongo array becomes a child table, and an array inside that array becomes a grandchild table. A field at any level can also be a &lt;code&gt;lookup:&lt;/code&gt;, resolved against another entity's already-migrated rows and rewritten as a foreign key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;explode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;facilities&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;explode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;categoryParts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;categoryId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;lookup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;zcategories&lt;/span&gt;   &lt;span class="c1"&gt;# one level deeper than the top&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things need to know about that lookup: &lt;code&gt;entity_dependencies()&lt;/code&gt; (so &lt;code&gt;zcategories&lt;/code&gt; loads &lt;em&gt;before&lt;/em&gt; whatever references it) and &lt;code&gt;validate_structure()&lt;/code&gt; (so a typo'd entity name gets caught early). Both only ever walked the first level of &lt;code&gt;explode&lt;/code&gt;. A lookup one level deeper was invisible to both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lookup at TOP explode level     -&amp;gt; order ['zcategories', 'hospitals']   correct
same lookup ONE LEVEL DEEPER    -&amp;gt; order ['hospitals', 'zcategories']   backwards
validate on typo'd nested lookup -&amp;gt; []   zero issues found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On its own, that's a real bug. But it used to be a &lt;em&gt;loud&lt;/em&gt; one, a lookup with an empty id_map raised an unconditional &lt;code&gt;LoadError&lt;/code&gt; and crashed the run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The twist
&lt;/h2&gt;

&lt;p&gt;Separately, the tool had just grown an &lt;code&gt;on_missing&lt;/code&gt; policy: what to do when a reference genuinely doesn't resolve (source doc deleted, a normal case). &lt;code&gt;on_missing: null&lt;/code&gt; writes NULL instead of crashing the whole migration. Reasonable on its own.&lt;/p&gt;

&lt;p&gt;Combine both:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Wrong order schedules the referencing entity &lt;strong&gt;before&lt;/strong&gt; &lt;code&gt;zcategories&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Every lookup misses, because &lt;code&gt;zcategories&lt;/code&gt;'s id_map is empty, not because anything is actually dangling.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;on_missing: null&lt;/code&gt;, built for real dangling references, fires on every miss and writes NULL.&lt;/li&gt;
&lt;li&gt;Row counts are unaffected - NULLs don't change counts.&lt;/li&gt;
&lt;li&gt;The post-migration validator re-checks dangling references &lt;em&gt;after&lt;/em&gt; the full run, by which point &lt;code&gt;zcategories&lt;/code&gt; has loaded, so it finds nothing wrong and reports clean.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;End state: a whole foreign-key column silently NULL, and count diff, dangling-reference check, and structural validation all report success. Each check answered the exact question it was built to answer, correctly. The chain connecting them was wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  the fix: two parts, not one
&lt;/h2&gt;

&lt;p&gt;A regression test for this exact repro would fix the one shape found and miss the actual assumption: a policy for "this one reference is dangling" isn't a safe answer to "the entity it points at has never loaded a single row." Those are different failure modes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause&lt;/strong&gt; recurse through every level of nested &lt;code&gt;explode&lt;/code&gt;, not just the first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_explode_lookup_targets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;explode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ExplodeSpec&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;exp&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;explode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;fspec&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fspec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fspec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;targets&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="nf"&gt;_explode_lookup_targets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;explode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# recurse
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;targets&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Defense in depth&lt;/strong&gt; even with correct ordering, refuse to apply &lt;code&gt;on_missing&lt;/code&gt; blind. Check whether the referenced entity has loaded &lt;em&gt;anything at all&lt;/em&gt; first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;on_missing&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;OnMissing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;idmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has_any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lookup_conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lookup_entity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;lookup_schema&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;LoadError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;lookup_entity&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt; has NO id_map rows at all — this looks like a load-order &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bug or a forgotten prerequisite run, not a genuinely dangling reference. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Refusing to apply on_missing=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;on_missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt; here.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second check matters independently; it also catches referencing an entity from a &lt;em&gt;separate&lt;/em&gt; migration run that a human simply never ran. No amount of correct ordering fixes that; checking "has this entity loaded anything, ever" does. It's cached per entity, so it's one extra indexed query the first time a lookup to that entity misses, not one per row.&lt;/p&gt;

&lt;h2&gt;
  
  
  the same shape, twice more
&lt;/h2&gt;

&lt;p&gt;I first wrote this up as a one-off. Two bugs since have changed my mind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two checks aimed at one mistake are one check.&lt;/strong&gt; &lt;code&gt;--pg-schema&lt;/code&gt; exists so you can migrate into a schema other than &lt;code&gt;public&lt;/code&gt;. It reached &lt;code&gt;introspect_postgres()&lt;/code&gt; and nothing else. Every write named its table with no schema qualifier, so rows landed wherever the connecting role's &lt;code&gt;search_path&lt;/code&gt; pointed, normally &lt;code&gt;public&lt;/code&gt;. Nothing errored; the load was perfectly valid against the tables it found. Then &lt;code&gt;validate&lt;/code&gt; counted those &lt;em&gt;same wrong tables&lt;/em&gt; and printed a clean pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[OK] hospitals (hospitals): mongo=4002 postgres=4002
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The migration reported success. The validation agreed. Both were pointed at the wrong tables. It had been there since the flag was added, and it could only ever have hurt someone whose tables don't live in &lt;code&gt;public&lt;/code&gt;, which is to say, someone who wasn't me. All three commands now set &lt;code&gt;search_path&lt;/code&gt; explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A precondition that only held at fixture scale.&lt;/strong&gt; To make &lt;code&gt;lookup:&lt;/code&gt; fast over a slow link, the tool prefetches a referenced entity's whole id_map into memory before the first document. That was unbounded, at ~200 bytes a row, ~1.2 GB at 5M rows and ~12 GB at 50M, allocated up front. It never came close in development, because the largest entity there was ~65k rows. There's now a ceiling (&lt;code&gt;--idmap-prefetch-max&lt;/code&gt;, default 2,000,000); above it, lookups go per row behind a bounded cache, slower, but it can't exhaust memory, and the run says which mode it picked.&lt;/p&gt;

&lt;p&gt;Same pattern all three times: logic that's correct &lt;em&gt;given&lt;/em&gt; an assumption nobody wrote down, composed with other correct-in-isolation logic, producing something wrong that looks clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson
&lt;/h2&gt;

&lt;p&gt;"Every check passed" is a claim about which checks exist, not a claim about correctness. And two checks that share an assumption are, for catching a violation of that assumption, one check.&lt;/p&gt;

&lt;p&gt;Worth asking of any &lt;code&gt;null&lt;/code&gt;/&lt;code&gt;skip&lt;/code&gt;/&lt;code&gt;retry&lt;/code&gt; fallback in a system: what does this do if the precondition I'm assuming turns out to be silently false?&lt;/p&gt;




&lt;p&gt;Repo: &lt;a href="https://github.com/aggtushar123/mongopg-migrate" rel="noopener noreferrer"&gt;https://github.com/aggtushar123/mongopg-migrate&lt;/a&gt; - alpha, building in the open.&lt;br&gt;
&lt;code&gt;pip install mongopg-migrate&lt;/code&gt;.&lt;/p&gt;




</description>
      <category>postgres</category>
      <category>mongodb</category>
      <category>python</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
