<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: sqlite-utils</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/sqlite-utils.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2026-07-12T20:55:30+00:00</updated><author><name>Simon Willison</name></author><entry><title>sqlite-utils 4.1.1</title><link href="https://simonwillison.net/2026/Jul/12/sqlite-utils/#atom-tag" rel="alternate"/><published>2026-07-12T20:55:30+00:00</published><updated>2026-07-12T20:55:30+00:00</updated><id>https://simonwillison.net/2026/Jul/12/sqlite-utils/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/4.1.1"&gt;sqlite-utils 4.1.1&lt;/a&gt;&lt;/p&gt;
        &lt;p&gt;Mainly a fix for an edge case that regular Claude chat spotted while &lt;a href="https://claude.ai/share/564b187d-d126-47ea-9b59-07c16ade0b70"&gt;experimenting with the 4.1 release&lt;/a&gt; to answer a question about ON DELETE.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;table.transform()&lt;/code&gt; now raises a &lt;code&gt;TransactionError&lt;/code&gt; if called while a transaction is open with &lt;code&gt;PRAGMA foreign_keys&lt;/code&gt; enabled and the table is referenced by foreign keys with destructive &lt;code&gt;ON DELETE&lt;/code&gt; actions - &lt;code&gt;CASCADE&lt;/code&gt;, &lt;code&gt;SET NULL&lt;/code&gt; or &lt;code&gt;SET DEFAULT&lt;/code&gt;. The pragma cannot be changed inside a transaction, so previously dropping the old table as part of the transform could fire those actions and silently delete or modify referencing rows. See &lt;a href="https://sqlite-utils.datasette.io/en/stable/python-api.html#python-api-transform-foreign-keys-transactions"&gt;Foreign keys and transactions&lt;/a&gt; for details and workarounds. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/794"&gt;#794&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html"&gt;CLI&lt;/a&gt; and &lt;a href="https://sqlite-utils.datasette.io/en/stable/python-api.html"&gt;Python API&lt;/a&gt; documentation now cross-reference each other: CLI sections link to the equivalent Python API functionality and Python API sections link back to the corresponding CLI command. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/791"&gt;#791&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
    
    
        &lt;p&gt;Tags: &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="sqlite"/><category term="sqlite-utils"/></entry><entry><title>sqlite-utils 4.1</title><link href="https://simonwillison.net/2026/Jul/11/sqlite-utils/#atom-tag" rel="alternate"/><published>2026-07-11T23:50:20+00:00</published><updated>2026-07-11T23:50:20+00:00</updated><id>https://simonwillison.net/2026/Jul/11/sqlite-utils/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/4.1"&gt;sqlite-utils 4.1&lt;/a&gt;&lt;/p&gt;
        &lt;p&gt;The first dot-release since &lt;a href="https://simonwillison.net/2026/Jul/7/sqlite-utils-4/"&gt;4.0 a few days ago&lt;/a&gt;, introducing a number of minor new features.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sqlite-utils insert&lt;/code&gt; and &lt;code&gt;sqlite-utils upsert&lt;/code&gt; now accept a &lt;code&gt;--code&lt;/code&gt; option for &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#cli-insert-code"&gt;providing a block of Python code&lt;/a&gt; (or a path to a &lt;code&gt;.py&lt;/code&gt; file) that defines a &lt;code&gt;rows()&lt;/code&gt; function or &lt;code&gt;rows&lt;/code&gt; iterable of rows to insert, as an alternative to importing from a file. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/684"&gt;#684&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;sqlite-utils&lt;/code&gt; already had features that allow you to pass blocks of Python code as CLI arguments, for example &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#converting-data-in-columns"&gt;this one&lt;/a&gt; for the &lt;code&gt;sqlite-utils convert&lt;/code&gt; command:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-s1"&gt;sqlite&lt;/span&gt;&lt;span class="pl-c1"&gt;-&lt;/span&gt;&lt;span class="pl-s1"&gt;utils&lt;/span&gt; &lt;span class="pl-s1"&gt;convert&lt;/span&gt; &lt;span class="pl-s1"&gt;content&lt;/span&gt;.&lt;span class="pl-c1"&gt;db&lt;/span&gt; &lt;span class="pl-s1"&gt;articles&lt;/span&gt; &lt;span class="pl-s1"&gt;headline&lt;/span&gt; '
&lt;span class="pl-s1"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;convert&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-s1"&gt;value&lt;/span&gt;.&lt;span class="pl-c1"&gt;upper&lt;/span&gt;()'&lt;/pre&gt;
&lt;p&gt;Allowing blocks of code to &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#inserting-rows-generated-by-python-code"&gt;generate new rows directly&lt;/a&gt; was on obvious extension of that pattern:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-s1"&gt;sqlite&lt;/span&gt;&lt;span class="pl-c1"&gt;-&lt;/span&gt;&lt;span class="pl-s1"&gt;utils&lt;/span&gt; &lt;span class="pl-s1"&gt;insert&lt;/span&gt; &lt;span class="pl-s1"&gt;data&lt;/span&gt;.&lt;span class="pl-c1"&gt;db&lt;/span&gt; &lt;span class="pl-s1"&gt;creatures&lt;/span&gt; &lt;span class="pl-c1"&gt;-&lt;/span&gt;&lt;span class="pl-c1"&gt;-&lt;/span&gt;&lt;span class="pl-s1"&gt;code&lt;/span&gt; '
&lt;span class="pl-s1"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;rows&lt;/span&gt;():
    &lt;span class="pl-k"&gt;yield&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-k"&gt;yield&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;"Suna"&lt;/span&gt;}
' &lt;span class="pl-c1"&gt;-&lt;/span&gt;&lt;span class="pl-c1"&gt;-&lt;/span&gt;&lt;span class="pl-s1"&gt;pk&lt;/span&gt; &lt;span class="pl-s1"&gt;id&lt;/span&gt;&lt;/pre&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sqlite-utils insert&lt;/code&gt; and &lt;code&gt;sqlite-utils upsert&lt;/code&gt; now accept &lt;code&gt;--type column-name type&lt;/code&gt; to &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#cli-insert-csv-tsv-column-types"&gt;override the type automatically chosen when the table is created&lt;/a&gt;. This is useful for CSV or TSV columns such as ZIP codes that look like integers but should be stored as &lt;code&gt;TEXT&lt;/code&gt; to preserve leading zeros. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/131"&gt;#131&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;A long-standing feature request which turned out to be a &lt;a href="https://github.com/SAY-5/sqlite-utils/commit/d2ac3765ed9f0516bb0cbc2508a5c3907fb6a71a"&gt;simple implementation&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;New &lt;code&gt;table.drop_index(name)&lt;/code&gt; method and &lt;code&gt;sqlite-utils drop-index&lt;/code&gt; command for dropping an index by name. Both accept &lt;code&gt;ignore=True&lt;/code&gt;/&lt;code&gt;--ignore&lt;/code&gt; to ignore a missing index. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/626"&gt;#626&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sqlite-utils query&lt;/code&gt; can now read the SQL query from standard input by passing &lt;code&gt;-&lt;/code&gt; in place of the query, for example &lt;code&gt;echo "select * from dogs" | sqlite-utils query dogs.db -&lt;/code&gt;. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/765"&gt;#765&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Two more small features. I had Codex review all open issues and highlight the easiest ones!&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sqlite-utils upsert&lt;/code&gt; can now infer the primary key of an existing table, so &lt;code&gt;--pk&lt;/code&gt; can be omitted when upserting into a table that already has a primary key.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Another Codex suggestion, an obvious missing CLI feature from a Python library improvement that shipped in the 4.0 release.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;table.transform()&lt;/code&gt; and &lt;code&gt;table.transform_sql()&lt;/code&gt; now accept &lt;code&gt;strict=True&lt;/code&gt; or &lt;code&gt;strict=False&lt;/code&gt; to change a table’s &lt;a href="https://www.sqlite.org/stricttables.html"&gt;SQLite strict mode&lt;/a&gt;. Omitting the option preserves the existing mode. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/787"&gt;#787&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;sqlite-utils transform&lt;/code&gt; command now accepts &lt;code&gt;--strict&lt;/code&gt; and &lt;code&gt;--no-strict&lt;/code&gt; to change a table’s strict mode. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/787"&gt;#787&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;These two were inspired by &lt;a href="https://evanhahn.com/prefer-strict-tables-in-sqlite/"&gt;Prefer STRICT tables in SQLite&lt;/a&gt; by Evan Hahn, which did the rounds &lt;a href="https://news.ycombinator.com/item?id=48873940"&gt;on Hacker News&lt;/a&gt; today. Evan pointed out that:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Unfortunately, I don’t think there’s a way to ALTER a table to make it strict. I think you have to copy the data out of the non-strict table into the strict one.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That's exactly what the &lt;a href="https://sqlite-utils.datasette.io/en/stable/python-api.html#transforming-a-table"&gt;sqlite-utils transform mechanism&lt;/a&gt; does, so I extended it to add the ability to switch tables from strict to non-strict and vice-versa.&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/ab8256b81646ad967a601975e206de64"&gt;the GPT-5.6 Sol xhigh Codex transcript&lt;/a&gt; I used to implement those new strict table features. One of the most useful prompts I ran was this one:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;use uv run python -c and manually exercise the new .transform(strict=) option, see if you can find any edge-cases or bugs&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Effectively telling the model to manually test its work, outside of the automated tests it had already written. This turned up two minor issues that we then fixed.&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/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/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;/p&gt;
    

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

