<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: schema-migrations</title><link href="http://feeds.simonwillison.net/" rel="alternate"/><link href="http://feeds.simonwillison.net/tags/schema-migrations.atom" rel="self"/><id>http://feeds.simonwillison.net/</id><updated>2026-07-07T19:32:57+00:00</updated><author><name>Simon Willison</name></author><entry><title>sqlite-utils 4.0, now with database schema migrations</title><link href="https://simonwillison.net/2026/Jul/7/sqlite-utils-4/#atom-tag" rel="alternate"/><published>2026-07-07T19:32:57+00:00</published><updated>2026-07-07T19:32:57+00:00</updated><id>https://simonwillison.net/2026/Jul/7/sqlite-utils-4/#atom-tag</id><summary type="html">
    &lt;p&gt;This morning I released &lt;a href="https://sqlite-utils.datasette.io/en/stable/changelog.html#v4-0"&gt;sqlite-utils 4.0&lt;/a&gt;, the 124th release of that project and the first major version bump since &lt;a href="https://sqlite-utils.datasette.io/en/stable/changelog.html#v3-0"&gt;3.0&lt;/a&gt; in November 2020. In addition to some small but significant breaking changes (described in &lt;a href="https://sqlite-utils.datasette.io/en/stable/upgrading.html"&gt;this upgrade guide&lt;/a&gt;), this version introduces three major features: &lt;strong&gt;database migrations&lt;/strong&gt;, &lt;strong&gt;nested transactions&lt;/strong&gt; (via a new &lt;code&gt;db.atomic()&lt;/code&gt; method), and support for &lt;strong&gt;compound foreign keys&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id="database-schema-migrations-using-sqlite-utils"&gt;Database schema migrations using sqlite-utils&lt;/h4&gt;
&lt;p&gt;Schema migrations define a sequence of changes to be made to a SQLite database, plus a mechanism for tracking which migrations have been applied and applying any that are found to be pending.&lt;/p&gt;
&lt;p&gt;Migrations are defined in Python files using the &lt;a href="https://sqlite-utils.datasette.io/en/stable/python-api.html"&gt;sqlite-utils Python library&lt;/a&gt;, which includes a powerful &lt;code&gt;table.transform()&lt;/code&gt; method providing &lt;a href="https://sqlite-utils.datasette.io/en/stable/python-api.html#transforming-a-table"&gt;enhanced alter table capabilities&lt;/a&gt; that are not supported by SQLite's &lt;code&gt;ALTER TABLE&lt;/code&gt; statement.&lt;/p&gt;
&lt;p&gt;(&lt;code&gt;table.transform()&lt;/code&gt; implements the pattern &lt;a href="https://www.sqlite.org/lang_altertable.html#otheralter"&gt;recommended by the SQLite documentation&lt;/a&gt; - create a new temporary table with the new schema, copy across the data, then drop the old table and rename the temporary one in its place.)&lt;/p&gt;
&lt;p&gt;Here's an example migration file which creates a table called &lt;code&gt;creatures&lt;/code&gt;, adds an additional column to it in a second step, then changes the types of two of the columns in a third:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;sqlite_utils&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-v"&gt;Migrations&lt;/span&gt;

&lt;span class="pl-s1"&gt;migrations&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;Migrations&lt;/span&gt;(&lt;span class="pl-s"&gt;"creatures"&lt;/span&gt;)

&lt;span class="pl-en"&gt;@&lt;span class="pl-en"&gt;migrations&lt;/span&gt;()&lt;/span&gt;
&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;create_table&lt;/span&gt;(&lt;span class="pl-s1"&gt;db&lt;/span&gt;):
    &lt;span class="pl-s1"&gt;db&lt;/span&gt;[&lt;span class="pl-s"&gt;"creatures"&lt;/span&gt;].&lt;span class="pl-c1"&gt;create&lt;/span&gt;(
        {&lt;span class="pl-s"&gt;"id"&lt;/span&gt;: &lt;span class="pl-s1"&gt;int&lt;/span&gt;, &lt;span class="pl-s"&gt;"name"&lt;/span&gt;: &lt;span class="pl-s1"&gt;str&lt;/span&gt;, &lt;span class="pl-s"&gt;"species"&lt;/span&gt;: &lt;span class="pl-s1"&gt;str&lt;/span&gt;},
        &lt;span class="pl-s1"&gt;pk&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"id"&lt;/span&gt;,
    )

&lt;span class="pl-en"&gt;@&lt;span class="pl-en"&gt;migrations&lt;/span&gt;()&lt;/span&gt;
&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;add_weight&lt;/span&gt;(&lt;span class="pl-s1"&gt;db&lt;/span&gt;):
    &lt;span class="pl-s1"&gt;db&lt;/span&gt;[&lt;span class="pl-s"&gt;"creatures"&lt;/span&gt;].&lt;span class="pl-c1"&gt;add_column&lt;/span&gt;(&lt;span class="pl-s"&gt;"weight"&lt;/span&gt;, &lt;span class="pl-s1"&gt;float&lt;/span&gt;)

&lt;span class="pl-en"&gt;@&lt;span class="pl-en"&gt;migrations&lt;/span&gt;()&lt;/span&gt;
&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;change_column_types&lt;/span&gt;(&lt;span class="pl-s1"&gt;db&lt;/span&gt;):
    &lt;span class="pl-s1"&gt;db&lt;/span&gt;[&lt;span class="pl-s"&gt;"creatures"&lt;/span&gt;].&lt;span class="pl-c1"&gt;transform&lt;/span&gt;(&lt;span class="pl-s1"&gt;types&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;{&lt;span class="pl-s"&gt;"species"&lt;/span&gt;: &lt;span class="pl-s1"&gt;int&lt;/span&gt;, &lt;span class="pl-s"&gt;"weight"&lt;/span&gt;: &lt;span class="pl-s1"&gt;str&lt;/span&gt;})&lt;/pre&gt;
&lt;p&gt;Save that as &lt;code&gt;migrations.py&lt;/code&gt; and run it against a fresh database like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;uvx sqlite-utils migrate data.db migrations.py&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then if you check the schema of that database:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;uvx sqlite-utils schema data.db&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You'll see this SQL:&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;CREATE&lt;/span&gt; &lt;span class="pl-k"&gt;TABLE&lt;/span&gt; "&lt;span class="pl-en"&gt;_sqlite_migrations&lt;/span&gt;" (
   &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;id&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;INTEGER&lt;/span&gt; &lt;span class="pl-k"&gt;PRIMARY KEY&lt;/span&gt;,
   &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;migration_set&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;TEXT&lt;/span&gt;,
   &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;name&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;TEXT&lt;/span&gt;,
   &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;applied_at&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;TEXT&lt;/span&gt;
);
&lt;span class="pl-k"&gt;CREATE&lt;/span&gt; &lt;span class="pl-k"&gt;UNIQUE INDEX&lt;/span&gt; "&lt;span class="pl-en"&gt;idx__sqlite_migrations_migration_set_name&lt;/span&gt;"
    &lt;span class="pl-k"&gt;ON&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;_sqlite_migrations&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; (&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;migration_set&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;, &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;name&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;);
&lt;span class="pl-k"&gt;CREATE&lt;/span&gt; &lt;span class="pl-k"&gt;TABLE&lt;/span&gt; "&lt;span class="pl-en"&gt;creatures&lt;/span&gt;" (
   &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;id&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;INTEGER&lt;/span&gt; &lt;span class="pl-k"&gt;PRIMARY KEY&lt;/span&gt;,
   &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;name&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;TEXT&lt;/span&gt;,
   &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;species&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;INTEGER&lt;/span&gt;,
   &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;weight&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;TEXT&lt;/span&gt;
);&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;_sqlite_migrations&lt;/code&gt; table is used to keep track of which migration functions have been run. The &lt;code&gt;creatures&lt;/code&gt; table above is the schema after all three migrations have been applied.&lt;/p&gt;
&lt;p&gt;To see a list of migrations, both pending and applied, run this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;uvx sqlite-utils migrate data.db migrations.py --list&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Migrations for: creatures

  Applied:
    create_table - 2026-07-07 17:58:41.360051+00:00
    add_weight - 2026-07-07 17:58:41.360608+00:00
    change_column_types - 2026-07-07 18:01:15.802000+00:00

  Pending:
    (none)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you don't specify a migrations file, the &lt;code&gt;sqlite-utils migrate data.db&lt;/code&gt; command will scan the current directory and its subdirectories for files called &lt;code&gt;migrations.py&lt;/code&gt; and apply any &lt;code&gt;Migrations()&lt;/code&gt; instances it finds in them.&lt;/p&gt;
&lt;p&gt;You can also execute migrations &lt;a href="https://sqlite-utils.datasette.io/en/stable/migrations.html#applying-migrations-in-python"&gt;from Python code&lt;/a&gt; using the &lt;code&gt;migrations.apply(db)&lt;/code&gt; method, which is useful for building tools that manage their own database schemas over multiple versions. My own &lt;a href="https://llm.datasette.io/"&gt;LLM tool&lt;/a&gt; has been using a version of this pattern for several years now, as shown in &lt;a href="https://github.com/simonw/llm/blob/0.31/llm/embeddings_migrations.py"&gt;llm/embeddings_migrations.py&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="prior-art"&gt;Prior art&lt;/h4&gt;
&lt;p&gt;My favorite implementation of this pattern remains &lt;a href="https://docs.djangoproject.com/en/6.0/topics/migrations/"&gt;Django's Migrations&lt;/a&gt;, developed by Andrew Godwin based on his earlier project &lt;a href="https://github.com/andrewgodwin/south"&gt;South&lt;/a&gt;. Fun fact: Andrew, Russ Keith-Magee, and I presented our competing approaches to schema migrations for Django on the &lt;a href="https://www.youtube.com/watch?v=VSq8m00p1FM"&gt;Schema Evolution panel&lt;/a&gt; at the very first DjangoCon back in 2008! My attempt was called &lt;a href="https://simonwillison.net/2008/Sep/3/dmigrations/"&gt;dmigrations&lt;/a&gt;, developed with a team at Global Radio in London.&lt;/p&gt;
&lt;p&gt;Django's migrations can be automatically generated from model definitions and include the ability to roll back to a previous version. The &lt;code&gt;sqlite-utils&lt;/code&gt; approach is deliberately simpler: unlike Django, &lt;code&gt;sqlite-utils&lt;/code&gt; encourages programmatic table creation rather than a model definition ORM, so there isn't anything we can use to automatically generate migrations.&lt;/p&gt;
&lt;p&gt;I decided to skip rollback, since in my experience it's a feature that is rarely used. With a SQLite project, an easy way to achieve rollback is to create a copy of your database file before you apply the migrations!&lt;/p&gt;
&lt;h4 id="migrating-from-sqlite-migrate"&gt;Migrating from sqlite-migrate&lt;/h4&gt;
&lt;p&gt;The design of &lt;code&gt;sqlite-utils&lt;/code&gt; migrations is three years old now - I had originally released it as a separate package called &lt;a href="https://github.com/simonw/sqlite-migrate"&gt;sqlite-migrate&lt;/a&gt;, which never quite graduated beyond a beta release.&lt;/p&gt;
&lt;p&gt;I've used that package in enough places now that I'm confident in the design, so I've decided to promote it to a feature of &lt;code&gt;sqlite-utils&lt;/code&gt; to make it available by default to all of the other tools in the growing sqlite-utils/Datasette/LLM ecosystem.&lt;/p&gt;
&lt;p&gt;I made &lt;a href="https://github.com/simonw/sqlite-migrate/releases/tag/0.2"&gt;one last release&lt;/a&gt; of &lt;code&gt;sqlite-migrate&lt;/code&gt;, which switches it to depend on &lt;code&gt;sqlite-utils&amp;gt;=4&lt;/code&gt; and replaces the &lt;code&gt;__init__.py&lt;/code&gt; file with the following:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;sqlite_utils&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-v"&gt;Migrations&lt;/span&gt;

