幾週前我曾撰寫關於 sqlite-utils 4.0rc1 版本的文章。由於我的 Max 訂閱方案中,Claude Fable 只剩下幾天的使用額度,我決定看看它能否幫助我完成一個讓我真正滿意的 4.0 穩定版,因為我盡量遵守 SemVer 規範,並希望不相容的主要版本發布越少越好。

我從 iPhone 上的 Claude Code 網頁版開始,輸入了這個提示詞:「在發布 4.0 穩定版之前的最終審查,找出任何可能在日後修復時造成破壞性變更的最後一刻問題,這非常重要。」

這是它為我生成的第一份報告。其中包含一些我尚未遇到的重大問題,Fable 將其中 5 個歸類為「發布阻礙」。其中最嚴重的一個是:

delete_where() 從未提交且會污染連線(導致資料遺失)。Table.delete_where() (sqlite_utils/db.py:2948) 透過裸露的 self.db.execute() 執行其 DELETE 操作,沒有 atomic() 包裝器,相比之下,Table.delete() (db.py:2944) 則正確地進行了包裝。

連線會保持 in_transaction=True,因此所有後續的 atomic() 呼叫都會走儲存點分支 (db.py:430-440),並且也從不提交。

這是一個非常嚴重的錯誤!我很高興沒有發布這個版本,儘管至少這是一個我可以在 4.0.1 小版本中修復的錯誤,而不是一個會迫使我發布 5.0 版本的設計缺陷。

透過 37 個提示詞、34 次提交以及橫跨 30 個獨立檔案的 +1,321 -190 行程式碼變更,我們逐一處理了所有回饋,並在此過程中進行了多項其他設計改進。

關於程式碼代理的一個奇怪之處在於,像這樣更困難的任務實際上提供了更多同時處理其他事情的機會,因為代理有時需要 10-15 分鐘來處理新任務。我出去享受了半月灣的七月四日遊行,偶爾從手機上查看並提示 Fable 進行下一步。

完整細節請參閱 PR 和這份共享的對話紀錄。我切換到筆記型電腦進行最終審查,並透過 GitHub 的 PR 介面進行。最顯著的變更與交易處理有關,這是早期 RC 版本中的標誌性新功能。

新的 RC 版本現在包含了關於新交易模型的全面文件,我將在此完整引用其介紹:「此函式庫中所有寫入資料庫的方法,insert()、upsert()、update()、delete()、delete_where()、transform()、create_table()、create_index()、enable_fts() 等等,都在自己的交易中執行,並在返回前提交。

您的變更在方法呼叫完成後立即儲存到磁碟:db = Database("data.db"),db.table("news").insert({"headline": "Dog wins award"})。新行已儲存,無需呼叫 commit()。

這同樣適用於使用 db.execute() 執行的原始 SQL,寫入語句在執行後立即提交。您永遠不需要呼叫 commit(),也不需要關閉資料庫來持久化您的變更。只有兩種情況下您需要考慮交易:您希望將多個寫入操作分組在一起,以便它們要麼全部成功,要麼全部失敗,使用 db.atomic()。

您正在使用 db.begin() 自行管理交易,在這種情況下,除非您提交,否則不會提交任何內容,函式庫永遠不會提交您開啟的交易。」

在審查 Fable 的文件時,我發現首先審查文件編輯是建立對變更初步理解的絕佳方式,我發現了這個細節:「db.atomic() 和自動的每個方法交易是為 Python 預設交易處理模式下的連線設計的。不支援使用 Python 3.12+ sqlite3.connect(..., autocommit=True) 或 autocommit=False 選項建立的連線,因為 commit() 和 rollback() 在這些連線上的行為不同。」

我承認我沒有考慮到 sqlite-utils 會如何應對 Python 3.12 中新增的較新 autocommit 設定。結果發現「在這些連線上的行為不同」導致幾乎整個測試套件失敗,所以我與模型合作,確保這種差異不會破壞函式庫的運作方式。

我曾經認為讓一個模型審查另一個模型的工作有些荒謬,感覺有點迷信。問題是它確實有效,我已經開始習慣性地讓 Anthropic 最好的模型審查 OpenAI 的工作,反之亦然,因為我經常發現這種做法能產生有趣且有價值的結果。

我向 Codex Desktop 和 GPT-5.5 xhigh 提示了以下內容:「審查自上次 RC 以來的變更。同時確認變更日誌是最新的。」這足以發現兩個值得調查的問題:

發現:[P1] sqlite_utils/db.py:663 db.query() 現在只有在呼叫 db.execute() 之後才拒絕非行語句,並且 sqlite_utils/db.py:705 會先自動提交這些寫入。因此 db.query("update ...") 會引發 ValueError,但更新已經提交。

對於一個被文件描述為「只能用於返回行的 SQL」的方法來說,這是一個令人驚訝的副作用。

[P1] 透過 db.query() 進行的 INSERT ... RETURNING 只有在返回的生成器完全耗盡後才提交。db.query("insert ... returning ...") 在沒有迭代的情況下,或常見的 next(db.query(...)) 用法,會使交易保持開啟,並且寫入可能會在關閉時回滾。

這與 docs/changelog.rst:15 和 docs/python-api.rst:232 相矛盾,這些文件表示它無需迭代即可生效。

我將這些內容貼到一個新的 Fable 會話中,它進行了一些實驗來確認問題:「兩個發現都已確認。db.query() 首先呼叫了 self.execute(),它會自動提交寫入,然後才檢查 cursor.description,因此 db.query("update ...") 在引發 ValueError 之前提交了更新。