</summary><category term="schema-migrations"/><category term="projects"/><category term="sqlite"/><category term="ai"/><category term="sqlite-utils"/><category term="annotated-release-notes"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="anthropic"/><category term="claude"/><category term="agentic-engineering"/><category term="claude-mythos-fable"/></entry><entry><title>sqlite-migrate 0.2</title><link href="https://simonwillison.net/2026/Jul/7/sqlite-migrate/#atom-tag" rel="alternate"/><published>2026-07-07T16:33:55+00:00</published><updated>2026-07-07T16:33:55+00:00</updated><id>https://simonwillison.net/2026/Jul/7/sqlite-migrate/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/sqlite-migrate/releases/tag/0.2"&gt;sqlite-migrate 0.2&lt;/a&gt;&lt;/p&gt;
        &lt;p&gt;The version that retires the library, instead implementing a compatibility shim against the new sqlite-utils 4.0 dependency.&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="sqlite-utils"/></entry><entry><title>sqlite-utils 4.0</title><link href="https://simonwillison.net/2026/Jul/7/sqlite-utils/#atom-tag" rel="alternate"/><published>2026-07-07T15:42:43+00:00</published><updated>2026-07-07T15:42:43+00:00</updated><id>https://simonwillison.net/2026/Jul/7/sqlite-utils/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/4.0"&gt;sqlite-utils 4.0&lt;/a&gt;&lt;/p&gt;
        &lt;p&gt;See &lt;a href="https://simonwillison.net/2026/Jul/7/sqlite-utils-4/"&gt;sqlite-utils 4.0, now with database schema migrations&lt;/a&gt; for details.&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;&lt;/p&gt;
    

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

</summary><category term="schema-migrations"/><category term="projects"/><category term="sqlite"/><category term="sqlite-utils"/><category term="annotated-release-notes"/></entry><entry><title>sqlite-utils 4.0rc1</title><link href="https://simonwillison.net/2026/Jun/21/sqlite-utils/#atom-tag" rel="alternate"/><published>2026-06-21T23:30:04+00:00</published><updated>2026-06-21T23:30:04+00:00</updated><id>https://simonwillison.net/2026/Jun/21/sqlite-utils/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/4.0rc1"&gt;sqlite-utils 4.0rc1&lt;/a&gt;&lt;/p&gt;
        &lt;p&gt;See &lt;a href="https://simonwillison.net/2026/Jun/21/sqlite-utils-40rc1/"&gt;sqlite-utils 4.0rc1 adds migrations and nested transactions&lt;/a&gt;.&lt;/p&gt;
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="sqlite-utils"/></entry><entry><title>Under the hood of Canada Spends with Brendan Samek</title><link href="https://simonwillison.net/2025/Dec/9/canada-spends/#atom-tag" rel="alternate"/><published>2025-12-09T23:52:05+00:00</published><updated>2025-12-09T23:52:05+00:00</updated><id>https://simonwillison.net/2025/Dec/9/canada-spends/#atom-tag</id><summary type="html">
    &lt;p&gt;I talked to Brendan Samek about &lt;a href="https://canadaspends.com/"&gt;Canada Spends&lt;/a&gt;, a project from &lt;a href="https://www.buildcanada.com/"&gt;Build Canada&lt;/a&gt; that makes Canadian government financial data accessible and explorable using a combination of Datasette, a neat custom frontend, Ruby ingestion scripts, &lt;a href="https://sqlite-utils.datasette.io/"&gt;sqlite-utils&lt;/a&gt; and pieces of LLM-powered PDF extraction.&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://www.youtube.com/watch?v=T8xiMgmb8po"&gt;the video on YouTube&lt;/a&gt;.&lt;/p&gt;
&lt;iframe style="margin-bottom: 1.5em;" width="560" height="315" src="https://www.youtube-nocookie.com/embed/T8xiMgmb8po" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="allowfullscreen"&gt; &lt;/iframe&gt;

