2# +==== BEGIN polyguard =================+
20# FILE: sqlite_handler.py
21# CREATION DATE: 21-03-2026
22# LAST Modified: 19:51:7 21-03-2026
24# A module that provides a set of swearwords to listen to when filtering while allowing to toggle on and off different languages.
26# COPYRIGHT: (c) Henry Letellier
27# PURPOSE: SQLite handler for PolyGuard word-lists.
28# This module provides a small, explicit handler to manage a read-only or read-write SQLite database containing (lang, word) rows. It keeps a connection open, exposes simple helpers for lookups and bulk inserts, and avoids inline comprehensions to favour legibility.
30# +==== END polyguard =================+
35from threading
import Lock
36from typing
import Optional, Iterable, Set, Dict
38from display_tty
import Disp, initialise_logger
40from .
import constants
as POLY_CONST
44 """Manage a connection to an SQLite database storing language word-lists.
46 The expected schema is a table `words(lang TEXT, word TEXT, PRIMARY KEY(lang, word))`.
48 _instance: Optional[
"SQLiteHandler"] =
None
49 _class_lock: Lock = Lock()
50 disp: Disp = initialise_logger(__qualname__,
False)
52 def __new__(cls, *args, **kwargs) -> "SQLiteHandler":
58 def __init__(self, db_path: str, readonly: bool =
True, *, log: bool =
True, debug: bool =
False) ->
None:
59 """Initialize SQLite connection handler.
61 Sets up connection parameters but does not open the connection until
62 connect() is called. Supports both read-only and read-write modes.
65 db_path: Path to SQLite database file.
66 readonly: If True, opens database in read-only mode. Defaults to True.
67 log: If True, enables logging of database operations. Defaults to True.
68 debug: If True, enables debug-level logging. Defaults to False.
77 self.
disp.update_disp_debug(debug)
80 """Open SQLite database connection.
82 If readonly mode is enabled, uses SQLite URI syntax to open in read-only
83 mode. Enables check_same_thread=False to allow multithreaded access
84 (concurrency is controlled by instance lock). If already connected,
85 this method is a no-op.
88 sqlite3.DatabaseError: When connection fails (e.g., file not found in readonly mode).
96 f
"Opening SQLite connection (readonly={self.readonly}) to {self.db_path}"
100 uri = f
"file:{os.path.abspath(self.db_path)}?mode=ro"
107 check_same_thread=
False
112 check_same_thread=
False
118 self.
disp.log_info(
"SQLite connection opened")
121 """Close SQLite database connection.
123 If not connected, this method is a no-op. Safe to call multiple times.
131 self.
disp.log_debug(
"Closing SQLite connection")
136 self.
disp.log_info(
"SQLite connection closed")
139 """Context manager entry: open database connection.
142 SQLiteHandler: This instance with connection open.
148 """Context manager exit: close database connection.
150 Closes connection regardless of whether an exception occurred.
155 """Create words table if not already present.
157 Creates a table with columns (lang TEXT, word TEXT) and a composite
158 primary key on (lang, word). Idempotent — safe to call on existing tables.
159 Requires a writable database connection.
162 RuntimeError: When connection is not open.
163 sqlite3.Error: When table creation fails.
167 raise RuntimeError(
"Connection is not open")
170 self.
disp.log_debug(
"Creating schema if missing")
175 CREATE TABLE IF NOT EXISTS words (
178 PRIMARY KEY(lang, word)
185 self.
disp.log_info(
"Schema ensured")
187 def bulk_insert(self, mapping: Dict[POLY_CONST.Langs, Iterable[str]]) -> int:
188 """Bulk insert words from language to words mapping.
190 Normalizes and inserts words from each language. Uses INSERT OR IGNORE
191 to skip duplicate (lang, word) pairs. Returns cumulative row count
192 across all languages provided (sum of per-language totals, not insert count).
195 mapping: Dictionary mapping Langs enums to iterables of words.
198 int: Sum of word counts across all languages in mapping after insert.
201 RuntimeError: When connection is not open.
202 sqlite3.Error: When database operation fails (transaction is rolled back).
206 raise RuntimeError(
"Connection is not open")
214 self.
disp.log_debug(
"Beginning bulk insert")
217 for lang_key, words
in mapping.items():
219 if not isinstance(lang_key, POLY_CONST.Langs):
222 lang_text = lang_key.value
228 word = w.strip().lower()
235 "INSERT OR IGNORE INTO words (lang, word) VALUES (?, ?)",
244 for lang_key
in mapping.keys():
245 if not isinstance(lang_key, POLY_CONST.Langs):
249 "SELECT COUNT(1) FROM words WHERE lang = ?",
262 f
"Bulk insert complete; total rows (per-lang sum)={inserted}"
265 except sqlite3.Error
as exc:
267 self.
disp.log_error(f
"Bulk insert failed: {exc}")
273 def get_words(self, lang: POLY_CONST.Langs) -> Set[str]:
274 """Retrieve all words for a specific language from database.
276 Returns an empty set if the database is empty or the language has no entries.
279 lang: Langs enum member specifying which language to retrieve.
282 Set[str]: Set of all words for the language.
285 RuntimeError: When connection is not open.
289 raise RuntimeError(
"Connection is not open")
292 self.
disp.log_debug(f
"Fetching words for lang={lang.value}")
295 cur.execute(
"SELECT word FROM words WHERE lang = ?", (lang.value,))
297 rows = cur.fetchall()
299 result: Set[str] = set()
314 f
"Found {len(result)} words for lang={lang.value}"
319 """List all languages and their word counts in database.
321 Queries all distinct language codes and returns a mapping of code to word count.
322 Returns an empty dict if the database is empty or the words table is missing.
325 Dict[str, int]: Mapping of language code strings to word counts.
328 RuntimeError: When connection is not open.
332 raise RuntimeError(
"Connection is not open")
335 cur.execute(
"SELECT lang, COUNT(1) FROM words GROUP BY lang")
336 rows = cur.fetchall()
338 result: Dict[str, int] = {}
348 if row[1]
is not None:
350 except (ValueError, TypeError):
356 self.
disp.log_info(f
"Languages in DB: {len(result)} found")
360 def has_word(self, lang: POLY_CONST.Langs, word: str) -> bool:
361 """Check if a word exists in a specific language.
363 Normalizes the input word (lowercase, strip whitespace) before checking.
364 Returns False for empty or whitespace-only inputs.
367 lang: Langs enum member specifying which language to search.
368 word: Word string to check.
371 bool: True if word is present for the language, False otherwise.
374 RuntimeError: When connection is not open.
378 raise RuntimeError(
"Connection is not open")
382 text = word.strip().lower()
389 f
"Checking existence for word={text!r} in lang={lang.value}"
392 "SELECT 1 FROM words WHERE lang = ? AND word = ? LIMIT 1",
398 found = row
is not None
401 f
"Word matched: {text!r} (lang={lang.value})"
Optional[sqlite3.Connection] _conn
None __exit__(self, exc_type, exc, tb)
None __init__(self, str db_path, bool readonly=True, *, bool log=True, bool debug=False)
Optional[sqlite3.Connection] _lock
Optional[sqlite3.Connection] log
"SQLiteHandler" __new__(cls, *args, **kwargs)
bool has_word(self, POLY_CONST.Langs lang, str word)
Dict[str, int] list_languages(self)
int bulk_insert(self, Dict[POLY_CONST.Langs, Iterable[str]] mapping)
Set[str] get_words(self, POLY_CONST.Langs lang)
"SQLiteHandler" __enter__(self)