<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: New features in sqlite-utils</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/series/sqlite-utils-features.atom" rel="self"/><id>http://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-series" 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-series</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.0rc2, mostly written by Claude Fable (for about $149.25)</title><link href="https://simonwillison.net/2026/Jul/5/sqlite-utils-fable/#atom-series" rel="alternate"/><published>2026-07-05T01:00:48+00:00</published><updated>2026-07-05T01:00:48+00:00</updated><id>https://simonwillison.net/2026/Jul/5/sqlite-utils-fable/#atom-series</id><summary type="html">
    &lt;p&gt;I wrote about the &lt;a href="https://simonwillison.net/2026/Jun/21/sqlite-utils-40rc1/"&gt;sqlite-utils 4.0rc1&lt;/a&gt; release a couple of weeks ago. Since we only have Claude Fable on our Max subscriptions for a few more days, I decided to see if it could help me get to a 4.0 stable release that I felt truly comfortable about, since I try to keep to &lt;a href="https://semver.org"&gt;SemVer&lt;/a&gt; and like my incompatible major versions to be as rare as possible.&lt;/p&gt;
&lt;p&gt;I started with this prompt, in Claude Code for web on my iPhone:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Final review before shipping a stable 4.0 release - very important to spot any last minute things that would be a breaking change if we fix them later&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's &lt;a href="https://github.com/simonw/sqlite-utils/blob/0c369a447eeaf39084f0d14a45b3eeb7eacb631b/fable-review-4.0rc1.md"&gt;that initial report&lt;/a&gt; it created for me. There were some &lt;em&gt;significant&lt;/em&gt; problems that I hadn't myself encountered yet - 5 that Fable categorized as "release blockers". Here's the worst of the bunch:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;delete_where()&lt;/code&gt; never commits and poisons the connection (data loss)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Table.delete_where()&lt;/code&gt; (&lt;code&gt;sqlite_utils/db.py:2948&lt;/code&gt;) runs its DELETE via a bare &lt;code&gt;self.db.execute()&lt;/code&gt; with no &lt;code&gt;atomic()&lt;/code&gt; wrapper — compare &lt;code&gt;Table.delete()&lt;/code&gt; at &lt;code&gt;db.py:2944&lt;/code&gt;, which wraps correctly. The connection is left &lt;code&gt;in_transaction=True&lt;/code&gt;, so every &lt;em&gt;subsequent&lt;/em&gt; &lt;code&gt;atomic()&lt;/code&gt; call takes the savepoint branch (&lt;code&gt;db.py:430-440&lt;/code&gt;) and never commits either.&lt;/p&gt;
&lt;p&gt;Reproduced end-to-end:&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-s1"&gt;sqlite_utils&lt;/span&gt;.&lt;span class="pl-c1"&gt;Database&lt;/span&gt;(&lt;span class="pl-s"&gt;"dw.db"&lt;/span&gt;)
&lt;span class="pl-s1"&gt;db&lt;/span&gt;[&lt;span class="pl-s"&gt;"t"&lt;/span&gt;].&lt;span class="pl-c1"&gt;insert_all&lt;/span&gt;([{&lt;span class="pl-s"&gt;"id"&lt;/span&gt;: &lt;span class="pl-s1"&gt;i&lt;/span&gt;} &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;i&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-en"&gt;range&lt;/span&gt;(&lt;span class="pl-c1"&gt;3&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-s"&gt;"t"&lt;/span&gt;].&lt;span class="pl-c1"&gt;delete_where&lt;/span&gt;(&lt;span class="pl-s"&gt;"id = ?"&lt;/span&gt;, [&lt;span class="pl-c1"&gt;0&lt;/span&gt;])   &lt;span class="pl-c"&gt;# conn.in_transaction is now True&lt;/span&gt;
&lt;span class="pl-s1"&gt;db&lt;/span&gt;[&lt;span class="pl-s"&gt;"t"&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;50&lt;/span&gt;})
&lt;span class="pl-s1"&gt;db&lt;/span&gt;[&lt;span class="pl-s"&gt;"u"&lt;/span&gt;].&lt;span class="pl-c1"&gt;insert&lt;/span&gt;({&lt;span class="pl-s"&gt;"a"&lt;/span&gt;: &lt;span class="pl-c1"&gt;1&lt;/span&gt;})
&lt;span class="pl-s1"&gt;db&lt;/span&gt;.&lt;span class="pl-c1"&gt;close&lt;/span&gt;()
&lt;span class="pl-c"&gt;# Reopen: rows are [0, 1, 2] — the delete, row 50, AND table u are all gone.&lt;/span&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;That's a really bad bug! Very glad I didn't ship that, although at least it would have been a bug I could fix in a 4.0.1 point release, not a design flaw that would force a 5.0.&lt;/p&gt;
&lt;p&gt;Over the course of 37 prompts, 34 commits and +1,321 -190 code changes over 30 separate files, we worked through the entire set of feedback in turn, making several other design improvements along the way.&lt;/p&gt;
&lt;p&gt;A weird thing about coding agents is that harder tasks like this one actually provide &lt;em&gt;more&lt;/em&gt; opportunity to do other things at the same time, since the agent sometimes needs 10-15 minutes to churn away on a new task. I went out to enjoy the Half Moon Bay 4th of July parade, occasionally checking in and prompting the next step for Fable from my phone.&lt;/p&gt;
&lt;p&gt;Full details &lt;a href="https://github.com/simonw/sqlite-utils/pull/767"&gt;in the PR&lt;/a&gt; and &lt;a href="https://claude.ai/code/session_01UnLnhsH25Nnv7LHhekUfPd"&gt;this shared transcript&lt;/a&gt;. I switched to my laptop for the final review, which I conducted through GitHub's PR interface.&lt;/p&gt;
&lt;p&gt;The most significant changes relate to transaction handling, which was the signature new feature in &lt;a href="https://simonwillison.net/2026/Jun/21/sqlite-utils-40rc1/#new-feature-db-atomic-transactions"&gt;the earlier RC&lt;/a&gt;. The new RC now includes &lt;a href="https://sqlite-utils.datasette.io/en/latest/python-api.html#transactions-and-saving-your-changes"&gt;comprehensive documentation&lt;/a&gt; on the new transaction model, the intro to which I'll quote here in full:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Every method in this library that writes to the database - &lt;code&gt;insert()&lt;/code&gt;, &lt;code&gt;upsert()&lt;/code&gt;, &lt;code&gt;update()&lt;/code&gt;, &lt;code&gt;delete()&lt;/code&gt;, &lt;code&gt;delete_where()&lt;/code&gt;, &lt;code&gt;transform()&lt;/code&gt;, &lt;code&gt;create_table()&lt;/code&gt;, &lt;code&gt;create_index()&lt;/code&gt;, &lt;code&gt;enable_fts()&lt;/code&gt; and the rest - runs inside its own transaction and commits it before returning. Your changes are saved to disk as soon as the method call finishes:&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;"data.db"&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;"news"&lt;/span&gt;).&lt;span class="pl-c1"&gt;insert&lt;/span&gt;({&lt;span class="pl-s"&gt;"headline"&lt;/span&gt;: &lt;span class="pl-s"&gt;"Dog wins award"&lt;/span&gt;})
&lt;span class="pl-c"&gt;# The new row is already saved - no commit() required&lt;/span&gt;&lt;/pre&gt;
&lt;p&gt;The same applies to raw SQL executed with &lt;a href="https://sqlite-utils.datasette.io/en/latest/python-api.html#python-api-transactions-execute"&gt;db.execute()&lt;/a&gt; - a write statement is committed as soon as it has run.&lt;/p&gt;
&lt;p&gt;You never need to call &lt;code&gt;commit()&lt;/code&gt;, and you do not need to close the database to persist your changes. There are exactly two situations where you need to think about transactions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;You want to group several write operations together, so they either all succeed or all fail - use &lt;a href="https://sqlite-utils.datasette.io/en/latest/python-api.html#python-api-atomic"&gt;db.atomic()&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;You are &lt;a href="https://sqlite-utils.datasette.io/en/latest/python-api.html#python-api-transactions-manual"&gt;managing a transaction yourself&lt;/a&gt; with &lt;code&gt;db.begin()&lt;/code&gt;, in which case nothing is committed until you commit - the library will never commit a transaction you opened.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;p&gt;In reviewing Fable's documentation - I find that reviewing the documentation edits first is an &lt;em&gt;excellent&lt;/em&gt; way to build an initial understanding of what has changed - I spotted &lt;a href="https://github.com/simonw/sqlite-utils/blob/6c88067ab76b9597fb1c538c53164632526a2891/docs/python-api.rst?plain=1#L386"&gt;this detail&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;db.atomic()&lt;/code&gt; and the automatic per-method transactions are designed for connections in Python's default transaction handling mode. Connections created with the Python 3.12+ &lt;code&gt;sqlite3.connect(..., autocommit=True)&lt;/code&gt; or &lt;code&gt;autocommit=False&lt;/code&gt; options are not supported, because &lt;code&gt;commit()&lt;/code&gt; and &lt;code&gt;rollback()&lt;/code&gt; behave differently on those connections.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I admit I hadn't thought about how &lt;code&gt;sqlite-utils&lt;/code&gt; would react to the more recent &lt;a href="https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.autocommit"&gt;autocommit setting&lt;/a&gt;, added in Python 3.12. It turns out "behave differently on those connections" equated to almost the entire test suite failing, so I worked with the model to &lt;a href="https://github.com/simonw/sqlite-utils/commit/f7ff3e2027aefb9905ebb2e611e5bbb0a62382c5"&gt;ensure that this difference&lt;/a&gt; would not break how the library works.&lt;/p&gt;
&lt;h4 id="and-a-final-review-by-gpt-5-5"&gt;And a final review by GPT-5.5&lt;/h4&gt;
&lt;p&gt;I used to think that the idea of having one model review the work of another was somewhat absurd - it felt weirdly superstitious. The problem is &lt;em&gt;it really does work&lt;/em&gt; - I've started habitually having Anthropic's best model review OpenAI's work and vice versa, because I've had that turn up interesting results often enough to be valuable.&lt;/p&gt;
&lt;p&gt;I prompted Codex Desktop and GPT-5.5 xhigh with the following:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Review changes since the last RC. Also confirm that the changelog is up-to-date.&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Which was enough to turn up two issues worth investigating:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Findings&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[P1] &lt;a href="https://github.com/simonw/sqlite-utils/blob/04f8971546418962aaf6579d4028c7117d6c3a20/sqlite_utils/db.py#L663"&gt;sqlite_utils/db.py:663&lt;/a&gt; &lt;code&gt;db.query()&lt;/code&gt; now rejects non-row statements only after calling &lt;code&gt;db.execute()&lt;/code&gt;, and &lt;a href="https://github.com/simonw/sqlite-utils/blob/04f8971546418962aaf6579d4028c7117d6c3a20/sqlite_utils/db.py#L705"&gt;sqlite_utils/db.py:705&lt;/a&gt; auto-commits those writes first. So &lt;code&gt;db.query("update ...")&lt;/code&gt; raises &lt;code&gt;ValueError&lt;/code&gt; but the update is already committed. That is a surprising side effect for a method documented as “can only be used with SQL that returns rows.”&lt;/li&gt;
&lt;li&gt;[P1] &lt;a href="https://github.com/simonw/sqlite-utils/blob/04f8971546418962aaf6579d4028c7117d6c3a20/sqlite_utils/db.py#L672"&gt;sqlite_utils/db.py:672&lt;/a&gt; &lt;code&gt;INSERT ... RETURNING&lt;/code&gt; through &lt;code&gt;db.query()&lt;/code&gt; only commits after the returned generator is fully exhausted. &lt;code&gt;db.query("insert ... returning ...")&lt;/code&gt; without iteration, or common &lt;code&gt;next(db.query(...))&lt;/code&gt; usage, leaves the transaction open and the write can be rolled back on close. This contradicts &lt;a href="https://github.com/simonw/sqlite-utils/blob/04f8971546418962aaf6579d4028c7117d6c3a20/docs/changelog.rst?plain=1#L15"&gt;docs/changelog.rst:15&lt;/a&gt; and &lt;a href="https://github.com/simonw/sqlite-utils/blob/04f8971546418962aaf6579d4028c7117d6c3a20/docs/python-api.rst?plain=1#L232"&gt;docs/python-api.rst:232&lt;/a&gt;, which say it takes effect without iteration.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;I pasted that into a fresh Fable session, which ran some experiments to confirm the problem:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Both findings were confirmed. &lt;code&gt;db.query()&lt;/code&gt; called &lt;code&gt;self.execute()&lt;/code&gt; first, which auto-commits writes, and only then checked &lt;code&gt;cursor.description&lt;/code&gt; — so &lt;code&gt;db.query("update ...")&lt;/code&gt; committed the update before raising &lt;code&gt;ValueError&lt;/code&gt;. And the &lt;code&gt;INSERT ... RETURNING&lt;/code&gt; commit lived at the end of the returned generator, so it never fired unless you exhausted the iterator — &lt;code&gt;next(db.query(...))&lt;/code&gt; or an un-iterated call left the transaction open, contradicting what the changelog and docs promise.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's &lt;a href="https://github.com/simonw/sqlite-utils/pull/768"&gt;the PR&lt;/a&gt; with the fix, and the &lt;a href="https://claude.ai/code/session_012U3iRfJoTZ5vd22cBSF2nJ"&gt;full Claude Code transcript&lt;/a&gt;. Reviewing this code helped me build a better mental model of the edge cases of SQLite transaction semantics!&lt;/p&gt;
&lt;h4 id="for-an-estimated-unsubsidized-cost-of-149-25"&gt;For an estimated (unsubsidized) cost of $149.25&lt;/h4&gt;
&lt;p&gt;I upgraded to the Claude Max $200/month plan (I was previously on $100/month) to increase my Fable allowance for the remaining time until &lt;a href="https://www.anthropic.com/news/redeploying-fable-5"&gt;the July 7th Fablepocalypse&lt;/a&gt;, when even Claude Max subscribers will have to pay full API cost for the model.&lt;/p&gt;
&lt;p&gt;I was curious as to how much this would have cost me if I had been paying those costs directly. At first I thought those numbers weren't available to me since I had run the work remotely using Claude Code for web, and then I realized I could run &lt;a href="https://www.agentsview.io"&gt;AgentsView&lt;/a&gt; inside that existing session to get that cost estimate!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Run "uvx agentsview --help"​ and then use that tool to calculate the cost of this session&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Claude figured out how to use the &lt;code&gt;session list --include-children&lt;/code&gt; command and came out with the following:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Transcript&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Main session&lt;/td&gt;
&lt;td&gt;claude-fable-5&lt;/td&gt;
&lt;td&gt;$141.02&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API-surface sweep agent&lt;/td&gt;
&lt;td&gt;claude-fable-5&lt;/td&gt;
&lt;td&gt;$2.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transactions/atomic review agent&lt;/td&gt;
&lt;td&gt;claude-fable-5&lt;/td&gt;
&lt;td&gt;$2.39&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-rc1 commits review agent&lt;/td&gt;
&lt;td&gt;claude-fable-5&lt;/td&gt;
&lt;td&gt;$1.72&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migrations review agent&lt;/td&gt;
&lt;td&gt;claude-fable-5&lt;/td&gt;
&lt;td&gt;$1.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt-counting agent&lt;/td&gt;
&lt;td&gt;claude-opus-4-8&lt;/td&gt;
&lt;td&gt;$0.32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$149.25&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;I'm very glad I'm on that subscription! I really should have &lt;a href="https://simonwillison.net/2026/Jul/3/judgement/"&gt;followed my own advice&lt;/a&gt; and leaned more heavily into subagents with cheaper models.&lt;/p&gt;
&lt;p&gt;Here's what &lt;a href="https://claude.ai/settings/usage"&gt;claude.ai/settings/usage&lt;/a&gt; is showing me right now:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2026/fable-plan-usage.webp" alt="Screenshot of a Claude plan usage limits panel: &amp;quot;Plan usage limits Max (20x)&amp;quot;; &amp;quot;Current session&amp;quot; with &amp;quot;Resets in 3 hr 52 min&amp;quot; showing a progress bar at &amp;quot;7% used&amp;quot;; &amp;quot;Weekly limits&amp;quot; heading with a &amp;quot;Learn more about usage limits&amp;quot; link; &amp;quot;All models&amp;quot; with &amp;quot;Resets Wed 12:00 PM&amp;quot; showing a progress bar at &amp;quot;32% used&amp;quot;; &amp;quot;Fable&amp;quot; with &amp;quot;Resets Wed 12:00 PM&amp;quot; showing a progress bar at &amp;quot;63% used&amp;quot;." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;I have several other major Fable-driven projects on the go right now as well, with the goal of hitting 100% on that Fable bar just in time for the price increase.&lt;/p&gt;
&lt;h4 id="the-full-release-notes-for-sqlite-utils-4-0rc2"&gt;The full release notes for sqlite-utils 4.0rc2&lt;/h4&gt;
&lt;p&gt;Here are &lt;a href="https://sqlite-utils.datasette.io/en/latest/changelog.html#rc2-2026-07-04"&gt;the full release notes&lt;/a&gt; for the RC. I had Fable add these to an "Unreleased" section of the changelog as each change landed, reviewing them as it went. This has the neat side effect that &lt;a href="https://github.com/simonw/sqlite-utils/commits/4.0rc2/docs/changelog.rst"&gt;the commit history of the changelog&lt;/a&gt; acts as a concise summary of each of the changes that went into the release.&lt;/p&gt;
&lt;p&gt;In the past I've had a policy of writing release notes by hand, but honestly these are better than I would have created myself. Release notes are a great example of writing that I'm OK to outsource to agents because they need to be boring, predictable and accurate.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Breaking changes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Write statements executed with &lt;code&gt;db.execute()&lt;/code&gt; are now committed automatically, unless a transaction is already open in which case they join it. Previously they opened an implicit transaction that stayed open until something committed it - writes appeared to work when read on the same connection but were silently rolled back when the connection closed. Code that relied on rolling back uncommitted &lt;code&gt;db.execute()&lt;/code&gt; writes should use the new &lt;code&gt;db.begin()&lt;/code&gt; method to open an explicit transaction first. The transaction model is documented in full at &lt;a href="https://sqlite-utils.datasette.io/en/latest/python-api.html#python-api-transactions"&gt;Transactions and saving your changes&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;db.query()&lt;/code&gt; now executes its SQL as soon as it is called, rather than waiting until the returned generator is first iterated. Rows are still fetched lazily during iteration. SQL errors are now raised at the call site, statements such as &lt;code&gt;INSERT ... RETURNING&lt;/code&gt; are executed and committed immediately without needing to iterate over their results, and passing a statement that returns no rows - previously a silent no-op - now raises a &lt;code&gt;ValueError&lt;/code&gt; recommending &lt;code&gt;db.execute()&lt;/code&gt; instead. A statement rejected this way is rolled back before the error is raised, so it has no effect on the database.&lt;/li&gt;
&lt;li&gt;Python API validation errors now raise &lt;code&gt;ValueError&lt;/code&gt; instead of &lt;code&gt;AssertionError&lt;/code&gt;. Previously invalid arguments - such as &lt;code&gt;create_table()&lt;/code&gt; with no columns, &lt;code&gt;transform()&lt;/code&gt; on a table that does not exist, or passing both &lt;code&gt;ignore=True&lt;/code&gt; and &lt;code&gt;replace=True&lt;/code&gt; - were rejected using bare &lt;code&gt;assert&lt;/code&gt; statements, which are silently skipped when Python runs with the &lt;code&gt;-O&lt;/code&gt; flag. Code that caught &lt;code&gt;AssertionError&lt;/code&gt; for these cases should catch &lt;code&gt;ValueError&lt;/code&gt; instead.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;table.upsert()&lt;/code&gt; and &lt;code&gt;table.upsert_all()&lt;/code&gt; now raise &lt;code&gt;PrimaryKeyRequired&lt;/code&gt; if a record is missing a value for any primary key column, or has a value of &lt;code&gt;None&lt;/code&gt; for one. Previously such records - which can never match an existing row - were quietly inserted as brand new rows, or triggered a confusing &lt;code&gt;KeyError&lt;/code&gt; after the insert had already taken place.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;db.enable_wal()&lt;/code&gt; and &lt;code&gt;db.disable_wal()&lt;/code&gt; now raise a &lt;code&gt;sqlite_utils.db.TransactionError&lt;/code&gt; if called while a transaction is open. Previously they would silently commit the open transaction as a side effect of changing the journal mode, breaking the rollback guarantee of &lt;code&gt;db.atomic()&lt;/code&gt; and of user-managed transactions.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;View&lt;/code&gt; class no longer has an &lt;code&gt;enable_fts()&lt;/code&gt; method. It existed only to raise &lt;code&gt;NotImplementedError&lt;/code&gt;, since full-text search is not supported for views - calling it now raises &lt;code&gt;AttributeError&lt;/code&gt; instead, and the method no longer appears in the API reference. The &lt;code&gt;sqlite-utils enable-fts&lt;/code&gt; command shows a clean error when pointed at a view.&lt;/li&gt;
&lt;li&gt;The no-op &lt;code&gt;-d/--detect-types&lt;/code&gt; flag has been removed from the &lt;code&gt;insert&lt;/code&gt; and &lt;code&gt;upsert&lt;/code&gt; commands. Type detection has been the default for CSV/TSV data since 4.0a1, so the flag did nothing - invocations using it should simply drop it. &lt;code&gt;--no-detect-types&lt;/code&gt; remains available to disable detection.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Database()&lt;/code&gt; now raises a &lt;code&gt;sqlite_utils.db.TransactionError&lt;/code&gt; if passed a connection created with the Python 3.12+ &lt;code&gt;sqlite3.connect(..., autocommit=True)&lt;/code&gt; or &lt;code&gt;autocommit=False&lt;/code&gt; options. &lt;code&gt;commit()&lt;/code&gt; and &lt;code&gt;rollback()&lt;/code&gt; behave differently on those connections, which previously caused every write made by the library to be silently discarded when the connection closed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Everything else:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fixed a bug where &lt;code&gt;table.delete_where()&lt;/code&gt;, &lt;code&gt;table.optimize()&lt;/code&gt; and &lt;code&gt;table.rebuild_fts()&lt;/code&gt; did not commit their changes, leaving the connection inside an open transaction. Their work - and any subsequent writes - could then be silently rolled back when the connection was closed. All three now use &lt;code&gt;db.atomic()&lt;/code&gt;, consistent with the other write methods.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;sqlite-utils drop-table&lt;/code&gt; command now refuses to drop a view, and &lt;code&gt;drop-view&lt;/code&gt; refuses to drop a table. Previously each would silently drop the wrong type of object if the name matched. Both now exit with an error suggesting the correct command to use.&lt;/li&gt;
&lt;li&gt;Migrations applied by the new &lt;a href="https://sqlite-utils.datasette.io/en/latest/migrations.html#migrations"&gt;migrations system&lt;/a&gt; now run inside a transaction, together with the record of the migration having been applied. If a migration raises an exception its changes are rolled back and it stays pending, so it can be safely re-applied after the error is fixed. Migrations that cannot run inside a transaction, such as those executing &lt;code&gt;VACUUM&lt;/code&gt;, can opt out using &lt;code&gt;@migrations(transactional=False)&lt;/code&gt; - see &lt;a href="https://sqlite-utils.datasette.io/en/latest/migrations.html#migrations-transactions"&gt;Migrations and transactions&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;table.upsert()&lt;/code&gt; and &lt;code&gt;table.upsert_all()&lt;/code&gt; now detect the primary key or compound primary key of an existing table, so the &lt;code&gt;pk=&lt;/code&gt; argument is no longer required when upserting into a table that already has a primary key.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;db.table(table_name).insert({})&lt;/code&gt; can now be used to insert a row consisting entirely of default values into an existing table, using &lt;code&gt;INSERT INTO ... DEFAULT VALUES&lt;/code&gt;. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/759"&gt;#759&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Improvements to the &lt;code&gt;sqlite-utils migrate&lt;/code&gt; command: &lt;code&gt;--stop-before&lt;/code&gt; values that do not match any known migration are now an error instead of being silently ignored, &lt;code&gt;--stop-before&lt;/code&gt; now works correctly with migration files that still use the older &lt;code&gt;sqlite_migrate.Migrations&lt;/code&gt; class, and &lt;code&gt;--list&lt;/code&gt; is now a read-only operation that no longer creates the database file or the migrations tracking table. &lt;code&gt;migrations.applied()&lt;/code&gt; now returns migrations in the order they were applied.&lt;/li&gt;
&lt;li&gt;New &lt;code&gt;db.begin()&lt;/code&gt;, &lt;code&gt;db.commit()&lt;/code&gt; and &lt;code&gt;db.rollback()&lt;/code&gt; methods for taking manual control of transactions, as an alternative to the &lt;code&gt;db.atomic()&lt;/code&gt; context manager.&lt;/li&gt;
&lt;li&gt;New documentation: &lt;a href="https://sqlite-utils.datasette.io/en/latest/python-api.html#python-api-transactions"&gt;Transactions and saving your changes&lt;/a&gt; describes how transactions work and when changes are committed, and a new &lt;a href="https://sqlite-utils.datasette.io/en/latest/upgrading.html#upgrading"&gt;Upgrading&lt;/a&gt; page details the changes needed to move between major versions.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
    
        &lt;p&gt;Tags: &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;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/llm-pricing"&gt;llm-pricing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-code"&gt;claude-code&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/gpt"&gt;gpt&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="projects"/><category term="sqlite"/><category term="sqlite-utils"/><category term="annotated-release-notes"/><category term="anthropic"/><category term="claude"/><category term="llm-pricing"/><category term="coding-agents"/><category term="claude-code"/><category term="agentic-engineering"/><category term="gpt"/><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-series" 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-series</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>sqlite-utils 4.0a1 has several (minor) backwards incompatible changes</title><link href="https://simonwillison.net/2025/Nov/24/sqlite-utils-40a1/#atom-series" rel="alternate"/><published>2025-11-24T14:52:34+00:00</published><updated>2025-11-24T14:52:34+00:00</updated><id>https://simonwillison.net/2025/Nov/24/sqlite-utils-40a1/#atom-series</id><summary type="html">
    &lt;p&gt;I released a &lt;a href="https://sqlite-utils.datasette.io/en/latest/changelog.html#a1-2025-11-23"&gt;new alpha version&lt;/a&gt; of &lt;a href="https://sqlite-utils.datasette.io/"&gt;sqlite-utils&lt;/a&gt; last night - the 128th release of that package since I started building it back in 2018.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sqlite-utils&lt;/code&gt; is two things in one package: a Python library for conveniently creating and manipulating SQLite databases and a CLI tool for working with them in the terminal. Almost every feature provided by the package is available via both of those surfaces.&lt;/p&gt;
