import sqlite3 import pytest def _create_partial_schema(db_path, current_schema_version) -> None: conn = sqlite3.connect(str(db_path)) try: conn.execute("CREATE TABLE metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL)") conn.execute( "INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', ?)", (str(current_schema_version),), ) conn.execute( """ CREATE TABLE assets ( id INTEGER PRIMARY KEY AUTOINCREMENT, filename TEXT NOT NULL, filepath TEXT NOT NULL UNIQUE, kind TEXT NOT NULL, ext TEXT NOT NULL, size INTEGER NOT NULL, mtime INTEGER NOT NULL ) """ ) conn.execute( """ CREATE TABLE asset_metadata ( asset_id INTEGER PRIMARY KEY, rating INTEGER DEFAULT 0 ) """ ) conn.commit() finally: conn.close() async def _assert_table_contains_columns(db, table: str, expected_columns: list[str]) -> None: cols = await db.aquery(f"PRAGMA table_info('{table}')") assert cols.ok, cols.error col_names = {row["name"] for row in (cols.data or [])} for name in expected_columns: assert name in col_names async def _assert_schema_healed_queries(db) -> None: assert ( await db.aquery("SELECT source, root_id, enhanced_caption, content_hash, phash, hash_state FROM assets LIMIT 0") ).ok assert ( await db.aquery( """ SELECT metadata_text, metadata_raw, has_workflow, has_generation_data, metadata_quality FROM asset_metadata LIMIT 0 """ ) ).ok @pytest.mark.asyncio async def test_schema_self_heals_missing_columns(tmp_path): from mjr_am_backend.adapters.db.schema import CURRENT_SCHEMA_VERSION, migrate_schema from mjr_am_backend.adapters.db.sqlite import Sqlite db_path = tmp_path / "partial_schema.db" _create_partial_schema(db_path, CURRENT_SCHEMA_VERSION) db = Sqlite(str(db_path), attach={"vec": str(tmp_path / "vectors.sqlite")}) try: result = await migrate_schema(db) assert result.ok, result.error await _assert_table_contains_columns( db, "assets", [ "source", "root_id", "subfolder", "width", "height", "duration", "enhanced_caption", "indexed_at", "content_hash", "phash", "hash_state", ], ) await _assert_table_contains_columns( db, "asset_metadata", [ "metadata_text", "workflow_hash", "has_workflow", "has_generation_data", "metadata_quality", "metadata_raw", ], ) await _assert_schema_healed_queries(db) finally: try: await db.aclose() except Exception: pass @pytest.mark.asyncio async def test_sqlite_query_missing_column_triggers_self_heal(tmp_path): from mjr_am_backend.adapters.db.schema import ensure_tables_exist from mjr_am_backend.adapters.db.sqlite import Sqlite db_path = tmp_path / "missing_hash_columns.db" conn = sqlite3.connect(str(db_path)) try: conn.execute( """ CREATE TABLE assets ( id INTEGER PRIMARY KEY AUTOINCREMENT, filename TEXT NOT NULL, filepath TEXT NOT NULL UNIQUE, kind TEXT NOT NULL, ext TEXT NOT NULL, size INTEGER NOT NULL, mtime INTEGER NOT NULL ) """ ) conn.commit() finally: conn.close() db = Sqlite(str(db_path), attach={"vec": str(tmp_path / "vectors.sqlite")}) try: # Ensure required companion tables exist, but intentionally do not run migrate_schema(). assert (await ensure_tables_exist(db)).ok res = await db.aquery("SELECT a.content_hash FROM assets a LIMIT 0") assert res.ok, res.error finally: try: await db.aclose() except Exception: pass