"""Post-migration verification: row counts + sha256 sampling.""" from __future__ import annotations import hashlib from pathlib import Path from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from ...models import Artist, Credential, ImageRecord, Source, Tag async def verify_async(db: AsyncSession, *, expected: dict | None = None) -> dict: """Return per-check status dicts. `expected` is optional row-count assertions; checks default to status='ok' when no expected provided.""" expected = expected or {} results: dict[str, dict] = {} checks = { "artist_subscriptions": ( select(func.count(Artist.id)).where(Artist.is_subscription.is_(True)) ), "source_count": select(func.count(Source.id)), "credential_count": select(func.count(Credential.id)), "tag_count": select(func.count(Tag.id)), "image_record_imported_or_downloaded": ( select(func.count(ImageRecord.id)) .where(ImageRecord.origin.in_(["imported_filesystem", "downloaded"])) ), } for name, stmt in checks.items(): actual = (await db.execute(stmt)).scalar_one() exp = expected.get(name) status = "ok" if exp is None or exp == actual else "mismatch" results[name] = {"status": status, "actual": int(actual), "expected": exp} return results async def verify_sha256_sample( db: AsyncSession, *, sample_size: int = 20, ) -> dict: """Sample N image_records; verify file exists + sha256 matches.""" rows = (await db.execute( select(ImageRecord.id, ImageRecord.path, ImageRecord.sha256) .order_by(func.random()).limit(sample_size) )).all() matched = 0 mismatched = 0 missing = 0 samples: list[dict] = [] for img_id, path, expected_sha in rows: p = Path(path) # Sync stdlib filesystem ops are intentional: this verify pass runs # inside a Celery task under asyncio.run; no other awaitables compete # for the loop. Same pattern as download_service.py. if not p.exists(): # noqa: ASYNC240 missing += 1 samples.append({"id": img_id, "path": path, "result": "missing"}) continue h = hashlib.sha256() with p.open("rb") as f: # noqa: ASYNC230 for chunk in iter(lambda: f.read(65536), b""): h.update(chunk) if h.hexdigest() == expected_sha: matched += 1 samples.append({"id": img_id, "result": "ok"}) else: mismatched += 1 samples.append({ "id": img_id, "path": path, "result": "mismatch", "expected_sha": expected_sha, "actual_sha": h.hexdigest(), }) return { "sample_size": len(rows), "matched": matched, "mismatched": mismatched, "missing": missing, "samples": samples, }