&lt;p&gt;Sections within that video:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.youtube.com/watch?v=T8xiMgmb8po&amp;amp;t=177s"&gt;02:57&lt;/a&gt; Data sources and the PDF problem&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.youtube.com/watch?v=T8xiMgmb8po&amp;amp;t=351s"&gt;05:51&lt;/a&gt; Crowdsourcing financial data across Canada&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.youtube.com/watch?v=T8xiMgmb8po&amp;amp;t=447s"&gt;07:27&lt;/a&gt; Datasette demo: Search and facets&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.youtube.com/watch?v=T8xiMgmb8po&amp;amp;t=753s"&gt;12:33&lt;/a&gt; Behind the scenes: Ingestion code&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.youtube.com/watch?v=T8xiMgmb8po&amp;amp;t=1044s"&gt;17:24&lt;/a&gt; Data quality horror stories&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.youtube.com/watch?v=T8xiMgmb8po&amp;amp;t=1246s"&gt;20:46&lt;/a&gt; Using Gemini to extract PDF data&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.youtube.com/watch?v=T8xiMgmb8po&amp;amp;t=1524s"&gt;25:24&lt;/a&gt; Why SQLite is perfect for data distribution&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="build-canada-and-canada-spends"&gt;Build Canada and Canada Spends&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://www.buildcanada.com/"&gt;Build Canada&lt;/a&gt; is a volunteer-driven non-profit that launched in February 2025 - here's &lt;a href="https://www.canadianaffairs.news/2025/09/26/builders-at-the-gate-inside-the-civic-movement-to-jolt-canada-out-of-stagnation/"&gt;some background information&lt;/a&gt; on the organization, which has a strong pro-entrepreneurship and pro-technology angle.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://canadaspends.com/"&gt;Canada Spends&lt;/a&gt; is their project to make Canadian government financial data more accessible and explorable. It includes a tax sources and sinks visualizer and a searchable database of government contracts, plus a collection of tools covering financial data from different levels of government.&lt;/p&gt;
&lt;h4 id="datasette-for-data-exploration"&gt;Datasette for data exploration&lt;/h4&gt;
&lt;p&gt;The project maintains a Datasette instance at &lt;a href="https://api.canadasbuilding.com/"&gt;api.canadasbilding.com&lt;/a&gt; containing the data they have gathered and processed from multiple data sources - currently more than 2 million rows plus a combined search index across a denormalized copy of that data.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/api-canadasbuilding-com-canada-spends.jpg" alt="  Datasette UI for a canada-spends database.  aggregated-contracts-under-10k:  year, contract_goods_number_of, contracts_goods_original_value, contracts_goods_amendment_value, contract_service_number_of, contracts_service_original_value, contracts_service_amendment_value, contract_construction_number_of, contracts_construction_original_value, contracts_construction_amendment_value, acquisition_card_transactions_number_of, acquisition_card_transactions_total_value, owner_org, owner_org_title  487 rows cihr_grants  external_id, title, project_lead_name, co_researchers, institution, province, country, competition_year, award_amount, program, program_type, theme, research_subject, keywords, abstract, duration, source_url  53,420 rows contracts-over-10k:   reference_number, procurement_id, vendor_name, vendor_postal_code, buyer_name, contract_date, economic_object_code, description_en, description_fr, contract_period_start, delivery_date, contract_value, original_value, amendment_value, comments_en, comments_fr, additional_comments_en, additional_comments_fr, agreement_type_code, trade_agreement, land_claims, commodity_type, commodity_code, country_of_vendor, solicitation_procedure, limited_tendering_reason, trade_agreement_exceptions, indigenous_business, indigenous_business_excluding_psib, intellectual_property, potential_commercial_exploitation, former_public_servant, contracting_entity, standing_offer_number, instrument_type, ministers_office, number_of_bids, article_6_exceptions, award_criteria, socioeconomic_indicator, reporting_period, owner_org, owner_org_title  1,172,575 rows global_affairs_grants:   id, projectNumber, dateModified, title, description, status, start, end, countries, executingAgencyPartner, DACSectors, maximumContribution, ContributingOrganization, expectedResults, resultsAchieved, aidType, collaborationType, financeType, flowType, reportingOrganisation, programName, selectionMechanism, policyMarkers, regions, alternameImPositions, budgets, Locations, otherIdentifiers, participatingOrgs, programDataStructure, relatedActivities, transactions  2,378 rows nserc_grants:   title, award_summary, application_id, competition_year, fiscal_year, project_lead_name, institution, department, province, award_amount, installment, program, selection_committee, research_subject, area_of_application, co-researchers, partners, external_id, source_url  701,310 rows sshrc_grants:   id, title, program, fiscal_year, competition_year, applicant, organization, amount, discipline, area_of_research, co_applicant, keywords, source_url  213,085 rows transfers:   FSCL_YR, MINC, MINE, MINF, DepartmentNumber-Numéro-de-Ministère, DEPT_EN_DESC, DEPT_FR_DESC, RCPNT_CLS_EN_DESC, RCPNT_CLS_FR_DESC, RCPNT_NML_EN_DESC, RCPNT_NML_FR_DESC, CTY_EN_NM, CTY_FR_NM, PROVTER_EN, PROVTER_FR, CNTRY_EN_NM, CNTRY_FR_NM, TOT_CY_XPND_AMT, AGRG_PYMT_AMT  357,797 rows  Download SQLite DB: canada-spends.db 2.4 GB Powered by Datasette · Queries took 24.733ms " style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;h4 id="processing-pdfs"&gt;Processing PDFs&lt;/h4&gt;
&lt;p&gt;The highest quality government financial data comes from the audited financial statements that every Canadian government department is required to publish. As is so often the case with government data, these are usually published as PDFs.&lt;/p&gt;
&lt;p&gt;Brendan has been using Gemini to help extract data from those PDFs. Since this is accounting data the numbers can be summed and cross-checked to help validate the LLM didn't make any obvious mistakes.&lt;/p&gt;
&lt;h4 id="further-reading"&gt;Further reading&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://datasette.io/"&gt;datasette.io&lt;/a&gt;, the official website for Datasette&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sqlite-utils.datasette.io/"&gt;sqlite-utils.datasette.io&lt;/a&gt; for more on &lt;code&gt;sqlite-utils&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://canadaspends.com/"&gt;Canada Spends&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/BuildCanada/CanadaSpends"&gt;BuildCanada/CanadaSpends&lt;/a&gt; on GitHub&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/data-journalism"&gt;data-journalism&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/politics"&gt;politics&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/youtube"&gt;youtube&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="data-journalism"/><category term="politics"/><category term="sqlite"/><category term="youtube"/><category term="datasette"/><category term="sqlite-utils"/></entry><entry><title>sqlite-utils 3.39</title><link href="https://simonwillison.net/2025/Nov/24/sqlite-utils-339/#atom-tag" rel="alternate"/><published>2025-11-24T18:59:14+00:00</published><updated>2025-11-24T18:59:14+00:00</updated><id>https://simonwillison.net/2025/Nov/24/sqlite-utils-339/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://sqlite-utils.datasette.io/en/stable/changelog.html#v3-39"&gt;sqlite-utils 3.39&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I got a report of &lt;a href="https://github.com/simonw/sqlite-utils/issues/687"&gt;a bug&lt;/a&gt; in &lt;code&gt;sqlite-utils&lt;/code&gt; concerning plugin installation - if you installed the package using &lt;code&gt;uv tool install&lt;/code&gt; further attempts to install plugins with &lt;code&gt;sqlite-utils install X&lt;/code&gt; would fail, because &lt;code&gt;uv&lt;/code&gt; doesn't bundle &lt;code&gt;pip&lt;/code&gt; by default. I had the same bug with Datasette &lt;a href="https://github.com/simonw/sqlite-utils/issues/687"&gt;a while ago&lt;/a&gt;, turns out I forgot to apply the fix to &lt;code&gt;sqlite-utils&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Since I was pushing a new dot-release I decided to integrate some of the non-breaking changes from the 4.0 alpha &lt;a href="https://simonwillison.net/2025/Nov/24/sqlite-utils-40a1/"&gt;I released last night&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I tried to have Claude Code do the backporting for me:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;create a new branch called 3.x starting with the 3.38 tag, then consult 
&lt;a href="https://github.com/simonw/sqlite-utils/issues/688"&gt;https://github.com/simonw/sqlite-utils/issues/688&lt;/a&gt; and cherry-pick the commits it lists in the second comment, then review each of the links in the first comment and cherry-pick those as well. After each cherry-pick run the command "just test" to confirm the tests pass and fix them if they don't. Look through the commit history on main since the 3.38 tag to help you with this task.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This worked reasonably well - &lt;a href="https://gistpreview.github.io/?83c7a7ea96d6b7763ad5d72d251ce1a6"&gt;here's the terminal transcript&lt;/a&gt;. It successfully argued me out of two of the larger changes which would have added more complexity than I want in a small dot-release like this.&lt;/p&gt;
&lt;p&gt;I still had to do a bunch of manual work to get everything up to scratch, which I carried out in &lt;a href="https://github.com/simonw/sqlite-utils/pull/689"&gt;this PR&lt;/a&gt; - including adding comments there and then telling Claude Code:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Apply changes from the review on this PR &lt;a href="https://github.com/simonw/sqlite-utils/pull/689"&gt;https://github.com/simonw/sqlite-utils/pull/689&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's &lt;a href="https://gistpreview.github.io/?f4c89636cc58fc7bf9820c06f2488b91"&gt;the transcript from that&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The release is now out with the following release notes:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Fixed a bug with &lt;code&gt;sqlite-utils install&lt;/code&gt; when the tool had been installed using &lt;code&gt;uv&lt;/code&gt;. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/687"&gt;#687&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;--functions&lt;/code&gt; argument now optionally accepts a path to a Python file as an alternative to a string full of code, and can be specified multiple times - see &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#cli-query-functions"&gt;Defining custom SQL functions&lt;/a&gt;. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/659"&gt;#659&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sqlite-utils&lt;/code&gt; now requires on Python 3.10 or higher.&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/uv"&gt;uv&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="uv"/><category term="coding-agents"/><category term="claude-code"/></entry><entry><title>sqlite-utils 3.39</title><link href="https://simonwillison.net/2025/Nov/24/sqlite-utils/#atom-tag" rel="alternate"/><published>2025-11-24T18:42:29+00:00</published><updated>2025-11-24T18:42:29+00:00</updated><id>https://simonwillison.net/2025/Nov/24/sqlite-utils/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/3.39"&gt;sqlite-utils 3.39&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &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="sqlite"/><category term="sqlite-utils"/></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-tag" 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-tag</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 4.0a1</title><link href="https://simonwillison.net/2025/Nov/24/sqlite-utils-2/#atom-tag" rel="alternate"/><published>2025-11-24T06:32:51+00:00</published><updated>2025-11-24T06:32:51+00:00</updated><id>https://simonwillison.net/2025/Nov/24/sqlite-utils-2/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/4.0a1"&gt;sqlite-utils 4.0a1&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &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="sqlite"/><category term="sqlite-utils"/></entry><entry><title>Datasette ecosystem poster for PyCon US</title><link href="https://simonwillison.net/2025/May/17/pycon-poster/#atom-tag" rel="alternate"/><published>2025-05-17T20:34:39+00:00</published><updated>2025-05-17T20:34:39+00:00</updated><id>https://simonwillison.net/2025/May/17/pycon-poster/#atom-tag</id><summary type="html">
    &lt;p&gt;In addition to &lt;a href="https://simonwillison.net/2025/May/15/building-on-llms/"&gt;my workshop the other day&lt;/a&gt; I'm also participating in the &lt;a href="https://us.pycon.org/2025/schedule/posters/list/"&gt;poster session&lt;/a&gt; at PyCon US this year.&lt;/p&gt;
