Files
kleiderboerse/app/database.py
T
StefanandClaude Opus 5 3d7b7c706a Phase 1: Datenmodell, CRUD-API und Stammdaten
FastAPI mit SQLAlchemy und SQLite, Migrationen über Alembic. Die
Endpunkte aus projekt.md für items, categories und sizes stehen samt
Reservierung; Bilder und Anmeldung folgen in Phase 2 und 3.

Umgesetzt wie in Plan.md festgelegt:

- Feste Wertelisten als Enum UND als CHECK in der Datenbank. Die
  CHECK-Bedingung ist der eigentliche Schutz: an SQLAlchemy vorbei (Import,
  sqlite3 von Hand) käme sonst "Gril" durch, und die Filter griffen still
  nicht mehr.
- Status "draft" für den Stapel-Import. Entwürfe erscheinen weder in der
  Galerie noch in GET /items, solange nicht ausdrücklich status=draft
  angefragt wird, und lassen sich nicht reservieren.
- Titel ist freiwillig und wird sonst beim ANZEIGEN aus Kategorie und
  Grösse gebildet ("Jacken 98/104") - nicht beim Speichern, damit er einer
  späteren Korrektur der Grösse folgt.
- reservation_token: nur wer es hat, kann die eigene Reservierung aufheben.
  Verglichen mit compare_digest, gelöscht beim Freigeben und beim
  Erledigen, damit ein alter Link nicht später eine fremde Reservierung
  aufhebt.
- Eigene sizes-Tabelle mit sort_order statt SELECT DISTINCT: sonst stünde
  "104" vor "56" und jeder Tippfehler würde zur Filteroption.
- Pagination auf GET /items, in projekt.md nicht vorgesehen.

Zwei SQLite-Eigenheiten, die leicht untergehen: foreign_keys ist
standardmässig AUS (ohne PRAGMA greift ON DELETE CASCADE nicht), und
check_same_thread muss für FastAPI abgeschaltet werden. Beides in
database.py, dazu WAL fürs gleichzeitige Lesen.

24 Tests, alle grün. Zusätzlich von Hand geprüft: Migration anwenden,
Eintrag anlegen, reservieren, ohne Token freigeben (403), mit Token
freigeben (200), Swagger UI erreichbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GR4bNaj9GtRu57J4Niii8o
2026-08-29 22:00:58 +02:00

62 lines
1.8 KiB
Python

"""Datenbank-Anbindung.
SQLite braucht zwei Einstellungen, die man leicht übersieht:
* ``check_same_thread=False`` - FastAPI bearbeitet Anfragen in mehreren
Threads, SQLite verbietet das sonst für eine Verbindung.
* ``PRAGMA foreign_keys=ON`` - Fremdschlüssel sind in SQLite standardmässig
**aus**. Ohne diese Zeile greift ``ON DELETE CASCADE`` nicht, und beim
Löschen eines Eintrags blieben dessen Bilder als Waisen zurück.
"""
from collections.abc import Iterator
from sqlalchemy import create_engine, event
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
from app.config import einstellungen
class Basis(DeclarativeBase):
pass
def engine_erzeugen(url: str | None = None):
cfg = einstellungen()
if url is None:
cfg.datenbank_pfad.parent.mkdir(parents=True, exist_ok=True)
url = cfg.datenbank_url
engine = create_engine(
url,
connect_args={"check_same_thread": False},
# Wartet, statt sofort abzubrechen, wenn parallel geschrieben wird.
pool_pre_ping=True,
)
@event.listens_for(engine, "connect")
def _sqlite_pragmas(dbapi_connection, _connection_record):
cur = dbapi_connection.cursor()
cur.execute("PRAGMA foreign_keys=ON")
# WAL: Lesen wird nicht mehr durch einen laufenden Schreibvorgang
# blockiert - bei einer Galerie, die viele gleichzeitig ansehen,
# der spürbarere Unterschied.
cur.execute("PRAGMA journal_mode=WAL")
cur.execute("PRAGMA busy_timeout=5000")
cur.close()
return engine
engine = engine_erzeugen()
SessionLocal = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False)
def get_db() -> Iterator[Session]:
"""FastAPI-Abhängigkeit: eine Sitzung pro Anfrage, danach geschlossen."""
db = SessionLocal()
try:
yield db
finally:
db.close()