313 lines
6.9 KiB
Python
Executable File
313 lines
6.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sqlite3
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Iterable
|
|
|
|
from common import CONFIG, resolve_pdf_path
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class MissingPdf:
|
|
"""見つからないPDFの情報。"""
|
|
|
|
paper_id: int
|
|
title: str
|
|
pdf_path: str
|
|
resolved_path: str | None
|
|
reason: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class IntegrityResult:
|
|
"""SQLite整合性検証の結果。"""
|
|
|
|
integrity: str
|
|
foreign_key_count: int
|
|
errors: tuple[str, ...]
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
"""コマンドライン引数を解析する。"""
|
|
parser = argparse.ArgumentParser(
|
|
description="papers.dbの整合性を検証する"
|
|
)
|
|
parser.add_argument(
|
|
"database",
|
|
nargs="?",
|
|
default=str(CONFIG.path.database),
|
|
help="検証するSQLiteデータベース",
|
|
)
|
|
parser.add_argument(
|
|
"--pdf-root",
|
|
default=str(CONFIG.path.pdf_root),
|
|
help="PDF_ROOTのパス",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def open_database(database: Path) -> sqlite3.Connection:
|
|
"""SQLiteデータベースへ接続する。"""
|
|
connection = sqlite3.connect(database)
|
|
connection.row_factory = sqlite3.Row
|
|
connection.execute("PRAGMA foreign_keys=ON")
|
|
return connection
|
|
|
|
|
|
def check_integrity(
|
|
connection: sqlite3.Connection,
|
|
) -> IntegrityResult:
|
|
"""SQLite本体と外部キーの整合性を検証する。"""
|
|
errors: list[str] = []
|
|
|
|
integrity = connection.execute(
|
|
"PRAGMA integrity_check"
|
|
).fetchone()[0]
|
|
|
|
if integrity != "ok":
|
|
errors.append(f"integrity_check: {integrity}")
|
|
|
|
foreign_keys = connection.execute(
|
|
"PRAGMA foreign_key_check"
|
|
).fetchall()
|
|
|
|
foreign_key_count = len(foreign_keys)
|
|
|
|
if foreign_key_count:
|
|
errors.append(
|
|
f"foreign keys: {foreign_key_count}"
|
|
)
|
|
|
|
return IntegrityResult(
|
|
integrity=integrity,
|
|
foreign_key_count=foreign_key_count,
|
|
errors=tuple(errors),
|
|
)
|
|
|
|
|
|
def validation_queries() -> tuple[tuple[str, str], ...]:
|
|
"""DB内容の検証用SQLを返す。"""
|
|
return (
|
|
(
|
|
"duplicate DOI",
|
|
"""
|
|
SELECT doi
|
|
FROM papers
|
|
WHERE doi IS NOT NULL
|
|
AND doi <> ''
|
|
GROUP BY doi
|
|
HAVING COUNT(*) > 1
|
|
""",
|
|
),
|
|
(
|
|
"duplicate SHA256",
|
|
"""
|
|
SELECT file_sha256
|
|
FROM papers
|
|
WHERE file_sha256 IS NOT NULL
|
|
AND file_sha256 <> ''
|
|
GROUP BY file_sha256
|
|
HAVING COUNT(*) > 1
|
|
""",
|
|
),
|
|
(
|
|
"empty title",
|
|
"""
|
|
SELECT id
|
|
FROM papers
|
|
WHERE title IS NULL
|
|
OR TRIM(title) = ''
|
|
""",
|
|
),
|
|
(
|
|
"invalid relevance",
|
|
"""
|
|
SELECT id
|
|
FROM papers
|
|
WHERE relevance NOT BETWEEN 0 AND 5
|
|
""",
|
|
),
|
|
(
|
|
"invalid priority",
|
|
"""
|
|
SELECT id
|
|
FROM papers
|
|
WHERE priority NOT IN ('A', 'B', 'C', 'D')
|
|
""",
|
|
),
|
|
)
|
|
|
|
|
|
def check_database_values(
|
|
connection: sqlite3.Connection,
|
|
) -> list[str]:
|
|
"""重複や値域を検証する。"""
|
|
errors: list[str] = []
|
|
|
|
for label, sql in validation_queries():
|
|
rows = connection.execute(sql).fetchall()
|
|
|
|
if rows:
|
|
errors.append(f"{label}: {len(rows)}")
|
|
|
|
return errors
|
|
|
|
|
|
def iter_papers_with_pdf(
|
|
connection: sqlite3.Connection,
|
|
) -> Iterable[sqlite3.Row]:
|
|
"""PDFパスが登録された論文を返す。"""
|
|
sql = """
|
|
SELECT id, title, pdf_path
|
|
FROM papers
|
|
WHERE pdf_path IS NOT NULL
|
|
AND TRIM(pdf_path) <> ''
|
|
ORDER BY id
|
|
"""
|
|
return connection.execute(sql)
|
|
|
|
|
|
def inspect_pdf(
|
|
pdf_root: Path,
|
|
row: sqlite3.Row,
|
|
) -> MissingPdf | None:
|
|
"""1件のPDFが存在するか確認する。"""
|
|
try:
|
|
path = resolve_pdf_path(
|
|
pdf_root,
|
|
row["pdf_path"],
|
|
)
|
|
|
|
if path.is_file():
|
|
return None
|
|
|
|
return MissingPdf(
|
|
paper_id=row["id"],
|
|
title=row["title"],
|
|
pdf_path=row["pdf_path"],
|
|
resolved_path=str(path),
|
|
reason="file does not exist",
|
|
)
|
|
|
|
except Exception as exc:
|
|
return MissingPdf(
|
|
paper_id=row["id"],
|
|
title=row["title"],
|
|
pdf_path=row["pdf_path"],
|
|
resolved_path=None,
|
|
reason=f"{type(exc).__name__}: {exc}",
|
|
)
|
|
|
|
|
|
def find_missing_pdfs(
|
|
connection: sqlite3.Connection,
|
|
pdf_root: Path,
|
|
) -> list[MissingPdf]:
|
|
"""存在しないPDFを検索する。"""
|
|
missing: list[MissingPdf] = []
|
|
|
|
for row in iter_papers_with_pdf(connection):
|
|
item = inspect_pdf(pdf_root, row)
|
|
|
|
if item is not None:
|
|
missing.append(item)
|
|
|
|
return missing
|
|
|
|
|
|
def print_missing_pdfs(
|
|
missing: Iterable[MissingPdf],
|
|
) -> None:
|
|
"""見つからないPDFの詳細を表示する。"""
|
|
items = list(missing)
|
|
|
|
if not items:
|
|
return
|
|
|
|
print()
|
|
print("=== Missing PDFs ===")
|
|
|
|
for item in items:
|
|
print(f"paper_id: {item.paper_id}")
|
|
print(f"title: {item.title}")
|
|
print(f"pdf_path: {item.pdf_path}")
|
|
print(
|
|
"resolved_path:",
|
|
item.resolved_path
|
|
or "(resolve failed)",
|
|
)
|
|
print(f"reason: {item.reason}")
|
|
print()
|
|
|
|
|
|
def print_summary(
|
|
integrity: IntegrityResult,
|
|
errors: list[str],
|
|
) -> None:
|
|
"""検証結果を表示する。"""
|
|
print(f"integrity_check: {integrity.integrity}")
|
|
print(
|
|
"foreign_key_violations:",
|
|
integrity.foreign_key_count,
|
|
)
|
|
print(f"errors: {len(errors)}")
|
|
|
|
for error in errors:
|
|
print(f"- {error}")
|
|
|
|
|
|
def validate(
|
|
database: Path,
|
|
pdf_root: Path,
|
|
) -> list[str]:
|
|
"""データベース全体を検証する。"""
|
|
with open_database(database) as connection:
|
|
integrity = check_integrity(connection)
|
|
value_errors = check_database_values(
|
|
connection
|
|
)
|
|
missing = find_missing_pdfs(
|
|
connection,
|
|
pdf_root,
|
|
)
|
|
|
|
errors = list(integrity.errors)
|
|
errors.extend(value_errors)
|
|
|
|
if missing:
|
|
errors.append(
|
|
f"missing PDF: {len(missing)}"
|
|
)
|
|
|
|
print_missing_pdfs(missing)
|
|
print_summary(integrity, errors)
|
|
|
|
return errors
|
|
|
|
|
|
def main() -> int:
|
|
"""メイン処理。"""
|
|
args = parse_args()
|
|
|
|
database = Path(
|
|
args.database
|
|
).expanduser().resolve()
|
|
|
|
pdf_root = Path(
|
|
args.pdf_root
|
|
).expanduser().resolve()
|
|
|
|
errors = validate(
|
|
database,
|
|
pdf_root,
|
|
)
|
|
|
|
return 1 if errors else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|