<?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>SQLite on Tarragon</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/</link><description>Recent content in SQLite on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 13 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/index.xml" rel="self" type="application/rss+xml"/><item><title>PostgreSQL to SQLite Simplification</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/migrate-from-postgresql-simplification/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/migrate-from-postgresql-simplification/</guid><description>&lt;p>PostgreSQL to SQLite simplification 的核心責任是處理反向路線：服務責任縮小後，評估 SQLite 是否能降低操作成本。這條路線適合 single-user app、CLI、desktop app、內部工具、read-mostly artifact store、demo environment、local-first prototype 或 edge-local utility。&lt;/p>
&lt;p>本文的判讀錨點是：降級到 SQLite 是責任縮小，也是讓資料模型回到 single-process / file-owned / local-state 的工程選擇。只要正式需求從 multi-user server DB 回到這個範圍，SQLite 可以提供更低元件數、更容易搬移與更低維護成本。&lt;/p>
&lt;h2 id="simplification-drivers">Simplification Drivers&lt;/h2>
&lt;p>Simplification drivers 的核心責任是確認 PostgreSQL 的能力已超過服務需求。若 server DB 的 HA、role、replica、pool、vacuum、PITR、schema governance 都變成維運負擔，而產品只需要單一 process 持有資料，就可以評估 SQLite。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Driver&lt;/th>
 &lt;th>代表情境&lt;/th>
 &lt;th>SQLite 帶來的收益&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Single-user app&lt;/td>
 &lt;td>desktop、CLI、local admin tool&lt;/td>
 &lt;td>file portability、offline use&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Read-mostly artifact&lt;/td>
 &lt;td>build metadata、catalog snapshot&lt;/td>
 &lt;td>deployment simple、低 runtime dependency&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Internal tool&lt;/td>
 &lt;td>小團隊使用、資料量小、低寫入&lt;/td>
 &lt;td>降低 DB server operation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Demo / fixture&lt;/td>
 &lt;td>每個 environment 一份可重建資料&lt;/td>
 &lt;td>quick reset、deterministic seed&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Edge-local utility&lt;/td>
 &lt;td>request-local / device-local state&lt;/td>
 &lt;td>low latency、local ownership&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Driver 要連到 ownership。SQLite 適合「這份資料由某個 process / device / artifact 明確持有」；若資料仍屬於多服務共同真相，保留 PostgreSQL 或改成 managed SQL 會更穩定。&lt;/p>
&lt;h2 id="no-go-conditions">No-Go Conditions&lt;/h2>
&lt;p>No-go condition 的核心責任是保護仍需要 server DB 的服務。若 PostgreSQL 的核心能力仍被業務依賴，遷到 SQLite 會把風險轉移到 application code、file backup 與人工流程。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>No-go 訊號&lt;/th>
 &lt;th>代表責任&lt;/th>
 &lt;th>保留路由&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>多 tenant 與 centralized permission&lt;/td>
 &lt;td>DB role、grant、audit 仍有價值&lt;/td>
 &lt;td>PostgreSQL&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>多 instance concurrent writer&lt;/td>
 &lt;td>SQLite writer boundary 壓力過高&lt;/td>
 &lt;td>PostgreSQL / MySQL&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>PITR / HA 是合約要求&lt;/td>
 &lt;td>server DB operation 是正式責任&lt;/td>
 &lt;td>Managed PostgreSQL / Aurora&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Analyst / job 直接查 DB&lt;/td>
 &lt;td>access control 與 query isolation&lt;/td>
 &lt;td>PostgreSQL read replica / warehouse&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cross-service source of truth&lt;/td>
 &lt;td>單檔 ownership 與服務邊界衝突&lt;/td>
 &lt;td>保留 server DB 或拆 bounded context&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>No-go 條件要寫進 migration proposal。Simplification 的目標是降低操作成本；若降級後要用大量自製機制補回 role、audit、HA 與 concurrent write，成本會回到系統裡。&lt;/p>
&lt;h2 id="diff-audit">Diff Audit&lt;/h2>
&lt;p>Diff audit 的核心責任是把 PostgreSQL 語意縮到 SQLite 可以清楚承擔的範圍。PostgreSQL extension、function、type、index、constraint、sequence、view、trigger、role 與 transaction behavior 都要盤點。&lt;/p></description><content:encoded><![CDATA[<p>PostgreSQL to SQLite simplification 的核心責任是處理反向路線：服務責任縮小後，評估 SQLite 是否能降低操作成本。這條路線適合 single-user app、CLI、desktop app、內部工具、read-mostly artifact store、demo environment、local-first prototype 或 edge-local utility。</p>
<p>本文的判讀錨點是：降級到 SQLite 是責任縮小，也是讓資料模型回到 single-process / file-owned / local-state 的工程選擇。只要正式需求從 multi-user server DB 回到這個範圍，SQLite 可以提供更低元件數、更容易搬移與更低維護成本。</p>
<h2 id="simplification-drivers">Simplification Drivers</h2>
<p>Simplification drivers 的核心責任是確認 PostgreSQL 的能力已超過服務需求。若 server DB 的 HA、role、replica、pool、vacuum、PITR、schema governance 都變成維運負擔，而產品只需要單一 process 持有資料，就可以評估 SQLite。</p>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>代表情境</th>
          <th>SQLite 帶來的收益</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Single-user app</td>
          <td>desktop、CLI、local admin tool</td>
          <td>file portability、offline use</td>
      </tr>
      <tr>
          <td>Read-mostly artifact</td>
          <td>build metadata、catalog snapshot</td>
          <td>deployment simple、低 runtime dependency</td>
      </tr>
      <tr>
          <td>Internal tool</td>
          <td>小團隊使用、資料量小、低寫入</td>
          <td>降低 DB server operation</td>
      </tr>
      <tr>
          <td>Demo / fixture</td>
          <td>每個 environment 一份可重建資料</td>
          <td>quick reset、deterministic seed</td>
      </tr>
      <tr>
          <td>Edge-local utility</td>
          <td>request-local / device-local state</td>
          <td>low latency、local ownership</td>
      </tr>
  </tbody>
</table>
<p>Driver 要連到 ownership。SQLite 適合「這份資料由某個 process / device / artifact 明確持有」；若資料仍屬於多服務共同真相，保留 PostgreSQL 或改成 managed SQL 會更穩定。</p>
<h2 id="no-go-conditions">No-Go Conditions</h2>
<p>No-go condition 的核心責任是保護仍需要 server DB 的服務。若 PostgreSQL 的核心能力仍被業務依賴，遷到 SQLite 會把風險轉移到 application code、file backup 與人工流程。</p>
<table>
  <thead>
      <tr>
          <th>No-go 訊號</th>
          <th>代表責任</th>
          <th>保留路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多 tenant 與 centralized permission</td>
          <td>DB role、grant、audit 仍有價值</td>
          <td>PostgreSQL</td>
      </tr>
      <tr>
          <td>多 instance concurrent writer</td>
          <td>SQLite writer boundary 壓力過高</td>
          <td>PostgreSQL / MySQL</td>
      </tr>
      <tr>
          <td>PITR / HA 是合約要求</td>
          <td>server DB operation 是正式責任</td>
          <td>Managed PostgreSQL / Aurora</td>
      </tr>
      <tr>
          <td>Analyst / job 直接查 DB</td>
          <td>access control 與 query isolation</td>
          <td>PostgreSQL read replica / warehouse</td>
      </tr>
      <tr>
          <td>Cross-service source of truth</td>
          <td>單檔 ownership 與服務邊界衝突</td>
          <td>保留 server DB 或拆 bounded context</td>
      </tr>
  </tbody>
</table>
<p>No-go 條件要寫進 migration proposal。Simplification 的目標是降低操作成本；若降級後要用大量自製機制補回 role、audit、HA 與 concurrent write，成本會回到系統裡。</p>
<h2 id="diff-audit">Diff Audit</h2>
<p>Diff audit 的核心責任是把 PostgreSQL 語意縮到 SQLite 可以清楚承擔的範圍。PostgreSQL extension、function、type、index、constraint、sequence、view、trigger、role 與 transaction behavior 都要盤點。</p>
<table>
  <thead>
      <tr>
          <th>PostgreSQL feature</th>
          <th>SQLite 轉換策略</th>
          <th>審查問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>timestamptz</code></td>
          <td>UTC ISO text 或 integer epoch</td>
          <td>timezone policy 是否固定</td>
      </tr>
      <tr>
          <td><code>jsonb</code> + GIN</td>
          <td>JSON text + limited query / app filter</td>
          <td>query 是否仍需 index</td>
      </tr>
      <tr>
          <td>Sequence / identity</td>
          <td>INTEGER PRIMARY KEY 或 app ID</td>
          <td>id stability 與 import collision</td>
      </tr>
      <tr>
          <td>Partial index</td>
          <td>SQLite partial index</td>
          <td>predicate 與 query planner 是否對齊</td>
      </tr>
      <tr>
          <td>Role / grant</td>
          <td>filesystem permission + app auth</td>
          <td>權限是否可移到 application boundary</td>
      </tr>
      <tr>
          <td>Extension</td>
          <td>application logic 或放棄 feature</td>
          <td>feature 是否仍是正式需求</td>
      </tr>
  </tbody>
</table>
<p>Diff audit 的輸出是一份保留 / 移除 / 改寫清單。每個 PostgreSQL feature 都要回答：這是正式需求、歷史殘留，還是可以移到 application layer 的便利功能。</p>
<h2 id="phase-plan">Phase Plan</h2>
<p>Phase plan 的核心責任是把 server DB 退場變成可回復流程。反向 migration 要超過一次性 dump：先收斂寫入、建立 SQLite schema、匯入資料、跑 adapter test、演練 backup，再退役 PostgreSQL。</p>
<table>
  <thead>
      <tr>
          <th>Phase</th>
          <th>目的</th>
          <th>Evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Scope reduction</td>
          <td>確認資料責任已縮小</td>
          <td>ownership doc、no-go review</td>
      </tr>
      <tr>
          <td>Schema rewrite</td>
          <td>建立 SQLite schema</td>
          <td>migration dry run、STRICT / constraint</td>
      </tr>
      <tr>
          <td>Data export</td>
          <td>從 PostgreSQL 匯出 snapshot</td>
          <td>row count、checksum、dump metadata</td>
      </tr>
      <tr>
          <td>Data import</td>
          <td>寫入 SQLite file</td>
          <td>integrity check、foreign key check</td>
      </tr>
      <tr>
          <td>Adapter switch</td>
          <td>app 改用 SQLite repository</td>
          <td>contract test、error mapping</td>
      </tr>
      <tr>
          <td>Backup runbook</td>
          <td>建立 file lifecycle evidence</td>
          <td>backup restore drill</td>
      </tr>
      <tr>
          <td>Server retirement</td>
          <td>關閉 PostgreSQL 寫入與 credential</td>
          <td>retention、credential removal、incident route</td>
      </tr>
  </tbody>
</table>
<p>Scope reduction 是第一關。若資料仍被多個服務寫入，應先拆出 bounded context 或建立 event / export boundary；SQLite file 才能成為明確 owned artifact。</p>
<h2 id="data-movement">Data Movement</h2>
<p>Data movement 的核心責任是把 PostgreSQL snapshot 轉成 SQLite file 並保留驗證。可用 <code>COPY</code> / CSV、application ETL 或 dedicated migration tool；選擇取決於 type conversion 與資料量。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">psql <span class="s2">&#34;</span><span class="nv">$DATABASE_URL</span><span class="s2">&#34;</span> -c <span class="s2">&#34;\\copy orders TO &#39;orders.csv&#39; CSV HEADER&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sqlite3 app.db <span class="s2">&#34;.mode csv&#34;</span> <span class="s2">&#34;.import --skip 1 orders.csv orders&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">sqlite3 app.db <span class="s2">&#34;PRAGMA integrity_check;&#34;</span></span></span></code></pre></div><p>這段命令是教學骨架。正式流程要處理 NULL、delimiter、timezone、numeric precision、FK order、transaction、temporary disk、sensitive data 與 import log。</p>
<p>Import 後要跑三種 evidence：database integrity、row count / checksum、business invariant。Business invariant 例如 active user count、total balance、latest event id、pending job count；這些比單純 row count 更能抓到語意錯誤。</p>
<h2 id="runbook-shift">Runbook Shift</h2>
<p>Runbook shift 的核心責任是把 PostgreSQL operation 移轉成 SQLite file operation。Server DB 的 backup / role / monitoring 退場後，要補上 SQLite 的 backup、restore、file permission、WAL、migration 與 disk 觀測。</p>
<p>最小 SQLite runbook 包含：</p>
<ol>
<li>Database file path、owner process、filesystem permission。</li>
<li>Journal mode、busy timeout、foreign key、schema version。</li>
<li>Backup command、restore drill、retention、checksum。</li>
<li>Migration command、pre-migration snapshot、rollback path。</li>
<li>Observability：busy、WAL size、disk free、backup age。</li>
<li>Incident route：disk full、bad migration、corruption signal。</li>
</ol>
<p>Runbook shift 要同步移除 PostgreSQL credential。Server database 退役時，保留 read-only archive、刪除 application secret、關閉 scheduled job、更新 dashboard 與 incident routing。</p>
<h2 id="cleanup-and-retention">Cleanup and Retention</h2>
<p>Cleanup and retention 的核心責任是讓舊 PostgreSQL 不再成為影子真相。Migration 後若舊 DB 長期可寫，團隊會在事故中分不清哪份資料有效。</p>
<table>
  <thead>
      <tr>
          <th>Cleanup 項目</th>
          <th>操作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Write disable</td>
          <td>PostgreSQL role 改 read-only 或關閉 app access</td>
      </tr>
      <tr>
          <td>Archive snapshot</td>
          <td>保存最後 dump、checksum、schema</td>
      </tr>
      <tr>
          <td>Credential removal</td>
          <td>移除 app secret、CI secret、admin token</td>
      </tr>
      <tr>
          <td>Dashboard update</td>
          <td>停用 PostgreSQL alert、啟用 SQLite alert</td>
      </tr>
      <tr>
          <td>Documentation</td>
          <td>更新 source-of-truth 與 restore route</td>
      </tr>
  </tbody>
</table>
<p>Retention 要和 data protection 對齊。若 PostgreSQL 內有 PII、audit log 或 legal retention，退役流程要依 retention policy 保存或銷毀，而非直接刪除。</p>
<h2 id="decision-route">Decision Route</h2>
<p>Decision route 的核心責任是讓 simplification 保持可逆。若未來 concurrent writer、central audit、PITR 或 multi-service source-of-truth 回來，系統要能沿 <a href="/blog/backend/01-database/vendors/sqlite/migrate-to-postgresql/" data-link-title="SQLite to PostgreSQL Migration" data-link-desc="SQLite 升級到 PostgreSQL 的 driver、schema diff、data copy、dual run、cutover、rollback 與 cleanup">SQLite to PostgreSQL migration</a> 重新升級。</p>
<table>
  <thead>
      <tr>
          <th>現況</th>
          <th>建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Single-user / local artifact</td>
          <td>SQLite simplification</td>
      </tr>
      <tr>
          <td>Small internal tool + low write</td>
          <td>SQLite + restore drill</td>
      </tr>
      <tr>
          <td>Read-mostly dataset for app bundle</td>
          <td>SQLite artifact + release version</td>
      </tr>
      <tr>
          <td>Multi-user SaaS</td>
          <td>保留 PostgreSQL</td>
      </tr>
      <tr>
          <td>Audit / HA / role 是正式要求</td>
          <td>保留 managed PostgreSQL</td>
      </tr>
  </tbody>
</table>
<p>Simplification 的完成標準是：SQLite file 可以被重建、備份、恢復、升級與交接。只要這些 evidence 完整，從 PostgreSQL 退到 SQLite 是清楚的工程決策。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>PostgreSQL to SQLite simplification 完成後，先讀 <a href="/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">file lifecycle / backup boundary</a> 建立 file operation；再讀 <a href="/blog/backend/01-database/vendors/sqlite/observability-runbook/" data-link-title="SQLite Observability and Runbook" data-link-desc="SQLite production runbook、backup evidence、WAL growth、busy errors、disk usage、restore drill 與 incident route">SQLite observability / runbook</a> 補 evidence；若之後需求再成長，回到 <a href="/blog/backend/01-database/vendors/sqlite/migrate-to-postgresql/" data-link-title="SQLite to PostgreSQL Migration" data-link-desc="SQLite 升級到 PostgreSQL 的 driver、schema diff、data copy、dual run、cutover、rollback 與 cleanup">SQLite to PostgreSQL migration</a>。</p>
]]></content:encoded></item><item><title>SQLite D1 / Turso / libSQL Comparison</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/d1-turso-libsql-comparison/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/d1-turso-libsql-comparison/</guid><description>&lt;p>D1 / Turso / libSQL comparison 的核心責任是把 SQLite-compatible edge products 和 local SQLite 分開判讀。它們共享 SQLite 開發體驗的一部分，但它們承擔的服務責任不同：Cloudflare D1 把 SQLite-like database 放進 Workers 生態與 managed edge platform；Turso / libSQL 把 SQLite family 延伸到 remote primary、embedded replica 與同步模型；local SQLite 則是 application process 直接管理單一 database file。&lt;/p>
&lt;p>本文的判讀錨點是：SQLite compatibility 代表開發入口接近，服務責任仍要重新審查。採用 edge SQLite 前，要先確認 write authority、read freshness、migration limit、backup evidence、observability、cost 與 vendor exit，而非只看 SQL 語法能否執行。&lt;/p>
&lt;h2 id="product-boundary">Product Boundary&lt;/h2>
&lt;p>Product boundary 的核心責任是定義誰持有資料、誰執行 SQL、誰負責恢復。Local SQLite 的資料在你的 filesystem；D1 的資料由 Cloudflare D1 平台管理並和 Workers binding 整合；Turso / libSQL 的資料通常有 remote database 與 client / embedded replica 的分工。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>選項&lt;/th>
 &lt;th>主要責任&lt;/th>
 &lt;th>適合情境&lt;/th>
 &lt;th>關鍵審查點&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Local SQLite&lt;/td>
 &lt;td>Process-local formal state&lt;/td>
 &lt;td>CLI、desktop、single-node app&lt;/td>
 &lt;td>file lifecycle、backup、WAL、lock&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cloudflare D1&lt;/td>
 &lt;td>Workers-integrated database&lt;/td>
 &lt;td>edge app、serverless API、low ops&lt;/td>
 &lt;td>platform limit、migration、binding&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Turso / libSQL&lt;/td>
 &lt;td>Remote primary + replicas&lt;/td>
 &lt;td>low-latency read、embedded replica&lt;/td>
 &lt;td>freshness、sync、driver semantics&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Litestream / LiteFS&lt;/td>
 &lt;td>Backup / replica operation&lt;/td>
 &lt;td>single-node app with recovery / read&lt;/td>
 &lt;td>RPO、RTO、primary ownership&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>PostgreSQL&lt;/td>
 &lt;td>Server SQL operation&lt;/td>
 &lt;td>multi-tenant、central audit、HA、role&lt;/td>
 &lt;td>operation team、PITR、schema gate&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Local SQLite 的判斷重點是 file ownership。若 app 與 database file 位於同一個 host，備份、restore、disk full、permission 與 app upgrade 都在你的 runbook 裡；這條路線承接 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">file lifecycle / backup boundary&lt;/a>。&lt;/p>
&lt;p>D1 的判斷重點是 platform integration。Cloudflare 官方 D1 docs 把 D1 放在 Workers 與 Wrangler workflow 內，並公開 &lt;a href="https://developers.cloudflare.com/d1/platform/limits/">D1 limits&lt;/a>；因此採用 D1 時要把 database decision 與 Workers deployment、local preview、batch migration、import/export limit 一起審查。&lt;/p></description><content:encoded><![CDATA[<p>D1 / Turso / libSQL comparison 的核心責任是把 SQLite-compatible edge products 和 local SQLite 分開判讀。它們共享 SQLite 開發體驗的一部分，但它們承擔的服務責任不同：Cloudflare D1 把 SQLite-like database 放進 Workers 生態與 managed edge platform；Turso / libSQL 把 SQLite family 延伸到 remote primary、embedded replica 與同步模型；local SQLite 則是 application process 直接管理單一 database file。</p>
<p>本文的判讀錨點是：SQLite compatibility 代表開發入口接近，服務責任仍要重新審查。採用 edge SQLite 前，要先確認 write authority、read freshness、migration limit、backup evidence、observability、cost 與 vendor exit，而非只看 SQL 語法能否執行。</p>
<h2 id="product-boundary">Product Boundary</h2>
<p>Product boundary 的核心責任是定義誰持有資料、誰執行 SQL、誰負責恢復。Local SQLite 的資料在你的 filesystem；D1 的資料由 Cloudflare D1 平台管理並和 Workers binding 整合；Turso / libSQL 的資料通常有 remote database 與 client / embedded replica 的分工。</p>
<table>
  <thead>
      <tr>
          <th>選項</th>
          <th>主要責任</th>
          <th>適合情境</th>
          <th>關鍵審查點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Local SQLite</td>
          <td>Process-local formal state</td>
          <td>CLI、desktop、single-node app</td>
          <td>file lifecycle、backup、WAL、lock</td>
      </tr>
      <tr>
          <td>Cloudflare D1</td>
          <td>Workers-integrated database</td>
          <td>edge app、serverless API、low ops</td>
          <td>platform limit、migration、binding</td>
      </tr>
      <tr>
          <td>Turso / libSQL</td>
          <td>Remote primary + replicas</td>
          <td>low-latency read、embedded replica</td>
          <td>freshness、sync、driver semantics</td>
      </tr>
      <tr>
          <td>Litestream / LiteFS</td>
          <td>Backup / replica operation</td>
          <td>single-node app with recovery / read</td>
          <td>RPO、RTO、primary ownership</td>
      </tr>
      <tr>
          <td>PostgreSQL</td>
          <td>Server SQL operation</td>
          <td>multi-tenant、central audit、HA、role</td>
          <td>operation team、PITR、schema gate</td>
      </tr>
  </tbody>
</table>
<p>Local SQLite 的判斷重點是 file ownership。若 app 與 database file 位於同一個 host，備份、restore、disk full、permission 與 app upgrade 都在你的 runbook 裡；這條路線承接 <a href="/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">file lifecycle / backup boundary</a>。</p>
<p>D1 的判斷重點是 platform integration。Cloudflare 官方 D1 docs 把 D1 放在 Workers 與 Wrangler workflow 內，並公開 <a href="https://developers.cloudflare.com/d1/platform/limits/">D1 limits</a>；因此採用 D1 時要把 database decision 與 Workers deployment、local preview、batch migration、import/export limit 一起審查。</p>
<p>Turso / libSQL 的判斷重點是 replica freshness 與 client semantics。Turso docs 對 <a href="https://docs.turso.tech/features/embedded-replicas/introduction">embedded replicas</a> 的描述顯示：application 可以持有 local replica 並透過同步取得資料；這會把「讀得快」和「讀到多新」變成同一個設計問題。</p>
<h2 id="edge-data-model">Edge Data Model</h2>
<p>Edge data model 的核心責任是把 latency 改善與一致性責任拆開。Edge database 的價值常來自 closer read path、serverless deployment 與較低操作表面；風險則集中在 write authority、replication lag、region routing 與平台限制。</p>
<table>
  <thead>
      <tr>
          <th>問題</th>
          <th>要觀察的訊號</th>
          <th>設計含義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>誰可以寫</td>
          <td>single primary、remote write、queue</td>
          <td>決定 conflict、retry、idempotency 設計</td>
      </tr>
      <tr>
          <td>讀取要多新</td>
          <td>read-after-write、sync interval</td>
          <td>決定 UI freshness、cache invalidation、fallback</td>
      </tr>
      <tr>
          <td>migration 怎麼跑</td>
          <td>CLI、batch limit、preview / prod gap</td>
          <td>決定 release gate 與 rollback plan</td>
      </tr>
      <tr>
          <td>失敗時如何恢復</td>
          <td>export、backup、restore command</td>
          <td>決定 RPO / RTO 與 vendor exit</td>
      </tr>
      <tr>
          <td>observability 在哪一層</td>
          <td>platform metrics、app log、query log</td>
          <td>決定 incident triage 從 app 還是 platform 開始查</td>
      </tr>
  </tbody>
</table>
<p>Write authority 是 edge SQLite 的第一個分水嶺。若所有 write 都集中到 remote primary，application 要處理 network error、retry、idempotency 與 read freshness；若 write 發生在 local replica，系統要有 conflict resolution、sync ordering 與 delete propagation。</p>
<p>Read locality 是 edge SQLite 的主要收益。它適合 session-local preference、read-mostly catalog、低風險 personalization、feature flag snapshot、tenant-local small dataset；這些情境的共同點是資料量小、write rate 低、freshness 可以定義。</p>
<p>Global transaction 是 edge SQLite 的高風險區。若產品需求包含跨 region balance transfer、inventory reservation、ledger posting、strongly consistent permission decision，設計應路由到 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">Global Distributed OLTP</a> 或 PostgreSQL / CockroachDB / Spanner 的 transactional model。</p>
<h2 id="migration-gap">Migration Gap</h2>
<p>Migration gap 的核心責任是確認 SQLite file 可以搬到 edge product 後，release workflow 仍可驗證。SQL syntax compatibility 只解決起點；真正會造成事故的是 batch limit、extension 差異、driver API、local preview 與 production platform 行為差異。</p>
<table>
  <thead>
      <tr>
          <th>差異面</th>
          <th>審查問題</th>
          <th>Evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SQL dialect</td>
          <td>schema、index、trigger、JSON 是否可用</td>
          <td>compatibility matrix + migration dry run</td>
      </tr>
      <tr>
          <td>Data movement</td>
          <td>seed / import / export 的容量與時間</td>
          <td>sample import、row count、checksum</td>
      </tr>
      <tr>
          <td>Runtime binding</td>
          <td>app 如何取得 database connection</td>
          <td>staging deployment + smoke test</td>
      </tr>
      <tr>
          <td>Transaction</td>
          <td>write path 是否跨 request / region</td>
          <td>failure injection、retry log、freshness test</td>
      </tr>
      <tr>
          <td>Backup / exit</td>
          <td>如何拿回 SQLite-compatible artifact</td>
          <td>export file、restore drill、retention note</td>
      </tr>
  </tbody>
</table>
<p>D1 migration 要把 Wrangler workflow 納入 release gate。Cloudflare D1 的 limits 文件明確列出 import、query、batch 等限制；因此大型 update / delete 要拆 batch，migration 要有 staging dry run 與 production rollback step。</p>
<p>Turso / libSQL migration 要把 driver semantics 納入 release gate。Local SQLite driver 直連 file；libSQL client 可能連 remote endpoint 或 embedded replica；application 要把 connection lifecycle、sync timing、auth token、network failure 與 local cache freshness 寫進測試。</p>
<h2 id="operational-model">Operational Model</h2>
<p>Operational model 的核心責任是把 managed convenience 轉成 ownership map。Edge SQLite 減少了部分 server operation，但新增 platform limit、billing、region behavior、vendor incident、CLI workflow 與 local preview mismatch。</p>
<p>Production runbook 至少要保存五種證據：</p>
<ol>
<li>Schema migration history 與每次 release 的 dry-run result。</li>
<li>Data import / export 指令、檔案大小、row count 與 checksum。</li>
<li>Region latency、read freshness、write error rate 與 retry count。</li>
<li>Platform limit 命中紀錄、batch policy 與成本警戒線。</li>
<li>Vendor exit route：回 local SQLite、PostgreSQL 或另一個 edge database 的最小搬遷步驟。</li>
</ol>
<p>成本模型要同時看 request、storage、egress、operation time 與工程鎖定。Edge product 常把起步成本壓低，但當資料變大、batch migration 變長、observability 需要外掛、vendor API 滲入 repository layer 時，長期成本會出現在 release 與 incident。</p>
<h2 id="decision-route">Decision Route</h2>
<p>Decision route 的核心責任是把需求送到相符的資料模型。D1 / Turso / libSQL 適合 edge locality 與低操作表面；當需求轉向 high-write OLTP、central audit、role-based permission、global transaction 或跨服務資料治理，應轉向 server SQL 或 distributed OLTP。</p>
<table>
  <thead>
      <tr>
          <th>需求訊號</th>
          <th>優先路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Workers app 需要小型 relational data</td>
          <td>Cloudflare D1 + explicit limits review</td>
      </tr>
      <tr>
          <td>App 需要 local read latency + remote sync</td>
          <td>Turso / libSQL + freshness contract</td>
      </tr>
      <tr>
          <td>Single-node app 只需要備份與恢復</td>
          <td>Local SQLite + <a href="/blog/backend/01-database/vendors/sqlite/litestream-litefs-replication/" data-link-title="SQLite Litestream / LiteFS Replication" data-link-desc="Litestream、LiteFS、SQLite backup replication、read replica、failover 與 restore route">Litestream / LiteFS</a></td>
      </tr>
      <tr>
          <td>多 tenant、central audit、DB role</td>
          <td><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a></td>
      </tr>
      <tr>
          <td>Global write consistency</td>
          <td><a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB</a> 或 Spanner</td>
      </tr>
  </tbody>
</table>
<p>D1 的採用條件是 edge runtime 本身就是主平台。若 application 已在 Workers 上、資料量可控、query pattern 清楚、migration 可 batch，D1 可以把 database operation 融入 deployment workflow。</p>
<p>Turso / libSQL 的採用條件是 local read value 高於同步複雜度。若產品可明確定義 stale read window、write path 與 conflict policy，embedded replica 可以降低 latency；若使用者需要立即看見跨裝置變更，就要先設計 freshness evidence。</p>
<h2 id="production-tripwires">Production Tripwires</h2>
<p>Production tripwires 的核心責任是指出何時重新評估 edge SQLite。這些訊號出現時，系統通常已從「SQLite-compatible convenience」進入正式 database governance。</p>
<table>
  <thead>
      <tr>
          <th>Tripwire</th>
          <th>意義</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Migration batch 經常碰 limit</td>
          <td>schema 與資料量超過 edge workflow</td>
          <td>評估 PostgreSQL / managed SQL</td>
      </tr>
      <tr>
          <td>Read freshness ticket 增加</td>
          <td>replica / sync 語意影響產品體驗</td>
          <td>建 freshness SLO 或改集中讀寫</td>
      </tr>
      <tr>
          <td>Export / restore 未演練</td>
          <td>vendor exit 與災難恢復缺 evidence</td>
          <td>補 restore drill 與 retention policy</td>
      </tr>
      <tr>
          <td>Driver API 滲入 domain</td>
          <td><a href="/blog/backend/knowledge-cards/vendor-lock-in/" data-link-title="Vendor Lock-In" data-link-desc="說明採用供應商產品後，其 API 與格式滲入程式碼造成的退出成本">vendor lock-in</a> 進入核心程式碼</td>
          <td>建 repository adapter 與 compatibility test</td>
      </tr>
      <tr>
          <td>Cross-region write 需求出現</td>
          <td>edge-local read 已不足</td>
          <td>路由到 distributed OLTP</td>
      </tr>
  </tbody>
</table>
<p>這些 tripwire 要寫進設計文件與 runbook。Edge SQLite 的優勢在於低摩擦起步；它的長期品質來自早期把 ownership、limits、exit 與 evidence 設計清楚。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>D1 / Turso / libSQL comparison 完成後，下一步要依壓力路由。要處理 local file 與 backup，讀 <a href="/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">file lifecycle / backup boundary</a>；要處理 replica / restore，讀 <a href="/blog/backend/01-database/vendors/sqlite/litestream-litefs-replication/" data-link-title="SQLite Litestream / LiteFS Replication" data-link-desc="Litestream、LiteFS、SQLite backup replication、read replica、failover 與 restore route">Litestream / LiteFS replication</a>；要從 local SQLite 移到 edge product，讀 <a href="/blog/backend/01-database/vendors/sqlite/migrate-to-d1-turso/" data-link-title="SQLite to D1 / Turso Migration" data-link-desc="SQLite 轉向 Cloudflare D1、Turso / libSQL 的 edge driver、compatibility audit、data movement 與 rollback">SQLite to D1 / Turso migration</a>；要處理 global write，回到 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">Global Distributed OLTP</a>。</p>
]]></content:encoded></item><item><title>SQLite file lifecycle 與 backup boundary</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 適合 embedded、local-first、edge 與低操作成本場景；本文聚焦 &lt;em>SQLite 檔案生命週期 + backup / restore 邊界&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;p>SQLite 的 file lifecycle 是把「一個資料庫檔案」升級成正式狀態的操作契約。SQLite 省掉 server process、帳號管理與網路連線，但它把 durability、backup、restore、locking 與 corruption recovery 放回 application process、filesystem 與 runbook；讀者要判斷的是這些責任是否已經有人承擔。&lt;/p>
&lt;p>這篇文章適合三種情境。第一種是 CLI、desktop、mobile 或 edge service 已經用 SQLite 保存正式資料；第二種是 single-instance backend 想用 SQLite 降低操作成本；第三種是 test fixture 用 SQLite，但需要知道哪些差異會讓 production database 的 bug 漏掉。&lt;/p>
&lt;h2 id="核心模型資料庫檔案是一組受-sqlite-管理的狀態檔">核心模型：資料庫檔案是一組受 SQLite 管理的狀態檔&lt;/h2>
&lt;p>SQLite 的資料庫狀態由 main database file 與 journal / WAL sidecar 共同構成。Rollback journal mode 會在寫入期間產生 journal file；WAL mode 會讓寫入先進入 &lt;code>-wal&lt;/code> 檔，並用 &lt;code>-shm&lt;/code> 檔協調 reader / writer。操作上看似「一個 &lt;code>.db&lt;/code> 檔」，production runbook 要把 sidecar file、checkpoint、backup API 與 restore test 一起納入。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>檔案 / 機制&lt;/th>
 &lt;th>服務責任&lt;/th>
 &lt;th>操作判讀&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>.db&lt;/code>&lt;/td>
 &lt;td>持久化資料、schema、index&lt;/td>
 &lt;td>file owner、permission、storage durability、snapshot 位置&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>-wal&lt;/code>&lt;/td>
 &lt;td>WAL mode 下尚未 checkpoint 的寫入&lt;/td>
 &lt;td>WAL growth、checkpoint cadence、backup 是否包含一致快照&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>-shm&lt;/code>&lt;/td>
 &lt;td>WAL index 與跨 connection 協調&lt;/td>
 &lt;td>local filesystem lock 是否可靠、部署是否跨 process 共用檔案&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>checkpoint&lt;/td>
 &lt;td>把 WAL 內容合併回 main database&lt;/td>
 &lt;td>checkpoint latency、writer pause、檔案大小是否持續膨脹&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>backup API&lt;/td>
 &lt;td>線上複製一致 snapshot&lt;/td>
 &lt;td>backup 是否在 application 還活著時仍能取得一致狀態&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的讀法是先找「誰有權改檔案」。SQLite 的核心風險多半來自繞過 SQLite library 的檔案操作，例如直接 copy 活躍 WAL database、把 database 放在 lock 語意不可靠的 filesystem、或讓多個不協調的 process 同時寫同一份檔案。&lt;/p>
