"""Real-Postgres check that the full export is ORDERED, so the backup file deduplicates between runs (milestone 523). Only Postgres shows this. An unordered select returns rows in heap order, and an UPDATE writes the row's new version at the end of the heap — so before export_full_backup ordered its queries, editing one old note moved it to the bottom of `notes`. Every byte after its old place shifted, and a restic run over the next backup stored most of the table again. A mocked session returns whatever order it was handed and could never catch it. """ import io import tarfile import pytest import pytest_asyncio from sqlalchemy import select from scribe.models import async_session from scribe.models.note import Note from scribe.models.user import User from scribe.services import backup from scribe.services.backup_file import write_archive_to from tests.helpers import ensure_user pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")] USERNAME = "backup_file_ordering_owner" async def _purge() -> None: async with async_session() as s: for user in (await s.execute( select(User).where(User.username == USERNAME) )).scalars().all(): for note in (await s.execute( select(Note).where(Note.user_id == user.id) )).scalars().all(): await s.delete(note) await s.delete(user) await s.commit() @pytest_asyncio.fixture(autouse=True) async def _no_leftovers(): """Setup only — see the rule_version roundtrip module for why a teardown here would break the next test's event loop.""" await _purge() @pytest_asyncio.fixture async def notes(): async with async_session() as s: user = await ensure_user(s, USERNAME) rows = [Note(user_id=user.id, title=f"ordering {n}", body="b" * 100) for n in range(5)] s.add_all(rows) await s.commit() ids = [n.id for n in rows] yield ids await _purge() def _sections(payload: dict) -> dict[str, bytes]: buf = io.BytesIO() write_archive_to(payload, buf, app_version="test") buf.seek(0) with tarfile.open(fileobj=buf, mode="r:") as tar: return {m.name: tar.extractfile(m).read() for m in tar.getmembers() if m.name != "manifest.json"} async def _edit_first(ids: list[int]) -> None: async with async_session() as s: note = await s.get(Note, ids[0]) note.body = "edited after the first export" await s.commit() async def test_an_edited_row_keeps_its_place(notes): await _edit_first(notes) payload = await backup.export_full_backup() exported = [row["id"] for row in payload["notes"]] assert exported == sorted(exported) mine = [i for i in exported if i in set(notes)] assert mine == notes async def test_two_exports_of_unchanged_data_are_the_same_bytes(notes): await _edit_first(notes) first = _sections(await backup.export_full_backup()) second = _sections(await backup.export_full_backup()) assert first.keys() == second.keys() for name in first: assert first[name] == second[name], name