"""SQLite-backed global terminology glossary.""" from __future__ import annotations import sqlite3 from collections.abc import Iterable, Iterator from contextlib import contextmanager from dataclasses import dataclass from datetime import UTC, datetime from pathlib import Path GLOSSARY_CATEGORIES = ( "product", "material", "organization", "technical_term", "acronym", "other", ) class GlossaryConflictError(ValueError): """Raised when a canonical term or alias conflicts with existing terminology.""" @dataclass(frozen=True) class GlossaryEntry: id: int canonical_term: str category: str description: str | None is_active: bool aliases: tuple[str, ...] created_at: str updated_at: str class GlossaryRepository: """Small data-access boundary for the local glossary database.""" def __init__(self, database_path: Path) -> None: self.database_path = Path(database_path) def initialize(self) -> None: """Create the database and current schema when absent.""" self.database_path.parent.mkdir(parents=True, exist_ok=True) with self._connect() as connection: connection.executescript( """ CREATE TABLE IF NOT EXISTS glossary_entries ( id INTEGER PRIMARY KEY, canonical_term TEXT NOT NULL COLLATE NOCASE UNIQUE, category TEXT NOT NULL CHECK (category IN ( 'product', 'material', 'organization', 'technical_term', 'acronym', 'other' )), description TEXT, is_active INTEGER NOT NULL DEFAULT 1 CHECK (is_active IN (0, 1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS glossary_aliases ( id INTEGER PRIMARY KEY, entry_id INTEGER NOT NULL REFERENCES glossary_entries(id) ON DELETE CASCADE, alias TEXT NOT NULL COLLATE NOCASE UNIQUE, created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_glossary_aliases_entry_id ON glossary_aliases(entry_id); PRAGMA user_version = 1; """ ) def create( self, canonical_term: str, category: str, *, aliases: Iterable[str] = (), description: str | None = None, is_active: bool = True, ) -> GlossaryEntry: canonical, normalized_aliases = self._validate_values(canonical_term, category, aliases) now = _timestamp() try: with self._connect() as connection: self._ensure_terms_available(connection, canonical, normalized_aliases) cursor = connection.execute( """INSERT INTO glossary_entries (canonical_term, category, description, is_active, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)""", (canonical, category, _optional_text(description), is_active, now, now), ) entry_id = int(cursor.lastrowid) connection.executemany( "INSERT INTO glossary_aliases (entry_id, alias, created_at) VALUES (?, ?, ?)", ((entry_id, alias, now) for alias in normalized_aliases), ) except sqlite3.IntegrityError as exc: raise GlossaryConflictError("Canonical term or alias already exists.") from exc return self.get(entry_id) def get(self, entry_id: int) -> GlossaryEntry: with self._connect() as connection: row = connection.execute( "SELECT * FROM glossary_entries WHERE id = ?", (entry_id,) ).fetchone() if row is None: raise KeyError(f"Unknown glossary entry: {entry_id}") return self._to_entry(connection, row) def list(self, search: str = "", *, active_only: bool = False) -> list[GlossaryEntry]: clauses: list[str] = [] parameters: list[object] = [] if active_only: clauses.append("entry.is_active = 1") if search.strip(): clauses.append( "(entry.canonical_term LIKE ? COLLATE NOCASE " "OR entry.category LIKE ? COLLATE NOCASE " "OR entry.description LIKE ? COLLATE NOCASE " "OR alias.alias LIKE ? COLLATE NOCASE)" ) pattern = f"%{search.strip()}%" parameters.extend([pattern] * 4) where = f"WHERE {' AND '.join(clauses)}" if clauses else "" with self._connect() as connection: rows = connection.execute( f"""SELECT DISTINCT entry.* FROM glossary_entries AS entry LEFT JOIN glossary_aliases AS alias ON alias.entry_id = entry.id {where} ORDER BY entry.canonical_term COLLATE NOCASE""", # noqa: S608 parameters, ).fetchall() return [self._to_entry(connection, row) for row in rows] def update( self, entry_id: int, canonical_term: str, category: str, *, aliases: Iterable[str] = (), description: str | None = None, is_active: bool = True, ) -> GlossaryEntry: canonical, normalized_aliases = self._validate_values(canonical_term, category, aliases) try: with self._connect() as connection: exists = connection.execute( "SELECT 1 FROM glossary_entries WHERE id = ?", (entry_id,) ).fetchone() if exists is None: raise KeyError(f"Unknown glossary entry: {entry_id}") self._ensure_terms_available( connection, canonical, normalized_aliases, excluding_entry_id=entry_id ) now = _timestamp() connection.execute( """UPDATE glossary_entries SET canonical_term = ?, category = ?, description = ?, is_active = ?, updated_at = ? WHERE id = ?""", ( canonical, category, _optional_text(description), is_active, now, entry_id, ), ) connection.execute("DELETE FROM glossary_aliases WHERE entry_id = ?", (entry_id,)) connection.executemany( "INSERT INTO glossary_aliases (entry_id, alias, created_at) VALUES (?, ?, ?)", ((entry_id, alias, now) for alias in normalized_aliases), ) except sqlite3.IntegrityError as exc: raise GlossaryConflictError("Canonical term or alias already exists.") from exc return self.get(entry_id) def set_active(self, entry_id: int, is_active: bool) -> GlossaryEntry: entry = self.get(entry_id) return self.update( entry.id, entry.canonical_term, entry.category, aliases=entry.aliases, description=entry.description, is_active=is_active, ) def delete(self, entry_id: int) -> None: with self._connect() as connection: cursor = connection.execute("DELETE FROM glossary_entries WHERE id = ?", (entry_id,)) if cursor.rowcount == 0: raise KeyError(f"Unknown glossary entry: {entry_id}") @contextmanager def _connect(self) -> Iterator[sqlite3.Connection]: connection = sqlite3.connect(self.database_path, timeout=5) try: connection.row_factory = sqlite3.Row connection.execute("PRAGMA foreign_keys = ON") connection.execute("PRAGMA journal_mode = WAL") connection.execute("PRAGMA busy_timeout = 5000") with connection: yield connection finally: connection.close() @staticmethod def _validate_values( canonical_term: str, category: str, aliases: Iterable[str] ) -> tuple[str, tuple[str, ...]]: canonical = canonical_term.strip() if not canonical: raise ValueError("Canonical term is required.") if category not in GLOSSARY_CATEGORIES: raise ValueError(f"Unsupported glossary category: {category}") normalized_aliases = tuple( dict.fromkeys(alias.strip() for alias in aliases if alias.strip()) ) folded = [alias.casefold() for alias in normalized_aliases] if len(folded) != len(set(folded)) or canonical.casefold() in folded: raise GlossaryConflictError( "Aliases must be unique and differ from the canonical term." ) return canonical, normalized_aliases @staticmethod def _ensure_terms_available( connection: sqlite3.Connection, canonical: str, aliases: tuple[str, ...], *, excluding_entry_id: int | None = None, ) -> None: terms = (canonical, *aliases) placeholders = ", ".join("?" for _ in terms) exclusion = "AND entry_id != ?" if excluding_entry_id is not None else "" alias_parameters: list[object] = [*terms] if excluding_entry_id is not None: alias_parameters.append(excluding_entry_id) alias_conflict = connection.execute( f"SELECT 1 FROM glossary_aliases WHERE alias IN ({placeholders}) {exclusion} LIMIT 1", # noqa: S608 alias_parameters, ).fetchone() entry_exclusion = "AND id != ?" if excluding_entry_id is not None else "" entry_parameters: list[object] = [*terms] if excluding_entry_id is not None: entry_parameters.append(excluding_entry_id) canonical_conflict = connection.execute( f"SELECT 1 FROM glossary_entries WHERE canonical_term IN ({placeholders}) " # noqa: S608 f"{entry_exclusion} LIMIT 1", entry_parameters, ).fetchone() if alias_conflict or canonical_conflict: raise GlossaryConflictError("Canonical term or alias already exists.") @staticmethod def _to_entry(connection: sqlite3.Connection, row: sqlite3.Row) -> GlossaryEntry: aliases = connection.execute( "SELECT alias FROM glossary_aliases WHERE entry_id = ? ORDER BY alias COLLATE NOCASE", (row["id"],), ).fetchall() return GlossaryEntry( id=row["id"], canonical_term=row["canonical_term"], category=row["category"], description=row["description"], is_active=bool(row["is_active"]), aliases=tuple(alias["alias"] for alias in aliases), created_at=row["created_at"], updated_at=row["updated_at"], ) def render_glossary_terms(entries: Iterable[GlossaryEntry]) -> list[str]: """Render concise canonical terms with recognition aliases for Meeting Context.""" rendered = [] for entry in entries: item = entry.canonical_term if entry.aliases: item += f" (aliases: {', '.join(entry.aliases)})" rendered.append(item) return rendered def _timestamp() -> str: return datetime.now(UTC).isoformat(timespec="seconds") def _optional_text(value: str | None) -> str | None: stripped = value.strip() if value else "" return stripped or None