SQLite 3.36.0 Restore Command Fails with Read-Only Database Error After Upgrade


Understanding the Read-Only Database Error During Sequential Restores in SQLite 3.36.0

The core issue revolves around a change in behavior observed when restoring data from a disk-based database to an in-memory database using SQLite’s .restore command across multiple operations. After upgrading from SQLite 3.31.1 to 3.36.0, the second restore operation fails with an error indicating an attempt to write to a read-only database. This error does not occur in the earlier version, suggesting that transactional or locking behavior has changed between releases. The problem is critical for workflows involving repeated restores to in-memory databases, as it disrupts data migration pipelines, testing environments, or caching mechanisms that rely on sequential restore operations.

The error message itself—though not explicitly quoted in the discussion—is consistent with SQLite’s SQLITE_READONLY or SQLITE_READONLY_DBMOVED error codes. These errors signal that the target database connection does not have write privileges, either due to insufficient filesystem permissions, an explicit read-only mode set during connection, or unresolved transactional locks that prevent further modifications. The crux of the issue lies in the interaction between the .restore command’s implementation, the transactional state of the in-memory database, and potential threading conflicts introduced or exacerbated in SQLite 3.36.0.


Potential Drivers of the Read-Only Database Error in Sequential Restores

Transactional State Mismanagement in the Backup/Restore Workflow

SQLite’s .restore command utilizes the Backup API under the hood, which copies data between a source and destination database. The Backup API operates in a non-blocking manner for disk databases but requires careful handling of transactional states. In SQLite 3.36.0, changes to how the Backup API finalizes transactions during a restore operation could leave the destination database in a read-only state if the backup process is interrupted or not properly concluded. For example, if the first restore operation leaves an implicit transaction open on the in-memory database, the second restore operation may attempt to modify the database while it is still locked, triggering the read-only error.

Threading Conflicts in Concurrent Database Access

The Backup API documentation explicitly warns about threading considerations: backup operations should not be interrupted by other threads modifying the source or destination database. While the original discussion does not confirm multi-threaded code, SQLite 3.36.0 introduced optimizations to concurrency control that might expose latent race conditions. If the application uses separate threads to manage the first and second restore operations, the destination in-memory database might not have fully released its write locks before the second restore begins. This could result in the destination database being temporarily marked as read-only until the lock is cleared—a scenario not encountered in SQLite 3.31.1 due to differences in lock acquisition timing.

Changes to In-Memory Database Handling Across Connections

In-memory databases in SQLite are ephemeral and exist only for the duration of their connection lifetime unless named explicitly. If the application reuses the same connection for both restore operations, the in-memory database’s transactional state might persist between restores. SQLite 3.36.0 may enforce stricter isolation levels or transactional consistency checks, preventing subsequent restores from overwriting data if the prior transaction is not explicitly committed or rolled back. This would leave the in-memory database in a read-only state until the transaction is resolved.


Diagnosing and Resolving Read-Only Errors in Multi-Restore Workflows

Step 1: Verify the Transactional State of the In-Memory Database

Before initiating the second restore operation, explicitly check the transactional state of the in-memory database using PRAGMA schema.integrity_check; or SELECT * FROM sqlite_schema;. If the integrity check fails or the schema is locked, this indicates an unresolved transaction. Manually committing any open transactions with COMMIT; or finalizing the Backup API process with sqlite3_backup_finish() ensures the destination database is writable. In SQLite 3.36.0, the Backup API may require explicit finalization steps that were previously handled automatically in older versions.

Step 2: Isolate Restore Operations Using Separate Connections

To avoid transactional contamination between restore operations, use a dedicated database connection for each restore. For in-memory databases, this can be achieved by opening a new connection with sqlite3_open_v2(":memory:", &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL); before each restore. This guarantees that the prior restore’s transactional state does not affect subsequent operations. If the application shares a single connection across restores, reset the in-memory database by closing and reopening the connection or executing sqlite3_db_config(db, SQLITE_DBCONFIG_RESET_DATABASE, 1, 0); to clear its state.

Step 3: Audit Threading and Concurrency Patterns

Review the application’s threading model to ensure no concurrent access to the in-memory database during restores. Wrap backup operations in mutex locks or critical sections to serialize access. If using the SQLite CLI, test the restore sequence in a single-threaded environment to isolate threading as the culprit. Enable SQLite’s debugging mode with sqlite3_config(SQLITE_CONFIG_LOG, ...) to log lock states and thread IDs, revealing contention points. Adjust the threading model to guarantee that the Backup API’s requirements—no concurrent writes to the source or destination—are strictly met.

