SQLite rename-and-swap migration: bounding the final exclusive lock for a small application
0 reputation · 28 Aug 2021, 07:21 UTC
0 reputation · 28 Aug 2021, 07:21 UTC
A small application runs on SQLite 3.45 in WAL mode with a few tables under 100 MB each. The team wants to change a column type and add a NOT NULL constraint — operations that require a full table rewrite. The documented pattern is to create a new table, copy rows in batches, install triggers to mirror writes, then rename the old table away and the new table into place.
The final RENAME TABLE step (and trigger cleanup) still demands an EXCLUSIVE lock, however brief. WAL mode does not relax this requirement. The application can tolerate a few hundred milliseconds of write unavailability, but not seconds.
SQLite documentation states that RENAME TABLE acquires an exclusive lock, but does not quantify its duration for a database of this size under realistic concurrent read load. Checkpoint pressure from WAL may also extend the window.
PRAGMA busy_timeout on the migration connection risk leaving the schema in an inconsistent state if the rename times out?31275 reputation · 28 Aug 2021, 10:08 UTC
Yes, the final ALTER TABLE ... RENAME can almost always be kept under 200 ms for a 100 MB table — because the lock hold time has essentially nothing to do with table size. The rename itself is a schema-header update measured in microseconds. What determines your real window is how long the migration connection must wait for existing readers to finish, and whether a WAL checkpoint gets pulled into the same transaction. And no, a short busy_timeout cannot corrupt the schema: a timed-out rename fails cleanly with SQLITE_BUSY and leaves the database exactly as it was.
Separate two phases: waiting to acquire the lock, and holding it.
wal_autocheckpoint or a closing connection) adds I/O. Run PRAGMA wal_checkpoint(TRUNCATE); on the migration connection before the rename so the final step starts with a nearly empty WAL.PRAGMA wal_checkpoint(TRUNCATE);PRAGMA busy_timeout = 3000; — long enough to ride out normal short readers, short enough to fail fast if something is stuck.SQLITE_BUSY, retry the whole transaction a bounded number of times, then abort the migration and alert.busy_timeout only controls how long SQLite retries before returning SQLITE_BUSY. It never interrupts a lock already held, and it never partially applies a rename. Because the rename-and-cleanup runs inside one transaction, the outcome is binary: either the whole swap is visible, or nothing changed. The only real risk is application-level — code that misreads a timeout as success. Check the return code explicitly.
On a staging copy, spawn readers with controlled transaction lengths (e.g., 50 ms, 500 ms, 5 s) and time the rename with a high-resolution clock. You should see hold time near zero and total time tracking the longest active reader. Then set busy_timeout = 100, hold a reader open, and confirm the rename returns SQLITE_BUSY while PRAGMA table_list / sqlite_master show the original schema untouched.
The 200 ms figure is an engineering target, not a documented guarantee — SQLite does not publish lock-duration bounds. If your application has any code path that can hold a read transaction open for seconds, no timeout setting will save the window; you must pause or drain those readers during the cutover. That reader behavior is the one diagnostic worth confirming before scheduling the migration.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.