Watch at Ease看見世界,讀懂生活。

科技 · 實用指南

SQLite RETURNING 已取一列仍無法提交?結束每個寫入游標

取得 ID 不一定代表陳述式執行完畢。重播寫入或捨棄結果前,先比較單列、多列與兩個游標的行為。

八種 RETURNING 讀取方式與提交結果示意
原創實測示意圖:CPython3.12.14/SQLite3.53.1 的八組暫存檔案結果,使用明確的舊式交易控制。 Watch at Ease

INSERT ... RETURNING 後 COMMIT 顯示「SQL statements in progress」時,先檢查自己的未完成寫入結果,別直接認定其他處理程序鎖住資料庫。取得一個 ID 與陳述式執行完畢是兩件事。

八組暫存檔案測試說明 fetchone 何時有效、為何不能推廣,以及關閉游標為什麼能讓寫入提交,卻遺失應用程式所需的回傳 ID。

1. 記錄驅動程式與交易控制方

記錄 Python、sqlite3.sqlite_version、完整 SQL、所有仍存活的結果游標與交易設定。測試使用 Linux 上的 CPython3.12.14 與 SQLite3.53.1,明確設定 autocommit=sqlite3.LEGACY_TRANSACTION_CONTROL 及 isolation_level="DEFERRED"。結果僅適用於標準函式庫 sqlite3,不代表 APSW 或 ORM。

保留完整例外與失敗階段。SQLite 允許有未完成 SELECT 時執行明確的 COMMIT,但未完成寫入可能阻止提交。檢查本連線的 RETURNING 寫入前,不要將所有 SQLITE_BUSY 歸為外部競爭,或先增加逾時。也不要透過切換交易模式意外提交。

2. 單列成功並非通用規則

每組測試均建立新的檔案與 t(x integer) 資料表。未讀取的 INSERT INTO t VALUES(1) RETURNING x 阻止提交;在這個驅動程式中,以 fetchone() 取出唯一一列後可提交。但 INSERT INTO t VALUES(1),(2),(3) RETURNING x 只取一列後,提交仍失敗。

另外兩組三列樣本分別以 fetchall()、反覆 fetchmany(1) 直到空批次,皆可提交,重新開啟後保留全部三個值。繼續前應儲存每個所需 ID。不能假定回傳順序來配對輸入:SQLite 不保證 RETURNING 順序。分批讀取限制的是 Python 清單大小,無法消除引擎已緩衝的 RETURNING 輸出。

3. 結束所有寫入游標,保留所需輸出

第六組關閉未讀取的三列游標後,提交成功,重新開啟可見三個插入值,但應用程式未收集任何回傳列。因此 Cursor.close() 不能取代回復交易,也不能取代交付所需 ID。只有明確決定放棄這些輸出時,才採用此處理。

另外兩組在同一連線保留兩個 INSERT ... RETURNING 游標。讀完第一個後,第二個仍未完成,提交失敗;兩個都讀完後提交成功,重新開啟可見值 1 與 99。由交易控制元件保留游標參照,不要讓輔助函式回傳一個可見結果,卻悄悄留下另一個寫入結果。

4. 回傳結果與提交分開驗收

所有提交失敗的測試在嘗試後,in_transaction 仍為 true。測試接著關閉未完成游標並明確回復交易,重新開啟後沒有插入列。空結果來自主動清理,並非提交例外自動撤銷寫入。交易仍進行時重播 INSERT,可能執行額外寫入。

需要回傳結果時,應先讀取並驗證相關輸出、關閉游標,再由約定的交易控制方提交。從新連線核對持久保存的值,並保留精確列數的回歸測試。出錯時遵循應用程式的回復交易策略,不要因取得 ID 就報告成功。本次未測試並行讀取端、觸發程序、大量輸出或其他 autocommit 模式,應分別診斷這些情況。

讀取回傳列、結束游標與提交的差異示意
原創診斷示意圖:輸出交付、陳述式結束及交易提交分別驗收;關閉游標並未撤銷已執行的插入。 Watch at Ease