<?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>Forem: ulku</title>
    <description>The latest articles on Forem by ulku (@pyroot).</description>
    <link>https://forem.com/pyroot</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3822036%2F21aec982-45f5-4daa-a821-3b46ceddc790.jpg</url>
      <title>Forem: ulku</title>
      <link>https://forem.com/pyroot</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/pyroot"/>
    <language>en</language>
    <item>
      <title>Detecting Schema Drift Before It Breaks Production</title>
      <dc:creator>ulku</dc:creator>
      <pubDate>Thu, 30 Apr 2026 08:39:53 +0000</pubDate>
      <link>https://forem.com/pyroot/detecting-schema-drift-before-it-breaks-production-3mmi</link>
      <guid>https://forem.com/pyroot/detecting-schema-drift-before-it-breaks-production-3mmi</guid>
      <description>&lt;p&gt;At some point it happens to everyone. You run &lt;code&gt;alembic revision --autogenerate&lt;/code&gt;, look at the output — and there it is: a &lt;code&gt;DROP TABLE&lt;/code&gt; for a table that doesn't exist in production. Or the opposite: the migration history looks clean, but the production table greets you with three extra columns nobody can account for.&lt;/p&gt;

&lt;p&gt;Because the application keeps running, the gap goes unnoticed for weeks. Then one day someone asks: "What's the actual difference between our models and the database?" Everyone looks at each other.&lt;/p&gt;

&lt;p&gt;The tool I wrote to answer that question is &lt;strong&gt;SmartMigrate&lt;/strong&gt; — a library that compares a live database against SQLAlchemy models, flags every difference by risk level, and generates runnable SQL.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Idea: Two Snapshots, One Diff
&lt;/h2&gt;

&lt;p&gt;At the center of the library is &lt;code&gt;SchemaSnapshot&lt;/code&gt; — a full picture of a database schema at a given moment: tables, columns, types, indexes, foreign keys, nullable/unique/default metadata. This Pydantic model can be serialized to JSON and version-controlled in a CI/CD pipeline.&lt;/p&gt;

&lt;p&gt;A snapshot can be taken from two sources:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From models (offline):&lt;/strong&gt; SQLAlchemy declarative model classes are imported and &lt;code&gt;Model.__table__&lt;/code&gt; is introspected. No live database connection needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From a live database:&lt;/strong&gt; &lt;code&gt;sqlalchemy.inspect(engine)&lt;/code&gt; reflects the current schema.&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;smartmigrate.snapshot&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;snapshot_from_models&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapshot_from_url&lt;/span&gt;

&lt;span class="n"&gt;snap_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;snapshot_from_models&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app.models&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;snap_db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;snapshot_from_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mysql://root:pass@localhost:3306/mydb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;db&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;Both snapshots serialize to JSON. In environments without database access, a previously saved file can be used — I'll get to that shortly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Differ: What Changed?
&lt;/h2&gt;

&lt;p&gt;Once two snapshots are in hand, &lt;code&gt;diff_snapshots&lt;/code&gt; runs. The pipeline is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tables in the old snapshot but not the new → potential DROP&lt;/li&gt;
&lt;li&gt;Tables in the new snapshot but not the old → potential ADD&lt;/li&gt;
&lt;li&gt;For tables present in both, column and index differences are compared&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The raw output is a list of &lt;code&gt;DiffOp&lt;/code&gt; objects. Supported operation types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;ADD_TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DROP_TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RENAME_TABLE&lt;/span&gt;
&lt;span class="n"&gt;ADD_COLUMN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DROP_COLUMN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RENAME_COLUMN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MODIFY_COLUMN&lt;/span&gt;
&lt;span class="n"&gt;ADD_INDEX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DROP_INDEX&lt;/span&gt;
&lt;span class="n"&gt;ADD_FK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DROP_FK&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;smartmigrate&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;differ&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;smartmigrate.safety&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;annotate&lt;/span&gt;

&lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;differ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;diff_snapshots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snap_db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snap_model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;annotated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;annotate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&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;op&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;annotated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ops&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&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="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;risk&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;:&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&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;:&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&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="s"&gt;         Why: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="si"&gt;}&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;A raw diff is just a list. The real question is what each difference actually means.&lt;/p&gt;