&lt;span class="pl-s1"&gt;__all__&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; [&lt;span class="pl-s"&gt;"Migrations"&lt;/span&gt;]&lt;/pre&gt;
&lt;p&gt;Any existing project that depends on &lt;code&gt;sqlite-migrate&lt;/code&gt; should continue to work without alterations.&lt;/p&gt;
&lt;h4 id="everything-else-in-sqlite-utils-4-0"&gt;Everything else in sqlite-utils 4.0&lt;/h4&gt;
&lt;p&gt;Here are the release notes for this version, with some inline annotations:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The 4.0 release includes some minor backwards-incompatible fixes (hence the major version number bump) and introduces three major new features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sqlite-utils.datasette.io/en/stable/migrations.html#migrations"&gt;Database migrations&lt;/a&gt;, providing a structured mechanism for evolving a project’s schema over time. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/752"&gt;#752&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;I think of migrations as the signature new feature, hence this blog post.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://sqlite-utils.datasette.io/en/stable/python-api.html#python-api-atomic"&gt;Nested transaction support&lt;/a&gt; via &lt;code&gt;db.atomic()&lt;/code&gt;, plus numerous improvements to how transactions work across the library. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/755"&gt;#755&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;sqlite-utils&lt;/code&gt; has long had a confused relationship with database transactions, partly because when I started designing the library back in 2018 I didn't yet have a great feel for how those worked in SQLite itself.&lt;/p&gt;
&lt;p&gt;Adding migrations to the core library made me determined to finally crack this nut, since transactions make migration systems a whole lot safer and easier to reason about.&lt;/p&gt;
&lt;p&gt;I ended up building this around a &lt;code&gt;db.atomic()&lt;/code&gt; context manager which looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;with&lt;/span&gt; &lt;span class="pl-s1"&gt;db&lt;/span&gt;.&lt;span class="pl-c1"&gt;atomic&lt;/span&gt;():
    &lt;span class="pl-s1"&gt;db&lt;/span&gt;.&lt;span class="pl-c1"&gt;table&lt;/span&gt;(&lt;span class="pl-s"&gt;"dogs"&lt;/span&gt;).&lt;span class="pl-c1"&gt;insert&lt;/span&gt;({&lt;span class="pl-s"&gt;"id"&lt;/span&gt;: &lt;span class="pl-c1"&gt;1&lt;/span&gt;, &lt;span class="pl-s"&gt;"name"&lt;/span&gt;: &lt;span class="pl-s"&gt;"Cleo"&lt;/span&gt;}, &lt;span class="pl-s1"&gt;pk&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"id"&lt;/span&gt;)
    &lt;span class="pl-s1"&gt;db&lt;/span&gt;.&lt;span class="pl-c1"&gt;table&lt;/span&gt;(&lt;span class="pl-s"&gt;"dogs"&lt;/span&gt;).&lt;span class="pl-c1"&gt;insert&lt;/span&gt;({&lt;span class="pl-s"&gt;"id"&lt;/span&gt;: &lt;span class="pl-c1"&gt;2&lt;/span&gt;, &lt;span class="pl-s"&gt;"name"&lt;/span&gt;: &lt;span class="pl-s"&gt;"Pancakes"&lt;/span&gt;})&lt;/pre&gt;
&lt;p&gt;SQLite supports &lt;a href="https://sqlite.org/lang_savepoint.html"&gt;Savepoints&lt;/a&gt;, and as a result &lt;code&gt;db.atomic()&lt;/code&gt; can be nested to carry out transactions inside of transactions. It's pretty neat!&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Support for &lt;a href="https://sqlite-utils.datasette.io/en/stable/python-api.html#python-api-compound-foreign-keys"&gt;compound foreign keys&lt;/a&gt;, including creation, transformation and introspection through &lt;a href="https://sqlite-utils.datasette.io/en/stable/python-api.html#python-api-introspection-foreign-keys"&gt;table.foreign_keys&lt;/a&gt;. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/594"&gt;#594&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;This came about when I asked a coding agent to review all open issues and PRs for things that should be included in a 4.0 release since they would represent breaking changes if I added them later, and it correctly identified that compound foreign keys were exactly that kind of feature.&lt;/p&gt;
&lt;p&gt;I started with a breaking change to the &lt;a href="https://sqlite-utils.datasette.io/en/stable/python-api.html#python-api-introspection-foreign-keys"&gt;table.foreign_keys&lt;/a&gt; introspection method, and then decided to see if Claude Fable 5 could handle the more fiddly job of integrating compound foreign key &lt;em&gt;creation&lt;/em&gt; into the library. The API design it helped create felt &lt;a href="https://sqlite-utils.datasette.io/en/stable/python-api.html#compound-foreign-keys"&gt;exactly right to me&lt;/a&gt; - consistent with how the rest of the library worked already.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Other notable changes include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Upserts now use SQLite’s &lt;code&gt;INSERT ... ON CONFLICT ... DO UPDATE SET&lt;/code&gt; syntax, detect existing table primary keys automatically and reject records that are missing required primary key values. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/652"&gt;#652&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;This was the change that first pushed me to consider a breaking-change 4.0 version bump. I built this to help support &lt;a href="https://github.com/simonw/sqlite-chronicle"&gt;sqlite-chronicle&lt;/a&gt;, which uses triggers to keep track of rows in a table that have been inserted, updated or deleted.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;db.query()&lt;/code&gt; now executes immediately and rejects statements that do not return rows; use &lt;code&gt;db.execute()&lt;/code&gt; for writes and DDL.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Probably the &lt;a href="https://sqlite-utils.datasette.io/en/stable/upgrading.html#python-api-changes"&gt;most disruptive breaking change&lt;/a&gt; - I've had to update a few places in my own code to switch from &lt;code&gt;db.query()&lt;/code&gt; to &lt;code&gt;db.execute()&lt;/code&gt; as a result.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;CSV and TSV imports now detect column types by default, while inserts into existing tables preserve those tables’ column types. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/679"&gt;#679&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;The &lt;code&gt;sqlite-utils insert data.db creatures creatures.csv --detect-types&lt;/code&gt; flag was a later addition to allow column types (text, integer, real) to be automatically detected based on the data in a CSV. It should be the default, and releasing a 4.0 means I can make it so.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;table.extract()&lt;/code&gt; and &lt;code&gt;extracts=&lt;/code&gt; no longer create lookup table records for all-&lt;code&gt;null&lt;/code&gt; values. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/186"&gt;#186&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;The oldest issue addressed by this release - the underlying bug was opened (by me) in October 2020.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;See &lt;a href="https://sqlite-utils.datasette.io/en/stable/upgrading.html#upgrading-3-to-4"&gt;Upgrading from 3.x to 4.0&lt;/a&gt; for details on backwards-incompatible changes.&lt;/p&gt;
&lt;p&gt;The detailed release notes for the features and fixes shipped during the 4.0 pre-release cycle are available in &lt;a href="https://sqlite-utils.datasette.io/en/stable/changelog.html#v4-0a0"&gt;4.0a0&lt;/a&gt;, &lt;a href="https://sqlite-utils.datasette.io/en/stable/changelog.html#v4-0a1"&gt;4.0a1&lt;/a&gt;, &lt;a href="https://sqlite-utils.datasette.io/en/stable/changelog.html#v4-0rc1"&gt;4.0rc1&lt;/a&gt;, &lt;a href="https://sqlite-utils.datasette.io/en/stable/changelog.html#v4-0rc2"&gt;4.0rc2&lt;/a&gt;, &lt;a href="https://sqlite-utils.datasette.io/en/stable/changelog.html#v4-0rc3"&gt;4.0rc3&lt;/a&gt; and &lt;a href="https://sqlite-utils.datasette.io/en/stable/changelog.html#v4-0rc4"&gt;4.0rc4&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The upgrade guide was entirely written by Claude Fable 5, Claude Opus 4.8 and GPT-5.5. The same is true of the release notes.&lt;/p&gt;
&lt;p&gt;This is the kind of documentation I've slowly become comfortable outsourcing to the robots. It doesn't need to convince people of anything, or express any opinions - its job is to be as accurate and detailed as possible. I've reviewed the release notes closely and can confirm they are accurate and comprehensive.&lt;/p&gt;
&lt;h4 id="claude-fable-5-helped-a-lot"&gt;Claude Fable 5 helped a lot&lt;/h4&gt;
&lt;p&gt;I released the first alpha of sqlite-utils 4.0 &lt;a href="https://sqlite-utils.datasette.io/en/stable/changelog.html#a0-2025-05-08"&gt;over a year ago&lt;/a&gt;. I've been dragging my heels on the stable release because of the amount of work it would take to track down and clean up the many other minor design flaws that a major version number allowed me to take on.&lt;/p&gt;
&lt;p&gt;Assistance from Claude Fable 5 (and to a lesser extent Opus 4.8 and GPT-5.5) gave me just the boost I needed to overcome inertia and make the most of the time I could afford to spend on this library.&lt;/p&gt;
&lt;p&gt;Fable has &lt;em&gt;really good taste&lt;/em&gt; in API design, and is &lt;a href="https://simonwillison.net/2026/Jun/11/fable-is-relentlessly-proactive/"&gt;relentlessly proactive&lt;/a&gt; if you give it a more open goal. My most successful prompt was a review task that I issued against what I thought was the last release candidate:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;review the changes on main since the last tagged 3.x release - I am about to ship them as sqlite-utils 4.0, a stable version that promises no backwards-incompatible fixes for a very long time.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;review the changelog and upgrade guide, and write yourself scratch scripts to try out all of the new features in v4 - save those scripts but don't commit them&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I tried this with GPT-5.5 xhigh in Codex Desktop and Fable 5 in Claude Code.&lt;/p&gt;
&lt;p&gt;GPT-5.5 &lt;a href="https://gist.github.com/simonw/823fdecc031371d56dce39537adc0096"&gt;wrote 5 Python scripts&lt;/a&gt; and didn't turn up anything particularly interesting - its &lt;a href="https://github.com/simonw/sqlite-utils/issues/769#issuecomment-4899982463"&gt;final report is here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Fable 5 &lt;a href="https://gist.github.com/simonw/95800bf584f8e437f1cf0d48d9ef81e6"&gt;wrote 12 scripts&lt;/a&gt;, identified 4 release blockers and 10 additional issues &lt;a href="https://github.com/simonw/sqlite-utils/issues/769#issuecomment-4900034150"&gt;in its report&lt;/a&gt;, and built a neat &lt;a href="https://gist.githubusercontent.com/simonw/95800bf584f8e437f1cf0d48d9ef81e6/raw/c43918b36a129bba1d2f2a129117aa11c85146c0/12_bug_repros.py"&gt;combined repro script&lt;/a&gt;, which, when run, output the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;=== 1. Failed db.execute() write leaves an implicit transaction open ===
  in_transaction after failed write: True
  BUG: table 'other' silently lost when connection closed

