furtka-gateway/control_plane/db.py

56 lines
1.9 KiB
Python
Raw Normal View History

"""Thin sqlite3 wrapper: one connection per process, guarded by a lock.
Why sqlite3 instead of furtka-core's flat-JSON-plus-flock convention: that
convention works because each file is effectively single-writer. This
gateway's defining requirement is many unrelated accounts/boxes registering
and publishing routes concurrently a locked JSON file would serialize
every tenant's writes against every other tenant's. sqlite3 (stdlib, zero
extra dependencies) gives real concurrent relational access instead.
"""
from __future__ import annotations
import sqlite3
import threading
from pathlib import Path
from control_plane import paths
_SCHEMA_PATH = Path(__file__).parent / "schema.sql"
class Database:
def __init__(self, db_path: Path | None = None) -> None:
path = db_path or paths.db_path()
path.parent.mkdir(parents=True, exist_ok=True)
self._lock = threading.Lock()
self._conn = sqlite3.connect(path, check_same_thread=False)
self._conn.row_factory = sqlite3.Row
self._conn.execute("PRAGMA foreign_keys = ON")
with self._lock:
self._conn.executescript(_SCHEMA_PATH.read_text())
self._conn.commit()
def execute(self, sql: str, params: tuple = ()) -> None:
with self._lock:
self._conn.execute(sql, params)
self._conn.commit()
def query_one(self, sql: str, params: tuple = ()) -> sqlite3.Row | None:
with self._lock:
return self._conn.execute(sql, params).fetchone()
def query_all(self, sql: str, params: tuple = ()) -> list[sqlite3.Row]:
with self._lock:
return self._conn.execute(sql, params).fetchall()
def close(self) -> None:
with self._lock:
self._conn.close()
def row_to_dict(row: sqlite3.Row | None) -> dict | None:
if row is None:
return None
return dict(zip(row.keys(), row, strict=True))