&lt;p&gt;This is hopefully the last alpha before a 4.0 stable release. I use semantic versioning for this library, so the 4.0 version number indicates that there are backward incompatible changes that may affect code written against the 3.x line.&lt;/p&gt;
&lt;p&gt;These changes are mostly very minor: I don't want to break any existing code if I can avoid it. I made it all the way to version 3.38 before I had to ship a major release and I'm sad I couldn't push that even further!&lt;/p&gt;
&lt;p&gt;Here are the &lt;a href="https://simonwillison.net/tags/annotated-release-notes/"&gt;annotated release notes&lt;/a&gt; for 4.0a1.&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;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;This change is for type hint enthusiasts. The Python library used to encourage accessing both SQL tables and SQL views through the &lt;code&gt;db["name_of_table_or_view"]&lt;/code&gt; syntactic sugar - but tables and view have different interfaces since there's no way to handle a &lt;code&gt;.insert(row)&lt;/code&gt; on a SQLite view. If you want clean type hints for your code you can now use the &lt;code&gt;db.table(table_name)&lt;/code&gt; and &lt;code&gt;db.view(view_name)&lt;/code&gt; methods instead.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&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/stable/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;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;A new feature, not a breaking change. I realized that supporting a stream of lists or tuples as an option for populating large tables would be a neat optimization over always dealing with dictionaries each of which duplicated the column names.&lt;/p&gt;
&lt;p&gt;I had the idea for this one while walking the dog and built the first prototype by prompting Claude Code for web on my phone. Here's &lt;a href="https://github.com/simonw/research/pull/31"&gt;the prompt I used&lt;/a&gt; and the &lt;a href="https://github.com/simonw/research/blob/main/sqlite-utils-iterator-support/README.md"&gt;prototype report it created&lt;/a&gt;, which included a benchmark estimating how much of a performance boost could be had for different sizes of tables.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&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;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;I was horrified to discover a while ago that I'd been creating SQLite columns called FLOAT but the correct type to use was REAL! This change fixes that. Previously the fix was to ask for tables to be created in strict mode.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&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;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;As part of this I also figured out recipes for using &lt;code&gt;uv&lt;/code&gt; as a development environment for the package, which are now baked into the &lt;a href="https://github.com/simonw/sqlite-utils/blob/4.0a1/Justfile"&gt;Justfile&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&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;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;This one is best explained &lt;a href="https://github.com/simonw/sqlite-utils/issues/655"&gt;in the issue&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;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;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Another change which I would have made earlier but, since it introduces a minor behavior change to an existing feature, I reserved it for the 4.0 release.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&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;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Back in 2018 when I started this project I was new to working in-depth with SQLite and incorrectly concluded that the correct way to create tables and columns named after reserved words was like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;create table [my table] (
  [id] integer primary key,
  [key] text
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That turned out to be a non-standard SQL syntax which the SQLite documentation &lt;a href="https://sqlite.org/lang_keywords.html"&gt;describes like this&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A keyword enclosed in square brackets is an identifier. This is not standard SQL. This quoting mechanism is used by MS Access and SQL Server and is included in SQLite for compatibility.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Unfortunately I baked it into the library early on and it's been polluting the world with weirdly escaped table and column names ever since!&lt;/p&gt;
&lt;p&gt;I've finally fixed that, with the help of Claude Code which took on the mind-numbing task of &lt;a href="https://github.com/simonw/sqlite-utils/pull/678/files"&gt;updating hundreds of existing tests&lt;/a&gt; that asserted against the generated schemas.&lt;/p&gt;
&lt;p&gt;The above example table schema now looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;create table "my table" (
  "id" integer primary key,
  "key" text
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This may seem like a pretty small change but I expect it to cause a fair amount of downstream pain purely in terms of updating tests that work against tables created by &lt;code&gt;sqlite-utils&lt;/code&gt;!&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&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;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;I made this change first &lt;a href="https://github.com/simonw/llm/issues/1016#issuecomment-2877305544"&gt;in LLM&lt;/a&gt; and decided to bring it to &lt;code&gt;sqlite-utils&lt;/code&gt; for consistency between the two tools.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&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;p&gt;One last minor ugliness that I waited for a major version bump to fix.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Now that the embargo has lifted I can reveal that a substantial amount of the work on this release was performed using a preview version of Anthropic's &lt;a href="https://simonwillison.net/2025/Nov/24/claude-opus/"&gt;new Claude Opus 4.5 model&lt;/a&gt;. Here's the &lt;a href="https://gistpreview.github.io/?f40971b693024fbe984a68b73cc283d2"&gt;Claude Code transcript&lt;/a&gt; for the work to implement the ability to use an iterator over lists instead of dictionaries for bulk insert and upsert operations.&lt;/p&gt;
    
        &lt;p&gt;Tags: &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;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-code"&gt;claude-code&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="projects"/><category term="sqlite"/><category term="sqlite-utils"/><category term="annotated-release-notes"/><category term="ai-assisted-programming"/><category term="coding-agents"/><category term="claude-code"/></entry><entry><title>sqlite-utils now supports plugins</title><link href="https://simonwillison.net/2023/Jul/24/sqlite-utils-plugins/#atom-series" rel="alternate"/><published>2023-07-24T17:06:23+00:00</published><updated>2023-07-24T17:06:23+00:00</updated><id>https://simonwillison.net/2023/Jul/24/sqlite-utils-plugins/#atom-series</id><summary type="html">
    &lt;p&gt;&lt;a href="https://sqlite-utils.datasette.io/en/stable/changelog.html#v3-34"&gt;sqlite-utils 3.34&lt;/a&gt; is out with a major new feature: support for &lt;a href="https://sqlite-utils.datasette.io/en/stable/plugins.html"&gt;plugins&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sqlite-utils&lt;/code&gt; is my combination Python library and command-line tool for manipulating SQLite databases. It recently celebrated its fifth birthday, and has had over 100 releases since it first launched back in 2018.&lt;/p&gt;
&lt;p&gt;The new plugin system is inspired by similar mechanisms &lt;a href="https://docs.datasette.io/en/stable/plugins.html"&gt;in Datasette&lt;/a&gt; and &lt;a href="https://llm.datasette.io/en/stable/plugins/index.html"&gt;LLM&lt;/a&gt;. It lets developers add new features to &lt;code&gt;sqlite-utils&lt;/code&gt; without needing to get their changes accepted by the core project.&lt;/p&gt;
&lt;p&gt;I love plugin systems. As an open source maintainer they are by far the best way to encourage people to contribute to my projects - I can genuinely wake up in the morning and my software has new features, and I didn't even need to review a pull request.&lt;/p&gt;
&lt;p&gt;Plugins also offer a fantastic medium for exploration and experimentation. I can try out new ideas without committing to supporting them in core, and without needing to tie improvements to them to the core release cycle.&lt;/p&gt;
&lt;p&gt;Version 3.34 adds &lt;a href="https://sqlite-utils.datasette.io/en/stable/plugins.html#plugin-hooks"&gt;two initial plugin hooks&lt;/a&gt;: &lt;code&gt;register_commands()&lt;/code&gt; and &lt;code&gt;prepare_connection()&lt;/code&gt;. These are both based on the equivalent hooks in Datasette.&lt;/p&gt;
&lt;p&gt;I planned to just ship &lt;code&gt;register_commands()&lt;/code&gt;, but Alex Garcia spotted my activity on the repo and submitted &lt;a href="https://github.com/simonw/sqlite-utils/pull/573"&gt;a PR&lt;/a&gt; adding &lt;code&gt;prepare_connection()&lt;/code&gt; literally minutes before I had intended to ship the release!&lt;/p&gt;
&lt;h4&gt;register_commands()&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;register_commands()&lt;/code&gt; hook lets you add new commands to the &lt;code&gt;sqlite-utils&lt;/code&gt; command-line tool - so users can run &lt;code&gt;sqlite-utils your-new-command&lt;/code&gt; to access your feature.&lt;/p&gt;
&lt;p&gt;I've learned from past experience that you should never ship a plugin hook without also releasing at least one plugin that uses it. I've built two so far for &lt;code&gt;register_commands()&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/simonw/sqlite-utils-shell"&gt;sqlite-utils-shell&lt;/a&gt; adds a simply interactive shell, accessed using &lt;code&gt;sqlite-utils shell&lt;/code&gt; for an in-memory database or &lt;code&gt;sqlite-utils shell data.db&lt;/code&gt; to run it against a specific database file.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/simonw/sqlite-migrate"&gt;sqlite-migrate&lt;/a&gt; is my first draft of a database migrations system for SQLite, loosely inspired by Django migrations and previewed by the migration mechanism I &lt;a href="https://github.com/simonw/llm/blob/0.6.1/llm/migrations.py"&gt;added to LLM&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Try out the shell plugin like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;sqlite-utils install sqlite-utils-shell
sqlite-utils shell&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The interface looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;In-memory database, content will be lost on exit
Type 'exit' to exit.
sqlite-utils&amp;gt; select 3 + 5;
  3 + 5
-------
      8
sqlite-utils&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;prepare_connection()&lt;/h4&gt;
&lt;p&gt;This hook, contributed by Alex, lets you modify the connection object before it is used to execute any SQL. Most importantly, this lets you register custom SQLite functions.&lt;/p&gt;
&lt;p&gt;I expect this to be the most common category of plugin. I've built one so far: &lt;a href="https://github.com/simonw/sqlite-utils-dateutil"&gt;sqlite-utils-dateutil&lt;/a&gt;, which adds functions for parsing dates and times using the &lt;a href="https://dateutil.readthedocs.io/"&gt;dateutil&lt;/a&gt; library.&lt;/p&gt;
&lt;p&gt;It lets you do things like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;sqlite-utils install sqlite-utils-dateutil
sqlite-utils memory &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;select dateutil_parse('3rd october')&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; -t&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dateutil_parse('3rd october')
-------------------------------
2023-10-03T00:00:00
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This works inside &lt;code&gt;sqlite-shell&lt;/code&gt; too.&lt;/p&gt;
&lt;p&gt;Plugins that you install also become available in the Python API interface to &lt;code&gt;sqlite-utils&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-text-python-console"&gt;&lt;pre&gt;&amp;gt;&amp;gt;&amp;gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; sqlite_utils
&amp;gt;&amp;gt;&amp;gt; db &lt;span class="pl-k"&gt;=&lt;/span&gt; sqlite_utils.Database(&lt;span class="pl-v"&gt;memory&lt;/span&gt;&lt;span class="pl-k"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;)
&amp;gt;&amp;gt;&amp;gt; &lt;span class="pl-c1"&gt;list&lt;/span&gt;(db.query(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;select dateutil_parse('3rd october')&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;))
[{"dateutil_parse('3rd october')": '2023-10-03T00:00:00'}]&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can opt out of executing installed plugins by passing &lt;code&gt;execute_plugins=False&lt;/code&gt; to the &lt;code&gt;Database()&lt;/code&gt; constructor:&lt;/p&gt;
&lt;div class="highlight highlight-text-python-console"&gt;&lt;pre&gt;&amp;gt;&amp;gt;&amp;gt; db &lt;span class="pl-k"&gt;=&lt;/span&gt; sqlite_utils.Database(&lt;span class="pl-v"&gt;memory&lt;/span&gt;&lt;span class="pl-k"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;, &lt;span class="pl-v"&gt;execute_plugins&lt;/span&gt;&lt;span class="pl-k"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;False&lt;/span&gt;)
&amp;gt;&amp;gt;&amp;gt; &lt;span class="pl-c1"&gt;list&lt;/span&gt;(db.query(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;select dateutil_parse('3rd october')&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;))
Traceback (most recent call last):
  File "&amp;lt;stdin&amp;gt;", line 1, in &amp;lt;module&amp;gt;
  File ".../site-packages/sqlite_utils/db.py", line 494, in query
    cursor = self.execute(sql, params or tuple())
  File ".../site-packages/sqlite_utils/db.py", line 512, in execute
    return self.conn.execute(sql, parameters)
sqlite3.OperationalError: no such function: dateutil_parse&lt;/pre&gt;&lt;/div&gt;
&lt;h4&gt;sqlite-ml by Romain Clement&lt;/h4&gt;
&lt;p&gt;I quietly released &lt;code&gt;sqlite-utils 3.34&lt;/code&gt; on Saturday. The community has already released several plugins for it!&lt;/p&gt;
&lt;p&gt;Romain Clement built &lt;a href="https://github.com/rclement/sqlite-utils-ml"&gt;sqlite-utils-ml&lt;/a&gt;, a plugin wrapper for his &lt;a href="https://github.com/rclement/sqlite-ml"&gt;sqlite-ml&lt;/a&gt; project.&lt;/p&gt;
&lt;p&gt;This adds custom SQL functions for training machine learning models and running predictions, entirely within SQLite, using algorithms from &lt;a href="https://scikit-learn.org"&gt;scikit-learn&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here's what that looks like running inside &lt;code&gt;sqlite-utils shell&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;sqlite-utils install sqlite-utils-shell sqlite-utils-ml
sqlite-utils shell ml.db&lt;/pre&gt;&lt;/div&gt;
&lt;pre&gt;&lt;code&gt;Attached to ml.db
Type 'exit' to exit.
sqlite-utils&amp;gt; select sqml_load_dataset('iris') as dataset;
dataset
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
{"table": "dataset_iris", "feature_names": ["sepal length (cm)", "sepal width (cm)", "petal length (cm)", "petal width (cm)"], "target_names": ["setosa", "versicolor", "virginica"], "size": 150}
sqlite-utils&amp;gt; select sqml_train(
         ...&amp;gt;   'Iris prediction',
         ...&amp;gt;   'classification',
         ...&amp;gt;   'logistic_regression',
         ...&amp;gt;   'dataset_iris',
         ...&amp;gt;   'target'
         ...&amp;gt; ) as training;
training
--------------------------------------------------------------------------------------------------------------------------------------------------------------
{"experiment_name": "Iris prediction", "prediction_type": "classification", "algorithm": "logistic_regression", "deployed": true, "score": 0.9736842105263158}
sqlite-utils&amp;gt; select
         ...&amp;gt;   dataset_iris.*,
         ...&amp;gt;   sqml_predict(
         ...&amp;gt;     'Iris prediction',
         ...&amp;gt;     json_object(
         ...&amp;gt;       'sepal length (cm)', [sepal length (cm)],
         ...&amp;gt;       'sepal width (cm)', [sepal width (cm)],
         ...&amp;gt;       'petal length (cm)', [petal length (cm)],
         ...&amp;gt;       'petal width (cm)', [petal width (cm)]
         ...&amp;gt;     )
         ...&amp;gt;   ) as prediction
         ...&amp;gt; from dataset_iris
         ...&amp;gt; limit 1;
  sepal length (cm)    sepal width (cm)    petal length (cm)    petal width (cm)    target    prediction
-------------------  ------------------  -------------------  ------------------  --------  ------------
                5.1                 3.5                  1.4                 0.2         0             0
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;SQLite extensions by Alex Garcia&lt;/h4&gt;
&lt;p&gt;Alex Garcia has &lt;a href="https://github.com/asg017/sqlite-ecosystem"&gt;a growing collection&lt;/a&gt; of SQLite extensions, many of which are written in Rust but are packaged as wheels for ease of installation using Python.&lt;/p&gt;
&lt;p&gt;Alex released five plugins for SQLite corresponding to five of his existing extensions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sqlite-utils-sqlite-regex&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sqlite-utils-sqlite-path&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sqlite-utils-sqlite-url&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sqlite-utils-sqlite-ulid&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sqlite-utils-sqlite-lines&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's an example of &lt;code&gt;sqlite-utils-sqlite-ulid&lt;/code&gt; in action:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;sqlite-utils install sqlite-utils-sqlite-ulid
sqlite-utils memory &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;select ulid() u1, ulid() u2, ulid() u3&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;|&lt;/span&gt; jq&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;div class="highlight highlight-source-json"&gt;&lt;pre&gt;[
  {
    &lt;span class="pl-ent"&gt;"u1"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;01h64d1ysg1rx63z1gwy7nah4n&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
    &lt;span class="pl-ent"&gt;"u2"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;01h64d1ysgd7vx04sc9pncqh10&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
    &lt;span class="pl-ent"&gt;"u3"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;01h64d1ysgz1sy7njkqt86dkq9&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
  }
]&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I've started a &lt;a href="https://github.com/simonw/sqlite-utils-plugins"&gt;sqlite-utils plugin directory&lt;/a&gt; with a list of all of the plugins so far.&lt;/p&gt;
&lt;h4&gt;Building your own plugin&lt;/h4&gt;
&lt;p&gt;If you want to try building your own plugin, the documentation includes a &lt;a href="https://sqlite-utils.datasette.io/en/stable/plugins.html#building-a-plugin"&gt;simple step-by-step guide&lt;/a&gt;. A plugin can be built with as little as two files: a Python module implementing the hooks, and a &lt;code&gt;pyproject.toml&lt;/code&gt; module with metadata about how it should be installed.&lt;/p&gt;
&lt;p&gt;I've also released a new &lt;a href="https://pypi.org/project/cookiecutter/"&gt;cookiecutter&lt;/a&gt; template: &lt;a href="https://github.com/simonw/sqlite-utils-plugin"&gt;simonw/sqlite-utils-plugin&lt;/a&gt;. Here's how to use that to get started building a plugin:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;cookiecutter gh:simonw/sqlite-utils-plugin&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Answer the form fields like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;plugin_name []: rot13
description []: select rot13('text') as a sqlite-utils plugin
hyphenated [rot13]: 
underscored [rot13]: 
github_username []: your-username
author_name []: your-name
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Change directory into the new folder and use &lt;code&gt;sqlite-utils install -e&lt;/code&gt; to install an editable version of your plugin, so changes you make will be reflected when you run the tool:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;&lt;span class="pl-c1"&gt;cd&lt;/span&gt; sqlite-utils-rot13
sqlite-utils install -e &lt;span class="pl-c1"&gt;.&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Run this command to confirm the plugin has been installed:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;sqlite-utils plugins&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You should see this:&lt;/p&gt;
&lt;div class="highlight highlight-source-json"&gt;&lt;pre&gt;[
  {
    &lt;span class="pl-ent"&gt;"name"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;sqlite-utils-rot13&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
    &lt;span class="pl-ent"&gt;"hooks"&lt;/span&gt;: [
      &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;prepare_connection&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
    ],
    &lt;span class="pl-ent"&gt;"version"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;0.1&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
  }
]&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now drop this code into the &lt;code&gt;sqlite_utils_rot13.py&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;sqlite_utils&lt;/span&gt;


&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;rot13&lt;/span&gt;(&lt;span class="pl-s1"&gt;s&lt;/span&gt;):
    &lt;span class="pl-s1"&gt;chars&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; []
    &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;v&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;s&lt;/span&gt;:
        &lt;span class="pl-s1"&gt;c&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;ord&lt;/span&gt;(&lt;span class="pl-s1"&gt;v&lt;/span&gt;)
        &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;c&lt;/span&gt; &lt;span class="pl-c1"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;ord&lt;/span&gt;(&lt;span class="pl-s"&gt;"a"&lt;/span&gt;) &lt;span class="pl-c1"&gt;and&lt;/span&gt; &lt;span class="pl-s1"&gt;c&lt;/span&gt; &lt;span class="pl-c1"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="pl-en"&gt;ord&lt;/span&gt;(&lt;span class="pl-s"&gt;"z"&lt;/span&gt;):
            &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;c&lt;/span&gt; &lt;span class="pl-c1"&gt;&amp;gt;&lt;/span&gt; &lt;span class="pl-en"&gt;ord&lt;/span&gt;(&lt;span class="pl-s"&gt;"m"&lt;/span&gt;):
                &lt;span class="pl-s1"&gt;c&lt;/span&gt; &lt;span class="pl-c1"&gt;-=&lt;/span&gt; &lt;span class="pl-c1"&gt;13&lt;/span&gt;
            &lt;span class="pl-k"&gt;else&lt;/span&gt;:
                &lt;span class="pl-s1"&gt;c&lt;/span&gt; &lt;span class="pl-c1"&gt;+=&lt;/span&gt; &lt;span class="pl-c1"&gt;13&lt;/span&gt;
        &lt;span class="pl-k"&gt;elif&lt;/span&gt; &lt;span class="pl-s1"&gt;c&lt;/span&gt; &lt;span class="pl-c1"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;ord&lt;/span&gt;(&lt;span class="pl-s"&gt;"A"&lt;/span&gt;) &lt;span class="pl-c1"&gt;and&lt;/span&gt; &lt;span class="pl-s1"&gt;c&lt;/span&gt; &lt;span class="pl-c1"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="pl-en"&gt;ord&lt;/span&gt;(&lt;span class="pl-s"&gt;"Z"&lt;/span&gt;):
            &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;c&lt;/span&gt; &lt;span class="pl-c1"&gt;&amp;gt;&lt;/span&gt; &lt;span class="pl-en"&gt;ord&lt;/span&gt;(&lt;span class="pl-s"&gt;"M"&lt;/span&gt;):
                &lt;span class="pl-s1"&gt;c&lt;/span&gt; &lt;span class="pl-c1"&gt;-=&lt;/span&gt; &lt;span class="pl-c1"&gt;13&lt;/span&gt;
            &lt;span class="pl-k"&gt;else&lt;/span&gt;:
                &lt;span class="pl-s1"&gt;c&lt;/span&gt; &lt;span class="pl-c1"&gt;+=&lt;/span&gt; &lt;span class="pl-c1"&gt;13&lt;/span&gt;
        &lt;span class="pl-s1"&gt;chars&lt;/span&gt;.&lt;span class="pl-en"&gt;append&lt;/span&gt;(&lt;span class="pl-en"&gt;chr&lt;/span&gt;(&lt;span class="pl-s1"&gt;c&lt;/span&gt;))

    &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-s"&gt;""&lt;/span&gt;.&lt;span class="pl-en"&gt;join&lt;/span&gt;(&lt;span class="pl-s1"&gt;chars&lt;/span&gt;)


&lt;span class="pl-en"&gt;@&lt;span class="pl-s1"&gt;sqlite_utils&lt;/span&gt;.&lt;span class="pl-s1"&gt;hookimpl&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;prepare_connection&lt;/span&gt;(&lt;span class="pl-s1"&gt;conn&lt;/span&gt;):
    &lt;span class="pl-s1"&gt;conn&lt;/span&gt;.&lt;span class="pl-en"&gt;create_function&lt;/span&gt;(&lt;span class="pl-s"&gt;"rot13"&lt;/span&gt;, &lt;span class="pl-c1"&gt;1&lt;/span&gt;, &lt;span class="pl-s1"&gt;rot13&lt;/span&gt;)&lt;/pre&gt;
&lt;p&gt;And try it out like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;sqlite-utils memory &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;select rot13('hello world')&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;div class="highlight highlight-source-json"&gt;&lt;pre&gt;[{&lt;span class="pl-ent"&gt;"rot13('hello world')"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;uryyb jbeyq&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;}]&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And to reverse that:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sqlite-utils memory "select rot13('uryyb jbeyq')"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;div class="highlight highlight-source-json"&gt;&lt;pre&gt;[{&lt;span class="pl-ent"&gt;"rot13('uryyb jbeyq')"&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;hello world&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;}]&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;As you can see, building plugins can be done with very little code. I'm excited to see what else people build with this new capability!&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&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/alex-garcia"&gt;alex-garcia&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="plugins"/><category term="projects"/><category term="sqlite"/><category term="sqlite-utils"/><category term="alex-garcia"/></entry><entry><title>What's new in sqlite-utils 3.20 and 3.21: --lines, --text, --convert</title><link href="https://simonwillison.net/2022/Jan/11/sqlite-utils/#atom-series" rel="alternate"/><published>2022-01-11T18:19:17+00:00</published><updated>2022-01-11T18:19:17+00:00</updated><id>https://simonwillison.net/2022/Jan/11/sqlite-utils/#atom-series</id><summary type="html">
    &lt;p&gt;&lt;a href="https://sqlite-utils.datasette.io/"&gt;sqlite-utils&lt;/a&gt; is my combined CLI tool and Python library for manipulating SQLite databases. Consider this the &lt;a href="https://simonwillison.net/tags/annotatedreleasenotes/"&gt;annotated release notes&lt;/a&gt; for sqlite-utils &lt;a href="https://sqlite-utils.datasette.io/en/stable/changelog.html#v3-20"&gt;3.20&lt;/a&gt; and &lt;a href="https://sqlite-utils.datasette.io/en/stable/changelog.html#v3-21"&gt;3.21&lt;/a&gt;, both released in the past week.&lt;/p&gt;
&lt;h4&gt;sqlite-utils insert --convert with --lines and --text&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;sqlite-utils insert&lt;/code&gt; command inserts rows into a SQLite database from a JSON, CSV or TSV file, creating a table with the necessary columns if one does not exist already.&lt;/p&gt;
&lt;p&gt;It gained three new options in v3.20:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sqlite-utils insert ... --lines&lt;/code&gt; to insert the lines from a file into a table with a single &lt;code&gt;line&lt;/code&gt; column, see &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#cli-insert-unstructured"&gt;Inserting unstructured data with --lines and --text&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sqlite-utils insert ... --text&lt;/code&gt; to insert the contents of the file into a table with a single &lt;code&gt;text&lt;/code&gt; column and a single row.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sqlite-utils insert ... --convert&lt;/code&gt; allows a Python function to be provided that will be used to convert each row that is being inserted into the database. See &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#cli-insert-convert"&gt;Applying conversions while inserting data&lt;/a&gt;, including details on special behavior when combined with &lt;code&gt;--lines&lt;/code&gt; and &lt;code&gt;--text&lt;/code&gt;. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/356"&gt;#356&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;These features all evolved from an idea I had while re-reading my blog entry from last year, &lt;a href="https://simonwillison.net/2021/Aug/6/sqlite-utils-convert/"&gt;Apply conversion functions to data in SQLite columns with the sqlite-utils CLI tool&lt;/a&gt;. That blog entry introduced the &lt;code&gt;sqlite-utils convert&lt;/code&gt; comand, which can run a custom Python function against a column in a table to convert that data in some way.&lt;/p&gt;
&lt;p&gt;Given a log file &lt;code&gt;log.txt&lt;/code&gt; that looks something like this:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2021-08-05T17:58:28.880469+00:00 app[web.1]: measure#nginx.service=4.212 request="GET /search/?type=blogmark&amp;amp;page=2&amp;amp;tag=highavailability HTTP/1.1" status_code=404 request_id=25eb296e-e970-4072-b75a-606e11e1db5b remote_addr="10.1.92.174" forwarded_for="114.119.136.88, 172.70.142.28" forwarded_proto="http" via="1.1 vegur" body_bytes_sent=179 referer="-" user_agent="Mozilla/5.0 (Linux; Android 7.0;) AppleWebKit/537.36 (KHTML, like Gecko) Mobile Safari/537.36 (compatible; PetalBot;+https://webmaster.petalsearch.com/site/petalbot)" request_time="4.212" upstream_response_time="4.212" upstream_connect_time="0.000" upstream_header_time="4.212";
&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I provided this example code to insert lines from a log file into a table with a single &lt;code&gt;line&lt;/code&gt; column:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat log.txt | \
    jq --raw-input '{line: .}' --compact-output | \
    sqlite-utils insert logs.db log - --nl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since &lt;code&gt;sqlite-utils insert&lt;/code&gt; requires JSON, this example first used &lt;code&gt;jq&lt;/code&gt; to convert the lines into &lt;code&gt;{"line": "..."}&lt;/code&gt; JSON objects.&lt;/p&gt;
&lt;p&gt;My first idea was to improve this with the new &lt;code&gt;--lines&lt;/code&gt; option, which lets you replace the above with this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sqlite-utils insert logs.db log log.txt --lines
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using &lt;code&gt;--lines&lt;/code&gt; will create a table with a single &lt;code&gt;lines&lt;/code&gt; column and import every line from the file as a row in that table.&lt;/p&gt;
&lt;p&gt;In the article, I then demonstrated how &lt;code&gt;--convert&lt;/code&gt; could be used to convert those imported lines into structured rows using a regular expression:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sqlite-utils convert logs.db log line --import re --multi "$(cat &amp;lt;&amp;lt;EOD
    r = re.compile(r'([^\s=]+)=(?:"(.*?)"|(\S+))')
    pairs = {}
    for key, value1, value2 in r.findall(value):
        pairs[key] = value1 or value2
    return pairs
EOD
)"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The new &lt;code&gt;--convert&lt;/code&gt; option to &lt;code&gt;sqlite-utils&lt;/code&gt; means you can now achieve the same thing using:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sqlite-utils insert logs.db log log.txt --lines \
  --import re --convert "$(cat &amp;lt;&amp;lt;EOD
    r = re.compile(r'([^\s=]+)=(?:"(.*?)"|(\S+))')
    for key, value1, value2 in r.findall(line):
        pairs[key] = value1 or value2
    return pairs
EOD
)"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since the &lt;code&gt;--lines&lt;/code&gt; option allows you to consume mostly unstructured files split by newlines, I decided to also add an option to consume an entire unstructured file as a single record. I originally called that &lt;code&gt;--all&lt;/code&gt; but found the code got messy because it conflicted with Python's &lt;code&gt;all()&lt;/code&gt; built-in, so I renamed it to &lt;code&gt;--text&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Used on its own, &lt;code&gt;--text&lt;/code&gt; creates a table with a single column called &lt;code&gt;text&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;% sqlite-utils insert logs.db fulllog log.txt --text
% sqlite-utils schema logs.db
CREATE TABLE [fulllog] (
   [text] TEXT
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But with &lt;code&gt;--convert&lt;/code&gt; you can pass a snippet of Python code which can take that &lt;code&gt;text&lt;/code&gt; value and convert it into a list of dictionaries, which will then be used to populate the table.&lt;/p&gt;
&lt;p&gt;Here's a fun example. The following one-liner uses the classic &lt;a href="https://feedparser.readthedocs.io/"&gt;feedparser&lt;/a&gt; library to parse the Atom feed for my blog and load it into a database table:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl 'https://simonwillison.net/atom/everything/' | \
  sqlite-utils insert feed.db entries --text --convert '
    feed = feedparser.parse(text)
    return feed.entries' - --import feedparser
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The resulting database looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;% sqlite-utils tables feed.db --counts -t
table      count
-------  -------
feed          30

% sqlite-utils schema feed.db
CREATE TABLE [feed] (
   [title] TEXT,
   [title_detail] TEXT,
   [links] TEXT,
   [link] TEXT,
   [published] TEXT,
   [published_parsed] TEXT,
   [updated] TEXT,
   [updated_parsed] TEXT,
   [id] TEXT,
   [guidislink] INTEGER,
   [summary] TEXT,
   [summary_detail] TEXT,
   [tags] TEXT
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Not bad for a one-liner!&lt;/p&gt;
&lt;p&gt;This example uses the &lt;code&gt;--import&lt;/code&gt; option to import that &lt;code&gt;feedparser&lt;/code&gt; library. This means you'll need to have that library installed in the same virtual environment as &lt;code&gt;sqlite-utils&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you run into problems here (maybe due to having installed &lt;code&gt;sqlite-utils&lt;/code&gt; via Homebrew) one way to do this is to use the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python3 -m pip install feedparser sqlite-utils
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then use &lt;code&gt;python3 -m sqlite_utils&lt;/code&gt; in place of &lt;code&gt;sqlite-utils&lt;/code&gt; - this will ensure you are running the command from the same virtual environment where you installed the library.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update 13th December 2022&lt;/strong&gt;: &lt;a href="https://sqlite-utils.datasette.io/en/stable/changelog.html#v3-30"&gt;sqlite-utils 3.30&lt;/a&gt; introduced a new &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#cli-install"&gt;sqlite-utils install&lt;/a&gt; command for installing PyPI packages directly into the same virtual environment as &lt;code&gt;sqlite-utils&lt;/code&gt; itself.&lt;/p&gt;
&lt;h4&gt;--convert for regular rows&lt;/h4&gt;
&lt;p&gt;The above examples combine &lt;code&gt;--convert&lt;/code&gt; with the &lt;code&gt;--lines&lt;/code&gt; and &lt;code&gt;--text&lt;/code&gt; options to parse unstructured text into database tables.&lt;/p&gt;
&lt;p&gt;But &lt;code&gt;--convert&lt;/code&gt; works with the existing &lt;code&gt;sqlite-utils insert&lt;/code&gt; options as well.&lt;/p&gt;
&lt;p&gt;To review, those are the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sqlite-utils insert&lt;/code&gt; by default expects a JSON file that's a list of objects, &lt;code&gt;[{"id": 1, "text": "Like"}, {"id": 2, "text": "This"}]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sqlite-utils insert --nl&lt;/code&gt; accepts newline-delimited JSON, &lt;code&gt;{"id": 1, "text": "Like"}\n{"id": 2, "text": "This"}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sqlite-utils insert --csv&lt;/code&gt; and &lt;code&gt;--tsv&lt;/code&gt; accepts CSV/TSV - with &lt;code&gt;--delimiter&lt;/code&gt; and &lt;code&gt;--encoding&lt;/code&gt; and &lt;code&gt;--quotechar&lt;/code&gt; and &lt;code&gt;--no-headers&lt;/code&gt; options for customizing that import, and a &lt;code&gt;--sniff&lt;/code&gt; option for automatically detecting those settings.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can now use &lt;code&gt;--convert&lt;/code&gt; to define a Python function that accepts a &lt;code&gt;row&lt;/code&gt; dictionary representing each row from the import and modifies that dictionary or returns a fresh one with changes.&lt;/p&gt;
&lt;p&gt;Here's a simple example that produces just the capitalized name, the latitude and the longitude from the WRI's &lt;a href="https://github.com/wri/global-power-plant-database/blob/master/output_database/global_power_plant_database.csv"&gt;global power plants&lt;/a&gt; CSV file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl https://raw.githubusercontent.com/wri/global-power-plant-database/master/output_database/global_power_plant_database.csv | \
  sqlite-utils insert plants.db plants - --csv --convert '
  return {
      "name": row["name"].upper(),
      "latitude": float(row["latitude"]),
      "longitude": float(row["longitude"]),
  }'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The resulting database looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;% sqlite-utils schema plants.db
CREATE TABLE [plants] (
   [name] TEXT,
   [latitude] FLOAT,
   [longitude] FLOAT
);

~ % sqlite-utils rows plants.db plants | head -n 3
[{"name": "KAJAKI HYDROELECTRIC POWER PLANT AFGHANISTAN", "latitude": 32.322, "longitude": 65.119},
 {"name": "KANDAHAR DOG", "latitude": 31.67, "longitude": 65.795},
 {"name": "KANDAHAR JOL", "latitude": 31.623, "longitude": 65.792},
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;sqlite-utils bulk&lt;/h4&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;New &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#cli-bulk"&gt;sqlite-utils bulk command&lt;/a&gt; which can import records in the same way as &lt;code&gt;sqlite-utils insert&lt;/code&gt; (from JSON, CSV or TSV) and use them to bulk execute a parametrized SQL query. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/375"&gt;#375&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;With the addition of &lt;code&gt;--lines&lt;/code&gt;, &lt;code&gt;--text&lt;/code&gt;, &lt;code&gt;--convert&lt;/code&gt; and &lt;code&gt;--import&lt;/code&gt; the &lt;code&gt;sqlite-utils insert&lt;/code&gt; command is now a powerful tool for turning anything into a list of Python dictionaries, which can then in turn be inserted into a SQLite database table.&lt;/p&gt;
&lt;p&gt;Which gave me an idea... what if you could use the same mechanisms to execute SQL statements in bulk instead?&lt;/p&gt;
&lt;p&gt;Python's SQLite library supports named parameters in SQL queries, which look like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;insert into&lt;/span&gt; plants (id, name) &lt;span class="pl-k"&gt;values&lt;/span&gt; (:id, :name)&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Those &lt;code&gt;:id&lt;/code&gt; and &lt;code&gt;:name&lt;/code&gt; parameters can be populated from a Python dictionary. And the &lt;code&gt;.executemany()&lt;/code&gt; method can efficiently apply the same SQL query to a big list (or iterator or generator) of dictionaries in one go:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-s1"&gt;cursor&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;db&lt;/span&gt;.&lt;span class="pl-en"&gt;cursor&lt;/span&gt;()
&lt;span class="pl-s1"&gt;cursor&lt;/span&gt;.&lt;span class="pl-en"&gt;executemany&lt;/span&gt;(
    &lt;span class="pl-s"&gt;"insert into plants (id, name) values (:id, :name)"&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;"One"&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;"Two"&lt;/span&gt;}]
)&lt;/pre&gt;
&lt;p&gt;So I implemented the &lt;code&gt;sqlite-utils bulk&lt;/code&gt; command, which takes the same import options as &lt;code&gt;sqlite-utils&lt;/code&gt; but instead of creating and populating the specified table requires a &lt;code&gt;SQL&lt;/code&gt; argument with a query that will be executed using the imported rows as arguments.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;% sqlite-utils bulk demo.db \
  'insert into plants (id, name) values (:id, :name)' \
  plants.csv --csv&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This feels like a powerful new feature, which was very simple to implement because the hard work of importing the data had already been done by the &lt;code&gt;insert&lt;/code&gt; command.&lt;/p&gt;
&lt;h4 id="running-analyze"&gt;Running ANALYZE&lt;/h4&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;New Python methods for running &lt;code&gt;ANALYZE&lt;/code&gt; against a database, table or index: &lt;code&gt;db.analyze()&lt;/code&gt; and &lt;code&gt;table.analyze()&lt;/code&gt;, see &lt;a href="https://sqlite-utils.datasette.io/en/stable/python-api.html#python-api-analyze"&gt;Optimizing index usage with ANALYZE&lt;/a&gt;. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/366"&gt;#366&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;New &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#cli-analyze"&gt;sqlite-utils analyze command&lt;/a&gt; for running &lt;code&gt;ANALYZE&lt;/code&gt; using the CLI. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/379"&gt;#379&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;create-index&lt;/code&gt;, &lt;code&gt;insert&lt;/code&gt; and &lt;code&gt;upsert&lt;/code&gt; commands now have a new &lt;code&gt;--analyze&lt;/code&gt; option for running &lt;code&gt;ANALYZE&lt;/code&gt; after the command has completed. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/379"&gt;#379&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;This idea came from Forest Gregg, who &lt;a href="https://github.com/simonw/sqlite-utils/issues/365"&gt;initially suggested&lt;/a&gt; running &lt;code&gt;ANALYZE&lt;/code&gt; automatically as part of the &lt;code&gt;sqlite-utils create-index&lt;/code&gt; command.&lt;/p&gt;
&lt;p&gt;I have to confess: in all of my years of using SQLite, I'd never actually explored &lt;a href="https://www.sqlite.org/lang_analyze.html"&gt;the ANALYZE command&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;When run, it builds a new table called &lt;code&gt;sqlite_stats1&lt;/code&gt; containing statistics about each of the indexes on the table - indicating how "selective" each index is - effectively how many rows on average you are likely to filter down to if you use the index.&lt;/p&gt;
&lt;p&gt;The SQLite query planner can then use this to decide which index to consult. For example, given the following query:&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;select&lt;/span&gt; &lt;span class="pl-k"&gt;*&lt;/span&gt; &lt;span class="pl-k"&gt;from&lt;/span&gt; ny_times_us_counties
&lt;span class="pl-k"&gt;where&lt;/span&gt; state &lt;span class="pl-k"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;Missouri&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;and&lt;/span&gt; county &lt;span class="pl-k"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;Greene&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;(&lt;a href="https://covid-19.datasettes.com/covid?sql=select+*+from+ny_times_us_counties%0D%0Awhere+state+%3D+%27Missouri%27+and+county+%3D+%27Greene%27&amp;amp;p0=Greene&amp;amp;p1=Missouri"&gt;Try that here&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;If there are indexes on both columns, should the query planner use the &lt;code&gt;state&lt;/code&gt; column or the &lt;code&gt;county&lt;/code&gt; column?&lt;/p&gt;
&lt;p&gt;In this case the &lt;code&gt;state&lt;/code&gt; column will filter down to 75,209 rows, while the &lt;code&gt;county&lt;/code&gt; column filters to 9,186 - so &lt;code&gt;county&lt;/code&gt; is clearly the better query plan.&lt;/p&gt;
&lt;p&gt;Impressively, SQLite seems to make this kind of decision perfectly well without the &lt;code&gt;sqlite_stat1&lt;/code&gt; table being populated: &lt;a href="https://covid-19.datasettes.com/covid?sql=explain+query+plan+select+*+from+ny_times_us_counties+where+%22county%22+%3D+%3Ap0+and+%22state%22+%3D+%3Ap1&amp;amp;p0=Greene&amp;amp;p1=Missouri"&gt;explain query plan select * from ny_times_us_counties where "county" = 'Greene' and "state" = 'Missouri'&lt;/a&gt; returns the following:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SEARCH TABLE ny_times_us_counties USING INDEX idx_ny_times_us_counties_county (county=?)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I've not actually found a good example of a query where the &lt;code&gt;sqlite_stat1&lt;/code&gt; table makes a difference yet, but I'm confident such queries exist!&lt;/p&gt;
&lt;p&gt;Using SQL, you can run &lt;code&gt;ANALYZE&lt;/code&gt; against an entire database by executing &lt;code&gt;ANALYZE;&lt;/code&gt;, or against all of the indexes for a specific table with &lt;code&gt;ANALYZE tablename;&lt;/code&gt;, or against a specific index by name using &lt;code&gt;ANALYZE indexname;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;There's one catch with &lt;code&gt;ANALYZE&lt;/code&gt;: since running it populates a static &lt;code&gt;sqlite_stat1&lt;/code&gt; table, the data in that table can get out of date. If you insert another million rows into a table for example your analyzye statistics might no longer reflect ground truth to the point that the query planner starts to make bad decisions.&lt;/p&gt;
&lt;p&gt;For &lt;code&gt;sqlite-utils&lt;/code&gt; I decided to make &lt;code&gt;ANALYZE&lt;/code&gt; an explicit operation. In the Python library you can now run the following:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-s1"&gt;db&lt;/span&gt;.&lt;span class="pl-en"&gt;analyze&lt;/span&gt;() &lt;span class="pl-c"&gt;# Analyze every index in the database&lt;/span&gt;
&lt;span class="pl-s1"&gt;db&lt;/span&gt;.&lt;span class="pl-en"&gt;analyze&lt;/span&gt;(&lt;span class="pl-s"&gt;"indexname"&lt;/span&gt;) &lt;span class="pl-c"&gt;# Analyze a specific index&lt;/span&gt;
&lt;span class="pl-s1"&gt;db&lt;/span&gt;.&lt;span class="pl-en"&gt;analyze&lt;/span&gt;(&lt;span class="pl-s"&gt;"tablename"&lt;/span&gt;) &lt;span class="pl-c"&gt;# Analyze every index for that table&lt;/span&gt;
&lt;span class="pl-c"&gt;# Or the same thing using a table object:&lt;/span&gt;
&lt;span class="pl-s1"&gt;db&lt;/span&gt;[&lt;span class="pl-s"&gt;"tablename"&lt;/span&gt;].&lt;span class="pl-en"&gt;analyze&lt;/span&gt;()&lt;/pre&gt;
&lt;p&gt;I also added an optional &lt;code&gt;analyze=True&lt;/code&gt; parameter to several methods, which you can use to trigger an &lt;code&gt;ANALZYE&lt;/code&gt; once that operation completes:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-s1"&gt;db&lt;/span&gt;[&lt;span class="pl-s"&gt;"tablename"&lt;/span&gt;].&lt;span class="pl-en"&gt;create_index&lt;/span&gt;([&lt;span class="pl-s"&gt;"column"&lt;/span&gt;], &lt;span class="pl-s1"&gt;analyze&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;)
&lt;span class="pl-s1"&gt;db&lt;/span&gt;[&lt;span class="pl-s"&gt;"tablename"&lt;/span&gt;].&lt;span class="pl-en"&gt;insert_rows&lt;/span&gt;(&lt;span class="pl-s1"&gt;rows&lt;/span&gt;, &lt;span class="pl-s1"&gt;analyze&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;)
&lt;span class="pl-s1"&gt;db&lt;/span&gt;[&lt;span class="pl-s"&gt;"tablename"&lt;/span&gt;].&lt;span class="pl-en"&gt;delete_where&lt;/span&gt;(&lt;span class="pl-s1"&gt;analyze&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;)&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;sqlite-utils&lt;/code&gt; CLI command has equivalent functionality:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Analyze every index in a database:
% sqlite-utils analyze database.db
# Analyze a specific index:
% sqlite-utils analyze database.db indexname
# Analyze all indexes for a table:
% sqlite-utils analyze database.db tablename
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And an &lt;code&gt;--analyze&lt;/code&gt; option for various commands:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;% sqlite-utils create-index ... --analyze
% sqlite-utils insert ... --analyze
% sqlite-utils upsert ... --analyze
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Other smaller changes&lt;/h4&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;New &lt;code&gt;sqlite-utils create-database&lt;/code&gt; command for creating new empty database files. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/348"&gt;#348&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Most &lt;code&gt;sqlite-utils&lt;/code&gt; commands such as &lt;code&gt;insert&lt;/code&gt; or &lt;code&gt;create-table&lt;/code&gt; create the database file for you if it doesn't already exist, but I decided it would be neat to have an explicit &lt;code&gt;create-database&lt;/code&gt; command for deliberately creating an empty database.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update 13th January 2022&lt;/strong&gt;:  I wrote a detailed description of my process building this command in &lt;a href="https://simonwillison.net/2022/Jan/12/how-i-build-a-feature/"&gt;How I build a feature&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;The CLI tool can now also be run using &lt;code&gt;python -m sqlite_utils&lt;/code&gt;. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/368"&gt;#368&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;I initially added this to help write a unit test that exercised the tool through a subprocess (see TIL &lt;a href="https://til.simonwillison.net/pytest/test-click-app-with-streaming-input"&gt;Testing a Click app with streaming input&lt;/a&gt;) but it's a neat pattern in general. &lt;code&gt;datasette&lt;/code&gt; gained this through &lt;a href="https://github.com/simonw/datasette/pull/556"&gt;a contribution&lt;/a&gt; from Abdussamet Koçak a few years ago.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Using &lt;code&gt;--fmt&lt;/code&gt; now implies &lt;code&gt;--table&lt;/code&gt;, so you don't need to pass both options. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/374"&gt;#374&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;A nice tiny usability enhancement. You can now run &lt;code&gt;sqlite-utils rows my.db mytable --fmt rst&lt;/code&gt; to get back a reStructuredText table - previously you also needed to add &lt;code&gt;--table&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#cli-insert-files"&gt;insert-files command&lt;/a&gt; supports two new columns: &lt;code&gt;stem&lt;/code&gt; and &lt;code&gt;suffix&lt;/code&gt;. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/372"&gt;#372&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;I sometimes re-read the documentation for older features to remind me what they do, and occasionally an idea for a feature jumps out from that. Implementing these was &lt;a href="https://github.com/simonw/sqlite-utils/commit/c9ecd0d6a32d4518c9b92bcc08183a10268d52d7#diff-76294b3d4afeb27e74e738daa01c26dd4dc9ccb6f4477451483a2ece1095902e"&gt;a very small change&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;--nl&lt;/code&gt; import option now ignores blank lines in the input. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/376"&gt;#376&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Fixed bug where streaming input to the &lt;code&gt;insert&lt;/code&gt; command with &lt;code&gt;--batch-size 1&lt;/code&gt; would appear to only commit after several rows had been ingested, due to unnecessary input buffering. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/364"&gt;#364&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;That &lt;code&gt;--nl&lt;/code&gt; improvement came from tinkering around trying to fix the bug.&lt;/p&gt;
&lt;p&gt;The bug itself was interesting: I initially thought that my entire mechanism for comitting on every &lt;code&gt;--batch-size&lt;/code&gt; chunk was broken, but it turned out I was unnecessarily buffering data from standard input in order to support the &lt;code&gt;--sniff&lt;/code&gt; option for detecting the shape of incoming CSV data.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;db.supports_strict&lt;/code&gt; property showing if the database connection supports &lt;a href="https://www.sqlite.org/stricttables.html"&gt;SQLite strict tables&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;table.strict&lt;/code&gt; property (see &lt;a href="https://sqlite-utils.datasette.io/en/stable/python-api.html#python-api-introspection-strict"&gt;.strict&lt;/a&gt;) indicating if the table uses strict mode. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/344"&gt;#344&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;See &lt;a href="https://simonwillison.net/2021/Dec/1/beautiful-yaks/"&gt;previous weeknotes&lt;/a&gt;: this is the first part of my ongoing support for the new STRICT tables in SQLite.&lt;/p&gt;
&lt;p&gt;I'm currently blocked on implementing more due to the need to get a robust mechanism up and running for executing &lt;code&gt;sqlite-utils&lt;/code&gt; tests in CI against specific SQLite versions, see &lt;a href="https://github.com/simonw/sqlite-utils/issues/346"&gt;issue #346&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;Releases this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/3.21"&gt;3.21&lt;/a&gt; - (&lt;a href="https://github.com/simonw/sqlite-utils/releases"&gt;92 releases total&lt;/a&gt;) - 2022-01-11
&lt;br /&gt;Python CLI utility and library for manipulating SQLite databases&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/3.20"&gt;3.20&lt;/a&gt; - 2022-01-05&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/stream-delay"&gt;stream-delay&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/stream-delay/releases/tag/0.1"&gt;0.1&lt;/a&gt; - 2022-01-08
&lt;br /&gt;Stream a file or stdin one line at a time with a delay&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;TILs this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/pytest/pytest-argparse"&gt;Writing pytest tests against tools written with argparse&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/pytest/test-click-app-with-streaming-input"&gt;Testing a Click app with streaming input&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cli"&gt;cli&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/weeknotes"&gt;weeknotes&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="cli"/><category term="projects"/><category term="sqlite"/><category term="weeknotes"/><category term="sqlite-utils"/><category term="annotated-release-notes"/></entry><entry><title>Apply conversion functions to data in SQLite columns with the sqlite-utils CLI tool</title><link href="https://simonwillison.net/2021/Aug/6/sqlite-utils-convert/#atom-series" rel="alternate"/><published>2021-08-06T06:05:15+00:00</published><updated>2021-08-06T06:05:15+00:00</updated><id>https://simonwillison.net/2021/Aug/6/sqlite-utils-convert/#atom-series</id><summary type="html">
    &lt;p&gt;Earlier this week I released &lt;a href="https://sqlite-utils.datasette.io/en/stable/changelog.html#v3-14"&gt;sqlite-utils 3.14&lt;/a&gt; with a powerful new command-line tool: &lt;code&gt;sqlite-utils convert&lt;/code&gt;, which applies a conversion function to data stored in a SQLite column.&lt;/p&gt;
&lt;p&gt;Anyone who works with data will tell you that 90% of the work is cleaning it up. Running command-line conversions against data in a SQLite file turns out to be a really productive way to do that.&lt;/p&gt;
&lt;h4&gt;Transforming a column&lt;/h4&gt;
&lt;p&gt;Here's a simple example. Say someone gave you data with numbers that are formatted with commas - like &lt;code&gt;3,044,502&lt;/code&gt; - in a &lt;code&gt;count&lt;/code&gt; column in a &lt;code&gt;states&lt;/code&gt; table.&lt;/p&gt;
&lt;p&gt;You can strip those commas out like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sqlite-utils convert states.db states count \
    'value.replace(",", "")'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;convert&lt;/code&gt; command takes four arguments: the database file, the name of the table, the name of the column and a string containing a fragment of Python code that defines the conversion to be applied.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Animated demo using sqlite-utils convert to strip out commas" src="https://static.simonwillison.net/static/2021/sqlite-convert-demo.gif" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;The conversion function can be anything you can express with Python. If you want to import extra modules you can do so using &lt;code&gt;--import module&lt;/code&gt; - here's an example that wraps text using the &lt;a href=""&gt;textwrap&lt;/a&gt; module from the Python standard library:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sqlite-utils convert content.db articles content \
    '"\n".join(textwrap.wrap(value, 100))' \
    --import=textwrap
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can consider this analogous to using &lt;code&gt;Array.map()&lt;/code&gt; in JavaScript, or running a transformation using a list comprehension in Python.&lt;/p&gt;
&lt;h4&gt;Custom functions in SQLite&lt;/h4&gt;
&lt;p&gt;Under the hood, the tool takes advantage of a powerful SQLite feature: the ability to &lt;a href="https://docs.python.org/3/library/sqlite3.html#sqlite3.Connection.create_function"&gt;register custom functions&lt;/a&gt; written in Python (or other languages) and call them from SQL.&lt;/p&gt;
&lt;p&gt;The text wrapping example above works by executing the following SQL:&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;update&lt;/span&gt; articles &lt;span class="pl-k"&gt;set&lt;/span&gt; content &lt;span class="pl-k"&gt;=&lt;/span&gt; convert_value(content)&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;convert_value(value)&lt;/code&gt; is a custom SQL function, compiled as Python code and then made available to the database connection.&lt;/p&gt;
&lt;p&gt;The equivalent code using just the Python standard library would look like this:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;sqlite3&lt;/span&gt;
&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;textwrap&lt;/span&gt;

&lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;convert_value&lt;/span&gt;(&lt;span class="pl-s1"&gt;value&lt;/span&gt;):
    &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-s"&gt;"&lt;span class="pl-cce"&gt;\n&lt;/span&gt;"&lt;/span&gt;.&lt;span class="pl-en"&gt;join&lt;/span&gt;(&lt;span class="pl-s1"&gt;textwrap&lt;/span&gt;.&lt;span class="pl-en"&gt;wrap&lt;/span&gt;(&lt;span class="pl-s1"&gt;value&lt;/span&gt;, &lt;span class="pl-c1"&gt;100&lt;/span&gt;))

&lt;span class="pl-s1"&gt;conn&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;sqlite3&lt;/span&gt;.&lt;span class="pl-en"&gt;connect&lt;/span&gt;(&lt;span class="pl-s"&gt;"content.db"&lt;/span&gt;)
&lt;span class="pl-s1"&gt;conn&lt;/span&gt;.&lt;span class="pl-en"&gt;create_function&lt;/span&gt;(&lt;span class="pl-s"&gt;"convert_value"&lt;/span&gt;, &lt;span class="pl-c1"&gt;1&lt;/span&gt;, &lt;span class="pl-s1"&gt;convert_value&lt;/span&gt;)
&lt;span class="pl-s1"&gt;conn&lt;/span&gt;.&lt;span class="pl-en"&gt;execute&lt;/span&gt;(&lt;span class="pl-s"&gt;"update articles set content = convert_value(content)"&lt;/span&gt;)&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;sqlite-utils convert&lt;/code&gt; works by &lt;a href="https://github.com/simonw/sqlite-utils/blob/cc90745f4e8bb1ac57d8ee973863cfe00c2e4fe5/sqlite_utils/cli.py#L2019-L2028"&gt;compiling the code argument&lt;/a&gt; to a Python function, registering it with the connection and executing the above SQL query.&lt;/p&gt;
&lt;h4&gt;Splitting columns into multiple other columns&lt;/h4&gt;
&lt;p&gt;Sometimes when I'm working with a table I find myself wanting to split a column into multiple other columns.&lt;/p&gt;
&lt;p&gt;A classic example is locations - if a &lt;code&gt;location&lt;/code&gt; column contains &lt;code&gt;latitude,longitude&lt;/code&gt; values I'll often want to split that into separate &lt;code&gt;latitude&lt;/code&gt; and &lt;code&gt;longitude&lt;/code&gt; columns, so I can visualize the data with &lt;a href="https://datasette.io/plugins/datasette-cluster-map"&gt;datasette-cluster-map&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;--multi&lt;/code&gt; option lets you do that using &lt;code&gt;sqlite-utils convert&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sqlite-utils convert data.db places location '
latitude, longitude = value.split(",")
return {
    "latitude": float(latitude),
    "longitude": float(longitude),
}' --multi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--multi&lt;/code&gt; tells the command to expect the Python code to return dictionaries. It will then create new columns in the database corresponding to the keys in those dictionaries and populate them using the results of the transformation.&lt;/p&gt;
&lt;p&gt;If the &lt;code&gt;places&lt;/code&gt; table started with just a &lt;code&gt;location&lt;/code&gt; column, after running the above command the new table schema will look like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;CREATE TABLE [places] (
    [location] &lt;span class="pl-k"&gt;TEXT&lt;/span&gt;,
    [latitude] FLOAT,
    [longitude] FLOAT
);&lt;/pre&gt;&lt;/div&gt;
&lt;h4&gt;Common recipes&lt;/h4&gt;
&lt;p&gt;This new feature in &lt;code&gt;sqlite-utils&lt;/code&gt; actually started life as a separate tool entirely, called &lt;a href="https://github.com/simonw/sqlite-transform"&gt;sqlite-transform&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Part of the rationale for adding it to &lt;code&gt;sqlite-utils&lt;/code&gt; was to avoid confusion between what that tool did and the &lt;a href="https://simonwillison.net/2020/Sep/23/sqlite-advanced-alter-table/"&gt;sqlite-utils transform&lt;/a&gt; tool, which does something completely different (applies table transformations that aren't possible using SQLite's default &lt;code&gt;ALTER TABLE&lt;/code&gt; statement). Somewhere along the line I messed up with the naming of the two tools!&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sqlite-transform&lt;/code&gt; bundles a number of useful &lt;a href="https://github.com/simonw/sqlite-transform/blob/main/README.md#parsedate-and-parsedatetime"&gt;default transformation recipes&lt;/a&gt;, in addition to allowing arbitrary Python code. I ended up making these available in &lt;code&gt;sqlite-utils convert&lt;/code&gt; by exposing them as functions that can be called from the command-line code argument like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sqlite-utils convert my.db articles created_at \
    'r.parsedate(value)'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Implementing them as Python functions in this way meant I didn't need to invent a new command-line mechanism for passing in additional options to the individual recipes - instead, parameters are passed like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sqlite-utils convert my.db articles created_at \
    'r.parsedate(value, dayfirst=True)'
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Also available in the sqlite_utils Python library&lt;/h4&gt;
&lt;p&gt;Almost every feature that is exposed by the &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html"&gt;sqlite-utils command-line tool&lt;/a&gt; has a matching API in the &lt;a href="https://sqlite-utils.datasette.io/en/stable/python-api.html"&gt;sqlite_utils Python library&lt;/a&gt;. &lt;code&gt;convert&lt;/code&gt; is no exception.&lt;/p&gt;
&lt;p&gt;The Python API lets you perform operations like the following:&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-s1"&gt;sqlite_utils&lt;/span&gt;.&lt;span class="pl-v"&gt;Database&lt;/span&gt;(&lt;span class="pl-s"&gt;"dogs.db"&lt;/span&gt;)

&lt;span class="pl-s1"&gt;db&lt;/span&gt;[&lt;span class="pl-s"&gt;"dogs"&lt;/span&gt;].&lt;span class="pl-en"&gt;convert&lt;/span&gt;(&lt;span class="pl-s"&gt;"name"&lt;/span&gt;, &lt;span class="pl-k"&gt;lambda&lt;/span&gt; &lt;span class="pl-s1"&gt;value&lt;/span&gt;: &lt;span class="pl-s1"&gt;value&lt;/span&gt;.&lt;span class="pl-en"&gt;upper&lt;/span&gt;())&lt;/pre&gt;
&lt;p&gt;Any Python callable can be passed to &lt;code&gt;convert&lt;/code&gt;, and it will be applied to every value in the specified column - again, like using &lt;code&gt;map()&lt;/code&gt; to apply a transformation to every item in an array.&lt;/p&gt;
&lt;p&gt;You can also use the Python API to perform more complex operations like the following two examples:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;# Convert title to upper case only for rows with id &amp;gt; 20&lt;/span&gt;
&lt;span class="pl-s1"&gt;table&lt;/span&gt;.&lt;span class="pl-en"&gt;convert&lt;/span&gt;(
    &lt;span class="pl-s"&gt;"title"&lt;/span&gt;,
    &lt;span class="pl-k"&gt;lambda&lt;/span&gt; &lt;span class="pl-s1"&gt;v&lt;/span&gt;: &lt;span class="pl-s1"&gt;v&lt;/span&gt;.&lt;span class="pl-en"&gt;upper&lt;/span&gt;(),
    &lt;span class="pl-s1"&gt;where&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"id &amp;gt; :id"&lt;/span&gt;,
    &lt;span class="pl-s1"&gt;where_args&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-c1"&gt;20&lt;/span&gt;}
)

&lt;span class="pl-c"&gt;# Create two new columns, "upper" and "lower",&lt;/span&gt;
&lt;span class="pl-c"&gt;# and populate them from the converted title&lt;/span&gt;
&lt;span class="pl-s1"&gt;table&lt;/span&gt;.&lt;span class="pl-en"&gt;convert&lt;/span&gt;(
    &lt;span class="pl-s"&gt;"title"&lt;/span&gt;,
    &lt;span class="pl-k"&gt;lambda&lt;/span&gt; &lt;span class="pl-s1"&gt;v&lt;/span&gt;: {
        &lt;span class="pl-s"&gt;"upper"&lt;/span&gt;: &lt;span class="pl-s1"&gt;v&lt;/span&gt;.&lt;span class="pl-en"&gt;upper&lt;/span&gt;(),
        &lt;span class="pl-s"&gt;"lower"&lt;/span&gt;: &lt;span class="pl-s1"&gt;v&lt;/span&gt;.&lt;span class="pl-en"&gt;lower&lt;/span&gt;()
    }, &lt;span class="pl-s1"&gt;multi&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;
)&lt;/pre&gt;
&lt;p&gt;See the &lt;a href="https://sqlite-utils.datasette.io/en/stable/python-api.html#converting-data-in-columns"&gt;full documentation for table.convert()&lt;/a&gt; for more options.&lt;/p&gt;
&lt;h4 id="blog-performance"&gt;A more sophisticated example: analyzing log files&lt;/h4&gt;
&lt;p&gt;I used the new &lt;code&gt;sqlite-utils convert&lt;/code&gt; command earlier today, to debug a performance issue with my blog.&lt;/p&gt;
&lt;p&gt;Most of my blog traffic is served via Cloudflare with a 15 minute cache timeout - but occasionally I'll hit an uncached page, and they had started to feel not quite as snappy as I would expect.&lt;/p&gt;
&lt;p&gt;So I dipped into the Heroku dashboard, and saw this pretty sad looking graph:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Performance graph showing 95th percentile of 17s and max of 23s" src="https://static.simonwillison.net/static/2021/sad-performance.png" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Somehow my 50th percentile was nearly 10 seconds, and my maximum page response time was 23 seconds! Something was clearly very wrong.&lt;/p&gt;
&lt;p&gt;I use NGINX as part of my Heroku setup to buffer responses (see &lt;a href="https://simonwillison.net/2017/Oct/2/nginx-heroku/"&gt;Running gunicorn behind nginx on Heroku for buffering and logging&lt;/a&gt;), and I have custom NGINX configuration to write to the Heroku logs - mainly to work around a limitation in Heroku's default logging where it fails to record full user-agents or referrer headers.&lt;/p&gt;
&lt;p&gt;I extended that configuration to record the NGINX &lt;code&gt;request_time&lt;/code&gt;, &lt;code&gt;upstream_response_time&lt;/code&gt;, &lt;code&gt;upstream_connect_time&lt;/code&gt; and &lt;code&gt;upstream_header_time&lt;/code&gt; variables, which I hoped would help me figure out what was going on.&lt;/p&gt;
&lt;p&gt;After &lt;a href="https://github.com/simonw/simonwillisonblog/commit/dd0faaa64c0e361ae1d760894e201cac7b0224a4"&gt;applying that change&lt;/a&gt; I started seeing Heroku log lines that looked like this:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;2021-08-05T17:58:28.880469+00:00 app[web.1]: measure#nginx.service=4.212 request="GET /search/?type=blogmark&amp;amp;page=2&amp;amp;tag=highavailability HTTP/1.1" status_code=404 request_id=25eb296e-e970-4072-b75a-606e11e1db5b remote_addr="10.1.92.174" forwarded_for="114.119.136.88, 172.70.142.28" forwarded_proto="http" via="1.1 vegur" body_bytes_sent=179 referer="-" user_agent="Mozilla/5.0 (Linux; Android 7.0;) AppleWebKit/537.36 (KHTML, like Gecko) Mobile Safari/537.36 (compatible; PetalBot;+https://webmaster.petalsearch.com/site/petalbot)" request_time="4.212" upstream_response_time="4.212" upstream_connect_time="0.000" upstream_header_time="4.212";&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Next step: analyze those log lines.&lt;/p&gt;
&lt;p&gt;I ran this command for a few minutes to gather some logs:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;heroku logs -a simonwillisonblog --tail | grep 'measure#nginx.service' &amp;gt; /tmp/log.txt&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Having collected 488 log lines, the next step was to load them into SQLite.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;sqlite-utils insert&lt;/code&gt; command likes to work with JSON, but I just had raw log lines. I used &lt;code&gt;jq&lt;/code&gt; to convert each line into a &lt;code&gt;{"line": "raw log line"}&lt;/code&gt; JSON object, then piped that as newline-delimited JSON into &lt;code&gt;sqlite-utils insert&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat /tmp/log.txt | \
    jq --raw-input '{line: .}' --compact-output | \
    sqlite-utils insert /tmp/logs.db log - --nl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;jq --raw-input&lt;/code&gt; accepts input that is just raw lines of text, not yet valid JSON. &lt;code&gt;'{line: .}'&lt;/code&gt; is a tiny &lt;code&gt;jq&lt;/code&gt; program that builds &lt;code&gt;{"line": "raw input"}&lt;/code&gt; objects. &lt;code&gt;--compact-output&lt;/code&gt; causes &lt;code&gt;jq&lt;/code&gt; to output newline-delimited JSON.&lt;/p&gt;
&lt;p&gt;Then &lt;code&gt;sqlite-utils insert /tmp/logs.db log - --nl&lt;/code&gt; reads that newline-delimited JSON into a new SQLite &lt;code&gt;log&lt;/code&gt; table in a &lt;code&gt;logs.db&lt;/code&gt; database file (&lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#inserting-newline-delimited-json"&gt;full documentation here&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Update 6th January 2022:&lt;/strong&gt; &lt;a href="https://sqlite-utils.datasette.io/en/stable/changelog.html#v3-20"&gt;sqlite-utils 3.20&lt;/a&gt; introduced a new &lt;code&gt;sqlite-utils insert ... --lines&lt;/code&gt; option for importing raw lines, so you can now achieve this without using &lt;code&gt;jq&lt;/code&gt; at all. See 
&lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#inserting-unstructured-data-with-lines-and-text"&gt;Inserting unstructured data with --lines and --text&lt;/a&gt; for details.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now I had a SQLite table with a single column, &lt;code&gt;line&lt;/code&gt;. Next step: parse that nasty log format.&lt;/p&gt;
&lt;p&gt;To my surprise I couldn't find an existing Python library for parsing &lt;code&gt;key=value key2="quoted value"&lt;/code&gt; log lines. Instead I had to figure out a regular expression:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;([^\s=]+)=(?:"(.*?)"|(\S+))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's that expression visualized using &lt;a href="https://www.debuggex.com/"&gt;Debuggex&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of the regex visualized with debuggex" src="https://static.simonwillison.net/static/2021/debuggex-log-parser-regex.png" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;I used that regular expression as part of a custom function passed in to the &lt;code&gt;sqlite-utils convert&lt;/code&gt; tool:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sqlite-utils convert /tmp/logs.db log line --import re --multi "$(cat &amp;lt;&amp;lt;EOD
    r = re.compile(r'([^\s=]+)=(?:"(.*?)"|(\S+))')
    pairs = {}
    for key, value1, value2 in r.findall(value):
        pairs[key] = value1 or value2
    return pairs
EOD
)"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(This uses a &lt;code&gt;cat &amp;lt;&amp;lt;EOD&lt;/code&gt; trick to avoid having to figure out how to escape the single and double quotes in the Python code for usage in a zsh shell command.)&lt;/p&gt;
&lt;p&gt;Using &lt;code&gt;--multi&lt;/code&gt; here created new columns for each of the key/value pairs seen in that log file.&lt;/p&gt;
&lt;p&gt;One last step: convert the types. The new columns are all of type &lt;code&gt;text&lt;/code&gt; but I want to do sorting and arithmetic on them so I need to convert them to integers and floats. I used &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#transforming-tables"&gt;sqlite-utils transform&lt;/a&gt; for that:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sqlite-utils transform /tmp/logs.db log \
    --type 'measure#nginx.service' float \
    --type 'status_code' integer \
    --type 'body_bytes_sent' integer \
    --type 'request_time' float \
    --type 'upstream_response_time' float \
    --type 'upstream_connect_time' float \
    --type 'upstream_header_time' float
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's the &lt;a href="https://lite.datasette.io/?url=https://gist.githubusercontent.com/simonw/3454951e23cab709da42d25520dd78cf/raw/3383a16cd1f423d39c9c6923b6b37a3e74c4f148/logs.db#/logs/log"&gt;resulting log table&lt;/a&gt; (in Datasette Lite).&lt;/p&gt;
&lt;p&gt;&lt;img alt="Datasette showing the log table" src="https://static.simonwillison.net/static/2021/performance-logs.png" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Once the logs were in Datasette, the problem quickly became apparent when I &lt;a href="https://lite.datasette.io/?url=https://gist.githubusercontent.com/simonw/3454951e23cab709da42d25520dd78cf/raw/3383a16cd1f423d39c9c6923b6b37a3e74c4f148/logs.db#/logs/log?_sort_desc=request_time"&gt;sorted by request_time&lt;/a&gt;: an army of search engine crawlers were hitting deep linked filters in &lt;a href="https://simonwillison.net/2017/Oct/5/django-postgresql-faceted-search/"&gt;my faceted search engine&lt;/a&gt;, like &lt;code&gt;/search/?tag=geolocation&amp;amp;tag=offlineresources&amp;amp;tag=canvas&amp;amp;tag=javascript&amp;amp;tag=performance&amp;amp;tag=dragndrop&amp;amp;tag=crossdomain&amp;amp;tag=mozilla&amp;amp;tag=video&amp;amp;tag=tracemonkey&amp;amp;year=2009&amp;amp;type=blogmark&lt;/code&gt;. These are expensive pages to generate! They're also very unlikely to be in my Cloudflare cache.&lt;/p&gt;
&lt;p&gt;Could the answer be as simple as a &lt;code&gt;robots.txt&lt;/code&gt; rule blocking access to &lt;code&gt;/search/&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;I &lt;a href="https://github.com/simonw/simonwillisonblog/commit/4c0de5b9f01bb16fc89c587128a276055b0033bb"&gt;shipped that change&lt;/a&gt; and waited a few hours to see what the impact would be:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Heroku metrics showing a dramatic improvement after the deploy, and especially about 8 hours later" src="https://static.simonwillison.net/static/2021/robots-txt-effect.png" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;It took a while for the crawlers to notice that my &lt;code&gt;robots.txt&lt;/code&gt; had changed, but by 8 hours later my site performance was dramatically improved - I'm now seeing 99th percentile of around 450ms, compared to 25 seconds before I shipped the &lt;code&gt;robots.txt&lt;/code&gt; change!&lt;/p&gt;
&lt;p&gt;With this latest addition, &lt;a href="https://sqlite-utils.datasette.io/"&gt;sqlite-utils&lt;/a&gt; has evolved into a powerful tool for importing, cleaning and re-shaping data - especially when coupled with Datasette in order to explore, analyze and publish the results.&lt;/p&gt;
&lt;h4&gt;TIL this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/vscode/vs-code-regular-expressions"&gt;Search and replace with regular expressions in VS Code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/python/codespell"&gt;Check spelling using codespell&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/imagemagick/set-a-gif-to-loop"&gt;Set a GIF to loop using ImageMagick&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/sqlite/sqlite-aggregate-filter-clauses"&gt;SQLite aggregate filter clauses&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/imagemagick/compress-animated-gif"&gt;Compressing an animated GIF with ImageMagick mogrify&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/sqlite-transform"&gt;sqlite-transform&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/sqlite-transform/releases/tag/1.2.1"&gt;1.2.1&lt;/a&gt; - (&lt;a href="https://github.com/simonw/sqlite-transform/releases"&gt;10 releases total&lt;/a&gt;) - 2021-08-02
&lt;br /&gt;Tool for running transformations on columns in a SQLite database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/3.14"&gt;3.14&lt;/a&gt; - (&lt;a href="https://github.com/simonw/sqlite-utils/releases"&gt;82 releases total&lt;/a&gt;) - 2021-08-02
&lt;br /&gt;Python CLI utility and library for manipulating SQLite databases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-json-html"&gt;datasette-json-html&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-json-html/releases/tag/1.0.1"&gt;1.0.1&lt;/a&gt; - (&lt;a href="https://github.com/simonw/datasette-json-html/releases"&gt;6 releases total&lt;/a&gt;) - 2021-07-31
&lt;br /&gt;Datasette plugin for rendering HTML based on JSON values&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-publish-fly"&gt;datasette-publish-fly&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-publish-fly/releases/tag/1.0.2"&gt;1.0.2&lt;/a&gt; - (&lt;a href="https://github.com/simonw/datasette-publish-fly/releases"&gt;5 releases total&lt;/a&gt;) - 2021-07-30
&lt;br /&gt;Datasette plugin for publishing data using Fly&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cli"&gt;cli&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/performance"&gt;performance&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/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/data-science"&gt;data-science&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="cli"/><category term="performance"/><category term="projects"/><category term="sqlite"/><category term="datasette"/><category term="data-science"/><category term="weeknotes"/><category term="sqlite-utils"/></entry><entry><title>Joining CSV and JSON data with an in-memory SQLite database</title><link href="https://simonwillison.net/2021/Jun/19/sqlite-utils-memory/#atom-series" rel="alternate"/><published>2021-06-19T22:55:57+00:00</published><updated>2021-06-19T22:55:57+00:00</updated><id>https://simonwillison.net/2021/Jun/19/sqlite-utils-memory/#atom-series</id><summary type="html">
    &lt;p&gt;The new &lt;code&gt;sqlite-utils memory&lt;/code&gt; command can import CSV and JSON data directly into an in-memory SQLite database, combine and query it using SQL and output the results as CSV, JSON or various other formats of plain text tables.&lt;/p&gt;
&lt;h4&gt;sqlite-utils memory&lt;/h4&gt;
&lt;p&gt;The new feature is part of &lt;a href="https://sqlite-utils.datasette.io/en/latest/changelog.html#v3-10"&gt;sqlite-utils 3.10&lt;/a&gt;, which I released this morning. You can install it using &lt;code&gt;brew install sqlite-utils&lt;/code&gt; or &lt;code&gt;pip install sqlite-utils&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I've recorded &lt;a href="https://www.youtube.com/watch?v=OUjd0rkc678"&gt;this video&lt;/a&gt; demonstrating the new feature - with full accompanying notes below.&lt;/p&gt;

&lt;iframe style="max-width: 100%" width="560" height="315" src="https://www.youtube-nocookie.com/embed/OUjd0rkc678" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="allowfullscreen"&gt; &lt;/iframe&gt;

&lt;p&gt;&lt;code&gt;sqlite-utils&lt;/code&gt; already offers a mechanism for importing CSV and JSON data into a SQLite database file, in the form of the &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#cli-inserting-data"&gt;sqlite-utils insert&lt;/a&gt; command. Processing data with this involves two steps: first import it into a &lt;code&gt;temp.db&lt;/code&gt; file, then use &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#running-sql-queries"&gt;sqlite-utils query&lt;/a&gt; to run queries and output the results.&lt;/p&gt;
&lt;p&gt;Using SQL to re-shape data is really useful - since &lt;code&gt;sqlite-utils&lt;/code&gt; can output in multiple different formats, I frequently find myself loading in a CSV file and exporting it back out as JSON, or vice-versa.&lt;/p&gt;
&lt;p&gt;This week I realized that I had most of the pieces in place to reduce this to a single step. The new &lt;code&gt;sqlite-utils memory&lt;/code&gt; command (&lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#cli-memory"&gt;full documentation here&lt;/a&gt;) operates against a temporary, in-memory SQLite database. It can import data, execute SQL and output the result in a one-liner, without needing any temporary database files along the way.&lt;/p&gt;
&lt;p&gt;Here's an example. My &lt;a href="https://github.com/dogsheep"&gt;Dogsheep&lt;/a&gt; GitHub organization has a number of repositories. GitHub make those available via an authentication-optional API endpoint at &lt;a href="https://api.github.com/users/dogsheep/repos"&gt;https://api.github.com/users/dogsheep/repos&lt;/a&gt; - which returns JSON that looks like this (simplified):&lt;/p&gt;
&lt;div class="highlight highlight-source-json"&gt;&lt;pre&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-c1"&gt;197431109&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-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;dogsheep-beta&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;full_name&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;dogsheep/dogsheep-beta&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;size&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-c1"&gt;61&lt;/span&gt;,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;stargazers_count&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-c1"&gt;79&lt;/span&gt;,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;watchers_count&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-c1"&gt;79&lt;/span&gt;,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;forks&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-c1"&gt;0&lt;/span&gt;,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;open_issues&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-c1"&gt;11&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-c1"&gt;256834907&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-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;dogsheep-photos&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;full_name&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;dogsheep/dogsheep-photos&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;size&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-c1"&gt;64&lt;/span&gt;,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;stargazers_count&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-c1"&gt;116&lt;/span&gt;,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;watchers_count&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-c1"&gt;116&lt;/span&gt;,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;forks&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-c1"&gt;5&lt;/span&gt;,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;open_issues&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-c1"&gt;18&lt;/span&gt;
  }
]&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;With &lt;code&gt;sqlite-utils memory&lt;/code&gt; we can see the 3 most popular repos by number of stars like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ curl -s 'https://api.github.com/users/dogsheep/repos' \
  | sqlite-utils memory - '
      select full_name, forks_count, stargazers_count as stars
      from stdin order by stars desc limit 3
    ' -t
full_name                     forks_count    stars
--------------------------  -------------  -------
dogsheep/twitter-to-sqlite             12      225
dogsheep/github-to-sqlite              14      139
dogsheep/dogsheep-photos                5      116
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We're using &lt;code&gt;curl&lt;/code&gt; to fetch the JSON and pipe it into &lt;code&gt;sqlite-utils memory&lt;/code&gt; - the &lt;code&gt;-&lt;/code&gt; means "read from standard input". Then we pass the following SQL query:&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;select&lt;/span&gt; full_name, forks_count, stargazers_count &lt;span class="pl-k"&gt;as&lt;/span&gt; stars
&lt;span class="pl-k"&gt;from&lt;/span&gt; stdin &lt;span class="pl-k"&gt;order by&lt;/span&gt; stars &lt;span class="pl-k"&gt;desc&lt;/span&gt; &lt;span class="pl-k"&gt;limit&lt;/span&gt; &lt;span class="pl-c1"&gt;3&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;stdin&lt;/code&gt; is the temporary table created for the data piped in to the tool. The query selects three of the JSON properties, renames &lt;code&gt;stargazers_count&lt;/code&gt; to &lt;code&gt;stars&lt;/code&gt;, sorts by stars and return the first three.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;-t&lt;/code&gt; option here means "output as a formatted table" - without that option we get JSON:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ curl -s 'https://api.github.com/users/dogsheep/repos' \
  | sqlite-utils memory - '
      select full_name, forks_count, stargazers_count as stars
      from stdin order by stars desc limit 3
    '  
[{"full_name": "dogsheep/twitter-to-sqlite", "forks_count": 12, "stars": 225},
 {"full_name": "dogsheep/github-to-sqlite", "forks_count": 14, "stars": 139},
 {"full_name": "dogsheep/dogsheep-photos", "forks_count": 5, "stars": 116}]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or we can use &lt;code&gt;--csv&lt;/code&gt; to get back CSV:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ curl -s 'https://api.github.com/users/dogsheep/repos' \
  | sqlite-utils memory - '
      select full_name, forks_count, stargazers_count as stars
      from stdin order by stars desc limit 3
    ' --csv
full_name,forks_count,stars
dogsheep/twitter-to-sqlite,12,225
dogsheep/github-to-sqlite,14,139
dogsheep/dogsheep-photos,5,116
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;-t&lt;/code&gt; option supports a number of different formats, specified using &lt;code&gt;--fmt&lt;/code&gt;. If I wanted to generate a LaTeX table of the top repos by stars I could do this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ curl -s 'https://api.github.com/users/dogsheep/repos' \
  | sqlite-utils memory - '
      select full_name, forks_count, stargazers_count as stars
      from stdin order by stars desc limit 3
    ' -t --fmt=latex
\begin{tabular}{lrr}
\hline
 full\_name                  &amp;amp;   forks\_count &amp;amp;   stars \\
\hline
 dogsheep/twitter-to-sqlite &amp;amp;            12 &amp;amp;     225 \\
 dogsheep/github-to-sqlite  &amp;amp;            14 &amp;amp;     139 \\
 dogsheep/dogsheep-photos   &amp;amp;             5 &amp;amp;     116 \\
\hline
\end{tabular}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can run aggregate queries too - let's add up the total size and total number of stars across all of those repositories:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ curl -s 'https://api.github.com/users/dogsheep/repos' \
| sqlite-utils memory - '
    select sum(size), sum(stargazers_count) from stdin
' -t
  sum(size)    sum(stargazers_count)
-----------  -----------------------
        843                      934
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(I believe size here is measured in kilobytes: the GitHub API documentation isn't clear on this point.)&lt;/p&gt;
&lt;h4 id="joining-across-different-files"&gt;Joining across different files&lt;/h4&gt;
&lt;p&gt;All of these examples have worked with JSON data piped into the tool - but you can also pass one or more files, of different formats, in a way that lets you execute joins against them.&lt;/p&gt;
&lt;p&gt;As an example, let's combine two sources of data.&lt;/p&gt;
&lt;p&gt;The New York Times publish a &lt;a href="https://github.com/nytimes/covid-19-data/blob/master/us-states.csv"&gt;us-states.csv&lt;/a&gt; file with Covid cases and deaths by state over time.&lt;/p&gt;
&lt;p&gt;The CDC have an &lt;a href="https://covid.cdc.gov/covid-data-tracker/COVIDData/getAjaxData?id=vaccination_data"&gt;undocumented JSON endpoint&lt;/a&gt; (which I've been &lt;a href="https://github.com/simonw/cdc-vaccination-history"&gt;archiving here&lt;/a&gt;) tracking the progress of vaccination across different states.&lt;/p&gt;
&lt;p&gt;We're going to run a join from that CSV data to that JSON data, and output a table of results.&lt;/p&gt;
&lt;p&gt;First, we need to download the files. The &lt;a href="https://covid.cdc.gov/covid-data-tracker/COVIDData/getAjaxData?id=vaccination_data"&gt;CDC JSON data&lt;/a&gt; isn't quite in the right shape for our purposes:&lt;/p&gt;
&lt;div class="highlight highlight-source-json"&gt;&lt;pre&gt;{
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;runid&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;: &lt;span class="pl-c1"&gt;2023&lt;/span&gt;,
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;vaccination_data&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;Date&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;2021-06-19&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;Location&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;US&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;ShortName&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;USA&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
      &lt;span class="pl-s"&gt;...&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;sqlite-utils&lt;/code&gt; expects a flat JSON array of objects - we can use &lt;a href="https://stedolan.github.io/jq/"&gt;jq&lt;/a&gt; to re-shape the data like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ curl https://covid.cdc.gov/covid-data-tracker/COVIDData/getAjaxData?id=vaccination_data \
  | jq .vaccination_data &amp;gt; vaccination_data.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The New York Times data is good as is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ wget 'https://github.com/nytimes/covid-19-data/raw/master/us-states.csv'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now that we have the data locally, we can run a join to combine it using the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sqlite-utils memory us-states.csv vaccination_data.json "
  select
    max(t1.date),
    t1.state,
    t1.cases,
    t1.deaths,
    t2.Census2019,
    t2.Dist_Per_100K
  from
    t1
      join t2 on t1.state = replace(t2.LongName, 'New York State', 'New York')
  group by
    t1.state
  order by
    Dist_Per_100K desc
" -t
max(t1.date)    state                       cases    deaths    Census2019    Dist_Per_100K
--------------  ------------------------  -------  --------  ------------  ---------------
2021-06-18      District of Columbia        49243      1141        705749           149248
2021-06-18      Vermont                     24360       256        623989           146257
2021-06-18      Rhode Island               152383      2724       1059361           141291
2021-06-18      Massachusetts              709263     17960       6892503           139692
2021-06-18      Maryland                   461852      9703       6045680           138193
2021-06-18      Maine                       68753       854       1344212           136894
2021-06-18      Hawaii                      35903       507       1415872           136024
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I'm using automatically created numeric aliases &lt;code&gt;t1&lt;/code&gt; and &lt;code&gt;t2&lt;/code&gt; for the files here, but I can also use their full table names &lt;code&gt;"us-states"&lt;/code&gt; (quotes needed due to the hyphen) and &lt;code&gt;vaccination_data&lt;/code&gt; instead.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;replace()&lt;/code&gt; operation there is needed because the &lt;code&gt;vaccination_data.json&lt;/code&gt; file calls New York "New York State" while the &lt;code&gt;us-states.csv&lt;/code&gt; file just calls it "New York".&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;max(t1.date)&lt;/code&gt; and &lt;code&gt;group by t1.state&lt;/code&gt; is &lt;a href="http://www.sqlite.org/draft/lang_select.html#bareagg"&gt;a useful SQLite&lt;/a&gt; trick: if you perform a &lt;code&gt;group by&lt;/code&gt; and then ask for the &lt;code&gt;max()&lt;/code&gt; of a value, the other columns returned from that table will be the columns for the row that contains that maximum value.&lt;/p&gt;
&lt;p&gt;This demo is a bit of a stretch - once I reach this level of complexity I'm more likely to load the files into a SQLite database file on disk and open them up in &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; - but it's a fun example of a more complex join in action.&lt;/p&gt;
&lt;h4&gt;Also in sqlite-utils 3.10&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;sqlite-utils memory&lt;/code&gt; command has another new trick up its sleeve: it automatically detects which columns in a CSV or TSV file contain integer or float values and creates the corresponding in-memory SQLite table with the correct types. This ensures &lt;code&gt;max()&lt;/code&gt; and &lt;code&gt;sum()&lt;/code&gt; and &lt;code&gt;order by&lt;/code&gt; work in a predictable manner, without accidentally sorting &lt;code&gt;1&lt;/code&gt; as higher than &lt;code&gt;11&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I didn't want to break backwards compatibility for existing users of the &lt;code&gt;sqlite-utils insert&lt;/code&gt; command so I've added type detection there as a new option, &lt;code&gt;--detect-types&lt;/code&gt; or &lt;code&gt;-d&lt;/code&gt; for short:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sqlite-utils insert my.db us_states us-states.csv --csv -d
  [####################################]  100%
$ sqlite-utils schema my.db
CREATE TABLE "us_states" (
   [date] TEXT,
   [state] TEXT,
   [fips] INTEGER,
   [cases] INTEGER,
   [deaths] INTEGER
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There's more &lt;a href="https://sqlite-utils.datasette.io/en/latest/changelog.html#v3-10"&gt;in the changelog&lt;/a&gt;.&lt;/p&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/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/3.10"&gt;3.10&lt;/a&gt; - (&lt;a href="https://github.com/simonw/sqlite-utils/releases"&gt;78 releases total&lt;/a&gt;) - 2021-06-19
&lt;br /&gt;Python CLI utility and library for manipulating SQLite databases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/dogsheep/dogsheep-beta"&gt;dogsheep-beta&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/dogsheep/dogsheep-beta/releases/tag/0.10.2"&gt;0.10.2&lt;/a&gt; - (&lt;a href="https://github.com/dogsheep/dogsheep-beta/releases"&gt;20 releases total&lt;/a&gt;) - 2021-06-13
&lt;br /&gt;Build a search index across content from multiple SQLite database tables and run faceted searches against it using Datasette&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/yaml-to-sqlite"&gt;yaml-to-sqlite&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/yaml-to-sqlite/releases/tag/1.0"&gt;1.0&lt;/a&gt; - (&lt;a href="https://github.com/simonw/yaml-to-sqlite/releases"&gt;5 releases total&lt;/a&gt;) - 2021-06-13
&lt;br /&gt;Utility for converting YAML files to SQLite&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/markdown-to-sqlite"&gt;markdown-to-sqlite&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/markdown-to-sqlite/releases/tag/1.0"&gt;1.0&lt;/a&gt; - (&lt;a href="https://github.com/simonw/markdown-to-sqlite/releases"&gt;2 releases total&lt;/a&gt;) - 2021-06-13
&lt;br /&gt;CLI tool for loading markdown files into a SQLite database&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;TIL this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/vim/mouse-support-in-vim"&gt;Mouse support in vim&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/csv"&gt;csv&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/json"&gt;json&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sql"&gt;sql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="csv"/><category term="json"/><category term="projects"/><category term="sql"/><category term="sqlite"/><category term="weeknotes"/><category term="sqlite-utils"/></entry><entry><title>Refactoring databases with sqlite-utils extract</title><link href="https://simonwillison.net/2020/Sep/23/sqlite-utils-extract/#atom-series" rel="alternate"/><published>2020-09-23T16:02:08+00:00</published><updated>2020-09-23T16:02:08+00:00</updated><id>https://simonwillison.net/2020/Sep/23/sqlite-utils-extract/#atom-series</id><summary type="html">
    &lt;p&gt;Yesterday &lt;a href="https://simonwillison.net/2020/Sep/23/sqlite-advanced-alter-table/"&gt;I described&lt;/a&gt; the new &lt;a href="https://sqlite-utils.readthedocs.io/en/stable/cli.html#transforming-tables"&gt;sqlite-utils transform&lt;/a&gt; mechanism for applying SQLite table transformations that go beyond those supported by &lt;code&gt;ALTER TABLE&lt;/code&gt;. The other new feature in &lt;a href="https://sqlite-utils.readthedocs.io/en/stable/changelog.html#v2-20"&gt;sqlite-utils 2.20&lt;/a&gt; builds on that capability to allow you to refactor a database table by extracting columns into separate tables. I’ve called it &lt;a href="https://sqlite-utils.readthedocs.io/en/stable/cli.html#cli-extract"&gt;sqlite-utils extract&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;The problem&lt;/h4&gt;
&lt;p&gt;Much of the data I work with in Datasette starts off as a CSV file published by an organization or government. Since CSV files aren’t relational databases, they are often denormalized. It’s particularly common to see something like this:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Organization Group Code&lt;/th&gt;
&lt;th&gt;Organization Group&lt;/th&gt;
&lt;th&gt;Department Code&lt;/th&gt;
&lt;th&gt;Department&lt;/th&gt;
&lt;th&gt;Union Code&lt;/th&gt;
&lt;th&gt;Union&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Public Protection&lt;/td&gt;
&lt;td&gt;POL&lt;/td&gt;
&lt;td&gt;Police&lt;/td&gt;
&lt;td&gt;911&lt;/td&gt;
&lt;td&gt;POA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Community Health&lt;/td&gt;
&lt;td&gt;DPH&lt;/td&gt;
&lt;td&gt;Public Health&lt;/td&gt;
&lt;td&gt;250&lt;/td&gt;
&lt;td&gt;SEIU, Local 1021, Misc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Public Protection&lt;/td&gt;
&lt;td&gt;FIR&lt;/td&gt;
&lt;td&gt;Fire Department&lt;/td&gt;
&lt;td&gt;798&lt;/td&gt;
&lt;td&gt;Firefighters,Local 798, Unit 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Public Protection&lt;/td&gt;
&lt;td&gt;POL&lt;/td&gt;
&lt;td&gt;Police&lt;/td&gt;
&lt;td&gt;911&lt;/td&gt;
&lt;td&gt;POA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This is an extract from the &lt;a href="https://data.sfgov.org/City-Management-and-Ethics/Employee-Compensation/88g8-5mnd"&gt;San Francisco Employee Compensation&lt;/a&gt; dataset from DataSF.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;sqlite-utils extract&lt;/code&gt; command-line tool, and the &lt;code&gt;table.extract()&lt;/code&gt; Python method that underlies it, can be used to extract these duplicated column pairs out into separate tables with foreign key relationships from the main table.&lt;/p&gt;
&lt;h4&gt;How to refactor that data&lt;/h4&gt;
&lt;p&gt;Here's how to use &lt;code&gt;sqlite-utils&lt;/code&gt; to clean up and refactor that compensation data.&lt;/p&gt;
&lt;p&gt;First, grab the data. It's a 150M CSV file containing over 600,000 rows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -o salaries.csv 'https://data.sfgov.org/api/views/88g8-5mnd/rows.csv?accessType=DOWNLOAD'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;sqlite-utils insert&lt;/code&gt; to load that into a SQLite database:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sqlite-utils insert salaries.db salaries salaries.csv --csv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fire up Datasette to check that the data looks right:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;datasette salaries.db
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There's a catch here: the schema for the generated table (shown at the bottom of &lt;code&gt;http://localhost:8001/salaries/salaries&lt;/code&gt;) reveals that because we imported from CSV every column is a text column. Since some of this data is numeric we should convert to numbers, so we can do things like sort the table by the highest salary.&lt;/p&gt;
&lt;p&gt;We can do that using &lt;code&gt;sqlite-transform&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sqlite-utils transform salaries.db salaries \
  --type 'Employee Identifier' integer \
  --type Salaries float \
  --type Overtime float \
  --type 'Other Salaries' float \
  --type 'Total Salary' float \
  --type 'Retirement' float \
  --type 'Health and Dental' float \
  --type 'Other Benefits' float \
  --type 'Total Benefits' float \
  --type 'Total Compensation' float
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;[&lt;strong&gt;Update 13 August 2021&lt;/strong&gt;: As of &lt;a href="https://sqlite-utils.datasette.io/en/stable/changelog.html#v3-10"&gt;sqlite-utils 3.10&lt;/a&gt; you can instead run &lt;code&gt;sqlite-utils insert salaries.db salaries salaries.csv --csv --detect-types&lt;/code&gt;. The &lt;code&gt;--detect-types&lt;/code&gt; (or &lt;code&gt;-d&lt;/code&gt;) option will detect types for you during the initial import.]&lt;/p&gt;
&lt;p&gt;Having run that command, here's the new database schema:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sqlite3 salaries.db '.schema salaries'
CREATE TABLE IF NOT EXISTS "salaries" (
   [rowid] INTEGER PRIMARY KEY,
   [Year Type] TEXT,
   [Year] TEXT,
   [Organization Group Code] TEXT,
   [Organization Group] TEXT,
   [Department Code] TEXT,
   [Department] TEXT,
   [Union Code] TEXT,
   [Union] TEXT,
   [Job Family Code] TEXT,
   [Job Family] TEXT,
   [Job Code] TEXT,
   [Job] TEXT,
   [Employee Identifier] INTEGER,
   [Salaries] FLOAT,
   [Overtime] FLOAT,
   [Other Salaries] FLOAT,
   [Total Salary] FLOAT,
   [Retirement] FLOAT,
   [Health and Dental] FLOAT,
   [Other Benefits] FLOAT,
   [Total Benefits] FLOAT,
   [Total Compensation] FLOAT
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can start extracting those columns. We do this using several rounds of the &lt;code&gt;sqlite-utils extract&lt;/code&gt; command, one for each duplicated pairs.&lt;/p&gt;
&lt;p&gt;For &lt;code&gt;Organization Group Code&lt;/code&gt; and &lt;code&gt;Organization Group&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sqlite-utils extract salaries.db salaries \
   'Organization Group Code' 'Organization Group' \
  --table 'organization_groups' \
  --fk-column 'organization_group_id' \
  --rename 'Organization Group Code' code \
  --rename 'Organization Group' name
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This took about 12 minutes on my laptop, and displayed a progress bar as it runs. (UPDATE: &lt;a href="https://github.com/simonw/sqlite-utils/issues/172"&gt;in issue #172&lt;/a&gt; I improved the performance and knocked it down to just 4 seconds. I also removed the progress bar).&lt;/p&gt;
&lt;p&gt;Here's the refactored database schema:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sqlite3 salaries.db .schema
CREATE TABLE [organization_groups] (
   [id] INTEGER PRIMARY KEY,
   [code] TEXT,
   [name] TEXT
);
CREATE TABLE IF NOT EXISTS "salaries" (
   [rowid] INTEGER PRIMARY KEY,
   [Year Type] TEXT,
   [Year] TEXT,
   [organization_group_id] INTEGER,
   [Department Code] TEXT,
   [Department] TEXT,
   [Union Code] TEXT,
   [Union] TEXT,
   [Job Family Code] TEXT,
   [Job Family] TEXT,
   [Job Code] TEXT,
   [Job] TEXT,
   [Employee Identifier] INTEGER,
   [Salaries] FLOAT,
   [Overtime] FLOAT,
   [Other Salaries] FLOAT,
   [Total Salary] FLOAT,
   [Retirement] FLOAT,
   [Health and Dental] FLOAT,
   [Other Benefits] FLOAT,
   [Total Benefits] FLOAT,
   [Total Compensation] FLOAT,
   FOREIGN KEY(organization_group_id) REFERENCES organization_groups(id)
);
CREATE UNIQUE INDEX [idx_organization_groups_code_name]
    ON [organization_groups] ([code], [name]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now fire up Datasette to confirm it had the desired effect:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;datasette salaries.db
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here's what that looks like:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2020/refactored-one-column.png" alt="Screenshot of the first few columns of the table, showing links displayed in the new organization_group_id column" data-canonical-src="https://static.simonwillison.net/static/2020/refactored-one-column.png" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Note that the new &lt;code&gt;organization_group_id&lt;/code&gt; column still shows the name of the organization group, because Datasette automatically de-references foreign key relationships when it displays a table and uses any column called &lt;code&gt;name&lt;/code&gt; (or &lt;code&gt;title&lt;/code&gt; or &lt;code&gt;value&lt;/code&gt;) as the label for a link to the record.&lt;/p&gt;
&lt;p&gt;Let's extract the other columns. This will take a while:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sqlite-utils extract salaries.db salaries \
   'Department Code' 'Department' \
  --table 'departments' \
  --fk-column 'department_id' \
  --rename 'Department Code' code \
  --rename 'Department' name

sqlite-utils extract salaries.db salaries \
   'Union Code' 'Union' \
  --table 'unions' \
  --fk-column 'union_id' \
  --rename 'Union Code' code \
  --rename 'Union' name

sqlite-utils extract salaries.db salaries \
   'Job Family Code' 'Job Family' \
  --table 'job_families' \
  --fk-column 'job_family_id' \
  --rename 'Job Family Code' code \
  --rename 'Job Family' name

sqlite-utils extract salaries.db salaries \
   'Job Code' 'Job' \
  --table 'jobs' \
  --fk-column 'job_id' \
  --rename 'Job Code' code \
  --rename 'Job' name
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Our finished schema looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sqlite3 salaries.db .schema
CREATE TABLE [organization_groups] (
   [id] INTEGER PRIMARY KEY,
   [code] TEXT,
   [name] TEXT
);
CREATE TABLE [departments] (
   [id] INTEGER PRIMARY KEY,
   [code] TEXT,
   [name] TEXT
);
CREATE TABLE [unions] (
   [id] INTEGER PRIMARY KEY,
   [code] TEXT,
   [name] TEXT
);
CREATE TABLE [job_families] (
   [id] INTEGER PRIMARY KEY,
   [code] TEXT,
   [name] TEXT
);
CREATE TABLE [jobs] (
   [id] INTEGER PRIMARY KEY,
   [code] TEXT,
   [name] TEXT
);
CREATE TABLE IF NOT EXISTS "salaries" (
   [rowid] INTEGER PRIMARY KEY,
   [Year Type] TEXT,
   [Year] TEXT,
   [organization_group_id] INTEGER REFERENCES [organization_groups]([id]),
   [department_id] INTEGER REFERENCES [departments]([id]),
   [union_id] INTEGER REFERENCES [unions]([id]),
   [job_family_id] INTEGER REFERENCES [job_families]([id]),
   [job_id] INTEGER,
   [Employee Identifier] INTEGER,
   [Salaries] FLOAT,
   [Overtime] FLOAT,
   [Other Salaries] FLOAT,
   [Total Salary] FLOAT,
   [Retirement] FLOAT,
   [Health and Dental] FLOAT,
   [Other Benefits] FLOAT,
   [Total Benefits] FLOAT,
   [Total Compensation] FLOAT,
   FOREIGN KEY(job_id) REFERENCES jobs(id)
);
CREATE UNIQUE INDEX [idx_organization_groups_code_name]
    ON [organization_groups] ([code], [name]);
CREATE UNIQUE INDEX [idx_departments_code_name]
    ON [departments] ([code], [name]);
CREATE UNIQUE INDEX [idx_unions_code_name]
    ON [unions] ([code], [name]);
CREATE UNIQUE INDEX [idx_job_families_code_name]
    ON [job_families] ([code], [name]);
CREATE UNIQUE INDEX [idx_jobs_code_name]
    ON [jobs] ([code], [name]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We've also shrunk our database file quite a bit. Before the transformations &lt;code&gt;salaries.db&lt;/code&gt; was 159MB. It's now just 70MB - that's less than half the size!&lt;/p&gt;
&lt;p&gt;I used &lt;code&gt;datasette publish cloudrun&lt;/code&gt; to publish a copy of my final database here:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://sf-employee-compensation.datasettes.com/salaries/salaries"&gt;https://sf-employee-compensation.datasettes.com/salaries/salaries&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Here's the command I used to publish it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;datasette publish cloudrun salaries.db \
  --service sf-employee-compensation \
  --title "San Francisco Employee Compensation (as-of 21 Sep 2020)" \
  --source "DataSF" \
  --source_url "https://data.sfgov.org/City-Management-and-Ethics/Employee-Compensation/88g8-5mnd" \
  --about "About this project" \
  --about_url "https://simonwillison.net/2020/Sep/23/sqlite-utils-extract/" \
  --install datasette-block-robots \
  --install datasette-vega \
  --install datasette-copyable \
  --install datasette-graphql
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Bonus: explore salaries with GraphQL&lt;/h4&gt;
&lt;p&gt;You may have noticed my &lt;code&gt;datasette publish&lt;/code&gt; line above finished with the line &lt;code&gt;--install datasette-graphql&lt;/code&gt;. This installs the &lt;a href="https://github.com/simonw/datasette-graphql"&gt;datasette-graphql&lt;/a&gt; plugin as part of the deployment to Cloud Run. Which means we can query the salary data using GraphQL as an alternative to SQL!&lt;/p&gt;
&lt;p&gt;Here's a GraphQL query that shows the ten highest paid employees, including their various expanded foreign key references:&lt;/p&gt;
&lt;div class="highlight highlight-source-graphql"&gt;&lt;pre&gt;{
  &lt;span class="pl-v"&gt;salaries&lt;/span&gt;(&lt;span class="pl-v"&gt;sort_desc&lt;/span&gt;:&lt;span class="pl-c1"&gt; Total_Compensation&lt;/span&gt;, &lt;span class="pl-v"&gt;first&lt;/span&gt;: &lt;span class="pl-c1"&gt;10&lt;/span&gt;) {
    &lt;span class="pl-v"&gt;nodes&lt;/span&gt; {
      &lt;span class="pl-v"&gt;Year_Type&lt;/span&gt;
      &lt;span class="pl-v"&gt;Year&lt;/span&gt;
      &lt;span class="pl-v"&gt;union_id&lt;/span&gt; {
        &lt;span class="pl-v"&gt;id&lt;/span&gt;
        &lt;span class="pl-v"&gt;name&lt;/span&gt;
      }
      &lt;span class="pl-v"&gt;job_id&lt;/span&gt; {
        &lt;span class="pl-v"&gt;id&lt;/span&gt;
        &lt;span class="pl-v"&gt;name&lt;/span&gt;
      }
      &lt;span class="pl-v"&gt;job_family_id&lt;/span&gt; {
        &lt;span class="pl-v"&gt;id&lt;/span&gt;
        &lt;span class="pl-v"&gt;name&lt;/span&gt;
      }
      &lt;span class="pl-v"&gt;department_id&lt;/span&gt; {
        &lt;span class="pl-v"&gt;id&lt;/span&gt;
        &lt;span class="pl-v"&gt;name&lt;/span&gt;
      }
      &lt;span class="pl-v"&gt;organization_group_id&lt;/span&gt; {
        &lt;span class="pl-v"&gt;id&lt;/span&gt;
        &lt;span class="pl-v"&gt;name&lt;/span&gt;
      }
      &lt;span class="pl-v"&gt;Salaries&lt;/span&gt;
      &lt;span class="pl-v"&gt;Overtime&lt;/span&gt;
      &lt;span class="pl-v"&gt;Other_Salaries&lt;/span&gt;
      &lt;span class="pl-v"&gt;Total_Salary&lt;/span&gt;
      &lt;span class="pl-v"&gt;Retirement&lt;/span&gt;
      &lt;span class="pl-v"&gt;Health_and_Dental&lt;/span&gt;
      &lt;span class="pl-v"&gt;Other_Benefits&lt;/span&gt;
      &lt;span class="pl-v"&gt;Total_Benefits&lt;/span&gt;
      &lt;span class="pl-v"&gt;Total_Compensation&lt;/span&gt;
      &lt;span class="pl-v"&gt;rowid&lt;/span&gt;
      &lt;span class="pl-v"&gt;Employee_Identifier&lt;/span&gt;
    }
  }
}&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can &lt;a href="https://sf-employee-compensation.datasettes.com/graphql?query=%7B%0A%20%20salaries(sort_desc%3A%20Total_Compensation%2C%20first%3A%2010)%20%7B%0A%20%20%20%20nodes%20%7B%0A%20%20%20%20%20%20Year_Type%0A%20%20%20%20%20%20Year%0A%20%20%20%20%20%20union_id%20%7B%0A%20%20%20%20%20%20%20%20id%0A%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20job_id%20%7B%0A%20%20%20%20%20%20%20%20id%0A%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20job_family_id%20%7B%0A%20%20%20%20%20%20%20%20id%0A%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20department_id%20%7B%0A%20%20%20%20%20%20%20%20id%0A%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20organization_group_id%20%7B%0A%20%20%20%20%20%20%20%20id%0A%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20Salaries%0A%20%20%20%20%20%20Overtime%0A%20%20%20%20%20%20Other_Salaries%0A%20%20%20%20%20%20Total_Salary%0A%20%20%20%20%20%20Retirement%0A%20%20%20%20%20%20Health_and_Dental%0A%20%20%20%20%20%20Other_Benefits%0A%20%20%20%20%20%20Total_Benefits%0A%20%20%20%20%20%20Total_Compensation%0A%20%20%20%20%20%20rowid%0A%20%20%20%20%20%20Employee_Identifier%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A"&gt;try that query out here&lt;/a&gt; in the GraphiQL API explorer.&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/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/graphql"&gt;graphql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="databases"/><category term="projects"/><category term="sqlite"/><category term="graphql"/><category term="sqlite-utils"/></entry><entry><title>Executing advanced ALTER TABLE operations in SQLite</title><link href="https://simonwillison.net/2020/Sep/23/sqlite-advanced-alter-table/#atom-series" rel="alternate"/><published>2020-09-23T01:00:35+00:00</published><updated>2020-09-23T01:00:35+00:00</updated><id>https://simonwillison.net/2020/Sep/23/sqlite-advanced-alter-table/#atom-series</id><summary type="html">
    &lt;p&gt;SQLite's ALTER TABLE has some significant limitations: it can't drop columns (UPDATE: that was fixed in &lt;a href="https://www.sqlite.org/changes.html#version_3_35_0"&gt;SQLite 3.35.0&lt;/a&gt; in March 2021), it can't alter NOT NULL status, it can't change column types. Since I spend a lot of time with SQLite these days I've written some code to fix this - both from Python and as a command-line utility.&lt;/p&gt;
&lt;p&gt;To SQLite's credit, not only are these limitations &lt;a href="https://www.sqlite.org/lang_altertable.html"&gt;well explained&lt;/a&gt; in the documentation but the explanation is accompanied by &lt;a href="https://www.sqlite.org/lang_altertable.html#making_other_kinds_of_table_schema_changes"&gt;a detailed description&lt;/a&gt; of the recommended workaround. The short version looks something like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Start a transaction&lt;/li&gt;
&lt;li&gt;Create a new temporary table with the exact shape you would like&lt;/li&gt;
&lt;li&gt;Copy all of your old data across using INSERT INTO temp_table SELECT FROM old_table&lt;/li&gt;
&lt;li&gt;Drop the old table&lt;/li&gt;
&lt;li&gt;Rename the temp table to the old table&lt;/li&gt;
&lt;li&gt;Commit the transaction&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My &lt;a href="https://sqlite-utils.readthedocs.io/"&gt;sqlite-utils&lt;/a&gt; tool and Python library aims to make working with SQLite as convenient as possible. So I set out to build a utility method for performing this kind of large scale table transformation. I've called it &lt;code&gt;table.transform(...)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here are some simple examples of what it can do, lifted from &lt;a href="https://sqlite-utils.readthedocs.io/en/stable/python-api.html#python-api-transform"&gt;the documentation&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;# Convert the 'age' column to an integer, and 'weight' to a float&lt;/span&gt;
&lt;span class="pl-s1"&gt;table&lt;/span&gt;.&lt;span class="pl-en"&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;"age"&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;float&lt;/span&gt;})

&lt;span class="pl-c"&gt;# Rename the 'age' column to 'initial_age':&lt;/span&gt;
&lt;span class="pl-s1"&gt;table&lt;/span&gt;.&lt;span class="pl-en"&gt;transform&lt;/span&gt;(&lt;span class="pl-s1"&gt;rename&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;{&lt;span class="pl-s"&gt;"age"&lt;/span&gt;: &lt;span class="pl-s"&gt;"initial_age"&lt;/span&gt;})

&lt;span class="pl-c"&gt;# Drop the 'age' column:&lt;/span&gt;
&lt;span class="pl-s1"&gt;table&lt;/span&gt;.&lt;span class="pl-en"&gt;transform&lt;/span&gt;(&lt;span class="pl-s1"&gt;drop&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;{&lt;span class="pl-s"&gt;"age"&lt;/span&gt;})

&lt;span class="pl-c"&gt;# Make `user_id` the new primary key&lt;/span&gt;
&lt;span class="pl-s1"&gt;table&lt;/span&gt;.&lt;span class="pl-en"&gt;transform&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;"user_id"&lt;/span&gt;)

&lt;span class="pl-c"&gt;# Make the 'age' and 'weight' columns NOT NULL&lt;/span&gt;
&lt;span class="pl-s1"&gt;table&lt;/span&gt;.&lt;span class="pl-en"&gt;transform&lt;/span&gt;(&lt;span class="pl-s1"&gt;not_null&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;{&lt;span class="pl-s"&gt;"age"&lt;/span&gt;, &lt;span class="pl-s"&gt;"weight"&lt;/span&gt;})

&lt;span class="pl-c"&gt;# Make age allow NULL and switch weight to being NOT NULL:&lt;/span&gt;
&lt;span class="pl-s1"&gt;table&lt;/span&gt;.&lt;span class="pl-en"&gt;transform&lt;/span&gt;(&lt;span class="pl-s1"&gt;not_null&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;{&lt;span class="pl-s"&gt;"age"&lt;/span&gt;: &lt;span class="pl-c1"&gt;False&lt;/span&gt;, &lt;span class="pl-s"&gt;"weight"&lt;/span&gt;: &lt;span class="pl-c1"&gt;True&lt;/span&gt;})

&lt;span class="pl-c"&gt;# Set default age to 1:&lt;/span&gt;
&lt;span class="pl-s1"&gt;table&lt;/span&gt;.&lt;span class="pl-en"&gt;transform&lt;/span&gt;(&lt;span class="pl-s1"&gt;defaults&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;{&lt;span class="pl-s"&gt;"age"&lt;/span&gt;: &lt;span class="pl-c1"&gt;1&lt;/span&gt;})

&lt;span class="pl-c"&gt;# Now remove the default from that column:&lt;/span&gt;
&lt;span class="pl-s1"&gt;table&lt;/span&gt;.&lt;span class="pl-en"&gt;transform&lt;/span&gt;(&lt;span class="pl-s1"&gt;defaults&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;{&lt;span class="pl-s"&gt;"age"&lt;/span&gt;: &lt;span class="pl-c1"&gt;None&lt;/span&gt;})&lt;/pre&gt;
&lt;p&gt;Each time the &lt;code&gt;table.transform(...)&lt;/code&gt; method runs it will create a brand new table, copy the data across and then drop the old table. You can combine multiple operations together in a single call, avoiding copying the table multiple times.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;table.transform_sql(...)&lt;/code&gt; method returns the SQL that would be executed instead of executing it directly, useful if you want to handle even more complex requirements.&lt;/p&gt;
&lt;h4 id="sqlite-utils-transform-cli"&gt;The "sqlite-utils transform" command-line tool&lt;/h4&gt;
&lt;p&gt;Almost every feature in &lt;code&gt;sqlite-utils&lt;/code&gt; is available in both the Python library and as a command-line utility, and &lt;code&gt;.transform()&lt;/code&gt; is no exception. The &lt;code&gt;sqlite-utils transform&lt;/code&gt; command can be used to apply complex table transformations directly from the command-line.&lt;/p&gt;
&lt;p&gt;Here's an example, starting with the &lt;a href="https://latest.datasette.io/fixtures"&gt;fixtures.db&lt;/a&gt; database that powers Datasette's unit tests:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ wget https://static.simonwillison.net/static/2020/fixtures.db
$ sqlite3 fixtures.db '.schema facetable'
CREATE TABLE facetable (
    pk integer primary key,
    created text,
    planet_int integer,
    on_earth integer,
    state text,
    city_id integer,
    neighborhood text,
    tags text,
    complex_array text,
    distinct_some_null,
    FOREIGN KEY ("city_id") REFERENCES [facet_cities](id)
);
$ sqlite-utils transform fixtures.db facetable \
  --type on_earth text \
  --drop complex_array \
  --drop state \
  --rename tags the_tags
$ sqlite3 fixtures.db '.schema facetable'       
CREATE TABLE IF NOT EXISTS "facetable" (
   [pk] INTEGER PRIMARY KEY,
   [created] TEXT,
   [planet_int] INTEGER,
   [on_earth] TEXT,
   [city_id] INTEGER REFERENCES [facet_cities]([id]),
   [neighborhood] TEXT,
   [the_tags] TEXT,
   [distinct_some_null] TEXT
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can use the &lt;code&gt;--sql&lt;/code&gt; option to see the SQL that would be executed without actually running it:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ wget https://latest.datasette.io/fixtures.db
$ sqlite-utils transform fixtures.db facetable \
  --type on_earth text \
  --drop complex_array \
  --drop state \
  --rename tags the_tags \
  --sql
CREATE TABLE [facetable_new_442f07e26eef] (
   [pk] INTEGER PRIMARY KEY,
   [created] TEXT,
   [planet_int] INTEGER,
   [on_earth] TEXT,
   [city_id] INTEGER REFERENCES [facet_cities]([id]),
   [neighborhood] TEXT,
   [the_tags] TEXT,
   [distinct_some_null] TEXT
);
INSERT INTO [facetable_new_442f07e26eef] ([pk], [created], [planet_int], [on_earth], [city_id], [neighborhood], [the_tags], [distinct_some_null])
   SELECT [pk], [created], [planet_int], [on_earth], [city_id], [neighborhood], [tags], [distinct_some_null] FROM [facetable];
DROP TABLE [facetable];
ALTER TABLE [facetable_new_442f07e26eef] RENAME TO [facetable];
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Plenty more tricks&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;sqlite-utils&lt;/code&gt; has plenty more tricks up its sleeve. I suggest spending some time browsing the &lt;a href="https://sqlite-utils.readthedocs.io/en/stable/python-api.html"&gt;Python library reference&lt;/a&gt; and the &lt;a href="https://sqlite-utils.readthedocs.io/en/stable/cli.html"&gt;sqlite-utils CLI documentation&lt;/a&gt;, or taking a look through through the &lt;a href="https://sqlite-utils.readthedocs.io/en/stable/changelog.html"&gt;release notes&lt;/a&gt;.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cli"&gt;cli&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;/p&gt;
    

</summary><category term="cli"/><category term="projects"/><category term="sqlite"/><category term="sqlite-utils"/></entry><entry><title>Fun with binary data and SQLite</title><link href="https://simonwillison.net/2020/Jul/30/fun-binary-data-and-sqlite/#atom-series" rel="alternate"/><published>2020-07-30T23:22:17+00:00</published><updated>2020-07-30T23:22:17+00:00</updated><id>https://simonwillison.net/2020/Jul/30/fun-binary-data-and-sqlite/#atom-series</id><summary type="html">
    &lt;p&gt;This week I've been mainly experimenting with binary data storage in SQLite. &lt;a href="https://sqlite-utils.datasette.io/"&gt;sqlite-utils&lt;/a&gt; can now insert data from binary files, and &lt;a href="https://datasette.io/plugins/datasette-media"&gt;datasette-media&lt;/a&gt; can serve content over HTTP that originated as binary BLOBs in a database file.&lt;/p&gt;

&lt;p&gt;Paul Ford piqued my interest in this when he tweeted about loading thousands of PDF documents into a SQLite database:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2020/paul-ford-slides.jpg" alt="Garish slides in a grid" style="max-width: 100%" /&gt;&lt;/p&gt;
&lt;p lang="en" dir="ltr"&gt;I made a shell script that loads thousands of PDF docs into SQLite databases. That means I can have a web server that produces infinite randomly chosen US Military PowerPoint slides that I can scroll on my phone when I&amp;#39;m around the house.&lt;/p&gt;&amp;#8212; Paul Ford (@ftrain) &lt;a href="https://twitter.com/ftrain/status/1287183861785473027?ref_src=twsrc%5Etfw"&gt;July 26, 2020&lt;/a&gt;&lt;/blockquote&gt;

&lt;p&gt;The SQLite documentation claims that serving smaller binary files from BLOB columns can be &lt;a href="https://www.sqlite.org/fasterthanfs.html"&gt;35% faster than the filesystem&lt;/a&gt;. I've done a little bit of work with binary files in SQLite - the &lt;a href="https://github.com/simonw/datasette-render-binary"&gt;datasette-render-binary&lt;/a&gt; and &lt;a href="https://github.com/simonw/datasette-render-images"&gt;datasette-render-images&lt;/a&gt; both help display BLOB data - but I'd never really dug into it in much detail.&lt;/p&gt;

&lt;h4&gt;sqlite-utils insert-files&lt;/h4&gt;

&lt;p&gt;The first step was to make it easier to build database files that include binary data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sqlite-utils.readthedocs.io/"&gt;sqlite-utils&lt;/a&gt; is my combination Python library and CLI tool for building SQLite databases. I've been steadily evolving it for a couple of years now, and it's the engine behind my &lt;a href="https://dogsheep.github.io/"&gt;Dogsheep&lt;/a&gt; collection of tools for personal analytics.&lt;/p&gt;

&lt;p&gt;The new &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#inserting-data-from-files"&gt;insert-files command&lt;/a&gt; can be used to insert content from binary files into a SQLite database, along with file metadata.&lt;/p&gt;

&lt;p&gt;The most basic usage looks like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sqlite-utils insert-files gifs.db images *.gif&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;By default, this creates a database table like so:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;CREATE TABLE [images] (
    [path] TEXT PRIMARY KEY,
    [content] BLOB,
    [size] INTEGER
);&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You can customize this table to include other file metadata using the &lt;code&gt;-c&lt;/code&gt; (short for &lt;code&gt;--column&lt;/code&gt;) option:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sqlite-utils insert-files gifs.db images *.gif \
    -c path -c md5 -c last_modified:mtime -c size --pk=path&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This creates a table with the following schema:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;CREATE TABLE [images] (
    [path] TEXT PRIMARY KEY,
    [md5] TEXT,
    [last_modified] FLOAT,
    [size] INTEGER
);&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;If you pass a directory instead of a file name the command will recursively add every file in that directory.&lt;/p&gt;

&lt;p&gt;I also improved &lt;code&gt;sqlite-utils&lt;/code&gt; with respect to outputting binary data. The new &lt;code&gt;--raw&lt;/code&gt; option dumps the binary contents of a column directly to standard out, so you can read an image back out of one of the above tables like so:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sqlite-utils photos.db \
    "select content from images where path=:path" \
    -p path 'myphoto.jpg' \
    --raw &amp;gt; myphoto.jpg&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This example also demonstrates the new support for &lt;code&gt;:parameters&lt;/code&gt; passed using the new &lt;code&gt;-p&lt;/code&gt; option, see &lt;a href="https://github.com/simonw/sqlite-utils/issues/124"&gt;#124&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sqlite-utils&lt;/code&gt; usually communicates using JSON, but JSON doesn't have the ability to represent binary values. Datasette outputs binary values &lt;a href="https://datasette-render-images-demo.datasette.io/favicons/favicons.json?_shape=array&amp;amp;_size=1"&gt;like so&lt;/a&gt;:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;"data": {
  "$base64": true,
  "encoded": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAY..."
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;I added support for the same format to &lt;code&gt;sqlite-utils&lt;/code&gt; - so you can now query binary columns and get out that nested object, or pipe JSON with that nested structure in to &lt;code&gt;sqlite-utils insert&lt;/code&gt; and have it stored as a binary BLOB in the database.&lt;/p&gt;

&lt;h4&gt;datasette-media&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://github.com/simonw/datasette-media"&gt;datasette-media&lt;/a&gt; is a plugin for serving binary content directly from Datasette on a special URL. I originally built it while working on &lt;a href="https://github.com/dogsheep/dogsheep-photos"&gt;Dogsheep Photos&lt;/a&gt; - given a SQLite file full of Apple Photos metadata I wanted to be able to serve thumbnails of the actual images via my Datasette web server.&lt;/p&gt;

&lt;p&gt;Those photos were still stored on disk - the plugin lets you configure a SQL query like this which will cause hits to &lt;code&gt;/-/media/photos/$UUID&lt;/code&gt; to serve that file from disk:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{
    "plugins": {
        "datasette-media": {
            "photo": {
                "sql": "select filepath from apple_photos where uuid=:key"
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;a href="https://github.com/simonw/datasette-media/issues/14"&gt;Issue #14&lt;/a&gt; added support for &lt;code&gt;BLOB&lt;/code&gt; columns as well. You can now configure the plugin like this to serve binary content that was stored in the database:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;{
    "plugins": {
        "datasette-media": {
            "thumb": {
                "sql": "select content from thumbnails where uuid=:key"
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This would serve content from a BLOB column in a &lt;code&gt;thumbnails&lt;/code&gt; table from the URL &lt;code&gt;/-/media/thumb/$UUID&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I really like this pattern of configuring plugins using SQL queries, where the returned column names have special meaning that is interpreted by the plugin. &lt;a href="https://github.com/simonw/datasette-atom"&gt;datasette-atom&lt;/a&gt; and &lt;a href="https://github.com/simonw/datasette-ics"&gt;datasette-ics&lt;/a&gt; use a similar trick.&lt;/p&gt;

&lt;p&gt;I expanded &lt;code&gt;datasette-media&lt;/code&gt; with a few other related features:&lt;/p&gt;

&lt;ul&gt;&lt;li&gt;Return a &lt;code&gt;content_url&lt;/code&gt; column and it will proxy content from that URL&lt;/li&gt;&lt;li&gt;Set &lt;code&gt;"enable_transform": true&lt;/code&gt; for a media bucket to enable &lt;code&gt;?w=&lt;/code&gt; and &lt;code&gt;?h=&lt;/code&gt; and &lt;code&gt;?format=&lt;/code&gt; parameters for transforming the image before it is served to the user&lt;/li&gt;&lt;li&gt;Return a &lt;code&gt;content_filename&lt;/code&gt; column to &lt;a href="https://github.com/simonw/datasette-media#setting-a-download-file-name"&gt;set a download file name&lt;/a&gt; (in a &lt;code&gt;content-disposition&lt;/code&gt; HTTP header) prompting the user's browser to download the file&lt;/li&gt;&lt;/ul&gt;

&lt;p&gt;See the &lt;a href="https://github.com/simonw/datasette-media#readme"&gt;README&lt;/a&gt; or &lt;a href="https://github.com/simonw/datasette-media/releases"&gt;release notes&lt;/a&gt; for more details.&lt;/p&gt;

&lt;h4&gt;Also this week&lt;/h4&gt;

&lt;p&gt;I renamed &lt;code&gt;datasette-insert-api&lt;/code&gt; to just &lt;a href="https://github.com/simonw/datasette-insert"&gt;datasette-insert&lt;/a&gt;, reflecting my plans to add non-API features to that plugin in the future.&lt;/p&gt;

&lt;p&gt;In doing so I had to figure out how to rename a PyPI package such that dependent projects would continue to work. I ended up building a &lt;a href="https://github.com/simonw/pypi-rename"&gt;pypi-rename cookiecutter template&lt;/a&gt; encoding what I learned.&lt;/p&gt;

&lt;p&gt;I enabled PostgreSQL full-text search for my blog's Django Admin interface, and wrote &lt;a href="https://github.com/simonw/til/blob/master/django/postgresql-full-text-search-admin.md"&gt;a TIL on how I did it&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I &lt;a href="https://github.com/simonw/db-to-sqlite/issues/26"&gt;added compound primary key support&lt;/a&gt; to &lt;code&gt;db-to-sqlite&lt;/code&gt;, so now it can convert PostgreSQL or MySQL databases to SQLite if they use compound primary keys.&lt;/p&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/master/javascript/copy-button.md"&gt;Implementing a "copy to clipboard" button&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="https://github.com/simonw/til/blob/master/django/postgresql-full-text-search-admin.md"&gt;PostgreSQL full-text search in the Django Admin&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href="https://github.com/simonw/til/blob/master/sqlite/blob-literals.md"&gt;SQLite BLOB literals&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;a href="https://github.com/simonw/sqlite-utils/releases/tag/2.13"&gt;sqlite-utils 2.13&lt;/a&gt; - 2020-07-30&lt;/li&gt;&lt;li&gt;&lt;a href="https://github.com/simonw/datasette-media/releases/tag/0.5"&gt;datasette-media 0.5&lt;/a&gt; - 2020-07-29&lt;/li&gt;&lt;li&gt;&lt;a href="https://github.com/simonw/db-to-sqlite/releases/tag/1.3"&gt;db-to-sqlite 1.3&lt;/a&gt; - 2020-07-27&lt;/li&gt;&lt;li&gt;&lt;a href="https://github.com/simonw/datasette-media/releases/tag/0.4"&gt;datasette-media 0.4&lt;/a&gt; - 2020-07-27&lt;/li&gt;&lt;li&gt;&lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/2.12"&gt;sqlite-utils 2.12&lt;/a&gt; - 2020-07-27&lt;/li&gt;&lt;li&gt;&lt;a href="https://github.com/simonw/datasette-render-images/releases/tag/0.3.1"&gt;datasette-render-images 0.3.1&lt;/a&gt; - 2020-07-27&lt;/li&gt;&lt;li&gt;&lt;a href="https://github.com/simonw/datasette-render-images/releases/tag/0.3"&gt;datasette-render-images 0.3&lt;/a&gt; - 2020-07-27&lt;/li&gt;&lt;li&gt;&lt;a href="https://github.com/simonw/datasette-auth-passwords/releases/tag/0.3.1"&gt;datasette-auth-passwords 0.3.1&lt;/a&gt; - 2020-07-26&lt;/li&gt;&lt;li&gt;&lt;a href="https://github.com/simonw/datasette-insert/releases/tag/0.5"&gt;datasette-insert 0.5&lt;/a&gt; - 2020-07-25&lt;/li&gt;&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/paul-ford"&gt;paul-ford&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&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/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="paul-ford"/><category term="plugins"/><category term="projects"/><category term="sqlite"/><category term="datasette"/><category term="weeknotes"/><category term="sqlite-utils"/></entry><entry><title>sqlite-utils: a Python library and CLI tool for building SQLite databases</title><link href="https://simonwillison.net/2019/Feb/25/sqlite-utils/#atom-series" rel="alternate"/><published>2019-02-25T03:29:20+00:00</published><updated>2019-02-25T03:29:20+00:00</updated><id>https://simonwillison.net/2019/Feb/25/sqlite-utils/#atom-series</id><summary type="html">
    &lt;p&gt;&lt;a href="https://github.com/simonw/sqlite-utils"&gt;sqlite-utils&lt;/a&gt; is a combination Python library and command-line tool I’ve been building over the past six months which aims to make creating new SQLite databases as quick and easy as possible.&lt;/p&gt;
&lt;p&gt;It’s part of &lt;a href="https://datasette.readthedocs.io/en/stable/ecosystem.html"&gt;the ecosystem of tools&lt;/a&gt; I’m building around my &lt;a href="https://datasette.readthedocs.io/"&gt;Datasette&lt;/a&gt; project.&lt;/p&gt;
&lt;p&gt;I spent the weekend adding all kinds of exciting command-line options to it, so I’m ready to describe it to the world.&lt;/p&gt;
&lt;h3&gt;&lt;a id="A_Python_library_for_quickly_creating_databases_8"&gt;&lt;/a&gt;A Python library for quickly creating databases&lt;/h3&gt;
&lt;p&gt;A core idea behind Datasette is that &lt;a href="https://www.sqlite.org/"&gt;SQLite&lt;/a&gt; is the ideal format for publishing all kinds of interesting structured data. Datasette takes any SQLite database and adds a browsable web interface, &lt;a href="https://datasette.readthedocs.io/en/stable/json_api.html"&gt;a JSON API&lt;/a&gt; and the ability to &lt;a href="https://datasette.readthedocs.io/en/stable/csv_export.html"&gt;export tables and queries as CSV&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The other half of the equation then is tools to create SQLite databases. &lt;a href="https://github.com/simonw/csvs-to-sqlite"&gt;csvs-to-sqlite&lt;/a&gt; was my first CLI attempt at this. &lt;code&gt;sqlite-utils&lt;/code&gt; takes a much more flexible and comprehensive approach.&lt;/p&gt;
&lt;p&gt;I started working on &lt;code&gt;sqlite-utils&lt;/code&gt; last year as part of my project to &lt;a href="https://simonwillison.net/2018/Aug/6/russian-facebook-ads/"&gt;Analyze US Election Russian Facebook Ads&lt;/a&gt;. The initial aim was to build a library that made constructing new SQLite databases inside of a &lt;a href="https://jupyter.org/"&gt;Jupyter notebook&lt;/a&gt; as productive as possible.&lt;/p&gt;
&lt;p&gt;The core idea behind the library is that you can give it a list of Python dictionaries (equivalent to JSON objects) and it will automatically create a SQLite table with the correct schema, then insert those items into the new table.&lt;/p&gt;
&lt;p&gt;To illustrate, let’s create a database using &lt;a href="https://data.nasa.gov/resource/y77d-th95.json"&gt;this JSON file of meteorite landings&lt;/a&gt; released by NASA (discovered via &lt;a href="https://github.com/jdorfman/awesome-json-datasets"&gt;awesome-json-datasets&lt;/a&gt; curated by Justin Dorfman).&lt;/p&gt;
&lt;p&gt;Here’s the quickest way in code to turn that into a database:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import requests
import sqlite_utils

db = sqlite_utils.Database(&amp;quot;meteorites.db&amp;quot;)
db[&amp;quot;meteorites&amp;quot;].insert_all(
    requests.get(
        &amp;quot;https://data.nasa.gov/resource/y77d-th95.json&amp;quot;
    ).json(),
    pk=&amp;quot;id&amp;quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This two lines of code creates a new SQLite database on disk called &lt;code&gt;meteorites.db&lt;/code&gt;, creates a table in that file called &lt;code&gt;meteorites&lt;/code&gt;, detects the necessary columns based on the incoming data, inserts all of the rows and sets the &lt;code&gt;id&lt;/code&gt; column up as the primary key.&lt;/p&gt;
&lt;p&gt;To see the resulting database, run &lt;code&gt;datasette meteorites.db&lt;/code&gt; and browse to &lt;code&gt;http://127.0.0.1:8001/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You can do a &lt;em&gt;lot more&lt;/em&gt; with the library. You can create tables, insert and upsert data in bulk, configure foreign key relationships, configure SQLite full-text search and much more. I encourage you to &lt;a href="https://sqlite-utils.readthedocs.io/en/latest/python-api.html"&gt;consult the documentation&lt;/a&gt; for all of the details.&lt;/p&gt;
&lt;h3&gt;&lt;a id="The_sqliteutils_commandline_tool_39"&gt;&lt;/a&gt;The sqlite-utils command-line tool&lt;/h3&gt;
&lt;p&gt;This is the new stuff built over the past few days, and I think it’s really fun.&lt;/p&gt;
&lt;p&gt;First install the tool &lt;a href="https://pypi.org/project/sqlite-utils/"&gt;from PyPI&lt;/a&gt;, using &lt;code&gt;pip3 install sqlite-utils&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Let’s start by grabbing a copy of &lt;a href="https://static.simonwillison.net/static/2019/russian-ads.db"&gt;the russian-ads.db database&lt;/a&gt; I created in &lt;a href="https://simonwillison.net/2018/Aug/6/russian-facebook-ads/"&gt;Analyzing US Election Russian Facebook Ads&lt;/a&gt; (4MB):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ cd /tmp
$ wget https://static.simonwillison.net/static/2019/russian-ads.db
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can see a list of tables in the database and their counts using the &lt;code&gt;tables&lt;/code&gt; command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sqlite-utils tables russian-ads.db --counts
[{&amp;quot;table&amp;quot;: &amp;quot;ads&amp;quot;, &amp;quot;count&amp;quot;: 3498},
 {&amp;quot;table&amp;quot;: &amp;quot;targets&amp;quot;, &amp;quot;count&amp;quot;: 1665},
 {&amp;quot;table&amp;quot;: &amp;quot;ad_targets&amp;quot;, &amp;quot;count&amp;quot;: 36559},
 {&amp;quot;table&amp;quot;: &amp;quot;ads_fts&amp;quot;, &amp;quot;count&amp;quot;: 3498},
 {&amp;quot;table&amp;quot;: &amp;quot;ads_fts_segments&amp;quot;, &amp;quot;count&amp;quot;: 120},
 {&amp;quot;table&amp;quot;: &amp;quot;ads_fts_segdir&amp;quot;, &amp;quot;count&amp;quot;: 1},
 {&amp;quot;table&amp;quot;: &amp;quot;ads_fts_docsize&amp;quot;, &amp;quot;count&amp;quot;: 3498},
 {&amp;quot;table&amp;quot;: &amp;quot;ads_fts_stat&amp;quot;, &amp;quot;count&amp;quot;: 1}]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default, &lt;code&gt;sqlite-utils&lt;/code&gt; outputs data as neatly formatted JSON. You can get CSV instead using the &lt;code&gt;--csv&lt;/code&gt; option:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sqlite-utils tables russian-ads.db --counts --csv
table,count
ads,3498
targets,1665
ad_targets,36559
ads_fts,3498
ads_fts_segments,120
ads_fts_segdir,1
ads_fts_docsize,3498
ads_fts_stat,1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or if you want a pretty ASCII-art table, use &lt;code&gt;--table&lt;/code&gt; (or the shortcut, &lt;code&gt;-t&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sqlite-utils tables russian-ads.db --counts -t
table               count
----------------  -------
ads                  3498
targets              1665
ad_targets          36559
ads_fts              3498
ads_fts_segments      120
ads_fts_segdir          1
ads_fts_docsize      3498
ads_fts_stat            1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The table view is built on top of &lt;a href="https://pypi.org/project/tabulate/"&gt;tabulate&lt;/a&gt;, which offers dozens of table variations. Run &lt;code&gt;sqlite-utils tables --help&lt;/code&gt; for the full list - try &lt;code&gt;--table -fmt=rst&lt;/code&gt; for output that can be pasted directly into a reStructuredText document (handy for writing documentation).&lt;/p&gt;
&lt;p&gt;So far we’ve just looked at a list of tables. Lets run a SQL query:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sqlite-utils russian-ads.db &amp;quot;select category, count(*) from targets group by category&amp;quot;
[{&amp;quot;category&amp;quot;: &amp;quot;accessing_facebook_on&amp;quot;, &amp;quot;count(*)&amp;quot;: 1},
 {&amp;quot;category&amp;quot;: &amp;quot;age&amp;quot;, &amp;quot;count(*)&amp;quot;: 82},
 {&amp;quot;category&amp;quot;: &amp;quot;and_must_also_match&amp;quot;, &amp;quot;count(*)&amp;quot;: 228},
 {&amp;quot;category&amp;quot;: &amp;quot;army_reserve_industry&amp;quot;, &amp;quot;count(*)&amp;quot;: 3},
 {&amp;quot;category&amp;quot;: &amp;quot;behaviors&amp;quot;, &amp;quot;count(*)&amp;quot;: 16},
 ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Again, this can be output as CSV using &lt;code&gt;--csv&lt;/code&gt;, or a table with &lt;code&gt;--table&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The default JSON output is objects wrapped in an array. Use &lt;code&gt;--arrays&lt;/code&gt; to get an array of arrays instead. More interestingly: &lt;code&gt;--nl&lt;/code&gt; causes the data to be output as &lt;a href="http://ndjson.org/"&gt;newline-delimited JSON&lt;/a&gt;, like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sqlite-utils russian-ads.db &amp;quot;select category, count(*) from targets group by category&amp;quot; --nl
{&amp;quot;category&amp;quot;: &amp;quot;accessing_facebook_on&amp;quot;, &amp;quot;count(*)&amp;quot;: 1}
{&amp;quot;category&amp;quot;: &amp;quot;age&amp;quot;, &amp;quot;count(*)&amp;quot;: 82}
{&amp;quot;category&amp;quot;: &amp;quot;and_must_also_match&amp;quot;, &amp;quot;count(*)&amp;quot;: 228}
{&amp;quot;category&amp;quot;: &amp;quot;army_reserve_industry&amp;quot;, &amp;quot;count(*)&amp;quot;: 3}
{&amp;quot;category&amp;quot;: &amp;quot;behaviors&amp;quot;, &amp;quot;count(*)&amp;quot;: 16}
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a really interesting format for piping to other tools.&lt;/p&gt;
&lt;h3&gt;&lt;a id="Creating_databases_from_JSON_on_the_commandline_115"&gt;&lt;/a&gt;Creating databases from JSON on the command-line&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;sqlite-utils insert&lt;/code&gt; command can be used to create new tables by piping JSON or CSV directly into the tool. It’s the command-line equivalent of the &lt;code&gt;.insert_all()&lt;/code&gt; Python function I demonstrated earlier.&lt;/p&gt;
&lt;p&gt;Here’s how to create that meteorite database directly from the command-line:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ curl &amp;quot;https://data.nasa.gov/resource/y77d-th95.json&amp;quot; | \
    sqlite-utils insert meteorites.db meteorites - --pk=id
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will use a SQLite database file called &lt;code&gt;meteorites.db&lt;/code&gt; (creating one if it does not yet exist), create or use a table called &lt;code&gt;meteorites&lt;/code&gt; and read the data from standard in (hence the pipe). You can pass a filename instead of a &lt;code&gt;-&lt;/code&gt; here to read data from a file on disk.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;insert&lt;/code&gt; command accepts multiple formats - it defaults to expecting a JSON array of objects, but you can use &lt;code&gt;--nl&lt;/code&gt; to accept newline-delimited JSON and &lt;code&gt;--csv&lt;/code&gt; to accept CSV.&lt;/p&gt;
&lt;p&gt;This means you can combine the tools! Let’s create a brand new database by exporting data from the old one, using newline-delimited JSON as the intermediary format:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ sqlite-utils russian-ads.db \
    &amp;quot;select * from ads where text like '%veterans%'&amp;quot; --nl | \
    sqlite-utils insert veterans.db ads - --nl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates a new file called &lt;code&gt;veterans.db&lt;/code&gt; containing an &lt;code&gt;ads&lt;/code&gt; table with just the ads that mentioned veterans somewhere in their body text.&lt;/p&gt;
&lt;p&gt;Since we’re working with JSON, we can introduce other command-line tools into the mix.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://stedolan.github.io/jq/"&gt;jq&lt;/a&gt; is a neat little tool for extracting data from a JSON file using its own mini domain-specific language.&lt;/p&gt;
&lt;p&gt;The Nobel Prize API offers &lt;a href="https://api.nobelprize.org/v1/laureate.json"&gt;a JSON file&lt;/a&gt; listing all of the Nobel laureates - but they are contained as an array in a top level &lt;code&gt;&amp;quot;laureates&amp;quot;&lt;/code&gt; key. &lt;code&gt;sqlite-utils&lt;/code&gt; needs a flat array - so we can use &lt;code&gt;jq&lt;/code&gt; to get exactly that:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ curl &amp;quot;https://api.nobelprize.org/v1/laureate.json&amp;quot; | \
  jq &amp;quot;.laureates&amp;quot; | \
  sqlite-utils insert nobel.db laureates -
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we have a file called &lt;code&gt;nobel.db&lt;/code&gt; containing all of the Nobel laureates.&lt;/p&gt;
&lt;p&gt;Since Datasette recently &lt;a href="https://datasette.readthedocs.io/en/stable/changelog.html#v0-27"&gt;grew the ability to export newline-delimited JSON&lt;/a&gt;, we can also use this ability to directly consume data from Datasette. Lets grab &lt;a href="https://fivethirtyeight.datasettes.com/fivethirtyeight-aa93d24/bob-ross%2Felements-by-episode?BEACH=1"&gt;every episode of the Joy of Painting in which Bob Ross painted a beach&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ curl &amp;quot;https://fivethirtyeight.datasettes.com/fivethirtyeight-aa93d24/bob-ross%2Felements-by-episode.json?_facet=BEACH&amp;amp;BEACH=1&amp;amp;_shape=array&amp;amp;_nl=on&amp;quot; \
| sqlite-utils insert bob.db beach_episodes - --nl
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a id="Plenty_more_features_153"&gt;&lt;/a&gt;Plenty more features&lt;/h3&gt;
&lt;p&gt;As with the Python API, the &lt;code&gt;sqlite-utils&lt;/code&gt; CLI tool has dozens of other options and &lt;a href="https://sqlite-utils.readthedocs.io/en/latest/cli.html"&gt;extensive documentation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I’ve been really enjoying growing an &lt;a href="https://datasette.readthedocs.io/en/stable/ecosystem.html"&gt;ecosystem of tools around Datasette&lt;/a&gt;. &lt;code&gt;sqlite-utils&lt;/code&gt; is the keystone here: it’s fundamental to other tools I’m building, such as &lt;a href="https://github.com/simonw/db-to-sqlite"&gt;db-to-sqlite&lt;/a&gt; (which can export any SQLAlchemy-supported database directly to a SQLite file on disk).&lt;/p&gt;
&lt;p&gt;I’ve found myself increasingly turning to SQLite first for all kinds of ad-hoc analysis, and I’m excited to try out these new command-line abilities of &lt;code&gt;sqlite-utils&lt;/code&gt; for real-world data spelunking tasks.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/cli"&gt;cli&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;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="cli"/><category term="open-source"/><category term="projects"/><category term="python"/><category term="sqlite"/><category term="datasette"/><category term="sqlite-utils"/></entry></feed>