&lt;p&gt;This means that tomorrow (Sunday 18th May) I'll be hanging out next to my poster from 10am to 1pm in Hall A talking to people about my various projects.&lt;/p&gt;
&lt;p&gt;I'll confess: I didn't pay close enough attention to the poster information, so when I first put my poster up it looked a little small:&lt;/p&gt;
&lt;p&gt;&lt;img alt="My Datasette poster on a huge black poster board. It looks a bit lonely in the middle surrounded by empty space." src="https://static.simonwillison.net/static/2025/poster-before.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;... so I headed to the nearest CVS and printed out some photos to better represent my interests and personality. I'm going for a "teenage bedroom" aesthetic here, I'm very happy with the result:&lt;/p&gt;
&lt;p&gt;&lt;img alt="My Datasette poster is now surrounded by nearly 100 photos - mostly of pelicans, SVGs of pelicans and niche museums I've been to." src="https://static.simonwillison.net/static/2025/poster-after.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;Here's the poster in the middle (also available &lt;a href="https://static.simonwillison.net/static/2025/datasette-poster-v2.pdf"&gt;as a PDF&lt;/a&gt;). It has columns for &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt;, &lt;a href="https://sqlite-utils.datasette.io/"&gt;sqlite-utils&lt;/a&gt; and &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img id="datasette-poster" alt="Datasette: An ecosystem of tools for finding stories in data. Three projects: Datasette is a tool for exploring and publishing data. It helps data journalists (and everyone else) take data of any shape, analyze and explore it, and publish it as an interactive website and accompanying API. There's a screenshot of the table interface against a legislators table. Datasette has over 180 plugins adding features for visualizing, editing and transforming data. datasette-cluster-map, datasette-graphql, datasette-publish-cloudrun, datasette-comments, datasette-query-assistant, datasette-extract. datasette.io. sqlite-utils is a Python library and CLI tool for manipulating SQLite databases. It aims to make the gap from “I have data” to “that data is in SQLite” as small as possible. There's a code example showing inserting three chickens into a database and configuring full-text search. And in the terminal: sqlite-utils transform places.db roadside_attractions  --rename pk id  --default name Untitled  --drop address.  sqlite-utils.datasette.io. LLM is a Python library and CLI tool for interacting with Large Language Models. It provides a plugin-based abstraction over hundreds of different models, both local and hosted, and logs every interaction with them to SQLite. LLMs are proficient at SQL and extremely good at extracting structured data from unstructured text, images and documents. LLM’s asyncio Python library powers several Datasette plugins, including datasette-query-assistant, datasette-enrichments and datasette-extract. llm.datasette.io" src="https://static.simonwillison.net/static/2025/poster.jpg" style="max-width: 100%;"&gt;&lt;/p&gt;
&lt;p&gt;If you're at PyCon I'd love to talk to you about things I'm working on!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Thanks to everyone who came along. Here's a &lt;a href="https://static.simonwillison.net/static/2025/poster-full-size.jpeg"&gt;6MB photo of the poster setup&lt;/a&gt;. The museums were all from my &lt;a href="https://www.niche-museums.com/"&gt;www.niche-museums.com&lt;/a&gt; site and the pelicans riding a bicycle SVGs came from my &lt;a href="https://simonwillison.net/tags/pelican-riding-a-bicycle/"&gt;pelican-riding-a-bicycle tag&lt;/a&gt;.&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/museums"&gt;museums&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pycon"&gt;pycon&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;a href="https://simonwillison.net/tags/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pelican-riding-a-bicycle"&gt;pelican-riding-a-bicycle&lt;/a&gt;&lt;/p&gt;



</summary><category term="museums"/><category term="pycon"/><category term="datasette"/><category term="sqlite-utils"/><category term="llm"/><category term="pelican-riding-a-bicycle"/></entry><entry><title>sqlite-utils 4.0a0</title><link href="https://simonwillison.net/2025/May/9/sqlite-utils-40a0/#atom-tag" rel="alternate"/><published>2025-05-09T04:02:31+00:00</published><updated>2025-05-09T04:02:31+00:00</updated><id>https://simonwillison.net/2025/May/9/sqlite-utils-40a0/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/4.0a0"&gt;sqlite-utils 4.0a0&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
New alpha release of &lt;a href="https://sqlite-utils.datasette.io/"&gt;sqlite-utils&lt;/a&gt;, my Python library and CLI tool for manipulating SQLite databases.&lt;/p&gt;
&lt;p&gt;It's the first 4.0 alpha because there's a (minor) backwards-incompatible change: I've upgraded the &lt;code&gt;.upsert()&lt;/code&gt; and &lt;code&gt;.upsert_all()&lt;/code&gt; methods to use SQLIte's &lt;a href="https://www.sqlite.org/lang_upsert.html"&gt;UPSERT&lt;/a&gt; mechanism, &lt;code&gt;INSERT INTO ... ON CONFLICT DO UPDATE&lt;/code&gt;. Details in &lt;a href="https://github.com/simonw/sqlite-utils/issues/652"&gt;this issue&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That feature was added to SQLite in version 3.24.0, released 2018-06-04. I'm pretty cautious about my SQLite version support since the underlying library can be difficult to upgrade, depending on your platform and operating system.&lt;/p&gt;
&lt;p&gt;I'm going to leave the new alpha to bake for a little while before pushing a stable release. Since this is a major version bump I'm going to &lt;a href="https://github.com/simonw/sqlite-utils/issues/656"&gt;take the opportunity&lt;/a&gt; to see if there are any other minor API warts that I can clean up at the same time.


    &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>sqlite-utils 4.0a0</title><link href="https://simonwillison.net/2025/May/9/sqlite-utils/#atom-tag" rel="alternate"/><published>2025-05-09T03:54:32+00:00</published><updated>2025-05-09T03:54:32+00:00</updated><id>https://simonwillison.net/2025/May/9/sqlite-utils/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/4.0a0"&gt;sqlite-utils 4.0a0&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &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="sqlite"/><category term="sqlite-utils"/></entry><entry><title>sqlite-utils-tui 0.1</title><link href="https://simonwillison.net/2025/Jan/11/sqlite-utils-tui/#atom-tag" rel="alternate"/><published>2025-01-11T01:11:59+00:00</published><updated>2025-01-11T01:11:59+00:00</updated><id>https://simonwillison.net/2025/Jan/11/sqlite-utils-tui/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/sqlite-utils-tui/releases/tag/0.1"&gt;sqlite-utils-tui 0.1&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &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="sqlite"/><category term="sqlite-utils"/></entry><entry><title>Ask questions of SQLite databases and CSV/JSON files in your terminal</title><link href="https://simonwillison.net/2024/Nov/25/ask-questions-of-sqlite/#atom-tag" rel="alternate"/><published>2024-11-25T01:33:03+00:00</published><updated>2024-11-25T01:33:03+00:00</updated><id>https://simonwillison.net/2024/Nov/25/ask-questions-of-sqlite/#atom-tag</id><summary type="html">
    &lt;p&gt;I built a new plugin for my &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html"&gt;sqlite-utils CLI tool&lt;/a&gt; that lets you ask human-language questions directly of SQLite databases and CSV/JSON files on your computer.&lt;/p&gt;
&lt;p&gt;It's called &lt;a href="https://github.com/simonw/sqlite-utils-ask"&gt;sqlite-utils-ask&lt;/a&gt;. Here's how you install it:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;sqlite-utils install sqlite-utils-ask&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It picks up API keys from an &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; environment variable, or you can &lt;a href="https://llm.datasette.io/"&gt;install LLM&lt;/a&gt; and use &lt;a href="https://llm.datasette.io/en/stable/setup.html#saving-and-using-stored-keys"&gt;llm keys set openai&lt;/a&gt; to store a key in a configuration file.&lt;/p&gt;
&lt;p&gt;Then you can use it like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;curl -O https://datasette.io/content.db
sqlite-utils ask content.db &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;how many sqlite-utils pypi downloads in 2024?&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This command will extract the SQL schema for the provided database file, send that through an LLM along with your question, get back a SQL query and attempt to run it to derive a result.&lt;/p&gt;
&lt;p&gt;If all goes well it spits out an answer something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT SUM(downloads)
FROM stats
WHERE package = 'sqlite-utils' AND date &amp;gt;= '2024-01-01' AND date &amp;lt; '2025-01-01';

