<?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-busy</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/sqlite-busy.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2025-02-17T07:04:22+00:00</updated><author><name>Simon Willison</name></author><entry><title>What to do about SQLITE_BUSY errors despite setting a timeout</title><link href="https://simonwillison.net/2025/Feb/17/sqlite-busy/#atom-tag" rel="alternate"/><published>2025-02-17T07:04:22+00:00</published><updated>2025-02-17T07:04:22+00:00</updated><id>https://simonwillison.net/2025/Feb/17/sqlite-busy/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://berthub.eu/articles/posts/a-brief-post-on-sqlite3-database-locked-despite-timeout/"&gt;What to do about SQLITE_BUSY errors despite setting a timeout&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Bert Hubert takes on the challenge of explaining SQLite's single biggest footgun: in WAL mode you may see &lt;code&gt;SQLITE_BUSY&lt;/code&gt; errors even when you have a generous timeout set if a transaction attempts to obtain a write lock after initially running at least one &lt;code&gt;SELECT&lt;/code&gt;. The fix is to use &lt;code&gt;BEGIN IMMEDIATE&lt;/code&gt; if you know your transaction is going to make a write.&lt;/p&gt;
&lt;p&gt;Bert provides the clearest explanation I've seen yet of &lt;em&gt;why&lt;/em&gt; this is necessary:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When the transaction on the left wanted to upgrade itself to a read-write transaction, SQLite could not allow this since the transaction on the right might already have made changes that the transaction on the left had not yet seen.&lt;/p&gt;
&lt;p&gt;This in turn means that if left and right transactions would commit sequentially, the result would not necessarily be what would have happened if all statements had been executed sequentially within the same transaction.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I've written about this a few times before, so I just started a &lt;a href="https://simonwillison.net/tags/sqlite-busy/"&gt;sqlite-busy tag&lt;/a&gt; to collect my notes together on a single page.

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


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