&lt;h2 id="wal-mode讀取並發提升後writer-boundary-仍然存在">WAL mode：讀取並發提升後，writer boundary 仍然存在&lt;/h2>
&lt;p>WAL mode 的工程價值是讓 reader 與 writer 的衝突下降。讀取可以看 main database 加上 WAL 中的 snapshot，寫入則 append 到 WAL；這讓 read-heavy workload 比 rollback journal mode 更容易撐住互動式服務。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite</a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 適合 embedded、local-first、edge 與低操作成本場景；本文聚焦 <em>SQLite 檔案生命週期 + backup / restore 邊界</em>。</p></blockquote>
<p>SQLite 的 file lifecycle 是把「一個資料庫檔案」升級成正式狀態的操作契約。SQLite 省掉 server process、帳號管理與網路連線，但它把 durability、backup、restore、locking 與 corruption recovery 放回 application process、filesystem 與 runbook；讀者要判斷的是這些責任是否已經有人承擔。</p>
<p>這篇文章適合三種情境。第一種是 CLI、desktop、mobile 或 edge service 已經用 SQLite 保存正式資料；第二種是 single-instance backend 想用 SQLite 降低操作成本；第三種是 test fixture 用 SQLite，但需要知道哪些差異會讓 production database 的 bug 漏掉。</p>
<h2 id="核心模型資料庫檔案是一組受-sqlite-管理的狀態檔">核心模型：資料庫檔案是一組受 SQLite 管理的狀態檔</h2>
<p>SQLite 的資料庫狀態由 main database file 與 journal / WAL sidecar 共同構成。Rollback journal mode 會在寫入期間產生 journal file；WAL mode 會讓寫入先進入 <code>-wal</code> 檔，並用 <code>-shm</code> 檔協調 reader / writer。操作上看似「一個 <code>.db</code> 檔」，production runbook 要把 sidecar file、checkpoint、backup API 與 restore test 一起納入。</p>
<table>
  <thead>
      <tr>
          <th>檔案 / 機制</th>
          <th>服務責任</th>
          <th>操作判讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>.db</code></td>
          <td>持久化資料、schema、index</td>
          <td>file owner、permission、storage durability、snapshot 位置</td>
      </tr>
      <tr>
          <td><code>-wal</code></td>
          <td>WAL mode 下尚未 checkpoint 的寫入</td>
          <td>WAL growth、checkpoint cadence、backup 是否包含一致快照</td>
      </tr>
      <tr>
          <td><code>-shm</code></td>
          <td>WAL index 與跨 connection 協調</td>
          <td>local filesystem lock 是否可靠、部署是否跨 process 共用檔案</td>
      </tr>
      <tr>
          <td>checkpoint</td>
          <td>把 WAL 內容合併回 main database</td>
          <td>checkpoint latency、writer pause、檔案大小是否持續膨脹</td>
      </tr>
      <tr>
          <td>backup API</td>
          <td>線上複製一致 snapshot</td>
          <td>backup 是否在 application 還活著時仍能取得一致狀態</td>
      </tr>
  </tbody>
</table>
<p>這張表的讀法是先找「誰有權改檔案」。SQLite 的核心風險多半來自繞過 SQLite library 的檔案操作，例如直接 copy 活躍 WAL database、把 database 放在 lock 語意不可靠的 filesystem、或讓多個不協調的 process 同時寫同一份檔案。</p>
<h2 id="wal-mode讀取並發提升後writer-boundary-仍然存在">WAL mode：讀取並發提升後，writer boundary 仍然存在</h2>
<p>WAL mode 的工程價值是讓 reader 與 writer 的衝突下降。讀取可以看 main database 加上 WAL 中的 snapshot，寫入則 append 到 WAL；這讓 read-heavy workload 比 rollback journal mode 更容易撐住互動式服務。</p>
<p>WAL mode 同時保留 single writer boundary。SQLite 仍以檔案鎖與 transaction serialisation 控制寫入；寫入交易越長，其他 writer 等待時間越長，application 看到的訊號通常是 <code>SQLITE_BUSY</code>、latency spike 或 background job 卡住。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>常見原因</th>
          <th>第一輪處理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>SQLITE_BUSY</code> 增加</td>
          <td>長交易、background migration、慢 disk</td>
          <td>縮短 write transaction、加 busy timeout、把批次寫入切小</td>
      </tr>
      <tr>
          <td><code>-wal</code> 檔持續變大</td>
          <td>checkpoint 追不上、long reader 卡住</td>
          <td>找出長讀取、調整 checkpoint cadence、把 analytics query 移出路徑</td>
      </tr>
      <tr>
          <td>restore 後資料落差</td>
          <td>backup 沒取得一致 snapshot</td>
          <td>改用 <code>.backup</code> / backup API / <code>VACUUM INTO</code>，並演練 restore</td>
      </tr>
      <tr>
          <td>latency 受 fsync 拉高</td>
          <td><code>synchronous=FULL</code> + 高寫入頻率</td>
          <td>重新定義 durability 需求，評估 server SQL 或 managed service</td>
      </tr>
  </tbody>
</table>
<p>WAL mode 的 capacity gate 是「寫入是否仍能用一個 writer 排隊」。如果服務壓力來自大量並行寫入、多 instance active write 或跨 region 寫入，SQLite 的簡單性開始變成排隊與恢復成本；這時候要回到 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a>、<a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a> 或 <a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">global distributed OLTP</a>。</p>
<h2 id="backup-boundary複製檔案與取得一致-snapshot-是兩件事">Backup boundary：複製檔案與取得一致 snapshot 是兩件事</h2>
<p>SQLite backup 的核心責任是取得某一時間點的一致 snapshot。當 database live 且 WAL mode 開啟時，直接複製 <code>.db</code> 檔容易漏掉 <code>-wal</code> 中尚未 checkpoint 的寫入；即使同時複製 sidecar file，也要面對複製期間狀態變動的 race。正式服務應使用 SQLite 提供的 backup path 或可驗證的 filesystem snapshot。</p>
<table>
  <thead>
      <tr>
          <th>方法</th>
          <th>適合情境</th>
          <th>邊界</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>.backup</code> / Backup API</td>
          <td>live database、application 仍在服務</td>
          <td>SQLite 管理 source lock，產出開始備份時的一致 snapshot</td>
      </tr>
      <tr>
          <td><code>VACUUM INTO</code></td>
          <td>想同時 compact + 輸出新檔</td>
          <td>需要 I/O 空間與時間，適合 maintenance 或低流量窗口</td>
      </tr>
      <tr>
          <td>filesystem snapshot</td>
          <td>VM / volume 層已有一致 snapshot 能力</td>
          <td>要確認 snapshot 包含 main file 與 WAL sidecar，且 lock 語意清楚</td>
      </tr>
      <tr>
          <td>Litestream</td>
          <td>single-primary SQLite 的持續備份</td>
          <td>適合 DR / restore，不把 SQLite 變成 multi-primary database</td>
      </tr>
      <tr>
          <td>手動 <code>cp</code></td>
          <td>database 已關閉或已完成 checkpoint</td>
          <td>live WAL database 的一致性風險高，production runbook 應改路由</td>
      </tr>
  </tbody>
</table>
<p>Backup method 的選擇要先回到 <a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a> 與 <a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a>。如果產品可以接受每天一次快照，<code>VACUUM INTO</code> 或 scheduled backup 足夠；如果資料損失窗口要降到分鐘級或秒級，就要看 Litestream 類連續複製，或直接升級到 server database 的 PITR / replica 模型。</p>
<h2 id="restore-drillsqlite-production-readiness-看還原不只看備份成功">Restore drill：SQLite production readiness 看還原，不只看備份成功</h2>
<p>Restore drill 的責任是證明備份能在事故時接回服務。SQLite 的備份檔通常只有一個 target file，表面上比 PostgreSQL PITR 或 MySQL binlog recovery 簡單；真正的風險在 application binary、schema migration version、file permission、deployment path 與舊 WAL sidecar 是否一起對齊。</p>
<p>一個最小 restore drill 應保留五個檢查點：</p>
<ol>
<li>從備份產出新的 database file，不覆蓋 production path。</li>
<li>用 application binary 啟動 read-only smoke test，確認 schema version 與 migration table。</li>
<li>跑 row count、critical query、checksum 或 domain validation query。</li>
<li>驗證 file owner、permission、disk path、SELinux / container mount 或 volume 設定。</li>
<li>以 incident decision log 記錄 restore time、data freshness、known gap 與 owner。</li>
</ol>
<p>Restore drill 的交付物應接回 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a> 與 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。SQLite 的低操作成本來自日常元件少；事故時仍需要 evidence、owner 與 rollback condition。</p>
<h2 id="corruption-recovery先保全證據再決定修復或還原">Corruption recovery：先保全證據，再決定修復或還原</h2>
<p>SQLite <a href="/blog/backend/knowledge-cards/corruption-recovery/" data-link-title="Corruption Recovery" data-link-desc="說明資料損毀事故如何先辨識來源、保全證據，再決定修復或還原">corruption recovery</a> 的核心責任是區分「資料庫檔案本身受損」與「application 寫入了錯誤資料」。前者要走 file-level evidence、<code>.recover</code>、backup restore 與 filesystem / hardware investigation；後者要走資料修復、migration rollback 或 business reconciliation。</p>
<table>
  <thead>
      <tr>
          <th>觀察訊號</th>
          <th>優先判讀</th>
          <th>下一步路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>SQLITE_CORRUPT</code></td>
          <td>database page / btree 受損</td>
          <td>複製原檔保存證據、用 <code>.recover</code> 嘗試導出、從最近 backup 建新檔</td>
      </tr>
      <tr>
          <td>power loss 後啟動異常</td>
          <td>journal / WAL recovery 問題</td>
          <td>確認 sidecar file 是否仍在、檢查 storage sync 與 <code>synchronous</code> 設定</td>
      </tr>
      <tr>
          <td>restore 後 business data 錯誤</td>
          <td>備份點或 migration 錯誤</td>
          <td>對照 validation query、migration log、事件補償與 <a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">reconciliation</a></td>
      </tr>
      <tr>
          <td>network filesystem 上偶發錯誤</td>
          <td>lock 語意與 filesystem 問題</td>
          <td>把 SQLite 移回 local disk，或升級 server database</td>
      </tr>
  </tbody>
</table>
<p>Corruption 事件的第一個操作是保存原始檔案與 sidecar。直接在疑似受損檔案上跑修復、vacuum 或 application migration，會讓後續 root cause analysis 失去證據；比較穩定的流程是複製原檔、在副本上嘗試 <code>.recover</code>，同時從備份恢復服務路徑。</p>
<h2 id="anti-recommendation維持-sqlite-的條件要可被操作驗證">Anti-recommendation：維持 SQLite 的條件要可被操作驗證</h2>
<p>SQLite 的合理使用條件是「單一 writer、檔案生命週期清楚、restore drill 成立」。只要這三件事能被 runbook 驗證，SQLite 在 embedded、desktop、mobile、edge-local 或 small backend 場景可以是 production state。</p>
<p>升級條件則來自操作責任外溢。需要 database user / role、中心化 audit、多人同時寫、跨 instance failover、online schema migration、PITR、read replica 或跨 region transaction 時，server SQL 或 managed SQL 的操作模型會比繼續包裝 SQLite 清楚。</p>
<table>
  <thead>
      <tr>
          <th>目前壓力</th>
          <th>留在 SQLite 的條件</th>
          <th>升級路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>read-heavy local store</td>
          <td>WAL + restore drill 成立</td>
          <td>維持 SQLite，補 observability 與 backup evidence</td>
      </tr>
      <tr>
          <td>single-instance backend</td>
          <td>writer queue 可接受、RPO / RTO 明確</td>
          <td>SQLite + Litestream；或升級 PostgreSQL / MySQL</td>
      </tr>
      <tr>
          <td>edge / serverless</td>
          <td>平台已提供 SQLite-compatible 運作模型</td>
          <td>Cloudflare D1 / Turso；跨 region transaction 回到 global DB</td>
      </tr>
      <tr>
          <td>multi-tenant SaaS</td>
          <td>tenant 數少且 file ownership 清楚</td>
          <td>PostgreSQL / Aurora / CockroachDB</td>
      </tr>
      <tr>
          <td>regulated data</td>
          <td>backup encryption、audit、restore 可驗證</td>
          <td>PostgreSQL / managed SQL + audit / PITR</td>
      </tr>
  </tbody>
</table>
<p>這張表的核心是把操作責任具體化，而非替 SQLite 設流量天花板。小型服務可能用 SQLite 長期穩定運作；同樣流量下，一旦合規、稽核、多人操作或 HA 需求進來，server database 的長期成本會更容易被治理。</p>
<h2 id="操作檢查清單">操作檢查清單</h2>
<p>SQLite production runbook 至少要能回答下列問題：</p>
<ol>
<li>Database file、WAL sidecar 與 backup target 在哪個 volume、由誰擁有。</li>
<li><code>journal_mode</code>、<code>synchronous</code>、busy timeout、checkpoint cadence 與 migration policy 如何設定。</li>
<li>Backup 用 <code>.backup</code> / backup API / <code>VACUUM INTO</code> / Litestream 的哪一條路徑。</li>
<li>Restore drill 最近一次何時執行，RPO / RTO 是否符合產品承諾。</li>
<li><code>SQLITE_BUSY</code>、WAL growth、disk full、backup failure 與 restore failure 如何告警。</li>
<li>Corruption recovery 時誰保存原檔、誰啟動 restore、誰決定修復或 fail-forward。</li>
</ol>
<p>這份清單要接到服務 ownership，而非留在工程師個人習慣。SQLite 的優勢是 deployment surface 小；production 化的代價是把檔案、備份與恢復流程寫進同一份可交接 runbook。</p>
<h2 id="引用路徑">引用路徑</h2>
<ul>
<li>上游 overview：<a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite vendor page</a></li>
<li>服務責任：<a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">Source of Truth</a>、<a href="/blog/backend/knowledge-cards/database/" data-link-title="Database" data-link-desc="說明 database 在後端系統中如何承擔正式狀態、查詢與一致性責任">Database</a></li>
<li>恢復目標：<a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a>、<a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a></li>
<li>證據交接：<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a>、<a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a></li>
<li>官方文件：<a href="https://www.sqlite.org/wal.html">SQLite Write-Ahead Logging</a>、<a href="https://www.sqlite.org/backup.html">SQLite Backup API</a>、<a href="https://www.sqlite.org/howtocorrupt.html">How To Corrupt An SQLite Database File</a>、<a href="https://www.sqlite.org/recovery.html">Recovering Data From A Corrupt SQLite Database</a>、<a href="https://www.sqlite.org/whentouse.html">Appropriate Uses For SQLite</a>、<a href="https://www.sqlite.org/mostdeployed.html">Most Widely Deployed SQL Database Engine</a></li>
<li>延伸工具：<a href="https://litestream.io/reference/restore/">Litestream restore reference</a>、<a href="https://litestream.io/getting-started/">Litestream getting started</a></li>
</ul>
]]></content:encoded></item><item><title>SQLite Litestream / LiteFS Replication</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/litestream-litefs-replication/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/litestream-litefs-replication/</guid><description>&lt;p>Litestream / LiteFS replication 的核心責任是把 SQLite 的 single-file operation 補成可恢復、可部署、可讀擴展的服務形狀。這類工具延伸 SQLite，但它們解決的問題不同：Litestream 主要把 WAL 變化持續送到 replica storage，強化 backup 與 restore；LiteFS 主要在 Fly.io 生態中透過 primary lease 與 filesystem layer 支援 replicated SQLite deployment。&lt;/p>
&lt;p>本文的判讀錨點是：replicated SQLite 要先說明 replica 的服務責任。它可能是 continuous backup、warm restore source、read replica、primary failover helper 或 deployment topology；每一種責任都有不同的 RPO、RTO、freshness 與 incident runbook。&lt;/p>
&lt;h2 id="replication-taxonomy">Replication Taxonomy&lt;/h2>
&lt;p>Replication taxonomy 的核心責任是把「有複本」拆成可操作的幾種能力。SQLite 周邊工具常用 replication 這個字，但 operator 需要知道它到底保護哪個風險。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>類型&lt;/th>
 &lt;th>主要責任&lt;/th>
 &lt;th>成功訊號&lt;/th>
 &lt;th>常見誤判&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Continuous backup&lt;/td>
 &lt;td>降低資料遺失窗口&lt;/td>
 &lt;td>replica lag、restore 成功&lt;/td>
 &lt;td>把 replica 當 active-active database&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Read replica&lt;/td>
 &lt;td>降低 read latency / 壓力&lt;/td>
 &lt;td>freshness、read error rate&lt;/td>
 &lt;td>忽略 stale read&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Warm standby&lt;/td>
 &lt;td>縮短 restore / failover&lt;/td>
 &lt;td>promotion drill、DNS / routing&lt;/td>
 &lt;td>只備份檔案、未演練切換&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Primary lease&lt;/td>
 &lt;td>控制單一 writer ownership&lt;/td>
 &lt;td>writer lease、fencing log&lt;/td>
 &lt;td>多個 node 同時寫同一份邏輯狀態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Consensus SQL&lt;/td>
 &lt;td>多節點一致性寫入&lt;/td>
 &lt;td>quorum、leader election&lt;/td>
 &lt;td>用 WAL shipping 取代 distributed OLTP&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Continuous backup 的語言是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO&lt;/a>。它關心最近一次成功送出的 WAL、snapshot freshness、object storage credential、restore 指令與演練結果。&lt;/p>
&lt;p>Read replica 的語言是 freshness。Replica 能降低 read latency 或保護 primary workload，但讀者要知道 stale window、read-after-write policy、fallback to primary 與 cache invalidation。&lt;/p>
&lt;p>Primary lease 的語言是 writer ownership。SQLite 的服務形狀仍適合 single writer；工具可以協助 deployment 切換，但 application 要配合 fencing、retry 與 promotion evidence。&lt;/p>
&lt;h2 id="litestream-boundary">Litestream Boundary&lt;/h2>
&lt;p>Litestream boundary 的核心責任是把 SQLite WAL 變成可持續複製的 backup stream。Litestream 官方說明把它定位為 SQLite streaming replication tool，並在 &lt;a href="https://litestream.io/how-it-works/">How it works&lt;/a> 與 &lt;a href="https://litestream.io/reference/restore/">restore command&lt;/a> 文件中強調 replica 與 restore workflow。&lt;/p>
&lt;p>Litestream 適合下列情境：&lt;/p>
&lt;ol>
&lt;li>單節點 SQLite app 要降低資料遺失窗口。&lt;/li>
&lt;li>系統可接受 restore 後重新啟動 service。&lt;/li>
&lt;li>Object storage credential、retention、restore drill 可以被管理。&lt;/li>
&lt;li>Write pattern 適中，WAL stream 與 snapshot 維護成本可控。&lt;/li>
&lt;/ol>
&lt;p>Litestream 的設計重點是 backup evidence。Runbook 要記錄 replica destination、last replicated generation、last restore test、expected RPO、expected RTO、restore target path、credential rotation 與 corruption triage。&lt;/p></description><content:encoded><![CDATA[<p>Litestream / LiteFS replication 的核心責任是把 SQLite 的 single-file operation 補成可恢復、可部署、可讀擴展的服務形狀。這類工具延伸 SQLite，但它們解決的問題不同：Litestream 主要把 WAL 變化持續送到 replica storage，強化 backup 與 restore；LiteFS 主要在 Fly.io 生態中透過 primary lease 與 filesystem layer 支援 replicated SQLite deployment。</p>
<p>本文的判讀錨點是：replicated SQLite 要先說明 replica 的服務責任。它可能是 continuous backup、warm restore source、read replica、primary failover helper 或 deployment topology；每一種責任都有不同的 RPO、RTO、freshness 與 incident runbook。</p>
<h2 id="replication-taxonomy">Replication Taxonomy</h2>
<p>Replication taxonomy 的核心責任是把「有複本」拆成可操作的幾種能力。SQLite 周邊工具常用 replication 這個字，但 operator 需要知道它到底保護哪個風險。</p>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>主要責任</th>
          <th>成功訊號</th>
          <th>常見誤判</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Continuous backup</td>
          <td>降低資料遺失窗口</td>
          <td>replica lag、restore 成功</td>
          <td>把 replica 當 active-active database</td>
      </tr>
      <tr>
          <td>Read replica</td>
          <td>降低 read latency / 壓力</td>
          <td>freshness、read error rate</td>
          <td>忽略 stale read</td>
      </tr>
      <tr>
          <td>Warm standby</td>
          <td>縮短 restore / failover</td>
          <td>promotion drill、DNS / routing</td>
          <td>只備份檔案、未演練切換</td>
      </tr>
      <tr>
          <td>Primary lease</td>
          <td>控制單一 writer ownership</td>
          <td>writer lease、fencing log</td>
          <td>多個 node 同時寫同一份邏輯狀態</td>
      </tr>
      <tr>
          <td>Consensus SQL</td>
          <td>多節點一致性寫入</td>
          <td>quorum、leader election</td>
          <td>用 WAL shipping 取代 distributed OLTP</td>
      </tr>
  </tbody>
</table>
<p>Continuous backup 的語言是 <a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO</a> 與 <a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a>。它關心最近一次成功送出的 WAL、snapshot freshness、object storage credential、restore 指令與演練結果。</p>
<p>Read replica 的語言是 freshness。Replica 能降低 read latency 或保護 primary workload，但讀者要知道 stale window、read-after-write policy、fallback to primary 與 cache invalidation。</p>
<p>Primary lease 的語言是 writer ownership。SQLite 的服務形狀仍適合 single writer；工具可以協助 deployment 切換，但 application 要配合 fencing、retry 與 promotion evidence。</p>
<h2 id="litestream-boundary">Litestream Boundary</h2>
<p>Litestream boundary 的核心責任是把 SQLite WAL 變成可持續複製的 backup stream。Litestream 官方說明把它定位為 SQLite streaming replication tool，並在 <a href="https://litestream.io/how-it-works/">How it works</a> 與 <a href="https://litestream.io/reference/restore/">restore command</a> 文件中強調 replica 與 restore workflow。</p>
<p>Litestream 適合下列情境：</p>
<ol>
<li>單節點 SQLite app 要降低資料遺失窗口。</li>
<li>系統可接受 restore 後重新啟動 service。</li>
<li>Object storage credential、retention、restore drill 可以被管理。</li>
<li>Write pattern 適中，WAL stream 與 snapshot 維護成本可控。</li>
</ol>
<p>Litestream 的設計重點是 backup evidence。Runbook 要記錄 replica destination、last replicated generation、last restore test、expected RPO、expected RTO、restore target path、credential rotation 與 corruption triage。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">litestream restore -o /var/lib/app/restored.db s3://example-bucket/app.db
</span></span><span class="line"><span class="ln">2</span><span class="cl">sqlite3 /var/lib/app/restored.db <span class="s2">&#34;PRAGMA integrity_check;&#34;</span></span></span></code></pre></div><p>這段命令是 restore drill 的最小骨架。正式 runbook 要補上 service stop、database path、sidecar file、permission、checksum、application smoke test 與 rollback decision。</p>
<p>Litestream 的風險集中在 restore path。備份存在和服務可恢復是兩件事；每次 release 或 schema migration 後，都應用 staging data 跑一次 restore、integrity check、row count 與 application smoke test。</p>
<h2 id="litefs-boundary">LiteFS Boundary</h2>
<p>LiteFS boundary 的核心責任是支援 replicated deployment topology，而非只做 backup。LiteFS 在 Fly.io 文件中被定位為 SQLite replication layer，透過 FUSE filesystem 與 primary lease 模型協助應用在多個 instance 間運作。</p>
<p>LiteFS 適合下列情境：</p>
<ol>
<li>App 仍希望使用 SQLite file 與 local SQL path。</li>
<li>Deployment 有多個 instance，但 write authority 可以集中到 primary。</li>
<li>Read replica freshness 可以被產品接受。</li>
<li>Team 願意把 filesystem layer、primary lease、promotion 與 platform operation 納入 runbook。</li>
</ol>
<p>LiteFS 的設計重點是 primary ownership。Application 要知道 write request 到哪裡執行、primary 切換時如何重試、read replica 讀到舊資料時如何回應，以及 promotion 完成前哪些 endpoint 要進入 degraded mode。</p>
<p>LiteFS 的 incident route 要從 writer ownership 開始查。若出現 write error、stale read 或 suspected split brain，先查看 primary lease、instance health、replication lag、pending writes 與 platform network，再處理 application retry。</p>
<h2 id="failure-modes">Failure Modes</h2>
<p>Failure modes 的核心責任是把 replicated SQLite 的事故從「資料庫壞了」拆成可排查訊號。SQLite file、WAL、object storage、filesystem layer、deployment platform 與 application retry 都可能是問題來源。</p>
<table>
  <thead>
      <tr>
          <th>Failure mode</th>
          <th>判讀訊號</th>
          <th>立即處理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Replica lag</td>
          <td>last replicated time 落後</td>
          <td>降低 write rate、檢查 credential / network</td>
      </tr>
      <tr>
          <td>Restore lag</td>
          <td>WAL files 過多、restore time 變長</td>
          <td>觸發 snapshot、演練 restore</td>
      </tr>
      <tr>
          <td>Stale read</td>
          <td>使用者讀到舊資料</td>
          <td>fallback primary read、標記 freshness</td>
      </tr>
      <tr>
          <td>Writer lease confusion</td>
          <td>多 instance write error</td>
          <td>暫停寫入、確認 primary、fencing old writer</td>
      </tr>
      <tr>
          <td>Object storage failure</td>
          <td>backup upload error</td>
          <td>切換 credential / destination、補上重送</td>
      </tr>
      <tr>
          <td>Sidecar file mismatch</td>
          <td>restore / copy 後 integrity fail</td>
          <td>回到 backup API / official restore path</td>
      </tr>
  </tbody>
</table>
<p>Replica lag 要接到 alert。對 Litestream，它意味著 RPO 正在擴大；對 LiteFS，它可能同時影響 read freshness 與 failover confidence。</p>
<p>Restore lag 要接到 release gate。若 restore time 已超過目標 <a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO</a>，就要調整 snapshot frequency、資料保留策略或搬到 server database。</p>
<p>Stale read 要接到產品語言。使用者看到舊資料時，系統可以顯示 sync state、重讀 primary、限制 critical action 或提供 refresh；這些策略要在設計階段決定。</p>
<h2 id="no-go-conditions">No-Go Conditions</h2>
<p>No-go condition 的核心責任是避免把 replicated SQLite 推到 distributed OLTP 的位置。SQLite 周邊 replication 工具可以強化單節點與 read replica，但高寫入、多 writer、強一致跨 region transaction 需要不同資料庫模型。</p>
<table>
  <thead>
      <tr>
          <th>No-go 訊號</th>
          <th>原因</th>
          <th>路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多 region 都要接受交易性寫入</td>
          <td>single writer / primary lease 壓力過高</td>
          <td><a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB</a> 或 Spanner</td>
      </tr>
      <tr>
          <td>每秒大量 concurrent writer</td>
          <td>lock contention 與 replica lag 擴大</td>
          <td>PostgreSQL / MySQL / managed OLTP</td>
      </tr>
      <tr>
          <td>Central audit / DB role 是硬需求</td>
          <td>SQLite file model 缺少 server role</td>
          <td><a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a></td>
      </tr>
      <tr>
          <td>Restore drill 經常超過 RTO</td>
          <td>file size / WAL backlog 已超界</td>
          <td>server DB、sharding 或資料生命週期重整</td>
      </tr>
      <tr>
          <td>Incident team 缺少 filesystem layer 維護能力</td>
          <td>operation model 超過組織能力</td>
          <td>managed SQL 或 D1 / Turso managed path</td>
      </tr>
  </tbody>
</table>
<p>No-go 條件要在 design review 階段列出。SQLite replication 的好處是低成本與低元件數；當核心需求變成跨節點一致性寫入，繼續調工具會把風險藏在 incident 時刻。</p>
<h2 id="decision-route">Decision Route</h2>
<p>Decision route 的核心責任是把資料保護、讀擴展與高可用分開選型。Litestream / LiteFS 位置清楚時，SQLite 可以保持簡潔；位置混淆時，系統會同時缺 backup evidence 與 transaction guarantee。</p>
<table>
  <thead>
      <tr>
          <th>需求</th>
          <th>建議路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單節點 SQLite 需要 continuous backup</td>
          <td>Litestream + restore drill</td>
      </tr>
      <tr>
          <td>多 instance deployment 需要 primary lease</td>
          <td>LiteFS + write routing / promotion runbook</td>
      </tr>
      <tr>
          <td>Edge app 需要 managed SQL-like platform</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/d1-turso-libsql-comparison/" data-link-title="SQLite D1 / Turso / libSQL Comparison" data-link-desc="Cloudflare D1、Turso、libSQL 與 local SQLite 在 edge、replication、consistency、migration 與 vendor boundary 的比較">D1 / Turso / libSQL comparison</a></td>
      </tr>
      <tr>
          <td>多 tenant OLTP 需要 central operation</td>
          <td>PostgreSQL / MySQL / Aurora</td>
      </tr>
      <tr>
          <td>Global transaction 是核心需求</td>
          <td>Distributed OLTP</td>
      </tr>
  </tbody>
</table>
<p>選擇 Litestream 時，完成標準是能在 staging 從 replica restore 出可用 DB。選擇 LiteFS 時，完成標準是能演練 primary 切換、read freshness、write retry 與 degraded mode。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>Litestream / LiteFS replication 完成後，下一步要回到 SQLite operation evidence。File copy、backup API 與 WAL sidecar 請讀 <a href="/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">file lifecycle / backup boundary</a>；busy、lock 與 writer 壓力請讀 <a href="/blog/backend/01-database/vendors/sqlite/wal-concurrency-locking/" data-link-title="SQLite WAL Concurrency and Locking" data-link-desc="SQLite WAL mode 如何降低 reader / writer 衝突、保留 single writer boundary，並用 SQLITE_BUSY、WAL growth、checkpoint 訊號判斷 production 上限">WAL concurrency / locking</a>；完整 runbook 請讀 <a href="/blog/backend/01-database/vendors/sqlite/observability-runbook/" data-link-title="SQLite Observability and Runbook" data-link-desc="SQLite production runbook、backup evidence、WAL growth、busy errors、disk usage、restore drill 與 incident route">SQLite observability / runbook</a>。</p>
]]></content:encoded></item><item><title>SQLite Local-first Sync Boundary</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/local-first-sync-boundary/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/local-first-sync-boundary/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 適合 local-first / offline-first 場景；本文聚焦 &lt;em>SQLite local store 與 multi-device sync protocol 的責任分界&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;p>SQLite local-first sync boundary 的核心責任是把「本機可用」和「多端一致」分成兩個問題。SQLite 很適合保存 device-local state；但它不提供 identity、transport、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/conflict-resolution/" data-link-title="Conflict Resolution" data-link-desc="說明並發或離線寫入產生衝突時，如何偵測、呈現與合併成可接受狀態">conflict resolution&lt;/a>、delete propagation、server authority 或 audit trail。當資料要跨裝置、跨使用者或跨服務同步時，SQLite 只是 local replica / working copy。&lt;/p>
&lt;p>本文的判讀錨點是：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/local-first/" data-link-title="Local-First" data-link-desc="說明本機優先的資料架構如何讓離線可用，並把同步當成獨立問題">local-first&lt;/a> 的產品價值來自離線可用，工程成本來自同步語意。SQLite 解的是 local durability；sync layer 解的是資料合併、順序、權威來源與錯誤修復。&lt;/p>
&lt;h2 id="local-state-taxonomy">Local state taxonomy&lt;/h2>
&lt;p>Local-first 設計的第一步是標記本機資料角色。不同資料角色對 sync、backup、conflict 與 delete 的要求不同。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>資料角色&lt;/th>
 &lt;th>例子&lt;/th>
 &lt;th>Sync 語意&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Local cache&lt;/td>
 &lt;td>API response cache、thumbnail metadata&lt;/td>
 &lt;td>可清除、可重抓&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Draft / working copy&lt;/td>
 &lt;td>草稿、離線表單、未送出 action&lt;/td>
 &lt;td>需要 upload / retry / conflict handling&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Local source of truth&lt;/td>
 &lt;td>單裝置日記、CLI state&lt;/td>
 &lt;td>需要 backup / export，可能不需要 server&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Local replica&lt;/td>
 &lt;td>server record 的本地副本&lt;/td>
 &lt;td>server authority、stale read、sync lag&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Sync queue&lt;/td>
 &lt;td>pending mutation / event log&lt;/td>
 &lt;td>ordering、idempotency、replay&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的重點是資料角色先於 sync 工具。若所有資料都只是 cache，SQLite + TTL 足夠；若有 pending mutation 或 multi-device edit，就需要 sync protocol。&lt;/p>
