"""Sources-version counter (phase 53, task 01). The single-row ``sources_meta`` table (migration 0010, the ``kb_overview`` id=1 precedent) records **which generation of the knowledge base** is current. ``version`` is bumped exactly once per sync that actually changed the KB (task 02 — change-gated on ``added + updated + pruned > 0``; a failed sync never bumps), so it doubles as the invalidation marker for saved chats: the chats API stamps each saved row with the current version at save time (task 03) and flags rows stamped with an older generation *stale* — their answers predate the current index and may be Regenerated against it (task 05). Session convention (phase 53): :func:`bump_sources_version` only *flushes* — the **caller** commits. The two sync paths (the admin Sync button, ``scripts/import_docs``) each own their session, and the bump must land in the same transaction as the KB changes it records. """ from __future__ import annotations from sqlalchemy.orm import Session from app.models import SourcesMeta def current_sources_version(db: Session) -> int: """The current KB generation (one PK read of the seeded row). Returns ``0`` when the row is absent — defensive, never raises (the migration seeds the row, so absence only happens if someone deleted it out-of-band). """ row = db.get(SourcesMeta, 1) if row is None: return 0 return row.version def bump_sources_version(db: Session) -> int: """Advance the KB generation by one; return the new version. Upserts the single row (creating it if it was ever deleted), increments ``version`` by one, and ``flush``es — the **caller** commits, because each of the two sync paths owns its session and the bump must commit together with the KB changes it witnessed. """ row = db.get(SourcesMeta, 1) if row is None: row = SourcesMeta(id=1, version=0) db.add(row) row.version += 1 db.flush() return row.version