"""SQLite の DB の複製(バックアップ API)と,複製した DB の点検・件数.
目録(`catalog.sqlite`)と対応表(`identity.sqlite`)の複製に使う(`tiasdata backup`).
対応表を開くのは取り込みの処理だけという規則(coding-style.md)の例外として,複製と点検の
ためにだけ開く.中身(患者 ID)は読まず,行の数だけを返す.
"""
import sqlite3
from contextlib import closing
from pathlib import Path
def _connect_read_only(path: Path) -> sqlite3.Connection:
# 複製元・点検する複製は書き換えない(無ければ作らない)
uri = f"{path.resolve().as_uri()}?mode=ro"
try:
return sqlite3.connect(uri, uri=True)
except sqlite3.OperationalError as e:
raise ValueError(f"DB を開けない: {e}") from e
def copy_database(source: Path, target: Path) -> None:
"""SQLite のバックアップ API(`Connection.backup`)で DB を複製する.
ファイルのコピーと違い,書き込みの途中の状態を写さない(複製元の 1 つの時点の内容になる).
Args:
source: 複製元の DB.
target: 複製先のファイル(空のファイルか,無いこと).
Raises:
ValueError: 複製元を開けない.
sqlite3.Error: 複製に失敗した.
"""
with closing(_connect_read_only(source)) as src, closing(sqlite3.connect(target)) as dst:
src.backup(dst)
def integrity_problems(path: Path) -> int:
"""DB に `PRAGMA integrity_check` をかけ,問題の数を返す(0 なら ok).
問題の内容(表・ページの位置)は返さない.
Args:
path: 点検する DB.
Returns:
問題の数.
"""
with closing(_connect_read_only(path)) as conn:
rows = conn.execute("PRAGMA integrity_check").fetchall()
if rows == [("ok",)]:
return 0
return len(rows)
def user_version(path: Path) -> int:
"""DB のスキーマの版(`PRAGMA user_version`)を返す."""
with closing(_connect_read_only(path)) as conn:
return conn.execute("PRAGMA user_version").fetchone()[0]
# 目録の件数として表示する表(表示名 → SQL).値の埋め込みを避けるため SQL ごと固定する
_CATALOG_COUNTS: dict[str, str] = {
"subjects": "SELECT COUNT(*) FROM subjects",
"captures": "SELECT COUNT(*) FROM captures",
"assets": "SELECT COUNT(*) FROM assets",
"annotations": "SELECT COUNT(*) FROM annotations",
"dataset_versions": "SELECT COUNT(*) FROM dataset_versions",
}
def count_catalog_rows(path: Path) -> dict[str, int]:
"""目録の DB の主な表の行の数を返す(症例・撮影・ファイル・所見・版).
Args:
path: 目録の DB(複製したもの).
Returns:
表の名前 → 行の数.
"""
with closing(_connect_read_only(path)) as conn:
return {name: conn.execute(sql).fetchone()[0] for name, sql in _CATALOG_COUNTS.items()}
def count_identity_rows(path: Path) -> int:
"""対応表の DB の行の数を返す(中身は読まない).
Args:
path: 対応表の DB(複製したもの).
Returns:
`identity_map` の行の数.
"""
with closing(_connect_read_only(path)) as conn:
return conn.execute("SELECT COUNT(*) FROM identity_map").fetchone()[0]