&lt;h2 id="authority-boundary">Authority boundary&lt;/h2>
&lt;p>Authority boundary 的核心責任是決定衝突時誰說了算。Local-first app 可以讓 device、server、field-level merge 或 CRDT 成為不同層的 authority；SQLite 本身只保存狀態，不替系統決策。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Authority model&lt;/th>
 &lt;th>適合情境&lt;/th>
 &lt;th>代價&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Server authority&lt;/td>
 &lt;td>帳務、權限、共享資料&lt;/td>
 &lt;td>離線寫入要排隊，回線後可能被拒絕&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Device authority&lt;/td>
 &lt;td>單使用者、單裝置資料&lt;/td>
 &lt;td>多裝置同步能力弱&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Last-write-wins&lt;/td>
 &lt;td>低價值設定、簡單 preference&lt;/td>
 &lt;td>資料覆蓋風險&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Field merge&lt;/td>
 &lt;td>profile、表單、可分欄位資料&lt;/td>
 &lt;td>merge rule 要測，使用者理解成本上升&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CRDT / operation log&lt;/td>
 &lt;td>協作編輯、順序敏感操作&lt;/td>
 &lt;td>實作與除錯成本高&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Authority model 要和 product semantics 對齊。庫存、付款、權限這類資料通常需要 server authority；notes、draft、local settings 可以接受更偏 local 的權威模型。&lt;/p>
&lt;h2 id="sync-transport-與-local-log">Sync transport 與 local log&lt;/h2>
&lt;p>Sync transport 的核心責任是把 SQLite local state 轉成可重送、可去重、可驗證的資料流。最常見做法是本地維護 pending mutation table 或 change log，再由 background sync worker 送到 server。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite</a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 適合 local-first / offline-first 場景；本文聚焦 <em>SQLite local store 與 multi-device sync protocol 的責任分界</em>。</p></blockquote>
<p>SQLite local-first sync boundary 的核心責任是把「本機可用」和「多端一致」分成兩個問題。SQLite 很適合保存 device-local state；但它不提供 identity、transport、<a href="/blog/backend/knowledge-cards/conflict-resolution/" data-link-title="Conflict Resolution" data-link-desc="說明並發或離線寫入產生衝突時，如何偵測、呈現與合併成可接受狀態">conflict resolution</a>、delete propagation、server authority 或 audit trail。當資料要跨裝置、跨使用者或跨服務同步時，SQLite 只是 local replica / working copy。</p>
<p>本文的判讀錨點是：<a href="/blog/backend/knowledge-cards/local-first/" data-link-title="Local-First" data-link-desc="說明本機優先的資料架構如何讓離線可用，並把同步當成獨立問題">local-first</a> 的產品價值來自離線可用，工程成本來自同步語意。SQLite 解的是 local durability；sync layer 解的是資料合併、順序、權威來源與錯誤修復。</p>
<h2 id="local-state-taxonomy">Local state taxonomy</h2>
<p>Local-first 設計的第一步是標記本機資料角色。不同資料角色對 sync、backup、conflict 與 delete 的要求不同。</p>
<table>
  <thead>
      <tr>
          <th>資料角色</th>
          <th>例子</th>
          <th>Sync 語意</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Local cache</td>
          <td>API response cache、thumbnail metadata</td>
          <td>可清除、可重抓</td>
      </tr>
      <tr>
          <td>Draft / working copy</td>
          <td>草稿、離線表單、未送出 action</td>
          <td>需要 upload / retry / conflict handling</td>
      </tr>
      <tr>
          <td>Local source of truth</td>
          <td>單裝置日記、CLI state</td>
          <td>需要 backup / export，可能不需要 server</td>
      </tr>
      <tr>
          <td>Local replica</td>
          <td>server record 的本地副本</td>
          <td>server authority、stale read、sync lag</td>
      </tr>
      <tr>
          <td>Sync queue</td>
          <td>pending mutation / event log</td>
          <td>ordering、idempotency、replay</td>
      </tr>
  </tbody>
</table>
<p>這張表的重點是資料角色先於 sync 工具。若所有資料都只是 cache，SQLite + TTL 足夠；若有 pending mutation 或 multi-device edit，就需要 sync protocol。</p>
<h2 id="authority-boundary">Authority boundary</h2>
<p>Authority boundary 的核心責任是決定衝突時誰說了算。Local-first app 可以讓 device、server、field-level merge 或 CRDT 成為不同層的 authority；SQLite 本身只保存狀態，不替系統決策。</p>
<table>
  <thead>
      <tr>
          <th>Authority model</th>
          <th>適合情境</th>
          <th>代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Server authority</td>
          <td>帳務、權限、共享資料</td>
          <td>離線寫入要排隊，回線後可能被拒絕</td>
      </tr>
      <tr>
          <td>Device authority</td>
          <td>單使用者、單裝置資料</td>
          <td>多裝置同步能力弱</td>
      </tr>
      <tr>
          <td>Last-write-wins</td>
          <td>低價值設定、簡單 preference</td>
          <td>資料覆蓋風險</td>
      </tr>
      <tr>
          <td>Field merge</td>
          <td>profile、表單、可分欄位資料</td>
          <td>merge rule 要測，使用者理解成本上升</td>
      </tr>
      <tr>
          <td>CRDT / operation log</td>
          <td>協作編輯、順序敏感操作</td>
          <td>實作與除錯成本高</td>
      </tr>
  </tbody>
</table>
<p>Authority model 要和 product semantics 對齊。庫存、付款、權限這類資料通常需要 server authority；notes、draft、local settings 可以接受更偏 local 的權威模型。</p>
<h2 id="sync-transport-與-local-log">Sync transport 與 local log</h2>
<p>Sync transport 的核心責任是把 SQLite local state 轉成可重送、可去重、可驗證的資料流。最常見做法是本地維護 pending mutation table 或 change log，再由 background sync worker 送到 server。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">pending_mutations</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="n">id</span><span class="w"> </span><span class="nb">TEXT</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="n">entity_type</span><span class="w"> </span><span class="nb">TEXT</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="n">entity_id</span><span class="w"> </span><span class="nb">TEXT</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="k">operation</span><span class="w"> </span><span class="nb">TEXT</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="n">payload</span><span class="w"> </span><span class="nb">TEXT</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="n">created_at</span><span class="w"> </span><span class="nb">TEXT</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="n">retry_count</span><span class="w"> </span><span class="nb">INTEGER</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="w"> </span><span class="k">DEFAULT</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="n">last_error</span><span class="w"> </span><span class="nb">TEXT</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="p">);</span></span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>設計點</th>
          <th>判讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>idempotency</td>
          <td>每個 mutation 需要穩定 id，避免重送副作用</td>
      </tr>
      <tr>
          <td>ordering</td>
          <td>同 entity 操作是否必須按順序</td>
      </tr>
      <tr>
          <td>retry</td>
          <td>transient failure、backoff、dead-letter</td>
      </tr>
      <tr>
          <td>compaction</td>
          <td>已同步 local log 何時清除</td>
      </tr>
      <tr>
          <td>reconciliation</td>
          <td>server / local 差異如何修復</td>
      </tr>
  </tbody>
</table>
<p>這裡和 backend queue 概念相通：pending mutation table 是本機版 durable queue。它需要 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a>、retry 與 replay 思維，而不只是「存一張表」。</p>
<h2 id="conflict-resolution">Conflict resolution</h2>
<p>Conflict resolution 的核心責任是讓兩個合法 local write 合併成可接受狀態。SQLite 可以保存 local write；sync layer 要決定衝突偵測、呈現與合併。</p>
<table>
  <thead>
      <tr>
          <th>衝突型態</th>
          <th>例子</th>
          <th>處理策略</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Same field update</td>
          <td>兩台裝置改同一個 display name</td>
          <td>LWW、server reject、manual merge</td>
      </tr>
      <tr>
          <td>Disjoint field update</td>
          <td>一台改 phone，一台改 address</td>
          <td>field merge</td>
      </tr>
      <tr>
          <td>Delete vs update</td>
          <td>一台刪除，一台修改</td>
          <td>tombstone、manual review</td>
      </tr>
      <tr>
          <td>Ordered operation</td>
          <td>task reorder、ledger append</td>
          <td>operation log、server sequence</td>
      </tr>
  </tbody>
</table>
<p>Conflict policy 要在資料模型設計時決定。等衝突發生後才補策略，通常會導致資料修復、客服流程與 audit evidence 同時缺位。</p>
<h2 id="delete-propagation-與-privacy">Delete propagation 與 privacy</h2>
<p>Delete propagation 的核心責任是讓 server、device、backup 與 sync queue 對「刪除」有一致語意。Local-first app 常見風險是 server 已刪，但 device local DB、pending queue 或 OS backup 還留著資料。</p>
<table>
  <thead>
      <tr>
          <th>刪除語意</th>
          <th>適合情境</th>
          <th>SQLite 設計</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Soft delete</td>
          <td>可恢復、需要 sync tombstone</td>
          <td><code>deleted_at</code>、sync tombstone、retention job</td>
      </tr>
      <tr>
          <td>Hard delete</td>
          <td>privacy / compliance</td>
          <td>local purge、backup exclusion、sync confirmation</td>
      </tr>
      <tr>
          <td>Redaction</td>
          <td>support bundle / log</td>
          <td>export 時遮罩 sensitive fields</td>
      </tr>
  </tbody>
</table>
<p>刪除在同步系統裡是一個跨裝置生命週期。若資料跨裝置同步，delete 需要 <a href="/blog/backend/knowledge-cards/tombstone/" data-link-title="Tombstone" data-link-desc="說明刪除如何用一筆標記記錄下來，讓刪除事件能跨副本與裝置傳播">tombstone</a>、ack、retry、backup retention 與 evidence；這些責任要接到 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection</a>。</p>
<h2 id="production-踩雷">Production 踩雷</h2>
<h3 id="case-1pending-mutation-沒有-idempotency-key">Case 1：pending mutation 沒有 idempotency key</h3>
<p>Pending mutation 沒有 idempotency key 的核心風險是重送造成重複副作用。網路 timeout 後 worker 重送，server 已經處理第一次請求，第二次又建立一筆資料或扣一次庫存。</p>
<p>修正方向是每個 mutation 生成 stable id，server 以 idempotency key 去重，local SQLite 保存 retry state 與 server ack。</p>
<h3 id="case-2lww-覆蓋使用者資料">Case 2：LWW 覆蓋使用者資料</h3>
<p>Last-write-wins 的核心風險是把衝突靜默變成資料遺失。Preference 類資料可接受；草稿、文件、表單、付款資料通常需要更清楚的 conflict handling。</p>
<p>修正方向是依資料價值分層。低價值設定用 LWW；高價值內容用 field merge、manual conflict 或 operation log。</p>
<h3 id="case-3delete-沒傳到離線裝置">Case 3：delete 沒傳到離線裝置</h3>
<p>Delete propagation 失敗的核心風險是 privacy / compliance 失效。使用者刪除 server 資料後，一台長期離線裝置重新上線又把舊資料同步回來。</p>
<p>修正方向是 tombstone + server authority。Server 要能拒絕過期 mutation，device 要能接收 delete tombstone 並 purge local state。</p>
<h2 id="操作檢查清單">操作檢查清單</h2>
<p>Local-first SQLite 設計要回答：</p>
<ol>
<li>哪些 table 是 local source of truth，哪些是 server replica。</li>
<li>Pending mutation 是否有 idempotency key 與 retry state。</li>
<li>Conflict policy 是 LWW、field merge、manual merge 還是 operation log。</li>
<li>Delete 是否有 tombstone、ack 與 local purge。</li>
<li>Sync worker 是否有 backoff、dead-letter、reconciliation。</li>
<li>Device backup 是否會保存已刪資料。</li>
<li>Server 是否能拒絕過期 local write。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/vendors/sqlite/mobile-desktop-embedded-store/" data-link-title="SQLite Mobile / Desktop Embedded Store" data-link-desc="SQLite 在 mobile、desktop、CLI、browser profile 與 embedded device 中承擔 local formal state 的資料責任、backup、privacy 與 sync boundary">Mobile / Desktop Embedded Store</a></li>
<li>Sibling：<a href="/blog/backend/01-database/vendors/sqlite/migrate-to-d1-turso/" data-link-title="SQLite to D1 / Turso Migration" data-link-desc="SQLite 轉向 Cloudflare D1、Turso / libSQL 的 edge driver、compatibility audit、data movement 與 rollback">SQLite to D1 / Turso</a>、<a href="/blog/backend/01-database/vendors/sqlite/d1-turso-libsql-comparison/" data-link-title="SQLite D1 / Turso / libSQL Comparison" data-link-desc="Cloudflare D1、Turso、libSQL 與 local SQLite 在 edge、replication、consistency、migration 與 vendor boundary 的比較">D1 / Turso / libSQL Comparison</a></li>
<li>卡片：<a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">Idempotency</a>、<a href="/blog/backend/knowledge-cards/eventual-consistency/" data-link-title="Eventual Consistency" data-link-desc="允許短暫不一致、最終收斂到同一資料狀態的一致性語意">Eventual Consistency</a>、<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">Stale Read</a></li>
<li>跨模組：<a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection</a></li>
</ul>
]]></content:encoded></item><item><title>SQLite Mobile / Desktop Embedded Store</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/mobile-desktop-embedded-store/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/mobile-desktop-embedded-store/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 適合 mobile、desktop、CLI 與 embedded device；本文聚焦 &lt;em>device-local formal state 的資料責任、backup、privacy 與 sync boundary&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;p>SQLite embedded store 的核心責任是讓 application process 在本機持有正式狀態。Mobile app、desktop app、browser profile、CLI tool 與 embedded device 常用 SQLite 保存 local data；這些資料可能只是 cache，也可能是使用者唯一資料來源。教學上要先判斷它是否承擔 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a>，再決定 backup、sync、privacy 與 migration 責任。&lt;/p>
&lt;p>本文的判讀錨點是：embedded SQLite 的 production boundary 在 device lifecycle，database server 層的邊界在這裡不適用。OS backup、app upgrade、device loss、profile corruption、local PII、multi-device sync 與 user export / delete 都是資料庫責任的一部分。&lt;/p>
&lt;h2 id="embedded-state-model">Embedded state model&lt;/h2>
&lt;p>Embedded state model 的核心責任是把 local database file 放回 application lifecycle。SQLite 是典型的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/embedded-database/" data-link-title="Embedded Database" data-link-desc="說明嵌入式資料庫如何隨 application process 運作，並把檔案生命週期責任交回應用">embedded database&lt;/a>：database file 通常跟著 app sandbox、user profile、CLI config directory 或 device storage 存在，它的 owner 是 application，而非獨立 DBA。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>場景&lt;/th>
 &lt;th>SQLite 資料角色&lt;/th>
 &lt;th>主要風險&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Mobile app&lt;/td>
 &lt;td>offline state、draft、cache、local profile&lt;/td>
 &lt;td>app upgrade、device loss、cloud backup leakage&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Desktop app&lt;/td>
 &lt;td>user profile、history、settings&lt;/td>
 &lt;td>profile corruption、manual file copy、multi-version app&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CLI tool&lt;/td>
 &lt;td>local index、metadata、state cache&lt;/td>
 &lt;td>command interruption、portable file path&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Browser / profile&lt;/td>
 &lt;td>cookies、history、bookmark 類資料&lt;/td>
 &lt;td>privacy、profile migration、lock collision&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Embedded device&lt;/td>
 &lt;td>offline event、sensor / config state&lt;/td>
 &lt;td>power loss、flash wear、delayed sync&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的重點是資料角色而非產品名稱。同樣是 SQLite file，cache 可以清掉重建；draft、local-only note、sensor event 或 user history 可能需要正式 backup / export / delete。&lt;/p>
&lt;h2 id="backup-與-export">Backup 與 export&lt;/h2>
&lt;p>Embedded backup 的核心責任是讓使用者或服務能從 device / profile failure 復原。Mobile / desktop / CLI 的 backup 路徑常和 OS backup、app export、cloud sync 或手動複製混在一起；SQLite file lifecycle 要明確。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite</a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 適合 mobile、desktop、CLI 與 embedded device；本文聚焦 <em>device-local formal state 的資料責任、backup、privacy 與 sync boundary</em>。</p></blockquote>
<p>SQLite embedded store 的核心責任是讓 application process 在本機持有正式狀態。Mobile app、desktop app、browser profile、CLI tool 與 embedded device 常用 SQLite 保存 local data；這些資料可能只是 cache，也可能是使用者唯一資料來源。教學上要先判斷它是否承擔 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>，再決定 backup、sync、privacy 與 migration 責任。</p>
<p>本文的判讀錨點是：embedded SQLite 的 production boundary 在 device lifecycle，database server 層的邊界在這裡不適用。OS backup、app upgrade、device loss、profile corruption、local PII、multi-device sync 與 user export / delete 都是資料庫責任的一部分。</p>
<h2 id="embedded-state-model">Embedded state model</h2>
<p>Embedded state model 的核心責任是把 local database file 放回 application lifecycle。SQLite 是典型的 <a href="/blog/backend/knowledge-cards/embedded-database/" data-link-title="Embedded Database" data-link-desc="說明嵌入式資料庫如何隨 application process 運作，並把檔案生命週期責任交回應用">embedded database</a>：database file 通常跟著 app sandbox、user profile、CLI config directory 或 device storage 存在，它的 owner 是 application，而非獨立 DBA。</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>SQLite 資料角色</th>
          <th>主要風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Mobile app</td>
          <td>offline state、draft、cache、local profile</td>
          <td>app upgrade、device loss、cloud backup leakage</td>
      </tr>
      <tr>
          <td>Desktop app</td>
          <td>user profile、history、settings</td>
          <td>profile corruption、manual file copy、multi-version app</td>
      </tr>
      <tr>
          <td>CLI tool</td>
          <td>local index、metadata、state cache</td>
          <td>command interruption、portable file path</td>
      </tr>
      <tr>
          <td>Browser / profile</td>
          <td>cookies、history、bookmark 類資料</td>
          <td>privacy、profile migration、lock collision</td>
      </tr>
      <tr>
          <td>Embedded device</td>
          <td>offline event、sensor / config state</td>
          <td>power loss、flash wear、delayed sync</td>
      </tr>
  </tbody>
</table>
<p>這張表的重點是資料角色而非產品名稱。同樣是 SQLite file，cache 可以清掉重建；draft、local-only note、sensor event 或 user history 可能需要正式 backup / export / delete。</p>
<h2 id="backup-與-export">Backup 與 export</h2>
<p>Embedded backup 的核心責任是讓使用者或服務能從 device / profile failure 復原。Mobile / desktop / CLI 的 backup 路徑常和 OS backup、app export、cloud sync 或手動複製混在一起；SQLite file lifecycle 要明確。</p>
<table>
  <thead>
      <tr>
          <th>路徑</th>
          <th>適合資料</th>
          <th>注意事項</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>OS / device backup</td>
          <td>user-owned local state</td>
          <td>local PII、encryption、restore compatibility</td>
      </tr>
      <tr>
          <td>App export</td>
          <td>使用者可攜資料</td>
          <td>schema version、format stability、privacy</td>
      </tr>
      <tr>
          <td><code>.backup</code> / snapshot</td>
          <td>application-managed backup</td>
          <td>live DB consistency、WAL sidecar handling</td>
      </tr>
      <tr>
          <td>Cloud sync</td>
          <td>multi-device state</td>
          <td>conflict、server authority、delete propagation</td>
      </tr>
  </tbody>
</table>
<p>Backup 設計要先決定 restore target。Restore 到同 app version、未來 app version、或不同 device，會帶來不同 schema compatibility 與 privacy requirement。</p>
<h2 id="privacy-與-local-pii">Privacy 與 local PII</h2>
<p>Embedded SQLite 的 privacy 責任是治理 device-local data。資料在 server DB 中通常有 access log、IAM、DLP 與 retention policy；進入 SQLite file 後，風險轉到 device encryption、app sandbox、backup retention、debug export 與 support bundle。</p>
<table>
  <thead>
      <tr>
          <th>風險</th>
          <th>真實情境</th>
          <th>控制方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Local PII</td>
          <td>profile、token、message、draft</td>
          <td>最小化欄位、加密敏感值、限制 export</td>
      </tr>
      <tr>
          <td>Backup leakage</td>
          <td>OS cloud backup 含 database file</td>
          <td>設定 backup exclusion 或加密</td>
      </tr>
      <tr>
          <td>Support bundle</td>
          <td>使用者回報問題附上 DB</td>
          <td>scrub / redaction、只匯出必要 table</td>
      </tr>
      <tr>
          <td>Delete request</td>
          <td>server 刪除但 device local 留存</td>
          <td>sync delete、local purge、retention evidence</td>
      </tr>
  </tbody>
</table>
<p>SQLite file 要進入資料保護盤點。若 local DB 保存敏感資料，應連到 <a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection</a> 與 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log</a> 的相同問題，只是控制面改在 device / app。</p>
<h2 id="app-upgrade-與-schema-compatibility">App upgrade 與 schema compatibility</h2>
<p>App upgrade 的核心責任是保證新版 binary 能安全打開舊 database file。Mobile / desktop app 的使用者不會按照 backend deployment order 升級；同一時間可能存在多個 app version 與多個 DB schema version。</p>
<table>
  <thead>
      <tr>
          <th>問題</th>
          <th>設計策略</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>新 app 打舊 DB</td>
          <td>startup migration、<code>user_version</code>、backup before migration</td>
      </tr>
      <tr>
          <td>舊 app 打新 DB</td>
          <td>backward-compatible column、feature gate、minimum supported version</td>
      </tr>
      <tr>
          <td>使用者降版</td>
          <td>export / import、read-only fallback、no-downgrade notice</td>
      </tr>
      <tr>
          <td>多裝置不同版本</td>
          <td>sync protocol version、server-side compatibility</td>
      </tr>
  </tbody>
</table>
<p>這些策略要和 <a href="/blog/backend/01-database/vendors/sqlite/schema-migration-versioning/" data-link-title="SQLite Schema Migration and Versioning" data-link-desc="SQLite schema migration、user_version、table rebuild、ALTER TABLE 限制、app release compatibility 與 migration evidence">Schema Migration / Versioning</a> 對齊。Embedded app 的 migration failure 通常直接影響使用者啟動體驗，因此 migration 要能快速、可恢復、可診斷。</p>
<h2 id="sync-boundary">Sync boundary</h2>
<p>Sync boundary 的核心責任是把 single-device SQLite 和 multi-device state 分開。SQLite 保存本地狀態；跨裝置同步需要 transport、identity、conflict resolution、delete propagation 與 server authority。</p>
<table>
  <thead>
      <tr>
          <th>Sync 需求</th>
          <th>SQLite 角色</th>
          <th>下一步路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單裝置 offline</td>
          <td>local source of truth</td>
          <td>SQLite + backup / export</td>
      </tr>
      <tr>
          <td>多裝置同步</td>
          <td>local replica / cache</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/local-first-sync-boundary/" data-link-title="SQLite Local-first Sync Boundary" data-link-desc="SQLite local-first app、multi-device sync、server authority、conflict resolution、delete propagation 與 offline-first trade-off">Local-first sync boundary</a></td>
      </tr>
      <tr>
          <td>即時多人協作</td>
          <td>local working copy</td>
          <td>server authority、CRDT、event log</td>
      </tr>
      <tr>
          <td>Server reporting</td>
          <td>local data upload / ETL</td>
          <td>API sync、queue、analytics store</td>
      </tr>
  </tbody>
</table>
<p>當 sync 需求出現時，SQLite 仍可作為 local store，但不再單獨承擔完整資料一致性。完整性要由 sync protocol 與 server-side validation 補上。</p>
<h2 id="production-踩雷">Production 踩雷</h2>
<h3 id="case-1把-cache-當正式資料">Case 1：把 cache 當正式資料</h3>
<p>Cache 被誤當正式資料的核心風險是清除 local DB 會造成不可恢復資料損失。許多 app 初期把 SQLite 當 cache；後來加入 draft、offline action 或 local-only setting，資料責任就改變了。</p>
<p>修正方向是逐 table 標示資料角色。Cache table 可清；formal state table 要 backup、migration、export 與 delete policy。</p>
<h3 id="case-2os-backup-帶走敏感資料">Case 2：OS backup 帶走敏感資料</h3>
<p>OS backup 的核心風險是 device-local PII 進入使用者或平台雲端備份。Server 端已刪除的資料，可能仍存在 device backup。</p>
<p>修正方向是決定哪些資料可被備份。Token、secret、敏感 PII 可排除或加密；user-owned content 則要提供 export / restore 語意。</p>
<h3 id="case-3app-upgrade-migration-失敗讓使用者卡在啟動頁">Case 3：App upgrade migration 失敗讓使用者卡在啟動頁</h3>
<p>Startup migration 失敗的核心風險是使用者卡在 app 啟動前，且修復能力有限。SQLite file 在使用者裝置上，SRE 通常需要透過 app update、support bundle 或 restore flow 處理。</p>
<p>修正方向是保留 pre-migration snapshot、提供 safe mode、收集匿名 schema / error evidence，並避免長 migration 放在 cold start。</p>
<h2 id="操作檢查清單">操作檢查清單</h2>
<p>Embedded SQLite 設計要回答：</p>
<ol>
<li>每張 table 是 cache、formal state、derived state 還是 sync queue。</li>
<li>Database file 在 app / OS 的哪個 storage boundary。</li>
<li>OS backup 是否包含 database file。</li>
<li>敏感欄位是否加密、排除或可清除。</li>
<li>App upgrade migration 是否有 pre-migration backup。</li>
<li>使用者 export / delete / support bundle 如何處理 SQLite data。</li>
<li>Multi-device sync 是否有 conflict 與 server authority 設計。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite overview</a></li>
<li>Sibling：<a href="/blog/backend/01-database/vendors/sqlite/local-first-sync-boundary/" data-link-title="SQLite Local-first Sync Boundary" data-link-desc="SQLite local-first app、multi-device sync、server authority、conflict resolution、delete propagation 與 offline-first trade-off">Local-first Sync Boundary</a>、<a href="/blog/backend/01-database/vendors/sqlite/schema-migration-versioning/" data-link-title="SQLite Schema Migration and Versioning" data-link-desc="SQLite schema migration、user_version、table rebuild、ALTER TABLE 限制、app release compatibility 與 migration evidence">Schema Migration / Versioning</a></li>
<li>操作：<a href="/blog/backend/01-database/vendors/sqlite/hands-on/" data-link-title="SQLite Hands-on 操作路線" data-link-desc="SQLite local file lab、backup / restore drill、WAL busy reproduction、migration fixture、D1 / Turso preview 的操作型章節設計">SQLite Hands-on</a></li>
<li>跨模組：<a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection</a></li>
<li>官方：<a href="https://www.sqlite.org/whentouse.html">SQLite Appropriate Uses</a>、<a href="https://www.sqlite.org/backup.html">SQLite Backup API</a></li>
</ul>
]]></content:encoded></item><item><title>SQLite Observability and Runbook</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/observability-runbook/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/observability-runbook/</guid><description>&lt;p>SQLite observability and runbook 的核心責任是把低操作成本服務補成可交接的 production evidence。SQLite 的元件少，但正式服務仍需要觀測 busy errors、WAL growth、backup freshness、restore drill、disk usage、migration result、file permission 與 application-level query health。&lt;/p>
&lt;p>本文的判讀錨點是：SQLite 的 observability 要貼近 file、process、filesystem 與 application。它通常沒有 server DB 那種長駐監控平面，因此 runbook 要把 signal 從 app metrics、log、scheduled job、file metadata 與 restore evidence 裡組出來。&lt;/p>
&lt;h2 id="signal-inventory">Signal Inventory&lt;/h2>
&lt;p>Signal inventory 的核心責任是列出 SQLite production 化後最能預告事故的訊號。這些訊號要放進 dashboard、log search 或 scheduled report，讓事故前後都能直接查。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Signal&lt;/th>
 &lt;th>來源&lt;/th>
 &lt;th>代表風險&lt;/th>
 &lt;th>建議反應&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>SQLITE_BUSY&lt;/code> count&lt;/td>
 &lt;td>app log / metric&lt;/td>
 &lt;td>writer contention、long reader&lt;/td>
 &lt;td>查 transaction duration、busy timeout&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>WAL file size&lt;/td>
 &lt;td>filesystem metric&lt;/td>
 &lt;td>checkpoint lag、long reader&lt;/td>
 &lt;td>查 checkpoint result、reader age&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Backup age&lt;/td>
 &lt;td>scheduled job metric&lt;/td>
 &lt;td>RPO 擴大&lt;/td>
 &lt;td>重跑 backup、檢查 storage&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Restore drill age&lt;/td>
 &lt;td>release evidence&lt;/td>
 &lt;td>RTO 信心下降&lt;/td>
 &lt;td>排程 restore drill&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Disk free&lt;/td>
 &lt;td>host / platform metric&lt;/td>
 &lt;td>write failure、checkpoint failure&lt;/td>
 &lt;td>清理、擴容、降級寫入&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Migration version&lt;/td>
 &lt;td>app startup / metadata&lt;/td>
 &lt;td>schema drift&lt;/td>
 &lt;td>block release、跑 validation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Integrity check result&lt;/td>
 &lt;td>maintenance job&lt;/td>
 &lt;td>corruption / storage issue&lt;/td>
 &lt;td>進入 restore decision&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>SQLITE_BUSY&lt;/code> 是 writer boundary 的最直接訊號。它可能代表長交易、read cursor 未關、parallel test 共用 DB、checkpoint 壓力或 write burst；runbook 要先查 query duration 與 transaction boundary，再調 busy timeout。&lt;/p>
