Files
transcription/tests/test_db.py
T

291 lines
11 KiB
Python

"""Tests for the database runtime and V2 schema bootstrap behavior."""
from uuid import uuid4
import pytest
from sqlalchemy import inspect
from sqlalchemy import text
from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession
from transcription.config import Settings
from transcription.config import SqliteSettings
from transcription.db import create_all
from transcription.db import dispose_database_runtime
from transcription.db import initialize_database_runtime
from transcription.db import session_scope
from transcription.db import upgrade_schema
from transcription.db.models import DocumentType
from transcription.db.models import PersonRole
@pytest.mark.asyncio
async def test_create_all_creates_expected_tables(tmp_path):
settings = Settings(
openrouter_api_key="test-key",
database=SqliteSettings(path=str(tmp_path / "schema.db")),
environment="test",
)
runtime = initialize_database_runtime(settings=settings)
try:
await create_all(engine=runtime.engine)
async with runtime.engine.connect() as conn:
table_names = set(await conn.run_sync(lambda c: inspect(c).get_table_names()))
assert "document" in table_names
assert "document_type" in table_names
assert "person" in table_names
assert "person_role" in table_names
assert "document_person" in table_names
assert "job" in table_names
assert "source" in table_names
assert "job_source" in table_names
assert "execution_attempt" in table_names
assert "processing_artifact" in table_names
assert "revision" not in table_names
finally:
await dispose_database_runtime()
@pytest.mark.asyncio
async def test_get_session_yields_async_session(tmp_path):
settings = Settings(
openrouter_api_key="test-key",
database=SqliteSettings(path=str(tmp_path / "session.db")),
environment="test",
)
initialize_database_runtime(settings=settings)
try:
async with session_scope(settings=settings) as session:
assert session is not None
finally:
await dispose_database_runtime()
@pytest.mark.asyncio
async def test_runtime_rejects_reinitialization_for_different_database(tmp_path):
"""An existing process runtime cannot silently switch database targets."""
first_settings = Settings(
openrouter_api_key="test-key",
database=SqliteSettings(path=str(tmp_path / "first.db")),
environment="test",
)
second_settings = Settings(
openrouter_api_key="test-key",
database=SqliteSettings(path=str(tmp_path / "second.db")),
environment="test",
)
initialize_database_runtime(settings=first_settings)
try:
with pytest.raises(RuntimeError, match="already initialized for a different database"):
initialize_database_runtime(settings=second_settings)
finally:
await dispose_database_runtime()
@pytest.mark.asyncio
async def test_create_all_seeds_default_registry_rows(tmp_path):
settings = Settings(
openrouter_api_key="test-key",
database=SqliteSettings(path=str(tmp_path / "seed.db")),
environment="test",
)
runtime = initialize_database_runtime(settings=settings)
try:
await create_all(engine=runtime.engine)
async with AsyncSession(runtime.engine, expire_on_commit=False) as session:
role_codes = set((await session.exec(select(PersonRole.code))).all())
type_labels = set((await session.exec(select(DocumentType.label))).all())
assert {"author", "recipient", "mentioned"}.issubset(role_codes)
assert {"Letter", "Record", "Memo"}.issubset(type_labels)
finally:
await dispose_database_runtime()
@pytest.mark.asyncio
async def test_create_all_upgrades_existing_person_table_for_family_search(tmp_path):
settings = Settings(
openrouter_api_key="test-key",
database=SqliteSettings(path=str(tmp_path / "upgrade.db")),
environment="test",
)
runtime = initialize_database_runtime(settings=settings)
try:
async with runtime.engine.begin() as connection:
await connection.execute(
text("CREATE TABLE person (id CHAR(32) PRIMARY KEY NOT NULL, full_name VARCHAR NOT NULL)")
)
await create_all(engine=runtime.engine)
async with runtime.engine.connect() as connection:
columns, indexes = await connection.run_sync(
lambda sync_connection: (
{column["name"] for column in inspect(sync_connection).get_columns("person")},
inspect(sync_connection).get_indexes("person"),
)
)
assert "family_search_id" in columns
assert any(index["column_names"] == ["family_search_id"] and index["unique"] for index in indexes)
finally:
await dispose_database_runtime()
@pytest.mark.asyncio
async def test_upgrade_migrates_document_types_to_uuid_only_identity(tmp_path):
settings = Settings(
openrouter_api_key="test-key",
database=SqliteSettings(path=str(tmp_path / "type-upgrade.db")),
environment="test",
)
runtime = initialize_database_runtime(settings=settings)
type_id = uuid4().hex
document_id = uuid4().hex
try:
async with runtime.engine.begin() as connection:
await connection.execute(
text(
"CREATE TABLE document_type ("
"id CHAR(32) PRIMARY KEY NOT NULL, "
"code VARCHAR NOT NULL, "
"label VARCHAR NOT NULL, "
"is_active BOOLEAN NOT NULL, "
"sort_order INTEGER NOT NULL, "
"created_at DATETIME NOT NULL, "
"updated_at DATETIME NOT NULL"
")"
)
)
await connection.execute(text("CREATE UNIQUE INDEX ix_document_type_code ON document_type (code)"))
await connection.execute(
text(
"CREATE TABLE document ("
"id CHAR(32) PRIMARY KEY NOT NULL, "
"name VARCHAR NOT NULL, "
"document_type_id CHAR(32), "
"document_type VARCHAR, "
"document_date DATE, "
"document_date_raw VARCHAR, "
"location_created VARCHAR, "
"notes VARCHAR, "
"archive_identifier VARCHAR, "
"created_at DATETIME NOT NULL, "
"updated_at DATETIME NOT NULL"
")"
)
)
await connection.execute(
text(
"INSERT INTO document_type "
"(id, code, label, is_active, sort_order, created_at, updated_at) "
"VALUES (:id, 'letter', 'Letter', 1, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
),
{"id": type_id},
)
await connection.execute(
text(
"INSERT INTO document "
"(id, name, document_type_id, document_type, created_at, updated_at) "
"VALUES (:id, 'Legacy Letter', NULL, 'letter', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)"
),
{"id": document_id},
)
await upgrade_schema(engine=runtime.engine)
async with runtime.engine.connect() as connection:
type_columns, document_columns, migrated_type_id, normalized_label = await connection.run_sync(
lambda sync_connection: (
{column["name"] for column in inspect(sync_connection).get_columns("document_type")},
{column["name"] for column in inspect(sync_connection).get_columns("document")},
sync_connection.execute(
text("SELECT document_type_id FROM document WHERE id = :id"),
{"id": document_id},
).scalar_one(),
sync_connection.execute(
text("SELECT normalized_label FROM document_type WHERE id = :id"),
{"id": type_id},
).scalar_one(),
)
)
assert {"code", "sort_order"}.isdisjoint(type_columns)
assert "document_type" not in document_columns
assert migrated_type_id == type_id
assert normalized_label == "letter"
finally:
await dispose_database_runtime()
@pytest.mark.asyncio
async def test_v42_upgrade_adds_evidence_tables_without_rewriting_legacy_snapshot(tmp_path):
settings = Settings(
openrouter_api_key="test-key",
database=SqliteSettings(path=str(tmp_path / "v42-upgrade.db")),
environment="test",
)
runtime = initialize_database_runtime(settings=settings)
try:
async with runtime.engine.begin() as connection:
await connection.execute(text("CREATE TABLE job (id CHAR(32) PRIMARY KEY NOT NULL)"))
await connection.execute(text("CREATE TABLE source (id CHAR(32) PRIMARY KEY NOT NULL)"))
await connection.execute(
text(
"CREATE TABLE job_source ("
"id CHAR(32) PRIMARY KEY NOT NULL, "
"job_id CHAR(32) NOT NULL, "
"source_id CHAR(32) NOT NULL, "
"raw_api_response JSON"
")"
)
)
await connection.execute(text("INSERT INTO job (id) VALUES ('job-1')"))
await connection.execute(text("INSERT INTO source (id) VALUES ('source-1')"))
await connection.execute(
text(
"INSERT INTO job_source (id, job_id, source_id, raw_api_response) "
"VALUES ('link-1', 'job-1', 'source-1', :snapshot)"
),
{"snapshot": '{"legacy":true}'},
)
await upgrade_schema(engine=runtime.engine)
await upgrade_schema(engine=runtime.engine)
async with runtime.engine.connect() as connection:
table_names = set(await connection.run_sync(lambda c: inspect(c).get_table_names()))
legacy_snapshot = (
await connection.execute(text("SELECT raw_api_response FROM job_source WHERE id = 'link-1'"))
).scalar_one()
assert {"execution_attempt", "processing_artifact"}.issubset(table_names)
assert "legacy" in legacy_snapshot
finally:
await dispose_database_runtime()
def test_bootstrap_policy_production_defaults_false():
settings = Settings(openrouter_api_key="test-key", environment="production")
assert settings.should_bootstrap_schema is False
def test_bootstrap_policy_development_defaults_true():
settings = Settings(openrouter_api_key="test-key", environment="development")
assert settings.should_bootstrap_schema is True
def test_bootstrap_policy_explicit_override_true():
settings = Settings(
openrouter_api_key="test-key",
environment="production",
bootstrap_schema_on_startup=True,
)
assert settings.should_bootstrap_schema is True