</summary><category term="databases"/><category term="sqlite"/><category term="transactions"/><category term="sqlite-busy"/></entry><entry><title>Supercharge the One Person Framework with SQLite: Rails World 2024</title><link href="https://simonwillison.net/2024/Oct/16/sqlite-rails/#atom-tag" rel="alternate"/><published>2024-10-16T22:24:45+00:00</published><updated>2024-10-16T22:24:45+00:00</updated><id>https://simonwillison.net/2024/Oct/16/sqlite-rails/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://fractaledmind.github.io/2024/10/16/sqlite-supercharges-rails/"&gt;Supercharge the One Person Framework with SQLite: Rails World 2024&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Stephen Margheim shares an annotated transcript of the &lt;a href="https://www.youtube.com/watch?v=l56IBad-5aQ"&gt;YouTube video&lt;/a&gt; of his recent talk at this year's Rails World conference in Toronto.&lt;/p&gt;
&lt;p&gt;The Rails community is leaning &lt;em&gt;hard&lt;/em&gt; into SQLite right now. Stephen's talk is some of the most effective evangelism I've seen anywhere for SQLite as a production database for web applications, highlighting several new changes &lt;a href="https://simonwillison.net/2024/Oct/7/whats-new-in-ruby-on-rails-8/"&gt;in Rails 8&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;... there are two additions coming with Rails 8 that merit closer consideration. Because these changes make Rails 8 the first version of Rails (and, as far as I know, the first version of any web framework) that provides a fully production-ready SQLite experience out-of-the-box. &lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Those changes: &lt;a href="https://github.com/rails/rails/pull/50371"&gt;Ensure SQLite transaction default to IMMEDIATE mode&lt;/a&gt; to avoid "database is locked" errors when a deferred transaction attempts to upgrade itself with a write lock (discussed here &lt;a href="https://simonwillison.net/2024/Mar/31/optimizing-sqlite-for-servers/"&gt;previously&lt;/a&gt;, and added to Datasette 1.0a14 &lt;a href="https://simonwillison.net/2024/Aug/5/datasette-1a14/#sqlite-isolation-level-immediate-"&gt;in August&lt;/a&gt;) and &lt;a href="https://github.com/rails/rails/pull/51958"&gt;SQLite non-GVL-blocking, fair retry interval busy handler&lt;/a&gt; - a lower-level change that ensures SQLite's busy handler doesn't hold Ruby's Global VM Lock (the Ruby version of Python's GIL) while a thread is waiting on a SQLite lock.&lt;/p&gt;
&lt;p&gt;The rest of the talk makes a passionate and convincing case for SQLite as an option for production deployments, in line with the Rails goal of being a &lt;a href="https://world.hey.com/dhh/the-one-person-framework-711e6318"&gt;One Person Framework&lt;/a&gt; - "a toolkit so powerful that it allows a single individual to create modern applications upon which they might build a competitive business".&lt;/p&gt;
&lt;p&gt;&lt;img alt="Animated slide. The text Single-machine SQLite-only deployments can't serve production workloads is stamped with a big red Myth stamp" src="https://static.simonwillison.net/static/2024/sqlite-myth-smaller.gif" /&gt;&lt;/p&gt;
&lt;p&gt;Back in April Stephen published &lt;a href="https://fractaledmind.github.io/2024/04/15/sqlite-on-rails-the-how-and-why-of-optimal-performance/"&gt;SQLite on Rails: The how and why of optimal performance&lt;/a&gt; describing some of these challenges in more detail (including the best explanation I've seen anywhere of &lt;code&gt;BEGIN IMMEDIATE TRANSACTION&lt;/code&gt;) and promising:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Unfortunately, running SQLite on Rails out-of-the-box isn’t viable today. But, with a bit of tweaking and fine-tuning, you can ship a very performant, resilient Rails application with SQLite. And my personal goal for Rails 8 is to make the out-of-the-box experience fully production-ready.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It looks like he achieved that goal!

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/gil"&gt;gil&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rails"&gt;rails&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ruby"&gt;ruby&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite-busy"&gt;sqlite-busy&lt;/a&gt;&lt;/p&gt;



</summary><category term="gil"/><category term="rails"/><category term="ruby"/><category term="scaling"/><category term="sqlite"/><category term="sqlite-busy"/></entry><entry><title>What's New in Ruby on Rails 8</title><link href="https://simonwillison.net/2024/Oct/7/whats-new-in-ruby-on-rails-8/#atom-tag" rel="alternate"/><published>2024-10-07T19:17:47+00:00</published><updated>2024-10-07T19:17:47+00:00</updated><id>https://simonwillison.net/2024/Oct/7/whats-new-in-ruby-on-rails-8/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.appsignal.com/2024/10/07/whats-new-in-ruby-on-rails-8.html"&gt;What&amp;#x27;s New in Ruby on Rails 8&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Rails 8 takes SQLite from a lightweight development tool to a reliable choice for production use, thanks to extensive work on the SQLite adapter and Ruby driver.&lt;/p&gt;
&lt;p&gt;With the introduction of the solid adapters discussed above, SQLite now has the capability to power Action Cable, Rails.cache, and Active Job effectively, expanding its role beyond just prototyping or testing environments. [...]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Transactions default to &lt;code&gt;IMMEDIATE&lt;/code&gt; mode to improve concurrency.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Also included in Rails 8: &lt;a href="https://kamal-deploy.org/"&gt;Kamal&lt;/a&gt;, a new automated deployment system by 37signals for self-hosting web applications on hardware or virtual servers:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Kamal basically is Capistrano for Containers, without the need to carefully prepare servers in advance. No need to ensure that the servers have just the right version of Ruby or other dependencies you need. That all lives in the Docker image now. You can boot a brand new Ubuntu (or whatever) server, add it to the list of servers in Kamal, and it’ll be auto-provisioned with Docker, and run right away.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;More from the &lt;a href="https://rubyonrails.org/2024/9/27/rails-8-beta1-no-paas-required"&gt;official blog post about the release&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;At 37signals, we're building a growing suite of apps that use SQLite in production with &lt;a href="https://once.com/"&gt;ONCE&lt;/a&gt;. There are now thousands of installations of both &lt;a href="https://once.com/campfire"&gt;Campfire&lt;/a&gt; and &lt;a href="https://once.com/writebook"&gt;Writebook&lt;/a&gt; running in the wild that all run SQLite. This has meant a lot of real-world pressure on ensuring that Rails (and Ruby) is working that wonderful file-based database as well as it can be. Through proper defaults like WAL and IMMEDIATE mode. Special thanks to Stephen Margheim for &lt;a href="https://github.com/rails/rails/pulls?q=is%3Apr+author%3Afractaledmind"&gt;a slew of such improvements&lt;/a&gt; and Mike Dalessio for &lt;a href="https://github.com/sparklemotion/SQLite3-ruby/pull/558"&gt;solving a last-minute SQLite file corruption issue&lt;/a&gt; in the Ruby driver.&lt;/p&gt;
&lt;/blockquote&gt;

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/37signals"&gt;37signals&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rails"&gt;rails&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ruby"&gt;ruby&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/docker"&gt;docker&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite-busy"&gt;sqlite-busy&lt;/a&gt;&lt;/p&gt;



</summary><category term="37signals"/><category term="rails"/><category term="ruby"/><category term="sqlite"/><category term="docker"/><category term="sqlite-busy"/></entry><entry><title>Datasette 1.0a14: The annotated release notes</title><link href="https://simonwillison.net/2024/Aug/5/datasette-1a14/#atom-tag" rel="alternate"/><published>2024-08-05T23:20:01+00:00</published><updated>2024-08-05T23:20:01+00:00</updated><id>https://simonwillison.net/2024/Aug/5/datasette-1a14/#atom-tag</id><summary type="html">
    &lt;p&gt;Released today: &lt;a href="https://docs.datasette.io/en/1.0a14/changelog.html#a14-2024-08-05"&gt;Datasette 1.0a14&lt;/a&gt;. This alpha includes significant contributions from &lt;a href="https://alexgarcia.xyz/"&gt;Alex Garcia&lt;/a&gt;, including some backwards-incompatible changes in the run-up to the 1.0 release.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="#metadata-now-lives-in-a-database"&gt;Metadata now lives in a database&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="#datasette-remote-metadata-0-2a0"&gt;datasette-remote-metadata 0.2a0&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="#sqlite-isolation-level-immediate-"&gt;SQLite isolation_level="IMMEDIATE"&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="#updating-the-urls"&gt;Updating the URLs&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="#everything-else"&gt;Everything else&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="#tricks-to-help-construct-the-release-notes"&gt;Tricks to help construct the release notes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id="metadata-now-lives-in-a-database"&gt;Metadata now lives in a database&lt;/h4&gt;
&lt;p&gt;The biggest change in the alpha concerns how Datasette's &lt;a href="https://docs.datasette.io/en/latest/metadata.html#metadata"&gt;metadata system&lt;/a&gt; works.&lt;/p&gt;
&lt;p&gt;Datasette can record and serve metadata about the databases, tables and columns that it is serving. This includes things like the source of the data, the license it is made available under and descriptions of the tables and columns.&lt;/p&gt;
&lt;p&gt;Historically this has been powered by a &lt;code&gt;metadata.json&lt;/code&gt; file. Over time, this file grew to include all sorts of things that weren't strictly metadata - things like plugin configuration. Cleaning this up is a major breaking change for Datasette 1.0, and Alex has been working on this across several alphas.&lt;/p&gt;
&lt;p&gt;The latest alpha adds a new &lt;a href="https://docs.datasette.io/en/1.0a14/upgrade_guide.html"&gt;upgrade guide&lt;/a&gt; describing changes plugin authors will need to make to support the new metadata system.&lt;/p&gt;
&lt;p&gt;The big change in 1.0a14 is that metadata now lives in Datasette's hidden &lt;code&gt;_internal&lt;/code&gt; SQLite database, in four new tables called &lt;code&gt;metadata_instance&lt;/code&gt;, &lt;code&gt;metadata_databases&lt;/code&gt;, &lt;code&gt;metadata_resources&lt;/code&gt; and &lt;code&gt;metadata_columns&lt;/code&gt;. The schema for these &lt;a href="https://docs.datasette.io/en/1.0a14/internals.html#datasette-s-internal-database"&gt;is now included in the documentation&lt;/a&gt; (updated &lt;a href="https://github.com/simonw/datasette/blob/f6bd2bf8b025dcee49248ae7224e242b448f558c/docs/internals.rst?plain=1#L1363-L1366"&gt;using this Cog code&lt;/a&gt;), but rather than accessing those tables directly plugins are encouraged to use the new &lt;a href="https://docs.datasette.io/en/1.0a14/internals.html#getting-and-setting-metadata"&gt;set_*_metadata() and get_*_metadata() methods&lt;/a&gt; on the &lt;code&gt;Datasette&lt;/code&gt; class.&lt;/p&gt;
&lt;p&gt;I plan to use these new tables to build a new performant, paginated homepage that shows all of the databases and tables that Datasette is serving, complete with their metadata - without needing to make potentially hundreds of calls to the now-removed &lt;code&gt;get_metadata()&lt;/code&gt; plugin hook.&lt;/p&gt;
&lt;h4 id="datasette-remote-metadata-0-2a0"&gt;datasette-remote-metadata 0.2a0&lt;/h4&gt;
&lt;p&gt;When introducing new plugin internals like this it's always good to accompany them with a plugin that exercises them. &lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-remote-metadata"&gt;datasette-remote-metadata&lt;/a&gt;&lt;/strong&gt; is a few years old now, and provides a mechanism for hosting the metadata for a Datasette instance at a separate URL. This means you can deploy a stateless Datasette instance with a large database and later update the attached metadata without having to re-deploy the whole thing.&lt;/p&gt;
&lt;p&gt;I released &lt;a href="https://github.com/simonw/datasette-remote-metadata/releases/tag/0.2a0"&gt;a new alpha&lt;/a&gt; of that plugin which &lt;a href="https://github.com/simonw/datasette-remote-metadata/issues/4"&gt;switches over to the new metadata mechanism&lt;/a&gt;. The core code ended up looking like this, imitating &lt;a href="https://github.com/simonw/datasette/blob/f6bd2bf8/datasette/app.py#L446-L472"&gt;code Alex wrote&lt;/a&gt; for Datasette Core:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;async&lt;/span&gt; &lt;span class="pl-k"&gt;def&lt;/span&gt; &lt;span class="pl-en"&gt;apply_metadata&lt;/span&gt;(&lt;span class="pl-s1"&gt;datasette&lt;/span&gt;, &lt;span class="pl-s1"&gt;metadata_dict&lt;/span&gt;):
    &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;key&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;metadata_dict&lt;/span&gt; &lt;span class="pl-c1"&gt;or&lt;/span&gt; {}:
        &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;key&lt;/span&gt; &lt;span class="pl-c1"&gt;==&lt;/span&gt; &lt;span class="pl-s"&gt;"databases"&lt;/span&gt;:
            &lt;span class="pl-k"&gt;continue&lt;/span&gt;
        &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;datasette&lt;/span&gt;.&lt;span class="pl-en"&gt;set_instance_metadata&lt;/span&gt;(&lt;span class="pl-s1"&gt;key&lt;/span&gt;, &lt;span class="pl-s1"&gt;metadata_dict&lt;/span&gt;[&lt;span class="pl-s1"&gt;key&lt;/span&gt;])
    &lt;span class="pl-c"&gt;# database-level&lt;/span&gt;
    &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;dbname&lt;/span&gt;, &lt;span class="pl-s1"&gt;db&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;metadata_dict&lt;/span&gt;.&lt;span class="pl-en"&gt;get&lt;/span&gt;(&lt;span class="pl-s"&gt;"databases"&lt;/span&gt;, {}).&lt;span class="pl-en"&gt;items&lt;/span&gt;():
        &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;key&lt;/span&gt;, &lt;span class="pl-s1"&gt;value&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;db&lt;/span&gt;.&lt;span class="pl-en"&gt;items&lt;/span&gt;():
            &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;key&lt;/span&gt; &lt;span class="pl-c1"&gt;==&lt;/span&gt; &lt;span class="pl-s"&gt;"tables"&lt;/span&gt;:
                &lt;span class="pl-k"&gt;continue&lt;/span&gt;
            &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;datasette&lt;/span&gt;.&lt;span class="pl-en"&gt;set_database_metadata&lt;/span&gt;(&lt;span class="pl-s1"&gt;dbname&lt;/span&gt;, &lt;span class="pl-s1"&gt;key&lt;/span&gt;, &lt;span class="pl-s1"&gt;value&lt;/span&gt;)
        &lt;span class="pl-c"&gt;# table-level&lt;/span&gt;
        &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;tablename&lt;/span&gt;, &lt;span class="pl-s1"&gt;table&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;db&lt;/span&gt;.&lt;span class="pl-en"&gt;get&lt;/span&gt;(&lt;span class="pl-s"&gt;"tables"&lt;/span&gt;, {}).&lt;span class="pl-en"&gt;items&lt;/span&gt;():
            &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;key&lt;/span&gt;, &lt;span class="pl-s1"&gt;value&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;table&lt;/span&gt;.&lt;span class="pl-en"&gt;items&lt;/span&gt;():
                &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;key&lt;/span&gt; &lt;span class="pl-c1"&gt;==&lt;/span&gt; &lt;span class="pl-s"&gt;"columns"&lt;/span&gt;:
                    &lt;span class="pl-k"&gt;continue&lt;/span&gt;
                &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;datasette&lt;/span&gt;.&lt;span class="pl-en"&gt;set_resource_metadata&lt;/span&gt;(&lt;span class="pl-s1"&gt;dbname&lt;/span&gt;, &lt;span class="pl-s1"&gt;tablename&lt;/span&gt;, &lt;span class="pl-s1"&gt;key&lt;/span&gt;, &lt;span class="pl-s1"&gt;value&lt;/span&gt;)
            &lt;span class="pl-c"&gt;# column-level&lt;/span&gt;
            &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;columnname&lt;/span&gt;, &lt;span class="pl-s1"&gt;column_description&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;table&lt;/span&gt;.&lt;span class="pl-en"&gt;get&lt;/span&gt;(&lt;span class="pl-s"&gt;"columns"&lt;/span&gt;, {}).&lt;span class="pl-en"&gt;items&lt;/span&gt;():
                &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;datasette&lt;/span&gt;.&lt;span class="pl-en"&gt;set_column_metadata&lt;/span&gt;(
                    &lt;span class="pl-s1"&gt;dbname&lt;/span&gt;, &lt;span class="pl-s1"&gt;tablename&lt;/span&gt;, &lt;span class="pl-s1"&gt;columnname&lt;/span&gt;, &lt;span class="pl-s"&gt;"description"&lt;/span&gt;, &lt;span class="pl-s1"&gt;column_description&lt;/span&gt;
                )&lt;/pre&gt;
&lt;h4 id="sqlite-isolation-level-immediate-"&gt;SQLite isolation_level="IMMEDIATE"&lt;/h4&gt;
&lt;p&gt;Sylvain Kerkour wrote about the &lt;a href="https://kerkour.com/sqlite-for-servers#use-immediate-transactions"&gt;benefits of IMMEDIATE transactions&lt;/a&gt; back in February. The key issue here is that SQLite defaults to starting transactions in &lt;code&gt;DEFERRED&lt;/code&gt; mode, which can lead to &lt;code&gt;SQLITE_BUSY&lt;/code&gt; errors if a transaction is upgraded to a write transaction mid-flight. Starting in &lt;code&gt;IMMEDIATE&lt;/code&gt; mode for Datasette's dedicated write connection should help avoid this.&lt;/p&gt;
&lt;p&gt;Frustratingly I &lt;a href="https://github.com/simonw/datasette/issues/2358"&gt;failed to replicate&lt;/a&gt; the underlying problem in my own tests, despite having anecdotally seen it happen in the past.&lt;/p&gt;
&lt;p&gt;After spending more time than I had budgeted for on this, I decided to ship it as an alpha to get it properly exercised before the 1.0 stable release.&lt;/p&gt;
&lt;h4 id="updating-the-urls"&gt;Updating the URLs&lt;/h4&gt;
&lt;p&gt;Here's another change that was important to get out before 1.0.&lt;/p&gt;
&lt;p&gt;Datasette's URL design had a subtle blemish. The following page had two potential meanings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/databasename&lt;/code&gt; - list all of the tables in the specified database&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/databasename?sql=&lt;/code&gt; - execute an arbitrary SQL query against that database&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This also meant that the JSON structure returned by &lt;code&gt;/database.json&lt;/code&gt; v.s. &lt;code&gt;/database.json?sql=&lt;/code&gt; was different.&lt;/p&gt;
&lt;p&gt;Alex and I decided to fix that. Alex laid out the new design in &lt;a href="https://github.com/simonw/datasette/issues/2360"&gt;issue #2360&lt;/a&gt; - there are quite a few other changes, but the big one is that we are splitting out the SQL query interface to a new URL: &lt;code&gt;/databasename/-/query?sql=&lt;/code&gt; - or &lt;code&gt;/databasename/-/query.json?sql=&lt;/code&gt; for the JSON API.&lt;/p&gt;
&lt;p&gt;We've added redirects from the old URLs to the new ones, so existing links should continue to work.&lt;/p&gt;
&lt;h4 id="everything-else"&gt;Everything else&lt;/h4&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Fix for a bug where canned queries with named parameters could fail against SQLite 3.46. (&lt;a href="https://github.com/simonw/datasette/issues/2353"&gt;#2353&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;This reflects a bug fix that went out in &lt;a href="https://docs.datasette.io/en/stable/changelog.html#v0-64-7"&gt;Datasette 0.64.7&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Datasette now serves &lt;code&gt;E-Tag&lt;/code&gt; headers for static files. Thanks, &lt;a href="https://github.com/redraw"&gt;Agustin Bacigalup&lt;/a&gt;. (&lt;a href="https://github.com/simonw/datasette/pull/2306"&gt;#2306&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;There's still more to be done making Datasette play well with caches, but this is a great, low-risk start.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Dropdown menus now use a &lt;code&gt;z-index&lt;/code&gt; that should avoid them being hidden by plugins. (&lt;a href="https://github.com/simonw/datasette/issues/2311"&gt;#2311&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;A cosmetic bug that showed up on Datasette Cloud when using the &lt;a href="https://datasette.io/plugins/datasette-cluster-map"&gt;datasette-cluster-map&lt;/a&gt; plugin.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Incorrect table and row names are no longer reflected back on the resulting 404 page. (&lt;a href="https://github.com/simonw/datasette/issues/2359"&gt;#2359&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;This was reported as a potential security issue. The table names were correctly escaped, so this wasn't an XSS, but there was still potential for confusion if an attacker constructed a URL along the lines of &lt;code&gt;/database-does-not-exist-visit-www.attacker.com-for-more-info&lt;/code&gt;. A similar fix went out in &lt;a href="https://docs.datasette.io/en/stable/changelog.html#v0-64-8"&gt;Datasette 0.64.8&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Improved documentation for async usage of the &lt;a href="https://docs.datasette.io/en/latest/plugin_hooks.html#plugin-hook-track-event"&gt;track_event(datasette, event)&lt;/a&gt; hook. (&lt;a href="https://github.com/simonw/datasette/issues/2319"&gt;#2319&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Fixed some HTTPX deprecation warnings. (&lt;a href="https://github.com/simonw/datasette/issues/2307"&gt;#2307&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Datasette now serves a &lt;code&gt;&amp;lt;html lange="en"&amp;gt;&lt;/code&gt; attribute. Thanks, &lt;a href="https://github.com/CharlesNepote"&gt;Charles Nepote&lt;/a&gt;. (&lt;a href="https://github.com/simonw/datasette/issues/2348"&gt;#2348&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Datasette's automated tests now run against the maximum and minimum supported versions of SQLite: 3.25 (from September 2018) and 3.46 (from May 2024). Thanks, Alex Garcia. (&lt;a href="https://github.com/simonw/datasette/pull/2352"&gt;#2352&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Fixed an issue where clicking twice on the URL output by &lt;code&gt;datasette --root&lt;/code&gt; produced a confusing error. (&lt;a href="https://github.com/simonw/datasette/issues/2375"&gt;#2375&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h4 id="tricks-to-help-construct-the-release-notes"&gt;Tricks to help construct the release notes&lt;/h4&gt;
&lt;p&gt;I still write the Datasette release notes entirely by hand (aside from a few words auto-completed by GitHub Copilot) - I find the process of writing them to be really useful as a way to construct a final review of everything before it goes out.&lt;/p&gt;
&lt;p&gt;I used a couple of tricks to help this time. I always start my longer release notes &lt;a href="https://github.com/simonw/datasette/issues/2381"&gt;with an issue&lt;/a&gt;. The GitHub &lt;a href="https://github.com/simonw/datasette/compare/1.0a13...2ad51baa31bfba7940c739e99d4270f563a77290"&gt;diff view&lt;/a&gt; is useful for seeing what's changed since the last release, but I took it a step further this time with the following shell command:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;git log --pretty=format:&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;- %ad: %s %h&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; --date=short --reverse 1.0a13...81b68a14&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This outputs a summary of each commit in the range, looking like this (truncated):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- 2024-03-12: Added two things I left out of the 1.0a13 release notes 8b6f155b
- 2024-03-15: Fix httpx warning about app=self.app, refs #2307 5af68377
- 2024-03-15: Fixed cookies= httpx warning, refs #2307 54f5604c
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Crucially, the syntax of this output is in GitHub Flavored Markdown - and pasting it into an issue comment causes both the issue references and the commit hashes to be expanded into links that &lt;a href="https://github.com/simonw/datasette/issues/2381#issuecomment-2269759462"&gt;look like this&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/release-notes-issues-markdown.jpg" alt="2024-03-12: Added two things I left out of the 1.0a13 release notes 8b6f155 2024-03-15: Fix httpx warning about app=self.app, refs Fix httpx deprecation warnings #2307 5af6837 2024-03-15: Fixed cookies= httpx warning, refs Fix httpx deprecation warnings #2307 54f5604" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;It's a neat way to get a quick review of what's changed, and also means that those issues will automatically link back to the new issue where I'm constructing the release notes.&lt;/p&gt;
&lt;p&gt;I wrote this up &lt;a href="https://til.simonwillison.net/github/release-note-assistance"&gt;in a TIL here&lt;/a&gt;, along with another trick I used where I used &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; to get Claude 3.5 Sonnet to summarize my changes for me:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;curl &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;https://github.com/simonw/datasette/compare/1.0a13...2ad51baa3.diff&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; \
  &lt;span class="pl-k"&gt;|&lt;/span&gt; llm -m claude-3.5-sonnet --system \
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;generate a short summary of these changes, then a bullet point list of detailed release notes&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&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/releases"&gt;releases&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/annotated-release-notes"&gt;annotated-release-notes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm"&gt;llm&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite-busy"&gt;sqlite-busy&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="projects"/><category term="releases"/><category term="sqlite"/><category term="datasette"/><category term="annotated-release-notes"/><category term="llm"/><category term="sqlite-busy"/></entry><entry><title>Optimal SQLite settings for Django</title><link href="https://simonwillison.net/2024/Jun/13/optimal-sqlite-settings-for-django/#atom-tag" rel="alternate"/><published>2024-06-13T05:04:36+00:00</published><updated>2024-06-13T05:04:36+00:00</updated><id>https://simonwillison.net/2024/Jun/13/optimal-sqlite-settings-for-django/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://gcollazo.com/optimal-sqlite-settings-for-django/"&gt;Optimal SQLite settings for Django&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Giovanni Collazo put the work in to figure out settings to make SQLite work well for production Django workloads. WAL mode and a &lt;code&gt;busy_timeout&lt;/code&gt; of 5000 make sense, but the most interesting recommendation here is &lt;code&gt;"transaction_mode": "IMMEDIATE"&lt;/code&gt; to avoid locking errors when a transaction is upgraded to a write transaction.&lt;/p&gt;
&lt;p&gt;Giovanni's configuration depends on the new &lt;code&gt;"init_command"&lt;/code&gt; support for SQLite PRAGMA options &lt;a href="https://docs.djangoproject.com/en/5.1/ref/databases/#setting-pragma-options"&gt;introduced in Django 5.1alpha&lt;/a&gt;.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://lobste.rs/s/9lchst/optimal_sqlite_settings_for_django"&gt;Lobste.rs&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


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



</summary><category term="django"/><category term="sqlite"/><category term="sqlite-busy"/></entry><entry><title>Optimizing SQLite for servers</title><link href="https://simonwillison.net/2024/Mar/31/optimizing-sqlite-for-servers/#atom-tag" rel="alternate"/><published>2024-03-31T20:16:23+00:00</published><updated>2024-03-31T20:16:23+00:00</updated><id>https://simonwillison.net/2024/Mar/31/optimizing-sqlite-for-servers/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://kerkour.com/sqlite-for-servers"&gt;Optimizing SQLite for servers&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Sylvain Kerkour's comprehensive set of lessons learned running SQLite for server-based applications.&lt;/p&gt;
&lt;p&gt;There's a lot of useful stuff in here, including detailed coverage of the different recommended &lt;code&gt;PRAGMA&lt;/code&gt; settings.&lt;/p&gt;
&lt;p&gt;There was also a tip I haven't seen before about &lt;code&gt;BEGIN IMMEDIATE&lt;/code&gt; transactions:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;By default, SQLite starts transactions in &lt;code&gt;DEFERRED&lt;/code&gt; mode: they are considered read only. They are upgraded to a write transaction that requires a database lock in-flight, when query containing a write/update/delete statement is issued.&lt;/p&gt;
&lt;p&gt;The problem is that by upgrading a transaction after it has started, SQLite will immediately return a &lt;code&gt;SQLITE_BUSY&lt;/code&gt; error without respecting the &lt;code&gt;busy_timeout&lt;/code&gt; previously mentioned, if the database is already locked by another connection.&lt;/p&gt;
&lt;p&gt;This is why you should start your transactions with &lt;code&gt;BEGIN IMMEDIATE&lt;/code&gt; instead of only &lt;code&gt;BEGIN&lt;/code&gt;. If the database is locked when the transaction starts, SQLite will respect &lt;code&gt;busy_timeout&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

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


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



</summary><category term="databases"/><category term="performance"/><category term="sql"/><category term="sqlite"/><category term="sqlite-busy"/></entry></feed>