&lt;p>WAL size 是 checkpoint 與 reader 壓力的綜合訊號。WAL 持續成長時，先確認是否有長 reader、backup process、未完成 transaction 或 checkpoint 失敗；接著才考慮手動 checkpoint。&lt;/p>
&lt;p>Backup age 是 RPO 的可觀測版本。若目標 RPO 是 5 分鐘，dashboard 就要顯示 last successful backup / replica time 與警戒線。&lt;/p>
&lt;h2 id="backup-evidence">Backup Evidence&lt;/h2>
&lt;p>Backup evidence 的核心責任是證明資料可被拿回來。SQLite backup 的完成標準包含成功建立備份、保存 sidecar 語意、恢復到新路徑、通過 integrity check、跑 application smoke test。&lt;/p></description><content:encoded><![CDATA[<p>SQLite observability and runbook 的核心責任是把低操作成本服務補成可交接的 production evidence。SQLite 的元件少，但正式服務仍需要觀測 busy errors、WAL growth、backup freshness、restore drill、disk usage、migration result、file permission 與 application-level query health。</p>
<p>本文的判讀錨點是：SQLite 的 observability 要貼近 file、process、filesystem 與 application。它通常沒有 server DB 那種長駐監控平面，因此 runbook 要把 signal 從 app metrics、log、scheduled job、file metadata 與 restore evidence 裡組出來。</p>
<h2 id="signal-inventory">Signal Inventory</h2>
<p>Signal inventory 的核心責任是列出 SQLite production 化後最能預告事故的訊號。這些訊號要放進 dashboard、log search 或 scheduled report，讓事故前後都能直接查。</p>
<table>
  <thead>
      <tr>
          <th>Signal</th>
          <th>來源</th>
          <th>代表風險</th>
          <th>建議反應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>SQLITE_BUSY</code> count</td>
          <td>app log / metric</td>
          <td>writer contention、long reader</td>
          <td>查 transaction duration、busy timeout</td>
      </tr>
      <tr>
          <td>WAL file size</td>
          <td>filesystem metric</td>
          <td>checkpoint lag、long reader</td>
          <td>查 checkpoint result、reader age</td>
      </tr>
      <tr>
          <td>Backup age</td>
          <td>scheduled job metric</td>
          <td>RPO 擴大</td>
          <td>重跑 backup、檢查 storage</td>
      </tr>
      <tr>
          <td>Restore drill age</td>
          <td>release evidence</td>
          <td>RTO 信心下降</td>
          <td>排程 restore drill</td>
      </tr>
      <tr>
          <td>Disk free</td>
          <td>host / platform metric</td>
          <td>write failure、checkpoint failure</td>
          <td>清理、擴容、降級寫入</td>
      </tr>
      <tr>
          <td>Migration version</td>
          <td>app startup / metadata</td>
          <td>schema drift</td>
          <td>block release、跑 validation</td>
      </tr>
      <tr>
          <td>Integrity check result</td>
          <td>maintenance job</td>
          <td>corruption / storage issue</td>
          <td>進入 restore decision</td>
      </tr>
  </tbody>
</table>
<p><code>SQLITE_BUSY</code> 是 writer boundary 的最直接訊號。它可能代表長交易、read cursor 未關、parallel test 共用 DB、checkpoint 壓力或 write burst；runbook 要先查 query duration 與 transaction boundary，再調 busy timeout。</p>
<p>WAL size 是 checkpoint 與 reader 壓力的綜合訊號。WAL 持續成長時，先確認是否有長 reader、backup process、未完成 transaction 或 checkpoint 失敗；接著才考慮手動 checkpoint。</p>
<p>Backup age 是 RPO 的可觀測版本。若目標 RPO 是 5 分鐘，dashboard 就要顯示 last successful backup / replica time 與警戒線。</p>
<h2 id="backup-evidence">Backup Evidence</h2>
<p>Backup evidence 的核心責任是證明資料可被拿回來。SQLite backup 的完成標準包含成功建立備份、保存 sidecar 語意、恢復到新路徑、通過 integrity check、跑 application smoke test。</p>
<table>
  <thead>
      <tr>
          <th>Evidence</th>
          <th>最小內容</th>
          <th>失敗時路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Backup job result</td>
          <td>timestamp、duration、file size、target</td>
          <td>重跑 job、檢查 credential / disk</td>
      </tr>
      <tr>
          <td>Restore artifact</td>
          <td>restored path、checksum、row count</td>
          <td>回前一份 backup、檢查 WAL / snapshot</td>
      </tr>
      <tr>
          <td>Integrity result</td>
          <td><code>PRAGMA integrity_check;</code></td>
          <td>停止寫入、進入 corruption triage</td>
      </tr>
      <tr>
          <td>Application smoke test</td>
          <td>啟動、讀核心頁、寫測試資料</td>
          <td>rollback、保留 evidence</td>
      </tr>
      <tr>
          <td>Retention note</td>
          <td>保存天數、刪除策略、legal hold</td>
          <td>更新 data protection policy</td>
      </tr>
  </tbody>
</table>
<p>SQLite 官方 <a href="https://www.sqlite.org/backup.html">backup API</a> 與 CLI <code>.backup</code> 是備份設計的基礎路由。WAL mode 下，直接複製單一 <code>.db</code> 檔容易漏掉 sidecar file 的時序；runbook 應使用 SQLite-aware backup 或經過 checkpoint / stop-the-world 的 snapshot。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sqlite3 app.db <span class="s2">&#34;.backup &#39;backup/app-2026-05-21.db&#39;&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sqlite3 backup/app-2026-05-21.db <span class="s2">&#34;PRAGMA integrity_check;&#34;</span></span></span></code></pre></div><p>這段命令提供最小 restore evidence 的起點。正式演練要把備份檔複製到隔離路徑，使用相同 application version 啟動，跑核心 read/write smoke test，再記錄耗時與失敗條件。</p>
<h2 id="migration-evidence">Migration Evidence</h2>
<p>Migration evidence 的核心責任是讓 SQLite schema change 可回退、可審查、可交接。單檔 DB 在使用者裝置或服務節點上升級時，migration 失敗會直接影響啟動、資料讀取與同步。</p>
<table>
  <thead>
      <tr>
          <th>Evidence</th>
          <th>內容</th>
          <th>Release gate</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema version</td>
          <td><code>PRAGMA user_version</code> 或 migration table</td>
          <td>app startup 比對 expected version</td>
      </tr>
      <tr>
          <td>Pre-migration snapshot</td>
          <td>backup path、size、checksum</td>
          <td>migration 前完成</td>
      </tr>
      <tr>
          <td>Validation query</td>
          <td>row count、FK check、domain invariant</td>
          <td>migration 後立即執行</td>
      </tr>
      <tr>
          <td>Smoke test</td>
          <td>核心 read/write workflow</td>
          <td>app release gate</td>
      </tr>
      <tr>
          <td>Rollback route</td>
          <td>restore snapshot 或 block startup</td>
          <td>migration 失敗時啟動</td>
      </tr>
  </tbody>
</table>
<p>Migration log 要包含版本、耗時、row count、錯誤、validation result 與 rollback decision。若 SQLite file 位於 end-user device，log 還要能被使用者支援流程收集，避免事故只停在「app 開不起來」。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">user_version</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">foreign_key_check</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="p">;</span></span></span></code></pre></div><p>這些 query 是 migration 後的最小 evidence。正式服務要再補 domain-specific invariant，例如「所有 active subscription 都有 owner」、「所有 pending mutation 都有 idempotency key」。</p>
<h2 id="incident-runbook">Incident Runbook</h2>
<p>Incident runbook 的核心責任是把 SQLite 事故分流到正確處置。SQLite 常見事故包含 disk full、busy storm、WAL growth、bad migration、corruption suspicion、backup failure 與 permission error。</p>
<table>
  <thead>
      <tr>
          <th>Incident</th>
          <th>第一個判讀問題</th>
          <th>立即處置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Busy storm</td>
          <td>有長 transaction 或 write burst 嗎</td>
          <td>暫停非必要寫入、查 transaction duration</td>
      </tr>
      <tr>
          <td>Disk full</td>
          <td>DB / WAL / backup 哪個吃掉空間</td>
          <td>停止寫入、清理 backup、擴容</td>
      </tr>
      <tr>
          <td>WAL growth</td>
          <td>checkpoint 被誰阻擋</td>
          <td>查 reader、跑 checkpoint evidence</td>
      </tr>
      <tr>
          <td>Bad migration</td>
          <td>schema version 與 app version 是否一致</td>
          <td>停止 rollout、restore snapshot、保留 failed DB</td>
      </tr>
      <tr>
          <td>Corruption signal</td>
          <td>integrity check 是否失敗</td>
          <td>進入 read-only、restore last good backup</td>
      </tr>
      <tr>
          <td>Backup failure</td>
          <td>credential、network、destination 是否可用</td>
          <td>切換 destination、補跑 restore drill</td>
      </tr>
  </tbody>
</table>
<p>Busy storm 要先保護使用者操作。可以降低 write endpoint、停用背景 job、延長 retry backoff，然後用 log 查最長 transaction 與最多重試的 query。</p>
<p>Disk full 要先停止寫入。SQLite 在 disk full 時可能讓 write / checkpoint / backup 同時失敗；runbook 要保留剩餘空間、DB file、WAL file、backup directory 與 tmp directory 的大小。</p>
<p>Bad migration 要保留 failed artifact。先複製 failed DB 到 evidence path，記錄 schema version、app version、migration id、validation error，再執行 rollback。</p>
<h2 id="dashboard-and-alert-route">Dashboard and Alert Route</h2>
<p>Dashboard and alert route 的核心責任是讓 SQLite 被納入正式服務的可觀測系統。SQLite signal 常來自 application，因此 metric 命名要接近操作問題。</p>
<table>
  <thead>
      <tr>
          <th>Metric name example</th>
          <th>類型</th>
          <th>用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>sqlite_busy_total</code></td>
          <td>counter</td>
          <td>writer contention</td>
      </tr>
      <tr>
          <td><code>sqlite_query_duration_ms</code></td>
          <td>histogram</td>
          <td>slow query / long transaction</td>
      </tr>
      <tr>
          <td><code>sqlite_wal_size_bytes</code></td>
          <td>gauge</td>
          <td>checkpoint pressure</td>
      </tr>
      <tr>
          <td><code>sqlite_backup_age_seconds</code></td>
          <td>gauge</td>
          <td>RPO evidence</td>
      </tr>
      <tr>
          <td><code>sqlite_restore_drill_age_days</code></td>
          <td>gauge</td>
          <td>RTO confidence</td>
      </tr>
      <tr>
          <td><code>sqlite_disk_free_bytes</code></td>
          <td>gauge</td>
          <td>disk full prevention</td>
      </tr>
      <tr>
          <td><code>sqlite_migration_version</code></td>
          <td>gauge</td>
          <td>schema drift</td>
      </tr>
  </tbody>
</table>
<p>Alert 要連到 runbook，並提供可執行的第一步。每個 alert 至少要有 owner、severity、first query、rollback condition 與 escalation route。</p>
<p>Log schema 要保留 query category，而非只記原始 SQL。正式服務通常應避免把完整 SQL 與 PII 直接寫入 log；可以記 operation name、duration、row count、error code、busy retry count 與 correlation id。</p>
<h2 id="handoff">Handoff</h2>
<p>Handoff 的核心責任是讓下一個維護者知道 SQLite service 的邊界。交接文件要把「誰負責檔案」、「誰負責備份」、「誰能執行 restore」、「何時升級資料庫」寫清楚。</p>
<p>最小 handoff 包含：</p>
<ol>
<li>Database file path、sidecar file policy、journal mode 與 PRAGMA baseline。</li>
<li>Backup command、destination、retention、last restore drill。</li>
<li>Migration command、schema version、rollback route。</li>
<li>Alert list、dashboard link、incident owner。</li>
<li>Known limits：writer concurrency、file size、edge / sync boundary。</li>
<li>Next route：PostgreSQL、D1 / Turso、Litestream / LiteFS 的評估條件。</li>
</ol>
<p>Handoff 的重點是把低操作成本保留下來。SQLite 的好處來自少元件；可交接文件讓少元件不等於少 evidence。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>Observability / runbook 完成後，下一步要接到具體演練。Backup 與 restore 讀 <a href="/blog/backend/01-database/vendors/sqlite/hands-on/backup-restore-drill/" data-link-title="SQLite Backup Restore Drill" data-link-desc="SQLite .backup、VACUUM INTO、restore validation、sidecar file handling 與 RPO / RTO note 的操作說明">SQLite backup restore drill</a>；WAL 與 busy 讀 <a href="/blog/backend/01-database/vendors/sqlite/hands-on/wal-busy-reproduction/" data-link-title="SQLite WAL Busy Reproduction" data-link-desc="SQLite long transaction、SQLITE_BUSY、busy_timeout、checkpoint growth 與 writer queue 的操作說明">WAL busy reproduction</a>；正式服務的 evidence 可對齊 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a> 與 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</p>
]]></content:encoded></item><item><title>SQLite PRAGMA Tuning and Performance</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/pragma-tuning-performance/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/pragma-tuning-performance/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 的容量規劃要點；本文聚焦 &lt;em>PRAGMA 設定如何變成 durability、latency、檔案大小與 restore risk 的取捨&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;p>SQLite PRAGMA tuning 的核心責任是把單檔資料庫的行為固定成可重複、可觀測、可回退的操作契約。SQLite 的許多重要行為由 connection-level 或 database-level PRAGMA 控制；這些設定看起來像小開關，實際上會影響 crash recovery、commit latency、reader / writer 衝突、檔案大小與測試一致性。&lt;/p>
&lt;p>本文的判讀錨點是：PRAGMA 是 durability / latency / maintenance 的顯性取捨，而非效能魔法。Production runbook 要記錄設定值、設定時機、驗證 query 與回退條件，避免不同 process、test runner 或 migration tool 用不同 SQLite 行為。&lt;/p>
&lt;h2 id="baseline-pragma">Baseline PRAGMA&lt;/h2>
&lt;p>SQLite baseline PRAGMA 的責任是讓 application 每次啟動都進入同一個資料庫模式。對 production-like local store、small backend 或 test fixture，建議把 journal、sync、foreign key、busy timeout 與 checkpoint 明確設定，而非依賴語言 binding 預設值。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">PRAGMA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">journal_mode&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">WAL&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">PRAGMA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">synchronous&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">NORMAL&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">PRAGMA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">foreign_keys&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">PRAGMA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">busy_timeout&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">5000&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">PRAGMA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">wal_autocheckpoint&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">1000&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>設定&lt;/th>
 &lt;th>服務責任&lt;/th>
 &lt;th>驗證方式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>journal_mode=WAL&lt;/code>&lt;/td>
 &lt;td>降低 reader / writer 衝突&lt;/td>
 &lt;td>回傳值為 &lt;code>wal&lt;/code>，觀察 &lt;code>-wal&lt;/code> file&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>synchronous=NORMAL&lt;/code>&lt;/td>
 &lt;td>平衡 fsync cost 與 crash durability&lt;/td>
 &lt;td>查 &lt;code>PRAGMA synchronous&lt;/code>，跑 restore drill&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>foreign_keys=ON&lt;/code>&lt;/td>
 &lt;td>啟用 FK enforcement&lt;/td>
 &lt;td>&lt;code>PRAGMA foreign_key_check&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>busy_timeout&lt;/code>&lt;/td>
 &lt;td>吸收短暫 writer queue&lt;/td>
 &lt;td>記錄 busy wait 與 timeout rate&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>wal_autocheckpoint&lt;/code>&lt;/td>
 &lt;td>控制 WAL growth cadence&lt;/td>
 &lt;td>觀察 WAL size 與 checkpoint duration&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的重點是把設定與 evidence 綁在一起。若某個 PRAGMA 缺少成功訊號與失敗訊號，就先維持保守預設；盲目追求「最快」通常會把風險推到 power loss、restore 或長尾 latency。&lt;/p>
&lt;h2 id="journal_mode-與-wal-boundary">&lt;code>journal_mode&lt;/code> 與 WAL boundary&lt;/h2>
&lt;p>&lt;code>journal_mode&lt;/code> 的核心責任是決定 transaction 如何保護原始資料。SQLite 預設 rollback journal 對簡單場景合理；WAL mode 則讓 reader 可以在 writer append WAL 時保有 snapshot，適合多 reader、短寫入、互動式 workload。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>模式&lt;/th>
 &lt;th>適合情境&lt;/th>
 &lt;th>注意事項&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>DELETE&lt;/code>&lt;/td>
 &lt;td>最簡單、低併發、短生命週期檔案&lt;/td>
 &lt;td>write / read 衝突較明顯&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>WAL&lt;/code>&lt;/td>
 &lt;td>read-heavy、local app、小型 API&lt;/td>
 &lt;td>需要治理 &lt;code>-wal&lt;/code>、&lt;code>-shm&lt;/code>、checkpoint&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>MEMORY&lt;/code>&lt;/td>
 &lt;td>暫存測試、可丟資料&lt;/td>
 &lt;td>crash 後 recovery 風險高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>OFF&lt;/code>&lt;/td>
 &lt;td>可重建資料、一次性 bulk load&lt;/td>
 &lt;td>production formal state 應避開&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>WAL mode 是多數 production-like SQLite 的 baseline，但它也引入 sidecar file 與 checkpoint 責任。完整判讀見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/wal-concurrency-locking/" data-link-title="SQLite WAL Concurrency and Locking" data-link-desc="SQLite WAL mode 如何降低 reader / writer 衝突、保留 single writer boundary，並用 SQLITE_BUSY、WAL growth、checkpoint 訊號判斷 production 上限">WAL concurrency / locking&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite</a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 的容量規劃要點；本文聚焦 <em>PRAGMA 設定如何變成 durability、latency、檔案大小與 restore risk 的取捨</em>。</p></blockquote>
<p>SQLite PRAGMA tuning 的核心責任是把單檔資料庫的行為固定成可重複、可觀測、可回退的操作契約。SQLite 的許多重要行為由 connection-level 或 database-level PRAGMA 控制；這些設定看起來像小開關，實際上會影響 crash recovery、commit latency、reader / writer 衝突、檔案大小與測試一致性。</p>
<p>本文的判讀錨點是：PRAGMA 是 durability / latency / maintenance 的顯性取捨，而非效能魔法。Production runbook 要記錄設定值、設定時機、驗證 query 與回退條件，避免不同 process、test runner 或 migration tool 用不同 SQLite 行為。</p>
<h2 id="baseline-pragma">Baseline PRAGMA</h2>
<p>SQLite baseline PRAGMA 的責任是讓 application 每次啟動都進入同一個資料庫模式。對 production-like local store、small backend 或 test fixture，建議把 journal、sync、foreign key、busy timeout 與 checkpoint 明確設定，而非依賴語言 binding 預設值。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">journal_mode</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">WAL</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">synchronous</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">NORMAL</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">foreign_keys</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ON</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">busy_timeout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">5000</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">wal_autocheckpoint</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1000</span><span class="p">;</span></span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>設定</th>
          <th>服務責任</th>
          <th>驗證方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>journal_mode=WAL</code></td>
          <td>降低 reader / writer 衝突</td>
          <td>回傳值為 <code>wal</code>，觀察 <code>-wal</code> file</td>
      </tr>
      <tr>
          <td><code>synchronous=NORMAL</code></td>
          <td>平衡 fsync cost 與 crash durability</td>
          <td>查 <code>PRAGMA synchronous</code>，跑 restore drill</td>
      </tr>
      <tr>
          <td><code>foreign_keys=ON</code></td>
          <td>啟用 FK enforcement</td>
          <td><code>PRAGMA foreign_key_check</code></td>
      </tr>
      <tr>
          <td><code>busy_timeout</code></td>
          <td>吸收短暫 writer queue</td>
          <td>記錄 busy wait 與 timeout rate</td>
      </tr>
      <tr>
          <td><code>wal_autocheckpoint</code></td>
          <td>控制 WAL growth cadence</td>
          <td>觀察 WAL size 與 checkpoint duration</td>
      </tr>
  </tbody>
</table>
<p>這張表的重點是把設定與 evidence 綁在一起。若某個 PRAGMA 缺少成功訊號與失敗訊號，就先維持保守預設；盲目追求「最快」通常會把風險推到 power loss、restore 或長尾 latency。</p>
<h2 id="journal_mode-與-wal-boundary"><code>journal_mode</code> 與 WAL boundary</h2>
<p><code>journal_mode</code> 的核心責任是決定 transaction 如何保護原始資料。SQLite 預設 rollback journal 對簡單場景合理；WAL mode 則讓 reader 可以在 writer append WAL 時保有 snapshot，適合多 reader、短寫入、互動式 workload。</p>
<table>
  <thead>
      <tr>
          <th>模式</th>
          <th>適合情境</th>
          <th>注意事項</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>DELETE</code></td>
          <td>最簡單、低併發、短生命週期檔案</td>
          <td>write / read 衝突較明顯</td>
      </tr>
      <tr>
          <td><code>WAL</code></td>
          <td>read-heavy、local app、小型 API</td>
          <td>需要治理 <code>-wal</code>、<code>-shm</code>、checkpoint</td>
      </tr>
      <tr>
          <td><code>MEMORY</code></td>
          <td>暫存測試、可丟資料</td>
          <td>crash 後 recovery 風險高</td>
      </tr>
      <tr>
          <td><code>OFF</code></td>
          <td>可重建資料、一次性 bulk load</td>
          <td>production formal state 應避開</td>
      </tr>
  </tbody>
</table>
<p>WAL mode 是多數 production-like SQLite 的 baseline，但它也引入 sidecar file 與 checkpoint 責任。完整判讀見 <a href="/blog/backend/01-database/vendors/sqlite/wal-concurrency-locking/" data-link-title="SQLite WAL Concurrency and Locking" data-link-desc="SQLite WAL mode 如何降低 reader / writer 衝突、保留 single writer boundary，並用 SQLITE_BUSY、WAL growth、checkpoint 訊號判斷 production 上限">WAL concurrency / locking</a>。</p>
<h2 id="synchronouscommit-latency-與資料損失窗口"><code>synchronous</code>：commit latency 與資料損失窗口</h2>
<p><code>synchronous</code> 的核心責任是控制 SQLite 在關鍵時刻要求 storage flush 的強度。官方 PRAGMA 文件說明 WAL mode 下 <code>NORMAL</code> 會把 sync 主要放在 checkpoint 路徑；這通常讓 commit 更快，但 crash durability 的語意要由 service owner 接受。</p>
<table>
  <thead>
      <tr>
          <th>設定</th>
          <th>服務語意</th>
          <th>適合情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>FULL</code></td>
          <td>更保守的 durability</td>
          <td>金錢、ledger、不可重建 local state</td>
      </tr>
      <tr>
          <td><code>NORMAL</code></td>
          <td>多數 WAL production-like baseline</td>
          <td>local app、小型服務、可接受極小 crash window</td>
      </tr>
      <tr>
          <td><code>OFF</code></td>
          <td>追求速度，放棄重要 durability</td>
          <td>scratch DB、可重建 cache、bulk import staging</td>
      </tr>
  </tbody>
</table>
<p><code>synchronous=OFF</code> 要被視為明確風險接受。若資料是 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>，設定檔、runbook 與 review 都應避免把 staging 的快速設定帶進 production。</p>
<h2 id="cachemmap-與-memory-pressure">Cache、mmap 與 memory pressure</h2>
<p>SQLite memory tuning 的核心責任是降低 read path I/O，同時避免把 device / container memory 壓到不可控。<code>cache_size</code> 控制 SQLite page cache；<code>mmap_size</code> 讓讀取可透過 memory-mapped I/O 加速，但仍受平台、檔案大小與 memory budget 影響。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">cache_size</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="o">-</span><span class="mi">64000</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">mmap_size</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">268435456</span><span class="p">;</span></span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>設定</th>
          <th>改善目標</th>
          <th>觀測訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>cache_size</code></td>
          <td>減少重複 page read</td>
          <td>query latency、disk read、memory usage</td>
      </tr>
      <tr>
          <td><code>mmap_size</code></td>
          <td>降低 read syscall cost</td>
          <td>p95 / p99 read latency、address space</td>
      </tr>
      <tr>
          <td><code>temp_store</code></td>
          <td>控制 temp table 位置</td>
          <td>sort / join query latency、memory pressure</td>
      </tr>
  </tbody>
</table>
<p>Memory 設定要和 workload size 一起看。Desktop app、mobile app、edge worker、container service 的 memory ceiling 不同；把 server 上的設定複製到 mobile 或 edge runtime 會讓風險轉移到 OOM 或 OS reclaim。</p>
<h2 id="vacuum-與檔案大小治理">Vacuum 與檔案大小治理</h2>
<p>Vacuum 設定的核心責任是控制 delete 後的空間回收。SQLite delete row 後，database file 不會自然縮小；<code>auto_vacuum</code> 要在 database 建立早期決定，後續切換通常需要 <code>VACUUM</code> 重整整個 database。</p>
<table>
  <thead>
      <tr>
          <th>設定 / 操作</th>
          <th>適合情境</th>
          <th>風險 / 成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>auto_vacuum=NONE</code></td>
          <td>資料量穩定、delete 少</td>
          <td>檔案可能長期保持高水位</td>
      </tr>
      <tr>
          <td><code>auto_vacuum=INCREMENTAL</code></td>
          <td>需要逐步回收空間</td>
          <td>需要排程 <code>incremental_vacuum</code></td>
      </tr>
      <tr>
          <td><code>VACUUM</code></td>
          <td>maintenance window、重整資料庫</td>
          <td>需要額外空間與 I/O，可能影響服務</td>
      </tr>
      <tr>
          <td><code>VACUUM INTO</code></td>
          <td>compact copy / backup</td>
          <td>產出新檔，適合 restore drill 或 export</td>
      </tr>
  </tbody>