&lt;h2&gt;
  
  
  Rename Intelligence: RENAME, Not DROP+CREATE
&lt;/h2&gt;

&lt;p&gt;Now we get to an interesting problem.&lt;/p&gt;

&lt;p&gt;Alembic's autogenerate has a known limitation: when a column or table is renamed, it can't detect it. It reports the old name as DROP and the new name as CREATE. On large tables, that's an unnecessary data-loss risk and a false alarm.&lt;/p&gt;

&lt;p&gt;"Can we do better?" was the starting point. SmartMigrate's rename detection module solves this with structural similarity scoring.&lt;/p&gt;

&lt;h3&gt;
  
  
  Column Renames
&lt;/h3&gt;

&lt;p&gt;When a column "disappears" and another "appears", a composite score is calculated between the two:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;score = 0.55 × name_sim + 0.30 × type_sim + 0.15 × pos_sim
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;name_sim&lt;/code&gt;: character-level similarity between the two names via &lt;code&gt;SequenceMatcher&lt;/code&gt; (0–1)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;type_sim&lt;/code&gt;: type compatibility — 1.0 for same type, 0.5 for compatible types (INT and BIGINT), 0.0 otherwise&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pos_sim&lt;/code&gt;: similarity of the column's ordinal position within the table&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why isn't name similarity alone enough? Because semantic shifts like &lt;code&gt;price&lt;/code&gt; → &lt;code&gt;cost&lt;/code&gt; exist. Without type and position weights, those pairs are impossible to catch. Greedy best-first assignment ensures each column is used in at most one match.&lt;/p&gt;

&lt;p&gt;Default threshold: &lt;code&gt;0.72&lt;/code&gt;. Adjustable via environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SMARTMIG_COL_RENAME_THRESHOLD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Table Renames
&lt;/h3&gt;

&lt;p&gt;For tables, Jaccard similarity over the column set is combined with name similarity:&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="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="n"&gt;name_similarity&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="n"&gt;column_jaccard&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the threshold is met, the system emits &lt;code&gt;RENAME_TABLE&lt;/code&gt; or &lt;code&gt;RENAME_COLUMN&lt;/code&gt; instead of a DROP+CREATE pair.&lt;/p&gt;




&lt;h2&gt;
  
  
  Safety Engine: Every Change Has a Cost
&lt;/h2&gt;

&lt;p&gt;When looking at a diff list, the real question in your head is: "What happens if I run this step?"&lt;/p&gt;

&lt;p&gt;Adding a column and dropping a column don't carry the same weight. SmartMigrate assigns each &lt;code&gt;DiffOp&lt;/code&gt; to one of three risk levels:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Risk&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Add nullable column&lt;/td&gt;
&lt;td&gt;SAFE&lt;/td&gt;
&lt;td&gt;Existing rows get NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add non-unique index&lt;/td&gt;
&lt;td&gt;SAFE&lt;/td&gt;
&lt;td&gt;Only query performance changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add NOT NULL column (with default)&lt;/td&gt;
&lt;td&gt;SAFE&lt;/td&gt;
&lt;td&gt;Existing rows get a valid value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rename table/column&lt;/td&gt;
&lt;td&gt;CAUTION&lt;/td&gt;
&lt;td&gt;Application code may break&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add unique index&lt;/td&gt;
&lt;td&gt;CAUTION&lt;/td&gt;
&lt;td&gt;Fails if duplicates exist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add foreign key&lt;/td&gt;
&lt;td&gt;CAUTION&lt;/td&gt;
&lt;td&gt;Fails if orphan rows exist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drop table/column&lt;/td&gt;
&lt;td&gt;DANGER&lt;/td&gt;
&lt;td&gt;Permanent data loss&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add NOT NULL column (no default)&lt;/td&gt;
&lt;td&gt;DANGER&lt;/td&gt;
&lt;td&gt;Existing rows violate the constraint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type change to incompatible family&lt;/td&gt;
&lt;td&gt;DANGER&lt;/td&gt;
&lt;td&gt;Risk of data corruption&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nullable → NOT NULL&lt;/td&gt;
&lt;td&gt;DANGER&lt;/td&gt;
&lt;td&gt;Existing NULL rows violate the constraint&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each &lt;code&gt;DiffOp&lt;/code&gt; gets not just a risk level, but also a &lt;code&gt;reason&lt;/code&gt; explaining why the risk exists and a &lt;code&gt;suggestion&lt;/code&gt; for how to mitigate it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DANGER: Adding NOT NULL column 'fee' without a default to 'transaction'.
        Existing rows cannot satisfy the constraint.
