fix: immutable=1 probe URI (no sidecar creation guaranteed), fs work inside executor job, WAL-header corruption test case
- probe URI adds immutable=1: SQLite's documented guarantee that the read-only connection performs no writes and creates no -wal/-shm sidecars, even against a WAL-header database - exists/stat/resolve and URI construction moved inside the executor job with the probe (no event-loop disk I/O) - new WAL-header corruption test asserts byte-identical file and no -shm creation from the probe
This commit is contained in:
@@ -66,19 +66,27 @@ async def _preflight_existing_file(
|
|||||||
that rewrites the header byte 18/19 and can create -wal/-shm
|
that rewrites the header byte 18/19 and can create -wal/-shm
|
||||||
sidecars — which would modify a corrupt file before detection and
|
sidecars — which would modify a corrupt file before detection and
|
||||||
break the leave-untouched guarantee. This preflight therefore
|
break the leave-untouched guarantee. This preflight therefore
|
||||||
validates the WHOLE file with a READ-ONLY connection (mode=ro URI,
|
validates the WHOLE file with a READ-ONLY connection (mode=ro +
|
||||||
PRAGMA integrity_check, run on the executor like every other DB
|
immutable=1 URI, PRAGMA integrity_check, run on the executor like
|
||||||
call): corruption anywhere fails there, before anything is
|
every other DB call): corruption anywhere fails there, before
|
||||||
written. Missing/empty files skip the preflight (they are created
|
anything is written. ``immutable=1`` is SQLite's documented
|
||||||
fresh by the real open).
|
guarantee that the connection performs NO writes and creates NO
|
||||||
|
sidecars — even a WAL-header file gets its -wal/-shm reads served
|
||||||
|
without creating the shared-memory file. Missing/empty files skip
|
||||||
|
the preflight (they are created fresh by the real open). All
|
||||||
|
filesystem stat/URI work runs inside the executor job with the
|
||||||
|
probe so the event loop never touches disk.
|
||||||
"""
|
"""
|
||||||
if not db_path.exists() or db_path.stat().st_size == 0:
|
|
||||||
return
|
|
||||||
# Properly escape the path: '#'/'?'/'%' in a directory or file name
|
|
||||||
# would otherwise be parsed as URI fragments/parameters/escapes.
|
|
||||||
uri = db_path.resolve().as_uri() + "?mode=ro"
|
|
||||||
|
|
||||||
def _probe() -> None:
|
def _probe() -> None:
|
||||||
|
# The file-state check and URI construction live inside the
|
||||||
|
# executor job: exists/stat/resolve are disk I/O.
|
||||||
|
if not db_path.exists() or db_path.stat().st_size == 0:
|
||||||
|
return
|
||||||
|
# Properly escape the path: '#'/'?'/'%' in a directory or file
|
||||||
|
# name would otherwise be parsed as URI fragments/parameters/
|
||||||
|
# escapes. immutable=1: SQLite guarantees no writes and no
|
||||||
|
# sidecar creation from this connection.
|
||||||
|
uri = db_path.resolve().as_uri() + "?mode=ro&immutable=1"
|
||||||
conn = sqlite3.connect(uri, uri=True)
|
conn = sqlite3.connect(uri, uri=True)
|
||||||
try:
|
try:
|
||||||
row = conn.execute("PRAGMA integrity_check").fetchone()
|
row = conn.execute("PRAGMA integrity_check").fetchone()
|
||||||
|
|||||||
@@ -250,6 +250,38 @@ def test_migration_stage_corruption_leaves_db_and_sidecars_untouched(
|
|||||||
assert not (tmp_path / "nestquest.db-shm").exists()
|
assert not (tmp_path / "nestquest.db-shm").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_wal_mode_corruption_leaves_file_and_sidecars_untouched(
|
||||||
|
tmp_path, monkeypatch
|
||||||
|
) -> None:
|
||||||
|
"""A corrupt WAL-HEADER file must be rejected without creating
|
||||||
|
-shm sidecars from the read-only preflight itself.
|
||||||
|
|
||||||
|
The immutable=1 probe URI is what guarantees a read-only
|
||||||
|
connection creates no sidecars even for a WAL-header database; the
|
||||||
|
sidecar assertions after the call prove it.
|
||||||
|
"""
|
||||||
|
db_path = tmp_path / "nestquest.db"
|
||||||
|
# WAL fixture: created via the wrapper (journal_mode=WAL persists).
|
||||||
|
_valid_database(db_path)
|
||||||
|
data = bytearray(db_path.read_bytes())
|
||||||
|
# Corrupt the WAL header flag region: bytes 18/19 hold the
|
||||||
|
# read/write version numbers (2 = WAL). Flip them to garbage so
|
||||||
|
# the file claims a broken journal mode.
|
||||||
|
assert len(data) > 64
|
||||||
|
data[18] = 0x99
|
||||||
|
data[19] = 0x99
|
||||||
|
db_path.write_bytes(bytes(data))
|
||||||
|
corrupt_bytes = db_path.read_bytes()
|
||||||
|
digest = hashlib.sha256(corrupt_bytes).hexdigest()
|
||||||
|
|
||||||
|
with pytest.raises(ConfigEntryNotReady):
|
||||||
|
_run(_async_open_database(_make_hass_mock(), db_path))
|
||||||
|
|
||||||
|
# Main file byte-identical; no -shm appeared from the probe.
|
||||||
|
assert hashlib.sha256(db_path.read_bytes()).hexdigest() == digest
|
||||||
|
assert not (tmp_path / "nestquest.db-shm").exists()
|
||||||
|
|
||||||
|
|
||||||
def test_open_failure_closes_connection_when_opened(
|
def test_open_failure_closes_connection_when_opened(
|
||||||
tmp_path, monkeypatch
|
tmp_path, monkeypatch
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user