</table>
<p>檔案大小治理要接到 backup 成本。Database file 長期膨脹會放大備份時間、restore 時間與 edge deploy artifact size；若服務有大量 delete / churn，vacuum policy 要被寫進 runbook。</p>
<h2 id="production-踩雷">Production 踩雷</h2>
<h3 id="case-1pragma-只在某個-connection-設定">Case 1：PRAGMA 只在某個 connection 設定</h3>
<p>Connection-level PRAGMA 的核心風險是不同程式路徑行為不一致。Application 啟動時設了 <code>foreign_keys=ON</code>，migration tool 或 test runner 沒設，就會出現 production / migration / test 三種語意。</p>
<p>修正方向是把 baseline PRAGMA 放進 shared DB open path，並在 startup health check 印出設定值。Migration CLI、background worker、test fixture 都要共用同一份 connection initialization。</p>
<h3 id="case-2synchronousoff-從測試環境流到正式資料">Case 2：<code>synchronous=OFF</code> 從測試環境流到正式資料</h3>
<p>快速測試設定外流的核心風險是資料損失只在 crash 後出現。平常 query 都正常，直到 power loss、container kill 或 host crash 後，資料庫出現落差。</p>
<p>修正方向是設定分層。Test / benchmark 可以用 faster profile；formal state profile 要用 <code>NORMAL</code> 或 <code>FULL</code>，並要求 restore drill。</p>
<h3 id="case-3wal-growth-被誤判成資料成長">Case 3：WAL growth 被誤判成資料成長</h3>
<p>WAL growth 的核心風險是 checkpoint 問題被當成容量問題。Disk alert 看到 <code>db-wal</code> 變大，若只擴 disk，長 reader 或 checkpoint starvation 仍會持續。</p>
<p>修正方向是把 WAL size、checkpoint return 與 long reader 一起看。先找 reader lifecycle，再調 checkpoint cadence。</p>
<h3 id="case-4vacuum-在高峰期執行">Case 4：Vacuum 在高峰期執行</h3>
<p>Vacuum 的核心風險是把 maintenance I/O 放到使用者路徑。檔案縮小是好事，但 full vacuum 會消耗 I/O 與時間，對 mobile / desktop / small backend 都可能造成卡頓。</p>
<p>修正方向是把 vacuum 當 maintenance job。大檔案用 <code>incremental_vacuum</code> 或低流量窗口；備份前的 compact copy 可考慮 <code>VACUUM INTO</code>。</p>
<h2 id="操作檢查清單">操作檢查清單</h2>
<p>SQLite PRAGMA runbook 至少要記錄：</p>
<ol>
<li>所有 connection 初始化時執行的 baseline PRAGMA。</li>
<li><code>journal_mode</code> 實際回傳值與 sidecar file 位置。</li>
<li><code>synchronous</code> profile 與資料風險接受者。</li>
<li><code>busy_timeout</code> 值、busy wait metric、timeout threshold。</li>
<li><code>wal_autocheckpoint</code>、manual checkpoint cadence 與 WAL size alert。</li>
<li><code>cache_size</code> / <code>mmap_size</code> 對 memory budget 的影響。</li>
<li><code>auto_vacuum</code> / <code>VACUUM</code> / <code>VACUUM INTO</code> 的 maintenance window。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite overview</a></li>
<li>前置：<a href="/blog/backend/01-database/vendors/sqlite/wal-concurrency-locking/" data-link-title="SQLite WAL Concurrency and Locking" data-link-desc="SQLite WAL mode 如何降低 reader / writer 衝突、保留 single writer boundary，並用 SQLITE_BUSY、WAL growth、checkpoint 訊號判斷 production 上限">WAL concurrency / locking</a></li>
<li>操作：<a href="/blog/backend/01-database/vendors/sqlite/hands-on/" data-link-title="SQLite Hands-on 操作路線" data-link-desc="SQLite local file lab、backup / restore drill、WAL busy reproduction、migration fixture、D1 / Turso preview 的操作型章節設計">SQLite Hands-on</a></li>
<li>平行：<a href="/blog/backend/01-database/vendors/sqlite/observability-runbook/" data-link-title="SQLite Observability and Runbook" data-link-desc="SQLite production runbook、backup evidence、WAL growth、busy errors、disk usage、restore drill 與 incident route">Observability / runbook</a></li>
<li>官方：<a href="https://www.sqlite.org/pragma.html">SQLite PRAGMA</a>、<a href="https://www.sqlite.org/lang_vacuum.html">SQLite VACUUM</a>、<a href="https://www.sqlite.org/wal.html">SQLite WAL</a></li>
</ul>
]]></content:encoded></item><item><title>SQLite Schema Migration and Versioning</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/schema-migration-versioning/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/schema-migration-versioning/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 的 embedded / single-file 定位；本文聚焦 &lt;em>schema version、ALTER TABLE boundary、table rebuild migration 與 application release compatibility&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;p>SQLite schema migration 的核心責任是讓單檔資料庫隨 application release 安全演進。SQLite 沒有獨立 database server，也沒有 DBA 在 server 端統一套 migration；migration 常在 application startup、CLI command、mobile app upgrade 或 desktop app launch 時發生，因此 schema version、binary compatibility、backup 與 rollback 要放在同一個 release contract 中設計。&lt;/p>
&lt;p>本文的判讀錨點是：SQLite migration 同時改資料庫檔案與 application 能讀的資料格式。只要使用者或服務可能拿舊 binary 打開新 database，或新 binary 打開舊 database，migration 就要處理 forward / backward compatibility，而不只是 SQL 成功執行。&lt;/p>
&lt;h2 id="version-model">Version model&lt;/h2>
&lt;p>SQLite schema versioning 的服務責任是讓 application 能判斷 database file 目前處於哪個契約。SQLite 提供 &lt;code>PRAGMA user_version&lt;/code> 作為 application-controlled integer；更複雜的服務也可以用 migration table 記錄多步驟版本、checksum 與執行時間。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">PRAGMA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">user_version&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">PRAGMA&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">user_version&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">2026052101&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>方式&lt;/th>
 &lt;th>適合情境&lt;/th>
 &lt;th>優點&lt;/th>
 &lt;th>邊界&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>user_version&lt;/code>&lt;/td>
 &lt;td>mobile / desktop / CLI single file&lt;/td>
 &lt;td>簡單、內建、開檔即可讀&lt;/td>
 &lt;td>只能存一個整數，缺 migration history&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>migration table&lt;/td>
 &lt;td>small backend、多人維護 schema&lt;/td>
 &lt;td>可記錄每步 migration 與 owner&lt;/td>
 &lt;td>需要先建立 table 與初始化流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>external manifest&lt;/td>
 &lt;td>fixture、artifact、read-only DB&lt;/td>
 &lt;td>可和 release artifact 綁定&lt;/td>
 &lt;td>DB file 本身不含完整 history&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Version model 要在第一版就定義。沒有版本欄位的 SQLite file 仍可 migration，但 application 只能靠 introspection 猜 schema，會讓 upgrade / downgrade runbook 複雜化。&lt;/p>
&lt;h2 id="alter-table-boundary">ALTER TABLE boundary&lt;/h2>
&lt;p>SQLite ALTER TABLE 的核心責任是處理有限集合的 schema 變更。官方文件說明 SQLite 支援 rename table、rename column、add column、drop column；更複雜的變更要走 table rebuild pattern。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>變更類型&lt;/th>
 &lt;th>SQLite 支援形態&lt;/th>
 &lt;th>操作判讀&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Rename table / column&lt;/td>
 &lt;td>直接 ALTER，版本差異影響 trigger / view&lt;/td>
 &lt;td>需要測 trigger、view、FK reference&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Add column&lt;/td>
 &lt;td>多數情境很快，受 default / constraint 限制&lt;/td>
 &lt;td>適合 expand migration&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Drop column&lt;/td>
 &lt;td>需要檢查 index、constraint、trigger、view&lt;/td>
 &lt;td>可能掃資料，需 maintenance window&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Change type / constraint&lt;/td>
 &lt;td>通常走 table rebuild&lt;/td>
 &lt;td>需要完整 copy、foreign key check、validation&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>SQLite schema 存在 &lt;code>sqlite_schema&lt;/code> 的 SQL text 中；這讓檔案格式簡潔，但也讓 ALTER TABLE 的安全條件和 server SQL 不同。Production migration 應優先用官方建議的 rebuild procedure，而非直接修改 &lt;code>sqlite_schema&lt;/code>。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite</a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 的 embedded / single-file 定位；本文聚焦 <em>schema version、ALTER TABLE boundary、table rebuild migration 與 application release compatibility</em>。</p></blockquote>
<p>SQLite schema migration 的核心責任是讓單檔資料庫隨 application release 安全演進。SQLite 沒有獨立 database server，也沒有 DBA 在 server 端統一套 migration；migration 常在 application startup、CLI command、mobile app upgrade 或 desktop app launch 時發生，因此 schema version、binary compatibility、backup 與 rollback 要放在同一個 release contract 中設計。</p>
<p>本文的判讀錨點是：SQLite migration 同時改資料庫檔案與 application 能讀的資料格式。只要使用者或服務可能拿舊 binary 打開新 database，或新 binary 打開舊 database，migration 就要處理 forward / backward compatibility，而不只是 SQL 成功執行。</p>
<h2 id="version-model">Version model</h2>
<p>SQLite schema versioning 的服務責任是讓 application 能判斷 database file 目前處於哪個契約。SQLite 提供 <code>PRAGMA user_version</code> 作為 application-controlled integer；更複雜的服務也可以用 migration table 記錄多步驟版本、checksum 與執行時間。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">user_version</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">user_version</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">2026052101</span><span class="p">;</span></span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>方式</th>
          <th>適合情境</th>
          <th>優點</th>
          <th>邊界</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>user_version</code></td>
          <td>mobile / desktop / CLI single file</td>
          <td>簡單、內建、開檔即可讀</td>
          <td>只能存一個整數，缺 migration history</td>
      </tr>
      <tr>
          <td>migration table</td>
          <td>small backend、多人維護 schema</td>
          <td>可記錄每步 migration 與 owner</td>
          <td>需要先建立 table 與初始化流程</td>
      </tr>
      <tr>
          <td>external manifest</td>
          <td>fixture、artifact、read-only DB</td>
          <td>可和 release artifact 綁定</td>
          <td>DB file 本身不含完整 history</td>
      </tr>
  </tbody>
</table>
<p>Version model 要在第一版就定義。沒有版本欄位的 SQLite file 仍可 migration，但 application 只能靠 introspection 猜 schema，會讓 upgrade / downgrade runbook 複雜化。</p>
<h2 id="alter-table-boundary">ALTER TABLE boundary</h2>
<p>SQLite ALTER TABLE 的核心責任是處理有限集合的 schema 變更。官方文件說明 SQLite 支援 rename table、rename column、add column、drop column；更複雜的變更要走 table rebuild pattern。</p>
<table>
  <thead>
      <tr>
          <th>變更類型</th>
          <th>SQLite 支援形態</th>
          <th>操作判讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Rename table / column</td>
          <td>直接 ALTER，版本差異影響 trigger / view</td>
          <td>需要測 trigger、view、FK reference</td>
      </tr>
      <tr>
          <td>Add column</td>
          <td>多數情境很快，受 default / constraint 限制</td>
          <td>適合 expand migration</td>
      </tr>
      <tr>
          <td>Drop column</td>
          <td>需要檢查 index、constraint、trigger、view</td>
          <td>可能掃資料，需 maintenance window</td>
      </tr>
      <tr>
          <td>Change type / constraint</td>
          <td>通常走 table rebuild</td>
          <td>需要完整 copy、foreign key check、validation</td>
      </tr>
  </tbody>
</table>
<p>SQLite schema 存在 <code>sqlite_schema</code> 的 SQL text 中；這讓檔案格式簡潔，但也讓 ALTER TABLE 的安全條件和 server SQL 不同。Production migration 應優先用官方建議的 rebuild procedure，而非直接修改 <code>sqlite_schema</code>。</p>
<h2 id="table-rebuild-migration">Table rebuild migration</h2>
<p>Table rebuild migration 的服務責任是安全完成 SQLite 直接 ALTER 難以表達的變更。官方 ALTER TABLE 文件建議的 generalized procedure 是建立新 table、copy data、drop old、rename new、重建 index / trigger / view、跑 foreign key check、commit。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">BEGIN</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">foreign_keys</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">OFF</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">new_orders</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="n">id</span><span class="w"> </span><span class="nb">INTEGER</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">  </span><span class="n">status</span><span class="w"> </span><span class="nb">TEXT</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span><span class="n">paid_at</span><span class="w"> </span><span class="nb">TEXT</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">new_orders</span><span class="w"> </span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">status</span><span class="p">,</span><span class="w"> </span><span class="n">paid_at</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">status</span><span class="p">,</span><span class="w"> </span><span class="n">paid_at</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w"></span><span class="k">DROP</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w"></span><span class="k">ALTER</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">new_orders</span><span class="w"> </span><span class="k">RENAME</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="n">orders</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">foreign_key_check</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">user_version</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">2026052101</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w"></span><span class="k">COMMIT</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">foreign_keys</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ON</span><span class="p">;</span></span></span></code></pre></div><p>這段範例是教學骨架，而非可直接複製到所有 schema 的萬用腳本。真實 migration 要先保存 index、trigger、view 與 FK reference，再依 schema 重建；有資料量時還要考慮 copy duration、disk 空間與 rollback snapshot。</p>
<h2 id="app-release-compatibility">App release compatibility</h2>
<p>SQLite migration 的 application compatibility 來自 binary 與 DB file 的同步問題。Server SQL migration 通常有 central deploy order；SQLite file 可能跟著使用者裝置、desktop profile、CLI artifact 或 edge deploy 留在不同版本。</p>
<table>
  <thead>
      <tr>
          <th>相容性問題</th>
          <th>真實情境</th>
          <th>設計策略</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>新 app 打開舊 DB</td>
          <td>使用者升級 app</td>
          <td>startup migration、read compatibility</td>
      </tr>
      <tr>
          <td>舊 app 打開新 DB</td>
          <td>使用者 downgrade、同步舊 binary</td>
          <td>保留 backward-compatible column、feature gate</td>
      </tr>
      <tr>
          <td>多裝置不同版本</td>
          <td>local-first / sync app</td>
          <td>sync protocol version、server authority</td>
      </tr>
      <tr>
          <td>fixture 與 production drift</td>
          <td>test fixture 沒更新</td>
          <td>fixture version、contract test、migration smoke</td>
      </tr>
  </tbody>
</table>
<p>Compatibility 的核心是先決定支援範圍。Mobile app 常要支援舊版資料庫升級；internal CLI 可能只支援最新版本；test fixture 則需要每次 migration 後重新產生。</p>
<h2 id="migration-evidence">Migration evidence</h2>
<p>Migration evidence 的責任是證明 schema 變更已完成且資料仍可用。SQLite migration evidence 比 server DB 簡單，但更依賴 application-level validation。</p>
<table>
  <thead>
      <tr>
          <th>Evidence</th>
          <th>目的</th>
          <th>範例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>schema version</td>
          <td>確認 DB file 契約</td>
          <td><code>PRAGMA user_version</code></td>
      </tr>
      <tr>
          <td>row count</td>
          <td>確認 copy / rebuild 無漏資料</td>
          <td><code>SELECT COUNT(*) FROM orders</code></td>
      </tr>
      <tr>
          <td>domain query</td>
          <td>確認重要 business invariant</td>
          <td>unpaid / paid 狀態數量</td>
      </tr>
      <tr>
          <td>foreign key check</td>
          <td>確認 reference integrity</td>
          <td><code>PRAGMA foreign_key_check</code></td>
      </tr>
      <tr>
          <td>integrity check</td>
          <td>檢查 DB 結構</td>
          <td><code>PRAGMA integrity_check</code></td>
      </tr>
      <tr>
          <td>backup marker</td>
          <td>回退點</td>
          <td>pre-migration <code>.backup</code> file</td>
      </tr>
  </tbody>
</table>
<p>這些 evidence 應接到 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a> 或 release note。SQLite migration 失敗時，最清楚的 rollback 通常是回到 migration 前 snapshot，而非在同一檔案上繼續試錯。</p>
<h2 id="production-踩雷">Production 踩雷</h2>
<h3 id="case-1startup-migration-讓-app-啟動卡住">Case 1：startup migration 讓 app 啟動卡住</h3>
<p>Startup migration 的核心風險是把長時間 table rebuild 放在使用者啟動路徑。小表新增 column 可能很快；大表 rebuild、index 重建或 vacuum 類操作會讓 app 啟動、CLI command 或 API cold start 變慢。</p>
<p>修正方向是先估資料量。短 migration 可在 startup；長 migration 要有 explicit command、progress、backup 與 rollback route。</p>
<h3 id="case-2fixture-schema-升級漏掉-production-gap">Case 2：fixture schema 升級漏掉 production gap</h3>
<p>Fixture schema drift 的核心風險是測試 DB 和 production DB 的 dialect / constraint 不一致。SQLite fixture 很快，但 production 若是 PostgreSQL / MySQL，type、date、NULL、constraint 與 transaction 行為都可能不同。</p>
<p>修正方向是把 SQLite fixture 明確標成 contract test 層。Repository error mapping、domain invariant 可以用 SQLite；production-specific SQL 要用 production database container 驗證。</p>
<h3 id="case-3直接改-sqlite_schema">Case 3：直接改 <code>sqlite_schema</code></h3>
<p>直接改 <code>sqlite_schema</code> 的核心風險是產生語法正確但語意破壞的 database file。SQLite 官方文件提供 writable schema route，但同時強調錯誤修改可能讓 database corrupt / unreadable。</p>
<p>修正方向是讓 writable schema 成為最後手段。一般 migration 優先用 ALTER TABLE 或 table rebuild；需要特殊修復時先複製原檔，在副本驗證。</p>
<h2 id="操作檢查清單">操作檢查清單</h2>
<p>SQLite migration runbook 至少要記錄：</p>
<ol>
<li>DB file 目前 <code>user_version</code> 與 application release version。</li>
<li>Migration 是否可重入、是否可中斷後恢復。</li>
<li>Migration 前 backup / snapshot 位置。</li>
<li>需要 table rebuild 的 table、資料量、index / trigger / view 清單。</li>
<li>Validation query、row count、foreign key check、integrity check。</li>
<li>舊 binary / 新 binary 的相容策略。</li>
<li>Fixture DB 是否已重新產生並被 contract test 使用。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite overview</a></li>
<li>操作：<a href="/blog/backend/01-database/vendors/sqlite/hands-on/migration-fixture-lab/" data-link-title="SQLite Migration Fixture Lab" data-link-desc="SQLite user_version、table rebuild migration、fixture snapshot、rollback note 與 CI evidence 的操作說明">Migration fixture lab</a></li>
<li>平行：<a href="/blog/backend/01-database/vendors/sqlite/test-fixture-best-practice/" data-link-title="SQLite Test Fixture Best Practice" data-link-desc="SQLite 作為 test fixture、repository contract test、production dialect gap、seed data、fixture snapshot 與 CI evidence 的操作判準">Test Fixture Best Practice</a></li>
<li>遷移：<a href="/blog/backend/01-database/vendors/sqlite/migrate-to-postgresql/" data-link-title="SQLite to PostgreSQL Migration" data-link-desc="SQLite 升級到 PostgreSQL 的 driver、schema diff、data copy、dual run、cutover、rollback 與 cleanup">SQLite to PostgreSQL</a></li>
<li>官方：<a href="https://www.sqlite.org/lang_altertable.html">SQLite ALTER TABLE</a>、<a href="https://www.sqlite.org/pragma.html">SQLite PRAGMA</a></li>
</ul>
]]></content:encoded></item><item><title>SQLite SQL Dialect and Index Limits</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/sql-dialect-index-limits/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/sql-dialect-index-limits/</guid><description>&lt;p>SQLite SQL dialect and index limits 的核心責任是說明 SQLite 和 server SQL 的語意差異。SQLite 可以執行大量 SQL，也支援 transaction、index、trigger、view、window function 與 JSON；但它的 typing、constraint、file-level operation、query planner 與 extension model 會影響測試可信度、migration 成本與 production adapter。&lt;/p>
&lt;p>本文的判讀錨點是：SQLite 測過代表某個 repository contract 在 SQLite 語意下成立。當 production target 是 PostgreSQL、MySQL、D1、Turso 或其他 server database 時，測試與 migration 要補上 dialect gap evidence。&lt;/p>
&lt;h2 id="type-affinity">Type Affinity&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/type-affinity/" data-link-title="Type Affinity" data-link-desc="說明 SQLite 如何用 type affinity 決定欄位的型別傾向與值的儲存方式">Type affinity&lt;/a> 的核心責任是定義資料寫入時如何被保存與比較。SQLite 官方 &lt;a href="https://www.sqlite.org/datatype3.html">Datatypes&lt;/a> 文件說明 SQLite 使用 dynamic typing，型別關聯在 value 層與 column affinity 層共同作用；&lt;a href="https://www.sqlite.org/stricttables.html">STRICT tables&lt;/a> 則提供較嚴格的型別檢查。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>議題&lt;/th>
 &lt;th>SQLite 行為重點&lt;/th>
 &lt;th>Production 影響&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Integer&lt;/td>
 &lt;td>value type 可依寫入內容變化&lt;/td>
 &lt;td>test fixture 可能放過錯誤型別&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Text&lt;/td>
 &lt;td>collation 與比較語意需明確設定&lt;/td>
 &lt;td>排序、大小寫、unique 判斷要對照 target DB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Date/time&lt;/td>
 &lt;td>常以 TEXT / REAL / INTEGER 表示&lt;/td>
 &lt;td>timezone、range query、serialization 要一致&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Boolean&lt;/td>
 &lt;td>常以 integer convention 表示&lt;/td>
 &lt;td>adapter 要定義 true / false encoding&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>STRICT&lt;/td>
 &lt;td>提供更接近 server DB 的型別 guard&lt;/td>
 &lt;td>適合作為 fixture 預設，仍需 production test&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Type affinity 的教學重點是把資料合約放在 application boundary。若 domain 說 &lt;code>created_at&lt;/code> 是 timestamp，就要定義 storage format、timezone、precision、comparison query 與 serialization，而非只讓 SQLite 接受任意 value。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">orders&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INTEGER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">PRIMARY&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">KEY&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">created_at&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">TEXT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">total_cents&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INTEGER&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NOT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">NULL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">CHECK&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">total_cents&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">STRICT&lt;/span>&lt;span class="p">;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段 schema 用 &lt;code>STRICT&lt;/code>、&lt;code>NOT NULL&lt;/code> 與 &lt;code>CHECK&lt;/code> 讓 fixture 更接近正式資料合約。Production target 仍要跑 PostgreSQL / MySQL container test，確認 timestamp、integer range 與 constraint error mapping。&lt;/p>
&lt;h2 id="constraint-behavior">Constraint Behavior&lt;/h2>
&lt;p>Constraint behavior 的核心責任是確保資料完整性由 database 和 application 共同維護。SQLite 支援 primary key、unique、check、foreign key 與 deferred constraint，但 foreign key enforcement 需要明確啟用，migration / test runner 也要確認連線設定。&lt;/p></description><content:encoded><![CDATA[<p>SQLite SQL dialect and index limits 的核心責任是說明 SQLite 和 server SQL 的語意差異。SQLite 可以執行大量 SQL，也支援 transaction、index、trigger、view、window function 與 JSON；但它的 typing、constraint、file-level operation、query planner 與 extension model 會影響測試可信度、migration 成本與 production adapter。</p>
<p>本文的判讀錨點是：SQLite 測過代表某個 repository contract 在 SQLite 語意下成立。當 production target 是 PostgreSQL、MySQL、D1、Turso 或其他 server database 時，測試與 migration 要補上 dialect gap evidence。</p>
<h2 id="type-affinity">Type Affinity</h2>
<p><a href="/blog/backend/knowledge-cards/type-affinity/" data-link-title="Type Affinity" data-link-desc="說明 SQLite 如何用 type affinity 決定欄位的型別傾向與值的儲存方式">Type affinity</a> 的核心責任是定義資料寫入時如何被保存與比較。SQLite 官方 <a href="https://www.sqlite.org/datatype3.html">Datatypes</a> 文件說明 SQLite 使用 dynamic typing，型別關聯在 value 層與 column affinity 層共同作用；<a href="https://www.sqlite.org/stricttables.html">STRICT tables</a> 則提供較嚴格的型別檢查。</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>SQLite 行為重點</th>
          <th>Production 影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Integer</td>
          <td>value type 可依寫入內容變化</td>
          <td>test fixture 可能放過錯誤型別</td>
      </tr>
      <tr>
          <td>Text</td>
          <td>collation 與比較語意需明確設定</td>
          <td>排序、大小寫、unique 判斷要對照 target DB</td>
      </tr>
      <tr>
          <td>Date/time</td>
          <td>常以 TEXT / REAL / INTEGER 表示</td>
          <td>timezone、range query、serialization 要一致</td>
      </tr>
      <tr>
          <td>Boolean</td>
          <td>常以 integer convention 表示</td>
          <td>adapter 要定義 true / false encoding</td>
      </tr>
      <tr>
          <td>STRICT</td>
          <td>提供更接近 server DB 的型別 guard</td>
          <td>適合作為 fixture 預設，仍需 production test</td>
      </tr>
  </tbody>
</table>
<p>Type affinity 的教學重點是把資料合約放在 application boundary。若 domain 說 <code>created_at</code> 是 timestamp，就要定義 storage format、timezone、precision、comparison query 與 serialization，而非只讓 SQLite 接受任意 value。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="n">id</span><span class="w"> </span><span class="nb">INTEGER</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="n">created_at</span><span class="w"> </span><span class="nb">TEXT</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="n">total_cents</span><span class="w"> </span><span class="nb">INTEGER</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="w"> </span><span class="k">CHECK</span><span class="w"> </span><span class="p">(</span><span class="n">total_cents</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="p">)</span><span class="w"> </span><span class="k">STRICT</span><span class="p">;</span></span></span></code></pre></div><p>這段 schema 用 <code>STRICT</code>、<code>NOT NULL</code> 與 <code>CHECK</code> 讓 fixture 更接近正式資料合約。Production target 仍要跑 PostgreSQL / MySQL container test，確認 timestamp、integer range 與 constraint error mapping。</p>
<h2 id="constraint-behavior">Constraint Behavior</h2>
<p>Constraint behavior 的核心責任是確保資料完整性由 database 和 application 共同維護。SQLite 支援 primary key、unique、check、foreign key 與 deferred constraint，但 foreign key enforcement 需要明確啟用，migration / test runner 也要確認連線設定。</p>
<table>
  <thead>
      <tr>
          <th>Constraint</th>
          <th>SQLite 審查點</th>
          <th>操作判準</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Foreign key</td>
          <td><code>PRAGMA foreign_keys = ON</code></td>
          <td>每個 connection / test setup 都要驗證</td>
      </tr>
      <tr>
          <td>Unique</td>
          <td>NULL、collation、expression</td>
          <td>對照 target DB 的 NULL uniqueness 與 collation</td>
      </tr>
      <tr>
          <td>Check</td>
          <td>type affinity 互動</td>
          <td>用 domain invalid case 驗證</td>
      </tr>
      <tr>
          <td>Deferred</td>
          <td>transaction boundary</td>
          <td>用 multi-step workflow 測 commit-time failure</td>
      </tr>
  </tbody>
</table>
<p>Foreign key 是 SQLite fixture 最常漏掉的設定。每個測試連線開啟後應立刻查 <code>PRAGMA foreign_keys;</code>，並用一個故意違反 FK 的 fixture case 確認錯誤會出現。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">foreign_keys</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">ON</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">foreign_keys</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">pragma_foreign_keys</span><span class="p">;</span></span></span></code></pre></div><p>Constraint error 要在 repository adapter 層被歸類。若 production target 會把 duplicate key、foreign key、check violation 映射成不同 error code，SQLite fixture 也要至少保留 domain-level classification test。</p>
<h2 id="transaction-behavior">Transaction Behavior</h2>
<p>Transaction behavior 的核心責任是定義讀寫隔離、savepoint、nested workflow 與 retry。SQLite 官方 <a href="https://www.sqlite.org/isolation.html">isolation</a> 文件說明 connection 之間的隔離語意；WAL mode 下 reader / writer behavior 也會影響 concurrent test。</p>
<table>
  <thead>
      <tr>
          <th>行為</th>
          <th>SQLite 判讀</th>
          <th>測試影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Single writer</td>
          <td>同一時間只有一個 writer 取得寫鎖</td>
          <td>concurrent writer test 要顯式設計</td>
      </tr>
      <tr>
          <td>Snapshot read</td>
          <td>WAL mode 下 reader 可讀舊 snapshot</td>
          <td>freshness 與 read-after-write 要分開測</td>
      </tr>
      <tr>
          <td>Savepoint</td>
          <td>適合 nested workflow</td>
          <td>repository transaction helper 要支援</td>
      </tr>
      <tr>
          <td>Busy timeout</td>
          <td>lock wait policy</td>
          <td>integration test 要設定固定 timeout</td>
      </tr>
  </tbody>
</table>
<p>Savepoint 可以讓 application 實作可組合的 transaction helper。若上層 workflow 已在 transaction 內，內層 repository 可以使用 savepoint 承接局部 rollback，而非開另一個 database transaction。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">SAVEPOINT</span><span class="w"> </span><span class="n">create_order</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">orders</span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">created_at</span><span class="p">,</span><span class="w"> </span><span class="n">total_cents</span><span class="p">)</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="s1">&#39;2026-05-21T00:00:00Z&#39;</span><span class="p">,</span><span class="w"> </span><span class="mi">1200</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="n">RELEASE</span><span class="w"> </span><span class="n">create_order</span><span class="p">;</span></span></span></code></pre></div><p>Busy timeout 是測試穩定性的關鍵設定。若 fixture 會平行跑測試，應每個 temp DB 獨立，或在專門 concurrency lab 裡測 <code>SQLITE_BUSY</code>；一般 contract test 要追求 deterministic result。</p>
<h2 id="index-model">Index Model</h2>
<p>Index model 的核心責任是把查詢形狀與資料量變成可觀測的計畫。SQLite 支援 B-tree index、covering index、partial index、expression index 與 query planner；但 planner choice、統計資訊與 function support 會和 target DB 不同。</p>
<table>
  <thead>
      <tr>
          <th>Index 類型</th>
          <th>適用情境</th>
          <th>審查問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Composite index</td>
          <td>多欄位 equality / range query</td>
          <td>欄位順序是否符合主要 query pattern</td>
      </tr>
      <tr>
          <td>Partial index</td>
          <td>active / pending / soft-delete row</td>
          <td>predicate 是否穩定、target DB 是否支援</td>
      </tr>
      <tr>
          <td>Expression index</td>
          <td>normalized email、date bucket</td>
          <td>function deterministic 與 migration 支援</td>
      </tr>
      <tr>
          <td>Covering index</td>
          <td>read-mostly list page</td>
          <td>index size 與 write overhead</td>
      </tr>
  </tbody>
</table>
<p>Index review 要從 query pattern 開始，而非從「常用欄位」開始。SQLite 可以用 <code>EXPLAIN QUERY PLAN</code> 檢查是否掃 index；production target 要用自己的 explain 工具重跑。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">EXPLAIN</span><span class="w"> </span><span class="n">QUERY</span><span class="w"> </span><span class="n">PLAN</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">total_cents</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="s1">&#39;2026-05-01T00:00:00Z&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="k">DESC</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">50</span><span class="p">;</span></span></span></code></pre></div><p>Index drift 是 migration 的常見風險。SQLite fixture 裡的 index 可以讓測試變快，但若 production schema 缺少同等 index，正式服務會在資料量成長後出現 latency spike；因此 index 要進入 schema diff audit。</p>
<h2 id="dialect-gap">Dialect Gap</h2>
<p>Dialect gap 的核心責任是把 SQLite 與 target database 的差異寫成 matrix。這份 matrix 應跟 repository adapter、migration plan 與 CI test suite 綁定。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>SQLite 審查點</th>
          <th>對照路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>ALTER TABLE</td>
          <td>支援範圍、table rebuild</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/schema-migration-versioning/" data-link-title="SQLite Schema Migration and Versioning" data-link-desc="SQLite schema migration、user_version、table rebuild、ALTER TABLE 限制、app release compatibility 與 migration evidence">Schema migration / versioning</a></td>
      </tr>
      <tr>
          <td>JSON</td>
          <td>function availability、index support</td>
          <td>production container test</td>
      </tr>
      <tr>
          <td>Generated column</td>
          <td>expression、storage、index</td>
          <td>migration dry run</td>
      </tr>
      <tr>
          <td>Window function</td>
          <td>target DB 支援與 planner</td>
          <td>query compatibility suite</td>
      </tr>
      <tr>
          <td>Extension</td>
          <td>FTS、vector、custom function</td>
          <td>vendor extension policy</td>
      </tr>
  </tbody>
</table>
<p>Dialect matrix 要以 query contract 為單位。每個 repository method 至少列出 SQL feature、SQLite behavior、production behavior、test layer 與 fallback strategy。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Contract: Search active documents by tenant and prefix
</span></span><span class="line"><span class="ln">2</span><span class="cl">SQLite: FTS5 virtual table in fixture
</span></span><span class="line"><span class="ln">3</span><span class="cl">PostgreSQL: tsvector + GIN index
</span></span><span class="line"><span class="ln">4</span><span class="cl">Risk: ranking / tokenizer / collation differ
</span></span><span class="line"><span class="ln">5</span><span class="cl">Evidence: golden result set + production container explain</span></span></code></pre></div><p>這種寫法讓測試負責驗證 domain contract，避免把兩個 SQL engine 的搜尋語意視為完全一致。</p>
<h2 id="test--migration-impact">Test / Migration Impact</h2>
<p>Test / migration impact 的核心責任是決定哪些東西可以用 SQLite 快速驗證，哪些東西要交給 production-like database。SQLite 很適合 repository contract、migration fixture、local development 與 file lifecycle drill；涉及 planner、extension、collation、locking、permission、role 與 HA 時，需要追加 target DB evidence。</p>
<table>
  <thead>
      <tr>
          <th>測試層</th>
          <th>SQLite 適合度</th>
          <th>必補 evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Domain repository</td>
          <td>高</td>
          <td>invalid data、constraint、transaction case</td>
      </tr>
      <tr>
          <td>Migration syntax</td>
          <td>中</td>
          <td>target DB dry run</td>
      </tr>
      <tr>
          <td>Query performance</td>
          <td>中</td>
          <td>target DB explain + realistic data volume</td>
      </tr>
      <tr>
          <td>Permission / role</td>
          <td>低</td>
          <td>server DB integration test</td>
      </tr>
      <tr>
          <td>HA / failover</td>
          <td>低</td>
          <td>vendor-specific drill</td>
      </tr>
  </tbody>
</table>
<p>SQLite fixture 的價值在於快、穩、便宜。它應承擔「資料合約是否被 repository 保護」；production container 或 staging database 承擔「正式 engine 是否用同樣方式執行」。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>SQL dialect and index limits 完成後，下一步要把 gap 接到實作層。測試設計讀 <a href="/blog/backend/01-database/vendors/sqlite/test-fixture-best-practice/" data-link-title="SQLite Test Fixture Best Practice" data-link-desc="SQLite 作為 test fixture、repository contract test、production dialect gap、seed data、fixture snapshot 與 CI evidence 的操作判準">Test Fixture Best Practice</a>；migration 實作讀 <a href="/blog/backend/01-database/vendors/sqlite/schema-migration-versioning/" data-link-title="SQLite Schema Migration and Versioning" data-link-desc="SQLite schema migration、user_version、table rebuild、ALTER TABLE 限制、app release compatibility 與 migration evidence">Schema migration / versioning</a>；要升級到 PostgreSQL，讀 <a href="/blog/backend/01-database/vendors/sqlite/migrate-to-postgresql/" data-link-title="SQLite to PostgreSQL Migration" data-link-desc="SQLite 升級到 PostgreSQL 的 driver、schema diff、data copy、dual run、cutover、rollback 與 cleanup">SQLite to PostgreSQL migration</a>。</p>
]]></content:encoded></item><item><title>SQLite Teaching Structure</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/teaching-structure/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/teaching-structure/</guid><description>&lt;p>SQLite teaching structure 的核心責任是把 SQLite 從單篇 vendor overview 擴成可教學的服務章節群。PostgreSQL / MySQL 的完整度來自 overview、deep article、migration playbook 與案例路由；SQLite 的完整度也要保留同樣層級，但正文重點要貼合它自己的服務語言：single file、embedded process、writer boundary、backup / restore、test fixture、local-first 與 edge SQLite 變體。&lt;/p>
&lt;h2 id="完成標準">完成標準&lt;/h2>
&lt;p>SQLite 章節群的完成標準是讀者能回答三個問題。第一，SQLite 何時是正式狀態而非臨時檔案；第二，SQLite production 化後要如何處理 WAL、backup、restore、migration、測試與觀測；第三，SQLite 成長後該升到 PostgreSQL / MySQL、Cloudflare D1、Turso / libSQL、Litestream / LiteFS 或 mobile sync。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層級&lt;/th>
 &lt;th>SQLite 對應文件&lt;/th>
 &lt;th>教學責任&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Service overview&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite&lt;/a>&lt;/td>
 &lt;td>第一輪服務定位、適用壓力、替代邊界與下一步路由&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Core deep article&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">File lifecycle / backup boundary&lt;/a>&lt;/td>
 &lt;td>WAL sidecar、backup API、restore drill、corruption recovery&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Hands-on&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/hands-on/" data-link-title="SQLite Hands-on 操作路線" data-link-desc="SQLite local file lab、backup / restore drill、WAL busy reproduction、migration fixture、D1 / Turso preview 的操作型章節設計">SQLite Hands-on&lt;/a>&lt;/td>
 &lt;td>local file、backup restore、WAL busy、migration fixture&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operations&lt;/td>
 &lt;td>WAL / locking、PRAGMA tuning、schema migration、observability&lt;/td>
 &lt;td>日常設定、排錯、容量訊號與 release gate&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application shape&lt;/td>
 &lt;td>test fixture、mobile / desktop store、local-first sync&lt;/td>
 &lt;td>SQLite 跟 application process / device / test workflow 的關係&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Edge / variants&lt;/td>
 &lt;td>D1 / Turso / libSQL、Litestream / LiteFS&lt;/td>
 &lt;td>分散式或 replicated SQLite 變體的責任邊界&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Migration route&lt;/td>
 &lt;td>SQLite → PostgreSQL、SQLite → D1 / Turso、PostgreSQL → SQLite&lt;/td>
 &lt;td>成長、edge 化或降操作成本時的階段化搬遷&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這份結構的重點是避免把 SQLite 寫成小型 PostgreSQL。SQLite deep article 要先處理檔案、process、filesystem、device、test 與 edge runtime；SQL dialect、index 與 migration 工具只有在這些責任成立後才展開。&lt;/p>