Suggestion: Add a server_default so existing rows get a valid value.
            Alternatively, add as nullable first, backfill, then add NOT NULL.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The risk matrix in &lt;code&gt;safety.py&lt;/code&gt; goes beyond a mechanical diff — it contains contextual reasoning. Whether &lt;code&gt;ADD_COLUMN NOT NULL&lt;/code&gt; is SAFE or DANGER depends entirely on whether &lt;code&gt;server_default&lt;/code&gt; is present. Two lines of code, completely different risk profiles.&lt;/p&gt;




&lt;h2&gt;
  
  
  Migration Graph: Not a List, a DAG
&lt;/h2&gt;

&lt;p&gt;Most migration systems treat changes as an ordered list. Is that enough? Usually yes — but not always.&lt;/p&gt;

&lt;p&gt;SmartMigrate thinks differently: it builds a Directed Acyclic Graph (DAG) where each &lt;code&gt;DiffOp&lt;/code&gt; is a node and dependencies are edges. Edges come from three rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Intra-table ordering&lt;/strong&gt;: &lt;code&gt;RENAME_TABLE → ADD_COLUMN → MODIFY_COLUMN → ADD_INDEX → DROP_INDEX → DROP_COLUMN → DROP_TABLE&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Foreign key dependencies&lt;/strong&gt;: An &lt;code&gt;ADD_COLUMN&lt;/code&gt; that references another table must run after that table's &lt;code&gt;ADD_TABLE&lt;/code&gt; step.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Independent tables&lt;/strong&gt;: Steps belonging to unrelated tables can run in any order.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The DAG is validated for cycles via &lt;code&gt;networkx&lt;/code&gt;. A topological sort produces a guaranteed execution order. The graph can also be visualized:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Print the migration DAG as ASCII&lt;/span&gt;
smartmigrate graph snapshots/prod.json snapshots/dev.json

&lt;span class="c"&gt;# Export as Graphviz DOT&lt;/span&gt;
smartmigrate graph snapshots/prod.json snapshots/dev.json &lt;span class="nt"&gt;--format&lt;/span&gt; dot &lt;span class="nt"&gt;--out&lt;/span&gt; migration.dot
dot &lt;span class="nt"&gt;-Tpng&lt;/span&gt; migration.dot &lt;span class="nt"&gt;-o&lt;/span&gt; migration.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Practical Use: Autogenerate in One Command
&lt;/h2&gt;

&lt;p&gt;In day-to-day use, most of the time there's one question: "What's the difference between my models and my database, and what SQL closes it?"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;smartmigrate autogenerate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--package&lt;/span&gt; app.models &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--db-url&lt;/span&gt; &lt;span class="s2"&gt;"mysql://root:pass@localhost:3306/mydb"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--message&lt;/span&gt; &lt;span class="s2"&gt;"add_fee_columns"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--out&lt;/span&gt; migrations/smartmigrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command runs in four steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Snapshot the database (via &lt;code&gt;sqlalchemy.inspect&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Snapshot the model package (offline, using stubs)&lt;/li&gt;
&lt;li&gt;Compute the DB → Model diff and run safety analysis&lt;/li&gt;
&lt;li&gt;Build the MigrationPlan and write four files
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;migrations/smartmigrate/
  20260429_130500_add_fee_columns.up.sql
  20260429_130500_add_fee_columns.down.sql
  20260429_130500_add_fee_columns.changelog.md
  20260429_130500_add_fee_columns.plan.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside &lt;code&gt;.up.sql&lt;/code&gt;, each step is annotated with its risk level and description:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Step: 3a7f9c12ab4e&lt;/span&gt;
