#!/usr/bin/env python3 """paper_idを指定してpapers.dbから論文を削除する。 PDFファイルそのものは削除しない。 既定はdry-run。実際に削除する場合は--applyを指定する。 """ from __future__ import annotations import argparse import shutil import sqlite3 import sys from dataclasses import dataclass from pathlib import Path from common import CONFIG @dataclass(frozen=True) class Paper: paper_id: int title: str first_author: str | None year: int | None doi: str | None pdf_path: str | None file_sha256: str | None def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="paper_idを指定してDBレコードを削除する" ) parser.add_argument( "paper_id", type=int, help="削除するpapers.id", ) parser.add_argument( "--database", type=Path, default=CONFIG.path.database, help="SQLiteデータベース", ) parser.add_argument( "--apply", action="store_true", help="削除を実際にDBへ反映する", ) parser.add_argument( "--no-backup", action="store_true", help="削除前バックアップを作成しない", ) return parser.parse_args() def open_database( database: Path, ) -> sqlite3.Connection: connection = sqlite3.connect(database) connection.row_factory = sqlite3.Row connection.execute("PRAGMA foreign_keys=ON") return connection def load_paper( connection: sqlite3.Connection, paper_id: int, ) -> Paper | None: row = connection.execute( """ SELECT id, title, first_author, year, doi, pdf_path, file_sha256 FROM papers WHERE id = ? """, (paper_id,), ).fetchone() if row is None: return None return Paper( paper_id=row["id"], title=row["title"], first_author=row["first_author"], year=row["year"], doi=row["doi"], pdf_path=row["pdf_path"], file_sha256=row["file_sha256"], ) def print_paper(paper: Paper) -> None: print("=== 削除対象 ===") print(f"id: {paper.paper_id}") print(f"title: {paper.title}") print( "first_author: " f"{paper.first_author or '(none)'}" ) print(f"year: {paper.year or '(none)'}") print(f"doi: {paper.doi or '(none)'}") print( f"pdf_path: {paper.pdf_path or '(none)'}" ) print( "file_sha256: " f"{paper.file_sha256 or '(none)'}" ) def create_backup(database: Path) -> Path: backup = database.with_name( database.name + ".before-delete" ) counter = 1 while backup.exists(): backup = database.with_name( database.name + f".before-delete.{counter}" ) counter += 1 shutil.copy2(database, backup) return backup def delete_paper( connection: sqlite3.Connection, paper_id: int, ) -> None: """関連レコードを含めて1トランザクションで削除する。""" with connection: tables = ( "paper_authors", "paper_keywords", "processing_log", ) for table in tables: if table_exists(connection, table): connection.execute( f"DELETE FROM {table} WHERE paper_id = ?", (paper_id,), ) cursor = connection.execute( "DELETE FROM papers WHERE id = ?", (paper_id,), ) if cursor.rowcount != 1: raise RuntimeError( f"papers.id={paper_id}を削除できませんでした" ) def table_exists( connection: sqlite3.Connection, table: str, ) -> bool: row = connection.execute( """ SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? """, (table,), ).fetchone() return row is not None def validate_database( connection: sqlite3.Connection, ) -> None: integrity = connection.execute( "PRAGMA integrity_check" ).fetchone()[0] if integrity != "ok": raise RuntimeError( f"integrity_check: {integrity}" ) foreign_keys = connection.execute( "PRAGMA foreign_key_check" ).fetchall() if foreign_keys: raise RuntimeError( "foreign_key_check: " f"{len(foreign_keys)}件" ) def main() -> int: args = parse_args() try: database = args.database.expanduser().resolve() if not database.is_file(): raise FileNotFoundError(database) with open_database(database) as connection: paper = load_paper( connection, args.paper_id, ) if paper is None: print( f"paper_id={args.paper_id}は存在しません。", file=sys.stderr, ) return 1 print_paper(paper) if not args.apply: print() print( "削除するには --apply を指定してください。" ) return 0 if not args.no_backup: backup = create_backup(database) print() print(f"DBバックアップ: {backup}") delete_paper( connection, args.paper_id, ) validate_database(connection) print() print( f"Deleted paper_id={args.paper_id}" ) print("PDFファイルは削除していません。") return 0 except KeyboardInterrupt: return 130 except Exception as exc: print( f"delete_paper.py: {exc}", file=sys.stderr, ) return 1 if __name__ == "__main__": raise SystemExit(main())