&lt;h2 id="推薦撰寫順序">推薦撰寫順序&lt;/h2>
&lt;p>撰寫順序要從正式狀態的最低操作責任開始，再逐步擴到應用形狀、edge 變體與 migration。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>順序&lt;/th>
 &lt;th>文件&lt;/th>
 &lt;th>狀態&lt;/th>
 &lt;th>為什麼排在這裡&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>1&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">File lifecycle / backup boundary&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>先回答 SQLite 如何成為可恢復的正式狀態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>2&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/wal-concurrency-locking/" data-link-title="SQLite WAL Concurrency and Locking" data-link-desc="SQLite WAL mode 如何降低 reader / writer 衝突、保留 single writer boundary，並用 SQLITE_BUSY、WAL growth、checkpoint 訊號判斷 production 上限">WAL concurrency / locking&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>writer boundary 是 SQLite production 判斷的核心&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>3&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/pragma-tuning-performance/" data-link-title="SQLite PRAGMA Tuning and Performance" data-link-desc="SQLite journal_mode、synchronous、busy_timeout、wal_autocheckpoint、cache_size、mmap_size、auto_vacuum 與 performance evidence 的操作判準">PRAGMA tuning / performance&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>把 journal、sync、cache、mmap 轉成可驗證的設定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>4&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/schema-migration-versioning/" data-link-title="SQLite Schema Migration and Versioning" data-link-desc="SQLite schema migration、user_version、table rebuild、ALTER TABLE 限制、app release compatibility 與 migration evidence">Schema migration / versioning&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>單檔案 DB 仍需要版本、rollback 與 app release 配合&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>5&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/test-fixture-best-practice/" data-link-title="SQLite Test Fixture Best Practice" data-link-desc="SQLite 作為 test fixture、repository contract test、production dialect gap、seed data、fixture snapshot 與 CI evidence 的操作判準">Test fixture best practice&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>SQLite 最常被語言教材引用，需要明確 production gap&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>6&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/mobile-desktop-embedded-store/" data-link-title="SQLite Mobile / Desktop Embedded Store" data-link-desc="SQLite 在 mobile、desktop、CLI、browser profile 與 embedded device 中承擔 local formal state 的資料責任、backup、privacy 與 sync boundary">Mobile / desktop embedded store&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>說明 device local state、backup、sync 與 privacy 責任&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>7&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/local-first-sync-boundary/" data-link-title="SQLite Local-first Sync Boundary" data-link-desc="SQLite local-first app、multi-device sync、server authority、conflict resolution、delete propagation 與 offline-first trade-off">Local-first sync boundary&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>把 single-device SQLite 與 multi-device sync 分開&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>8&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/d1-turso-libsql-comparison/" data-link-title="SQLite D1 / Turso / libSQL Comparison" data-link-desc="Cloudflare D1、Turso、libSQL 與 local SQLite 在 edge、replication、consistency、migration 與 vendor boundary 的比較">D1 / Turso / libSQL comparison&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>edge SQLite 變體需要獨立比較，和本地 SQLite 分開&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>9&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/litestream-litefs-replication/" data-link-title="SQLite Litestream / LiteFS Replication" data-link-desc="Litestream、LiteFS、SQLite backup replication、read replica、failover 與 restore route">Litestream / LiteFS replication&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>backup / read replica / failover 的語意要跟 multi-write 分開&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>10&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/sql-dialect-index-limits/" data-link-title="SQLite SQL Dialect and Index Limits" data-link-desc="SQLite type affinity、NULL / date handling、constraint、index、query planner 與 PostgreSQL / MySQL 差異">SQL dialect and index limits&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>對照 PostgreSQL / MySQL 測試與 migration gap&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>11&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/observability-runbook/" data-link-title="SQLite Observability and Runbook" data-link-desc="SQLite production runbook、backup evidence、WAL growth、busy errors、disk usage、restore drill 與 incident route">Observability / runbook&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>把 SQLite 的低操作成本補成可交接 evidence&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>12&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/hands-on/" data-link-title="SQLite Hands-on 操作路線" data-link-desc="SQLite local file lab、backup / restore drill、WAL busy reproduction、migration fixture、D1 / Turso preview 的操作型章節設計">Hands-on 操作路線&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>把 local file、backup、WAL busy、migration fixture 變成演練&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>13&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/migrate-to-postgresql/" data-link-title="SQLite to PostgreSQL Migration" data-link-desc="SQLite 升級到 PostgreSQL 的 driver、schema diff、data copy、dual run、cutover、rollback 與 cleanup">SQLite to PostgreSQL migration&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>多 tenant、權限、HA、schema governance 出現時的主要升級路徑&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>14&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/migrate-to-d1-turso/" data-link-title="SQLite to D1 / Turso Migration" data-link-desc="SQLite 轉向 Cloudflare D1、Turso / libSQL 的 edge driver、compatibility audit、data movement 與 rollback">SQLite to D1 / Turso route&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>edge / serverless 化時的 migration route&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>15&lt;/td>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/migrate-from-postgresql-simplification/" data-link-title="PostgreSQL to SQLite Simplification" data-link-desc="PostgreSQL 降低操作成本轉向 SQLite 的適用條件、資料責任縮小、export/import、runbook 與 no-go condition">PostgreSQL to SQLite simplification&lt;/a>&lt;/td>
 &lt;td>已有正文&lt;/td>
 &lt;td>小型工具、single-user app 或 embedded 需求的反向路徑&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這個順序讓 SQLite 先完成自己的核心語言，再處理相鄰產品。D1、Turso、LiteFS、Litestream 都帶有 SQLite 相容性，但教學上要先問它們承擔的是 backup、replication、edge locality、read replica 還是 distributed write。&lt;/p></description><content:encoded><![CDATA[<p>SQLite teaching structure 的核心責任是把 SQLite 從單篇 vendor overview 擴成可教學的服務章節群。PostgreSQL / MySQL 的完整度來自 overview、deep article、migration playbook 與案例路由；SQLite 的完整度也要保留同樣層級，但正文重點要貼合它自己的服務語言：single file、embedded process、writer boundary、backup / restore、test fixture、local-first 與 edge SQLite 變體。</p>
<h2 id="完成標準">完成標準</h2>
<p>SQLite 章節群的完成標準是讀者能回答三個問題。第一，SQLite 何時是正式狀態而非臨時檔案；第二，SQLite production 化後要如何處理 WAL、backup、restore、migration、測試與觀測；第三，SQLite 成長後該升到 PostgreSQL / MySQL、Cloudflare D1、Turso / libSQL、Litestream / LiteFS 或 mobile sync。</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>SQLite 對應文件</th>
          <th>教學責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Service overview</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite</a></td>
          <td>第一輪服務定位、適用壓力、替代邊界與下一步路由</td>
      </tr>
      <tr>
          <td>Core deep article</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">File lifecycle / backup boundary</a></td>
          <td>WAL sidecar、backup API、restore drill、corruption recovery</td>
      </tr>
      <tr>
          <td>Hands-on</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/hands-on/" data-link-title="SQLite Hands-on 操作路線" data-link-desc="SQLite local file lab、backup / restore drill、WAL busy reproduction、migration fixture、D1 / Turso preview 的操作型章節設計">SQLite Hands-on</a></td>
          <td>local file、backup restore、WAL busy、migration fixture</td>
      </tr>
      <tr>
          <td>Operations</td>
          <td>WAL / locking、PRAGMA tuning、schema migration、observability</td>
          <td>日常設定、排錯、容量訊號與 release gate</td>
      </tr>
      <tr>
          <td>Application shape</td>
          <td>test fixture、mobile / desktop store、local-first sync</td>
          <td>SQLite 跟 application process / device / test workflow 的關係</td>
      </tr>
      <tr>
          <td>Edge / variants</td>
          <td>D1 / Turso / libSQL、Litestream / LiteFS</td>
          <td>分散式或 replicated SQLite 變體的責任邊界</td>
      </tr>
      <tr>
          <td>Migration route</td>
          <td>SQLite → PostgreSQL、SQLite → D1 / Turso、PostgreSQL → SQLite</td>
          <td>成長、edge 化或降操作成本時的階段化搬遷</td>
      </tr>
  </tbody>
</table>
<p>這份結構的重點是避免把 SQLite 寫成小型 PostgreSQL。SQLite deep article 要先處理檔案、process、filesystem、device、test 與 edge runtime；SQL dialect、index 與 migration 工具只有在這些責任成立後才展開。</p>
<h2 id="推薦撰寫順序">推薦撰寫順序</h2>
<p>撰寫順序要從正式狀態的最低操作責任開始，再逐步擴到應用形狀、edge 變體與 migration。</p>
<table>
  <thead>
      <tr>
          <th>順序</th>
          <th>文件</th>
          <th>狀態</th>
          <th>為什麼排在這裡</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">File lifecycle / backup boundary</a></td>
          <td>已有正文</td>
          <td>先回答 SQLite 如何成為可恢復的正式狀態</td>
      </tr>
      <tr>
          <td>2</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/wal-concurrency-locking/" data-link-title="SQLite WAL Concurrency and Locking" data-link-desc="SQLite WAL mode 如何降低 reader / writer 衝突、保留 single writer boundary，並用 SQLITE_BUSY、WAL growth、checkpoint 訊號判斷 production 上限">WAL concurrency / locking</a></td>
          <td>已有正文</td>
          <td>writer boundary 是 SQLite production 判斷的核心</td>
      </tr>
      <tr>
          <td>3</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/pragma-tuning-performance/" data-link-title="SQLite PRAGMA Tuning and Performance" data-link-desc="SQLite journal_mode、synchronous、busy_timeout、wal_autocheckpoint、cache_size、mmap_size、auto_vacuum 與 performance evidence 的操作判準">PRAGMA tuning / performance</a></td>
          <td>已有正文</td>
          <td>把 journal、sync、cache、mmap 轉成可驗證的設定</td>
      </tr>
      <tr>
          <td>4</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/schema-migration-versioning/" data-link-title="SQLite Schema Migration and Versioning" data-link-desc="SQLite schema migration、user_version、table rebuild、ALTER TABLE 限制、app release compatibility 與 migration evidence">Schema migration / versioning</a></td>
          <td>已有正文</td>
          <td>單檔案 DB 仍需要版本、rollback 與 app release 配合</td>
      </tr>
      <tr>
          <td>5</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/test-fixture-best-practice/" data-link-title="SQLite Test Fixture Best Practice" data-link-desc="SQLite 作為 test fixture、repository contract test、production dialect gap、seed data、fixture snapshot 與 CI evidence 的操作判準">Test fixture best practice</a></td>
          <td>已有正文</td>
          <td>SQLite 最常被語言教材引用，需要明確 production gap</td>
      </tr>
      <tr>
          <td>6</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/mobile-desktop-embedded-store/" data-link-title="SQLite Mobile / Desktop Embedded Store" data-link-desc="SQLite 在 mobile、desktop、CLI、browser profile 與 embedded device 中承擔 local formal state 的資料責任、backup、privacy 與 sync boundary">Mobile / desktop embedded store</a></td>
          <td>已有正文</td>
          <td>說明 device local state、backup、sync 與 privacy 責任</td>
      </tr>
      <tr>
          <td>7</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/local-first-sync-boundary/" data-link-title="SQLite Local-first Sync Boundary" data-link-desc="SQLite local-first app、multi-device sync、server authority、conflict resolution、delete propagation 與 offline-first trade-off">Local-first sync boundary</a></td>
          <td>已有正文</td>
          <td>把 single-device SQLite 與 multi-device sync 分開</td>
      </tr>
      <tr>
          <td>8</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/d1-turso-libsql-comparison/" data-link-title="SQLite D1 / Turso / libSQL Comparison" data-link-desc="Cloudflare D1、Turso、libSQL 與 local SQLite 在 edge、replication、consistency、migration 與 vendor boundary 的比較">D1 / Turso / libSQL comparison</a></td>
          <td>已有正文</td>
          <td>edge SQLite 變體需要獨立比較，和本地 SQLite 分開</td>
      </tr>
      <tr>
          <td>9</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/litestream-litefs-replication/" data-link-title="SQLite Litestream / LiteFS Replication" data-link-desc="Litestream、LiteFS、SQLite backup replication、read replica、failover 與 restore route">Litestream / LiteFS replication</a></td>
          <td>已有正文</td>
          <td>backup / read replica / failover 的語意要跟 multi-write 分開</td>
      </tr>
      <tr>
          <td>10</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/sql-dialect-index-limits/" data-link-title="SQLite SQL Dialect and Index Limits" data-link-desc="SQLite type affinity、NULL / date handling、constraint、index、query planner 與 PostgreSQL / MySQL 差異">SQL dialect and index limits</a></td>
          <td>已有正文</td>
          <td>對照 PostgreSQL / MySQL 測試與 migration gap</td>
      </tr>
      <tr>
          <td>11</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/observability-runbook/" data-link-title="SQLite Observability and Runbook" data-link-desc="SQLite production runbook、backup evidence、WAL growth、busy errors、disk usage、restore drill 與 incident route">Observability / runbook</a></td>
          <td>已有正文</td>
          <td>把 SQLite 的低操作成本補成可交接 evidence</td>
      </tr>
      <tr>
          <td>12</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/hands-on/" data-link-title="SQLite Hands-on 操作路線" data-link-desc="SQLite local file lab、backup / restore drill、WAL busy reproduction、migration fixture、D1 / Turso preview 的操作型章節設計">Hands-on 操作路線</a></td>
          <td>已有正文</td>
          <td>把 local file、backup、WAL busy、migration fixture 變成演練</td>
      </tr>
      <tr>
          <td>13</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/migrate-to-postgresql/" data-link-title="SQLite to PostgreSQL Migration" data-link-desc="SQLite 升級到 PostgreSQL 的 driver、schema diff、data copy、dual run、cutover、rollback 與 cleanup">SQLite to PostgreSQL migration</a></td>
          <td>已有正文</td>
          <td>多 tenant、權限、HA、schema governance 出現時的主要升級路徑</td>
      </tr>
      <tr>
          <td>14</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/migrate-to-d1-turso/" data-link-title="SQLite to D1 / Turso Migration" data-link-desc="SQLite 轉向 Cloudflare D1、Turso / libSQL 的 edge driver、compatibility audit、data movement 與 rollback">SQLite to D1 / Turso route</a></td>
          <td>已有正文</td>
          <td>edge / serverless 化時的 migration route</td>
      </tr>
      <tr>
          <td>15</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/migrate-from-postgresql-simplification/" data-link-title="PostgreSQL to SQLite Simplification" data-link-desc="PostgreSQL 降低操作成本轉向 SQLite 的適用條件、資料責任縮小、export/import、runbook 與 no-go condition">PostgreSQL to SQLite simplification</a></td>
          <td>已有正文</td>
          <td>小型工具、single-user app 或 embedded 需求的反向路徑</td>
      </tr>
  </tbody>
</table>
<p>這個順序讓 SQLite 先完成自己的核心語言，再處理相鄰產品。D1、Turso、LiteFS、Litestream 都帶有 SQLite 相容性，但教學上要先問它們承擔的是 backup、replication、edge locality、read replica 還是 distributed write。</p>
<h2 id="文件命名規則">文件命名規則</h2>
<p>SQLite 章節群的檔名用服務責任命名，product-first 命名只留給 D1 / Turso / libSQL 這類 product boundary 本身就是教學主題的文件。</p>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>命名方式</th>
          <th>範例</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Core deep</td>
          <td><code>{mechanism}-{responsibility}</code></td>
          <td><code>wal-concurrency-locking.md</code></td>
      </tr>
      <tr>
          <td>Operation</td>
          <td><code>{operation}-{decision-signal}</code></td>
          <td><code>pragma-tuning-performance.md</code></td>
      </tr>
      <tr>
          <td>Application</td>
          <td><code>{context}-{state-role}</code></td>
          <td><code>mobile-desktop-embedded-store.md</code></td>
      </tr>
      <tr>
          <td>Variant</td>
          <td><code>{products}-comparison</code></td>
          <td><code>d1-turso-libsql-comparison.md</code></td>
      </tr>
      <tr>
          <td>Migration</td>
          <td><code>migrate-to-{target}</code></td>
          <td><code>migrate-to-postgresql.md</code></td>
      </tr>
  </tbody>
</table>
<h2 id="cross-module-路由">Cross-module 路由</h2>
<p>SQLite 章節群要固定連到四個 backend 模組。Backup / restore 連到 04 evidence 與 08 incident；test fixture 連到語言教材與 repository adapter；edge / local-first 連到 05 deployment / 07 data protection；performance tuning 連到 09 capacity。</p>
<table>
  <thead>
      <tr>
          <th>SQLite 議題</th>
          <th>主要跨模組路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Backup / restore</td>
          <td><a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a>、<a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a></td>
      </tr>
      <tr>
          <td>Test fixture</td>
          <td><a href="/blog/backend/01-database/repository-adapter/" data-link-title="1.4 Repository Adapter 實作" data-link-desc="Port / Adapter 邊界、row mapping、error translation、ORM vs query builder 選型、contract test 設計">Repository Adapter</a>、語言教材的 contract test</td>
      </tr>
      <tr>
          <td>Local-first / sync</td>
          <td><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">Data Protection</a>、offline / device privacy</td>
      </tr>
      <tr>
          <td>Edge SQLite</td>
          <td><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">Global Distributed OLTP</a>、deployment platform</td>
      </tr>
      <tr>
          <td>Performance</td>
          <td><a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">Bottleneck Localization</a></td>
      </tr>
  </tbody>
</table>
<h2 id="後續審查點">後續審查點</h2>
<p>SQLite 章節群完稿後要特別審查三個偏誤。第一是把 SQLite 過度美化成 production SQL 替代品；第二是把 edge SQLite 產品跟本地 SQLite 混成同一種能力；第三是把 test fixture 的便利性誤寫成 production equivalence。</p>
]]></content:encoded></item><item><title>SQLite Test Fixture Best Practice</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/test-fixture-best-practice/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/test-fixture-best-practice/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 適合作為 test fixture；本文聚焦 &lt;em>如何用 SQLite 加速測試，同時保留 production database 的語意邊界&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;p>SQLite test fixture 的核心責任是讓 repository / adapter 測試快速、可重複、可攜帶。SQLite 的單檔特性讓 CI 可以快速建立 DB、載入 seed、跑 contract test；但它的 type affinity、SQL dialect、locking 與 constraint behavior 和 PostgreSQL / MySQL 不完全相同，因此 fixture 要被定位為一層測試工具，而非 production equivalence。&lt;/p>
&lt;p>本文的判讀錨點是：SQLite fixture 適合驗證 application contract，不適合取代 production database compatibility test。若測試目標是 repository error mapping、domain invariant、migration fixture 或 deterministic seed，SQLite 很划算；若測試目標是 PostgreSQL extension、MySQL lock、query planner 或 SQL dialect，應使用 production-like container。&lt;/p>
&lt;h2 id="test-fixture-的位置">Test fixture 的位置&lt;/h2>
&lt;p>SQLite fixture 的服務責任是提供快、穩定、可重建的本地資料狀態。它通常位於 unit test 與 full integration test 之間，承擔 repository adapter 的 contract test。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>測試層級&lt;/th>
 &lt;th>SQLite 適合度&lt;/th>
 &lt;th>判讀&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Pure unit test&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>fake / in-memory object 通常更快&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Repository contract&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>驗證 CRUD、constraint mapping、transaction behavior&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Service integration&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>適合簡單流程，不覆蓋 production-specific SQL&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Production compatibility&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>用 PostgreSQL / MySQL container 或 staging DB&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Migration smoke&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>適合 fixture migration，不代表 production DDL&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的重點是把測試目的說清楚。SQLite fixture 讓語言教材與 backend 教材接起來；語言端測 interface / adapter，backend 端保留 production database 的深度文章與 migration playbook。&lt;/p>
&lt;h2 id="fixture-lifecycle">Fixture lifecycle&lt;/h2>
&lt;p>Fixture lifecycle 的核心責任是讓每次測試拿到已知資料狀態。常見策略有三種：每 test 建新 in-memory DB、每 suite 複製 template file、每 CI job 產生 versioned fixture。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>策略&lt;/th>
 &lt;th>適合情境&lt;/th>
 &lt;th>優點&lt;/th>
 &lt;th>邊界&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>:memory:&lt;/code> per test&lt;/td>
 &lt;td>小 schema、快速 unit-like contract&lt;/td>
 &lt;td>隔離最好、清理簡單&lt;/td>
 &lt;td>跨 connection / WAL 行為不同&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>template file copy&lt;/td>
 &lt;td>中等 seed、需要真實檔案行為&lt;/td>
 &lt;td>快速、可測 file lifecycle&lt;/td>
 &lt;td>要避免多 test 共用同一檔案&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>generated fixture&lt;/td>
 &lt;td>migration / seed 驗證&lt;/td>
 &lt;td>和 migration 同步&lt;/td>
 &lt;td>CI 時間較長&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>read-only fixture&lt;/td>
 &lt;td>查詢 / report 測試&lt;/td>
 &lt;td>避免 writer collision&lt;/td>
 &lt;td>不測 mutation&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Fixture file 應和 schema version 綁定。檔名、metadata 或 &lt;code>user_version&lt;/code> 要能回答「這個 fixture 對應哪個 migration 版本」，避免測試資料在多次 schema 變更後變成隱性技術債。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite</a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 適合作為 test fixture；本文聚焦 <em>如何用 SQLite 加速測試，同時保留 production database 的語意邊界</em>。</p></blockquote>
<p>SQLite test fixture 的核心責任是讓 repository / adapter 測試快速、可重複、可攜帶。SQLite 的單檔特性讓 CI 可以快速建立 DB、載入 seed、跑 contract test；但它的 type affinity、SQL dialect、locking 與 constraint behavior 和 PostgreSQL / MySQL 不完全相同，因此 fixture 要被定位為一層測試工具，而非 production equivalence。</p>
<p>本文的判讀錨點是：SQLite fixture 適合驗證 application contract，不適合取代 production database compatibility test。若測試目標是 repository error mapping、domain invariant、migration fixture 或 deterministic seed，SQLite 很划算；若測試目標是 PostgreSQL extension、MySQL lock、query planner 或 SQL dialect，應使用 production-like container。</p>
<h2 id="test-fixture-的位置">Test fixture 的位置</h2>
<p>SQLite fixture 的服務責任是提供快、穩定、可重建的本地資料狀態。它通常位於 unit test 與 full integration test 之間，承擔 repository adapter 的 contract test。</p>
<table>
  <thead>
      <tr>
          <th>測試層級</th>
          <th>SQLite 適合度</th>
          <th>判讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Pure unit test</td>
          <td>低</td>
          <td>fake / in-memory object 通常更快</td>
      </tr>
      <tr>
          <td>Repository contract</td>
          <td>高</td>
          <td>驗證 CRUD、constraint mapping、transaction behavior</td>
      </tr>
      <tr>
          <td>Service integration</td>
          <td>中</td>
          <td>適合簡單流程，不覆蓋 production-specific SQL</td>
      </tr>
      <tr>
          <td>Production compatibility</td>
          <td>低</td>
          <td>用 PostgreSQL / MySQL container 或 staging DB</td>
      </tr>
      <tr>
          <td>Migration smoke</td>
          <td>中</td>
          <td>適合 fixture migration，不代表 production DDL</td>
      </tr>
  </tbody>
</table>
<p>這張表的重點是把測試目的說清楚。SQLite fixture 讓語言教材與 backend 教材接起來；語言端測 interface / adapter，backend 端保留 production database 的深度文章與 migration playbook。</p>
<h2 id="fixture-lifecycle">Fixture lifecycle</h2>
<p>Fixture lifecycle 的核心責任是讓每次測試拿到已知資料狀態。常見策略有三種：每 test 建新 in-memory DB、每 suite 複製 template file、每 CI job 產生 versioned fixture。</p>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>適合情境</th>
          <th>優點</th>
          <th>邊界</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>:memory:</code> per test</td>
          <td>小 schema、快速 unit-like contract</td>
          <td>隔離最好、清理簡單</td>
          <td>跨 connection / WAL 行為不同</td>
      </tr>
      <tr>
          <td>template file copy</td>
          <td>中等 seed、需要真實檔案行為</td>
          <td>快速、可測 file lifecycle</td>
          <td>要避免多 test 共用同一檔案</td>
      </tr>
      <tr>
          <td>generated fixture</td>
          <td>migration / seed 驗證</td>
          <td>和 migration 同步</td>
          <td>CI 時間較長</td>
      </tr>
      <tr>
          <td>read-only fixture</td>
          <td>查詢 / report 測試</td>
          <td>避免 writer collision</td>
          <td>不測 mutation</td>
      </tr>
  </tbody>
</table>
<p>Fixture file 應和 schema version 綁定。檔名、metadata 或 <code>user_version</code> 要能回答「這個 fixture 對應哪個 migration 版本」，避免測試資料在多次 schema 變更後變成隱性技術債。</p>
<h2 id="production-dialect-gap">Production dialect gap</h2>
<p>Production dialect gap 的核心責任是避免 SQLite 測試通過後，PostgreSQL / MySQL production 出現不同語意。SQLite 的 dynamic typing、date / time representation、foreign key pragma、ALTER TABLE 支援與 lock model 都會影響測試可信度。</p>
<table>
  <thead>
      <tr>
          <th>Gap 類型</th>
          <th>SQLite 行為</th>
          <th>Production 風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Type affinity</td>
          <td>欄位有 affinity，值本身仍有 storage class</td>
          <td>PostgreSQL / MySQL type error 沒被測到</td>
      </tr>
      <tr>
          <td>Date / time</td>
          <td>常以 TEXT / REAL / INTEGER 表示</td>
          <td>timezone、precision、function 差異</td>
      </tr>
      <tr>
          <td>Foreign key</td>
          <td>需要 <code>PRAGMA foreign_keys=ON</code></td>
          <td>fixture 忘記開 FK，constraint bug 漏掉</td>
      </tr>
      <tr>
          <td>ALTER TABLE</td>
          <td>支援 subset，複雜變更需 rebuild</td>
          <td>production migration 工具行為不同</td>
      </tr>
      <tr>
          <td>Locking</td>
          <td>single-file lock / single writer</td>
          <td>server DB connection / lock model 不同</td>
      </tr>
      <tr>
          <td>SQL feature</td>
          <td>extension / JSON / index 差異</td>
          <td>vendor-specific query 需要 production evidence</td>
      </tr>
  </tbody>