而 INSERT ... RETURNING 的提交位於返回生成器的末尾,因此除非您耗盡迭代器,否則它永遠不會觸發,next(db.query(...)) 或未迭代的呼叫會使交易保持開啟,這與變更日誌和文件所承諾的相矛盾。」

這是包含修復的 PR,以及完整的 Claude Code 對話紀錄。審查這些程式碼幫助我對 SQLite 交易語義的邊緣情況建立了更好的心智模型!

我升級到 Claude Max 每月 200 美元的方案(我之前是每月 100 美元),以增加我在 7 月 7 日 Fablepocalypse 之前的 Fable 額度,屆時即使是 Claude Max 訂閱者也必須支付模型的完整 API 費用。

我很好奇如果我直接支付這些費用會花多少錢。起初我以為這些數字無法獲得,因為我是使用 Claude Code 網頁版遠端執行的工作,然後我意識到我可以在現有會話中執行 AgentsView 來獲取成本估計!

Claude 找到了如何使用 session list --include-children 命令,並得出了以下結果:

對話紀錄 模型 成本

主會話 claude-fable-5 $141.02

API 介面掃描代理 claude-fable-5 $2.40

交易/原子操作審查代理 claude-fable-5 $2.39

rc1 後提交審查代理 claude-fable-5 $1.72

遷移審查代理 claude-fable-5 $1.40

提示詞計數代理 claude-opus-4-8 $0.32

總計 $149.25

我很高興我訂閱了!我真的應該遵循自己的建議,更多地利用更便宜的子代理。這是 claude.ai/settings/usage 目前顯示的內容:我目前還有幾個其他由 Fable 驅動的重大專案正在進行中,目標是在漲價前讓 Fable 使用條達到 100%。

以下是 RC 的完整發布說明。我讓 Fable 在每次變更落地時,將這些內容添加到變更日誌的「未發布」部分,並在過程中進行審查。這有一個巧妙的副作用,即變更日誌的提交歷史可以作為每次發布變更的簡潔摘要。過去我一直有手寫發布說明的政策,但老實說,這些比我自己寫的還要好。發布說明是個很好的例子,說明我可以將寫作外包給代理,因為它們需要枯燥、可預測且準確。

破壞性變更:

使用 db.execute() 執行的寫入語句現在會自動提交,除非交易已經開啟,在這種情況下它們會加入現有交易。以前它們會開啟一個隱式交易,該交易會一直保持開啟直到某些東西提交它,寫入在同一連線上讀取時似乎有效,但在連線關閉時會被靜默回滾。

依賴回滾未提交 db.execute() 寫入的程式碼應使用新的 db.begin() 方法首先開啟顯式交易。交易模型在「交易與儲存您的變更」中有完整說明。

db.query() 現在會在呼叫時立即執行其 SQL,而不是等到返回的生成器首次迭代時。行仍然會在迭代期間惰性獲取。SQL 錯誤現在會在呼叫點引發,諸如 INSERT ... RETURNING 之類的語句會立即執行並提交,無需迭代其結果,並且傳遞一個不返回行的語句,以前是靜默的無操作,現在會引發 ValueError,建議改用 db.execute()。以這種方式被拒絕的語句會在錯誤引發之前回滾,因此它對資料庫沒有影響。

Python API 驗證錯誤現在會引發 ValueError 而不是 AssertionError。以前,無效的參數,例如 create_table() 沒有欄位,transform() 在不存在的表格上,或者同時傳遞 ignore=True 和 replace=True,是使用裸露的 assert 語句拒絕的,當 Python 帶有 -O 標誌運行時,這些語句會被靜默跳過。

針對這些情況捕獲 AssertionError 的程式碼應改為捕獲 ValueError。

table.upsert() 和 table.upsert_all() 現在會在記錄缺少任何主鍵欄位的值或其值為 None 時引發 PrimaryKeyRequired。以前,此類記錄,永遠無法匹配現有行,會被悄悄地作為全新行插入,或者在插入已經發生後觸發令人困惑的 KeyError。

db.enable_wal() 和 db.disable_wal() 現在在交易開啟時呼叫會引發 sqlite_utils.db.TransactionError。以前,它們會作為更改日誌模式的副作用靜默提交開啟的交易,從而破壞 db.atomic() 和使用者管理交易的回滾保證。

View 類別不再具有 enable_fts() 方法。它僅存在以引發 NotImplementedError,因為視圖不支援全文搜尋,現在呼叫它會引發 AttributeError,並且該方法不再出現在 API 參考中。sqlite-utils enable-fts 命令在指向視圖時會顯示一個清晰的錯誤。

無操作的 -d/--detect-types 標誌已從 insert 和 upsert 命令中移除。自 4.0a1 以來,CSV/TSV 資料的類型檢測已是預設值,因此該標誌沒有任何作用,使用它的調用應直接移除它。--no-detect-types 仍然可用於禁用檢測。

如果傳遞的連線是使用 Python 3.12+ sqlite3.connect(..., autocommit=True) 或 autocommit=False 選項建立的,Database() 現在會引發 sqlite_utils.db.TransactionError。

commit() 和 rollback() 在這些連線上的行為不同,這以前導致函式庫進行的每次寫入在連線關閉時被靜默丟棄。

其他所有:

修復了 table.delete_where()、table.optimize() 和 table.rebui 中的錯誤。