=== 2. Leading ';' bypasses the query() first-token scanner ===
  BUG: raised OperationalError: no such savepoint: sqlite_utils_query
  BUG: row persisted despite rollback (count=1)

=== 3. Rejected write PRAGMA via query() still takes effect ===
  BUG: user_version=5 after 'rejected' statement (docs say no effect)

=== 4. Implicit compound FK resolves pk columns in table order, not PK order ===
  BUG: other_columns reported as ('b', 'a'), should be ('a', 'b')
  BUG: transform of valid data raised IntegrityError: FOREIGN KEY constraint failed

=== 5. ForeignKey (now a dataclass) is no longer hashable ===
  BUG: cannot use 'sqlite_utils.db.ForeignKey' as a set element (unhashable type: 'ForeignKey')

=== 6. Mixed ForeignKey objects and tuples in foreign_keys= rejected ===
  BUG: foreign_keys= should be a list of tuples

=== 7. insert --csv into an EXISTING table transforms its column types ===
  BUG: existing zip '01234' is now 1234 (column type: int)

=== 8. insert(pk=, alter=True) regression: InvalidColumns before alter runs ===
  BUG: InvalidColumns: Invalid primary key column ['id'] for table t with columns ['a']

=== 9. migrate --stop-before an already-applied migration applies everything ===
  BUG: m2 was applied despite --stop-before m1 (m1 already applied)

=== 10. ensure_autocommit_on() silently commits an open transaction ===
  BUG: row survived rollback (count=1) - transaction was committed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I found myself agreeing with almost all of them. Here's &lt;a href="https://github.com/simonw/sqlite-utils/pull/779"&gt;the PR with 16 commits&lt;/a&gt; where we worked through them in turn.&lt;/p&gt;
&lt;p&gt;There's no doubt in my mind that sqlite-utils 4.0 is a significantly higher-quality release than if I had built it without the assistance of the latest frontier models.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/annotated-release-notes"&gt;annotated-release-notes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/anthropic"&gt;anthropic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/agentic-engineering"&gt;agentic-engineering&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-mythos-fable"&gt;claude-mythos-fable&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="schema-migrations"/><category term="projects"/><category term="sqlite"/><category term="ai"/><category term="sqlite-utils"/><category term="annotated-release-notes"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="anthropic"/><category term="claude"/><category term="agentic-engineering"/><category term="claude-mythos-fable"/></entry><entry><title>sqlite-utils 4.0rc1 adds migrations and nested transactions</title><link href="https://simonwillison.net/2026/Jun/21/sqlite-utils-40rc1/#atom-tag" rel="alternate"/><published>2026-06-21T23:35:47+00:00</published><updated>2026-06-21T23:35:47+00:00</updated><id>https://simonwillison.net/2026/Jun/21/sqlite-utils-40rc1/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;a href="https://sqlite-utils.datasette.io/en/latest/"&gt;sqlite-utils&lt;/a&gt; is my combined Python library and CLI tool for working with SQLite databases. It provides an extensive set of higher-level operations on top of Python's default &lt;a href="https://docs.python.org/3/library/sqlite3.html"&gt;sqlite3 package&lt;/a&gt;, including support for &lt;a href="https://sqlite-utils.datasette.io/en/latest/cli.html#transforming-tables"&gt;complex table transformations&lt;/a&gt;, automatic table creation &lt;a href="https://sqlite-utils.datasette.io/en/latest/cli.html#inserting-json-data"&gt;from JSON data&lt;/a&gt; and a whole lot more.&lt;/p&gt;
&lt;p&gt;I released &lt;a href="https://sqlite-utils.datasette.io/en/latest/changelog.html#rc1-2026-06-21"&gt;sqlite-utils 4.0rc1&lt;/a&gt;, the first release candidate for sqlite-utils v4. The major version bump indicates some (minor) backwards incompatible changes, so I'm interested in having people try this out before I commit to a stable release.&lt;/p&gt;
&lt;h4 id="new-feature-migrations"&gt;New feature: migrations&lt;/h4&gt;
&lt;p&gt;There are two significant new features in this RC compared to the previous 4.0 alphas.&lt;/p&gt;
&lt;p&gt;The first is support for &lt;strong&gt;database migrations&lt;/strong&gt;. This isn't a completely new implementation - it's a slightly modified port of the &lt;a href="https://github.com/simonw/sqlite-migrate"&gt;sqlite-migrate&lt;/a&gt; package I released a few years ago. I think that package has proved itself over time, so I'm now ready to bundle it with &lt;code&gt;sqlite-utils&lt;/code&gt; directly.&lt;/p&gt;
&lt;p&gt;Here's what a set of migrations in a &lt;code&gt;migrations.py&lt;/code&gt; file looks like:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;sqlite_utils&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-v"&gt;Database&lt;/span&gt;, &lt;span class="pl-v"&gt;Migrations&lt;/span&gt;

&lt;span class="pl-s1"&gt;migrations&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;Migrations&lt;/span&gt;(&lt;span class="pl-s"&gt;"creatures"&lt;/span&gt;)

&lt;span class="pl-en"&gt;@&lt;span class="pl-en"&gt;migrations&lt;/span&gt;()&lt;/span&gt;
&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;create_table&lt;/span&gt;(&lt;span class="pl-s1"&gt;db&lt;/span&gt;):
    &lt;span class="pl-s1"&gt;db&lt;/span&gt;[&lt;span class="pl-s"&gt;"creatures"&lt;/span&gt;].&lt;span class="pl-c1"&gt;create&lt;/span&gt;(
        {&lt;span class="pl-s"&gt;"id"&lt;/span&gt;: &lt;span class="pl-s1"&gt;int&lt;/span&gt;, &lt;span class="pl-s"&gt;"name"&lt;/span&gt;: &lt;span class="pl-s1"&gt;str&lt;/span&gt;, &lt;span class="pl-s"&gt;"species"&lt;/span&gt;: &lt;span class="pl-s1"&gt;str&lt;/span&gt;},
        &lt;span class="pl-s1"&gt;pk&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"id"&lt;/span&gt;,
    )

&lt;span class="pl-en"&gt;@&lt;span class="pl-en"&gt;migrations&lt;/span&gt;()&lt;/span&gt;
&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;add_weight&lt;/span&gt;(&lt;span class="pl-s1"&gt;db&lt;/span&gt;):
    &lt;span class="pl-s1"&gt;db&lt;/span&gt;[&lt;span class="pl-s"&gt;"creatures"&lt;/span&gt;].&lt;span class="pl-c1"&gt;add_column&lt;/span&gt;(&lt;span class="pl-s"&gt;"weight"&lt;/span&gt;, &lt;span class="pl-s1"&gt;float&lt;/span&gt;)&lt;/pre&gt;
&lt;p&gt;This defines a set of two migrations, one creating the &lt;code&gt;creatures&lt;/code&gt; table and another adding a column to it.&lt;/p&gt;
&lt;p&gt;You can then run those migrations either using Python:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-s1"&gt;db&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;Database&lt;/span&gt;(&lt;span class="pl-s"&gt;"creatures.db"&lt;/span&gt;)
&lt;span class="pl-s1"&gt;migrations&lt;/span&gt;.&lt;span class="pl-c1"&gt;apply&lt;/span&gt;(&lt;span class="pl-s1"&gt;db&lt;/span&gt;)&lt;/pre&gt;
&lt;p&gt;Or with the command-line &lt;code&gt;migrate&lt;/code&gt; command:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;sqlite-utils migrate creatures.db migrations.py&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The system is deliberately small: it doesn't provide reverse migrations, so any mistakes you make should be fixed by deploying a fresh migration to undo them.&lt;/p&gt;
&lt;p&gt;Its predecessor has been used by &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; and various other projects for several years, so I'm confident that the design is stable and works well.&lt;/p&gt;
&lt;p&gt;The new migrations feature &lt;a href="https://sqlite-utils.datasette.io/en/latest/migrations.html"&gt;is documented here&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="new-feature-db-atomic-transactions"&gt;New feature: db.atomic() transactions&lt;/h4&gt;
&lt;p&gt;This feature is a lot less exercised than migrations, so it deserves more attention from testers.&lt;/p&gt;
&lt;p&gt;Previously, &lt;code&gt;sqlite-utils&lt;/code&gt; mostly left transaction management up to its users, via a &lt;code&gt;with db.conn:&lt;/code&gt; construct that reused the &lt;code&gt;sqlite3&lt;/code&gt; mechanism directly.&lt;/p&gt;
&lt;p&gt;SQLite supports nested transactions in the form of savepoints, so I wanted an abstraction that could make those as easy to use as possible.&lt;/p&gt;
&lt;p&gt;I borrowed the terminology "atomic" from Django and Peewee. Here's what the new API looks like:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;with&lt;/span&gt; &lt;span class="pl-s1"&gt;db&lt;/span&gt;.&lt;span class="pl-c1"&gt;atomic&lt;/span&gt;():
    &lt;span class="pl-s1"&gt;db&lt;/span&gt;.&lt;span class="pl-c1"&gt;table&lt;/span&gt;(&lt;span class="pl-s"&gt;"dogs"&lt;/span&gt;).&lt;span class="pl-c1"&gt;insert&lt;/span&gt;({&lt;span class="pl-s"&gt;"id"&lt;/span&gt;: &lt;span class="pl-c1"&gt;1&lt;/span&gt;, &lt;span class="pl-s"&gt;"name"&lt;/span&gt;: &lt;span class="pl-s"&gt;"Cleo"&lt;/span&gt;}, &lt;span class="pl-s1"&gt;pk&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"id"&lt;/span&gt;)
    &lt;span class="pl-k"&gt;try&lt;/span&gt;:
        &lt;span class="pl-k"&gt;with&lt;/span&gt; &lt;span class="pl-s1"&gt;db&lt;/span&gt;.&lt;span class="pl-c1"&gt;atomic&lt;/span&gt;():
            &lt;span class="pl-s1"&gt;db&lt;/span&gt;.&lt;span class="pl-c1"&gt;table&lt;/span&gt;(&lt;span class="pl-s"&gt;"dogs"&lt;/span&gt;).&lt;span class="pl-c1"&gt;insert&lt;/span&gt;({&lt;span class="pl-s"&gt;"id"&lt;/span&gt;: &lt;span class="pl-c1"&gt;2&lt;/span&gt;, &lt;span class="pl-s"&gt;"name"&lt;/span&gt;: &lt;span class="pl-s"&gt;"Pancakes"&lt;/span&gt;})
            &lt;span class="pl-k"&gt;raise&lt;/span&gt; &lt;span class="pl-en"&gt;ValueError&lt;/span&gt;(&lt;span class="pl-s"&gt;"skip this one"&lt;/span&gt;)
    &lt;span class="pl-k"&gt;except&lt;/span&gt; &lt;span class="pl-v"&gt;ValueError&lt;/span&gt;:
        &lt;span class="pl-k"&gt;pass&lt;/span&gt;
    &lt;span class="pl-s1"&gt;db&lt;/span&gt;.&lt;span class="pl-c1"&gt;table&lt;/span&gt;(&lt;span class="pl-s"&gt;"dogs"&lt;/span&gt;).&lt;span class="pl-c1"&gt;insert&lt;/span&gt;({&lt;span class="pl-s"&gt;"id"&lt;/span&gt;: &lt;span class="pl-c1"&gt;3&lt;/span&gt;, &lt;span class="pl-s"&gt;"name"&lt;/span&gt;: &lt;span class="pl-s"&gt;"Marnie"&lt;/span&gt;})&lt;/pre&gt;
