Files
nimbusflow/backend/db/base_repository.py

109 lines
4.3 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# backend/db/base_repository.py
from __future__ import annotations
from typing import TypeVar, Generic, List, Sequence, Any, Mapping, Tuple
from backend.db.connection import DatabaseConnection
# Generic type for the model (your dataclasses such as Member, Service, …)
T = TypeVar("T")
class BaseRepository(Generic[T]):
"""
Very small generic repository that knows how to:
* INSERT a dataclasslike object (any object that implements ``to_dict`` and
``from_row``)
* SELECT all rows from a table and turn them into model instances
* (optionally) UPDATE or DELETE rows stubs are provided for future use
"""
def __init__(self, db: DatabaseConnection):
self.db = db
# ------------------------------------------------------------------
# INSERT
# ------------------------------------------------------------------
def _insert(self, table: str, obj: T, pk_name: str) -> T:
"""
Insert ``obj`` into ``table`` and populate the autoincrement primarykey
field named ``pk_name`` on the original object.
The model **must** implement:
* ``to_dict() -> Mapping[str, Any]`` returns a mapping of column →
value (including the PK, which we drop here)
* ``from_row(row: sqlite3.Row) -> Model`` classmethod used by
``_select_all``.
"""
# 1⃣ Turn the model into a plain dict and drop the PK column.
data: Mapping[str, Any] = obj.to_dict() # type: ignore[attr-defined]
if pk_name not in data:
raise ValueError(f"Primarykey column '{pk_name}' not found in model data.")
# Remove the autoincrement column SQLite will fill it in.
data_without_pk = {k: v for k, v in data.items() if k != pk_name}
# 2⃣ Build the column list and matching placeholders.
columns = ", ".join(data_without_pk.keys())
placeholders = ", ".join("?" for _ in data_without_pk)
sql = f"INSERT INTO {table} ({columns}) VALUES ({placeholders})"
# 3⃣ Execute the statement with a *tuple* of values that matches the
# number of placeholders.
cursor = self.db.execute(sql, tuple(data_without_pk.values()))
# 4⃣ SQLite gives us the newly generated rowid on the cursor.
setattr(obj, pk_name, cursor.lastrowid) # type: ignore[attr-defined]
return obj
# ------------------------------------------------------------------
# SELECT ALL
# ------------------------------------------------------------------
def _select_all(self, table: str, model_cls: type[T]) -> List[T]:
"""
Return every row from ``table`` as a list of ``model_cls`` instances.
``model_cls`` must provide a ``from_row`` classmethod that accepts a
``sqlite3.Row`` and returns an instantiated model.
"""
rows = self.db.fetchall(f"SELECT * FROM {table}")
return [model_cls.from_row(r) for r in rows] # type: ignore[attr-defined]
# ------------------------------------------------------------------
# OPTIONAL UPDATE helper (you can call it from concrete repos)
# ------------------------------------------------------------------
def _update(
self,
table: str,
pk_name: str,
pk_value: Any,
updates: Mapping[str, Any],
) -> None:
"""
Simple UPDATE helper.
Example:
repo._update(
table="Members",
pk_name="MemberId",
pk_value=42,
updates={"IsActive": 0, "Notes": "temporarily disabled"},
)
"""
if not updates:
return # nothing to do
set_clause = ", ".join(f"{col}=?" for col in updates)
sql = f"UPDATE {table} SET {set_clause} WHERE {pk_name} = ?"
params: Tuple[Any, ...] = tuple(updates.values()) + (pk_value,)
self.db.execute(sql, params)
# ------------------------------------------------------------------
# OPTIONAL DELETE helper
# ------------------------------------------------------------------
def _delete(self, table: str, pk_name: str, pk_value: Any) -> None:
"""
Delete a row by primary key.
"""
sql = f"DELETE FROM {table} WHERE {pk_name} = ?"
self.db.execute(sql, (pk_value,))