<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Go on Chris — freshteapot</title><link>https://freshteapot.net/tags/go/</link><description>Chris makes things, ships them, and writes down what happened.</description><generator>Hugo 0.163.3</generator><language>en</language><atom:link href="https://freshteapot.net/tags/go/" rel="self" type="application/rss+xml"/><item><title>When SQLite connection string order is API semantics</title><link>https://freshteapot.net/writing/sqlite-connection-string-order/</link><pubDate>Tue, 08 Sep 2026 00:00:00 +0000</pubDate><guid>https://freshteapot.net/writing/sqlite-connection-string-order/</guid><description>How two Go SQLite drivers handle pragma order, and why busy_timeout belongs before journal_mode.</description><content:encoded><![CDATA[<p>I was configuring two Go services to share a SQLite database. The connection string included a busy timeout and WAL mode:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">?_busy_timeout=10000&amp;_journal_mode=WAL
</span></span></code></pre></div><p>Then I found <a href="https://github.com/pocketbase/pocketbase/blob/bc8ffed4e7265a70a6e8de76c0b0b48b945e19ef/core/db_connect.go#L11-L15">a comment in PocketBase</a>: <code>busy_timeout</code> must be set first so that the connection will wait if switching to WAL encounters a busy database.</p>
<p>This is one of those cases where string order can be API semantics, not just presentation. The precise behaviour, however, belongs to the database driver.</p>
<h2 id="two-drivers-two-interfaces">Two drivers, two interfaces</h2>
<p>PocketBase uses <code>modernc.org/sqlite</code> and supplies repeated generic <code>_pragma</code> parameters:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">?_pragma=busy_timeout(10000)&amp;_pragma=journal_mode(WAL)
</span></span></code></pre></div><p>In <a href="https://github.com/modernc-org/sqlite/blob/v1.57.0/sqlite.go#L432-L456"><code>modernc.org/sqlite</code> v1.57.0</a>, the driver collects those values, moves <code>busy_timeout</code> first, sorts the rest, and then executes them. The order written by the caller expresses the intention, but this version of the driver also enforces the important part.</p>
<p><code>github.com/mattn/go-sqlite3</code> presents dedicated options instead:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">?_busy_timeout=10000&amp;_journal_mode=WAL
</span></span></code></pre></div><p>It <a href="https://github.com/mattn/go-sqlite3/blob/v1.14.49/sqlite3.go#L1217-L1221">parses the query string into named parameters</a>, then applies them in its own fixed order. It sets <a href="https://github.com/mattn/go-sqlite3/blob/v1.14.49/sqlite3.go#L1622-L1625"><code>PRAGMA busy_timeout</code></a> well before <a href="https://github.com/mattn/go-sqlite3/blob/v1.14.49/sqlite3.go#L1822-L1826"><code>PRAGMA journal_mode</code></a>. Swapping those two options in the original string does not change their execution order.</p>
<p>The lesson is small but useful: do not infer connection initialisation order from the appearance of a DSN. Read the driver. PocketBase&rsquo;s comment explained why the ordering matters; the two driver implementations showed where that ordering actually comes from.</p>
<h2 id="a-short-history">A short history</h2>
<p>There is a direct trail for the behaviour in <code>modernc.org/sqlite</code>.</p>
<p>In November 2024, a user reported intermittent <code>SQLITE_BUSY</code> errors while opening pooled connections. Their DSN placed <code>busy_timeout</code> after <code>journal_mode(WAL)</code>. A new connection could therefore attempt to set WAL before installing its busy handler, and fail immediately if another connection held the required lock.</p>
<p>The reporter compared the behaviour with <code>mattn/go-sqlite3</code>. Mattn&rsquo;s driver already applied <code>busy_timeout</code> early, regardless of its position in the DSN. In <a href="https://gitlab.com/cznic/sqlite/-/work_items/198#note_2233406358">the diagnostic comment</a>, they showed that applying <code>busy_timeout</code> first stopped the errors. They then <a href="https://gitlab.com/cznic/sqlite/-/work_items/198#note_2233423463">proposed a consistent pragma order</a>. The maintainer <a href="https://gitlab.com/cznic/sqlite/-/work_items/198#note_2233758420">confirmed that the suggestion had been incorporated</a> in the fix titled <a href="https://gitlab.com/cznic/sqlite/-/commit/2573fa9f372ad9324d129e65f8f79d044cca2f92"><code>apply busy_timeout pragma early</code></a>.</p>
<p>The root cause lies in SQLite itself. <a href="https://www.sqlite.org/pragma.html#pragma_busy_timeout"><code>busy_timeout</code></a> installs a busy handler on a connection. Changing <a href="https://www.sqlite.org/pragma.html#pragma_journal_mode"><code>journal_mode</code></a> changes persistent database state and may need a lock. The handler must be ready before that work begins.</p>
<p>The sequence is therefore: SQLite&rsquo;s locking semantics, Mattn&rsquo;s established ordering, a reproducible failure in <code>modernc</code>, an upstream fix, and PocketBase&rsquo;s concise comment preserving the reason.</p>
<h2 id="searched-for">Searched for</h2>
<ul>
<li><code>busy_timeout sqlite</code></li>
<li><code>modernc sqlite pragma order SQLITE_BUSY</code></li>
<li><code>mattn go-sqlite3 busy_timeout journal_mode order</code></li>
</ul>
<h2 id="references">References</h2>
<ul>
<li><a href="https://github.com/pocketbase/pocketbase/blob/bc8ffed4e7265a70a6e8de76c0b0b48b945e19ef/core/db_connect.go#L11-L15">PocketBase: SQLite connection setup and ordering comment</a></li>
<li><a href="https://github.com/modernc-org/sqlite/blob/v1.57.0/sqlite.go#L432-L456"><code>modernc.org/sqlite</code> v1.57.0: pragma ordering</a></li>
<li><a href="https://gitlab.com/cznic/sqlite/-/work_items/198#note_2233406358"><code>modernc.org/sqlite</code> issue 198: diagnosis and comparison with Mattn</a></li>
<li><a href="https://gitlab.com/cznic/sqlite/-/work_items/198#note_2233423463"><code>modernc.org/sqlite</code> issue 198: proposed pragma ordering</a></li>
<li><a href="https://gitlab.com/cznic/sqlite/-/work_items/198#note_2233758420"><code>modernc.org/sqlite</code> issue 198: maintainer&rsquo;s response</a></li>
<li><a href="https://gitlab.com/cznic/sqlite/-/commit/2573fa9f372ad9324d129e65f8f79d044cca2f92"><code>modernc.org/sqlite</code>: <code>apply busy_timeout pragma early</code></a></li>
<li><a href="https://github.com/mattn/go-sqlite3/blob/v1.14.49/sqlite3.go#L1217-L1221"><code>mattn/go-sqlite3</code> v1.14.49: parsing DSN parameters</a></li>
<li><a href="https://github.com/mattn/go-sqlite3/blob/v1.14.49/sqlite3.go#L1622-L1625"><code>mattn/go-sqlite3</code> v1.14.49: applying <code>busy_timeout</code></a></li>
<li><a href="https://github.com/mattn/go-sqlite3/blob/v1.14.49/sqlite3.go#L1822-L1826"><code>mattn/go-sqlite3</code> v1.14.49: applying <code>journal_mode</code></a></li>
<li><a href="https://www.sqlite.org/pragma.html#pragma_busy_timeout">SQLite: <code>PRAGMA busy_timeout</code></a></li>
<li><a href="https://www.sqlite.org/pragma.html#pragma_journal_mode">SQLite: <code>PRAGMA journal_mode</code></a></li>
</ul>
]]></content:encoded></item></channel></rss>