[
    {
        "SUM(downloads)": 4300221
    }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the SQL query fails to execute (due to a syntax error of some kind) it passes that error back to the model for corrections and retries up to three times before giving up.&lt;/p&gt;
&lt;p&gt;Add &lt;code&gt;-v/--verbose&lt;/code&gt; to see the exact prompt it's using:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;System prompt:
You will be given a SQLite schema followed by a question. Generate a single SQL
query to answer that question. Return that query in a ```sql ... ```
fenced code block.

Example: How many repos are there?
Answer:
```sql
select count(*) from repos
```

Prompt:
...
CREATE TABLE [stats] (
   [package] TEXT,
   [date] TEXT,
   [downloads] INTEGER,
   PRIMARY KEY ([package], [date])
);
...
how many sqlite-utils pypi downloads in 2024?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I've truncated the above to just the relevant table - it actually includes the full schema of every table in that database.&lt;/p&gt;
&lt;p&gt;By default, the tool sends just that database schema and your question to the LLM. If you add the &lt;code&gt;-e/--examples&lt;/code&gt; option it will also include five common values for each of the text columns in that schema with an average length less than 32 characters. This can sometimes help get a better result, for example sending values "CA" and "FL" and "TX" for a &lt;code&gt;state&lt;/code&gt; column can tip the model of that it should use state abbreviations rather than full names in its queries.&lt;/p&gt;
&lt;h4 id="ask-files"&gt;Asking questions of CSV and JSON data&lt;/h4&gt;
&lt;p&gt;The core &lt;code&gt;sqlite-utils&lt;/code&gt; CLI usually works against SQLite files directly, but three years ago I added the ability to run SQL queries against CSV and JSON files directly with the &lt;a href="https://simonwillison.net/2021/Jun/19/sqlite-utils-memory/"&gt;sqlite-utils memory&lt;/a&gt; command. This works by loading that data into an in-memory SQLite database before executing a SQL query.&lt;/p&gt;
&lt;p&gt;I decided to reuse that mechanism to enable LLM prompts against CSV and JSON data directly as well.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;sqlite-utils ask-files&lt;/code&gt; command looks like this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;sqlite-utils ask-files transactions.csv &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;total sales by year&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This command accepts one or more files, and you can provide a mix of CSV, TSV and JSON. Each provided file will be imported into a different table, allowing the model to construct join queries where necessary.&lt;/p&gt;
&lt;h4 id="implementation-notes"&gt;Implementation notes&lt;/h4&gt;
&lt;p&gt;The core of the plugin is implemented as around &lt;a href="https://github.com/simonw/sqlite-utils-ask/blob/0.2/sqlite_utils_ask.py"&gt;250 lines of Python&lt;/a&gt;, using the &lt;code&gt;sqlite-utils&lt;/code&gt; &lt;a href="https://sqlite-utils.datasette.io/en/stable/plugins.html#register-commands-cli"&gt;register_commands()&lt;/a&gt; plugin hook to add the &lt;code&gt;ask&lt;/code&gt; and &lt;code&gt;ask-files&lt;/code&gt; commands.&lt;/p&gt;
&lt;p&gt;It adds &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; as a dependency, and takes advantage of LLM's &lt;a href="https://llm.datasette.io/en/stable/python-api.html"&gt;Python API&lt;/a&gt; to abstract over the details of talking to the models. This means &lt;code&gt;sqlite-utils-ask&lt;/code&gt; can use any of the models supported by LLM or its plugins - if you want to run your prompt through Claude 3.5 Sonnet you can do this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;sqlite-utils install llm-claude-3
sqlite-utils ask content.db &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;count rows in news table&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; -m claude-3.5-sonnet&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The plugin defaults to &lt;a href="https://openai.com/index/gpt-4o-mini-advancing-cost-efficient-intelligence/"&gt;gpt-4o-mini&lt;/a&gt; initially to take advantage of that model's automatic prompt caching: if you run multiple questions against the same schema you'll end up sending the same lengthy prompt prefix multiple times, and OpenAI's prompt caching should automatically kick in and provide a 50% discount on those input tokens.&lt;/p&gt;
&lt;p&gt;Then I ran the actual numbers and found that &lt;code&gt;gpt-4o-mini&lt;/code&gt; is cheap enough that even without caching a 4,000 token prompt (that's a pretty large SQL schema) should cost less than a tenth of a cent. So those caching savings aren't worth anything at all!&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/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/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/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/llm"&gt;llm&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="cli"/><category term="plugins"/><category term="projects"/><category term="sqlite"/><category term="ai"/><category term="sqlite-utils"/><category term="generative-ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="llm"/></entry><entry><title>sqlite-utils 3.38</title><link href="https://simonwillison.net/2024/Nov/23/sqlite-utils/#atom-tag" rel="alternate"/><published>2024-11-23T22:40:30+00:00</published><updated>2024-11-23T22:40:30+00:00</updated><id>https://simonwillison.net/2024/Nov/23/sqlite-utils/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/3.38"&gt;sqlite-utils 3.38&lt;/a&gt;&lt;/p&gt;
        &lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Plugins can now reuse the implementation of the &lt;code&gt;sqlite-utils memory&lt;/code&gt; CLI command with the new &lt;code&gt;return_db=True&lt;/code&gt; parameter. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/643"&gt;#643&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;table.transform()&lt;/code&gt; now recreates indexes after transforming a table. A new &lt;code&gt;sqlite_utils.db.TransformError&lt;/code&gt;exception is raised if these indexes cannot be recreated due to conflicting changes to the table such as a column rename. Thanks, &lt;a href="https://github.com/matdmiller"&gt;Mat Miller&lt;/a&gt;. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/633"&gt;#633&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;table.search()&lt;/code&gt; now accepts a &lt;code&gt;include_rank=True&lt;/code&gt; parameter, causing the resulting rows to have a &lt;code&gt;rank&lt;/code&gt; column showing the calculated relevance score. Thanks, &lt;a href="https://github.com/liunux4odoo"&gt;liunux4odoo&lt;/a&gt;. (&lt;a href="https://github.com/simonw/sqlite-utils/pull/628"&gt;#628&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Fixed an error that occurred when creating a strict table with at least one floating point column. These &lt;code&gt;FLOAT&lt;/code&gt;columns are now correctly created as &lt;code&gt;REAL&lt;/code&gt; as well, but only for strict tables. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/644"&gt;#644&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
    
    
        &lt;p&gt;Tags: &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="sqlite"/><category term="sqlite-utils"/></entry><entry><title>sqlite-utils 3.38a0</title><link href="https://simonwillison.net/2024/Nov/8/sqlite-utils/#atom-tag" rel="alternate"/><published>2024-11-08T20:14:47+00:00</published><updated>2024-11-08T20:14:47+00:00</updated><id>https://simonwillison.net/2024/Nov/8/sqlite-utils/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/3.38a0"&gt;sqlite-utils 3.38a0&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &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="sqlite"/><category term="sqlite-utils"/></entry><entry><title>Using sqlite-vec with embeddings in sqlite-utils and Datasette</title><link href="https://simonwillison.net/2024/Aug/11/sqlite-vec/#atom-tag" rel="alternate"/><published>2024-08-11T23:37:42+00:00</published><updated>2024-08-11T23:37:42+00:00</updated><id>https://simonwillison.net/2024/Aug/11/sqlite-vec/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://til.simonwillison.net/sqlite/sqlite-vec"&gt;Using sqlite-vec with embeddings in sqlite-utils and Datasette&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
My notes on trying out Alex Garcia's newly released &lt;a href="https://github.com/asg017/sqlite-vec"&gt;sqlite-vec&lt;/a&gt; SQLite extension, including how to use it with OpenAI embeddings in both &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; and &lt;a href="https://sqlite-utils.datasette.io/"&gt;sqlite-utils&lt;/a&gt;.


    &lt;p&gt;Tags: &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;a href="https://simonwillison.net/tags/openai"&gt;openai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/alex-garcia"&gt;alex-garcia&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/embeddings"&gt;embeddings&lt;/a&gt;&lt;/p&gt;



</summary><category term="sqlite"/><category term="datasette"/><category term="sqlite-utils"/><category term="openai"/><category term="alex-garcia"/><category term="embeddings"/></entry><entry><title>Weeknotes: GPT-4o mini, LLM 0.15, sqlite-utils 3.37 and building a staging environment</title><link href="https://simonwillison.net/2024/Jul/19/weeknotes/#atom-tag" rel="alternate"/><published>2024-07-19T00:11:14+00:00</published><updated>2024-07-19T00:11:14+00:00</updated><id>https://simonwillison.net/2024/Jul/19/weeknotes/#atom-tag</id><summary type="html">
    &lt;p&gt;Upgrades to &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; to support the latest models, and a whole bunch of invisible work building out a staging environment for Datasette Cloud.&lt;/p&gt;
&lt;h4 id="gpt-4o-mini-and-llm-0-15"&gt;GPT-4o mini and LLM 0.15&lt;/h4&gt;
&lt;p&gt;Today's big news was the release of &lt;a href="https://openai.com/index/gpt-4o-mini-advancing-cost-efficient-intelligence/"&gt;GPT-4o mini&lt;/a&gt;, which I &lt;a href="https://simonwillison.net/2024/Jul/18/gpt-4o-mini/"&gt;wrote about here&lt;/a&gt;. If you build applications on top of LLMs this is a very significant release - it's the cheapest of the high performing hosted models (cheaper even than Claude 3 Haiku and Gemini 1.5 Flash) and has some notable characteristics, most importantly the 16,000 token output limit.&lt;/p&gt;
&lt;p&gt;I shipped a &lt;a href="https://simonwillison.net/2024/Jul/18/llm-015/"&gt;new LLM release&lt;/a&gt; to support the new model. Full release notes for &lt;a href="https://llm.datasette.io/en/stable/changelog.html#v0-15"&gt;LLM 0.15&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Support for OpenAI's &lt;a href="https://openai.com/index/gpt-4o-mini-advancing-cost-efficient-intelligence/"&gt;new GPT-4o mini&lt;/a&gt; model: &lt;code&gt;llm -m gpt-4o-mini 'rave about pelicans in French'&lt;/code&gt; &lt;a href="https://github.com/simonw/llm/issues/536"&gt;#536&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gpt-4o-mini&lt;/code&gt; is now the default model if you do not &lt;a href="https://llm.datasette.io/en/stable/setup.html#setting-a-custom-default-model"&gt;specify your own default&lt;/a&gt;, replacing GPT-3.5 Turbo. GPT-4o mini is both cheaper and better than GPT-3.5 Turbo.&lt;/li&gt;
&lt;li&gt;Fixed a bug where &lt;code&gt;llm logs -q 'flourish' -m haiku&lt;/code&gt; could not combine both the &lt;code&gt;-q&lt;/code&gt; search query and the &lt;code&gt;-m&lt;/code&gt; model specifier. &lt;a href="https://github.com/simonw/llm/issues/515"&gt;#515&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h4 id="sqlite-utils-3-37"&gt;sqlite-utils 3.37&lt;/h4&gt;
&lt;p&gt;LLM had a frustrating bug involving &lt;a href="https://github.com/simonw/llm/issues/531"&gt;a weird numpy issue&lt;/a&gt; that only manifested on LLM when installed via Homebrew. I ended up fixing that in its &lt;code&gt;sqlite-utils&lt;/code&gt; dependency - here are the full release notes for &lt;a href="https://sqlite-utils.datasette.io/en/stable/changelog.html#v3-37"&gt;sqlite-utils 3.37&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;create-table&lt;/code&gt; and &lt;code&gt;insert-files&lt;/code&gt; commands all now accept multiple &lt;code&gt;--pk&lt;/code&gt; options for compound primary keys. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/620"&gt;#620&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Now tested against Python 3.13 pre-release. (&lt;a href="https://github.com/simonw/sqlite-utils/pull/619"&gt;#619&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Fixed a crash that can occur in environments with a broken &lt;code&gt;numpy&lt;/code&gt; installation, producing a &lt;code&gt;module 'numpy' has no attribute 'int8'&lt;/code&gt;. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/632"&gt;#632&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h4 id="datasette-cloud-staging-environment"&gt;Datasette Cloud staging environment&lt;/h4&gt;
&lt;p&gt;I'm a big believer in reducing the friction involved in making changes to code. The main reason I'm so keen on the combination of automated tests, GitHub Actions for CI/CD and extensive documentation (as described in &lt;a href="https://simonwillison.net/2022/Nov/26/productivity/"&gt;Coping strategies for the serial project hoarder&lt;/a&gt;) is that&lt;/p&gt;
&lt;p&gt;Sadly, &lt;a href="https://www.datasette.cloud/"&gt;Datasette Cloud&lt;/a&gt; hasn't been living up these standards as much as I would like. I have great comprehensive tests for it, continuous deployment that deploys when those tests pass and pretty solid internal documentation (mainly spread out across dozens of GitHub Issues) - but the thing I've been missing is a solid staging environment.&lt;/p&gt;
&lt;p&gt;This matters because a lot of the most complex code in Datasette Cloud involves deploying new instances of Datasette to &lt;a href="https://fly.io/docs/machines/"&gt;Fly Machines&lt;/a&gt;. The thing that's been missing is a separate environment where I can exercise my Fly deployment code independently of the production cluster.&lt;/p&gt;
&lt;p&gt;I've been working towards this over the past week, and in doing so have found all sorts of pieces of the codebase that are hard-coded in a way that needs to be unwrapped to correctly support that alternative environment.&lt;/p&gt;
&lt;p&gt;I'm getting there, but it's been one of those frustrating projects where every step forward uncovers at least one more tiny problem that needs to be resolved.&lt;/p&gt;
&lt;p&gt;A lot of these problems relate to the GitHub Actions workflows being used to build, test and deploy my containers. Thankfully Claude 3.5 Sonnet is great at helping refactor GitHub Actions YAML, which has been saving me a lot of time.&lt;/p&gt;
&lt;p&gt;I'm really looking forward to wrapping this up, because I plan to celebrate by shipping a flurry of Datasette Cloud features that have been held up by the lack of a robust way to extensively test them before sending them out into the world.&lt;/p&gt;
&lt;h4 id="weeknotes-2024-07-19-blog-entries"&gt;Blog entries&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://simonwillison.net/2024/Jul/14/pycon/"&gt;Imitation Intelligence, my keynote for PyCon US 2024&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonwillison.net/2024/Jul/13/give-people-something-to-link-to/"&gt;Give people something to link to so they can talk about your features and ideas&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also updated my &lt;a href="https://simonwillison.net/2024/Jun/27/ai-worlds-fair/"&gt;write-up of my recent AI World's Fair keynote&lt;/a&gt; to include a link to the standalone YouTube video of the talk.&lt;/p&gt;
&lt;h4 id="weeknotes-2024-07-19-releases"&gt;Releases&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/llm/releases/tag/0.15"&gt;llm 0.15&lt;/a&gt;&lt;/strong&gt; - 2024-07-18&lt;br /&gt;Access large language models from the command-line&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/3.37"&gt;sqlite-utils 3.37&lt;/a&gt;&lt;/strong&gt; - 2024-07-18&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/llm-mistral/releases/tag/0.4"&gt;llm-mistral 0.4&lt;/a&gt;&lt;/strong&gt; - 2024-07-16&lt;br /&gt;LLM plugin providing access to Mistral models using the Mistral API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/datasette/datasette-python/releases/tag/0.1"&gt;datasette-python 0.1&lt;/a&gt;&lt;/strong&gt; - 2024-07-12&lt;br /&gt;Run a Python interpreter in the Datasette virtual environment&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="weeknotes-2024-07-19-tils"&gt;TILs&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://til.simonwillison.net/python/trying-free-threaded-python"&gt;Trying out free-threaded Python on macOS&lt;/a&gt; - 2024-07-13&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://til.simonwillison.net/macos/1password-terminal"&gt;Accessing 1Password items from the terminal&lt;/a&gt; - 2024-07-10&lt;/li&gt;
&lt;/ul&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/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-cloud"&gt;datasette-cloud&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/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm"&gt;llm&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="projects"/><category term="ai"/><category term="weeknotes"/><category term="datasette-cloud"/><category term="sqlite-utils"/><category term="llms"/><category term="llm"/></entry><entry><title>sqlite-utils 3.37</title><link href="https://simonwillison.net/2024/Jul/18/sqlite-utils/#atom-tag" rel="alternate"/><published>2024-07-18T18:44:02+00:00</published><updated>2024-07-18T18:44:02+00:00</updated><id>https://simonwillison.net/2024/Jul/18/sqlite-utils/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/3.37"&gt;sqlite-utils 3.37&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &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="sqlite"/><category term="sqlite-utils"/></entry><entry><title>Announcing our DjangoCon US 2024 Talks!</title><link href="https://simonwillison.net/2024/Jul/17/djangocon-us-2024-talks/#atom-tag" rel="alternate"/><published>2024-07-17T03:20:57+00:00</published><updated>2024-07-17T03:20:57+00:00</updated><id>https://simonwillison.net/2024/Jul/17/djangocon-us-2024-talks/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://2024.djangocon.us/news/announcing-lineup/"&gt;Announcing our DjangoCon US 2024 Talks!&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I'm speaking at DjangoCon in Durham, NC in September.&lt;/p&gt;
&lt;p&gt;My accepted talk title was &lt;strong&gt;How to design and implement extensible software with plugins&lt;/strong&gt;. Here's my abstract:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Plugins offer a powerful way to extend software packages. Tools that support a plugin architecture include WordPress, Jupyter, VS Code and pytest - each of which benefits from an enormous array of plugins adding all kinds of new features and expanded capabilities.&lt;/p&gt;
&lt;p&gt;Adding plugin support to an open source project can greatly reduce the friction involved in attracting new contributors. Users can work independently and even package and publish their work without needing to directly coordinate with the project's core maintainers. As a maintainer this means you can wake up one morning and your software grew new features without you even having to review a pull request!&lt;/p&gt;
&lt;p&gt;There's one catch: information on &lt;em&gt;how&lt;/em&gt; to design and implement plugin support for a project is scarce.&lt;/p&gt;
&lt;p&gt;I now have three major open source projects that support plugins, with over 200 plugins published across those projects. I'll talk about everything I've learned along the way: when and how to use plugins, how to design plugin hooks and how to ensure your plugin authors have as good an experience as possible.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I'm going to be talking about what I've learned integrating &lt;a href="https://pluggy.readthedocs.io/"&gt;Pluggy&lt;/a&gt; with &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt;, &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; and &lt;a href="https://sqlite-utils.datasette.io/"&gt;sqlite-utils&lt;/a&gt;. I've been looking for an excuse to turn this knowledge into a talk for ages, very excited to get to do it at DjangoCon!


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/djangocon"&gt;djangocon&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/speaking"&gt;speaking&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;a href="https://simonwillison.net/tags/llm"&gt;llm&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="djangocon"/><category term="plugins"/><category term="python"/><category term="speaking"/><category term="datasette"/><category term="sqlite-utils"/><category term="llm"/></entry><entry><title>fastlite</title><link href="https://simonwillison.net/2024/May/27/fastlite/#atom-tag" rel="alternate"/><published>2024-05-27T21:14:01+00:00</published><updated>2024-05-27T21:14:01+00:00</updated><id>https://simonwillison.net/2024/May/27/fastlite/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://answerdotai.github.io/fastlite/"&gt;fastlite&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
New Python library from Jeremy Howard that adds some neat utility functions and syntactic sugar to my &lt;a href="https://sqlite-utils.datasette.io/"&gt;sqlite-utils&lt;/a&gt; Python library, specifically for interactive use in Jupyter notebooks.&lt;/p&gt;
&lt;p&gt;The autocomplete support through newly exposed dynamic properties is particularly neat, as is the &lt;code&gt;diagram(db.tables)&lt;/code&gt; utility for rendering a graphviz diagram showing foreign key relationships between all of the tables.

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


    &lt;p&gt;Tags: &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/jupyter"&gt;jupyter&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/jeremy-howard"&gt;jeremy-howard&lt;/a&gt;&lt;/p&gt;



</summary><category term="python"/><category term="sqlite"/><category term="jupyter"/><category term="sqlite-utils"/><category term="jeremy-howard"/></entry><entry><title>sqlite-utils-shell 0.3</title><link href="https://simonwillison.net/2023/Dec/21/sqlite-utils-shell/#atom-tag" rel="alternate"/><published>2023-12-21T06:24:17+00:00</published><updated>2023-12-21T06:24:17+00:00</updated><id>https://simonwillison.net/2023/Dec/21/sqlite-utils-shell/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/sqlite-utils-shell/releases/tag/0.3"&gt;sqlite-utils-shell 0.3&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &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="sqlite"/><category term="sqlite-utils"/></entry><entry><title>Weeknotes: datasette-enrichments, datasette-comments, sqlite-chronicle</title><link href="https://simonwillison.net/2023/Dec/8/weeknotes/#atom-tag" rel="alternate"/><published>2023-12-08T06:04:54+00:00</published><updated>2023-12-08T06:04:54+00:00</updated><id>https://simonwillison.net/2023/Dec/8/weeknotes/#atom-tag</id><summary type="html">
    &lt;p&gt;I've mainly been working on &lt;a href="https://enrichments.datasette.io/"&gt;Datasette Enrichments&lt;/a&gt; and continuing to explore the possibilities enabled by &lt;a href="https://github.com/simonw/sqlite-chronicle"&gt;sqlite-chronicle&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="weeknotes-enrichments"&gt;Enrichments&lt;/h4&gt;
&lt;p&gt;This is the biggest new Datasette feature to arrive in quite a while, and it's entirely implemented as a plugin.&lt;/p&gt;
&lt;p&gt;I described these in detail in &lt;strong&gt;&lt;a href="https://simonwillison.net/2023/Dec/1/datasette-enrichments/"&gt;Datasette Enrichments: a new plugin framework for augmenting your data&lt;/a&gt;&lt;/strong&gt; (with an accompanying &lt;a href="https://www.youtube.com/watch?v=HqKlJCgdjfg"&gt;YouTube video demo&lt;/a&gt;). The short version: you can now install plugins that can "enrich" data by running transformations (or data fetches) against selected rows - geocoding addresses, or executing a GPT prompt, or applying a regular expression.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://datasette.io/plugins/datasette-enrichments"&gt;datasette-enrichments&lt;/a&gt; plugin provides the mechanism for running these enrichments. Other plugins can then depend on it and define all manner of interesting options for enriching and transforming data.&lt;/p&gt;
&lt;p&gt;I've built four of these so far, and I wrote some &lt;a href="https://enrichments.datasette.io/en/stable/developing.html"&gt;extensive documentation&lt;/a&gt; to help people build more. I'm excited to see how people use and build further on this initial foundation.&lt;/p&gt;
&lt;h4 id="weeknotes-datasette-comments"&gt;Datasette Comments&lt;/h4&gt;
&lt;p&gt;Alex Garcia released the first version of &lt;a href="https://datasette.io/plugins/datasette-comments"&gt;datasette-comments&lt;/a&gt; as part of our continuing collaboration to build out Datasette Cloud.&lt;/p&gt;
&lt;p&gt;He wrote about that on the Datasette Cloud blog: &lt;strong&gt;&lt;a href="https://www.datasette.cloud/blog/2023/datasette-comments/"&gt;Annotate and explore your data with datasette-comments&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2023/datasette-comments.jpg" alt="Three comment threads demonstrating features of Datasette Comments - replies, reaction emoji, hashtags and the ability to mention other users." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;This is another capability I've been looking forward to for years: the plugin lets you leave comments on individual rows within a Datasette instance, in order to collaborate with others on finding stories in data.&lt;/p&gt;
&lt;h4 id="weeknotes-chronicle"&gt;sqlite-chronicle and datasette-chronicle&lt;/h4&gt;
&lt;p&gt;I first wrote about &lt;a href="https://github.com/simonw/sqlite-chronicle"&gt;sqlite-chronicle&lt;/a&gt; in &lt;a href="https://simonwillison.net/2023/Sep/17/weeknotes-embeddings/#sqlite-chronicle"&gt;weeknotes back in September&lt;/a&gt;. This week, inspired by my work on embeddings, I spent a bit more time on it and shipped &lt;a href="https://github.com/simonw/sqlite-chronicle/releases/tag/0.2"&gt;a 0.2 release&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sqlite-chronicle&lt;/code&gt; is a Python library that implements a SQL pattern where a table can have a &lt;code&gt;_chronicle_tablename&lt;/code&gt; companion table created, which is then updated using triggers against the main table.&lt;/p&gt;
&lt;p&gt;The chronicle table has a shadow row for every row in the main table, duplicating its primary keys and then storing millisecond timestamp columns for &lt;code&gt;added_ms&lt;/code&gt; and &lt;code&gt;updated_ms&lt;/code&gt;, an integer &lt;code&gt;version&lt;/code&gt; column and a &lt;code&gt;deleted&lt;/code&gt; boolean indicator.&lt;/p&gt;
&lt;p&gt;The goal is to record when a row was last inserted or updated, with an atomically incrementing &lt;code&gt;version&lt;/code&gt; ID representing the version of the entire table.&lt;/p&gt;
&lt;p&gt;This can then enable all sorts of interesting potential use-cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Identify which rows have been updated or inserted since a previously recorded version&lt;/li&gt;
&lt;li&gt;Synchronize a table with another table, only updating/inserting/deleting rows that have changed since last time&lt;/li&gt;
&lt;li&gt;Run scheduled tasks that only consider rows that have changed in some way&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The relevance to enrichments is that I'd like to implement a form of "persistent" enrichment - an enrichment which is configured to run repeatedly against new or updated rows, geocoding new addresses for example.&lt;/p&gt;
&lt;p&gt;To do that, I need a mechanism to identify which rows have already been enriched and which need to be enriched again. &lt;code&gt;sqlite-chronicle&lt;/code&gt; is my current plan to provide that mechanism.&lt;/p&gt;
&lt;p&gt;It's still pretty experimental. I recently found that &lt;code&gt;INSERT OR REPLACE INTO&lt;/code&gt; queries don't behave how I would expect them to, see &lt;a href="https://github.com/simonw/sqlite-chronicle/issues/7"&gt;issue #7&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I also started a new plugin to accompany the feature: &lt;a href="https://datasette.io/plugins/datasette-chronicle"&gt;datasette-chronicle&lt;/a&gt;, which adds two features to Datasette:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;"enable/disable chronicle tracking" table actions for users with the correct permissions, which can be used in the Datasette UI to turn chronicle tracking on and off for a specific table&lt;/li&gt;
&lt;li&gt;For tables that have chronicle enabled, a &lt;code&gt;?_since=VERSION&lt;/code&gt; querystring parameter which can be used to filter the table to only rows that have changed since the specified version&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I'm running the plugin against the &lt;a href="https://demos.datasette.cloud/data/documents"&gt;documents&lt;/a&gt; table on &lt;code&gt;demos.datasette.cloud&lt;/code&gt; - see &lt;a href="https://demos.datasette.cloud/data/_chronicle_documents"&gt;_chronicle_documents&lt;/a&gt; there for the result. That table is populated via GitHub scheduled actions and the Datasette API, as described in &lt;a href="https://www.datasette.cloud/blog/2023/datasette-cloud-api/"&gt;Getting started with the Datasette Cloud API&lt;/a&gt; - it's also where I first spotted the &lt;code&gt;INSERT OR REPLACE INTO&lt;/code&gt; issue I described earlier.&lt;/p&gt;
&lt;h4 id="weeknotes-newsroom-robots"&gt;Newsroom Robots&lt;/h4&gt;
&lt;p&gt;I recorded an episode of the &lt;a href="https://www.newsroomrobots.com/"&gt;Newsroom Robots&lt;/a&gt; AI in journalism podcast with Nikita Roy a couple of weeks ago.&lt;/p&gt;
&lt;p&gt;She split our conversation into two episodes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.newsroomrobots.com/p/breaking-down-openais-new-features"&gt;Simon Willison (Part One): Breaking Down OpenAI's New Features &amp;amp; Security Risks of Large Language Models&lt;/a&gt; - which I ended up using as the basis for two blog entries:
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://simonwillison.net/2023/Nov/25/newsroom-robots/"&gt;I'm on the Newsroom Robots podcast, with thoughts on the OpenAI board&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonwillison.net/2023/Nov/27/prompt-injection-explained/"&gt;Prompt injection explained, November 2023 edition&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.newsroomrobots.com/p/how-datasette-helps-with-investigative"&gt;Simon Willison (Part Two): How Datasette Helps With Investigative Reporting&lt;/a&gt; which has the best audio description of Datasette I've managed to produce so far.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="weeknotes-sqlite-utils-3-36"&gt;sqlite-utils 3.36&lt;/h4&gt;
&lt;p&gt;Quoting the &lt;a href="https://sqlite-utils.datasette.io/en/stable/changelog.html#v3-36"&gt;release notes&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Support for creating tables in &lt;a href="https://www.sqlite.org/stricttables.html"&gt;SQLite STRICT mode&lt;/a&gt;. Thanks, &lt;a href="https://github.com/tkhattra"&gt;Taj Khattra&lt;/a&gt;. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/344"&gt;#344&lt;/a&gt;)
&lt;ul&gt;
&lt;li&gt;CLI commands &lt;code&gt;create-table&lt;/code&gt;, &lt;code&gt;insert&lt;/code&gt; and &lt;code&gt;upsert&lt;/code&gt; all now accept a &lt;code&gt;--strict&lt;/code&gt; option.&lt;/li&gt;
&lt;li&gt;Python methods that can create a table - &lt;code&gt;table.create()&lt;/code&gt; and &lt;code&gt;insert/upsert/insert_all/upsert_all&lt;/code&gt; all now accept an optional &lt;code&gt;strict=True&lt;/code&gt; parameter.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;transform&lt;/code&gt; command and &lt;code&gt;table.transform()&lt;/code&gt; method preserve strict mode when transforming a table.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;sqlite-utils create-table&lt;/code&gt; command now accepts &lt;code&gt;str&lt;/code&gt;, &lt;code&gt;int&lt;/code&gt; and &lt;code&gt;bytes&lt;/code&gt; as aliases for &lt;code&gt;text&lt;/code&gt;, &lt;code&gt;integer&lt;/code&gt; and &lt;code&gt;blob&lt;/code&gt; respectively. (&lt;a href="https://github.com/simonw/sqlite-utils/issues/606"&gt;#606&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Taj Khattra's contribution of the &lt;code&gt;--strict&lt;/code&gt; and &lt;code&gt;strict=True&lt;/code&gt; options is a beautiful example of my ideal pull request: a clean implementation, comprehensive tests and thoughtful updates to the documentation &lt;a href="https://github.com/simonw/sqlite-utils/pull/604"&gt;all bundled together in one go&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="weeknotes-releases"&gt;Releases&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/3.36"&gt;sqlite-utils 3.36&lt;/a&gt;&lt;/strong&gt; - 2023-12-08&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-leaflet-geojson/releases/tag/0.8.1"&gt;datasette-leaflet-geojson 0.8.1&lt;/a&gt;&lt;/strong&gt; - 2023-12-07&lt;br /&gt;Datasette plugin that replaces any GeoJSON column values with a Leaflet map.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/datasette/datasette-chronicle/releases/tag/0.2"&gt;datasette-chronicle 0.2&lt;/a&gt;&lt;/strong&gt; - 2023-12-06&lt;br /&gt;Enable sqlite-chronicle against tables in Datasette&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/datasette/datasette-enrichments-jinja/releases/tag/0.1"&gt;datasette-enrichments-jinja 0.1&lt;/a&gt;&lt;/strong&gt; - 2023-12-06&lt;br /&gt;Datasette enrichment for evaluating templates in a Jinja sandbox&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/sqlite-chronicle/releases/tag/0.2.1"&gt;sqlite-chronicle 0.2.1&lt;/a&gt;&lt;/strong&gt; - 2023-12-06&lt;br /&gt;Use triggers to track when rows in a SQLite table were updated or deleted&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/datasette/datasette-enrichments-gpt/releases/tag/0.3"&gt;datasette-enrichments-gpt 0.3&lt;/a&gt;&lt;/strong&gt; - 2023-12-01&lt;br /&gt;Datasette enrichment for analyzing row data using OpenAI's GPT models&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-statistics/releases/tag/0.2.1"&gt;datasette-statistics 0.2.1&lt;/a&gt;&lt;/strong&gt; - 2023-11-30&lt;br /&gt;SQL statistics functions for Datasette&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/datasette/datasette-enrichments-opencage/releases/tag/0.1"&gt;datasette-enrichments-opencage 0.1&lt;/a&gt;&lt;/strong&gt; - 2023-11-30&lt;br /&gt;Geocoding and reverse geocoding using OpenCage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/datasette/datasette-enrichments-re2/releases/tag/0.1"&gt;datasette-enrichments-re2 0.1&lt;/a&gt;&lt;/strong&gt; - 2023-11-30&lt;br /&gt;Enrich data using regular expressions powered by re2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/datasette/datasette-enrichments/releases/tag/0.2"&gt;datasette-enrichments 0.2&lt;/a&gt;&lt;/strong&gt; - 2023-11-29&lt;br /&gt;Tools for running enrichments against data stored in Datasette&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-pretty-json/releases/tag/0.3"&gt;datasette-pretty-json 0.3&lt;/a&gt;&lt;/strong&gt; - 2023-11-28&lt;br /&gt;Datasette plugin that pretty-prints any column values that are valid JSON objects or arrays&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="weeknotes-tils"&gt;TILs&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://til.simonwillison.net/macos/quick-whisper-youtube"&gt;Grabbing a transcript of a short snippet of a YouTube video with MacWhisper&lt;/a&gt; - 2023-12-01&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://til.simonwillison.net/pyodide/cryptography-in-pyodide"&gt;Cryptography in Pyodide&lt;/a&gt; - 2023-11-26&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://til.simonwillison.net/readthedocs/pip-install-docs"&gt;Running pip install '.[docs]' on ReadTheDocs&lt;/a&gt; - 2023-11-24&lt;/li&gt;
&lt;/ul&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/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;a href="https://simonwillison.net/tags/enrichments"&gt;enrichments&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="projects"/><category term="sqlite"/><category term="datasette"/><category term="weeknotes"/><category term="sqlite-utils"/><category term="enrichments"/></entry><entry><title>sqlite-utils 3.36</title><link href="https://simonwillison.net/2023/Dec/8/sqlite-utils/#atom-tag" rel="alternate"/><published>2023-12-08T05:32:21+00:00</published><updated>2023-12-08T05:32:21+00:00</updated><id>https://simonwillison.net/2023/Dec/8/sqlite-utils/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/3.36"&gt;sqlite-utils 3.36&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &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="sqlite"/><category term="sqlite-utils"/></entry></feed>