&lt;p&gt;More details &lt;a href="https://sqlite-utils.datasette.io/en/latest/python-api.html#transactions-with-db-atomic"&gt;in the documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="backwards-incompatible-changes"&gt;Backwards incompatible changes&lt;/h4&gt;
&lt;p&gt;The backwards incompatible changes in v4 were described in the alpha release notes. For &lt;a href="https://sqlite-utils.datasette.io/en/latest/changelog.html#a0-2025-05-08"&gt;4.0a0&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Upsert operations now use SQLite's &lt;code&gt;INSERT ... ON CONFLICT SET&lt;/code&gt; syntax on all SQLite versions later than 3.23.1. This is a very slight breaking change for apps that depend on the previous &lt;code&gt;INSERT OR IGNORE&lt;/code&gt; followed by &lt;code&gt;UPDATE&lt;/code&gt; behavior. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/652"&gt;#652&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Python library users can opt-in to the previous implementation by passing &lt;code&gt;use_old_upsert=True&lt;/code&gt; to the &lt;code&gt;Database()&lt;/code&gt; constructor, see &lt;a href="https://sqlite-utils.datasette.io/en/latest/python-api.html#python-api-old-upsert"&gt;Alternative upserts using INSERT OR IGNORE&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Dropped support for Python 3.8, added support for Python 3.13. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/646"&gt;#646&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sqlite-utils tui&lt;/code&gt; is now provided by the &lt;a href="https://github.com/simonw/sqlite-utils-tui"&gt;sqlite-utils-tui&lt;/a&gt; plugin. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/648"&gt;#648&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Test suite now also runs against SQLite 3.23.1, the last version (from 2018-04-10) before the new &lt;code&gt;INSERT ... ON CONFLICT SET&lt;/code&gt; syntax was added. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/654"&gt;#654&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;And for &lt;a href="https://sqlite-utils.datasette.io/en/latest/changelog.html#a1-2025-11-23"&gt;4.0a1&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Breaking change&lt;/strong&gt;: The &lt;code&gt;db.table(table_name)&lt;/code&gt; method now only works with tables. To access a SQL view use &lt;code&gt;db.view(view_name)&lt;/code&gt; instead. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/657"&gt;#657&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;table.insert_all()&lt;/code&gt; and &lt;code&gt;table.upsert_all()&lt;/code&gt; methods can now accept an iterator of lists or tuples as an alternative to dictionaries. The first item should be a list/tuple of column names. See &lt;a href="https://sqlite-utils.datasette.io/en/latest/python-api.html#python-api-insert-lists"&gt;Inserting data from a list or tuple iterator&lt;/a&gt; for details. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/672"&gt;#672&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Breaking change&lt;/strong&gt;: The default floating point column type has been changed from &lt;code&gt;FLOAT&lt;/code&gt; to &lt;code&gt;REAL&lt;/code&gt;, which is the correct SQLite type for floating point values. This affects auto-detected columns when inserting data. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/645"&gt;#645&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Now uses &lt;code&gt;pyproject.toml&lt;/code&gt; in place of &lt;code&gt;setup.py&lt;/code&gt; for packaging. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/675"&gt;#675&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Tables in the Python API now do a much better job of remembering the primary key and other schema details from when they were first created. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/655"&gt;#655&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Breaking change&lt;/strong&gt;: The &lt;code&gt;table.convert()&lt;/code&gt; and &lt;code&gt;sqlite-utils convert&lt;/code&gt; mechanisms no longer skip values that evaluate to &lt;code&gt;False&lt;/code&gt;. Previously the &lt;code&gt;--skip-false&lt;/code&gt; option was needed, this has been removed. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/542"&gt;#542&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Breaking change&lt;/strong&gt;: Tables created by this library now wrap table and column names in &lt;code&gt;"double-quotes"&lt;/code&gt; in the schema. Previously they would use &lt;code&gt;[square-braces]&lt;/code&gt;. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/677"&gt;#677&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;--functions&lt;/code&gt; CLI argument now accepts a path to a Python file in addition to accepting a string full of Python code. It can also now be specified multiple times. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/659"&gt;#659&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Breaking change:&lt;/strong&gt; Type detection is now the default behavior for the &lt;code&gt;insert&lt;/code&gt; and &lt;code&gt;upsert&lt;/code&gt; CLI commands when importing CSV or TSV data. Previously all columns were treated as &lt;code&gt;TEXT&lt;/code&gt; unless the &lt;code&gt;--detect-types&lt;/code&gt; flag was passed. Use the new &lt;code&gt;--no-detect-types&lt;/code&gt; flag to restore the old behavior. The &lt;code&gt;SQLITE_UTILS_DETECT_TYPES&lt;/code&gt; environment variable has been removed. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/679"&gt;#679&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h4 id="try-it-out"&gt;Try it out&lt;/h4&gt;
&lt;p&gt;You can install the new RC like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;pip install sqlite-utils==4.0rc1&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Or try the CLI version directly with &lt;code&gt;uvx&lt;/code&gt; like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;uvx --with sqlite-utils==4.0rc1 sqlite-utils --help&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Come chat with us about it in the &lt;a href="https://discord.gg/Ass7bCAMDw"&gt;sqlite-utils Discord channel&lt;/a&gt;, or file any bugs in &lt;a href="https://github.com/simonw/sqlite-utils/issues"&gt;GitHub Issues&lt;/a&gt;.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/annotated-release-notes"&gt;annotated-release-notes&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="schema-migrations"/><category term="projects"/><category term="sqlite"/><category term="sqlite-utils"/><category term="annotated-release-notes"/></entry><entry><title>Django: Test for pending migrations</title><link href="https://simonwillison.net/2024/Jun/28/django-test-for-pending-migrations/#atom-tag" rel="alternate"/><published>2024-06-28T15:23:00+00:00</published><updated>2024-06-28T15:23:00+00:00</updated><id>https://simonwillison.net/2024/Jun/28/django-test-for-pending-migrations/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://adamj.eu/tech/2024/06/23/django-test-pending-migrations/"&gt;Django: Test for pending migrations&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Neat recipe from Adam Johnson for adding an automated test to your Django test suite that runs &lt;code&gt;manage.py makemigrations --check&lt;/code&gt; to ensure you don't accidentally land code that deploys with a missing migration and crashes your site. I've made this mistake before myself so I'll be adding this to my projects.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://fosstodon.org/@adamchainz/112687118729636820"&gt;@adamchainz&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/adam-johnson"&gt;adam-johnson&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="schema-migrations"/><category term="adam-johnson"/></entry><entry><title>pgroll</title><link href="https://simonwillison.net/2024/Jan/30/pgroll/#atom-tag" rel="alternate"/><published>2024-01-30T21:27:13+00:00</published><updated>2024-01-30T21:27:13+00:00</updated><id>https://simonwillison.net/2024/Jan/30/pgroll/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/xataio/pgroll"&gt;pgroll&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
“Zero-downtime, reversible, schema migrations for Postgres”&lt;/p&gt;

&lt;p&gt;I love this kind of thing. This one is has a really interesting design: you define your schema modifications (adding/dropping columns, creating tables etc) using a JSON DSL, then apply them using a Go binary.&lt;/p&gt;

&lt;p&gt;When you apply a migration the tool first creates a brand new PostgreSQL schema (effectively a whole new database) which imitates your new schema design using PostgreSQL views. You can then point your applications that have been upgraded to the new schema at it, using the PostgreSQL search_path setting.&lt;/p&gt;

&lt;p&gt;Old applications can continue talking to the previous schema design, giving you an opportunity to roll out a zero-downtime deployment of the new code.&lt;/p&gt;

&lt;p&gt;Once your application has upgraded and the physical rows in the database have been transformed to the new schema you can run a --continue command to make the final destructive changes and drop the mechanism that simulates both schema designs at once.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://lobste.rs/s/buhd4e/postgresql_zero_downtime_reversible"&gt;lobste.rs&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zero-downtime"&gt;zero-downtime&lt;/a&gt;&lt;/p&gt;



</summary><category term="schema-migrations"/><category term="postgresql"/><category term="zero-downtime"/></entry><entry><title>Stripe: Online migrations at scale</title><link href="https://simonwillison.net/2023/Nov/5/online-migrations-at-scale/#atom-tag" rel="alternate"/><published>2023-11-05T16:06:32+00:00</published><updated>2023-11-05T16:06:32+00:00</updated><id>https://simonwillison.net/2023/Nov/5/online-migrations-at-scale/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://stripe.com/blog/online-migrations"&gt;Stripe: Online migrations at scale&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
This 2017 blog entry from Jacqueline Xu at Stripe provides a very clear description of the “dual writes” pattern for applying complex data migrations without downtime: dual write to new and old tables, update the read paths, update the write paths and finally remove the now obsolete data—illustrated with an example of upgrading customers from having a single to multiple subscriptions.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/eatonphil/status/1721195409647829052"&gt;@eatonphil&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/databases"&gt;databases&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/migrations"&gt;migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zero-downtime"&gt;zero-downtime&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/stripe"&gt;stripe&lt;/a&gt;&lt;/p&gt;



