SQLite recovery APIs¶
Use overwrite=True on a database builder to recover a corrupt cache from
source data. For direct control, import recover_cached_database or
restore_database from datacache; their parameters, return values, and
exceptions are in the API reference.
from contextlib import closing
from pathlib import Path
from tempfile import TemporaryDirectory
import pandas as pd
from datacache import db_from_dataframe, restore_database
with TemporaryDirectory() as directory:
source = Path(directory) / "complete.db"
destination = Path(directory) / "broken.db"
with closing(db_from_dataframe(source, "records", pd.DataFrame({"id": [1, 2]}))):
pass
destination.write_bytes(b"not a SQLite database")
restore_database(source, destination)
with closing(db_from_dataframe(destination, "records", None)) as connection:
print(connection.execute("SELECT id FROM records ORDER BY id").fetchall())
[(1,), (2,)]
Recovery validates the complete replacement before publishing. SQLite's locks protect the destination throughout publication and rollback. An existing complete cache, including a concurrent writer's completed replacement, remains usable if publication fails before commit. Errors after commit leave the complete replacement installed; reopen and inspect its version before retrying. Corrupt or incomplete source databases are rejected; they are not copied over a complete cache. Normal matching-cache reuse still returns the complete cached data without consuming replacement inputs.
The rebuilt destination passes integrity_check before commit. Recovery resets
corrupt vacuum metadata, retains AUTOINCREMENT high-water marks, and preserves a
readable destination text encoding. A temporary converted source makes UTF-8
and UTF-16 replacements compatible; if the destination encoding is lost, the
source encoding is used; readers that cached a different encoding must reopen.
Fresh schema and pager counters invalidate existing
readers, including when the original header is gone. WAL recovery also refreshes
the shared reader state. WAL connections can prevent exclusive access
while other connections remain open, even between transactions; a lock timeout
leaves the cache unchanged. Close those connections before retrying.
Snapshots use bounded memory mappings, which Windows allows across locked byte ranges. This preserves file contents during handled publication failures without raw reads or writes through a second handle at SQLite's 1 GiB lock boundary. All mappings close before SQLite continues. Recovery needs disk space for staging, the original-file snapshot, and the SQLite journal. Process termination, power loss, or storage failure during rollback is outside the handled-failure guarantee; do not treat an error as a successfully installed cache.
Worker interfaces¶
All APIs below are public imports from datacache.sqlite_recovery. Applications
normally need restore_database, which creates and waits for the worker. The
lower-level interfaces are for integrations that own their worker lifecycle.
They are documented here rather than hidden behind underscored helper names.
restore_database¶
restore_database(source, destination, *, timeout=5.0) is the same callable
exported from datacache. It returns None after publication and propagates
the worker's exception on failure. The source must remain stable until the
call returns, including while cancellation waits for the worker to finish.
restore_database_in_process¶
restore_database_in_process(source, destination, *, timeout=5.0) performs the
same validated copy and returns None, without starting a child process.
Call it only in a dedicated process with no other connections to either path.
In particular, independently linked SQLite libraries must not manage the
same files in one process: closing one library's descriptors can release the
other library's POSIX locks. Use restore_database unless you own that isolation.
read_database_schema¶
read_database_schema(source) validates a completed DataCache source using
quick_check and its single integer version record. It returns
(schema, encoding, has_sequence): ordered (type, name, create_sql) tuples for
ordinary tables and indexes, SQLite's canonical encoding name, and a boolean
indicating sqlite_sequence. Views, triggers, and virtual tables are rejected.
Invalid sources raise sqlite3.Error or ValueError. Keep the source stable
until publication finishes.
database_with_encoding¶
database_with_encoding(source, encoding, *, directory=None) is a context manager
yielding a stable source path. Validate the source with read_database_schema
first and keep it closed to writers. encoding must be UTF-8, UTF-16le, or
UTF-16be; other values raise ValueError. Matching encodings reuse the source.
Otherwise a temporary copy preserves table values, constraints, indexes, and
AUTOINCREMENT sequence state, transferring rows in batches of at most 1,000.
directory selects temporary storage; None uses the system default. Close all
connections that attach the yielded database before leaving the context, which
deletes any temporary copy. SQLite and filesystem errors propagate.
publish_change_counter¶
publish_change_counter(connection, database_file, *, wal=False) publishes a
fresh pager change counter after a committed recovery transaction and returns
it as an integer. The caller must retain SQLite's exclusive lock and supply an
open readable/writable file handle for the same destination. Active transactions
raise ValueError. The function checkpoints any WAL and updates both the file
change counter and its matching version-valid-for field through a flushed,
synchronized header mapping. No further SQL writes may use this connection.
Close it before closing database_file. SQLite and I/O errors propagate; the
already committed replacement remains intact.
With wal=True, restore the WAL mode lost with a damaged header. The connection
must have SQLITE_DBCONFIG_NO_CKPT_ON_CLOSE enabled so existing WAL file handles
remain valid. After closing all worker handles, call notify_wal_readers.
notify_wal_readers¶
notify_wal_readers(destination, *, timeout=5.0) opens a normal SQLite connection
and writes a page-one frame through the shared WAL index. Exclusive recovery
can use a private WAL index, so this step notifies existing readers after the
exclusive worker connection closes. It holds a write transaction and rewrites
the current user_version to the same value, preserving any completed intervening
writer's data and metadata. No other worker handles may remain open. Returns
None; timeout is lock-wait seconds, and SQLite errors propagate.
copy_file_contents¶
copy_file_contents(source, destination) copies the entire source file,
resizes the destination to the exact source length, and returns None.
The arguments are open binary regular files with fileno(); the destination
must be readable and writable. Buffered writes are flushed before mapping.
At most two 1 MiB windows are mapped at once on supported platforms. Mappings
are flushed and closed on exit; the supplied files stay open and their seek
positions are not used. Call os.fsync(destination.fileno()) when durable
storage synchronization is required.
The caller must prevent concurrent file changes and hold the appropriate
SQLite locks when copying an active database. This API does not acquire locks
and does not provide an atomic copy by itself. Passing the same file twice
raises ValueError; mapping and I/O failures propagate. A failed copy can leave
its destination partially written, so the recovery publisher only restores
from snapshots whose copy completed successfully.
open_recovery_connection¶
open_recovery_connection(path, timeout) opens an existing database for writing
without creating it. timeout is lock-wait seconds. It returns an autocommit
sqlite3.Connection on Python 3.12+, or NativeSQLiteConnection on Python 3.11.
Both provide execute, setconfig, in_transaction, and close. The caller
owns the returned connection and must close it. Use this in an isolated worker.
NativeSQLiteConnection¶
NativeSQLiteConnection(path, timeout) opens an existing database using public
SQLite C APIs and raises sqlite3.Error on failure. It is a control-SQL adapter
for recovery workers, not a replacement for Python's DB-API connection.
| Member | Contract |
|---|---|
execute(sql) |
Execute trusted SQL without parameters; return SQLiteRows. Results are strings or None; numeric results are text, and BLOB queries are unsupported. Copy cache rows with SQL INSERT … SELECT to preserve their original SQLite types. |
setconfig(option, enabled) |
Set an integer boolean SQLite database-configuration option. Unsupported options raise sqlite3.Error. |
in_transaction |
Boolean property indicating an active transaction. |
close() |
Close the connection. Repeated closes are safe; other operations require an open connection. |
api, handle |
Native library and owned c_void_p connection handle. Use only together; never substitute pointers from Python's managed connections. |
SQLiteRows¶
SQLiteRows(rows) stores materialized row tuples returned by the native
adapter. rows is the supplied collection. fetchone() returns its first
tuple, or None if empty; it does not advance a cursor. Values produced by
NativeSQLiteConnection are strings or None.
execute_sql¶
execute_sql(connection, sql) executes control SQL on either recovery
connection type, discards result rows, and returns None. SQLite exceptions
propagate. The connection remains caller-owned.
read_schema_version¶
read_schema_version(connection) returns the main database's schema cookie as
an integer or propagates a SQLite error. A readable cookie is still untrusted
when storage is corrupt; it cannot tell you what another reader has cached.
Recovery chooses a fresh high-range cookie independently of damaged headers.
load_sqlite_api¶
load_sqlite_api() returns a cached ctypes.CDLL whose SQLite recovery
functions have configured signatures. It prefers Python's linked SQLite and
falls back to the platform library. If the necessary functions are unavailable,
it raises RuntimeError. Use native handles only in an isolated worker.
open_native_database¶
open_native_database(api, path, flags) returns an owned native connection
handle using SQLite's integer sqlite3_open_v2 flags. It additionally enables
URI paths and private cache. api must be returned by load_sqlite_api.
Close successful handles with api.sqlite3_close(handle); failed opens close
their handle before raising. NativeSQLiteConnection manages that ownership
for ordinary worker code.
check_sqlite_result¶
check_sqlite_result(api, connection, code) returns None for SQLITE_OK.
Otherwise it raises sqlite3.DatabaseError for corruption or
sqlite3.OperationalError for other failures, preserving sqlite_errorcode.
connection must be a live native handle owned by the same api library.