Newer
Older
TIASDataHub / src / tiasdata / catalog / identity.py
"""対応表(`identity.sqlite`.実際の患者 ID ↔ 研究用 ID)の操作.

対応表を開くのは取り込みの処理だけにする(coding-style.md「臨床データを扱うコードの規則」).
目録の接続に `ATTACH` して使い,目録と対応表への登録を 1 つのトランザクションにまとめる
(途中で失敗したら両方とも何も残らないようにするため).

ここで扱う患者 ID は例外メッセージ・ログに出さない.
"""

import sqlite3
from pathlib import Path

from tiasdata.catalog.migrate import require_latest
from tiasdata.catalog.records import parse_id_number

# 目録の接続の中での対応表のスキーマ名
IDENTITY_SCHEMA = "identity"


def attach_identity(conn: sqlite3.Connection, path: Path) -> None:
    """目録の接続に対応表を `identity` として ATTACH する.

    対応表のファイルが無いときは作らずに停止する(`tiasdata init` の前に誤って空の対応表を
    作らないため).スキーマが最新でなければ停止する.

    Args:
        conn: `connect()` で開いた目録の接続(トランザクションの外であること).
        path: 対応表のパス.

    Raises:
        ValueError: 対応表が無い,開けない,またはスキーマが最新でない.
    """
    if not path.is_file():
        raise ValueError("対応表が無い.先に `tiasdata init` を実行する")
    # connect() は URI を有効にして開くため,ATTACH でも mode=rw(無ければ作らない)が効く
    uri = f"{path.resolve().as_uri()}?mode=rw"
    try:
        conn.execute(f"ATTACH DATABASE ? AS {IDENTITY_SCHEMA}", (uri,))
    except sqlite3.OperationalError as e:
        raise ValueError(f"対応表を開けない: {e}") from e
    require_latest(conn, "identity", schema=IDENTITY_SCHEMA)


def find_subject_id(conn: sqlite3.Connection, patient_key: str) -> str | None:
    """患者 ID に対応する研究用 ID を引く(無ければ None)."""
    row = conn.execute(
        f"SELECT subject_id FROM {IDENTITY_SCHEMA}.identity_map WHERE patient_key = ?",
        (patient_key,),
    ).fetchone()
    return row[0] if row else None


def max_identity_subject_number(conn: sqlite3.Connection) -> int:
    """対応表の研究用 ID の最大の番号を返す(無ければ 0).

    目録を作り直した場合等に,対応表にだけある研究用 ID と重ならないようにするため.
    """
    (value,) = conn.execute(
        f"SELECT MAX(subject_id) FROM {IDENTITY_SCHEMA}.identity_map"
    ).fetchone()
    return parse_id_number(value)


def insert_identity(
    conn: sqlite3.Connection, patient_key: str, subject_id: str, created_at: str
) -> None:
    """患者 ID と研究用 ID の対応を登録する."""
    conn.execute(
        f"INSERT INTO {IDENTITY_SCHEMA}.identity_map (patient_key, subject_id, created_at)"
        " VALUES (?, ?, ?)",
        (patient_key, subject_id, created_at),
    )