</summary><category term="databases"/><category term="migrations"/><category term="schema-migrations"/><category term="zero-downtime"/><category term="stripe"/></entry><entry><title>Database Migrations</title><link href="https://simonwillison.net/2023/Oct/1/database-migrations/#atom-tag" rel="alternate"/><published>2023-10-01T23:55:25+00:00</published><updated>2023-10-01T23:55:25+00:00</updated><id>https://simonwillison.net/2023/Oct/1/database-migrations/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://vadimkravcenko.com/shorts/database-migrations/"&gt;Database Migrations&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Vadim Kravcenko provides a useful, in-depth description of the less obvious challenges of applying database migrations successfully. Vadim uses and likes Django’s migrations (as do I) but notes that running them at scale still involves a number of thorny challenges.&lt;/p&gt;

&lt;p&gt;The biggest of these, which I’ve encountered myself multiple times, is that if you want truly zero downtime deploys you can’t guarantee that your schema migrations will be deployed at the exact same instant as changes you make to your application code.&lt;/p&gt;

&lt;p&gt;This means all migrations need to be forward-compatible: you need to apply a schema change in a way that your existing code will continue to work error-free, then ship the related code change as a separate operation.&lt;/p&gt;

&lt;p&gt;Vadim describes what this looks like in detail for a number of common operations: adding a field, removing a field and changing a field that has associated business logic implications. He also discusses the importance of knowing when to deploy a dual-write strategy.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/databases"&gt;databases&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ops"&gt;ops&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zero-downtime"&gt;zero-downtime&lt;/a&gt;&lt;/p&gt;



</summary><category term="databases"/><category term="django"/><category term="schema-migrations"/><category term="ops"/><category term="zero-downtime"/></entry><entry><title>Sqitch tutorial for SQLite</title><link href="https://simonwillison.net/2022/Jul/24/sqitch-tutorial-for-sqlite/#atom-tag" rel="alternate"/><published>2022-07-24T23:44:34+00:00</published><updated>2022-07-24T23:44:34+00:00</updated><id>https://simonwillison.net/2022/Jul/24/sqitch-tutorial-for-sqlite/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://sqitch.org/docs/manual/sqitchtutorial-sqlite/"&gt;Sqitch tutorial for SQLite&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Sqitch is an interesting implementation of database migrations: it’s a command-line tool written in Perl with an interface similar to Git, providing commands to create, run, revert and track migration scripts. The scripts the selves are written as SQL in whichever database engine you are using. The tutorial for SQLite gives a good idea as to how the whole system works.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/pdcawley/status/1551262845521854467"&gt;Piers Cawley&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/databases"&gt;databases&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;&lt;/p&gt;



</summary><category term="databases"/><category term="schema-migrations"/><category term="sqlite"/></entry><entry><title>Simple declarative schema migration for SQLite</title><link href="https://simonwillison.net/2022/May/3/simple-declarative-schema-migration-for-sqlite/#atom-tag" rel="alternate"/><published>2022-05-03T18:07:21+00:00</published><updated>2022-05-03T18:07:21+00:00</updated><id>https://simonwillison.net/2022/May/3/simple-declarative-schema-migration-for-sqlite/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://david.rothlis.net/declarative-schema-migration-for-sqlite/"&gt;Simple declarative schema migration for SQLite&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
This is an interesting, clearly explained approach to the database migration problem. Create a new in-memory database and apply the current schema, then run some code to compare that with the previous schema—which tables are new, and which tables have had columns added. Then apply those changes.&lt;/p&gt;

&lt;p&gt;I’d normally be cautious of running something like this because I can think of ways it could go wrong—but SQLite backups are so quick and cheap (just copy the file) that I could see this being a relatively risk-free way to apply migrations.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=31249823"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;&lt;/p&gt;



</summary><category term="schema-migrations"/><category term="sqlite"/></entry><entry><title>migra</title><link href="https://simonwillison.net/2022/Feb/26/migra/#atom-tag" rel="alternate"/><published>2022-02-26T23:23:05+00:00</published><updated>2022-02-26T23:23:05+00:00</updated><id>https://simonwillison.net/2022/Feb/26/migra/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/djrobstep/migra"&gt;migra&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
This looks like a very handy tool to have around: run “migra postgresql:///a postgresql:///b” and it will detect and output the SQL alter statements needed to modify the first PostgreSQL database schema to match the second. It’s written in Python, running on top of SQLAlchemy.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=30464882"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/databases"&gt;databases&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;&lt;/p&gt;



</summary><category term="databases"/><category term="schema-migrations"/><category term="postgresql"/></entry><entry><title>migrations.RunSQL.noop for reversible SQL migrations</title><link href="https://simonwillison.net/2021/May/2/migrations-runsql-noop/#atom-tag" rel="alternate"/><published>2021-05-02T17:48:46+00:00</published><updated>2021-05-02T17:48:46+00:00</updated><id>https://simonwillison.net/2021/May/2/migrations-runsql-noop/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;TIL:&lt;/strong&gt; &lt;a href="https://til.simonwillison.net/django/migrations-runsql-noop"&gt;migrations.RunSQL.noop for reversible SQL migrations&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="schema-migrations"/></entry><entry><title>Porting VaccinateCA to Django</title><link href="https://simonwillison.net/2021/Apr/12/porting-vaccinateca-to-django/#atom-tag" rel="alternate"/><published>2021-04-12T05:18:48+00:00</published><updated>2021-04-12T05:18:48+00:00</updated><id>https://simonwillison.net/2021/Apr/12/porting-vaccinateca-to-django/#atom-tag</id><summary type="html">
    &lt;p&gt;As I mentioned &lt;a href="https://simonwillison.net/2021/Feb/28/vaccinateca/"&gt;back in February&lt;/a&gt;, I've been working with the &lt;a href="https://www.vaccinateca.com/"&gt;VaccinateCA&lt;/a&gt; project to try to bring the pandemic to an end a little earlier by helping gather as accurate a model as possible of where the Covid vaccine is available in California and how people can get it.&lt;/p&gt;
&lt;p&gt;The key activity at VaccinateCA is calling places to check on their availability and eligibility criteria. Up until last night this was powered by a heavily customized Airtable instance, accompanied by a custom JavaScript app for the callers that communicated with the Airtable API via some Netlify functions.&lt;/p&gt;
&lt;p&gt;Today, the flow is powered by a new custom Django backend, running on top of PostgreSQL.&lt;/p&gt;
&lt;h4&gt;The thing you should never do&lt;/h4&gt;
&lt;blockquote class="twitter-tweet"&gt;&lt;p lang="en" dir="ltr"&gt;Here&amp;#39;s one that took me fifteen years to learn: &amp;quot;let&amp;#39;s build a new thing and replace this&amp;quot; is hideously dangerous: 90% of the time you won&amp;#39;t fully replace the old thing, and now you have two problems!&lt;/p&gt;- Simon Willison (@simonw) &lt;a href="https://twitter.com/simonw/status/1145114228170190848?ref_src=twsrc%5Etfw"&gt;June 29, 2019&lt;/a&gt;&lt;/blockquote&gt;
&lt;p&gt;Replacing an existing system with a from-scratch rewrite is risky. Replacing a system that is built on something as flexible as Airtable that is evolving on a daily basis is positively terrifying!&lt;/p&gt;
&lt;p&gt;Airtable served us extremely well, but unfortunately there are hard limits to the number of rows Airtable can handle and we've already bounced up against them and had to archive some of our data. To keep scaling the organization we needed to migrate away.&lt;/p&gt;
&lt;p&gt;We needed to build a matching relational database with a comprehensive, permission-controlled interface for editing it, plus APIs to drive our website and application. And we needed to do it using the most &lt;a href="http://boringtechnology.club/"&gt;boring technology&lt;/a&gt; possible, so we could focus on solving problems directly rather than researching anything new.&lt;/p&gt;
&lt;p&gt;It will never cease to surprise me that Django has attained boring technology status! VaccineCA sits firmly in Django's sweet-spot. So we used that to build our replacement.&lt;/p&gt;
&lt;p&gt;The new Django-based system is called VIAL, for "Vaccine Information Archive and Library" - a neat &lt;a href="https://twitter.com/obra"&gt;Jesse Vincent&lt;/a&gt; bacronym.&lt;/p&gt;
&lt;p&gt;We switched things over to VIAL last night, but we still have activity in Airtable as well. I expect we'll keep using Airtable for the lifetime of the organization - there are plenty of ad-hoc data projects for which it's a perfect fit.&lt;/p&gt;
&lt;p&gt;The most important thing here is to have a trusted single point of truth for any piece of information. I'm not quite ready to declare victory on that point just yet, but hopefully once things settle down over the next few days.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2021/vial-index.png" style="max-width: 100%" alt="Screenshot of the Django admin VIAL index page" /&gt;&lt;/p&gt;

&lt;h4&gt;Data synchronization patterns&lt;/h4&gt;
&lt;p&gt;The first challenge, before even writing any code, was how to get stuff out of Airtable. I built a tool for this a while ago called &lt;a href="https://datasette.io/tools/airtable-export"&gt;airtable-export&lt;/a&gt;, and it turned out the VaccinateCA team were using it already before I joined!&lt;/p&gt;
&lt;p&gt;&lt;code&gt;airtable-export&lt;/code&gt; was already running several times an hour, backing up the data in JSON format to a GitHub repository (a form of &lt;a href="https://simonwillison.net/2020/Oct/9/git-scraping/"&gt;Git scraping&lt;/a&gt;). This gave us a detailed history of changes to the Airtable data, which occasionally proved extremely useful for answering questions about when a specific record was changed or deleted.&lt;/p&gt;
&lt;p&gt;Having the data in a GitHub repository was also useful because it gave us somewhere to pull data from that wasn't governed by Airtable's rate limits.&lt;/p&gt;
&lt;p&gt;I iterated through a number of different approaches for writing importers for the data.&lt;/p&gt;
&lt;p&gt;Each Airtable table ended up as a single JSON file in our GitHub repository, containing an array of objects - those files got pretty big, topping out at about 80MB.&lt;/p&gt;
&lt;p&gt;I started out with Django management commands, which could be passed a file or a URL. A neat thing about using GitHub for this is that you can use the "raw data" link to obtain a URL with a short-lived token, which grants access to that file. So I could create a short-term URL and paste it directly to my import tool.&lt;/p&gt;
&lt;p&gt;I don't have a good pattern for running Django management commands on Google Cloud Run, so I started moving to API-based import scripts instead.&lt;/p&gt;
&lt;p&gt;The pattern that ended up working best was to provide a &lt;code&gt;/api/importRecords&lt;/code&gt; API endpoint which accepts a JSON array of items.&lt;/p&gt;
&lt;p&gt;The API expects the input to have a unique primary key in each record - &lt;code&gt;airtable_id&lt;/code&gt; in our case. It then uses Django's &lt;a href="https://docs.djangoproject.com/en/3.2/ref/models/querysets/#update-or-create"&gt;update_or_create()&lt;/a&gt; ORM method to create new records if they were missing, and update existing records otherwise.&lt;/p&gt;
&lt;p&gt;One remaining challenge: posting 80MB of JSON to an API in one go would likely run into resource limits. I needed a way to break that input up into smaller batches.&lt;/p&gt;
&lt;p&gt;I ended up building a new tool for this called &lt;a href="https://github.com/simonw/json-post"&gt;json-post&lt;/a&gt;. It has an extremely specific use-case: it's for when you want to POST a big JSON array to an API endpoint but you want to first break it up into batches!&lt;/p&gt;
&lt;p&gt;Here's how to break up the JSON in &lt;code&gt;Reports.json&lt;/code&gt; into 50 item arrays and send them to that API as separate POSTs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;json-post Reports.json \                              
   "https://example.com/api/importReports" \
   --batch-size 50
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here are some more complex options. Here we need to pass an &lt;code&gt;Authorization: Bearer XXXtokenXXX&lt;/code&gt; API key header, run the array in reverse, record our progress (the JSON responses from the API as newline-delimited JSON) to a log file, set a longer HTTP read timeout and filter for just specific items:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;% json-post Reports.json \                              
   "https://example.com/api/importReports" \
  -h Authorization 'Bearer XXXtokenXXX' \
  --batch-size 50 \
  --reverse \
  --log /tmp/progress.txt \
  --http-read-timeout 20 \
  --filter 'item.get("is_soft_deleted")'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;--filter&lt;/code&gt; option proved particularly useful. As we kicked the tires on VIAL we would spot new bugs - things like the import script failing to correctly record the &lt;code&gt;is_soft_deleted&lt;/code&gt; field we were using in Airtable. Being able to filter that input file with a command-line flag meant we could easily re-run the import just for a subset of reports that were affected by a particular bug.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--filter&lt;/code&gt; takes a Python expression that gets compiled into a function and passed &lt;code&gt;item&lt;/code&gt; as the current item in the list. I borrowed the pattern from &lt;a href="https://datasette.io/tools/sqlite-transform#user-content-lambda-for-executing-your-own-code"&gt;my sqlite-transform tool&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="value-of-api-logs"&gt;The value of API logs&lt;/h4&gt;
