"""対応表(`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),
)