Step 4: Downgrade or Patch SQLite to Identify Behavioral Changes

Temporarily revert to SQLite 3.31.1 to confirm that the error is version-specific. If the problem disappears, bisect SQLite versions between 3.31.1 and 3.36.0 to identify the exact commit or change responsible. Review SQLite’s changelog for modifications to the Backup API, transaction handling, or in-memory database management. For instance, SQLite 3.36.0 introduced stricter schema validation during backup operations, which might leave transactions open if schema inconsistencies are detected. Apply targeted patches or adjust the application code to accommodate these changes.

Step 5: Implement Explicit Transaction Control and Backup Finalization

Modify the restore workflow to explicitly control transactions around the Backup API calls. Before initiating a restore, execute BEGIN IMMEDIATE; to acquire a write lock on the destination database. After the restore, execute COMMIT; to release the lock. When using the .restore command in the CLI, follow it with COMMIT; to ensure no implicit transactions remain open. For programmatic use, ensure that sqlite3_backup_finish() is called and its return code checked to confirm the backup completed successfully. Handle SQLITE_BUSY or SQLITE_LOCKED errors by retrying the restore after a short delay.

Step 6: Configure Database Connections for Read-Write Access

Explicitly specify read-write mode when opening connections to the in-memory database. Use sqlite3_open_v2() with SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE flags to avoid accidental read-only mode. Verify the connection’s write permissions with PRAGMA journal_mode;—if this returns memory or wal, the database is writable. If it returns off or raises an error, the connection is read-only. Reset the connection or adjust filesystem permissions if applicable (though in-memory databases are not filesystem-dependent).

Step 7: Utilize the SQLITE_DBCONFIG_RESET_DATABASE Option

For applications that reuse the same in-memory database connection across restores, leverage the SQLITE_DBCONFIG_RESET_DATABASE configuration option to reset the database’s state between operations. This pragma clears all schema and data, effectively creating a fresh in-memory database. Execute sqlite3_db_config(db, SQLITE_DBCONFIG_RESET_DATABASE, 1, 0); followed by sqlite3_exec(db, "VACUUM;", 0, 0, 0); to reset and compact the database. This ensures that prior restore operations do not leave transactional artifacts that trigger read-only errors.

Step 8: Monitor and Adjust SQLite’s Locking Timeout Behavior

SQLite’s default busy timeout is 0 milliseconds, meaning any lock contention immediately returns SQLITE_BUSY. Increase the timeout to allow the restore operation to wait for locks to be released. Use sqlite3_busy_timeout(db, 5000); to set a 5-second timeout. For more granular control, implement a busy handler with sqlite3_busy_handler() to retry operations after escalating delays. This mitigates transient locks caused by delayed transaction finalization in SQLite 3.36.0.

Step 9: Replicate the Workflow in the SQLite CLI for Isolation

Reproduce the issue using the SQLite command-line interface to rule out application-specific factors. For example:

sqlite3
.open :memory:
.restore main 'backup1.db'
.restore main 'backup2.db'

If the second .restore fails in the CLI, the problem is inherent to SQLite 3.36.0’s restore implementation. If it succeeds, the issue lies in the application’s connection management, threading, or transaction logic. Use CLI testing to narrow down environmental variables.

Step 10: Review and Update Backup/Restore Code to Modern API Standards

Refactor legacy code using deprecated Backup API patterns. Ensure that sqlite3_backup_init() is called with valid source and destination database handles, and that sqlite3_backup_step() is looped until it returns SQLITE_DONE. Always check the return value of sqlite3_backup_remaining() and sqlite3_backup_pagecount() to monitor progress. In SQLite 3.36.0, incomplete backup processes due to unchecked return codes may leave the destination database in an inconsistent state, leading to read-only errors. Modernize error handling to account for stricter API enforcement in newer SQLite versions.


By systematically addressing transactional states, concurrency models, and API usage, developers can resolve read-only database errors during sequential restores in SQLite 3.36.0. The solutions emphasize explicit transactional control, connection isolation, and adherence to updated Backup API requirements, ensuring compatibility across SQLite versions.

Related Guides

Leave a Reply

Your email address will not be published. Required fields are marked *