From efefc3f5b0bd1ab75cb581b29295614be9f8e753 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Wed, 24 Jun 2026 17:10:22 +0200 Subject: [PATCH] =?UTF-8?q?feat(backup):=20snapshot=20SQLite=20coh=C3=A9re?= =?UTF-8?q?nt=20et=20contr=C3=B4le=20d'int=C3=A9grit=C3=A9=20(#89)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implémente les trois fonctions de gestion de snapshots SQLite: - make_snapshot: crée un snapshot gzippé cohérent via sqlite3.Connection.backup() - write_snapshot: écrit le snapshot décompressé à destination - verify_integrity: valide l'intégrité d'une base avec PRAGMA integrity_check Co-Authored-By: Claude Sonnet 4.6 --- src/backup/snapshot.py | 40 +++++++++++++++++++++++++++++++++++ tests/backup/test_snapshot.py | 36 +++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 src/backup/snapshot.py create mode 100644 tests/backup/test_snapshot.py diff --git a/src/backup/snapshot.py b/src/backup/snapshot.py new file mode 100644 index 0000000..eb3660e --- /dev/null +++ b/src/backup/snapshot.py @@ -0,0 +1,40 @@ +import gzip +import sqlite3 +import tempfile +from pathlib import Path + + +def make_snapshot(db_path: Path) -> bytes: + with tempfile.NamedTemporaryFile(suffix=".sqlite", delete=False) as tmp: + tmp_path = Path(tmp.name) + try: + src = sqlite3.connect(str(db_path)) + try: + dst = sqlite3.connect(str(tmp_path)) + try: + src.backup(dst) + finally: + dst.close() + finally: + src.close() + return gzip.compress(tmp_path.read_bytes()) + finally: + tmp_path.unlink(missing_ok=True) + + +def write_snapshot(gzipped: bytes, dest: Path) -> None: + dest.write_bytes(gzip.decompress(gzipped)) + + +def verify_integrity(db_path: Path) -> bool: + try: + conn = sqlite3.connect(str(db_path)) + except sqlite3.DatabaseError: + return False + try: + row = conn.execute("PRAGMA integrity_check").fetchone() + return row is not None and row[0] == "ok" + except sqlite3.DatabaseError: + return False + finally: + conn.close() diff --git a/tests/backup/test_snapshot.py b/tests/backup/test_snapshot.py new file mode 100644 index 0000000..fa0132f --- /dev/null +++ b/tests/backup/test_snapshot.py @@ -0,0 +1,36 @@ +import sqlite3 +from pathlib import Path + +from src.backup.snapshot import make_snapshot, verify_integrity, write_snapshot + + +def _make_db(path: Path): + conn = sqlite3.connect(str(path)) + conn.execute("CREATE TABLE t (id INTEGER, val TEXT)") + conn.execute("INSERT INTO t VALUES (1, 'a'), (2, 'b')") + conn.commit() + conn.close() + + +def test_snapshot_roundtrip_preserves_rows(tmp_path): + src = tmp_path / "users.sqlite" + _make_db(src) + gzipped = make_snapshot(src) + restored = tmp_path / "restored.sqlite" + write_snapshot(gzipped, restored) + conn = sqlite3.connect(str(restored)) + rows = conn.execute("SELECT id, val FROM t ORDER BY id").fetchall() + conn.close() + assert rows == [(1, "a"), (2, "b")] + + +def test_verify_integrity_ok(tmp_path): + src = tmp_path / "users.sqlite" + _make_db(src) + assert verify_integrity(src) is True + + +def test_verify_integrity_detects_corruption(tmp_path): + bad = tmp_path / "bad.sqlite" + bad.write_bytes(b"this is not a sqlite database") + assert verify_integrity(bad) is False