</table>
<p>這張表的用法是決定哪些測試留在 SQLite，哪些要升級到 production-like DB。Repository contract 可用 SQLite；query optimization、vendor SQL、online schema change、CDC、replication、pooling 都應回到 PostgreSQL / MySQL 章節。</p>
<h2 id="contract-test-設計">Contract test 設計</h2>
<p>Contract test 的核心責任是讓不同 DB adapter 對 application 呈現同一組語意。SQLite fixture 測的是 application port 的行為，例如 duplicate key、not found、transaction rollback、pagination、domain invariant，而非底層 engine 的所有細節。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Repository contract
</span></span><span class="line"><span class="ln">2</span><span class="cl">├── Create / read / update / delete
</span></span><span class="line"><span class="ln">3</span><span class="cl">├── Unique conflict → ErrAlreadyExists
</span></span><span class="line"><span class="ln">4</span><span class="cl">├── Missing row → ErrNotFound
</span></span><span class="line"><span class="ln">5</span><span class="cl">├── Transaction rollback restores domain invariant
</span></span><span class="line"><span class="ln">6</span><span class="cl">├── Pagination order stable
</span></span><span class="line"><span class="ln">7</span><span class="cl">└── Migration version matches fixture</span></span></code></pre></div><p>如果 production adapter 是 PostgreSQL / MySQL，contract test 應至少在 nightly 或 CI matrix 裡跑一輪 production-like database。SQLite 提供快速回饋，production-like test 提供 dialect confidence。</p>
<h2 id="ci-evidence">CI evidence</h2>
<p>SQLite fixture 的 CI evidence 要證明資料狀態和 schema version 一致。測試失敗時，讀者要能知道是 application contract 失效、fixture 過期、migration 漏跑，還是 SQLite / production dialect gap。</p>
<table>
  <thead>
      <tr>
          <th>Evidence</th>
          <th>目的</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>fixture version</td>
          <td>對齊 migration / app release</td>
      </tr>
      <tr>
          <td>seed checksum</td>
          <td>確認測試資料穩定</td>
      </tr>
      <tr>
          <td>migration log</td>
          <td>確認 fixture 可由 migration 重建</td>
      </tr>
      <tr>
          <td>contract test output</td>
          <td>確認 repository behavior</td>
      </tr>
      <tr>
          <td>dialect gap note</td>
          <td>標示未覆蓋 production behavior</td>
      </tr>
  </tbody>
</table>
<p>CI 產物不一定要很複雜，但要能被下一個維護者重建。SQLite fixture 的優勢是可攜帶；若 fixture 只能靠某個人的本機狀態生成，就失去教學與維護價值。</p>
<h2 id="production-踩雷">Production 踩雷</h2>
<h3 id="case-1共用同一個-db-檔跑平行測試">Case 1：共用同一個 <code>.db</code> 檔跑平行測試</h3>
<p>平行測試共用檔案的核心風險是 test runner 製造和 production 不同的 writer collision。測試偶發 <code>SQLITE_BUSY</code>，團隊可能以為 application 有 race；實際上是測試隔離不足。</p>
<p>修正方向是 per-test temp DB 或 read-only template copy。需要測 WAL / busy 行為時，用專門 hands-on lab，讓一般 contract test 專注在 repository contract。</p>
<h3 id="case-2忘記開-foreign-keys">Case 2：忘記開 foreign keys</h3>
<p>Foreign key pragma 漏開的核心風險是 constraint bug 被 fixture 隱藏。SQLite foreign key enforcement 需要明確啟用；若 production DB 一定 enforce FK，fixture 也要在 connection initialization 中開啟。</p>
<p>修正方向是 baseline PRAGMA 和 startup assertion。每個 test DB open 後都跑 <code>PRAGMA foreign_keys</code> 並驗證結果。</p>
<h3 id="case-3sqlite-fixture-掩蓋-vendor-specific-sql">Case 3：SQLite fixture 掩蓋 vendor-specific SQL</h3>
<p>Vendor-specific SQL 被 SQLite 掩蓋的核心風險是 query 到 production 才失敗。例如 PostgreSQL JSONB、partial index、full-text search 或 MySQL generated column、optimizer hint 都應在 vendor DB 測。</p>
<p>修正方向是把 SQL 分層。Portable repository contract 可以用 SQLite；vendor-specific query 要有 PostgreSQL / MySQL test container。</p>
<h2 id="操作檢查清單">操作檢查清單</h2>
<p>SQLite fixture 設計前要回答：</p>
<ol>
<li>這個測試驗證 application contract 還是 production dialect。</li>
<li>Fixture 是 in-memory、template copy、generated file 還是 read-only。</li>
<li><code>PRAGMA foreign_keys</code>、<code>journal_mode</code>、<code>busy_timeout</code> 是否固定。</li>
<li>Fixture version 如何對齊 migration version。</li>
<li>Parallel test 是否每個 worker 有獨立 DB file。</li>
<li>哪些 query 必須在 PostgreSQL / MySQL container 再跑。</li>
<li>CI artifact 是否保留 migration log 與 dialect gap note。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/repository-adapter/" data-link-title="1.4 Repository Adapter 實作" data-link-desc="Port / Adapter 邊界、row mapping、error translation、ORM vs query builder 選型、contract test 設計">Repository Adapter</a></li>
<li>Sibling：<a href="/blog/backend/01-database/vendors/sqlite/schema-migration-versioning/" data-link-title="SQLite Schema Migration and Versioning" data-link-desc="SQLite schema migration、user_version、table rebuild、ALTER TABLE 限制、app release compatibility 與 migration evidence">Schema Migration / Versioning</a>、<a href="/blog/backend/01-database/vendors/sqlite/sql-dialect-index-limits/" data-link-title="SQLite SQL Dialect and Index Limits" data-link-desc="SQLite type affinity、NULL / date handling、constraint、index、query planner 與 PostgreSQL / MySQL 差異">SQL Dialect and Index Limits</a></li>
<li>操作：<a href="/blog/backend/01-database/vendors/sqlite/hands-on/migration-fixture-lab/" data-link-title="SQLite Migration Fixture Lab" data-link-desc="SQLite user_version、table rebuild migration、fixture snapshot、rollback note 與 CI evidence 的操作說明">Migration Fixture Lab</a></li>
<li>平行：<a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a>、<a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a></li>
<li>官方：<a href="https://www.sqlite.org/datatype3.html">SQLite Datatypes</a>、<a href="https://www.sqlite.org/stricttables.html">SQLite STRICT Tables</a>、<a href="https://www.sqlite.org/pragma.html">SQLite PRAGMA</a></li>
</ul>
]]></content:encoded></item><item><title>SQLite to D1 / Turso Migration</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/migrate-to-d1-turso/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/migrate-to-d1-turso/</guid><description>&lt;p>SQLite to D1 / Turso migration 的核心責任是把 local SQLite 轉成 edge / serverless / distributed SQLite-compatible product。這條路線的 driver 通常是 edge locality、Workers integration、managed operation、global read latency、embedded replica 或 serverless deployment workflow。&lt;/p>
&lt;p>本文的判讀錨點是：D1 / Turso migration 是 runtime boundary 變更。Local file 直連變成 platform binding、remote endpoint 或 embedded replica；因此 migration 要同時審查 SQL support、data movement、driver API、auth、latency、freshness、backup 與 vendor exit。&lt;/p>
&lt;h2 id="migration-drivers">Migration Drivers&lt;/h2>
&lt;p>Migration drivers 的核心責任是確認 edge SQLite 產品解決的是哪個服務壓力。D1 與 Turso / libSQL 都接近 SQLite experience，但它們的採用理由應寫成具體 workload。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Driver&lt;/th>
 &lt;th>適合產品&lt;/th>
 &lt;th>判讀訊號&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Workers integration&lt;/td>
 &lt;td>Cloudflare D1&lt;/td>
 &lt;td>App 已在 Workers、資料量小、query 清楚&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Serverless low ops&lt;/td>
 &lt;td>D1 / Turso&lt;/td>
 &lt;td>不想維護 host DB、可接受 platform limit&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Low-latency read&lt;/td>
 &lt;td>Turso / embedded replica&lt;/td>
 &lt;td>read-heavy、freshness window 明確&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Edge-local app&lt;/td>
 &lt;td>D1 / Turso&lt;/td>
 &lt;td>使用者分散、write rate 可控&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Portable SQLite base&lt;/td>
 &lt;td>Turso / libSQL&lt;/td>
 &lt;td>想保留 SQLite-like schema 與 local dev&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>D1 的 migration driver 要和 Cloudflare platform 綁定。若 app 已用 Workers routing、KV、Queues 或 Pages，D1 可以降低跨平台整合成本；若 app 不在 Cloudflare 生態，D1 的價值要用 latency、operation 與成本證明。&lt;/p>
&lt;p>Turso / libSQL 的 migration driver 要和 replica freshness 綁定。若使用者需要 local read speed，embedded replica 有價值；若產品要求每次讀都立即看到最新 global state，就要先設計 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-after-write/" data-link-title="Read-After-Write Consistency" data-link-desc="說明寫入後能否立即讀到該筆寫入的一致性保證">read-after-write&lt;/a> path。&lt;/p>
&lt;h2 id="compatibility-audit">Compatibility Audit&lt;/h2>
&lt;p>Compatibility audit 的核心責任是確認 local SQLite schema、query 與 migration workflow 可在 target product 上運作。官方文件要作為 limits 與 feature 的單一來源：D1 參考 &lt;a href="https://developers.cloudflare.com/d1/">Cloudflare D1 docs&lt;/a> 與 &lt;a href="https://developers.cloudflare.com/d1/platform/limits/">D1 limits&lt;/a>；Turso 參考 &lt;a href="https://docs.turso.tech/">Turso docs&lt;/a> 與 libSQL client reference。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>審查問題&lt;/th>
 &lt;th>Evidence&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>SQL support&lt;/td>
 &lt;td>schema、trigger、index、JSON、FK&lt;/td>
 &lt;td>migration dry run、query suite&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Size / batch&lt;/td>
 &lt;td>import file、query duration、batch size&lt;/td>
 &lt;td>limit review、sample import&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Driver API&lt;/td>
 &lt;td>local file path 變成 binding / endpoint&lt;/td>
 &lt;td>repository adapter test&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Auth&lt;/td>
 &lt;td>token、binding、environment secret&lt;/td>
 &lt;td>staging deployment&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Transaction&lt;/td>
 &lt;td>request boundary、retry、write location&lt;/td>
 &lt;td>failure injection&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Backup&lt;/td>
 &lt;td>export、restore、retention&lt;/td>
 &lt;td>restore drill&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Compatibility audit 要以 production query 為單位。只跑 &lt;code>CREATE TABLE&lt;/code> 會漏掉最重要的差異；query suite 要包含 list page、pagination、unique violation、FK violation、transaction rollback、large batch 與 slow query。&lt;/p></description><content:encoded><![CDATA[<p>SQLite to D1 / Turso migration 的核心責任是把 local SQLite 轉成 edge / serverless / distributed SQLite-compatible product。這條路線的 driver 通常是 edge locality、Workers integration、managed operation、global read latency、embedded replica 或 serverless deployment workflow。</p>
<p>本文的判讀錨點是：D1 / Turso migration 是 runtime boundary 變更。Local file 直連變成 platform binding、remote endpoint 或 embedded replica；因此 migration 要同時審查 SQL support、data movement、driver API、auth、latency、freshness、backup 與 vendor exit。</p>
<h2 id="migration-drivers">Migration Drivers</h2>
<p>Migration drivers 的核心責任是確認 edge SQLite 產品解決的是哪個服務壓力。D1 與 Turso / libSQL 都接近 SQLite experience，但它們的採用理由應寫成具體 workload。</p>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>適合產品</th>
          <th>判讀訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Workers integration</td>
          <td>Cloudflare D1</td>
          <td>App 已在 Workers、資料量小、query 清楚</td>
      </tr>
      <tr>
          <td>Serverless low ops</td>
          <td>D1 / Turso</td>
          <td>不想維護 host DB、可接受 platform limit</td>
      </tr>
      <tr>
          <td>Low-latency read</td>
          <td>Turso / embedded replica</td>
          <td>read-heavy、freshness window 明確</td>
      </tr>
      <tr>
          <td>Edge-local app</td>
          <td>D1 / Turso</td>
          <td>使用者分散、write rate 可控</td>
      </tr>
      <tr>
          <td>Portable SQLite base</td>
          <td>Turso / libSQL</td>
          <td>想保留 SQLite-like schema 與 local dev</td>
      </tr>
  </tbody>
</table>
<p>D1 的 migration driver 要和 Cloudflare platform 綁定。若 app 已用 Workers routing、KV、Queues 或 Pages，D1 可以降低跨平台整合成本；若 app 不在 Cloudflare 生態，D1 的價值要用 latency、operation 與成本證明。</p>
<p>Turso / libSQL 的 migration driver 要和 replica freshness 綁定。若使用者需要 local read speed，embedded replica 有價值；若產品要求每次讀都立即看到最新 global state，就要先設計 <a href="/blog/backend/knowledge-cards/read-after-write/" data-link-title="Read-After-Write Consistency" data-link-desc="說明寫入後能否立即讀到該筆寫入的一致性保證">read-after-write</a> path。</p>
<h2 id="compatibility-audit">Compatibility Audit</h2>
<p>Compatibility audit 的核心責任是確認 local SQLite schema、query 與 migration workflow 可在 target product 上運作。官方文件要作為 limits 與 feature 的單一來源：D1 參考 <a href="https://developers.cloudflare.com/d1/">Cloudflare D1 docs</a> 與 <a href="https://developers.cloudflare.com/d1/platform/limits/">D1 limits</a>；Turso 參考 <a href="https://docs.turso.tech/">Turso docs</a> 與 libSQL client reference。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>審查問題</th>
          <th>Evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SQL support</td>
          <td>schema、trigger、index、JSON、FK</td>
          <td>migration dry run、query suite</td>
      </tr>
      <tr>
          <td>Size / batch</td>
          <td>import file、query duration、batch size</td>
          <td>limit review、sample import</td>
      </tr>
      <tr>
          <td>Driver API</td>
          <td>local file path 變成 binding / endpoint</td>
          <td>repository adapter test</td>
      </tr>
      <tr>
          <td>Auth</td>
          <td>token、binding、environment secret</td>
          <td>staging deployment</td>
      </tr>
      <tr>
          <td>Transaction</td>
          <td>request boundary、retry、write location</td>
          <td>failure injection</td>
      </tr>
      <tr>
          <td>Backup</td>
          <td>export、restore、retention</td>
          <td>restore drill</td>
      </tr>
  </tbody>
</table>
<p>Compatibility audit 要以 production query 為單位。只跑 <code>CREATE TABLE</code> 會漏掉最重要的差異；query suite 要包含 list page、pagination、unique violation、FK violation、transaction rollback、large batch 與 slow query。</p>
<h2 id="data-movement">Data Movement</h2>
<p>Data movement 的核心責任是把 SQLite file 轉成 target platform 可接受的 seed。Local SQLite 可以先 export 成 SQL dump、CSV 或 platform CLI 支援的 import format，再進 target product。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sqlite3 app.db <span class="s2">&#34;.dump&#34;</span> &gt; seed.sql</span></span></code></pre></div><p>這段命令只是 seed 起點。正式流程要處理 schema ordering、unsupported SQL、large transaction、batch split、sensitive data masking、import duration、row count 與 checksum。</p>
<p>D1 migration 要把 Wrangler / platform workflow 納入 runbook。Cloudflare D1 的 limits 文件列出 import 與 query 限制；大型資料變更應切 batch，並在 preview / staging database 跑完整 dry run。</p>
<p>Turso migration 要把 remote database 與 embedded replica 分開驗證。Seed 完 remote primary 後，要測 local embedded replica 的 bootstrap、sync、read freshness、write delegation 與 offline behavior。</p>
<h2 id="application-change">Application Change</h2>
<p>Application change 的核心責任是把 database access 從 file path 改成可替換 adapter。Local SQLite 常用 file path 與 process-local connection；D1 / Turso 會加入 binding、endpoint、token、client SDK、network failure 與 platform runtime。</p>
<table>
  <thead>
      <tr>
          <th>改動層</th>
          <th>Local SQLite</th>
          <th>D1 / Turso route</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Connection</td>
          <td>file path</td>
          <td>Workers binding、HTTP / libSQL endpoint</td>
      </tr>
      <tr>
          <td>Auth</td>
          <td>filesystem permission</td>
          <td>platform secret、token、binding</td>
      </tr>
      <tr>
          <td>Error model</td>
          <td>SQLite error code</td>
          <td>SDK / platform error + SQLite-like error</td>
      </tr>
      <tr>
          <td>Retry</td>
          <td>local busy / lock retry</td>
          <td>network retry、idempotency、timeout</td>
      </tr>
      <tr>
          <td>Observability</td>
          <td>app log + file metric</td>
          <td>app log + platform metric</td>
      </tr>
  </tbody>
</table>
<p>Repository adapter 要承擔 driver 差異。Domain layer 應看到穩定的 repository contract，例如 duplicate key、stale read、temporary unavailable、retryable write；底層才處理 D1 binding 或 libSQL client。</p>
<p>Idempotency 是 edge migration 的關鍵。Write request 進入 network / serverless runtime 後，retry 可能在 client、platform 或 application 層發生；每個 critical write 都應有 idempotency key 或 natural unique key。</p>
<h2 id="evidence">Evidence</h2>
<p>Evidence 的核心責任是證明 edge migration 帶來的收益大於新風險。D1 / Turso 的成功要同時看功能可用、region latency、freshness、error rate、cost、migration time 與 exit route。</p>
<table>
  <thead>
      <tr>
          <th>Evidence</th>
          <th>最小驗證方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Latency by region</td>
          <td>從主要 user region 跑 read/write test</td>
      </tr>
      <tr>
          <td>Freshness</td>
          <td>write 後在 replica / edge read 檢查</td>
      </tr>
      <tr>
          <td>Migration repeatability</td>
          <td>staging database 從空庫重跑 seed</td>
      </tr>
      <tr>
          <td>Error mapping</td>
          <td>duplicate、constraint、timeout、auth</td>
      </tr>
      <tr>
          <td>Cost</td>
          <td>request、storage、egress、operation</td>
      </tr>
      <tr>
          <td>Exit route</td>
          <td>export file + restore to local SQLite</td>
      </tr>
  </tbody>
</table>
<p>Freshness evidence 要用產品語言寫。若 UI 可以顯示「同步中」，freshness window 可被使用者理解；若是付款、庫存、權限決策，讀舊資料會直接造成業務錯誤，這類 workflow 要走 primary read 或 server SQL。</p>
<p>Exit route 要被演練。Edge product 的 adoption cost 低，exit cost 會出現在 driver API、migration workflow、platform binding 與 data export；至少要能把 staging data export 回 SQLite file 並通過 smoke test。</p>
<h2 id="rollback">Rollback</h2>
<p>Rollback 的核心責任是保留 local SQLite snapshot 與 read-only fallback。Edge migration 若在 cutover 後遇到 auth、latency、limit 或 query error，團隊要能快速回到上一個可用資料狀態。</p>
<table>
  <thead>
      <tr>
          <th>Rollback 觸發</th>
          <th>回退策略</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Import / migration 失敗</td>
          <td>清空 target、修 migration、重跑 seed</td>
      </tr>
      <tr>
          <td>Query error spike</td>
          <td>切回 local SQLite / previous endpoint</td>
      </tr>
      <tr>
          <td>Freshness issue</td>
          <td>critical read 改 primary path</td>
      </tr>
      <tr>
          <td>Cost / limit spike</td>
          <td>降低 traffic、batch migration、重評估</td>
      </tr>
      <tr>
          <td>Vendor incident</td>
          <td>read-only mode、fallback endpoint</td>
      </tr>
  </tbody>
</table>
<p>Local snapshot 要保存到 cutover 後的觀察窗口結束。若 cutover 期間已有 target-only writes，要設計回放或 reconciliation；高風險 workflow 可以先進 read-only cutover，再逐步開寫。</p>
<h2 id="decision-route">Decision Route</h2>
<p>Decision route 的核心責任是把 edge migration 和 server DB migration 分開。D1 / Turso 適合 edge runtime 與 SQLite-like workflow；當需求轉向 central audit、server role、high-write OLTP 或 distributed transaction，應改走 PostgreSQL / CockroachDB / Spanner。</p>
<table>
  <thead>
      <tr>
          <th>需求</th>
          <th>路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Workers app + small relational data</td>
          <td>D1</td>
      </tr>
      <tr>
          <td>Read-heavy app + local replica value</td>
          <td>Turso / libSQL</td>
      </tr>
      <tr>
          <td>Backup / restore 是主要問題</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/litestream-litefs-replication/" data-link-title="SQLite Litestream / LiteFS Replication" data-link-desc="Litestream、LiteFS、SQLite backup replication、read replica、failover 與 restore route">Litestream / LiteFS</a></td>
      </tr>
      <tr>
          <td>多 tenant + permission + audit</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/migrate-to-postgresql/" data-link-title="SQLite to PostgreSQL Migration" data-link-desc="SQLite 升級到 PostgreSQL 的 driver、schema diff、data copy、dual run、cutover、rollback 與 cleanup">SQLite to PostgreSQL</a></td>
      </tr>
      <tr>
          <td>Global write transaction</td>
          <td><a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">Global Distributed OLTP</a></td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<p>SQLite to D1 / Turso migration 完成後，先讀 <a href="/blog/backend/01-database/vendors/sqlite/d1-turso-libsql-comparison/" data-link-title="SQLite D1 / Turso / libSQL Comparison" data-link-desc="Cloudflare D1、Turso、libSQL 與 local SQLite 在 edge、replication、consistency、migration 與 vendor boundary 的比較">D1 / Turso / libSQL comparison</a> 釐清 product boundary；再用 <a href="/blog/backend/01-database/vendors/sqlite/sql-dialect-index-limits/" data-link-title="SQLite SQL Dialect and Index Limits" data-link-desc="SQLite type affinity、NULL / date handling、constraint、index、query planner 與 PostgreSQL / MySQL 差異">SQL dialect and index limits</a> 做 compatibility audit；需要操作演練時讀 <a href="/blog/backend/01-database/vendors/sqlite/hands-on/d1-turso-preview-lab/" data-link-title="SQLite D1 / Turso Preview Lab" data-link-desc="SQLite local DB 匯出到 Cloudflare D1 或 Turso preview environment 的 compatibility、latency 與 rollback 操作說明">D1 / Turso preview lab</a>。</p>
]]></content:encoded></item><item><title>SQLite to PostgreSQL Migration</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/migrate-to-postgresql/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/migrate-to-postgresql/</guid><description>&lt;p>SQLite to PostgreSQL migration 的核心責任是把 embedded single-file state 升級成 server SQL operational model。這條路線通常由 multi-user access、HA、central audit、permission、online schema governance、write concurrency 或 team handoff 壓力觸發。&lt;/p>
&lt;p>本文的判讀錨點是：升級到 PostgreSQL 是服務責任擴大，而非單純換 driver。Migration 要同時處理 schema 語意、資料搬遷、application adapter、backup / PITR、role、observability、cutover 與 rollback。&lt;/p>
&lt;h2 id="migration-drivers">Migration Drivers&lt;/h2>
&lt;p>Migration drivers 的核心責任是確認 PostgreSQL 真的承擔新增責任。SQLite 在 single-node、single-file、low-concurrency 場景很強；PostgreSQL 的價值出現在 server database governance。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Driver&lt;/th>
 &lt;th>代表需求&lt;/th>
 &lt;th>PostgreSQL 承擔的責任&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Concurrent writers&lt;/td>
 &lt;td>多 instance / 多使用者同時寫入&lt;/td>
 &lt;td>MVCC、connection management、lock insight&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>HA / PITR&lt;/td>
 &lt;td>需要時間點恢復與 managed backup&lt;/td>
 &lt;td>WAL archiving、replica、restore drill&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Central audit&lt;/td>
 &lt;td>需要查詢與變更證據&lt;/td>
 &lt;td>role、log、extension、SIEM integration&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Permission boundary&lt;/td>
 &lt;td>app / analyst / job 權限分離&lt;/td>
 &lt;td>DB role、grant、row / schema boundary&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Schema governance&lt;/td>
 &lt;td>migration 要 online 且可審查&lt;/td>
 &lt;td>migration tool、lock review、rollback&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Shared data platform&lt;/td>
 &lt;td>多服務共用正式資料&lt;/td>
 &lt;td>connection pool、capacity、ownership&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Driver 要被量化。若問題只是單一 CLI 檔案變大，先改善 backup、VACUUM、index 與 WAL runbook；若問題是多 instance 同時寫、權限分離、audit 與 PITR，PostgreSQL 才是正確路由。&lt;/p>