&lt;p&gt;VaccineCA's JavaScript caller application used to send data to Airtable via a Netlify function, which allowed additional authentication to be added built using  Auth0.&lt;/p&gt;
&lt;p&gt;Back in February, the team had the bright idea to log the API traffic to that function to a separate base in Airtable - including full request and response bodies.&lt;/p&gt;
&lt;p&gt;This proved invaluable for debugging. It also meant that when I started building VIAL's alternative implementation of the "submit a call report" API I could replay historic API traffic that had been recorded in that table, giving me a powerful way to exercise the new API with real-world traffic.&lt;/p&gt;
&lt;p&gt;This meant that when we turned on VIAL we could switch our existing JavaScript SPA over to talking to it using a fully tested clone of the existing Airtable-backed API.&lt;/p&gt;
&lt;p&gt;VIAL implements this logging pattern again, this time using Django and PostgreSQL.&lt;/p&gt;
&lt;p&gt;Given that the writable APIs will recieve in the low thousands of requests a day, keeping them in a database table works great. The table has grown to 90MB so far. I'm hoping that the pandemic will be over before we have to worry about logging capacity!&lt;/p&gt;
&lt;p&gt;We're using PostgreSQL &lt;code&gt;jsonb&lt;/code&gt; columns to store the incoming and returned JSON, via Django's &lt;a href="https://docs.djangoproject.com/en/3.2/ref/models/fields/#jsonfield"&gt;JSONField&lt;/a&gt;. This means we can do in-depth API analysis using PostgreSQL's JSON SQL functions! Being able to examine returned JSON error messages or aggregate across incoming request bodies helped enormously when debugging problems with the API import scripts.&lt;/p&gt;
&lt;h4&gt;Storing the original JSON&lt;/h4&gt;
&lt;p&gt;Today, almost all of the data stored in VIAL originated in Airtable. One trick that has really helped build the system is that each of the tables that might contain imported data has both an &lt;code&gt;airtable_id&lt;/code&gt; nullable column and an &lt;code&gt;import_json&lt;/code&gt; JSON field.&lt;/p&gt;
&lt;p&gt;Any time we import a record from Airtable, we record both the ID and the full, original Airtable JSON that we used for the import.&lt;/p&gt;
&lt;p&gt;This is another powerful tool for debugging: we can view the original Airtable JSON directly in the Django admin interface for a record, and confirm that it matches the ORM fields that we set from that.&lt;/p&gt;
&lt;p&gt;I came up with a simple pattern for &lt;a href="https://til.simonwillison.net/django/pretty-print-json-admin"&gt;Pretty-printing all read-only JSON in the Django admin&lt;/a&gt; that helps with this too.&lt;/p&gt;
&lt;h4&gt;Staying as flexible as possible&lt;/h4&gt;
&lt;p&gt;The thing that worried me most about replacing Airtable with Django was Airtable's incredible flexibility. In the organization's short life it has already solved &lt;em&gt;so many&lt;/em&gt; problems by adding new columns in Airtable, or building new views.&lt;/p&gt;
&lt;p&gt;Is it possible to switch to custom software without losing that huge cultural advantage?&lt;/p&gt;
&lt;p&gt;This is the same reason it's so hard for custom software to compete with spreadsheets.&lt;/p&gt;
&lt;p&gt;We've only just made the switch, so we won't know for a while how well we've done at handling this. I have a few mechanisms in place that I'm hoping will help.&lt;/p&gt;
&lt;p&gt;The first is &lt;a href="https://github.com/simonw/django-sql-dashboard"&gt;django-sql-dashboard&lt;/a&gt;. I wrote about this project in previous weeknotes &lt;a href="https://simonwillison.net/2021/Mar/14/weeknotes/"&gt;here&lt;/a&gt; and &lt;a href="https://simonwillison.net/2021/Mar/21/django-sql-dashboard-widgets/"&gt;here&lt;/a&gt; - the goal is to bring some of the ideas from &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; into the Django/PostgreSQL world, by providing a read-only mechanism for constructing SQL queries, bookmarking and saving the results and outputting simple SQL-driven visualizations.&lt;/p&gt;
&lt;p&gt;We have a lot of SQL knowledge at VaccinateCA, so my hope is that people with SQL will be able to solve their own problems, and people who don't know SQL yet will have no trouble finding someone who can help them.&lt;/p&gt;
&lt;p&gt;In the &lt;a href="http://boringtechnology.club/#17"&gt;boring technology&lt;/a&gt; model of things, &lt;code&gt;django-sql-dashboard&lt;/code&gt; counts as the main innovation token I'm spending for this project. I'm optimistic that it will pay off.&lt;/p&gt;
&lt;p&gt;I'm also leaning heavily on Django's migration system, with the aim of making database migrations common and boring, rather than their usual default of being rare and exciting. We're up to 77 migrations already, in a codebase that is just over two months old!&lt;/p&gt;
&lt;p&gt;I think a culture that evolves the database schema quickly and with as little drama as possible is crucial to maintaining the agility that this kind of organization needs.&lt;/p&gt;
&lt;p&gt;Aside from the Django Admin providing the editing interface, everything that comes into and goes out of VIAL happens through APIs. These are fully documented: I want people to be able to build against the APIs independently, especially for things like data import.&lt;/p&gt;
&lt;p&gt;After seeing significant success with PostgreSQL JSON already, I'm considering using it to add even more API-driven flexbility to VIAL in the future. Allowing our client developers to start collecting a new piece of data from our volunteers in an existing JSON field, then migrating that into a separate column once it has proven its value, is very tempting indeed.&lt;/p&gt;
&lt;h4&gt;Open source tools we are using&lt;/h4&gt;
&lt;p&gt;An incomplete list of open source packages we are using for VIAL so far:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://pydantic-docs.helpmanual.io/"&gt;pydantic&lt;/a&gt; - as a validation layer for some of the API endpoints&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/python-social-auth/social-app-django"&gt;social-auth-app-django&lt;/a&gt; - to integrate with &lt;a href="https://auth0.com/"&gt;Auth0&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/adamchainz/django-cors-headers"&gt;django-cors-headers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/mpdavis/python-jose"&gt;python-jose&lt;/a&gt; - for JWTs, which were already in use by our Airtable caller app&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/etianen/django-reversion"&gt;django-reversion&lt;/a&gt; and &lt;a href="https://github.com/jedie/django-reversion-compare/"&gt;django-reversion-compare&lt;/a&gt; to provide a diffable, revertable history of some of our core models&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/django-admin-tools/django-admin-tools"&gt;django-admin-tools&lt;/a&gt; - which adds a handy customizable menu to the admin, good for linking to additional custom tools&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/3YOURMIND/django-migration-linter"&gt;django-migration-linter&lt;/a&gt; - to help avoid accidentally shipping migrations that could cause downtime during a deploy&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://pytest-django.readthedocs.io/en/latest/"&gt;pytest-django&lt;/a&gt;, &lt;a href="https://github.com/adamchainz/time-machine"&gt;time-machine&lt;/a&gt; and &lt;a href="https://colin-b.github.io/pytest_httpx/"&gt;pytest-httpx&lt;/a&gt; for our unit tests&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.sentry.io/platforms/python/"&gt;sentry-sdk&lt;/a&gt;, &lt;a href="https://docs.honeycomb.io/getting-data-in/python/beeline/"&gt;honeycomb-beeline&lt;/a&gt; and  &lt;a href="https://github.com/prometheus/client_python"&gt;prometheus-client&lt;/a&gt; for error logging and observability&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Want to help out?&lt;/h4&gt;
&lt;p&gt;VaccinateCA &lt;a href="https://twitter.com/patio11/status/1379587878624190466"&gt;is hiring&lt;/a&gt;! It's an interesting gig, because the ultimate goal is to end the pandemic and put this non-profit permanently out of business. So if you want to help end things faster, get in touch.&lt;/p&gt;