&lt;span class="c1"&gt;--   Adding nullable column 'fee' (DECIMAL(18,2)) to 'transaction'.&lt;/span&gt;
&lt;span class="c1"&gt;--   Risk: SAFE&lt;/span&gt;
&lt;span class="c1"&gt;-- --------------------------------------------------&lt;/span&gt;

&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;`transaction`&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="nv"&gt;`fee`&lt;/span&gt; &lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A plan containing DANGER steps is blocked by default. Use dry-run to inspect the plan before deciding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Show the plan without writing any files&lt;/span&gt;
smartmigrate autogenerate &lt;span class="nt"&gt;--package&lt;/span&gt; app.models &lt;span class="nt"&gt;--db-url&lt;/span&gt; &lt;span class="s2"&gt;"..."&lt;/span&gt; &lt;span class="nt"&gt;--dry-run&lt;/span&gt;

&lt;span class="c"&gt;# Proceed through DANGER steps explicitly&lt;/span&gt;
smartmigrate autogenerate &lt;span class="nt"&gt;--package&lt;/span&gt; app.models &lt;span class="nt"&gt;--db-url&lt;/span&gt; &lt;span class="s2"&gt;"..."&lt;/span&gt; &lt;span class="nt"&gt;--allow-danger&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Offline Mode and CI/CD
&lt;/h2&gt;

&lt;p&gt;Connecting directly to a production database from CI/CD is usually not possible. Or if it is, you probably don't want to.&lt;/p&gt;

&lt;p&gt;SmartMigrate offers two approaches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snapshot file:&lt;/strong&gt; Taken once when the connection is available, reused in CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Capture a snapshot from the live DB&lt;/span&gt;
smartmigrate snapshot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--db-url&lt;/span&gt; &lt;span class="s2"&gt;"mysql://..."&lt;/span&gt; &lt;span class="nt"&gt;--version&lt;/span&gt; prod-2026-04-29 &lt;span class="nt"&gt;--out&lt;/span&gt; snapshots/prod.json

&lt;span class="c"&gt;# In CI: autogenerate from the saved snapshot&lt;/span&gt;
smartmigrate autogenerate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--package&lt;/span&gt; app.models &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--from-db-snapshot&lt;/span&gt; snapshots/prod.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--message&lt;/span&gt; &lt;span class="s2"&gt;"add_fee_columns"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Model snapshots are fully offline:&lt;/strong&gt; When &lt;code&gt;snapshot_from_models&lt;/code&gt; runs, application startup code is never triggered. Native drivers like &lt;code&gt;MySQLdb&lt;/code&gt; and &lt;code&gt;psycopg2&lt;/code&gt; are stubbed with &lt;code&gt;MagicMock&lt;/code&gt;; &lt;code&gt;app.db.database.Base&lt;/code&gt; is replaced with a real &lt;code&gt;declarative_base()&lt;/code&gt;. For large applications that spin up Redis, Celery, or OpenTelemetry on boot, this matters — you can analyze the model schema without starting the application.&lt;/p&gt;




&lt;h2&gt;
  
  
  Python API
&lt;/h2&gt;

&lt;p&gt;The API behind the CLI can be used directly. Every step above is callable individually:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;smartmigrate.snapshot&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;snapshot_from_models&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snapshot_from_url&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;smartmigrate.planner&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;plan_from_snapshots&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;render_sql_file&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;smartmigrate.explainer&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;explain_plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;generate_changelog&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;smartmigrate.safety&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;safety_report&lt;/span&gt;

&lt;span class="n"&gt;snap_db&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;snapshot_from_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mysql://root:pass@localhost/mydb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;snap_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;snapshot_from_models&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app.models&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;plan_from_snapshots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;snap_db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;snap_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dialect&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mysql&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;safety_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&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="s"&gt;Danger ops: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;risk_counts&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;danger&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;up_sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;render_sql_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;up&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;changelog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_changelog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;explain_plan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Revision History
&lt;/h2&gt;

