Newer
Older
TIASDataHub / tests / catalog / test_connection.py
"""目録への接続(SPEC_02 冒頭: 外部キー制約を有効にする)."""

from pathlib import Path

import pytest

from tiasdata import catalog


def test_connect_enables_foreign_keys(tmp_path: Path) -> None:
    conn = catalog.connect(tmp_path / "db.sqlite", create=True)
    try:
        assert conn.execute("PRAGMA foreign_keys").fetchone()[0] == 1
    finally:
        conn.close()


def test_connect_enables_foreign_keys_on_every_connection(tmp_path: Path) -> None:
    path = tmp_path / "db.sqlite"
    catalog.connect(path, create=True).close()
    conn = catalog.connect(path)
    try:
        assert conn.execute("PRAGMA foreign_keys").fetchone()[0] == 1
    finally:
        conn.close()


def test_connect_with_create_makes_db_file(tmp_path: Path) -> None:
    path = tmp_path / "db.sqlite"
    catalog.connect(path, create=True).close()
    assert path.is_file()


def test_connect_without_create_refuses_missing_db(tmp_path: Path) -> None:
    path = tmp_path / "missing.sqlite"
    with pytest.raises(ValueError):
        catalog.connect(path)
    # 空の目録を誤って作らない
    assert not path.exists()


def test_connect_missing_db_message_does_not_contain_path(tmp_path: Path) -> None:
    path = tmp_path / "missing.sqlite"
    with pytest.raises(ValueError) as exc:
        catalog.connect(path)
    assert str(tmp_path) not in str(exc.value)
    assert tmp_path.as_posix() not in str(exc.value)


# ---------------------------------------------------------------------------
# write_transaction(SPEC_03: 登録は 1 つのトランザクション.
# error-handling.md: 制約違反は ValueError)
# ---------------------------------------------------------------------------
def _subject(conn, subject_id: str) -> None:
    conn.execute(
        "INSERT INTO subjects (subject_id, created_at) VALUES (?, '2026-10-06T00:00:00Z')",
        (subject_id,),
    )


def _subjects(path: Path) -> list[str]:
    conn = catalog.connect(path)
    try:
        return [s for (s,) in conn.execute("SELECT subject_id FROM subjects ORDER BY 1")]
    finally:
        conn.close()


@pytest.fixture
def migrated_path(tmp_path: Path) -> Path:
    path = tmp_path / "catalog.sqlite"
    conn = catalog.connect(path, create=True)
    try:
        catalog.apply_migrations(conn, "catalog")
    finally:
        conn.close()
    return path


def test_write_transaction_commits_on_success(migrated_path: Path) -> None:
    conn = catalog.connect(migrated_path)
    try:
        with catalog.write_transaction(conn):
            _subject(conn, "S000001")
            _subject(conn, "S000002")
    finally:
        conn.close()
    assert _subjects(migrated_path) == ["S000001", "S000002"]


def test_write_transaction_converts_integrity_error_and_rolls_back(migrated_path: Path) -> None:
    conn = catalog.connect(migrated_path)
    try:
        with pytest.raises(ValueError), catalog.write_transaction(conn):
            _subject(conn, "S000001")
            _subject(conn, "bad-id")  # CHECK 違反
        assert not conn.in_transaction
    finally:
        conn.close()
    assert _subjects(migrated_path) == []


def test_write_transaction_rolls_back_on_other_error(migrated_path: Path) -> None:
    conn = catalog.connect(migrated_path)
    try:
        with pytest.raises(RuntimeError, match="boom"), catalog.write_transaction(conn):
            _subject(conn, "S000001")
            raise RuntimeError("boom")
    finally:
        conn.close()
    assert _subjects(migrated_path) == []


def test_write_transaction_includes_attached_db(migrated_path: Path, tmp_path: Path) -> None:
    # 目録と ATTACH した対応表への書き込みは同じトランザクションで取り消される
    identity_path = tmp_path / "identity.sqlite"
    conn = catalog.connect(identity_path, create=True)
    try:
        catalog.apply_migrations(conn, "identity")
    finally:
        conn.close()

    conn = catalog.connect(migrated_path)
    try:
        conn.execute("ATTACH DATABASE ? AS identity", (str(identity_path),))
        with pytest.raises(ValueError), catalog.write_transaction(conn):
            conn.execute(
                "INSERT INTO identity.identity_map (patient_key, subject_id, created_at)"
                " VALUES ('990001', 'S000001', '2026-10-06T00:00:00Z')"
            )
            _subject(conn, "S000001")
            _subject(conn, "S000001")  # 主キーの重複
    finally:
        conn.close()
    conn = catalog.connect(identity_path)
    try:
        assert conn.execute("SELECT COUNT(*) FROM identity_map").fetchone()[0] == 0
    finally:
        conn.close()
    assert _subjects(migrated_path) == []


def test_require_latest_rejects_unmigrated_db(tmp_path: Path) -> None:
    conn = catalog.connect(tmp_path / "db.sqlite", create=True)
    try:
        with pytest.raises(ValueError):
            catalog.require_latest(conn, "catalog")
    finally:
        conn.close()


def test_require_latest_accepts_migrated_db(migrated_path: Path) -> None:
    conn = catalog.connect(migrated_path)
    try:
        catalog.require_latest(conn, "catalog")
    finally:
        conn.close()


def test_require_latest_rejects_newer_db(migrated_path: Path) -> None:
    conn = catalog.connect(migrated_path)
    try:
        conn.execute(f"PRAGMA user_version = {catalog.latest_version('catalog') + 1}")
        with pytest.raises(ValueError):
            catalog.require_latest(conn, "catalog")
    finally:
        conn.close()