&lt;blockquote class="twitter-tweet"&gt;&lt;p lang="en" dir="ltr"&gt;VaccinateCA is hiring a handful of engineers to help scale our data ingestion and display by more than an order of magnitude.&lt;br /&gt;&lt;br /&gt;If you&amp;#39;d like to register interest:&lt;a href="https://t.co/BSvi40sW1M"&gt;https://t.co/BSvi40sW1M&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;Generalists welcome. Three subprojects; Python backend/pedestrian front-end JS.&lt;/p&gt;- Patrick McKenzie (@patio11) &lt;a href="https://twitter.com/patio11/status/1379587878624190466?ref_src=twsrc%5Etfw"&gt;April 7, 2021&lt;/a&gt;&lt;/blockquote&gt;
&lt;h4&gt;TIL this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/til/blob/main/vscode/language-specific-indentation-settings.md"&gt;Language-specific indentation settings in VS Code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/til/blob/main/django/efficient-bulk-deletions-in-django.md"&gt;Efficient bulk deletions in Django&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/til/blob/main/postgresql/unnest-csv.md"&gt;Using unnest() to use a comma-separated string as the input to an IN query&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Releases this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/json-post"&gt;json-post&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/json-post/releases/tag/0.2"&gt;0.2&lt;/a&gt; - (&lt;a href="https://github.com/simonw/json-post/releases"&gt;3 total releases&lt;/a&gt;) - 2021-04-11
&lt;br /&gt;Tool for posting JSON to an API, broken into pages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/airtable-export"&gt;airtable-export&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/airtable-export/releases/tag/0.7.1"&gt;0.7.1&lt;/a&gt; - (&lt;a href="https://github.com/simonw/airtable-export/releases"&gt;10 total releases&lt;/a&gt;) - 2021-04-09
&lt;br /&gt;Export Airtable data to YAML, JSON or SQLite files on disk&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/django-sql-dashboard"&gt;django-sql-dashboard&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/django-sql-dashboard/releases/tag/0.6a0"&gt;0.6a0&lt;/a&gt; - (&lt;a href="https://github.com/simonw/django-sql-dashboard/releases"&gt;13 total releases&lt;/a&gt;) - 2021-04-09
&lt;br /&gt;Django app for building dashboards using raw SQL queries&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django-admin"&gt;django-admin&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/migrations"&gt;migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/airtable"&gt;airtable&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vaccinate-ca"&gt;vaccinate-ca&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/boring-technology"&gt;boring-technology&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="django"/><category term="django-admin"/><category term="migrations"/><category term="schema-migrations"/><category term="postgresql"/><category term="weeknotes"/><category term="airtable"/><category term="vaccinate-ca"/><category term="boring-technology"/></entry><entry><title>How to Create an Index in Django Without Downtime</title><link href="https://simonwillison.net/2019/Apr/11/index-in-django-without-downtime/#atom-tag" rel="alternate"/><published>2019-04-11T15:06:09+00:00</published><updated>2019-04-11T15:06:09+00:00</updated><id>https://simonwillison.net/2019/Apr/11/index-in-django-without-downtime/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://realpython.com/create-django-index-without-downtime/"&gt;How to Create an Index in Django Without Downtime&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Excellent advanced tutorial on Django migrations, which uses a desire to create indexes in PostgreSQL without locking the table (with CREATE INDEX CONCURRENTLY) to explain the SeparateDatabaseAndState and atomic features of Django’s migration framework.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/webology/status/1116109854492516353"&gt;Jeff Triplett&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zero-downtime"&gt;zero-downtime&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="schema-migrations"/><category term="postgresql"/><category term="zero-downtime"/></entry><entry><title>github/gh-ost: Thoughts on Foreign Keys?</title><link href="https://simonwillison.net/2018/Jun/19/thoughts-on-foreign-keys/#atom-tag" rel="alternate"/><published>2018-06-19T16:12:42+00:00</published><updated>2018-06-19T16:12:42+00:00</updated><id>https://simonwillison.net/2018/Jun/19/thoughts-on-foreign-keys/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/github/gh-ost/issues/331"&gt;github/gh-ost: Thoughts on Foreign Keys?&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The biggest challenge I’ve seen with foreign key constraints at scale (at least with MySQL) is how they conflict with online schema migrations using tools like pt-online-schema-change or GitHub’s gh-ost. This is a good explanation of the issue by Shlomi Noach, one of the gh-ost maintainers.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/databases"&gt;databases&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mysql"&gt;mysql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sql"&gt;sql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/shlominoach"&gt;shlominoach&lt;/a&gt;&lt;/p&gt;



</summary><category term="databases"/><category term="schema-migrations"/><category term="mysql"/><category term="scaling"/><category term="sql"/><category term="shlominoach"/></entry><entry><title>How Balanced does Database Migrations with Zero-Downtime</title><link href="https://simonwillison.net/2017/Nov/7/how-balanced-does-database-migrations-with-zero-downtime/#atom-tag" rel="alternate"/><published>2017-11-07T11:36:25+00:00</published><updated>2017-11-07T11:36:25+00:00</updated><id>https://simonwillison.net/2017/Nov/7/how-balanced-does-database-migrations-with-zero-downtime/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://blog.balancedpayments.com/payments-infrastructure-suspending-traffic-zero-downtime-migrations/"&gt;How Balanced does Database Migrations with Zero-Downtime&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I’m fascinated by the idea of “pausing” traffic during a blocking site maintenance activity (like a database migration) and then un-pausing when the operation is complete—so end clients just see some of their requests taking a few seconds longer than expected. I first saw this trick described by Braintree. Balanced wrote about a neat way of doing this just using HAproxy, which lets you live reconfigure the maxconns to your backend down to zero (causing traffic to be queued up) and then bring the setting back up again a few seconds later to un-pause those requests.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/haproxy"&gt;haproxy&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/highavailability"&gt;highavailability&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/http"&gt;http&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/zero-downtime"&gt;zero-downtime&lt;/a&gt;&lt;/p&gt;



</summary><category term="haproxy"/><category term="highavailability"/><category term="http"/><category term="schema-migrations"/><category term="scaling"/><category term="zero-downtime"/></entry><entry><title>What tools and techniques are used for relational database version control (structure and data)?</title><link href="https://simonwillison.net/2012/Dec/24/what-tools-and-techniques/#atom-tag" rel="alternate"/><published>2012-12-24T12:29:00+00:00</published><updated>2012-12-24T12:29:00+00:00</updated><id>https://simonwillison.net/2012/Dec/24/what-tools-and-techniques/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;em&gt;My answer to &lt;a href="https://www.quora.com/What-tools-and-techniques-are-used-for-relational-database-version-control-structure-and-data/answer/Simon-Willison"&gt;What tools and techniques are used for relational database version control (structure and data)?&lt;/a&gt; on Quora&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The term you are looking for is database migrations (sometimes called database change scripts).&lt;/p&gt;

&lt;p&gt;The basic concept is pretty straight forward: you set up a table in the database that records which change scripts have already been applied. When you need to make a change (adding a column, adding a table, denormalising some data for performance reasons, adding an index etc) you write a change script that applies the change - in raw SQL or in another programming language, depending on how your migration system is set up.&lt;/p&gt;