&lt;p&gt;Every time &lt;code&gt;autogenerate&lt;/code&gt; runs, a new entry is appended to &lt;code&gt;smartmigrate_versions.json&lt;/code&gt; — tracking each revision's ID, message, risk summary, and applied status.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;smartmigrate &lt;span class="nb"&gt;history&lt;/span&gt; &lt;span class="nt"&gt;--out&lt;/span&gt; migrations/smartmigrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────┬──────────────────────┬───────────┬──────────┐
│ Revision ID                         │ Message              │ DB→Model  │ Status   │
├─────────────────────────────────────┼──────────────────────┼───────────┼──────────┤
│ 20260429_130500_add_fee_columns      │ add_fee_columns      │ db→model  │ pending  │
│ 20260402_091200_rename_user_id       │ rename_user_id       │ db→model  │ applied  │
└─────────────────────────────────────┴──────────────────────┴───────────┴──────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;The library is ten modules, each with a single responsibility:&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="n"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;        &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="n"&gt;SchemaSnapshot&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;live&lt;/span&gt; &lt;span class="n"&gt;DB&lt;/span&gt;
&lt;span class="n"&gt;differ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;          &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="n"&gt;Two&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;snapshot&lt;/span&gt; &lt;span class="n"&gt;comparison&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="n"&gt;DiffOp&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;
&lt;span class="n"&gt;rename_detector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="n"&gt;Rename&lt;/span&gt; &lt;span class="n"&gt;detection&lt;/span&gt; &lt;span class="n"&gt;via&lt;/span&gt; &lt;span class="n"&gt;structural&lt;/span&gt; &lt;span class="n"&gt;similarity&lt;/span&gt;
&lt;span class="n"&gt;safety&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;          &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="n"&gt;Risk&lt;/span&gt; &lt;span class="n"&gt;classification&lt;/span&gt; &lt;span class="n"&gt;per&lt;/span&gt; &lt;span class="n"&gt;DiffOp&lt;/span&gt;
&lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;           &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="n"&gt;DAG&lt;/span&gt; &lt;span class="n"&gt;construction&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;topological&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt;
&lt;span class="n"&gt;sql_generator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;   &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="n"&gt;SQL&lt;/span&gt; &lt;span class="n"&gt;DDL&lt;/span&gt; &lt;span class="n"&gt;generation&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;DiffOps&lt;/span&gt;
&lt;span class="n"&gt;planner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;         &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="n"&gt;Pipeline&lt;/span&gt; &lt;span class="n"&gt;orchestrator&lt;/span&gt;
&lt;span class="n"&gt;explainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;       &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="n"&gt;Human&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;readable&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;changelog&lt;/span&gt;
&lt;span class="n"&gt;versioning&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;      &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="n"&gt;Revision&lt;/span&gt; &lt;span class="n"&gt;IDs&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt; &lt;span class="n"&gt;management&lt;/span&gt;
&lt;span class="n"&gt;cli&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;             &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="n"&gt;Typer&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;based&lt;/span&gt; &lt;span class="n"&gt;CLI&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;differ.py&lt;/code&gt; doesn't know about risk — it just finds differences. &lt;code&gt;safety.py&lt;/code&gt; doesn't know about SQL — it just assigns risk. &lt;code&gt;sql_generator.py&lt;/code&gt; doesn't know about dependencies — it just generates SQL. This separation makes each layer independently testable:&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;test_not_null_no_default_is_danger&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)])})&lt;/span&gt;
    &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;snap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)])})&lt;/span&gt;
    &lt;span class="n"&gt;diff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;quick_diff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;add_ops&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ops&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;DiffKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ADD_COLUMN&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;add_ops&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;risk&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;RiskLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DANGER&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why Alongside Alembic?
&lt;/h2&gt;

&lt;p&gt;SmartMigrate isn't here to replace Alembic — it's here to catch what Alembic doesn't see.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detecting renames as &lt;code&gt;RENAME&lt;/code&gt; instead of misreading them as DROP+CREATE&lt;/li&gt;
&lt;li&gt;Explaining why each change carries risk&lt;/li&gt;
&lt;li&gt;Tracking dependencies between migration steps&lt;/li&gt;
&lt;li&gt;Keeping model snapshots under version control in CI/CD&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two work well together: use SmartMigrate to understand what changed, Alembic to apply it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;smartmigrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To install from source and run the demo — no database connection required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/bayraktarulku/smartmigrate
&lt;span class="nb"&gt;cd &lt;/span&gt;smartmigrate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
python demo.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The demo compares two snapshots: v1.5.0 → v1.6.0. Column rename detection, NOT NULL column risk, a new table and a dropped table — all visible in the terminal.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The gap between models and the database isn't a bug. It's inevitable. The question is how and how often you measure it.&lt;/p&gt;

