<?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>Data Modeling With SQLAlchemy and Alembic on Naveen Kannan</title><link>https://naveenkannan.dev/series/data-modeling-with-sqlalchemy-and-alembic/</link><description>Recent content in Data Modeling With SQLAlchemy and Alembic on Naveen Kannan</description><generator>Hugo</generator><language>en</language><lastBuildDate>Sun, 08 Feb 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://naveenkannan.dev/series/data-modeling-with-sqlalchemy-and-alembic/index.xml" rel="self" type="application/rss+xml"/><item><title>PostgreSQL Entity type Version Control with Alembic Utils.</title><link>https://naveenkannan.dev/posts/alembic_utils_introduction/</link><pubDate>Sun, 08 Feb 2026 00:00:00 +0000</pubDate><guid>https://naveenkannan.dev/posts/alembic_utils_introduction/</guid><description>Committing PostgreSQL entity types to version control using Alembic Utils.</description><content:encoded><![CDATA[<h1 id="introduction">Introduction</h1>
<p>PostgreSQL (and other RDBMS) have entity types such as <a href="https://www.postgresql.org/docs/current/sql-createfunction.html">functions</a>, <a href="https://www.postgresql.org/docs/current/sql-createview.html">views</a>, <a href="https://www.postgresql.org/docs/current/sql-creatematerializedview.html">materialized views</a>, <a href="https://www.postgresql.org/docs/current/sql-createtrigger.html">triggers</a>, and <a href="https://www.postgresql.org/docs/current/sql-createpolicy.html">policies</a>. These entity types do a lot of useful work, and a well designed database will use them heavily.</p>
<p>These entity types are not typically considered part of the SQLAlchemy ORM, and by extension, the data model itself. By default, Alembic has no functionality to detect the creation of new entity types when it autogenerates migration files. The default Alembic ORM also has no functionality to define these entity types with classes.</p>
<p>However, these entity types are part of a database&rsquo;s schema. Creation of/changes to them should be accounted for within the ORM and as part of migration version control, in much the same way as changes in a table are recorded in an ORM as part of migration version control.</p>
<p>In this blog post, we will pick up from the progress in the previous entry in this series and go over how we can use an extension to Alembic called <a href="https://github.com/olirice/alembic_utils">Alembic Utils</a> to add support for autogenerating migrations for some PostgreSQL entity types.</p>
<h2 id="what-weve-already-covered">What we&rsquo;ve already covered</h2>
<p>In <a href="/posts/sqlalchemy_data_modeling/">part 1:</a></p>
<ul>
<li>We used SQLAlchemy to create the first iteration of a rudimentary data model, with three tables.</li>
<li>We configured Alembic to connect to a fresh development database and migrated our rudimentary data model.</li>
</ul>
<p>In <a href="/posts/alembic_model_expansion/">part 2:</a></p>
<ul>
<li>We created a <strong>many-to-many</strong> association between orders and products, and a <strong>many-to-many</strong> association between products and tags.</li>
<li>We generated migration scripts using Alembic.</li>
<li>We committed the migration scripts to version control.</li>
</ul>
<figure class="align-center ">
    <img loading="lazy" src="../alembic_model_expansion/erd_diagram_producttags.png#center"
         alt="The data model at the end of the previous post."/> <figcaption>
            <p>The data model at the end of the previous post.</p>
        </figcaption>
</figure>

<h2 id="what-well-cover">What we&rsquo;ll cover</h2>
<p>In this blog post, we will cover how you can install Alembic Utils and begin incorporating entity types into the commerce data model.</p>
<p>We will do the following:</p>
<ul>
<li>Update the virtual environment with Alembic Utils.</li>
<li>Configure Alembic to use the newly installed extension.</li>
<li>Create a function and a view in the ORM.</li>
<li>Register these entities with Alembic Utils.</li>
<li>Autogenerate a migration script with Alembic that we will review, and then apply the revisions.</li>
</ul>
<h1 id="installing-alembic-utils">Installing Alembic Utils</h1>
<p>In <a href="/posts/sqlalchemy_data_modeling/#prerequisites">part 1</a> of this series, we created a micromamba virtual environment called <code>data_model</code> with SQLAlchemy and Alembic installed.</p>
<p>We will activate this virtual envrionment and then install Alembic Utils.</p>
<p>First, activate the virtual environment.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>micromamba activate data_model
</span></span></code></pre></div><p>Then, install the Alembic Utils extension within the environment. The documentation recommends using <code>pip</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;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>pip install alembic_utils
</span></span></code></pre></div><p>Export the new environment details using the following command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>micromamba env export &gt; environment.yaml
</span></span></code></pre></div>
    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>Updating your <code>environment.yaml</code> file any time your dependencies change is good practice.</p>
<p>It makes your virtual environment reproducible and accessible!</p>
      </div>
    </div><h1 id="adding-a-view-to-the-database">Adding a view to the database</h1>
