SwedenTaxRAGService / src /db /sqlite_document_repo.py
gokceai's picture
fix chunk pipelines
99da31c
Raw
History Blame Contribute Delete
7.1 kB
"""
SQLiteDocumentRepository for encrypted chunk storage.
Design decisions:
- Fernet encryption/decryption happens in this repository layer.
- Thread safety: a per-instance Lock guards every sqlite3 call. Connections are opened
per-operation (safest pattern for multi-threaded use without a connection pool).
- Schema uses INSERT OR REPLACE (UPSERT) so ingest remains idempotent.
"""
import json
import logging
import sqlite3
import threading
from src.core.exceptions import InfrastructureError
logger = logging.getLogger(__name__)
_SCHEMA = """
CREATE TABLE IF NOT EXISTS document_chunks (
chunk_id TEXT PRIMARY KEY,
encrypted_text TEXT NOT NULL,
source TEXT DEFAULT 'unknown',
chunk_index INTEGER DEFAULT 0,
extra_metadata TEXT DEFAULT '{}'
)
"""
class SQLiteDocumentRepository:
def __init__(self, db_path: str, encryption_manager):
self.db_path = db_path
self.encryption_manager = encryption_manager
self._lock = threading.Lock()
self._init_db()
# ------------------------------------------------------------------
# Internal helpers
# ------------------------------------------------------------------
def _init_db(self) -> None:
try:
with self._lock, sqlite3.connect(self.db_path) as conn:
conn.execute(_SCHEMA)
conn.commit()
logger.info("SQLite document store ready at '%s'.", self.db_path)
except Exception as e:
raise InfrastructureError(f"SQLite initialization failed: {e}") from e
def _conn(self) -> sqlite3.Connection:
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
return conn
# ------------------------------------------------------------------
# Public interface (mirrors DocumentRepository)
# ------------------------------------------------------------------
def save_document_chunk(self, chunk_id: str, original_text: str, metadata: dict) -> bool:
"""Encrypt and upsert chunk payload into SQLite."""
try:
encrypted_text = self.encryption_manager.encrypt_data(original_text)
source = metadata.get("source", "unknown")
chunk_index = metadata.get("chunk_index", 0)
extra = {
k: v
for k, v in metadata.items()
if k not in {
"chunk_id", "source", "chunk_index",
"encrypted_text", "decrypted_text", "text",
}
and v is not None
}
with self._lock, self._conn() as conn:
conn.execute(
"""
INSERT INTO document_chunks
(chunk_id, encrypted_text, source, chunk_index, extra_metadata)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(chunk_id) DO UPDATE SET
encrypted_text = excluded.encrypted_text,
source = excluded.source,
chunk_index = excluded.chunk_index,
extra_metadata = excluded.extra_metadata
""",
(chunk_id, encrypted_text, source, chunk_index, json.dumps(extra)),
)
conn.commit()
logger.info("Chunk '%s' encrypted and saved.", chunk_id)
return True
except Exception as e:
logger.error("Chunk save failed (%s): %s", chunk_id, e)
return False
def get_document_chunks(self, chunk_ids: list[str]) -> list[dict]:
if not chunk_ids:
return []
placeholders = ",".join("?" for _ in chunk_ids)
try:
with self._lock, self._conn() as conn:
rows = conn.execute(
f"SELECT * FROM document_chunks WHERE chunk_id IN ({placeholders})",
chunk_ids,
).fetchall()
by_id = {}
for row in rows:
result = dict(row)
result["decrypted_text"] = self.encryption_manager.decrypt_data(
result.pop("encrypted_text")
)
extra = json.loads(result.pop("extra_metadata", "{}"))
result.update(extra)
by_id[result["chunk_id"]] = result
return [by_id[cid] for cid in chunk_ids if cid in by_id]
except Exception as e:
logger.error("Batch chunk read failed: %s", e)
raise InfrastructureError(f"SQLite batch read failed: {e}") from e
def has_document_chunk(self, chunk_id: str) -> bool:
try:
with self._lock, self._conn() as conn:
row = conn.execute(
"SELECT 1 FROM document_chunks WHERE chunk_id = ? LIMIT 1", (chunk_id,)
).fetchone()
return row is not None
except Exception as e:
logger.error("Chunk existence check failed (%s): %s", chunk_id, e)
return False
def delete_document_chunk(self, chunk_id: str) -> None:
try:
with self._lock, self._conn() as conn:
conn.execute(
"DELETE FROM document_chunks WHERE chunk_id = ?", (chunk_id,)
)
conn.commit()
except Exception as e:
raise InfrastructureError(f"SQLite delete failed for {chunk_id}: {e}") from e
def delete_chunk_ids(self, chunk_ids: list[str]) -> None:
if not chunk_ids:
return
placeholders = ",".join("?" for _ in chunk_ids)
try:
with self._lock, self._conn() as conn:
conn.execute(
f"DELETE FROM document_chunks WHERE chunk_id IN ({placeholders})",
chunk_ids,
)
conn.commit()
except Exception as e:
raise InfrastructureError(f"SQLite bulk delete failed: {e}") from e
def delete_all_chunks(self) -> None:
try:
with self._lock, self._conn() as conn:
conn.execute("DELETE FROM document_chunks")
conn.commit()
except Exception as e:
raise InfrastructureError(f"SQLite full reset failed: {e}") from e
def list_chunk_ids(self) -> set[str]:
try:
with self._lock, self._conn() as conn:
rows = conn.execute(
"SELECT chunk_id FROM document_chunks"
).fetchall()
return {row[0] for row in rows}
except Exception as e:
raise InfrastructureError(f"SQLite list IDs failed: {e}") from e
def ping(self) -> bool:
"""Lightweight liveness check — verifies the DB file is accessible and schema exists."""
try:
with self._lock, self._conn() as conn:
conn.execute("SELECT 1 FROM document_chunks LIMIT 1")
return True
except Exception as e:
logger.error("SQLite ping failed: %s", e)
return False