&lt;p&gt;SmartMigrate answers "why is this change risky?" for every operation, avoids misreading renames as data loss, and treats migrations as a connected graph rather than an independent list. If you spot something missing or wrong, feel free to open an issue.&lt;/p&gt;

&lt;p&gt;Source code: &lt;strong&gt;github.com/bayraktarulku/smartmigrate&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://alembic.sqlalchemy.org/en/latest/" rel="noopener noreferrer"&gt;Alembic — SQLAlchemy Database Migrations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Directed_acyclic_graph" rel="noopener noreferrer"&gt;Directed Acyclic Graph (DAG) — Wikipedia&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/@n_sabab/airflow-dags-automate-your-etl-process-with-sql-python-and-sqlalchemy-96435ade16c5" rel="noopener noreferrer"&gt;Airflow DAGs: Automate Your ETL Process with SQL, Python and SQLAlchemy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.sqlalchemy.org/" rel="noopener noreferrer"&gt;SQLAlchemy — The Python SQL Toolkit and Object Relational Mapper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.pydantic.dev/" rel="noopener noreferrer"&gt;Pydantic — Data Validation using Python Type Hints&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://networkx.org/" rel="noopener noreferrer"&gt;NetworkX — Network Analysis in Python&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>sqlalchemy</category>
      <category>database</category>
      <category>opensource</category>
    </item>
    <item>
      <title>A Single Touch. Everything Changes.</title>
      <dc:creator>ulku</dc:creator>
      <pubDate>Fri, 13 Mar 2026 11:53:54 +0000</pubDate>
      <link>https://forem.com/pyroot/a-single-touch-everything-changes-ppa</link>
      <guid>https://forem.com/pyroot/a-single-touch-everything-changes-ppa</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the 2026 WeCoded Challenge: Frontend Art&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Show us your Art
&lt;/h2&gt;

&lt;p&gt;Live Demo: &lt;a href="https://bayraktarulku.github.io/equi_burst/" rel="noopener noreferrer"&gt;https://bayraktarulku.github.io/equi_burst/&lt;/a&gt;&lt;br&gt;
GitHub Repo: &lt;a href="https://github.com/bayraktarulku/equi_burst" rel="noopener noreferrer"&gt;https://github.com/bayraktarulku/equi_burst&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Click anywhere on the screen to experience it)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspiration
&lt;/h2&gt;

&lt;p&gt;A black canvas. Still. Silent. Waiting.&lt;/p&gt;

&lt;p&gt;Then — a single touch.&lt;/p&gt;

&lt;p&gt;Colors burst outward. Words appear. A quote from someone who changed the world,&lt;br&gt;
regardless of gender, background, or origin. Because that's all it takes:&lt;br&gt;
&lt;strong&gt;one touch, one moment, one person&lt;/strong&gt; to set everything in motion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;equi_burst&lt;/strong&gt; is a meditation on equality through interaction.&lt;br&gt;
The screen does not react until &lt;em&gt;you&lt;/em&gt; act — just as the world does not change&lt;br&gt;
until someone decides to change it.&lt;/p&gt;

&lt;p&gt;Every click is different. Every burst is unique.&lt;br&gt;
But the message is always the same:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Change doesn't wait for permission. It starts with a touch.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No gender. No border. Just a human hand, a screen, and a burst of color.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Code
&lt;/h2&gt;

&lt;p&gt;Built with &lt;strong&gt;HTML, CSS, and vanilla JavaScript&lt;/strong&gt; — no frameworks,&lt;br&gt;
no filters. Just code, color, and intention.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://github.com/bayraktarulku/equi_burst" rel="noopener noreferrer"&gt;https://github.com/bayraktarulku/equi_burst&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>wecoded</category>
      <category>devchallenge</category>
      <category>frontend</category>
      <category>css</category>
    </item>
  </channel>
</rss>