<h2 id="creating-a-view-in-the-orm">Creating a View in the ORM</h2>
<p>To explain what a view is, we can start by asking a straightforward high level question. What are the names of the products associated with a particular order? The stakeholders who have access to data might need to be able to access the products associated with a single specific order, or a group of specific orders.</p>
<p>In our database, there is a table called <code>order_products</code> that maps the associations between <code>orders</code> and <code>products</code> using the primary keys from that table. However, just looking at this table alone won&rsquo;t give us the contextual information we need. We will need to write an SQL query to join the <code>order_products</code> table with the <code>products</code> table in order to retrieve the name and description of a particular order&rsquo;s products.</p>
<p>This is a query that can be easily <strong>generalizable</strong>. A query can be written to join <strong>all</strong> the products referenced in <code>order_products</code> with the relevant product data from <code>products</code>. This query can then be filtered to return the associations with a single order or group of orders.</p>
<p>This query can be abstracted into a view. Views are named queries that allow for data to be queried from it <strong>like a regular table</strong>. The view is not physically stored, however. The query in the view is run each time the view itself is referenced in a query.</p>
<p>To return the product data associated with each <code>product_id</code> reference in <code>order_products</code>, we could write a query like this.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">select</span>
</span></span><span style="display:flex;"><span>  op.order_id,
</span></span><span style="display:flex;"><span>  op.product_id,
</span></span><span style="display:flex;"><span>  op.product_count,
</span></span><span style="display:flex;"><span>  p.product_name,
</span></span><span style="display:flex;"><span>  p.description,
</span></span><span style="display:flex;"><span>  p.price
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">from</span>
</span></span><span style="display:flex;"><span>  order_products op
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">join</span> products p <span style="color:#66d9ef">on</span> op.product_id <span style="color:#f92672">=</span> p.product_id
</span></span></code></pre></div><p>We can add an adjustment to this query to return the total price of each product associated with an order when accounting for the order count.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">select</span>
</span></span><span style="display:flex;"><span>  op.order_id,
</span></span><span style="display:flex;"><span>  op.product_id,
</span></span><span style="display:flex;"><span>  op.product_count,
</span></span><span style="display:flex;"><span>  p.product_name,
</span></span><span style="display:flex;"><span>  p.description,
</span></span><span style="display:flex;"><span>  p.price <span style="color:#66d9ef">as</span> unit_price,
</span></span><span style="display:flex;"><span>  op.product_count <span style="color:#f92672">*</span> p.price <span style="color:#66d9ef">as</span> order_price
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">from</span>
</span></span><span style="display:flex;"><span>  order_products op
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">join</span> products p <span style="color:#66d9ef">on</span> op.product_id <span style="color:#f92672">=</span> p.product_id
</span></span></code></pre></div><p>We multiply the product count with the unit price to get the total cost of the product within an order. With this query, we can now filter to a specific set of <code>order_id</code> values to return all the products associated with those orders and their total cost!</p>
<p>Creating a view from a query like this, that has the potential to be referenced multiple times, is great database design. We can now add this view to our ORM.</p>
<p>First, we will create a script called <code>db_views.py</code> in the root of the project. This script will be where we store the views that we create for our ORM.</p>
<p><code>db_views.py</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;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic_utils.pg_view <span style="color:#f92672">import</span> PGView
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>order_line_items <span style="color:#f92672">=</span> PGView(
</span></span><span style="display:flex;"><span>    schema<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;public&#34;</span>,
</span></span><span style="display:flex;"><span>    signature<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;order_line_items&#34;</span>,
</span></span><span style="display:flex;"><span>    definition<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">select op.order_id ,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">	op.product_id,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">	op.product_count,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">	p.product_name,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">	p.description,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">	p.price as unit_price,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">	op.product_count * p.price as order_price
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">	from order_products op
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">join products p
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">on op.product_id = p.product_id
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>With this code, we first create a <code>PGView</code> class object called <code>order_line_items</code>. This defines a PostgreSQL view also called <code>order_line_items</code> that references the query that we wrote to return the line items (products) associated with orders.</p>
<h2 id="registering-the-newly-defined-entity">Registering the newly defined entity</h2>
<p>We will update <code>alembic/env.py</code> to then register our newly created entity with alembic_utils.</p>
<p><code>alembic/env.py</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;display:grid;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> logging.config <span style="color:#f92672">import</span> fileConfig
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> engine_from_config
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> pool
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic_utils.replaceable_entity <span style="color:#f92672">import</span> register_entities
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic <span style="color:#f92672">import</span> context
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> models <span style="color:#f92672">import</span> Base
</span></span><span style="display:flex; background-color:#3c3d38"><span><span style="color:#f92672">from</span> db_views <span style="color:#f92672">import</span> order_line_items
</span></span><span style="display:flex; background-color:#3c3d38"><span>
</span></span><span style="display:flex; background-color:#3c3d38"><span>register_entities([order_line_items])
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># this is the Alembic Config object, which provides</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># access to the values within the .ini file in use.</span>
</span></span><span style="display:flex;"><span>config <span style="color:#f92672">=</span> context<span style="color:#f92672">.</span>config
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Interpret the config file for Python logging.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># This line sets up loggers basically.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> config<span style="color:#f92672">.</span>config_file_name <span style="color:#f92672">is</span> <span style="color:#f92672">not</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    fileConfig(config<span style="color:#f92672">.</span>config_file_name)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># add your model&#39;s MetaData object here</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># for &#39;autogenerate&#39; support</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># from myapp import mymodel</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># target_metadata = mymodel.Base.metadata</span>
</span></span><span style="display:flex;"><span>target_metadata <span style="color:#f92672">=</span> Base<span style="color:#f92672">.</span>metadata
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># other values from the config, defined by the needs of env.py,</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># can be acquired:</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># my_important_option = config.get_main_option(&#34;my_important_option&#34;)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ... etc.</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">def</span> <span style="color:#a6e22e">run_migrations_offline</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;Run migrations in &#39;offline&#39; mode.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    This configures the context with just a URL
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    and not an Engine, though an Engine is acceptable
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    here as well.  By skipping the Engine creation
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    we don&#39;t even need a DBAPI to be available.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    Calls to context.execute() here emit the given string to the
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    script output.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    url <span style="color:#f92672">=</span> config<span style="color:#f92672">.</span>get_main_option(<span style="color:#e6db74">&#34;sqlalchemy.url&#34;</span>)
</span></span><span style="display:flex;"><span>    context<span style="color:#f92672">.</span>configure(
</span></span><span style="display:flex;"><span>        url<span style="color:#f92672">=</span>url,
</span></span><span style="display:flex;"><span>        target_metadata<span style="color:#f92672">=</span>target_metadata,
</span></span><span style="display:flex;"><span>        literal_binds<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>        dialect_opts<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;paramstyle&#34;</span>: <span style="color:#e6db74">&#34;named&#34;</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">with</span> context<span style="color:#f92672">.</span>begin_transaction():
</span></span><span style="display:flex;"><span>        context<span style="color:#f92672">.</span>run_migrations()
</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">def</span> <span style="color:#a6e22e">run_migrations_online</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;Run migrations in &#39;online&#39; mode.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    In this scenario we need to create an Engine
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    and associate a connection with the context.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    connectable <span style="color:#f92672">=</span> engine_from_config(
</span></span><span style="display:flex;"><span>        config<span style="color:#f92672">.</span>get_section(config<span style="color:#f92672">.</span>config_ini_section, {}),
</span></span><span style="display:flex;"><span>        prefix<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;sqlalchemy.&#34;</span>,
</span></span><span style="display:flex;"><span>        poolclass<span style="color:#f92672">=</span>pool<span style="color:#f92672">.</span>NullPool,
</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">with</span> connectable<span style="color:#f92672">.</span>connect() <span style="color:#66d9ef">as</span> connection:
</span></span><span style="display:flex;"><span>        context<span style="color:#f92672">.</span>configure(connection<span style="color:#f92672">=</span>connection, target_metadata<span style="color:#f92672">=</span>target_metadata)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">with</span> context<span style="color:#f92672">.</span>begin_transaction():
</span></span><span style="display:flex;"><span>            context<span style="color:#f92672">.</span>run_migrations()
</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">if</span> context<span style="color:#f92672">.</span>is_offline_mode():
</span></span><span style="display:flex;"><span>    run_migrations_offline()
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>    run_migrations_online()
</span></span></code></pre></div><p>The additions to the code import the newly defined view and register it. This view should now be detected by <code>alembic revision --autogenerate</code>.</p>
<h2 id="autogenerating-a-migration">Autogenerating a migration</h2>
<p>We now autogenerate a revision with the command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic revision --autogenerate -m <span style="color:#e6db74">&#34;Created a view called order_line_items that returns order line items.&#34;</span>
</span></span></code></pre></div><p>This should create a new migration script located at <code>/path_to_your_project/alembic/versions</code>.</p>
<p><code>/path_to_your_project/alembic/versions/e2ba9d70fc4d_created_a_view_called_order_line_items_.py</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;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;Created a view called order_line_items that returns order line items.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Revision ID: e2ba9d70fc4d
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Revises: 6e58a26e5c70
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Create Date: 2026-02-07 19:26:28.816521
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> Sequence, Union
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic <span style="color:#f92672">import</span> op
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> sqlalchemy <span style="color:#66d9ef">as</span> sa
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic_utils.pg_view <span style="color:#f92672">import</span> PGView
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> text <span style="color:#66d9ef">as</span> sql_text
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># revision identifiers, used by Alembic.</span>
</span></span><span style="display:flex;"><span>revision: str <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;e2ba9d70fc4d&#39;</span>
</span></span><span style="display:flex;"><span>down_revision: Union[str, <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;6e58a26e5c70&#39;</span>
</span></span><span style="display:flex;"><span>branch_labels: Union[str, Sequence[str], <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>depends_on: Union[str, Sequence[str], <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</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">def</span> <span style="color:#a6e22e">upgrade</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### commands auto generated by Alembic - please adjust! ###</span>
</span></span><span style="display:flex;"><span>    public_order_line_items <span style="color:#f92672">=</span> PGView(
</span></span><span style="display:flex;"><span>        schema<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;public&#34;</span>,
</span></span><span style="display:flex;"><span>        signature<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;order_line_items&#34;</span>,
</span></span><span style="display:flex;"><span>        definition<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;select op.order_id , </span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">op.product_id, </span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">op.product_count,</span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">p.product_name, </span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">p.description, </span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">p.price as unit_price, </span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">op.product_count * p.price as order_price</span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">from order_products op </span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">join products p </span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">on op.product_id = p.product_id&#39;</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>create_entity(public_order_line_items)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### end Alembic commands ###</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">def</span> <span style="color:#a6e22e">downgrade</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### commands auto generated by Alembic - please adjust! ###</span>
</span></span><span style="display:flex;"><span>    public_order_line_items <span style="color:#f92672">=</span> PGView(
</span></span><span style="display:flex;"><span>        schema<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;public&#34;</span>,
</span></span><span style="display:flex;"><span>        signature<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;order_line_items&#34;</span>,
</span></span><span style="display:flex;"><span>        definition<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;select op.order_id , </span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">op.product_id, </span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">op.product_count,</span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">p.product_name, </span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">p.description, </span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">p.price as unit_price, </span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">op.product_count * p.price as order_price</span><span style="color:#ae81ff">\n\t</span><span style="color:#e6db74">from order_products op </span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">join products p </span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">on op.product_id = p.product_id&#39;</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>drop_entity(public_order_line_items)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### end Alembic commands ###</span>
</span></span></code></pre></div><p>This migration script now creates a PostgreSQL view with the name <code>order_line_items</code>.</p>

    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>The <code>PGView</code> object here is called <code>public_order_line_items</code> as the autogenerate function uses the schema name as a prefix to the object name. In this case, the schema used is <code>public</code>.</p>
      </div>
    </div><h2 id="applying-the-migration">Applying the migration</h2>
<p>We now apply the migration with the command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic upgrade head
</span></span></code></pre></div><p>Connecting to the database shows us that the view has been migrated to the database.</p>
<figure class="align-center ">
    <img loading="lazy" src="erd_diagram_view.png#center"
         alt="The newly migrated view."/> <figcaption>
            <p>The newly migrated view.</p>
        </figcaption>
</figure>

<h1 id="expanding-the-data-models-scope">Expanding the data model&rsquo;s scope</h1>
<p>We would like to now expand the data model to track when the attributes of a particular product have been edited. To do this, we will do the following:</p>
<ul>
<li>Add a <code>last_edited</code> timestamp column to the <code>products</code> table.</li>
<li>Create a function that will update the <code>last_edited</code> column of a table.</li>
<li>Create a trigger that will call the aforementioned function whenever a row of <code>products</code> is edited.</li>
</ul>
<h2 id="adding-a-timestamp-column-to-the-products-table">Adding a timestamp column to the <code>products</code> table</h2>
<p>Amend the data model in <code>models.py</code> to have a <code>last_edited</code> timestamp column.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> (
</span></span><span style="display:flex;"><span>    Column,
</span></span><span style="display:flex;"><span>    Integer,
</span></span><span style="display:flex;"><span>    String,
</span></span><span style="display:flex;"><span>    Float,
</span></span><span style="display:flex;"><span>    ForeignKey,
</span></span><span style="display:flex;"><span>    Identity,
</span></span><span style="display:flex;"><span>    Text,
</span></span><span style="display:flex;"><span>    DateTime,
</span></span><span style="display:flex;"><span>    Table,
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy.orm <span style="color:#f92672">import</span> declarative_base, relationship
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy.sql <span style="color:#f92672">import</span> func
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Base <span style="color:#f92672">=</span> declarative_base()
</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">class</span> <span style="color:#a6e22e">Customer</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;customers&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    customer_id <span style="color:#f92672">=</span> Column(Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    first_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    last_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    email_address <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    orders <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Order&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;customer&#34;</span>,
</span></span><span style="display:flex;"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</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 style="color:#66d9ef">class</span> <span style="color:#a6e22e">OrderProduct</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;order_products&#34;</span>
</span></span><span style="display:flex;"><span>    order_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer,
</span></span><span style="display:flex;"><span>        ForeignKey(<span style="color:#e6db74">&#34;orders.order_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>),
</span></span><span style="display:flex;"><span>        primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    product_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer,
</span></span><span style="display:flex;"><span>        ForeignKey(<span style="color:#e6db74">&#34;products.product_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>),
</span></span><span style="display:flex;"><span>        primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    product_count <span style="color:#f92672">=</span> Column(Integer, server_default<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;1&#34;</span>, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    order <span style="color:#f92672">=</span> relationship(<span style="color:#e6db74">&#34;Order&#34;</span>, back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;products&#34;</span>)
</span></span><span style="display:flex;"><span>    product <span style="color:#f92672">=</span> relationship(<span style="color:#e6db74">&#34;Product&#34;</span>, back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;orders&#34;</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">class</span> <span style="color:#a6e22e">Order</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;orders&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    order_id <span style="color:#f92672">=</span> Column(Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    customer_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer, ForeignKey(<span style="color:#e6db74">&#34;customers.customer_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    order_date <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        DateTime(timezone<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>),
</span></span><span style="display:flex;"><span>        server_default<span style="color:#f92672">=</span>func<span style="color:#f92672">.</span>now(),
</span></span><span style="display:flex;"><span>        nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    customer <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Customer&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;orders&#34;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    products <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;OrderProduct&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;order&#34;</span>,
</span></span><span style="display:flex;"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</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>product_tags <span style="color:#f92672">=</span> Table(
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;product_tags&#34;</span>,
</span></span><span style="display:flex;"><span>    Base<span style="color:#f92672">.</span>metadata,
</span></span><span style="display:flex;"><span>    Column(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;product_id&#34;</span>,
</span></span><span style="display:flex;"><span>        ForeignKey(<span style="color:#e6db74">&#34;products.product_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>),
</span></span><span style="display:flex;"><span>        primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>    ),
</span></span><span style="display:flex;"><span>    Column(<span style="color:#e6db74">&#34;tag_id&#34;</span>, ForeignKey(<span style="color:#e6db74">&#34;tags.tag_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</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 style="color:#66d9ef">class</span> <span style="color:#a6e22e">Product</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;products&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    product_id <span style="color:#f92672">=</span> Column(Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    product_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    description <span style="color:#f92672">=</span> Column(Text)
</span></span><span style="display:flex;"><span>    price <span style="color:#f92672">=</span> Column(Float, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex; background-color:#3c3d38"><span>    last_edited <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex; background-color:#3c3d38"><span>        DateTime(timezone<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), server_default<span style="color:#f92672">=</span>func<span style="color:#f92672">.</span>now(), onupdate<span style="color:#f92672">=</span>func<span style="color:#f92672">.</span>now()
</span></span><span style="display:flex; background-color:#3c3d38"><span>    )
</span></span><span style="display:flex;"><span>    orders <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;OrderProduct&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;product&#34;</span>,
</span></span><span style="display:flex;"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    tags <span style="color:#f92672">=</span> relationship(<span style="color:#e6db74">&#34;Tag&#34;</span>, secondary<span style="color:#f92672">=</span>product_tags, back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;products&#34;</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">class</span> <span style="color:#a6e22e">Tag</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tags&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    tag_id <span style="color:#f92672">=</span> Column(Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>, unique<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    products <span style="color:#f92672">=</span> relationship(<span style="color:#e6db74">&#34;Product&#34;</span>, secondary<span style="color:#f92672">=</span>product_tags, back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;tags&#34;</span>)
</span></span></code></pre></div>
    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>The <code>onupdate=func.now()</code> parameter for <code>last_edited</code> will update the column when changes are issued through the ORM.</p>
<p>To guarantee database level correctness, we will need to create a trigger as well to guarantee data accuracy
even when changes come from outside of any applications that use the ORM.</p>
      </div>
    </div><p>Generate a revision with <code>alembic --autogenerate</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;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;Added a column to products to track timestamps for row-level edits.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Revision ID: eebe110ef9d2
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Revises: e2ba9d70fc4d
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Create Date: 2026-02-08 16:29:55.536261
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> Sequence, Union
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic <span style="color:#f92672">import</span> op
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> sqlalchemy <span style="color:#66d9ef">as</span> sa
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># revision identifiers, used by Alembic.</span>
</span></span><span style="display:flex;"><span>revision: str <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;eebe110ef9d2&#39;</span>
</span></span><span style="display:flex;"><span>down_revision: Union[str, <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;e2ba9d70fc4d&#39;</span>
</span></span><span style="display:flex;"><span>branch_labels: Union[str, Sequence[str], <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>depends_on: Union[str, Sequence[str], <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</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">def</span> <span style="color:#a6e22e">upgrade</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### commands auto generated by Alembic - please adjust! ###</span>
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>add_column(<span style="color:#e6db74">&#39;products&#39;</span>, sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;last_edited&#39;</span>, sa<span style="color:#f92672">.</span>DateTime(timezone<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), server_default<span style="color:#f92672">=</span>sa<span style="color:#f92672">.</span>text(<span style="color:#e6db74">&#39;now()&#39;</span>), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>))
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### end Alembic commands ###</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">def</span> <span style="color:#a6e22e">downgrade</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### commands auto generated by Alembic - please adjust! ###</span>
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>drop_column(<span style="color:#e6db74">&#39;products&#39;</span>, <span style="color:#e6db74">&#39;last_edited&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### end Alembic commands ###</span>
</span></span></code></pre></div><p>We will then apply this migration with the <code>alembic upgrade head</code> command.</p>
<h2 id="creating-a-function-to-update-the-last_edited-column-of-products">Creating a function to update the <code>last_edited</code> column of <code>products</code></h2>
<p>Create a script called <code>db_functions.py</code> in the project root.</p>
<p><code>db_functions.py</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;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic_utils.pg_function <span style="color:#f92672">import</span> PGFunction
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Creating a function that updates the `last_edited` column of a table</span>
</span></span><span style="display:flex;"><span>timestamp_update_on_edit <span style="color:#f92672">=</span> PGFunction(
</span></span><span style="display:flex;"><span>    schema<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;public&#34;</span>,
</span></span><span style="display:flex;"><span>    signature<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;timestamp_update_on_edit()&#34;</span>,
</span></span><span style="display:flex;"><span>    definition<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">RETURNS TRIGGER AS $$ BEGIN NEW.last_edited = now();
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">RETURN NEW;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">END;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">$$ language PLPGSQL
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>With this code, we create a <code>PGFunction</code> class object that defines a PostgreSQL function called <code>timestamp_update_on_edit()</code>. When this function is called, the <code>last_edited</code> column is updated to have the time and date of the point in time when it was called.</p>
<h2 id="creating-a-trigger-to-call-the-timestamp-function-on-row-edits">Creating a trigger to call the timestamp function on row edits</h2>
<p>Create a script called <code>db_triggers.py</code> in the project root.</p>
<p><code>db_triggers.py</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;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic_utils.pg_trigger <span style="color:#f92672">import</span> PGTrigger
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">## Creating a trigger to apply `timestamp_update_on_edit()` to `products` on a row being updated</span>
</span></span><span style="display:flex;"><span>timestamp_update_apply_products_trigger <span style="color:#f92672">=</span> PGTrigger(
</span></span><span style="display:flex;"><span>    schema<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;public&#34;</span>,
</span></span><span style="display:flex;"><span>    signature<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;timestamp_update_apply_products_trigger&#34;</span>,
</span></span><span style="display:flex;"><span>    on_entity<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;public.products&#34;</span>,
</span></span><span style="display:flex;"><span>    is_constraint<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>,
</span></span><span style="display:flex;"><span>    definition<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&#34;&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">BEFORE
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">UPDATE
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  on products FOR EACH ROW EXECUTE FUNCTION timestamp_update_on_edit()
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>This code defines a <code>PGTrigger</code> class object with the signature <code>timestamp_update_apply_products_trigger</code>. When a row is updated on <code>products</code>, this trigger is activated and calls the <code>timestamp_update_on_edit()</code> function, which will update the <code>last_edited</code> column.</p>

    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>In the ORM, as part of the definition of the <code>Product</code> object, the <code>onupdate=func.now()</code> parameter
updates the <code>last_edited</code> column when changes are issued through the ORM.</p>
<p>When we create a trigger to perform the timestamp update for <code>products</code> in the database upon a row being edited,
this guarantees correctness on a database level even when the changes come outside of the application.</p>
      </div>
    </div><h2 id="registering-the-newly-defined-entities">Registering the newly defined entities</h2>
<p>We will update <code>alembic/env.py</code> to then register our newly created entities with alembic_utils.</p>
<p><code>alembic/env.py</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;display:grid;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> logging.config <span style="color:#f92672">import</span> fileConfig
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> engine_from_config
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> pool
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic_utils.replaceable_entity <span style="color:#f92672">import</span> register_entities
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic <span style="color:#f92672">import</span> context
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> models <span style="color:#f92672">import</span> Base
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> db_views <span style="color:#f92672">import</span> order_line_items
</span></span><span style="display:flex; background-color:#3c3d38"><span><span style="color:#f92672">from</span> db_functions <span style="color:#f92672">import</span> timestamp_update_on_edit
</span></span><span style="display:flex; background-color:#3c3d38"><span><span style="color:#f92672">from</span> db_triggers <span style="color:#f92672">import</span> timestamp_update_apply_products_trigger
</span></span><span style="display:flex; background-color:#3c3d38"><span>
</span></span><span style="display:flex; background-color:#3c3d38"><span>register_entities(
</span></span><span style="display:flex; background-color:#3c3d38"><span>    [
</span></span><span style="display:flex; background-color:#3c3d38"><span>        order_line_items,
</span></span><span style="display:flex; background-color:#3c3d38"><span>        timestamp_update_on_edit,
</span></span><span style="display:flex; background-color:#3c3d38"><span>        timestamp_update_apply_products_trigger,
</span></span><span style="display:flex; background-color:#3c3d38"><span>    ]
</span></span><span style="display:flex; background-color:#3c3d38"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># this is the Alembic Config object, which provides</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># access to the values within the .ini file in use.</span>
</span></span><span style="display:flex;"><span>config <span style="color:#f92672">=</span> context<span style="color:#f92672">.</span>config
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Interpret the config file for Python logging.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># This line sets up loggers basically.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> config<span style="color:#f92672">.</span>config_file_name <span style="color:#f92672">is</span> <span style="color:#f92672">not</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    fileConfig(config<span style="color:#f92672">.</span>config_file_name)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># add your model&#39;s MetaData object here</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># for &#39;autogenerate&#39; support</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># from myapp import mymodel</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># target_metadata = mymodel.Base.metadata</span>
</span></span><span style="display:flex;"><span>target_metadata <span style="color:#f92672">=</span> Base<span style="color:#f92672">.</span>metadata
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># other values from the config, defined by the needs of env.py,</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># can be acquired:</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># my_important_option = config.get_main_option(&#34;my_important_option&#34;)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ... etc.</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">def</span> <span style="color:#a6e22e">run_migrations_offline</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;Run migrations in &#39;offline&#39; mode.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    This configures the context with just a URL
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    and not an Engine, though an Engine is acceptable
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    here as well.  By skipping the Engine creation
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    we don&#39;t even need a DBAPI to be available.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    Calls to context.execute() here emit the given string to the
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    script output.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    url <span style="color:#f92672">=</span> config<span style="color:#f92672">.</span>get_main_option(<span style="color:#e6db74">&#34;sqlalchemy.url&#34;</span>)
</span></span><span style="display:flex;"><span>    context<span style="color:#f92672">.</span>configure(
</span></span><span style="display:flex;"><span>        url<span style="color:#f92672">=</span>url,
</span></span><span style="display:flex;"><span>        target_metadata<span style="color:#f92672">=</span>target_metadata,
</span></span><span style="display:flex;"><span>        literal_binds<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>        dialect_opts<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;paramstyle&#34;</span>: <span style="color:#e6db74">&#34;named&#34;</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">with</span> context<span style="color:#f92672">.</span>begin_transaction():
</span></span><span style="display:flex;"><span>        context<span style="color:#f92672">.</span>run_migrations()
</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">def</span> <span style="color:#a6e22e">run_migrations_online</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;Run migrations in &#39;online&#39; mode.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    In this scenario we need to create an Engine
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    and associate a connection with the context.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    connectable <span style="color:#f92672">=</span> engine_from_config(
</span></span><span style="display:flex;"><span>        config<span style="color:#f92672">.</span>get_section(config<span style="color:#f92672">.</span>config_ini_section, {}),
</span></span><span style="display:flex;"><span>        prefix<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;sqlalchemy.&#34;</span>,
</span></span><span style="display:flex;"><span>        poolclass<span style="color:#f92672">=</span>pool<span style="color:#f92672">.</span>NullPool,
</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">with</span> connectable<span style="color:#f92672">.</span>connect() <span style="color:#66d9ef">as</span> connection:
</span></span><span style="display:flex;"><span>        context<span style="color:#f92672">.</span>configure(connection<span style="color:#f92672">=</span>connection, target_metadata<span style="color:#f92672">=</span>target_metadata)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">with</span> context<span style="color:#f92672">.</span>begin_transaction():
</span></span><span style="display:flex;"><span>            context<span style="color:#f92672">.</span>run_migrations()
</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">if</span> context<span style="color:#f92672">.</span>is_offline_mode():
</span></span><span style="display:flex;"><span>    run_migrations_offline()
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>    run_migrations_online()
</span></span></code></pre></div><p>With this update, our newly created function and trigger have been registered. They should now be detected when using <code>autogenerate</code>.</p>
<h2 id="autogenerating-and-applying-a-revision-to-migrate-the-newly-added-entities">Autogenerating and applying a revision to migrate the newly added entities</h2>
<p>Create a revision script with:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic revision --autogenerate -m <span style="color:#e6db74">&#34;Created a function to update the last_edited timestamp column, and created a trigger to call this function for the products table.&#34;</span>
</span></span></code></pre></div><p>This should generate a revision file with the following content:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;Created a function to update the last_edited timestamp column, and created a trigger to call this function for the products table.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Revision ID: 172f9e67a364
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Revises: eebe110ef9d2
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Create Date: 2026-02-08 16:41:47.178179
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> Sequence, Union
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic <span style="color:#f92672">import</span> op
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> sqlalchemy <span style="color:#66d9ef">as</span> sa
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic_utils.pg_function <span style="color:#f92672">import</span> PGFunction
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> text <span style="color:#66d9ef">as</span> sql_text
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic_utils.pg_trigger <span style="color:#f92672">import</span> PGTrigger
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> text <span style="color:#66d9ef">as</span> sql_text
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># revision identifiers, used by Alembic.</span>
</span></span><span style="display:flex;"><span>revision: str <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;172f9e67a364&#39;</span>
</span></span><span style="display:flex;"><span>down_revision: Union[str, <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;eebe110ef9d2&#39;</span>
</span></span><span style="display:flex;"><span>branch_labels: Union[str, Sequence[str], <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>depends_on: Union[str, Sequence[str], <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</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">def</span> <span style="color:#a6e22e">upgrade</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### commands auto generated by Alembic - please adjust! ###</span>
</span></span><span style="display:flex;"><span>    public_timestamp_update_on_edit <span style="color:#f92672">=</span> PGFunction(
</span></span><span style="display:flex;"><span>        schema<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;public&#34;</span>,
</span></span><span style="display:flex;"><span>        signature<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;timestamp_update_on_edit()&#34;</span>,
</span></span><span style="display:flex;"><span>        definition<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;RETURNS TRIGGER AS $$ BEGIN NEW.last_edited = now();</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">RETURN NEW;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">END;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">$$ language PLPGSQL&#39;</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>create_entity(public_timestamp_update_on_edit)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    public_products_timestamp_update_apply_products_trigger <span style="color:#f92672">=</span> PGTrigger(
</span></span><span style="display:flex;"><span>        schema<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;public&#34;</span>,
</span></span><span style="display:flex;"><span>        signature<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;timestamp_update_apply_products_trigger&#34;</span>,
</span></span><span style="display:flex;"><span>        on_entity<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;public.products&#34;</span>,
</span></span><span style="display:flex;"><span>        is_constraint<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>,
</span></span><span style="display:flex;"><span>        definition<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;BEFORE </span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">UPDATE </span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">  on products FOR EACH ROW EXECUTE FUNCTION timestamp_update_on_edit()&#39;</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>create_entity(public_products_timestamp_update_apply_products_trigger)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### end Alembic commands ###</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">def</span> <span style="color:#a6e22e">downgrade</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### commands auto generated by Alembic - please adjust! ###</span>
</span></span><span style="display:flex;"><span>    public_products_timestamp_update_apply_products_trigger <span style="color:#f92672">=</span> PGTrigger(
</span></span><span style="display:flex;"><span>        schema<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;public&#34;</span>,
</span></span><span style="display:flex;"><span>        signature<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;timestamp_update_apply_products_trigger&#34;</span>,
</span></span><span style="display:flex;"><span>        on_entity<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;public.products&#34;</span>,
</span></span><span style="display:flex;"><span>        is_constraint<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>,
</span></span><span style="display:flex;"><span>        definition<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;BEFORE </span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">UPDATE </span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">  on products FOR EACH ROW EXECUTE FUNCTION timestamp_update_on_edit()&#39;</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>drop_entity(public_products_timestamp_update_apply_products_trigger)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    public_timestamp_update_on_edit <span style="color:#f92672">=</span> PGFunction(
</span></span><span style="display:flex;"><span>        schema<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;public&#34;</span>,
</span></span><span style="display:flex;"><span>        signature<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;timestamp_update_on_edit()&#34;</span>,
</span></span><span style="display:flex;"><span>        definition<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;RETURNS TRIGGER AS $$ BEGIN NEW.last_edited = now();</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">RETURN NEW;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">END;</span><span style="color:#ae81ff">\n</span><span style="color:#e6db74">$$ language PLPGSQL&#39;</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>drop_entity(public_timestamp_update_on_edit)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### end Alembic commands ###</span>
</span></span></code></pre></div><p>The migration script should create the function and the trigger that we defined in the ORM within the database. After reviewing it, we apply the migration with the command <code>alembic upgrade head</code>.</p>
<p>Once these changes have been migrated, run the following SQL code in the database to get the triggers associated with the <code>products</code> table.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span>
</span></span><span style="display:flex;"><span>    tgname <span style="color:#66d9ef">AS</span> <span style="color:#66d9ef">trigger_name</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span>
</span></span><span style="display:flex;"><span>    pg_trigger
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span>
</span></span><span style="display:flex;"><span>    tgrelid <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;public.products&#39;</span>::regclass
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ORDER</span> <span style="color:#66d9ef">BY</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">trigger_name</span>;
</span></span></code></pre></div><p>This should give you the output:</p>
<pre tabindex="0"><code>RI_ConstraintTrigger_a_16447
RI_ConstraintTrigger_a_16448
RI_ConstraintTrigger_a_16471
RI_ConstraintTrigger_a_16472
timestamp_update_apply_products_trigger
</code></pre><p>Indicating the trigger has been successfully migrated. Run the following code to get the functions associated with the <code>public</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;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">routine_name</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span>
</span></span><span style="display:flex;"><span>    information_schema.routines
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span>
</span></span><span style="display:flex;"><span>    routine_type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;FUNCTION&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">AND</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">routine_schema</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;public&#39;</span>;
</span></span></code></pre></div><p>This should give you the output:</p>
<pre tabindex="0"><code>routine_name
timestamp_update_on_edit
</code></pre><p>showing that our new function has been migrated to the database as well!</p>
<p>We&rsquo;ve successfully added a view, a function and a trigger to our database via migrations applied from our SQLAlchemy ORM.</p>
<p>Browsing through <code>alembic/versions</code> should give us an idea of the growth of our data model and how it evolves to match the change in scope of our e-commerce workflow.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic/versions/
</span></span><span style="display:flex;"><span>├── 172f9e67a364_created_a_function_to_update_the_last_.py
</span></span><span style="display:flex;"><span>├── 6e58a26e5c70_created_a_table_for_tags_and_created_a_.py
</span></span><span style="display:flex;"><span>├── 960969e75871_created_orderproduct_as_an_association_.py
</span></span><span style="display:flex;"><span>├── a0cf7f5d705d_initial_commit_created_customers_orders_.py
</span></span><span style="display:flex;"><span>├── e2ba9d70fc4d_created_a_view_called_order_line_items_.py
</span></span><span style="display:flex;"><span>└── eebe110ef9d2_added_a_column_to_products_to_track_.py
</span></span></code></pre></div><h1 id="next-steps">Next Steps</h1>
<p>In subsequent blog posts, we will cover:</p>
<ul>
<li>Applying our migrations to a production environment</li>
<li>Using the ORM as a module when coding a FastAPI server</li>
</ul>
<h1 id="references">References</h1>
<ul>
<li>The <a href="https://github.com/olirice/alembic_utils">repo for Alembic Utils.</a></li>
<li>The <a href="https://olirice.github.io/alembic_utils/">documentation for Alembic Utils.</a></li>
</ul>
]]></content:encoded></item><item><title>Data Model Version Control with Alembic.</title><link>https://naveenkannan.dev/posts/alembic_model_expansion/</link><pubDate>Mon, 26 Jan 2026 00:00:00 +0000</pubDate><guid>https://naveenkannan.dev/posts/alembic_model_expansion/</guid><description>Developing version-controlled relational data models using Alembic.</description><content:encoded><![CDATA[<h1 id="introduction">Introduction</h1>
<p>As the scope of a data model (and by extension, its downstream APIs) changes, it will need to be updated and expanded to account for this new scope.</p>
<p>When changes are made to a data model, especially to a model that is split across development and production environments, schema drift becomes a constant problem that looms in the background.</p>
<p>Typical coding workflows are managed by version control as an industry standard. Database schemas, however, are typically not submitted to a version control system. As different team members collaborate on a data model split across different environments, the possibility of schema drift gradually grows in an environment where the data model is not tracked in a central repository.</p>
<p>This is where Alembic shines! It is the de-facto migration tool for SQLAlchemy, and is the ideal tool to version control a data model&rsquo;s development.</p>
<p>In this blog post, we will pick up from the progress in the previous entry in this series and continue to develop our data model. We will build more tables and relationships into our data model, and commit all of these changes to version control using Alembic.</p>
<h2 id="what-weve-already-covered">What we&rsquo;ve already covered</h2>
<p>In our <a href="/posts/sqlalchemy_data_modeling/">previous blog post:</a></p>
<ul>
<li>We used SQLAlchemy to create the first iteration of a rudimentary data model, with three tables.</li>
<li>We configured Alembic to connect to a fresh development database and migrated our rudimentary data model.</li>
</ul>
<p>Please refer to the first post in this series for a detailed explanation of SQLAlchemy and Alembic, and how it is relevant in our workflows here.</p>
<h2 id="what-well-cover-in-this-post">What we&rsquo;ll cover in this post</h2>
<ul>
<li>Creating a <strong>many-to-many</strong> association between orders and products.</li>
<li>Creating a <strong>many-to-many</strong> association between products and tags.</li>
<li>Committing all of our changes to version control with Alembic.</li>
</ul>
<h1 id="getting-started">Getting started</h1>
<p>At this point, we have a rudimentary data model with three tables, and only two relationships.</p>
<p>There&rsquo;s <code>orders</code>,<code>customers</code> and <code>products</code>, with a one-to-many relationship between <code>customers</code> and <code>orders</code>, and a many-to-one relationship between <code>orders</code> and <code>customers</code>.</p>
<figure class="align-center ">
    <img loading="lazy" src="../sqlalchemy_data_modeling/basic_data_model_erd.png#center"
         alt="Our initial data model."/> <figcaption>
            <p>Our initial data model.</p>
        </figcaption>
</figure>

<p>The classes for these tables in our ORM were defined as follows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Customer</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;customers&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    customer_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    first_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    last_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    email_address <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    orders <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Order&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;customer&#34;</span>,
</span></span><span style="display:flex;"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</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">class</span> <span style="color:#a6e22e">Order</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;orders&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    order_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    customer_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer,
</span></span><span style="display:flex;"><span>        ForeignKey(<span style="color:#e6db74">&#34;customers.customer_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>),
</span></span><span style="display:flex;"><span>        nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    order_date <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        DateTime(timezone<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>),
</span></span><span style="display:flex;"><span>        server_default<span style="color:#f92672">=</span>func<span style="color:#f92672">.</span>now(),
</span></span><span style="display:flex;"><span>        nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    customer <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Customer&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;orders&#34;</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">class</span> <span style="color:#a6e22e">Product</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;products&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    product_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    product_name <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    description <span style="color:#f92672">=</span> Column(Text)
</span></span><span style="display:flex;"><span>    price <span style="color:#f92672">=</span> Column(Float, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span></code></pre></div>
    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>We generated an initial migration, but right now, this migration environment lives locally. We need to push this to a GitHub repository!</p>
<p>The data model at the end of the first blog post has been pushed to GitHub and can be <a href="https://github.com/naveenk2022/commerce_data_model">found here</a>.</p>
<p>This repository will evolve along with the blog! Every change we make moving forward will be committed to version control.</p>
      </div>
    </div><p>There are a lot of changes we need to make to make this data model usable. To start with:</p>
<ul>
<li>A connection needs to be built between orders and products. A single order can have multiple products, and a single product can be associated with multiple orders.</li>
<li>Products need to be associated with tags to enable better categorization within the product catalog. Each product can be associated with multiple tags, and
a single tag can be associated with multiple products.</li>
</ul>

    <div class="admonition tip">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M272 384c9.6-31.9 29.5-59.1 49.2-86.2c0 0 0 0 0 0c5.2-7.1 10.4-14.2 15.4-21.4c19.8-28.5 31.4-63 31.4-100.3C368 78.8 289.2 0 192 0S16 78.8 16 176c0 37.3 11.6 71.9 31.4 100.3c5 7.2 10.2 14.3 15.4 21.4c0 0 0 0 0 0c19.8 27.1 39.7 54.4 49.2 86.2l160 0zM192 512c44.2 0 80-35.8 80-80l0-16-160 0 0 16c0 44.2 35.8 80 80 80zM112 176c0 8.8-7.2 16-16 16s-16-7.2-16-16c0-61.9 50.1-112 112-112c8.8 0 16 7.2 16 16s-7.2 16-16 16c-44.2 0-80 35.8-80 80z"/></svg>
        <span>Tip</span>
      </div>
      <div class="admonition-content">
        <p>This change in relationship between orders and products is a <strong>many-to-many</strong> relationship!</p>
<p>When we expand our data model to include tags, the relationship between tags and products will also be a <strong>many-to-many</strong> relationship.</p>
      </div>
    </div><p>Let&rsquo;s begin implementing this change in scope within the data model. With each step, we will also commit our work to version control using Alembic.</p>
<h2 id="many-to-many-relationship-between-orders-and-products">Many-to-many relationship between <code>orders</code> and <code>products</code></h2>
<p>We want to build a many-to-many relationship between <code>orders</code> and <code>products</code>. We also want to expand this relationship to track the <strong>count</strong> of each product in an order.</p>
<p>To accomplish that, we will create an <strong>association table.</strong></p>

    <div class="admonition tip">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M272 384c9.6-31.9 29.5-59.1 49.2-86.2c0 0 0 0 0 0c5.2-7.1 10.4-14.2 15.4-21.4c19.8-28.5 31.4-63 31.4-100.3C368 78.8 289.2 0 192 0S16 78.8 16 176c0 37.3 11.6 71.9 31.4 100.3c5 7.2 10.2 14.3 15.4 21.4c0 0 0 0 0 0c19.8 27.1 39.7 54.4 49.2 86.2l160 0zM192 512c44.2 0 80-35.8 80-80l0-16-160 0 0 16c0 44.2 35.8 80 80 80zM112 176c0 8.8-7.2 16-16 16s-16-7.2-16-16c0-61.9 50.1-112 112-112c8.8 0 16 7.2 16 16s-7.2 16-16 16c-44.2 0-80 35.8-80 80z"/></svg>
        <span>Tip</span>
      </div>
      <div class="admonition-content">
        <p>Association tables are also referred to as join tables, junction tables or cross-reference tables.</p>
      </div>
    </div><p>An association table maps two (or more) tables together by using their primary keys as foreign keys. Each foreign key forms a <strong>many-to-one</strong> relationship between the association table and the individual data table. An association table creates a two-way link between the tables that represent each entity.</p>
<p>The best way to demonstrate this is by example! Let&rsquo;s begin by creating an association table to group together the products associated with each order.</p>
<p>To generate an association table, the following changes need to be made to <code>models.py</code>. Additions have been highlighted.</p>
<p><strong><code>models.py</code></strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> (
</span></span><span style="display:flex;"><span>    Column,
</span></span><span style="display:flex;"><span>    Integer,
</span></span><span style="display:flex;"><span>    String,
</span></span><span style="display:flex;"><span>    Float,
</span></span><span style="display:flex;"><span>    ForeignKey,
</span></span><span style="display:flex;"><span>    Identity,
</span></span><span style="display:flex;"><span>    Text,
</span></span><span style="display:flex;"><span>    DateTime,
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy.orm <span style="color:#f92672">import</span> declarative_base, relationship
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy.sql <span style="color:#f92672">import</span> func
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Base <span style="color:#f92672">=</span> declarative_base()
</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">class</span> <span style="color:#a6e22e">Customer</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;customers&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    customer_id <span style="color:#f92672">=</span> Column(Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    first_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    last_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    email_address <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    orders <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Order&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;customer&#34;</span>,
</span></span><span style="display:flex;"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</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; background-color:#3c3d38"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">OrderProduct</span>(Base):
</span></span><span style="display:flex; background-color:#3c3d38"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;order_products&#34;</span>
</span></span><span style="display:flex; background-color:#3c3d38"><span>    order_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex; background-color:#3c3d38"><span>        Integer,
</span></span><span style="display:flex; background-color:#3c3d38"><span>        ForeignKey(<span style="color:#e6db74">&#34;orders.order_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>),
</span></span><span style="display:flex; background-color:#3c3d38"><span>        primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex; background-color:#3c3d38"><span>    )
</span></span><span style="display:flex; background-color:#3c3d38"><span>
</span></span><span style="display:flex; background-color:#3c3d38"><span>    product_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex; background-color:#3c3d38"><span>        Integer,
</span></span><span style="display:flex; background-color:#3c3d38"><span>        ForeignKey(<span style="color:#e6db74">&#34;products.product_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>),
</span></span><span style="display:flex; background-color:#3c3d38"><span>        primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex; background-color:#3c3d38"><span>    )
</span></span><span style="display:flex; background-color:#3c3d38"><span>
</span></span><span style="display:flex; background-color:#3c3d38"><span>    product_count <span style="color:#f92672">=</span> Column(Integer, server_default<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;1&#34;</span>, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex; background-color:#3c3d38"><span>    order <span style="color:#f92672">=</span> relationship(<span style="color:#e6db74">&#34;Order&#34;</span>, back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;products&#34;</span>)
</span></span><span style="display:flex; background-color:#3c3d38"><span>    product <span style="color:#f92672">=</span> relationship(<span style="color:#e6db74">&#34;Product&#34;</span>, back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;orders&#34;</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">class</span> <span style="color:#a6e22e">Order</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;orders&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    order_id <span style="color:#f92672">=</span> Column(Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    customer_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer, ForeignKey(<span style="color:#e6db74">&#34;customers.customer_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    order_date <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        DateTime(timezone<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>),
</span></span><span style="display:flex;"><span>        server_default<span style="color:#f92672">=</span>func<span style="color:#f92672">.</span>now(),
</span></span><span style="display:flex;"><span>        nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    customer <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Customer&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;orders&#34;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex; background-color:#3c3d38"><span>    products <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex; background-color:#3c3d38"><span>        <span style="color:#e6db74">&#34;OrderProduct&#34;</span>,
</span></span><span style="display:flex; background-color:#3c3d38"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;order&#34;</span>,
</span></span><span style="display:flex; background-color:#3c3d38"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</span>,
</span></span><span style="display:flex; background-color:#3c3d38"><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">class</span> <span style="color:#a6e22e">Product</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;products&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    product_id <span style="color:#f92672">=</span> Column(Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    product_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    description <span style="color:#f92672">=</span> Column(Text)
</span></span><span style="display:flex;"><span>    price <span style="color:#f92672">=</span> Column(Float, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex; background-color:#3c3d38"><span>    orders <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex; background-color:#3c3d38"><span>        <span style="color:#e6db74">&#34;OrderProduct&#34;</span>,
</span></span><span style="display:flex; background-color:#3c3d38"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;product&#34;</span>,
</span></span><span style="display:flex; background-color:#3c3d38"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</span>,
</span></span><span style="display:flex; background-color:#3c3d38"><span>    )
</span></span></code></pre></div><h3 id="creating-the-association-table">Creating the association table</h3>
<p>We create a class to define the association table, since the association table contains an additional column beyond just the associations with <code>Order</code> and <code>Product</code>. This table is <code>OrderProduct</code>.</p>
<p>The<code>order_id</code> and <code>product_id</code> columns in this table are designated as <strong>foreign keys</strong>.</p>
<p>We then create associations between <code>OrderProduct</code> and the entity tables using the <code>relationship</code> parameter. <code>OrderProducts</code> has relationships built to both <code>Order</code> and <code>Product</code>.</p>
<h3 id="explaining-the-relationship-between-order-and-product">Explaining the relationship between <code>Order</code> and <code>Product</code></h3>
<p>There is no <strong>direct  relationship</strong> between <code>Order</code> and <code>Product</code>. This relationship is mediated by the <code>OrderProduct</code> class, which is an association table that decomposes the relationship between <code>Order</code> and <code>Product</code>.</p>
<p>A single instance of an <code>Order</code> can belong to multiple rows of <code>OrderProduct</code>.</p>
<p>A single instance of a <code>Product</code> can belong to multiple rows of <code>OrderProduct</code>.</p>
<p>However, every instance of <code>OrderProduct</code> can only have:</p>
<ul>
<li>
<p>A <strong>single</strong> <code>Order</code></p>
</li>
<li>
<p>A <strong>single</strong> <code>Product</code>.</p>
</li>
</ul>
<p>In essence, the many-to-many relationship between <code>Order</code> and <code>Product</code> is abstracted into being indirect via <code>OrderProduct</code>. Each row in <code>OrderProduct</code> represents a single product within a single order, along with additional metadata about that association, such as the product count for that order.</p>
<p>The following code snippets establish this indirect relationship between <code>Order</code> and <code>Product</code>.</p>
<p><strong><code>OrderProduct</code></strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>    order <span style="color:#f92672">=</span> relationship(<span style="color:#e6db74">&#34;Order&#34;</span>, back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;products&#34;</span>)
</span></span><span style="display:flex;"><span>    product <span style="color:#f92672">=</span> relationship(<span style="color:#e6db74">&#34;Product&#34;</span>, back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;orders&#34;</span>)
</span></span></code></pre></div><p><strong><code>Order</code></strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>products <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;OrderProduct&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;order&#34;</span>,
</span></span><span style="display:flex;"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span></code></pre></div><p><strong><code>Product</code></strong>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>orders <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;OrderProduct&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;product&#34;</span>,
</span></span><span style="display:flex;"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span></code></pre></div><p><strong>For the <code>OrderProduct</code> Class, this code:</strong></p>
<ul>
<li>
<p>Creates a relationship called <code>order</code>, that points <strong>towards</strong> the <code>Order</code> class. This now establishes a many-to-one relationship between <code>OrderProduct</code> and <code>Order</code>. Each instance of <code>OrderProduct</code> references exactly <strong>one</strong> <code>Order</code>.</p>
</li>
<li>
<p>Creates a relationship called <code>product</code>, that points <strong>towards</strong> the <code>Product</code> class. This now establishes a many-to-one relationship between <code>OrderProduct</code> and <code>Product</code>. Each instance of <code>OrderProduct</code> references exactly <strong>one</strong> <code>Product</code>.</p>
</li>
</ul>
<p><strong>For the <code>Order</code> Class, this code:</strong></p>
<ul>
<li>Creates a relationship called <code>products</code>, that points <strong>towards</strong> the <code>OrderProduct</code> class. This now establishes a one-to-many relationship between <code>Order</code> and <code>OrderProducts</code>. A single <code>Order</code> can be referenced by <strong>multiple</strong> instances of <code>OrderProduct</code>.</li>
</ul>

    <div class="admonition tip">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M272 384c9.6-31.9 29.5-59.1 49.2-86.2c0 0 0 0 0 0c5.2-7.1 10.4-14.2 15.4-21.4c19.8-28.5 31.4-63 31.4-100.3C368 78.8 289.2 0 192 0S16 78.8 16 176c0 37.3 11.6 71.9 31.4 100.3c5 7.2 10.2 14.3 15.4 21.4c0 0 0 0 0 0c19.8 27.1 39.7 54.4 49.2 86.2l160 0zM192 512c44.2 0 80-35.8 80-80l0-16-160 0 0 16c0 44.2 35.8 80 80 80zM112 176c0 8.8-7.2 16-16 16s-16-7.2-16-16c0-61.9 50.1-112 112-112c8.8 0 16 7.2 16 16s-7.2 16-16 16c-44.2 0-80 35.8-80 80z"/></svg>
        <span>Tip</span>
      </div>
      <div class="admonition-content">
        <p>Even though this relationship points to <code>OrderProduct</code>, it is named <code>products</code>.
This is intentional, and it refers to the conceptual representation of this relationship, not the literal schema itself.</p>
<ul>
<li>Conceptually:
An order has <strong>multiple products.</strong></li>
<li>Relationally:
An <code>Order</code> can be referenced by multiple <code>OrderProduct</code> rows.</li>
</ul>
<p>This relationship is named <code>products</code> to reflect the <em>conceptual</em> link between orders and products, and not the direct <em>relational</em> link between <code>Orders</code> and <code>OrderProducts</code>.</p>
<p>Remember that there is no <strong>direct</strong> connection between <code>Order</code> and <code>Product</code> in the data model!</p>
      </div>
    </div><p><strong>For the <code>Product</code> Class, this code:</strong></p>
<ul>
<li>
<p>Creates a relationship called <code>orders</code>, that points <strong>towards</strong> the <code>OrderProduct</code> class. This now establishes a one-to-many relationship between <code>Product</code> and <code>OrderProducts</code>. A single <code>Product</code> can be referenced by <strong>multiple</strong> instances of <code>OrderProduct</code>.</p>
</li>
<li>
<p>Similar to the <code>products</code> relationship defined in <code>Order</code>, this is a conceptual representation, which is why it is called <code>orders</code>.</p>
</li>
</ul>
<p>By wiring these connections, we abstract away the many-to-many relationship between <code>Orders</code> and <code>Products</code>.</p>

    <div class="admonition tip">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M272 384c9.6-31.9 29.5-59.1 49.2-86.2c0 0 0 0 0 0c5.2-7.1 10.4-14.2 15.4-21.4c19.8-28.5 31.4-63 31.4-100.3C368 78.8 289.2 0 192 0S16 78.8 16 176c0 37.3 11.6 71.9 31.4 100.3c5 7.2 10.2 14.3 15.4 21.4c0 0 0 0 0 0c19.8 27.1 39.7 54.4 49.2 86.2l160 0zM192 512c44.2 0 80-35.8 80-80l0-16-160 0 0 16c0 44.2 35.8 80 80 80zM112 176c0 8.8-7.2 16-16 16s-16-7.2-16-16c0-61.9 50.1-112 112-112c8.8 0 16 7.2 16 16s-7.2 16-16 16c-44.2 0-80 35.8-80 80z"/></svg>
        <span>Tip</span>
      </div>
      <div class="admonition-content">
        <p>The <code>back_populates</code> parameter establishes a bidirectional link between the two ORM relationships. It ensures that objects on both sides of each relationship synchronize in-Python state changes.</p>
      </div>
    </div><h1 id="auto-generating-migrations">Auto-generating migrations</h1>
<p>We now have a end state of our data model defined in the ORM. To generate a migration, we need to allow Alembic to compare the current state of the database against the ORM, and have it automatically generate a migration script that we <strong>must review</strong> before applying.</p>
<p>We configured and set up Alembic in the previous blog post, so this time, we just need to run the following command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic revision --autogenerate -m <span style="color:#e6db74">&#34;Created OrderProduct as an association table to track Order-Product association.&#34;</span>
</span></span></code></pre></div><p>This should autogenerate a migration script, located at <code>/path_to_your_project/alembic/versions/960969e75871_created_orderproduct_as_an_association_.py</code>. The <code>versions</code> directory will have the following content:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic/versions/
</span></span><span style="display:flex;"><span>└── a0cf7f5d705d_initial_commit_created_customers_orders_.py
</span></span><span style="display:flex;"><span>└── 960969e75871_created_orderproduct_as_an_association_.py
</span></span></code></pre></div><p>The <code>versions</code> directory now contains two migration scripts! One is the migration script for our initial commit in the previous blog post, and one is the newly generated script for creating the OrderProducts table.</p>

    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>In the previous blog post, the initial commit migration script had the ID <code>de5f32c11361</code>, and not <code>a0cf7f5d705d</code>.</p>
<p>The reason for this change is that I created a new revision script for the initial migration, and the revision ID is randomly generated!</p>
<p>Other aspects of the migration script are otherwise exactly the same, and the scripts are otherwise functionally identical.</p>
      </div>
    </div><p>The newly generated migration should have the following content:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;Created OrderProduct as an association table to track Order-Product association.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Revision ID: 960969e75871
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Revises: a0cf7f5d705d
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Create Date: 2026-01-26 17:30:18.314422
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> Sequence, Union
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic <span style="color:#f92672">import</span> op
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> sqlalchemy <span style="color:#66d9ef">as</span> sa
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># revision identifiers, used by Alembic.</span>
</span></span><span style="display:flex;"><span>revision: str <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;960969e75871&#39;</span>
</span></span><span style="display:flex;"><span>down_revision: Union[str, <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;a0cf7f5d705d&#39;</span>
</span></span><span style="display:flex;"><span>branch_labels: Union[str, Sequence[str], <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>depends_on: Union[str, Sequence[str], <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</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">def</span> <span style="color:#a6e22e">upgrade</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### commands auto generated by Alembic - please adjust! ###</span>
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>create_table(<span style="color:#e6db74">&#39;order_products&#39;</span>,
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;order_id&#39;</span>, sa<span style="color:#f92672">.</span>Integer(), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;product_id&#39;</span>, sa<span style="color:#f92672">.</span>Integer(), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;product_count&#39;</span>, sa<span style="color:#f92672">.</span>Integer(), server_default<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;1&#39;</span>, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>ForeignKeyConstraint([<span style="color:#e6db74">&#39;order_id&#39;</span>], [<span style="color:#e6db74">&#39;orders.order_id&#39;</span>], ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;CASCADE&#39;</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>ForeignKeyConstraint([<span style="color:#e6db74">&#39;product_id&#39;</span>], [<span style="color:#e6db74">&#39;products.product_id&#39;</span>], ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;CASCADE&#39;</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>PrimaryKeyConstraint(<span style="color:#e6db74">&#39;order_id&#39;</span>, <span style="color:#e6db74">&#39;product_id&#39;</span>)
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### end Alembic commands ###</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">def</span> <span style="color:#a6e22e">downgrade</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### commands auto generated by Alembic - please adjust! ###</span>
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>drop_table(<span style="color:#e6db74">&#39;order_products&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### end Alembic commands ###</span>
</span></span></code></pre></div><h1 id="reviewing-the-auto-generated-migration">Reviewing the auto-generated migration</h1>
<p>Remember that Alembic&rsquo;s documentation states,</p>
<blockquote>
<p>It is <strong>always</strong> necessary to manually review and correct the <strong>candidate migrations</strong> that autogenerate produces.</p>
</blockquote>
<p>Reviewing our migration script, it looks like:</p>
<ul>
<li>Alembic has created a table called <code>order_products</code>.</li>
<li><code>order_id</code> and <code>product_id</code> are both <strong>foreign keys</strong>. They are the primary keys from the tables <code>orders</code> and <code>products</code> respectively.</li>
<li><code>order_id</code> and <code>product_id</code> are also <strong>primary keys</strong> for the <code>order_products</code> table.</li>
<li><code>order_products</code> has a column called <code>product_count</code>, with a default value of 1.</li>
</ul>

    <div class="admonition tip">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M272 384c9.6-31.9 29.5-59.1 49.2-86.2c0 0 0 0 0 0c5.2-7.1 10.4-14.2 15.4-21.4c19.8-28.5 31.4-63 31.4-100.3C368 78.8 289.2 0 192 0S16 78.8 16 176c0 37.3 11.6 71.9 31.4 100.3c5 7.2 10.2 14.3 15.4 21.4c0 0 0 0 0 0c19.8 27.1 39.7 54.4 49.2 86.2l160 0zM192 512c44.2 0 80-35.8 80-80l0-16-160 0 0 16c0 44.2 35.8 80 80 80zM112 176c0 8.8-7.2 16-16 16s-16-7.2-16-16c0-61.9 50.1-112 112-112c8.8 0 16 7.2 16 16s-7.2 16-16 16c-44.2 0-80 35.8-80 80z"/></svg>
        <span>Tip</span>
      </div>
      <div class="admonition-content">
        <p><code>order_id</code> and <code>product_id</code> are both selected as primary keys to ensure that duplicate rows won’t be persisted within the <code>order_products</code> regardless of issues on the application side.</p>
      </div>
    </div><p>This migration script looks good! We can go ahead and apply it to the database.</p>

    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Why does the migration script look so sparse compared to the complex ORM relationships?</span>
      </div>
      <div class="admonition-content">
        <p><strong><code>Order</code></strong>:</p>
<pre tabindex="0"><code>products = relationship(
        &#34;OrderProduct&#34;,
        back_populates=&#34;order&#34;,
        cascade=&#34;all, delete-orphan&#34;,
    )
</code></pre><p>This code defines the behavior within the <strong>ORM</strong>, and will become relevant when you interact with the database via SQLAlchemy.
It defines the behaviour of these classes within Python and also documents the relationship on a conceptual level.
It <strong>doesn&rsquo;t create anything within the database</strong>.</p>
<p>On the database level, it is really straightforward to build this relationship,
which is why the migration script seems so sparse in comparison.
Within the migration script, on the database level, we&rsquo;re just creating <code>order_products</code>
as a table with foreign keys pointing to the parent entity tables.
The relational aspect of the data model is much lower level than the conceptual data model, which is what we define in the ORM.</p>
<p>The ORM helps us build a conceptual data model with rich, complex relationships as seen in our code.
Alembic reads the ORM and generates migrations to create a lower-level relational data model <strong>from</strong> the ORM.</p>
      </div>
    </div><h1 id="applying-the-migration">Applying the migration</h1>
<p>We can then apply the migration script with the command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic upgrade head
</span></span></code></pre></div><p>This should give us the following output:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>INFO  <span style="color:#f92672">[</span>alembic.runtime.migration<span style="color:#f92672">]</span> Context impl PostgresqlImpl.
</span></span><span style="display:flex;"><span>INFO  <span style="color:#f92672">[</span>alembic.runtime.migration<span style="color:#f92672">]</span> Will assume transactional DDL.
</span></span><span style="display:flex;"><span>INFO  <span style="color:#f92672">[</span>alembic.runtime.migration<span style="color:#f92672">]</span> Running upgrade a0cf7f5d705d -&gt; 960969e75871, Created OrderProduct as an association table to track Order-Product association.
</span></span></code></pre></div><p>Connecting to the database shows the following ERD:</p>
<figure class="align-center ">
    <img loading="lazy" src="erd_diagram_orderproducts.png#center"
         alt="Our newly iterated data model!"/> <figcaption>
            <p>Our newly iterated data model!</p>
        </figcaption>
</figure>

<p>We&rsquo;ve successfully created the association table, and we have built an indirect <strong>many-to-many</strong> association between <code>Orders</code> and <code>Products</code>! When we examine the contents of the <code>alembic_version</code> table, it now contains a single row with the value <code>960969e75871</code>, which is the revision it has applied. It applied this one specifically because we had alembic upgrade to the <code>head</code> version, which is the latest version.</p>
<h1 id="committing-to-github">Committing to GitHub</h1>
<p>At this point, we&rsquo;ve made changes to <code>models.py</code> and we have a new migration script called <code>960969e75871_created_orderproduct_as_an_association_.py</code>. Both these changes need to be pushed GitHub. I recommend committing and pushing these changes as a single commit with the same commit message that we used for auto-generating a migration script.</p>
<p>Remember to always commit migration scripts <strong>together with</strong> model changes.</p>
<h1 id="further-iterating">Further iterating</h1>
<p>Let&rsquo;s expand this data model more. A great addition to this data model would be the ability to create and assign tags to products for greater searchability.</p>
<p>To start with, these tags will be really simple. A tag will be a text-based descriptor that can be applied to any product. For example, a laptop as a product might have a tag called &lsquo;computer&rsquo; assigned to it.</p>
<p>To reflect this, we will create a table containing tags, and build a <strong>many-to-many</strong> relationship between products and tags. Each product can have multiple tags, and each tag can be assigned to multiple products. For this, like before, we will build an association table.</p>
<p>We will do the following:</p>
<ul>
<li>Create a class for <code>Tags</code> in the ORM</li>
<li>Create an association table called <code>product_tags</code> to associate products and tags</li>
<li>Update the relationships for <code>Products</code> to reflect the new relationship to the <code>Tags</code> class.</li>
</ul>

    <div class="admonition tip">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path d="M272 384c9.6-31.9 29.5-59.1 49.2-86.2c0 0 0 0 0 0c5.2-7.1 10.4-14.2 15.4-21.4c19.8-28.5 31.4-63 31.4-100.3C368 78.8 289.2 0 192 0S16 78.8 16 176c0 37.3 11.6 71.9 31.4 100.3c5 7.2 10.2 14.3 15.4 21.4c0 0 0 0 0 0c19.8 27.1 39.7 54.4 49.2 86.2l160 0zM192 512c44.2 0 80-35.8 80-80l0-16-160 0 0 16c0 44.2 35.8 80 80 80zM112 176c0 8.8-7.2 16-16 16s-16-7.2-16-16c0-61.9 50.1-112 112-112c8.8 0 16 7.2 16 16s-7.2 16-16 16c-44.2 0-80 35.8-80 80z"/></svg>
        <span>Tip</span>
      </div>
      <div class="admonition-content">
        <p>In the interest of brevity, this section won&rsquo;t be as thoroughly explanatory as the section for creating <code>OrderProducts</code>.
This process is very similar to what we&rsquo;ve covered thus far!</p>
      </div>
    </div><h2 id="creating-a-tags-class">Creating a <code>Tags</code> class</h2>
<p><strong><code>models.py</code></strong> (Changes to code have been highlighted):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> (
</span></span><span style="display:flex;"><span>    Column,
</span></span><span style="display:flex;"><span>    Integer,
</span></span><span style="display:flex;"><span>    String,
</span></span><span style="display:flex;"><span>    Float,
</span></span><span style="display:flex;"><span>    ForeignKey,
</span></span><span style="display:flex;"><span>    Identity,
</span></span><span style="display:flex;"><span>    Text,
</span></span><span style="display:flex;"><span>    DateTime,
</span></span><span style="display:flex; background-color:#3c3d38"><span>    Table,
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy.orm <span style="color:#f92672">import</span> declarative_base, relationship
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy.sql <span style="color:#f92672">import</span> func
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Base <span style="color:#f92672">=</span> declarative_base()
</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">class</span> <span style="color:#a6e22e">Customer</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;customers&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    customer_id <span style="color:#f92672">=</span> Column(Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    first_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    last_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    email_address <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    orders <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Order&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;customer&#34;</span>,
</span></span><span style="display:flex;"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</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 style="color:#66d9ef">class</span> <span style="color:#a6e22e">OrderProduct</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;order_products&#34;</span>
</span></span><span style="display:flex;"><span>    order_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer,
</span></span><span style="display:flex;"><span>        ForeignKey(<span style="color:#e6db74">&#34;orders.order_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>),
</span></span><span style="display:flex;"><span>        primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    product_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer,
</span></span><span style="display:flex;"><span>        ForeignKey(<span style="color:#e6db74">&#34;products.product_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>),
</span></span><span style="display:flex;"><span>        primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    product_count <span style="color:#f92672">=</span> Column(Integer, server_default<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;1&#34;</span>, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    order <span style="color:#f92672">=</span> relationship(<span style="color:#e6db74">&#34;Order&#34;</span>, back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;products&#34;</span>)
</span></span><span style="display:flex;"><span>    product <span style="color:#f92672">=</span> relationship(<span style="color:#e6db74">&#34;Product&#34;</span>, back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;orders&#34;</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">class</span> <span style="color:#a6e22e">Order</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;orders&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    order_id <span style="color:#f92672">=</span> Column(Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    customer_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer, ForeignKey(<span style="color:#e6db74">&#34;customers.customer_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    order_date <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        DateTime(timezone<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>),
</span></span><span style="display:flex;"><span>        server_default<span style="color:#f92672">=</span>func<span style="color:#f92672">.</span>now(),
</span></span><span style="display:flex;"><span>        nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    customer <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Customer&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;orders&#34;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    products <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;OrderProduct&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;order&#34;</span>,
</span></span><span style="display:flex;"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</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; background-color:#3c3d38"><span>product_tags <span style="color:#f92672">=</span> Table(
</span></span><span style="display:flex; background-color:#3c3d38"><span>    <span style="color:#e6db74">&#34;product_tags&#34;</span>,
</span></span><span style="display:flex; background-color:#3c3d38"><span>    Base<span style="color:#f92672">.</span>metadata,
</span></span><span style="display:flex; background-color:#3c3d38"><span>    Column(
</span></span><span style="display:flex; background-color:#3c3d38"><span>        <span style="color:#e6db74">&#34;product_id&#34;</span>,
</span></span><span style="display:flex; background-color:#3c3d38"><span>        ForeignKey(<span style="color:#e6db74">&#34;products.product_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>),
</span></span><span style="display:flex; background-color:#3c3d38"><span>        primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex; background-color:#3c3d38"><span>    ),
</span></span><span style="display:flex; background-color:#3c3d38"><span>    Column(<span style="color:#e6db74">&#34;tag_id&#34;</span>, ForeignKey(<span style="color:#e6db74">&#34;tags.tag_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>),
</span></span><span style="display:flex; background-color:#3c3d38"><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">class</span> <span style="color:#a6e22e">Product</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;products&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    product_id <span style="color:#f92672">=</span> Column(Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex;"><span>    product_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    description <span style="color:#f92672">=</span> Column(Text)
</span></span><span style="display:flex;"><span>    price <span style="color:#f92672">=</span> Column(Float, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    orders <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;OrderProduct&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;product&#34;</span>,
</span></span><span style="display:flex;"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex; background-color:#3c3d38"><span>    tags <span style="color:#f92672">=</span> relationship(<span style="color:#e6db74">&#34;Tag&#34;</span>, secondary<span style="color:#f92672">=</span>product_tags, back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;products&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex; background-color:#3c3d38"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Tag</span>(Base):
</span></span><span style="display:flex; background-color:#3c3d38"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;tags&#34;</span>
</span></span><span style="display:flex; background-color:#3c3d38"><span>
</span></span><span style="display:flex; background-color:#3c3d38"><span>    tag_id <span style="color:#f92672">=</span> Column(Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex; background-color:#3c3d38"><span>    name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>, unique<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>)
</span></span><span style="display:flex; background-color:#3c3d38"><span>    products <span style="color:#f92672">=</span> relationship(<span style="color:#e6db74">&#34;Product&#34;</span>, secondary<span style="color:#f92672">=</span>product_tags, back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;tags&#34;</span>)
</span></span></code></pre></div>
    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>The <code>product_tags</code> implementation here is different from the <code>OrderProducts</code> class.</p>
<p>This is because <code>product_tags</code> is a straightforward, simple association table,
which is what the documentation for SQLAlchemy says to use when your association table has no metadata.</p>
<p>This <code>secondary</code> table pattern is only usable when all you have in the table are foreign keys.
<code>product_tags</code> has no attributes apart from its foreign keys, so we use the association table with the <code>secondary</code> table pattern.</p>
<p>The reason <code>OrderProducts</code> was mapped as a class was because we have additional metadata beyond just the foreign keys in that table.</p>
<p><code>OrderProducts</code> goes from just a simple association table to a full entity in it&rsquo;s own right when you add even a single extra column beyond the foreign keys.
The recommended approach for adding metadata to an association table is to map the association table directly to a full class.</p>
      </div>
    </div><h2 id="generating-a-migration">Generating a migration</h2>
<p>We create an auto-generated migration script with the following command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic revision --autogenerate -m <span style="color:#e6db74">&#34;Created a table for tags, and created a table called product_tags as an association table between products and tags.&#34;</span>
</span></span></code></pre></div><h2 id="reviewing-the-migration-script">Reviewing the migration script</h2>
<p>The contents of the migration script are:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;Created a table for tags, and created a table called product_tags as an association table between products and tags.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Revision ID: 6e58a26e5c70
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Revises: 960969e75871
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Create Date: 2026-01-26 18:32:43.007353
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> Sequence, Union
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic <span style="color:#f92672">import</span> op
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> sqlalchemy <span style="color:#66d9ef">as</span> sa
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># revision identifiers, used by Alembic.</span>
</span></span><span style="display:flex;"><span>revision: str <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;6e58a26e5c70&#39;</span>
</span></span><span style="display:flex;"><span>down_revision: Union[str, <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;960969e75871&#39;</span>
</span></span><span style="display:flex;"><span>branch_labels: Union[str, Sequence[str], <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>depends_on: Union[str, Sequence[str], <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</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">def</span> <span style="color:#a6e22e">upgrade</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### commands auto generated by Alembic - please adjust! ###</span>
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>create_table(<span style="color:#e6db74">&#39;tags&#39;</span>,
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;tag_id&#39;</span>, sa<span style="color:#f92672">.</span>Integer(), sa<span style="color:#f92672">.</span>Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;name&#39;</span>, sa<span style="color:#f92672">.</span>String(), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>PrimaryKeyConstraint(<span style="color:#e6db74">&#39;tag_id&#39;</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>UniqueConstraint(<span style="color:#e6db74">&#39;name&#39;</span>)
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>create_table(<span style="color:#e6db74">&#39;product_tags&#39;</span>,
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;product_id&#39;</span>, sa<span style="color:#f92672">.</span>Integer(), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;tag_id&#39;</span>, sa<span style="color:#f92672">.</span>Integer(), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>ForeignKeyConstraint([<span style="color:#e6db74">&#39;product_id&#39;</span>], [<span style="color:#e6db74">&#39;products.product_id&#39;</span>], ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;CASCADE&#39;</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>ForeignKeyConstraint([<span style="color:#e6db74">&#39;tag_id&#39;</span>], [<span style="color:#e6db74">&#39;tags.tag_id&#39;</span>], ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;CASCADE&#39;</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>PrimaryKeyConstraint(<span style="color:#e6db74">&#39;product_id&#39;</span>, <span style="color:#e6db74">&#39;tag_id&#39;</span>)
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### end Alembic commands ###</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">def</span> <span style="color:#a6e22e">downgrade</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### commands auto generated by Alembic - please adjust! ###</span>
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>drop_table(<span style="color:#e6db74">&#39;product_tags&#39;</span>)
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>drop_table(<span style="color:#e6db74">&#39;tags&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### end Alembic commands ###</span>
</span></span></code></pre></div><p>This migration script (with the revision ID <code>6e58a26e5c70</code>) looks good!</p>
<p>You should now have three scripts inside <code>alembic/versions</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;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic/versions/
</span></span><span style="display:flex;"><span>├── 6e58a26e5c70_created_a_table_for_tags_and_created_a_.py
</span></span><span style="display:flex;"><span>├── 960969e75871_created_orderproduct_as_an_association_.py
</span></span><span style="display:flex;"><span>└── a0cf7f5d705d_initial_commit_created_customers_orders_.py
</span></span></code></pre></div><h2 id="applying-the-migration-script">Applying the migration script</h2>
<p>Apply the migration script by running:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic upgrade head
</span></span></code></pre></div><p>Connecting to the database shows the following ERD:</p>
<figure class="align-center ">
    <img loading="lazy" src="erd_diagram_producttags.png#center"
         alt="Tags are now associated with Products!"/> <figcaption>
            <p>Tags are now associated with Products!</p>
        </figcaption>
</figure>

<p>We&rsquo;ve successfully built a connection between Products and Tags via an association table!</p>
<p>Browsing through <code>alembic/versions</code> should give us an idea of the growth of our data model and how it evolves to match the change in scope of our e-commerce workflow. With these three migration scripts, we&rsquo;ve gone from a rudimentary data model to a more complex one, while being able to track how the data model has evolved from its conception.</p>
<h1 id="next-steps">Next Steps</h1>
<p>In subsequent blog posts, we will cover:</p>
<ul>
<li>Using an Async engine for asynchronous database connections</li>
<li>Using the <code>alembic_utils</code> library to incorporate views, functions and triggers into the version control system.</li>
<li>Configuring Alembic to connect to a production environment, and seamlessly bring the production environment&rsquo;s schema up-to-date with our final revision.</li>
</ul>
<h1 id="references">References</h1>
<ul>
<li>SQLAlchemy&rsquo;s <a href="https://docs.sqlalchemy.org/en/14/orm/basic_relationships.html#many-to-many">documentation on many-to-many relationships.</a></li>
<li>Alembic&rsquo;s <a href="https://alembic.sqlalchemy.org/en/latest/autogenerate.html#what-does-autogenerate-detect-and-what-does-it-not-detect">documentation on auto-generating migrations.</a></li>
</ul>
]]></content:encoded></item><item><title>Relational Data Model design with SQLAlchemy.</title><link>https://naveenkannan.dev/posts/sqlalchemy_data_modeling/</link><pubDate>Tue, 13 Jan 2026 00:00:00 +0000</pubDate><guid>https://naveenkannan.dev/posts/sqlalchemy_data_modeling/</guid><description>An introduction to using SQLAlchemy and Alembic for relational data model design.</description><content:encoded><![CDATA[<h1 id="introduction">Introduction</h1>
<h2 id="data-modeling">Data Modeling</h2>
<p>Data modeling is the process of creating a representation of data that defines the way it is structured and used in complex systems. When it comes to relational databases, designing a normalized data model is essential for efficient database operation, and to make your data consistent and clean.</p>
<p>With a fully normalized data model, data in your relational database becomes:</p>
<ul>
<li>Understandable</li>
</ul>
<p>A good data model can decompose the complexity of real-world systems and the data they generate into a relational model that is easy to read and comprehend, and standardizes the relationship of one data domain with every other data domain.</p>
<ul>
<li>Scalable</li>
</ul>
<p>A well defined and <strong>normalized</strong> data model becomes scalable, and allows for sustainable development of data-driven solutions for users of your database as the scope of your data grows.</p>
<ul>
<li>Modular</li>
</ul>
<p>A database with a well designed and thoroughly documented data model becomes easy to use by downstream teams such as API designers and front-end developers, minimizing the communication needed for these teams to function independently. Good design will naturally flow downstream of a properly normalized database.</p>
<p>Efficient data model design can help your database be a bedrock for your fellow team members to rely on.</p>
<h2 id="sqlalchemy">SQLAlchemy</h2>
<p><a href="https://www.sqlalchemy.org">SQLAlchemy</a> is a Python SQL toolkit and ORM (Object Relational Mapper) that allows Python developers to work with relational databases using the Python language. SQLAlchemy is indispensable if you&rsquo;re a Python developer looking to create a relational data model for your database. The two most significant components of SQLAlchemy are it&rsquo;s <strong>Object Relational Mapper (ORM)</strong> and <strong>SQLAlchemy Core</strong>.</p>
<ul>
<li>SQLAlchemy Core</li>
</ul>
<p>SQLALchemy Core is the foundation of SQLAlchemy, and is independent of the ORM. It provides the connectivity to the database, and by allowing for the construction of SQL expressions that can then be executed against a target database, enabling interaction with a database to in the form of queries.</p>
<ul>
<li>SQLAlchemy ORM</li>
</ul>
<p>The SQLAlchemy ORM (Object Relational Mapper) uses Core as a foundation. The ORM will be the bulk of what we discuss in this blog post. The ORM builds on Core to help create user-defined Python classes that can be mapped into database tables. The ORM is what allows SQLAlchemy to define a data model for a relational database.</p>
<figure class="align-center ">
    <img loading="lazy" src="https://docs.sqlalchemy.org/en/20/_images/sqla_arch_small.png#center"
         alt="The major components of SQLAlchemy."/> <figcaption>
            <p>The major components of SQLAlchemy.Taken from the <a href="https://docs.sqlalchemy.org/en/20/intro.html">SQLAlchemy documentation.</a></p>
        </figcaption>
</figure>

<h3 id="alembic">Alembic</h3>
<p><a href="https://alembic.sqlalchemy.org/en/latest/index.html">Alembic</a> is a database migration tool written by the author of SQLAlchemy. It helps create a version-controlled system of database migrations that can be used to upgrade a target database to any version of your data model. It uses SQLAlchemy as the underlying engine. By executing migrations in a sequential order, databases can be upgraded and downgraded across versions, even when you are building from scratch.</p>
<p>Alembic is a great way to test your migrations and schema design in a development environment before transferring these migrations to a production environment. An empty database environment can be converted into a database with a production ready schema with just a single command using Alembic.</p>
<h2 id="what-well-cover">What we&rsquo;ll cover</h2>
<p>In this blog post, we will cover how you can begin using SQLAlchemy and Alembic to start developing the first iteration of your data model.</p>
<p>We will do the following:</p>
<ul>
<li>Create a virtual environment with SQLAlchemy and Alembic installed</li>
<li>Initialize Alembic</li>
<li>Design a simple, rudimentary data model with three tables</li>
<li>Configure Alembic to connect to your database</li>
<li>Utilize Alembic to generate a migration script</li>
<li>Migrate your initial data model to the database for the first time</li>
</ul>
<p>In future posts, we will cover how you can use Alembic to generate database migrations and help grow your database within a version-controlled system, and how this data model can then be plugged into a FastAPI server as a module to help design a modern API service.</p>
<h1 id="prerequisites">Prerequisites</h1>
<p>The code in this blog post was written with Python 3.13.1, Alembic 1.14.0 and SQLAlchemy 2.0.36.</p>
<p>You will need the following prerequisites:</p>
<ul>
<li>
<p>A PostgreSQL server initialized and running, with a database that has been initialized but is otherwise empty. We will refer to this server as <code>development</code> from here.</p>
</li>
<li>
<p>An installation of <code>mamba</code>,<code>micromamba</code> or <code>conda</code> as a package manager to create a new environment. I like to use <code>micromamba</code>. <a href="https://mamba.readthedocs.io/en/latest/installation/micromamba-installation.html">You can find instructions on how to install <code>micromamba</code> here.</a></p>
</li>
</ul>
<p>We will create a new virtual environment using Python 3.13.1 as the pinned version. Create an environment named <code>data_model</code> as follows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>micromamba create <span style="color:#f92672">-</span>n data_model python<span style="color:#f92672">=</span><span style="color:#ae81ff">3.13.1</span> sqlalchemy<span style="color:#f92672">=</span><span style="color:#ae81ff">2.0.36</span> alembic<span style="color:#f92672">=</span><span style="color:#ae81ff">1.14.0</span> psycopg2<span style="color:#f92672">=</span><span style="color:#ae81ff">2.9.9</span> <span style="color:#f92672">-</span>y
</span></span></code></pre></div><p>This will create a micromamba environment with the pinned versions of Python and SQLAlchemy that can then be used to follow this tutorial. This environment can be activated with the command <code>micromamba activate data_model</code>.</p>

    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>Using a virtual environment is a good way to keep your base environment uncluttered and free of broken dependencies.
It is good practice to begin any Python project with a virtual environment, even for development workflows.
Other options for virtual environment creation include <code>uv</code> and <code>Poetry</code>.</p>
      </div>
    </div><p>Lets now get into actually creating some data models!</p>
<h1 id="getting-started">Getting Started</h1>
<h2 id="setting-up-the-alembic-environment">Setting up the Alembic environment</h2>
<p>Before we can start working with our PostgreSQL database, we will first set up the migration environment using Alembic. The reason we set this up first is because we will use Alembic&rsquo;s config files to connect to our database first before we can begin creating our data model. Alembic uses SQLAlchemy core&rsquo;s <code>Engine</code> object as a dependency when connecting to a database.</p>
<p>In the root of your project directory, run:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>alembic init alembic
</span></span></code></pre></div><p>This will generate a directory of scripts. The root of your project directory will look like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>.
</span></span><span style="display:flex;"><span>├── alembic
</span></span><span style="display:flex;"><span>│   ├── README
</span></span><span style="display:flex;"><span>│   ├── env.py
</span></span><span style="display:flex;"><span>│   ├── script.py.mako
</span></span><span style="display:flex;"><span>│   └── versions
</span></span><span style="display:flex;"><span>└── alembic.ini
</span></span></code></pre></div><p>The directory now includes these files of importance:</p>
<h3 id="alembicini"><code>alembic.ini</code></h3>
<p>This is Alembic&rsquo;s main configuration file. When Alembic is run, it looks in the directory for this file. This file is where you will add the SQLAlchemy URL to connect to your database. You can also define <strong>multiple</strong> environments to run migrations in.</p>
<p>For now we will go with the boilerplate file generated by the alembic init file. Here is what the default file should look like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#75715e"># A generic, single database configuration.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[alembic]</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># path to migration scripts</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Use forward slashes (/) also on windows to provide an os agnostic path</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">script_location</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">alembic</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># template used to generate migration file names; The default value is %%(rev)s_%%(slug)s</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Uncomment the line below if you want the files to be prepended with date and time</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># for all available tokens</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># sys.path path, will be prepended to sys.path if present.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># defaults to the current working directory.</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">prepend_sys_path</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># timezone to use when rendering the date within the migration file</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># as well as the filename.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># If specified, requires the python&gt;=3.9 or backports.zoneinfo library.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Any required deps can installed by adding `alembic[tz]` to the pip requirements</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># string value is passed to ZoneInfo()</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># leave blank for localtime</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># timezone =</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># max length of characters to apply to the &#34;slug&#34; field</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># truncate_slug_length = 40</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># set to &#39;true&#39; to run the environment during</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># the &#39;revision&#39; command, regardless of autogenerate</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># revision_environment = false</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># set to &#39;true&#39; to allow .pyc and .pyo files without</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># a source .py file to be detected as revisions in the</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># versions/ directory</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># sourceless = false</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># version location specification; This defaults</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># to alembic/versions.  When using multiple version</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># directories, initial revisions must be specified with --version-path.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># The path separator used here should be the separator specified by &#34;version_path_separator&#34; below.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># version_locations = %(here)s/bar:%(here)s/bat:alembic/versions</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># version path separator; As mentioned above, this is the character used to split</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># version_locations. The default within new alembic.ini files is &#34;os&#34;, which uses os.pathsep.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Valid values for version_path_separator are:</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># version_path_separator = :</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># version_path_separator = ;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># version_path_separator = space</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># version_path_separator = newline</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">version_path_separator</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">os  # Use os.pathsep. Default configuration used for new projects.</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># set to &#39;true&#39; to search source files recursively</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># in each &#34;version_locations&#34; directory</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># new in Alembic version 1.10</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># recursive_version_locations = false</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># the output encoding used when revision files</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># are written from script.py.mako</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># output_encoding = utf-8</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">sqlalchemy.url</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">driver://user:pass@localhost/dbname</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">[post_write_hooks]</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># post_write_hooks defines scripts or Python functions that are run</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># on newly generated revision scripts.  See the documentation for further</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># detail and examples</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># format using &#34;black&#34; - use the console_scripts runner, against the &#34;black&#34; entrypoint</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># hooks = black</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># black.type = console_scripts</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># black.entrypoint = black</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># black.options = -l 79 REVISION_SCRIPT_FILENAME</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># lint with attempts to fix using &#34;ruff&#34; - use the exec runner, execute a binary</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># hooks = ruff</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ruff.type = exec</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ruff.executable = %(here)s/.venv/bin/ruff</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ruff.options = --fix REVISION_SCRIPT_FILENAME</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Logging configuration</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[loggers]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">keys</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">root,sqlalchemy,alembic</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[handlers]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">keys</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">console</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[formatters]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">keys</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">generic</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[logger_root]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">level</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">WARNING</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">handlers</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">console</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">qualname</span> <span style="color:#f92672">=</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[logger_sqlalchemy]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">level</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">WARNING</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">handlers</span> <span style="color:#f92672">=</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">qualname</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">sqlalchemy.engine</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[logger_alembic]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">level</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">INFO</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">handlers</span> <span style="color:#f92672">=</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">qualname</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">alembic</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[handler_console]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">class</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">StreamHandler</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">args</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">(sys.stderr,)</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">level</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">NOTSET</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">formatter</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">generic</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[formatter_generic]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">format</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">%(levelname)-5.5s [%(name)s] %(message)s</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">datefmt</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">%H:%M:%S</span>
</span></span></code></pre></div><p>To begin with, we will substitute the <code>sqlalchemy.url</code> parameter for the <code>alembic</code> section with the connection string used for our database. As an example, the following is a dummy connection string.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#a6e22e">sqlalchemy.url</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">postgresql://nk_dev:placeholder_password@localhost/development</span>
</span></span></code></pre></div><p>In the above code snippet:</p>
<ul>
<li>The database driver is <code>postgresql</code></li>
<li>The database username is <code>nk_dev</code></li>
<li>The password of the user is <code>placeholder_password</code></li>
<li>The database host is <code>localhost</code></li>
<li>The name of the database is <code>development</code></li>
</ul>

    <div class="admonition warning">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M256 32c14.2 0 27.3 7.5 34.5 19.8l216 368c7.3 12.4 7.3 27.7 .2 40.1S486.3 480 472 480L40 480c-14.3 0-27.6-7.7-34.7-20.1s-7-27.8 .2-40.1l216-368C228.7 39.5 241.8 32 256 32zm0 128c-13.3 0-24 10.7-24 24l0 112c0 13.3 10.7 24 24 24s24-10.7 24-24l0-112c0-13.3-10.7-24-24-24zm32 224a32 32 0 1 0 -64 0 32 32 0 1 0 64 0z"/></svg>
        <span>Warning</span>
      </div>
      <div class="admonition-content">
        <p>The above is an example to begin testing with.
This isn&rsquo;t the ideal way to store your PostgreSQL server connection details.
Ideally you manage them with a key vault or via injection of environment variables at the time of virtual environment activation.
Remember to <strong>never</strong> include the alembic.ini file within version control. Always have your config file (<code>alembic.ini</code>) added to <code>.gitignore</code>.</p>
<p>Here are the contents of the <code>.gitignore:</code> file:</p>
<pre tabindex="0"><code>alembic.ini
</code></pre>
      </div>
    </div><p>A more <a href="https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file">in-depth explanation of the <code>alembic.ini</code> file can be found in the official documentation here.</a></p>
<h3 id="the-alembic-directory">The <code>alembic</code> directory</h3>
<p>This directory is the home of the migration environment. The relevant contents of this directory include:</p>
<ul>
<li><strong><code>env.py</code></strong></li>
</ul>
<p>This is a Python script that can be run when the migration tool is invoked. You can do some cool stuff with this script, like registering entities like views, functions and triggers, and also generating filters to ignore tables that you don&rsquo;t want to migrate with Alembic.</p>
<ul>
<li><strong><code>versions/</code></strong></li>
</ul>
<p>This directory holds the individual version scripts. This directory will have the scripts in it run sequentially when upgrading to or downgrading to a particular version of your database. As you begin generating migrations and applying them, this folder will grow. Each migration will become it&rsquo;s own script, containing both an <code>upgrade</code> and a <code>downgrade</code> parameter for that particular version.</p>
<p>Now that all of this is finally sorted, we can get to creating basic data models!</p>
<h2 id="creating-a-file-for-our-data-models">Creating a file for our data models</h2>
<p>Create a file in the project directory called <code>models.py</code>. This file will be where we store our user-defined Python classes that will then be mapped into database tables in the database using Alembic to run the actual migrations. Let&rsquo;s start with a simple scenario where we have commerce data for customers, products and orders. Each customer can submit multiple orders, and each order can have several products in it.</p>
<p>As a basic assumption, this initial data model isn&rsquo;t bad, but there is plenty of room for improvement and refinement in the future. Let&rsquo;s start with defining these three tables as Python classes first.</p>
<p>We will build a one-to-many relationship between customers and orders, since every customer can have many orders, and each order belongs to just one customer. We will build a table to define our products as well.</p>

    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>In future posts, we will expand on this rudimentary data model, building in connections between products and orders.
We will also incorporate additional features into our data model, such as views, functions and triggers.</p>
      </div>
    </div><p>This is what your <code>models.py</code> file will look like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> (
</span></span><span style="display:flex;"><span>    Column,
</span></span><span style="display:flex;"><span>    Integer,
</span></span><span style="display:flex;"><span>    String,
</span></span><span style="display:flex;"><span>    Float,
</span></span><span style="display:flex;"><span>    ForeignKey,
</span></span><span style="display:flex;"><span>    Identity,
</span></span><span style="display:flex;"><span>    Text,
</span></span><span style="display:flex;"><span>    DateTime,
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy.orm <span style="color:#f92672">import</span> declarative_base, relationship
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy.sql <span style="color:#f92672">import</span> func
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Base <span style="color:#f92672">=</span> declarative_base()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Customer</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;customers&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    customer_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    first_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    last_name <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>    email_address <span style="color:#f92672">=</span> Column(String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    orders <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Order&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;customer&#34;</span>,
</span></span><span style="display:flex;"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</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">class</span> <span style="color:#a6e22e">Order</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;orders&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    order_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    customer_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer,
</span></span><span style="display:flex;"><span>        ForeignKey(<span style="color:#e6db74">&#34;customers.customer_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>),
</span></span><span style="display:flex;"><span>        nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    order_date <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        DateTime(timezone<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>),
</span></span><span style="display:flex;"><span>        server_default<span style="color:#f92672">=</span>func<span style="color:#f92672">.</span>now(),
</span></span><span style="display:flex;"><span>        nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    customer <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Customer&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;orders&#34;</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">class</span> <span style="color:#a6e22e">Product</span>(Base):
</span></span><span style="display:flex;"><span>    __tablename__ <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;products&#34;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    product_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer, Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), primary_key<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    product_name <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        String, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    description <span style="color:#f92672">=</span> Column(Text)
</span></span><span style="display:flex;"><span>    price <span style="color:#f92672">=</span> Column(Float, nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>)
</span></span></code></pre></div><p>With these classes in <code>models.py</code>, We have created the following objects: <code>Customer</code>, <code>Order</code> and <code>Product</code>.</p>
<h3 id="customer"><code>Customer</code>:</h3>
<p>This object describes the attributes of the customers. The <code>customer_id</code> column is the primary key of the table, and we initialize it as a serialized integer. We have added some additional parameters to the column (which also apply to the primary keys of every other table).</p>
<ul>
<li><code>always=True</code> means the identity is always generated (even if a value is provided)</li>
</ul>
<p><code>first_name</code>,<code>last_name</code> and <code>email_address</code> are string columns that cannot be null, and need to be present in the data. This code snippet:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>    orders <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Order&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;customer&#34;</span>,
</span></span><span style="display:flex;"><span>        cascade<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;all, delete-orphan&#34;</span>,
</span></span><span style="display:flex;"><span>    )
</span></span></code></pre></div><p>establishes a one-to-many relationship between <code>customers</code> and <code>orders</code>.</p>
<p>The <code>cascade</code> behavior (<code>&quot;all&quot;, &quot;delete-orphan&quot;</code>) means that when a instance of <code>customer</code> is deleted, all the associated <code>orders</code> (Orders that used the ID of the deleted customer as a foreign key) will also be deleted, and these changes will cascade downwards to any future child table of the <code>orders</code> table.</p>
<h3 id="order"><code>Order</code>:</h3>
<p>This object describes the attributes of each individual order. <code>order_id</code> is the primary key of this table. We designate the <code>customer_id</code> column as a foreign key from the <code>customers</code> table with this code snippet:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>  customer_id <span style="color:#f92672">=</span> Column(
</span></span><span style="display:flex;"><span>        Integer,
</span></span><span style="display:flex;"><span>        ForeignKey(<span style="color:#e6db74">&#34;customers.customer_id&#34;</span>, ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;CASCADE&#34;</span>),
</span></span><span style="display:flex;"><span>        nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>
</span></span><span style="display:flex;"><span>    )
</span></span></code></pre></div><p>The foreign key enforces referential integrity, while <code>nullable=False</code> ensures that each order must be associated with a customer. Orders that are associated with non-existent customers, or have no customer at all, will not be possible.</p>
<p>We also establish a many-to-one relationship between <code>orders</code> and <code>customers</code> using the following code snippet:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>  customer <span style="color:#f92672">=</span> relationship(
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;Customer&#34;</span>,
</span></span><span style="display:flex;"><span>        back_populates<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;orders&#34;</span>
</span></span><span style="display:flex;"><span>    )
</span></span></code></pre></div><h3 id="product"><code>Product</code>:</h3>
<p>This object describes the attributes of each individual product. <code>product_id</code> is the primary key of this table, and this table does not have any relationships with any other tables yet. This data model is currently incomplete, but it is a great starting point at which to begin the development and migration of our data model.</p>
<p>With this file complete, our rudimentary data model has been initialized. Without migrating these changes over to the database, however, these will only live as Python classes, and not as database objects.</p>
<h2 id="doing-our-first-database-migration">Doing our first database migration.</h2>
<p>The vast majority of database migration workflows with Alembic are done by <strong>auto-generating</strong> migration scripts. To begin auto-generating migration scripts, we will first need to connect Alembic to the database, and then point alembic towards the target metadata we would like to migrate the data model to.</p>
<p>Alembic can be configured to connect to the database (pointed to be the <code>sqlalchemy.url</code> parameter in the <code>alembic.ini</code> file). For Alembic to compare the state of the database <em>against</em> our target metadata, we will edit <code>alembic/env.py</code> so that Alembic can access the table metadata object that contains the target. Configuring Alembic therefore has two components:</p>
<ul>
<li>Connecting Alembic to the database, <strong>so that it can compare the state of the database against the target metadata.</strong></li>
<li>Importing the target metadata, <strong>so that Alembic knows what to compare the state of the database against.</strong></li>
</ul>
<p>Once <code>alembic/env.py</code> is configured, Alembic can then auto generate the “obvious” migrations based on a comparison. In this case, we want it to compare the empty database against our models, and then auto-generate migrations that we can then review and modify before applying.</p>
<p>We have already defined the database connection string in our configuration file. We will begin working with the <code>env.py</code> to access the table metadata object that contains the target.</p>
<h3 id="editing-envpy-to-import-our-models">Editing <code>env.py</code> to import our models</h3>
<p>The following is the default content of <code>alembic/env.py</code>. The sections that we will edit have been highlighted.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"><code class="language-python" data-lang="python"><span style="display:flex; background-color:#3c3d38"><span><span style="color:#f92672">from</span> logging.config <span style="color:#f92672">import</span> fileConfig
</span></span><span style="display:flex; background-color:#3c3d38"><span>
</span></span><span style="display:flex; background-color:#3c3d38"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> engine_from_config
</span></span><span style="display:flex; background-color:#3c3d38"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> pool
</span></span><span style="display:flex; background-color:#3c3d38"><span>
</span></span><span style="display:flex; background-color:#3c3d38"><span><span style="color:#f92672">from</span> alembic <span style="color:#f92672">import</span> context
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># this is the Alembic Config object, which provides</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># access to the values within the .ini file in use.</span>
</span></span><span style="display:flex;"><span>config <span style="color:#f92672">=</span> context<span style="color:#f92672">.</span>config
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Interpret the config file for Python logging.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># This line sets up loggers basically.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> config<span style="color:#f92672">.</span>config_file_name <span style="color:#f92672">is</span> <span style="color:#f92672">not</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    fileConfig(config<span style="color:#f92672">.</span>config_file_name)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex; background-color:#3c3d38"><span><span style="color:#75715e"># add your model&#39;s MetaData object here</span>
</span></span><span style="display:flex; background-color:#3c3d38"><span><span style="color:#75715e"># for &#39;autogenerate&#39; support</span>
</span></span><span style="display:flex; background-color:#3c3d38"><span><span style="color:#75715e"># from myapp import mymodel</span>
</span></span><span style="display:flex; background-color:#3c3d38"><span><span style="color:#75715e"># target_metadata = mymodel.Base.metadata</span>
</span></span><span style="display:flex; background-color:#3c3d38"><span>target_metadata <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># other values from the config, defined by the needs of env.py,</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># can be acquired:</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># my_important_option = config.get_main_option(&#34;my_important_option&#34;)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ... etc.</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">def</span> <span style="color:#a6e22e">run_migrations_offline</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;Run migrations in &#39;offline&#39; mode.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    This configures the context with just a URL
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    and not an Engine, though an Engine is acceptable
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    here as well.  By skipping the Engine creation
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    we don&#39;t even need a DBAPI to be available.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    Calls to context.execute() here emit the given string to the
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    script output.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    url <span style="color:#f92672">=</span> config<span style="color:#f92672">.</span>get_main_option(<span style="color:#e6db74">&#34;sqlalchemy.url&#34;</span>)
</span></span><span style="display:flex;"><span>    context<span style="color:#f92672">.</span>configure(
</span></span><span style="display:flex;"><span>        url<span style="color:#f92672">=</span>url,
</span></span><span style="display:flex;"><span>        target_metadata<span style="color:#f92672">=</span>target_metadata,
</span></span><span style="display:flex;"><span>        literal_binds<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>        dialect_opts<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;paramstyle&#34;</span>: <span style="color:#e6db74">&#34;named&#34;</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">with</span> context<span style="color:#f92672">.</span>begin_transaction():
</span></span><span style="display:flex;"><span>        context<span style="color:#f92672">.</span>run_migrations()
</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">def</span> <span style="color:#a6e22e">run_migrations_online</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;Run migrations in &#39;online&#39; mode.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    In this scenario we need to create an Engine
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    and associate a connection with the context.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    connectable <span style="color:#f92672">=</span> engine_from_config(
</span></span><span style="display:flex;"><span>        config<span style="color:#f92672">.</span>get_section(config<span style="color:#f92672">.</span>config_ini_section, {}),
</span></span><span style="display:flex;"><span>        prefix<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;sqlalchemy.&#34;</span>,
</span></span><span style="display:flex;"><span>        poolclass<span style="color:#f92672">=</span>pool<span style="color:#f92672">.</span>NullPool,
</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">with</span> connectable<span style="color:#f92672">.</span>connect() <span style="color:#66d9ef">as</span> connection:
</span></span><span style="display:flex;"><span>        context<span style="color:#f92672">.</span>configure(
</span></span><span style="display:flex;"><span>            connection<span style="color:#f92672">=</span>connection, target_metadata<span style="color:#f92672">=</span>target_metadata
</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">with</span> context<span style="color:#f92672">.</span>begin_transaction():
</span></span><span style="display:flex;"><span>            context<span style="color:#f92672">.</span>run_migrations()
</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">if</span> context<span style="color:#f92672">.</span>is_offline_mode():
</span></span><span style="display:flex;"><span>    run_migrations_offline()
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>    run_migrations_online()
</span></span></code></pre></div><p>First, we will import the <code>models.py</code> script. Edit the highlighted snippet to add an import statement.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> models <span style="color:#f92672">import</span> Base
</span></span></code></pre></div><p>Then we will replace the <code>target_metadata</code> value with the metadata of our data model. We will change the second highlighted section to:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>target_metadata <span style="color:#f92672">=</span> Base<span style="color:#f92672">.</span>metadata
</span></span></code></pre></div><p>Our <code>env.py</code> file should now look like the following, with our changes being highlighted.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;display:grid;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#f92672">from</span> logging.config <span style="color:#f92672">import</span> fileConfig
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> engine_from_config
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> sqlalchemy <span style="color:#f92672">import</span> pool
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic <span style="color:#f92672">import</span> context
</span></span><span style="display:flex; background-color:#3c3d38"><span><span style="color:#f92672">from</span> models <span style="color:#f92672">import</span> Base
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># this is the Alembic Config object, which provides</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># access to the values within the .ini file in use.</span>
</span></span><span style="display:flex;"><span>config <span style="color:#f92672">=</span> context<span style="color:#f92672">.</span>config
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Interpret the config file for Python logging.</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># This line sets up loggers basically.</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> config<span style="color:#f92672">.</span>config_file_name <span style="color:#f92672">is</span> <span style="color:#f92672">not</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    fileConfig(config<span style="color:#f92672">.</span>config_file_name)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># add your model&#39;s MetaData object here</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># for &#39;autogenerate&#39; support</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># from myapp import mymodel</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># target_metadata = mymodel.Base.metadata</span>
</span></span><span style="display:flex; background-color:#3c3d38"><span>target_metadata <span style="color:#f92672">=</span> Base<span style="color:#f92672">.</span>metadata
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># other values from the config, defined by the needs of env.py,</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># can be acquired:</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># my_important_option = config.get_main_option(&#34;my_important_option&#34;)</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ... etc.</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">def</span> <span style="color:#a6e22e">run_migrations_offline</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;Run migrations in &#39;offline&#39; mode.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    This configures the context with just a URL
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    and not an Engine, though an Engine is acceptable
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    here as well.  By skipping the Engine creation
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    we don&#39;t even need a DBAPI to be available.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    Calls to context.execute() here emit the given string to the
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    script output.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    url <span style="color:#f92672">=</span> config<span style="color:#f92672">.</span>get_main_option(<span style="color:#e6db74">&#34;sqlalchemy.url&#34;</span>)
</span></span><span style="display:flex;"><span>    context<span style="color:#f92672">.</span>configure(
</span></span><span style="display:flex;"><span>        url<span style="color:#f92672">=</span>url,
</span></span><span style="display:flex;"><span>        target_metadata<span style="color:#f92672">=</span>target_metadata,
</span></span><span style="display:flex;"><span>        literal_binds<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>        dialect_opts<span style="color:#f92672">=</span>{<span style="color:#e6db74">&#34;paramstyle&#34;</span>: <span style="color:#e6db74">&#34;named&#34;</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">with</span> context<span style="color:#f92672">.</span>begin_transaction():
</span></span><span style="display:flex;"><span>        context<span style="color:#f92672">.</span>run_migrations()
</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">def</span> <span style="color:#a6e22e">run_migrations_online</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;&#34;&#34;Run migrations in &#39;online&#39; mode.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    In this scenario we need to create an Engine
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    and associate a connection with the context.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    connectable <span style="color:#f92672">=</span> engine_from_config(
</span></span><span style="display:flex;"><span>        config<span style="color:#f92672">.</span>get_section(config<span style="color:#f92672">.</span>config_ini_section, {}),
</span></span><span style="display:flex;"><span>        prefix<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;sqlalchemy.&#34;</span>,
</span></span><span style="display:flex;"><span>        poolclass<span style="color:#f92672">=</span>pool<span style="color:#f92672">.</span>NullPool,
</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">with</span> connectable<span style="color:#f92672">.</span>connect() <span style="color:#66d9ef">as</span> connection:
</span></span><span style="display:flex;"><span>        context<span style="color:#f92672">.</span>configure(
</span></span><span style="display:flex;"><span>            connection<span style="color:#f92672">=</span>connection, target_metadata<span style="color:#f92672">=</span>target_metadata
</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">with</span> context<span style="color:#f92672">.</span>begin_transaction():
</span></span><span style="display:flex;"><span>            context<span style="color:#f92672">.</span>run_migrations()
</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">if</span> context<span style="color:#f92672">.</span>is_offline_mode():
</span></span><span style="display:flex;"><span>    run_migrations_offline()
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">else</span>:
</span></span><span style="display:flex;"><span>    run_migrations_online()
</span></span></code></pre></div><h3 id="auto-generating-a-migration-script-with-alembic">Auto-generating a migration script with Alembic</h3>
<p>Now that <code>env.py</code> and <code>alembic.ini</code> have been configured, and we have created our first data model, we can auto-generate our first migration script! We will run the command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic revision --autogenerate -m <span style="color:#e6db74">&#34;Initial commit, created customers, orders and products&#34;</span>
</span></span></code></pre></div><p>This code does the following:</p>
<ul>
<li>Runs Alembic and creates a new revision.</li>
<li>Tells Alembic to auto-generate the migration script.</li>
<li>Creates a message string to use with the revision. In this case, this string is &ldquo;Initial commit, created customers, orders and products&rdquo;. This is analogous to a commit message when using git for version control.</li>
</ul>
<p>You should get output like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>INFO  <span style="color:#f92672">[</span>alembic.runtime.migration<span style="color:#f92672">]</span> Context impl PostgresqlImpl.
</span></span><span style="display:flex;"><span>INFO  <span style="color:#f92672">[</span>alembic.runtime.migration<span style="color:#f92672">]</span> Will assume transactional DDL.
</span></span><span style="display:flex;"><span>INFO  <span style="color:#f92672">[</span>alembic.autogenerate.compare<span style="color:#f92672">]</span> Detected added table <span style="color:#e6db74">&#39;customers&#39;</span>
</span></span><span style="display:flex;"><span>INFO  <span style="color:#f92672">[</span>alembic.autogenerate.compare<span style="color:#f92672">]</span> Detected added table <span style="color:#e6db74">&#39;products&#39;</span>
</span></span><span style="display:flex;"><span>INFO  <span style="color:#f92672">[</span>alembic.autogenerate.compare<span style="color:#f92672">]</span> Detected added table <span style="color:#e6db74">&#39;orders&#39;</span>
</span></span><span style="display:flex;"><span>  Generating /path_to_your_project/alembic/versions/de5f32c11361_initial_commit_created_customers_orders_.py ...  <span style="color:#66d9ef">done</span>
</span></span></code></pre></div><p>This output means that Alembic has successfully generated a migration script, and the script is located at <code>/path_to_your_project/alembic/versions/de5f32c11361_initial_commit_created_customers_orders_.py</code>. The <code>versions</code> directory will have the following content:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic/versions/
</span></span><span style="display:flex;"><span>└── de5f32c11361_initial_commit_created_customers_orders_.py
</span></span></code></pre></div><p>The contents of this auto-generated script are:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;Initial commit, created customers, orders and products
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Revision ID: de5f32c11361
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Revises:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Create Date: 2026-01-15 16:10:01.002285
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> typing <span style="color:#f92672">import</span> Sequence, Union
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">from</span> alembic <span style="color:#f92672">import</span> op
</span></span><span style="display:flex;"><span><span style="color:#f92672">import</span> sqlalchemy <span style="color:#66d9ef">as</span> sa
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># revision identifiers, used by Alembic.</span>
</span></span><span style="display:flex;"><span>revision: str <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;de5f32c11361&#39;</span>
</span></span><span style="display:flex;"><span>down_revision: Union[str, <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>branch_labels: Union[str, Sequence[str], <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>depends_on: Union[str, Sequence[str], <span style="color:#66d9ef">None</span>] <span style="color:#f92672">=</span> <span style="color:#66d9ef">None</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">def</span> <span style="color:#a6e22e">upgrade</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### commands auto generated by Alembic - please adjust! ###</span>
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>create_table(<span style="color:#e6db74">&#39;customers&#39;</span>,
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;customer_id&#39;</span>, sa<span style="color:#f92672">.</span>Integer(), sa<span style="color:#f92672">.</span>Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;first_name&#39;</span>, sa<span style="color:#f92672">.</span>String(), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;last_name&#39;</span>, sa<span style="color:#f92672">.</span>String(), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;email_address&#39;</span>, sa<span style="color:#f92672">.</span>String(), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>PrimaryKeyConstraint(<span style="color:#e6db74">&#39;customer_id&#39;</span>)
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>create_table(<span style="color:#e6db74">&#39;products&#39;</span>,
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;product_id&#39;</span>, sa<span style="color:#f92672">.</span>Integer(), sa<span style="color:#f92672">.</span>Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;product_name&#39;</span>, sa<span style="color:#f92672">.</span>String(), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;description&#39;</span>, sa<span style="color:#f92672">.</span>Text(), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;price&#39;</span>, sa<span style="color:#f92672">.</span>Float(), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>PrimaryKeyConstraint(<span style="color:#e6db74">&#39;product_id&#39;</span>)
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>create_table(<span style="color:#e6db74">&#39;orders&#39;</span>,
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;order_id&#39;</span>, sa<span style="color:#f92672">.</span>Integer(), sa<span style="color:#f92672">.</span>Identity(always<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;customer_id&#39;</span>, sa<span style="color:#f92672">.</span>Integer(), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>Column(<span style="color:#e6db74">&#39;order_date&#39;</span>, sa<span style="color:#f92672">.</span>DateTime(timezone<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>), server_default<span style="color:#f92672">=</span>sa<span style="color:#f92672">.</span>text(<span style="color:#e6db74">&#39;now()&#39;</span>), nullable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>ForeignKeyConstraint([<span style="color:#e6db74">&#39;customer_id&#39;</span>], [<span style="color:#e6db74">&#39;customers.customer_id&#39;</span>], ondelete<span style="color:#f92672">=</span><span style="color:#e6db74">&#39;CASCADE&#39;</span>),
</span></span><span style="display:flex;"><span>    sa<span style="color:#f92672">.</span>PrimaryKeyConstraint(<span style="color:#e6db74">&#39;order_id&#39;</span>)
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### end Alembic commands ###</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">def</span> <span style="color:#a6e22e">downgrade</span>() <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### commands auto generated by Alembic - please adjust! ###</span>
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>drop_table(<span style="color:#e6db74">&#39;orders&#39;</span>)
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>drop_table(<span style="color:#e6db74">&#39;products&#39;</span>)
</span></span><span style="display:flex;"><span>    op<span style="color:#f92672">.</span>drop_table(<span style="color:#e6db74">&#39;customers&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#75715e"># ### end Alembic commands ###</span>
</span></span></code></pre></div><p>We can see that a migration script has been generated, with an upgrade and a downgrade parameter. Alembic has compared our target metadata against the database, and has generated a migration script complete with the operations needed to upgrade our database to the state defined by our ORM.</p>

    <div class="admonition note">
      <div class="admonition-header"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 125.7-86.8 86.8c-10.3 10.3-17.5 23.1-21 37.2l-18.7 74.9c-2.3 9.2-1.8 18.8 1.3 27.5L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM549.8 235.7l14.4 14.4c15.6 15.6 15.6 40.9 0 56.6l-29.4 29.4-71-71 29.4-29.4c15.6-15.6 40.9-15.6 56.6 0zM311.9 417L441.1 287.8l71 71L382.9 487.9c-4.1 4.1-9.2 7-14.9 8.4l-60.1 15c-5.5 1.4-11.2-.2-15.2-4.2s-5.6-9.7-4.2-15.2l15-60.1c1.4-5.6 4.3-10.8 8.4-14.9z"/></svg>
        <span>Note</span>
      </div>
      <div class="admonition-content">
        <p>It is important to note that Alembic <strong>does not detect every change reliably</strong>. There are certain changes that it can always detect, but
it is <em>always</em> necessary to manually check the migration file to correct the auto-generated migrations.</p>
<p>From <a href="https://alembic.sqlalchemy.org/en/latest/autogenerate.html#what-does-autogenerate-detect-and-what-does-it-not-detect">Alembic&rsquo;s documentation on Autogenerate,</a></p>
<blockquote>
<p>It is critical to note that <strong>autogenerate is not intended to be perfect.</strong>
It is <strong>always</strong> necessary to manually review and correct the <strong>candidate migrations</strong> that autogenerate produces.</p>
</blockquote>
<p>Alembic&rsquo;s documentation has a <a href="https://alembic.sqlalchemy.org/en/latest/autogenerate.html#what-does-autogenerate-detect-and-what-does-it-not-detect">section covering what can and cannot be detected with autogeneration.</a></p>
<p>Given that our operations here are rudimentary, in this case, the changes needed to apply our data model
to the database have been correctly detected and applied, but it is always a good idea to
read through the migration script anyway.</p>
      </div>
    </div><h3 id="examining-the-auto-generated-migration-script">Examining the auto-generated migration script</h3>
<p>Going through the <code>upgrade</code> function generated by Alembic, we can see that the following migrations have been generated:</p>
<ul>
<li>
<p>The <code>customers</code>, <code>orders</code> and <code>products</code> tables have been created, with their primary keys being <code>customer_id</code>,<code>order_id</code> and <code>product_id</code> respectively.</p>
</li>
<li>
<p>The attributes defined by our <code>models.py</code> file have been correctly added to these newly created tables.</p>
</li>
<li>
<p>From this code snippet:</p>
<p><code>sa.ForeignKeyConstraint(['customer_id'], ['customers.customer_id'], ondelete='CASCADE'),</code>
We can see that the <code>customer_id</code> field in the <code>orders</code> table has been made a foreign key, connecting to the <code>customer_id</code> column in the <code>customers</code> table.</p>
</li>
</ul>
<p>The migration script looks good, and does not need further refinement. Let&rsquo;s now actually apply our first migration!</p>
<h3 id="applying-the-alembic-generated-migration-script">Applying the Alembic-generated migration script</h3>
<p>We will now apply the migration script with the following command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>alembic upgrade head
</span></span></code></pre></div><p>This command tells Alembic to upgrade the database to the latest revision (the &ldquo;head&rdquo;). Depending on the state of the database, it will work through the scripts located in the <code>versions</code> directory to bring the database&rsquo;s metadata in line with the most recent migration script.</p>
<p>The output of the command is as follows:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>INFO  <span style="color:#f92672">[</span>alembic.runtime.migration<span style="color:#f92672">]</span> Context impl PostgresqlImpl.
</span></span><span style="display:flex;"><span>INFO  <span style="color:#f92672">[</span>alembic.runtime.migration<span style="color:#f92672">]</span> Will assume transactional DDL.
</span></span><span style="display:flex;"><span>INFO  <span style="color:#f92672">[</span>alembic.runtime.migration<span style="color:#f92672">]</span> Running upgrade  -&gt; de5f32c11361, Initial commit, created customers, orders and products
</span></span></code></pre></div><p>Since our database is empty and there is only a single migration script, only that script has been run. The last line confirms that migration <code>de5f32c11361</code> has been successfully applied.</p>
<h3 id="examining-the-database-to-verify-our-migration">Examining the database to verify our migration</h3>
<p>Connect to your database with the database management tool of your choice. <a href="https://dbeaver.io/">I use DBeaver.</a></p>
<p>The Entity Relationship Diagram for the <code>public</code> schema of your database should look like this.</p>
<figure class="align-center ">
    <img loading="lazy" src="basic_data_model_erd.png#center"
         alt="Our newly created data model!"/> <figcaption>
            <p>Our newly created data model!</p>
        </figcaption>
</figure>

<p>We&rsquo;ve successfully applied our first database migration! We&rsquo;ve created the <code>customers</code>, <code>orders</code> and <code>products</code> tables, and have built a foreign key relationship between <code>customers</code> and <code>orders</code>.</p>
<p>The <code>alembic_version</code> table has been newly created by Alembic. Alembic uses this table to identify the path to upgrade the database from its current state to the version requested by the <code>alembic upgrade</code> command. In our case, we requested the version <code>head</code>.</p>
<p>Alembic works through the migration scripts in the <code>versions</code> directory, starting from the version found in the database&rsquo;s <code>alembic_version</code>, up to the script that defined the version requested by us, invoking the <code>upgrade()</code> method in each file to get to the target revision. You can check this by examining the data in the <code>alembic_version</code> table. It should have a single entry, corresponding to the revision of the script we just applied.</p>
<h1 id="next-steps">Next Steps</h1>
<p>This is a great first step towards building a comprehensive data model, but there is a long way to go before we can consider migrating our data model from a development environment to a production environment! In subsequent blog posts, we will cover the following:</p>
<ul>
<li>Updating our data model with more tables and relationships to create a proper e-commerce database schema.</li>
<li>Using an Async engine for asynchronous database connections</li>
<li>Using the <code>alembic_utils</code> library to incorporate views, functions and triggers into the version control system.</li>
<li>Configuring Alembic to connect to a production environment, and seamlessly bring the production environment&rsquo;s schema up-to-date with our final revision.</li>
</ul>
<h1 id="references">References</h1>
<ul>
<li>
<p><a href="https://docs.sqlalchemy.org/en/20/intro.html">SQLAlchemy documentation</a>, most notably the <a href="https://docs.sqlalchemy.org/en/20/tutorial/index.html#unified-tutorial">SQLAlchemy Unified Tutorial Section.</a></p>
</li>
<li>
<p><a href="https://alembic.sqlalchemy.org/en/latest/index.html">Alembic&rsquo;s documentation</a>, and importantly, the <a href="https://alembic.sqlalchemy.org/en/latest/autogenerate.html">section covering Autogeneration.</a></p>
</li>
</ul>
]]></content:encoded></item></channel></rss>