diff --git a/CHANGELOG.md b/CHANGELOG.md index 63fa22fa..670bef4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog ## [Unreleased] +### Fixed + +- Opening a database no longer writes to it: reads no longer rewrite the stored embedding settings or change the stored version, and the version is never downgraded. + ## [0.54.0] - 2026-06-04 ### Added diff --git a/haiku_rag_slim/haiku/rag/store/engine.py b/haiku_rag_slim/haiku/rag/store/engine.py index ac4a7b18..2e158a5c 100644 --- a/haiku_rag_slim/haiku/rag/store/engine.py +++ b/haiku_rag_slim/haiku/rag/store/engine.py @@ -12,6 +12,7 @@ import lancedb import pyarrow as pa from lancedb.index import FTS, BTree, IvfPq from lancedb.pydantic import LanceModel, Vector +from packaging.version import parse from pydantic import Field from haiku.rag.config import AppConfig, Config @@ -509,7 +510,7 @@ class Store: await self.set_haiku_version(metadata.version("haiku.rag-slim")) async def _check_migrations(self) -> None: - """Check if migrations are pending and error or update version accordingly. + """Raise if migrations are pending. Opening never writes the version. Raises: MigrationRequiredError: If migrations are pending. @@ -529,10 +530,6 @@ class Store: "Run 'haiku-rag migrate' to upgrade." ) - # No pending migrations - update version silently if needed (writable only) - if not self._read_only and db_version != current_version: - await self.set_haiku_version(current_version) - async def migrate(self) -> list[str]: """Run pending database migrations. @@ -551,8 +548,9 @@ class Store: applied = await run_pending_upgrades(self, db_version) - # Update version after successful migration - if applied or db_version != current_version: + # Advance the schema marker only forward — never downgrade a database + # opened with an older build than last stamped it. + if parse(current_version) > parse(db_version): await self.set_haiku_version(current_version) return applied diff --git a/tests/store/test_migrations.py b/tests/store/test_migrations.py index ff8f0f94..763649c4 100644 --- a/tests/store/test_migrations.py +++ b/tests/store/test_migrations.py @@ -38,21 +38,19 @@ class TestMigrationCheck: pass @pytest.mark.asyncio - async def test_version_bump_without_pending_migrations_updates_silently( - self, temp_db_path - ): - """When version is outdated but no migrations pending, update version silently.""" + async def test_open_does_not_change_version(self, temp_db_path): + """Opening a database never writes the version, even when it differs. + + A version newer than all upgrade steps has no pending migrations, so the + open succeeds; the stored version must be left untouched (no downgrade, + no bump) because opening is a pure read. + """ async with Store(temp_db_path, create=True) as store: - # Set an older version that has no pending migrations - # (newer than all current upgrade steps) await store.set_haiku_version("100.0.0") - # Re-open - should update version silently, no error + # Re-open writable - no error, version unchanged async with Store(temp_db_path) as store: - # Version should now be current - version = await store.get_haiku_version() - expected = metadata.version("haiku.rag-slim") - assert version == expected + assert await store.get_haiku_version() == "100.0.0" @pytest.mark.asyncio async def test_pending_migrations_raises_error(self, temp_db_path): @@ -160,6 +158,21 @@ class TestMigrateMethod: applied = await store.migrate() assert applied == [] + @pytest.mark.asyncio + async def test_migrate_does_not_downgrade_future_version(self, temp_db_path): + """migrate() must not move the version backwards. + + Running an older build's migrate against a DB stamped with a newer + version (no pending upgrades) leaves the stored version untouched. + """ + async with Store(temp_db_path, create=True) as store: + await store.set_haiku_version("100.0.0") + + async with Store(temp_db_path, skip_migration_check=True) as store: + applied = await store.migrate() + assert applied == [] + assert await store.get_haiku_version() == "100.0.0" + @pytest.mark.asyncio async def test_migrate_raises_read_only_error(self, temp_db_path): """Store.migrate() should raise ReadOnlyError in read-only mode."""