&lt;p&gt;These change scripts (let's call them migrations from here) are numbered so they can be applied in the correct order. Then you run a command which checks for scripts that have not yet been applied and runs them in the correct order - then records that they have been run to the relevant database table.&lt;/p&gt;

&lt;p&gt;The setup I've described above is a pretty good start. Some systems let you have reversible migrations: each migration includes instructions for reversing its effect (removing the index that was added, moving data back to its old location) which lets you run a command to revert back to a previous database state. In practise this is a nice-to-have but not essential: many migrations are by their nature irreversible, but it can make development faster if you can easily try out and then revert a database structure change within your development environment.&lt;/p&gt;

&lt;p&gt;Really clever migration systems can even introspect your database, figure out what has changed and attempt to generate the migration scripts automatically! South, the most popular migration system for Django, does this with surprisingly good results for many cases.&lt;/p&gt;

&lt;p&gt;If you're interested in learning more, it's worth reading through the South documentation: &lt;span&gt;&lt;a href="http://south.readthedocs.org/en/latest/"&gt;http://south.readthedocs.org/en/...&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/databases"&gt;databases&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mysql"&gt;mysql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/oracle"&gt;oracle&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sql"&gt;sql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/quora"&gt;quora&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rdbms"&gt;rdbms&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="databases"/><category term="schema-migrations"/><category term="mysql"/><category term="oracle"/><category term="postgresql"/><category term="sql"/><category term="quora"/><category term="rdbms"/></entry><entry><title>Is South the best tool to use when doing database migrations in Django?</title><link href="https://simonwillison.net/2011/Feb/8/is-south-the-best/#atom-tag" rel="alternate"/><published>2011-02-08T16:30:00+00:00</published><updated>2011-02-08T16:30:00+00:00</updated><id>https://simonwillison.net/2011/Feb/8/is-south-the-best/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;em&gt;My answer to &lt;a href="https://www.quora.com/Is-South-the-best-tool-to-use-when-doing-database-migrations-in-Django/answer/Simon-Willison"&gt;Is South the best tool to use when doing database migrations in Django?&lt;/a&gt; on Quora&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Yes. And I say that as an author of another Django migrations tool (dmigrations) which offered a small subset of South's current functionality.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/quora"&gt;quora&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="django"/><category term="schema-migrations"/><category term="python"/><category term="quora"/></entry><entry><title>Why isn't the schema and data migration tool South included in Django by default?</title><link href="https://simonwillison.net/2011/Jan/7/why-isnt-the-schema/#atom-tag" rel="alternate"/><published>2011-01-07T09:45:00+00:00</published><updated>2011-01-07T09:45:00+00:00</updated><id>https://simonwillison.net/2011/Jan/7/why-isnt-the-schema/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;em&gt;My answer to &lt;a href="https://www.quora.com/Why-isnt-the-schema-and-data-migration-tool-South-included-in-Django-by-default/answer/Simon-Willison"&gt;Why isn&amp;#39;t the schema and data migration tool South included in Django by default?&lt;/a&gt; on Quora&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Because shipping things as part of Django means they can't have separate releases, which means you only get a new released version every 6-12 months. South is improving far faster than that.&lt;/p&gt;

&lt;p&gt;Guido van Rossum once described packages that are included in the Python standard library as having "one foot in the grave" - the API is frozen, and improvements are hard to ship. &lt;span&gt;&lt;a href="http://tarekziade.wordpress.com/2010/03/03/the-fate-of-distutils-pycon-summit-packaging-sprint-detailed-report/"&gt;http://tarekziade.wordpress.com/...&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;Update 22nd March 2013: South is now scheduled to be merged in to Django core, there's even a Kickstarter to help fund the development work: &lt;span&gt;&lt;a href="http://www.kickstarter.com/projects/andrewgodwin/schema-migrations-for-django"&gt;http://www.kickstarter.com/proje...&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/quora"&gt;quora&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="django"/><category term="schema-migrations"/><category term="quora"/></entry><entry><title>On Django And Migrations</title><link href="https://simonwillison.net/2010/Jun/2/migrations/#atom-tag" rel="alternate"/><published>2010-06-02T16:27:00+00:00</published><updated>2010-06-02T16:27:00+00:00</updated><id>https://simonwillison.net/2010/Jun/2/migrations/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.aeracode.org/2010/6/2/django-and-migrations/"&gt;On Django And Migrations&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
South author Andrew Godwin on the plans for migrations in Django. His excellent South migration library will be split in to two parts—one handling database abstraction, dependency resolution and history tracking and the other providing autodetection and the South user interface. The former will go in to Django proper, encouraging other migration libraries to share the same core abstractions.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/andrew-godwin"&gt;andrew-godwin&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/orm"&gt;orm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/south"&gt;south&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/recovered"&gt;recovered&lt;/a&gt;&lt;/p&gt;



</summary><category term="andrew-godwin"/><category term="django"/><category term="schema-migrations"/><category term="orm"/><category term="south"/><category term="recovered"/></entry><entry><title>South's Design</title><link href="https://simonwillison.net/2009/May/13/south/#atom-tag" rel="alternate"/><published>2009-05-13T12:30:45+00:00</published><updated>2009-05-13T12:30:45+00:00</updated><id>https://simonwillison.net/2009/May/13/south/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.aeracode.org/2009/5/9/souths-design/"&gt;South&amp;#x27;s Design&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Andrew Godwin explains why South resorts to parsing your models.py file in order to construct information about for creating automatic migrations.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/andrew-godwin"&gt;andrew-godwin&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/models"&gt;models&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/orm"&gt;orm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/parsing"&gt;parsing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/south"&gt;south&lt;/a&gt;&lt;/p&gt;



</summary><category term="andrew-godwin"/><category term="django"/><category term="schema-migrations"/><category term="models"/><category term="orm"/><category term="parsing"/><category term="python"/><category term="south"/></entry><entry><title>Southerly Breezes</title><link href="https://simonwillison.net/2009/Mar/15/aeracode/#atom-tag" rel="alternate"/><published>2009-03-15T13:17:20+00:00</published><updated>2009-03-15T13:17:20+00:00</updated><id>https://simonwillison.net/2009/Mar/15/aeracode/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.aeracode.org/2009/3/10/southerly-breezes/"&gt;Southerly Breezes&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Andrew Godwin is slowly assimilating the best ideas from other Django migration systems in to South—the latest additions include ORM Freezing from Migratory and automatic change detection. Exciting stuff.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/andrew-godwin"&gt;andrew-godwin&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/databases"&gt;databases&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/orm"&gt;orm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/south"&gt;south&lt;/a&gt;&lt;/p&gt;



</summary><category term="andrew-godwin"/><category term="databases"/><category term="django"/><category term="schema-migrations"/><category term="orm"/><category term="south"/></entry><entry><title>Gearshift</title><link href="https://simonwillison.net/2008/Sep/15/gearshift/#atom-tag" rel="alternate"/><published>2008-09-15T14:51:46+00:00</published><updated>2008-09-15T14:51:46+00:00</updated><id>https://simonwillison.net/2008/Sep/15/gearshift/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://m.ac.nz/gearshift/"&gt;Gearshift&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Whoa, a full migrations library written in JavaScript for Gears (which uses SQLite for its data store).


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/gears"&gt;gears&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gearshift"&gt;gearshift&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/google-gears"&gt;google-gears&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;&lt;/p&gt;



</summary><category term="gears"/><category term="gearshift"/><category term="google-gears"/><category term="javascript"/><category term="schema-migrations"/><category term="sqlite"/></entry><entry><title>dmigrations thread on Django Nashville</title><link href="https://simonwillison.net/2008/Sep/3/nashville/#atom-tag" rel="alternate"/><published>2008-09-03T22:36:39+00:00</published><updated>2008-09-03T22:36:39+00:00</updated><id>https://simonwillison.net/2008/Sep/3/nashville/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://groups.google.com/group/django-nashville/browse_frm/thread/976e624f4f23b735/"&gt;dmigrations thread on Django Nashville&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The Django Nashville Google Group is currently hosting the most interesting discussion of dmigrations.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/dmigrations"&gt;dmigrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/google-groups"&gt;google-groups&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/nashville"&gt;nashville&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="dmigrations"/><category term="google-groups"/><category term="schema-migrations"/><category term="nashville"/><category term="python"/></entry><entry><title>Announcing dmigrations</title><link href="https://simonwillison.net/2008/Sep/3/dmigrations/#atom-tag" rel="alternate"/><published>2008-09-03T19:17:01+00:00</published><updated>2008-09-03T19:17:01+00:00</updated><id>https://simonwillison.net/2008/Sep/3/dmigrations/#atom-tag</id><summary type="html">
    &lt;p&gt;The team at &lt;a href="http://www.thisisglobal.com/"&gt;Global Radio&lt;/a&gt; (formerly &lt;a href="http://www.gcapmedia.com/"&gt;GCap Media&lt;/a&gt;) is the largest group of Django developers I've personally worked with, consisting of 14 developers split into two scrum teams, all contributing to the same overall codebase.&lt;/p&gt;

&lt;p&gt;Working with that many developers makes smart tools and processes essential, and in some cases we've had to develop our own. Today, we're releasing one of them as an open source project.&lt;/p&gt;

&lt;p&gt;&lt;a href="http://code.google.com/p/dmigrations/"&gt;dmigrations&lt;/a&gt; is a Django migrations tool. It addresses a common problem in Django development: if you change a model after creating the database tables for it with &lt;code&gt;syncdb&lt;/code&gt;, how do you reflect those changes in your database tables without blowing away your existing data and starting again from scratch?&lt;/p&gt;

&lt;p&gt;&lt;a href="http://code.google.com/p/django-evolution/"&gt;django-evolution&lt;/a&gt; attempts to address this problem the clever way, by detecting changes to models that are not yet reflected in the database schema and figuring out what needs to be done to bring the two back in sync. In contrast, dmigrations takes the stupid approach: it requires you to explicitly state the changes in a sequence of migrations, which will be applied in turn to bring a database up to the most recent state that reflects the underlying models.&lt;/p&gt;

&lt;p&gt;This means extra work for developers who create migrations, but it also makes the whole process completely transparent - for our projects, we decided to go with the simplest system that could possibly work.&lt;/p&gt;

&lt;p&gt;The interface to dmigrations is a pair of custom Django commands. The first, &lt;code&gt;./manage.py dmigrate&lt;/code&gt;, provides a set of command for listing, applying and unapplying (reverting) migrations. This entirely replaces Django's &lt;code&gt;syncdb&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;The second, &lt;code&gt;./manage.py dmigration&lt;/code&gt;, provides commands for code-generating new migrations. It turns out that most migrations fit a set of common patterns: add a new table, add the tables for a new Django application, add a column to an existing table, add an index. These common cases are handled by dmigration; if you want to do something more complex (rename a column while transforming its data for example) you'll need to write a custom migration class.&lt;/p&gt;

&lt;p&gt;The &lt;a href="http://code.google.com/p/dmigrations/wiki/DmigrationsTutorial"&gt;dmigrations tutorial&lt;/a&gt; provides a full introduction to both of these commands, as well as hints on writing your own custom migrations. Since migrations are just classes, one of our hopes is that external developers will write extra migration classes for operations like "rename column" - things that currently require a one-off custom migration.&lt;/p&gt;

&lt;p&gt;dmigrations is actually the third iteration of our in-house migrations system. The first, smigrations, was designed to do the least amount of work possible to give us a controlled way of applying changes to our database schemas. The 's' stood for 'simple'. The second version (dmigrations) written by &lt;a href="http://t-a-w.blogspot.com/"&gt;Tomasz Wegrzanowski&lt;/a&gt; consisted of a major upgrade to smigrations that addressed many of the frustrations we found when using it with branched development, in particular the problem of migrations in two branches conflicting with each other. The 'd' stood for 'distributed'.&lt;/p&gt;

&lt;p&gt;Version three, released today, is my refactoring of dmigrations to de-couple it from the rest of our codebase. I've also stubbed out hooks for adding support for alternative database engines; dmigrations only supports MySQL out of the box, but I'm keen on getting it working with other databases now that it's out in the wild. Patches welcome!&lt;/p&gt;

&lt;p&gt;How does this fit in with &lt;a href="http://south.aeracode.org/"&gt;South&lt;/a&gt; and &lt;a href="http://code.google.com/p/django-evolution/"&gt;django-evolution&lt;/a&gt;?&lt;/p&gt;

&lt;p&gt;That's an excellent question. We'll be discussing all three systems on the schema evolution panel at &lt;a href="http://djangocon.org/"&gt;DjangoCon&lt;/a&gt; this weekend. I would love to see co-operation between the projects; at the very least I'd like to see the emergence of a standard Django-style abstraction library for create/alter table statements (something we punted on entirely with dmigrations). You'll certainly be hearing a lot more about migrations in Django after the conference.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/dmigrations"&gt;dmigrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gcap"&gt;gcap&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/globalradio"&gt;globalradio&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/open-source"&gt;open-source&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="django"/><category term="dmigrations"/><category term="gcap"/><category term="globalradio"/><category term="schema-migrations"/><category term="open-source"/><category term="projects"/><category term="python"/></entry><entry><title>South</title><link href="https://simonwillison.net/2008/Aug/8/aeracode/#atom-tag" rel="alternate"/><published>2008-08-08T11:42:00+00:00</published><updated>2008-08-08T11:42:00+00:00</updated><id>https://simonwillison.net/2008/Aug/8/aeracode/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://aeracode.org/projects/south/"&gt;South&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
A brand new light-weight Django migrations tool from Andrew Godwin. On first glance, this is spookily similar to the system we’ve been putting together at GCap.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/andrew-godwin"&gt;andrew-godwin&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gcap"&gt;gcap&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/south"&gt;south&lt;/a&gt;&lt;/p&gt;



</summary><category term="andrew-godwin"/><category term="django"/><category term="gcap"/><category term="schema-migrations"/><category term="south"/></entry><entry><title>Django Evolution</title><link href="https://simonwillison.net/2007/Nov/23/evolution/#atom-tag" rel="alternate"/><published>2007-11-23T23:49:10+00:00</published><updated>2007-11-23T23:49:10+00:00</updated><id>https://simonwillison.net/2007/Nov/23/evolution/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://code.google.com/p/django-evolution/"&gt;Django Evolution&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Really smart take on the problem of updating database tables to reflect changes to Django models. Code that automatically modifies your database tables can be pretty scary, but Evolution seems to hit the right balance.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/databases"&gt;databases&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/djangoevolution"&gt;djangoevolution&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/orm"&gt;orm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/schema"&gt;schema&lt;/a&gt;&lt;/p&gt;



</summary><category term="databases"/><category term="django"/><category term="djangoevolution"/><category term="schema-migrations"/><category term="orm"/><category term="schema"/></entry><entry><title>DbMigration - a schema migration tool for Django</title><link href="https://simonwillison.net/2007/Sep/27/dbmigration/#atom-tag" rel="alternate"/><published>2007-09-27T15:04:34+00:00</published><updated>2007-09-27T15:04:34+00:00</updated><id>https://simonwillison.net/2007/Sep/27/dbmigration/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.aswmc.com/dbmigration/"&gt;DbMigration - a schema migration tool for Django&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Nice and simple tool for adding schema migrations to a Django application.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/djangoorm"&gt;djangoorm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/schema-migrations"&gt;schema-migrations&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/orm"&gt;orm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sql"&gt;sql&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="djangoorm"/><category term="schema-migrations"/><category term="orm"/><category term="python"/><category term="sql"/></entry></feed>