&lt;h2 id="diff-audit">Diff Audit&lt;/h2>
&lt;p>Diff audit 的核心責任是把 SQLite 語意轉成 PostgreSQL 語意。SQLite 的 type affinity、date / time convention、auto-increment、foreign key、index、JSON、transaction 與 extension 都要逐項審查。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>SQLite source 問題&lt;/th>
 &lt;th>PostgreSQL target 決策&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Type&lt;/td>
 &lt;td>dynamic typing、STRICT usage&lt;/td>
 &lt;td>integer / bigint / numeric / timestamptz&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Primary key&lt;/td>
 &lt;td>rowid、INTEGER PRIMARY KEY&lt;/td>
 &lt;td>identity、sequence、UUID&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Date/time&lt;/td>
 &lt;td>TEXT / INTEGER convention&lt;/td>
 &lt;td>timestamptz、timezone policy&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>JSON&lt;/td>
 &lt;td>JSON text / function usage&lt;/td>
 &lt;td>jsonb、GIN index、query rewrite&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Constraint&lt;/td>
 &lt;td>FK pragma、check、unique collation&lt;/td>
 &lt;td>enforced FK、deferrable、collation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Index&lt;/td>
 &lt;td>partial / expression / covering index&lt;/td>
 &lt;td>equivalent index + explain&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Transaction&lt;/td>
 &lt;td>single writer、savepoint&lt;/td>
 &lt;td>isolation level、deadlock retry&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Type mapping 要先保護 domain invariant。金額欄位用 integer cents 或 numeric、時間欄位用 timestamptz 或明確 UTC text、boolean 用 boolean；每個轉換都要有 invalid sample 與 round-trip test。&lt;/p></description><content:encoded><![CDATA[<p>SQLite to PostgreSQL migration 的核心責任是把 embedded single-file state 升級成 server SQL operational model。這條路線通常由 multi-user access、HA、central audit、permission、online schema governance、write concurrency 或 team handoff 壓力觸發。</p>
<p>本文的判讀錨點是：升級到 PostgreSQL 是服務責任擴大，而非單純換 driver。Migration 要同時處理 schema 語意、資料搬遷、application adapter、backup / PITR、role、observability、cutover 與 rollback。</p>
<h2 id="migration-drivers">Migration Drivers</h2>
<p>Migration drivers 的核心責任是確認 PostgreSQL 真的承擔新增責任。SQLite 在 single-node、single-file、low-concurrency 場景很強；PostgreSQL 的價值出現在 server database governance。</p>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>代表需求</th>
          <th>PostgreSQL 承擔的責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Concurrent writers</td>
          <td>多 instance / 多使用者同時寫入</td>
          <td>MVCC、connection management、lock insight</td>
      </tr>
      <tr>
          <td>HA / PITR</td>
          <td>需要時間點恢復與 managed backup</td>
          <td>WAL archiving、replica、restore drill</td>
      </tr>
      <tr>
          <td>Central audit</td>
          <td>需要查詢與變更證據</td>
          <td>role、log、extension、SIEM integration</td>
      </tr>
      <tr>
          <td>Permission boundary</td>
          <td>app / analyst / job 權限分離</td>
          <td>DB role、grant、row / schema boundary</td>
      </tr>
      <tr>
          <td>Schema governance</td>
          <td>migration 要 online 且可審查</td>
          <td>migration tool、lock review、rollback</td>
      </tr>
      <tr>
          <td>Shared data platform</td>
          <td>多服務共用正式資料</td>
          <td>connection pool、capacity、ownership</td>
      </tr>
  </tbody>
</table>
<p>Driver 要被量化。若問題只是單一 CLI 檔案變大，先改善 backup、VACUUM、index 與 WAL runbook；若問題是多 instance 同時寫、權限分離、audit 與 PITR，PostgreSQL 才是正確路由。</p>
<h2 id="diff-audit">Diff Audit</h2>
<p>Diff audit 的核心責任是把 SQLite 語意轉成 PostgreSQL 語意。SQLite 的 type affinity、date / time convention、auto-increment、foreign key、index、JSON、transaction 與 extension 都要逐項審查。</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>SQLite source 問題</th>
          <th>PostgreSQL target 決策</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Type</td>
          <td>dynamic typing、STRICT usage</td>
          <td>integer / bigint / numeric / timestamptz</td>
      </tr>
      <tr>
          <td>Primary key</td>
          <td>rowid、INTEGER PRIMARY KEY</td>
          <td>identity、sequence、UUID</td>
      </tr>
      <tr>
          <td>Date/time</td>
          <td>TEXT / INTEGER convention</td>
          <td>timestamptz、timezone policy</td>
      </tr>
      <tr>
          <td>JSON</td>
          <td>JSON text / function usage</td>
          <td>jsonb、GIN index、query rewrite</td>
      </tr>
      <tr>
          <td>Constraint</td>
          <td>FK pragma、check、unique collation</td>
          <td>enforced FK、deferrable、collation</td>
      </tr>
      <tr>
          <td>Index</td>
          <td>partial / expression / covering index</td>
          <td>equivalent index + explain</td>
      </tr>
      <tr>
          <td>Transaction</td>
          <td>single writer、savepoint</td>
          <td>isolation level、deadlock retry</td>
      </tr>
  </tbody>
</table>
<p>Type mapping 要先保護 domain invariant。金額欄位用 integer cents 或 numeric、時間欄位用 timestamptz 或明確 UTC text、boolean 用 boolean；每個轉換都要有 invalid sample 與 round-trip test。</p>
<p>Index mapping 要用 production query 重跑 explain。SQLite 的 <code>EXPLAIN QUERY PLAN</code> 只能說明 SQLite planner；PostgreSQL 需要自己的 <code>EXPLAIN (ANALYZE, BUFFERS)</code>，並使用接近真實分布的資料量。</p>
<h2 id="phase-plan">Phase Plan</h2>
<p>Phase plan 的核心責任是降低一次性 cutover 風險。SQLite to PostgreSQL migration 通常可以分成 schema 建模、資料匯出、adapter 切換、shadow read、freeze / cutover 與 cleanup。</p>
<table>
  <thead>
      <tr>
          <th>Phase</th>
          <th>目的</th>
          <th>Evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema rewrite</td>
          <td>建立 PostgreSQL target schema</td>
          <td>migration dry run、schema review</td>
      </tr>
      <tr>
          <td>Data export</td>
          <td>從 SQLite 取出穩定 snapshot</td>
          <td>source checksum、row count、export log</td>
      </tr>
      <tr>
          <td>Data import</td>
          <td>寫入 PostgreSQL</td>
          <td>target checksum、constraint validation</td>
      </tr>
      <tr>
          <td>Adapter layer</td>
          <td>將 repository 改為可切換</td>
          <td>dual test suite、error mapping</td>
      </tr>
      <tr>
          <td>Shadow read</td>
          <td>比對新舊 query result</td>
          <td>mismatch report、latency profile</td>
      </tr>
      <tr>
          <td>Cutover</td>
          <td>切正式寫入</td>
          <td>freeze window、rollback snapshot</td>
      </tr>
      <tr>
          <td>Cleanup</td>
          <td>退役 SQLite write path</td>
          <td>retention、credential、runbook update</td>
      </tr>
  </tbody>
</table>
<p>Adapter layer 是風險控制點。Repository 應把 SQLite 與 PostgreSQL driver 差異藏在 infrastructure layer，domain 不直接依賴 vendor-specific SQL exception 或 connection object。</p>
<p>Shadow read 適合先驗證 read contract。正式寫入仍留在 SQLite 時，background job 可以把相同 query 跑到 PostgreSQL mirror，記錄 row count、field diff、排序差異與 latency。</p>
<h2 id="data-movement">Data Movement</h2>
<p>Data movement 的核心責任是讓搬遷結果可驗證。SQLite database file 可以透過 <code>.dump</code>、CSV export、application-level export 或 custom ETL 搬入 PostgreSQL；選擇取決於資料量、型別轉換、FK order 與 downtime window。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sqlite3 app.db <span class="s2">&#34;.mode csv&#34;</span> <span class="s2">&#34;.headers on&#34;</span> <span class="s2">&#34;.once orders.csv&#34;</span> <span class="s2">&#34;SELECT * FROM orders ORDER BY id;&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">psql <span class="s2">&#34;</span><span class="nv">$DATABASE_URL</span><span class="s2">&#34;</span> -c <span class="s2">&#34;\\copy orders FROM &#39;orders.csv&#39; CSV HEADER&#34;</span></span></span></code></pre></div><p>這段命令是教學骨架。正式 migration 要處理 quoting、NULL、timezone、large object、FK order、batch size、transaction size、retry、import log 與 sensitive data handling。</p>
<p>Row count 是基本證據，checksum 是更強證據。可以針對每張表計算穩定排序後的 hash，或在 application layer 對 domain key 與重要欄位做 checksum。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">SUM</span><span class="p">(</span><span class="n">total_cents</span><span class="p">)</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="p">;</span></span></span></code></pre></div><p>Aggregate checksum 適合快速抓大錯。正式驗證還要補抽樣 row diff、edge case row、foreign key check 與 business invariant。</p>
<h2 id="cutover">Cutover</h2>
<p>Cutover 的核心責任是控制最後一次寫入切換。SQLite source 在 cutover 前應進入 read-only 或 writer freeze，確保最後 snapshot、import 與 validation 對齊。</p>
<table>
  <thead>
      <tr>
          <th>Cutover step</th>
          <th>操作</th>
          <th>Rollback 條件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Freeze writers</td>
          <td>停止背景 job、API write、admin tool</td>
          <td>source 寫入仍持續或 freeze 失敗</td>
      </tr>
      <tr>
          <td>Final snapshot</td>
          <td>SQLite backup / export</td>
          <td>checksum 失敗</td>
      </tr>
      <tr>
          <td>Final import</td>
          <td>PostgreSQL transaction / batch import</td>
          <td>constraint error、row mismatch</td>
      </tr>
      <tr>
          <td>Smoke test</td>
          <td>核心 read/write workflow</td>
          <td>error rate、latency、permission failure</td>
      </tr>
      <tr>
          <td>Switch traffic</td>
          <td>更新 config / secret / deployment</td>
          <td>application error rate 超過 tripwire</td>
      </tr>
      <tr>
          <td>Monitor</td>
          <td>query latency、lock、connection pool</td>
          <td>pool exhaustion、deadlock spike、data diff</td>
      </tr>
  </tbody>
</table>
<p>Rollback 要保存 source snapshot。若 cutover 後發現 PostgreSQL error mapping、permission 或 performance 問題，可以切回 SQLite read/write snapshot；前提是 cutover window 內所有新寫入都能回放或被阻擋。</p>
<h2 id="postgresql-operation-gate">PostgreSQL Operation Gate</h2>
<p>PostgreSQL operation gate 的核心責任是確認團隊準備好接手 server DB。Migration 成功要包含資料進入 target 與 operation readiness；PostgreSQL 需要 connection pool、backup / PITR、vacuum、index bloat、role、migration lock review 與 alert。</p>
<p>最小 operation checklist：</p>
<ol>
<li>Connection pool 設計：max connections、pool size、timeout、transaction pooling policy。</li>
<li>Backup / PITR：restore drill、retention、RPO / RTO。</li>
<li>Role / grant：application role、migration role、read-only role。</li>
<li>Migration lock review：DDL impact、online migration strategy。</li>
<li>Observability：slow query、lock wait、deadlock、replica lag、disk。</li>
<li>Incident route：rollback、restore、read-only mode、on-call owner。</li>
</ol>
<p>這個 gate 要在 cutover 前完成。SQLite 讓 operation surface 很小；PostgreSQL 擴大能力的同時，也擴大維護責任。</p>
<h2 id="no-go-conditions">No-Go Conditions</h2>
<p>No-go condition 的核心責任是阻止過早升級。若服務仍是 single-user、local-first、low-write、可用簡單 backup 解決，PostgreSQL 可能引入比問題更大的 operation cost。</p>
<table>
  <thead>
      <tr>
          <th>No-go 訊號</th>
          <th>更合適路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Single-user app 或 desktop app</td>
          <td>保留 SQLite + backup / migration runbook</td>
      </tr>
      <tr>
          <td>主要壓力是備份</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/litestream-litefs-replication/" data-link-title="SQLite Litestream / LiteFS Replication" data-link-desc="Litestream、LiteFS、SQLite backup replication、read replica、failover 與 restore route">Litestream / LiteFS</a></td>
      </tr>
      <tr>
          <td>主要壓力是 edge locality</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/migrate-to-d1-turso/" data-link-title="SQLite to D1 / Turso Migration" data-link-desc="SQLite 轉向 Cloudflare D1、Turso / libSQL 的 edge driver、compatibility audit、data movement 與 rollback">D1 / Turso route</a></td>
      </tr>
      <tr>
          <td>Team 尚未準備 server DB operation</td>
          <td>先補 observability / restore drill</td>
      </tr>
      <tr>
          <td>Schema / query 還在快速探索</td>
          <td>先穩定 domain model，再做正式 migration</td>
      </tr>
  </tbody>
</table>
<p>No-go 條件要轉成 tripwire。當 writer concurrency、audit、PITR、role 或 HA 需求跨過明確門檻，再啟動 migration。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>SQLite to PostgreSQL migration 完成後，下一步要看 target operation。PostgreSQL 能力讀 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a>；migration 方法讀 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">Database Migration Playbook</a>；若需求只是 edge platform，改讀 <a href="/blog/backend/01-database/vendors/sqlite/migrate-to-d1-turso/" data-link-title="SQLite to D1 / Turso Migration" data-link-desc="SQLite 轉向 Cloudflare D1、Turso / libSQL 的 edge driver、compatibility audit、data movement 與 rollback">SQLite to D1 / Turso migration</a>。</p>
]]></content:encoded></item><item><title>SQLite WAL Concurrency and Locking</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/wal-concurrency-locking/</link><pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/wal-concurrency-locking/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite&lt;/a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 的 single-file / embedded 定位；本文聚焦 &lt;em>WAL concurrency、single writer boundary、&lt;code>SQLITE_BUSY&lt;/code> 與 checkpoint strategy&lt;/em>。&lt;/p>&lt;/blockquote>
&lt;p>SQLite WAL concurrency 的核心責任是讓 reader / writer 衝突下降，同時保留單檔案資料庫的寫入邊界。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/write-ahead-log/" data-link-title="Write-Ahead Log" data-link-desc="說明資料庫如何先寫入 log 再合併回主資料，以提供持久性與崩潰復原">WAL mode&lt;/a> 把寫入 append 到 &lt;code>-wal&lt;/code> sidecar file，reader 可以從 main database file 加 WAL snapshot 讀取一致視圖；這讓 read-heavy workload 能比 rollback journal mode 更順。但 SQLite 仍遵循 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/single-writer-model/" data-link-title="Single Writer Model" data-link-desc="說明單寫者模型如何序列化寫入，並成為系統的容量邊界">single writer model&lt;/a>、只有一條 writer path，長交易、背景 migration、慢 disk 或多 process 寫入都會在這條 path 上排隊。&lt;/p>
&lt;p>本文的判讀錨點是：WAL 提升的是 reader concurrency，治理的是 writer queue。當服務看到 &lt;code>SQLITE_BUSY&lt;/code>、WAL file 持續變大、checkpoint duration 變長或偶發 commit latency spike，問題通常在 transaction duration、checkpoint cadence、filesystem lock 或 process ownership，而非單純「資料庫太小」。&lt;/p>
&lt;h2 id="wal-mode-的服務責任">WAL mode 的服務責任&lt;/h2>
&lt;p>WAL mode 的服務責任是把「寫入直接改 main database file」改成「寫入先 append 到 WAL，再由 checkpoint 合併回 main database」。SQLite 官方文件把 WAL 模型拆成 reading、writing、checkpointing 三個 primitive；這個 framing 對 production runbook 很重要，因為 checkpoint 會變成獨立的操作訊號。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>模式&lt;/th>
 &lt;th>寫入路徑&lt;/th>
 &lt;th>Reader 影響&lt;/th>
 &lt;th>Production 判讀&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Rollback journal&lt;/td>
 &lt;td>寫入前保存原始 page，再修改 main file&lt;/td>
 &lt;td>write 期間更容易和 reader 互相等待&lt;/td>
 &lt;td>適合簡單、低並發、短交易路徑&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>WAL&lt;/td>
 &lt;td>寫入 append 到 &lt;code>-wal&lt;/code>，checkpoint 後合併&lt;/td>
 &lt;td>reader 可看自己的 WAL snapshot&lt;/td>
 &lt;td>適合 read-heavy、互動式、短寫交易 workload&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的讀法是先看服務是否主要受 read / write 衝突影響。Read-heavy CLI、desktop、mobile、edge-local API 或 small backend 往往能從 WAL mode 受益；write-heavy queue consumer、batch import、multi-process writer 或 high-concurrency OLTP 則會先撞到 single writer boundary。&lt;/p>
&lt;h2 id="locking-model多-reader-與單-writer-是同時成立的">Locking model：多 reader 與單 writer 是同時成立的&lt;/h2>
&lt;p>SQLite locking model 的核心責任是保護單一 database file 的 ACID 邊界。Rollback journal mode 的官方 locking 文件描述了 SHARED、RESERVED、PENDING、EXCLUSIVE 等狀態；WAL mode 的細節另由 WAL 文件說明，但服務判讀上仍要記住同一件事：跨 connection / process 的寫入要被序列化。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite</a> overview 的 implementation-layer deep article。Overview 已說明 SQLite 的 single-file / embedded 定位；本文聚焦 <em>WAL concurrency、single writer boundary、<code>SQLITE_BUSY</code> 與 checkpoint strategy</em>。</p></blockquote>
<p>SQLite WAL concurrency 的核心責任是讓 reader / writer 衝突下降，同時保留單檔案資料庫的寫入邊界。<a href="/blog/backend/knowledge-cards/write-ahead-log/" data-link-title="Write-Ahead Log" data-link-desc="說明資料庫如何先寫入 log 再合併回主資料，以提供持久性與崩潰復原">WAL mode</a> 把寫入 append 到 <code>-wal</code> sidecar file，reader 可以從 main database file 加 WAL snapshot 讀取一致視圖；這讓 read-heavy workload 能比 rollback journal mode 更順。但 SQLite 仍遵循 <a href="/blog/backend/knowledge-cards/single-writer-model/" data-link-title="Single Writer Model" data-link-desc="說明單寫者模型如何序列化寫入，並成為系統的容量邊界">single writer model</a>、只有一條 writer path，長交易、背景 migration、慢 disk 或多 process 寫入都會在這條 path 上排隊。</p>
<p>本文的判讀錨點是：WAL 提升的是 reader concurrency，治理的是 writer queue。當服務看到 <code>SQLITE_BUSY</code>、WAL file 持續變大、checkpoint duration 變長或偶發 commit latency spike，問題通常在 transaction duration、checkpoint cadence、filesystem lock 或 process ownership，而非單純「資料庫太小」。</p>
<h2 id="wal-mode-的服務責任">WAL mode 的服務責任</h2>
<p>WAL mode 的服務責任是把「寫入直接改 main database file」改成「寫入先 append 到 WAL，再由 checkpoint 合併回 main database」。SQLite 官方文件把 WAL 模型拆成 reading、writing、checkpointing 三個 primitive；這個 framing 對 production runbook 很重要，因為 checkpoint 會變成獨立的操作訊號。</p>
<table>
  <thead>
      <tr>
          <th>模式</th>
          <th>寫入路徑</th>
          <th>Reader 影響</th>
          <th>Production 判讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Rollback journal</td>
          <td>寫入前保存原始 page，再修改 main file</td>
          <td>write 期間更容易和 reader 互相等待</td>
          <td>適合簡單、低並發、短交易路徑</td>
      </tr>
      <tr>
          <td>WAL</td>
          <td>寫入 append 到 <code>-wal</code>，checkpoint 後合併</td>
          <td>reader 可看自己的 WAL snapshot</td>
          <td>適合 read-heavy、互動式、短寫交易 workload</td>
      </tr>
  </tbody>
</table>
<p>這張表的讀法是先看服務是否主要受 read / write 衝突影響。Read-heavy CLI、desktop、mobile、edge-local API 或 small backend 往往能從 WAL mode 受益；write-heavy queue consumer、batch import、multi-process writer 或 high-concurrency OLTP 則會先撞到 single writer boundary。</p>
<h2 id="locking-model多-reader-與單-writer-是同時成立的">Locking model：多 reader 與單 writer 是同時成立的</h2>
<p>SQLite locking model 的核心責任是保護單一 database file 的 ACID 邊界。Rollback journal mode 的官方 locking 文件描述了 SHARED、RESERVED、PENDING、EXCLUSIVE 等狀態；WAL mode 的細節另由 WAL 文件說明，但服務判讀上仍要記住同一件事：跨 connection / process 的寫入要被序列化。</p>
<table>
  <thead>
      <tr>
          <th>角色</th>
          <th>WAL mode 下的責任</th>
          <th>常見失效訊號</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Reader</td>
          <td>讀取開始時固定自己的 snapshot end mark</td>
          <td>長讀取讓 checkpoint 停在舊 snapshot，WAL file 持續變大</td>
      </tr>
      <tr>
          <td>Writer</td>
          <td>append 新 transaction 到同一個 WAL file</td>
          <td>其他 writer 看到 <code>SQLITE_BUSY</code> 或 write latency spike</td>
      </tr>
      <tr>
          <td>Checkpoint</td>
          <td>把 WAL frame 合併回 main database file</td>
          <td>checkpoint duration 拉長、commit 偶發變慢</td>
      </tr>
      <tr>
          <td>Filesystem</td>
          <td>提供可靠 file lock 與 shared-memory 支援</td>
          <td>network filesystem、container mount 或權限造成異常</td>
      </tr>
  </tbody>
</table>
<p>多 reader 與單 writer 的組合是 SQLite 的正常設計。讀者在查問題時，要避免把 <code>SQLITE_BUSY</code> 直接解讀成資料毀損；它多半代表某個 connection 正在持有 writer 所需的 lock，或 checkpoint / transaction 正在等待可前進的窗口。</p>
<h2 id="sqlite_busy-的第一輪排查"><code>SQLITE_BUSY</code> 的第一輪排查</h2>
<p><code>SQLITE_BUSY</code> 的核心意義是某個 connection 當下拿不到需要的 lock。SQLite 提供 <code>busy_timeout</code> 讓 connection 等待一段時間；這能吸收短暫 writer queue，但它只是等待策略，single writer boundary 仍然存在。</p>
<table>
  <thead>
      <tr>
          <th>觀察訊號</th>
          <th>可能原因</th>
          <th>第一輪處理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>短暫 <code>SQLITE_BUSY</code></td>
          <td>多個短寫入撞在一起</td>
          <td>設定 bounded busy timeout，縮短 transaction duration</td>
      </tr>
      <tr>
          <td>持續 <code>SQLITE_BUSY</code></td>
          <td>長交易、migration、batch import</td>
          <td>找出持鎖 connection，拆小 transaction 或移到 maintenance window</td>
      </tr>
      <tr>
          <td>commit latency 偶發變慢</td>
          <td>auto-checkpoint 在 commit path 上</td>
          <td>調整 auto-checkpoint，改由 background checkpoint</td>
      </tr>
      <tr>
          <td>read query 讓 WAL 變大</td>
          <td>long reader 卡住 checkpoint</td>
          <td>限制長查詢、拆 reporting query、設定 reader timeout</td>
      </tr>
      <tr>
          <td>部署後 busy rate 上升</td>
          <td>instance 數增加、multi-process write</td>
          <td>重新檢查 writer ownership，必要時升級 server SQL</td>
      </tr>
  </tbody>
</table>
<p>這張表的重點是先找「誰持有 writer path」。如果問題來自單一長 transaction，修 transaction boundary；如果問題來自多個 process 同時寫同檔，修 process ownership；如果問題來自真實高寫入吞吐，SQLite 已經接近服務邊界。</p>
<h2 id="busy-timeout-是緩衝器容量邊界仍在-writer-path">Busy timeout 是緩衝器，容量邊界仍在 writer path</h2>
<p>Busy timeout 的服務責任是吸收短時間 lock collision。它適合 desktop app autosave、mobile local store、短 API write、測試 fixture 或偶發 background job；它不適合作為高寫入吞吐的主要容量策略。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">busy_timeout</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">5000</span><span class="p">;</span></span></span></code></pre></div><p>這個設定代表 connection 最多等待 5000 ms。Production runbook 要同時記錄三個訊號：busy 次數、等待時間分布、等待後成功率。若等待後成功率高且 p99 可接受，代表 writer queue 仍在服務邊界內；若等待常超時，代表 transaction duration 或 writer 並發已經超出單檔模型。</p>
<h2 id="checkpoint-strategywal-growth-是操作訊號">Checkpoint strategy：WAL growth 是操作訊號</h2>
<p>Checkpoint 的核心責任是把 WAL 中的 committed frames 合併回 main database file。SQLite 預設會在 WAL file 達到約 1000 pages 後自動 checkpoint；這個預設適合多數小型場景，但 production 服務要把 checkpoint 視為獨立操作。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">PRAGMA</span><span class="w"> </span><span class="n">wal_checkpoint</span><span class="p">(</span><span class="n">PASSIVE</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">wal_checkpoint</span><span class="p">(</span><span class="k">FULL</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">wal_checkpoint</span><span class="p">(</span><span class="k">RESTART</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="n">PRAGMA</span><span class="w"> </span><span class="n">wal_checkpoint</span><span class="p">(</span><span class="k">TRUNCATE</span><span class="p">);</span></span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>Checkpoint 型態</th>
          <th>操作語意</th>
          <th>適合場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>PASSIVE</td>
          <td>盡量前進，避免主動阻塞 reader / writer</td>
          <td>日常觀測、低風險背景 checkpoint</td>
      </tr>
      <tr>
          <td>FULL</td>
          <td>等待 writer，嘗試完成更多 checkpoint</td>
          <td>maintenance window、WAL growth 需要收斂</td>
      </tr>
      <tr>
          <td>RESTART</td>
          <td>完成後讓後續 writer 可重新使用 WAL</td>
          <td>想降低 WAL 持續膨脹，能接受等待</td>
      </tr>
      <tr>
          <td>TRUNCATE</td>
          <td>完成後截斷 WAL file</td>
          <td>低流量窗口、需要回收檔案空間</td>
      </tr>
  </tbody>
</table>
<p>Checkpoint 策略的判讀要看 workload cadence。互動式服務通常保留 auto-checkpoint，再加上低流量時段的 background checkpoint；長查詢或 reporting workload 需要避免讓 long reader 長期佔住 snapshot；batch import 則要把 transaction 切小，避免 WAL file 在單一交易期間快速膨脹。</p>
<h2 id="checkpoint-starvation長-reader-會讓-wal-持續長大">Checkpoint starvation：長 reader 會讓 WAL 持續長大</h2>
<p>Checkpoint starvation 的核心概念是：只要總有 reader 還在使用舊 snapshot，checkpoint 就可能停在 reset 之前。SQLite 官方 WAL 文件明確指出，checkpoint 可以和 reader 並行，但遇到仍被 reader 使用的 WAL 位置時要停下來；如果長時間沒有 reader gap，WAL file 會持續成長。</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>真實服務長相</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Desktop app 開著長報表</td>
          <td>使用者查詢大列表，背景寫入持續發生</td>
          <td>報表分頁、限制 read transaction duration</td>
      </tr>
      <tr>
          <td>API handler 把 cursor 留太久</td>
          <td>streaming response 邊讀邊回，交易未結束</td>
          <td>先 materialize 結果、縮短 DB read transaction</td>
      </tr>
      <tr>
          <td>Background sync 長讀取</td>
          <td>sync worker 掃全表，UI 仍在寫資料</td>
          <td>分批讀取、讀寫排程、低流量 checkpoint</td>
      </tr>
      <tr>
          <td>Test suite 平行讀寫 fixture</td>
          <td>測試共用同一 <code>.db</code>，多 worker 交錯</td>
          <td>per-test DB、read-only fixture、獨立 temp file</td>
      </tr>
  </tbody>
</table>
<p>這些情境的共同點是 reader lifecycle 沒有被 application 控制。SQLite 的 concurrency 問題常發生在 application boundary，而非 database engine 本身；修法也應回到 handler、worker、test runner 或 UI lifecycle。</p>
<h2 id="filesystem-與-deployment-boundary">Filesystem 與 deployment boundary</h2>
<p>SQLite WAL 的 deployment boundary 是 local filesystem 與可靠 shared-memory / file-locking primitive。官方 WAL 文件指出 wal-index 使用 shared memory，所有 reader 要位於同一台機器；這也是 WAL mode 不適合放在一般 network filesystem 上的主要原因。</p>
<table>
  <thead>
      <tr>
          <th>部署方式</th>
          <th>判讀</th>
          <th>建議路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單 process / 單機 local disk</td>
          <td>SQLite 最自然的部署形狀</td>
          <td>WAL + backup / restore runbook</td>
      </tr>
      <tr>
          <td>多 process / 同機 local disk</td>
          <td>可行，但要清楚 writer ownership 與 timeout</td>
          <td>WAL + busy timeout + checkpoint evidence</td>
      </tr>
      <tr>
          <td>多 instance / shared volume</td>
          <td>lock 與 writer ownership 風險上升</td>
          <td>升級 PostgreSQL / MySQL，或改用明確 primary pattern</td>
      </tr>
      <tr>
          <td>network filesystem</td>
          <td>WAL shared-memory 與 file lock 語意風險高</td>
          <td>改 local disk + replication，或 server database</td>
      </tr>
      <tr>
          <td>container ephemeral disk</td>
          <td>durability 與 restore 路徑要重新設計</td>
          <td>persistent volume、backup drill、restore evidence</td>
      </tr>
  </tbody>
</table>
<p>Deployment review 要問的第一個問題是「同一時間誰會寫這個檔案」。如果答案是多個 instance、跨機器 process 或不受控 job，SQLite 的服務邊界已經需要重新評估。</p>
<h2 id="production-踩雷">Production 踩雷</h2>
<h3 id="case-1多個-worker-同時寫同一個-sqlite-檔">Case 1：多個 worker 同時寫同一個 SQLite 檔</h3>
<p>多 worker 寫入同一個 SQLite 檔的核心風險是 writer ownership 消失。常見情境是小型服務從單 instance 擴到多 instance，但仍把 database file 放在 shared volume；早期看起來可運作，流量上升後開始出現 busy timeout、WAL growth 與偶發資料修復壓力。</p>
<p>修正方向是重新定義 writer。若服務仍是 small backend，可以收斂到單 writer process + queue；若 multi-instance 是長期需求，應遷移到 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> 或 <a href="/blog/backend/01-database/vendors/mysql/" data-link-title="MySQL" data-link-desc="高併發網路服務常用關聯式資料庫、Vitess / PlanetScale 分片生態、GitHub / Shopify / Facebook 規模驗證">MySQL</a>。</p>
<h3 id="case-2長讀取卡住-checkpoint磁碟被-wal-吃滿">Case 2：長讀取卡住 checkpoint，磁碟被 WAL 吃滿</h3>
<p>長讀取卡 checkpoint 的核心風險是 WAL file 成為隱性容量消耗。讀者可能只看到 disk usage 增長，誤以為是資料量變大；實際上 main database file 沒有明顯增長，<code>-wal</code> sidecar 持續膨脹。</p>
<p>修正方向是先找到長 reader，再調整 query lifecycle。Reporting query、background sync、streaming response、互動式 UI 大列表都要有 pagination、timeout 或低流量窗口；checkpoint 只負責收斂 WAL，application 仍要主動結束長讀取。</p>
<h3 id="case-3把-busy-timeout-當成擴容策略">Case 3：把 busy timeout 當成擴容策略</h3>
<p>Busy timeout 被當成擴容策略的核心風險是延遲被隱藏到使用者路徑。短暫 lock collision 可以等待；長期 write queue 則會把 API p99、UI freeze 或 worker backlog 拉高。</p>
<p>修正方向是把 busy wait 當 metric。設定 timeout 後要記錄等待時間與超時率；當 busy wait 成為常態，下一步是拆交易、調整 writer process、移走 batch job，或升級到 server database。</p>
<h3 id="case-4checkpoint-放在高流量-commit-path">Case 4：checkpoint 放在高流量 commit path</h3>
<p>Checkpoint 放在高流量 commit path 的核心風險是少數 commit 變得很慢。SQLite 預設 auto-checkpoint 對多數場景合理，但互動式服務可能看到偶發 latency spike；這時可以把 checkpoint 移到背景 thread / process 或低流量窗口。</p>
<p>修正方向是把 checkpoint duration 變成 evidence。觀察 WAL size、checkpoint return、commit latency 與 disk sync；若尖峰可接受，維持預設；若尖峰影響 UX，調整 checkpoint cadence。</p>
<h3 id="case-5wal-mode-版本與部署條件未納入維護">Case 5：WAL mode 版本與部署條件未納入維護</h3>
<p>WAL mode 的維護責任包含 SQLite runtime version、filesystem、sidecar file 與 release notes。SQLite 官方 WAL 文件記錄 2026-03 修正過罕見 WAL-reset bug；雖然觸發條件很窄，production runbook 仍應記錄 SQLite version、runtime package 與更新策略。</p>
<p>修正方向是把 SQLite runtime 當成 dependency。Mobile、desktop、embedded、language binding、OS bundled SQLite 可能各自帶不同版本；需要在 support matrix 中標明版本來源、WAL mode 行為與升級路徑。</p>
<h2 id="操作檢查清單">操作檢查清單</h2>
<p>SQLite WAL / locking runbook 至少要能回答下列問題：</p>
<ol>
<li>Database file、<code>-wal</code>、<code>-shm</code> 是否位於 local durable filesystem。</li>
<li>同一時間哪些 process / thread 會寫入 database file。</li>
<li><code>PRAGMA journal_mode</code>、<code>busy_timeout</code>、<code>wal_autocheckpoint</code> 如何設定。</li>
<li><code>SQLITE_BUSY</code> 次數、等待時間、超時率是否被記錄。</li>
<li>WAL file size、checkpoint duration、disk usage 是否被觀測。</li>
<li>長 read transaction 的來源與 timeout 如何治理。</li>
<li>Batch import、migration、background sync 是否避開互動式高峰。</li>
<li>SQLite runtime version 與 WAL 相關 release notes 如何追蹤。</li>
</ol>
<p>這份清單要接到 <a href="/blog/backend/01-database/vendors/sqlite/observability-runbook/" data-link-title="SQLite Observability and Runbook" data-link-desc="SQLite production runbook、backup evidence、WAL growth、busy errors、disk usage、restore drill 與 incident route">Observability / runbook</a> 與 <a href="/blog/backend/01-database/vendors/sqlite/hands-on/" data-link-title="SQLite Hands-on 操作路線" data-link-desc="SQLite local file lab、backup / restore drill、WAL busy reproduction、migration fixture、D1 / Turso preview 的操作型章節設計">SQLite Hands-on</a>；正文教判讀，hands-on 負責讓讀者重現 <code>SQLITE_BUSY</code>、WAL growth 與 checkpoint 行為。</p>
<h2 id="何時維持-sqlite何時升級">何時維持 SQLite，何時升級</h2>
<p>SQLite WAL mode 適合單機、短交易、read-heavy、writer ownership 清楚的服務。只要 busy wait 可控、checkpoint 能完成、backup / restore drill 成立，SQLite 可以承擔正式狀態。</p>
<p>升級訊號來自 writer boundary 外溢。多 instance write、多 region write、high-write OLTP、集中權限治理、read replica、PITR、DB account / role 與 audit requirement 都會把服務推向 server SQL、edge SQLite product 或 distributed SQL。</p>
<table>
  <thead>
      <tr>
          <th>壓力</th>
          <th>SQLite 內修正</th>
          <th>升級路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>偶發 <code>SQLITE_BUSY</code></td>
          <td>busy timeout、縮短 transaction</td>
          <td>維持 SQLite</td>
      </tr>
      <tr>
          <td>WAL growth</td>
          <td>找長 reader、manual checkpoint</td>
          <td>維持 SQLite，補 observability</td>
      </tr>
      <tr>
          <td>多 worker 寫入</td>
          <td>收斂單 writer、queue 化</td>
          <td>PostgreSQL / MySQL</td>
      </tr>
      <tr>
          <td>Edge locality</td>
          <td>D1 / Turso compatibility audit</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/d1-turso-libsql-comparison/" data-link-title="SQLite D1 / Turso / libSQL Comparison" data-link-desc="Cloudflare D1、Turso、libSQL 與 local SQLite 在 edge、replication、consistency、migration 與 vendor boundary 的比較">D1 / Turso route</a></td>
      </tr>
      <tr>
          <td>HA / PITR / audit governance</td>
          <td>file backup 已經難以治理</td>
          <td><a href="/blog/backend/01-database/vendors/sqlite/migrate-to-postgresql/" data-link-title="SQLite to PostgreSQL Migration" data-link-desc="SQLite 升級到 PostgreSQL 的 driver、schema diff、data copy、dual run、cutover、rollback 與 cleanup">SQLite to PostgreSQL</a></td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite overview</a></li>
<li>前置：<a href="/blog/backend/01-database/vendors/sqlite/file-lifecycle-backup-boundary/" data-link-title="SQLite file lifecycle 與 backup boundary" data-link-desc="把 SQLite 單檔案正式狀態拆成 WAL、backup API、restore drill、corruption recovery 與操作責任邊界">File lifecycle / backup boundary</a></li>
<li>操作：<a href="/blog/backend/01-database/vendors/sqlite/hands-on/" data-link-title="SQLite Hands-on 操作路線" data-link-desc="SQLite local file lab、backup / restore drill、WAL busy reproduction、migration fixture、D1 / Turso preview 的操作型章節設計">SQLite Hands-on</a> 與 <a href="/blog/backend/01-database/vendors/sqlite/hands-on/wal-busy-reproduction/" data-link-title="SQLite WAL Busy Reproduction" data-link-desc="SQLite long transaction、SQLITE_BUSY、busy_timeout、checkpoint growth 與 writer queue 的操作說明">WAL busy reproduction</a></li>
<li>平行：<a href="/blog/backend/01-database/vendors/sqlite/pragma-tuning-performance/" data-link-title="SQLite PRAGMA Tuning and Performance" data-link-desc="SQLite journal_mode、synchronous、busy_timeout、wal_autocheckpoint、cache_size、mmap_size、auto_vacuum 與 performance evidence 的操作判準">PRAGMA tuning / performance</a>、<a href="/blog/backend/01-database/vendors/sqlite/observability-runbook/" data-link-title="SQLite Observability and Runbook" data-link-desc="SQLite production runbook、backup evidence、WAL growth、busy errors、disk usage、restore drill 與 incident route">Observability / runbook</a></li>
<li>遷移：<a href="/blog/backend/01-database/vendors/sqlite/migrate-to-postgresql/" data-link-title="SQLite to PostgreSQL Migration" data-link-desc="SQLite 升級到 PostgreSQL 的 driver、schema diff、data copy、dual run、cutover、rollback 與 cleanup">SQLite to PostgreSQL</a></li>
<li>官方：<a href="https://www.sqlite.org/wal.html">SQLite Write-Ahead Logging</a>、<a href="https://www.sqlite.org/lockingv3.html">SQLite File Locking</a>、<a href="https://www.sqlite.org/isolation.html">SQLite Isolation</a>、<a href="https://www.sqlite.org/pragma.html">SQLite PRAGMA</a></li>
</ul>
]]></content:encoded></item></channel></rss>