Files
patrickandClaude Sonnet 5 035b22fd1e
CI / backend-tests (push) Failing after 1s
Migration-Bugfix: DDL statementweise statt als Multi-Statement-Block ausführen
asyncpg (SQLAlchemy async) lässt keine Mehrfach-Statements in einem prepared
statement zu - "cannot insert multiple commands into a prepared statement".
Aufgetreten beim ersten echten alembic upgrade head auf dem Zielserver.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L85hmKbvX7Cqkq47KnQhFt
2026-09-03 23:06:44 +02:00

304 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
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.
"""Initiales Schema (Prompt 20 / ergebnisse/20_datenbank_schema.md)
Revision ID: 0001_initial_schema
Revises:
Create Date: 2026-09-03
Vollständiges Ziel-Schema aus Prompt 20 (inkl. Karte-13-Vorbereitung: UUID auf
sync-relevanten Tabellen, systemknoten). Sprint 0 legt das komplette Schema an,
auch wenn erst spätere Sprints die zugehörigen Endpunkte/Business-Logik bauen
so entfällt ein späterer Schema-Umbau (Designziel, Prompt 06/19).
"""
from typing import Sequence, Union
from alembic import op
revision: str = "0001_initial_schema"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
UPGRADE_SQL = """
-- 1. Stammdaten
CREATE TABLE bereich (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
beschreibung TEXT
);
CREATE TABLE kategorie (
id SERIAL PRIMARY KEY,
bereich_id INTEGER NOT NULL REFERENCES bereich(id),
name TEXT NOT NULL,
ueberkategorie_id INTEGER REFERENCES kategorie(id),
UNIQUE (bereich_id, name)
);
CREATE TABLE standort (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
adresse TEXT
);
CREATE TABLE objekttyp (
id SERIAL PRIMARY KEY,
bereich_id INTEGER NOT NULL REFERENCES bereich(id),
kategorie_id INTEGER REFERENCES kategorie(id),
name TEXT NOT NULL,
UNIQUE (bereich_id, name)
);
CREATE TYPE materialtyp AS ENUM ('standard', 'ablauf_charge', 'geraet_sn');
CREATE TABLE material (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
artikelnummer TEXT,
einheit TEXT NOT NULL,
materialtyp materialtyp NOT NULL,
kategorie_id INTEGER REFERENCES kategorie(id),
hersteller TEXT,
beschreibung TEXT,
code TEXT UNIQUE,
warnzeitraum_tage INTEGER,
aktiv BOOLEAN NOT NULL DEFAULT TRUE
);
-- 2. Vorlagen
CREATE TYPE vorlage_status AS ENUM ('aktiv', 'veraltet');
CREATE TABLE beladungsvorlage (
id SERIAL PRIMARY KEY,
objekttyp_id INTEGER NOT NULL REFERENCES objekttyp(id),
name TEXT NOT NULL,
version INTEGER NOT NULL,
gueltig_ab TIMESTAMPTZ NOT NULL DEFAULT now(),
status vorlage_status NOT NULL DEFAULT 'aktiv',
UNIQUE (objekttyp_id, name, version)
);
CREATE TABLE vorlagenposition (
id SERIAL PRIMARY KEY,
vorlage_id INTEGER NOT NULL REFERENCES beladungsvorlage(id),
material_id INTEGER NOT NULL REFERENCES material(id),
fach TEXT,
sollmenge NUMERIC NOT NULL,
UNIQUE (vorlage_id, material_id)
);
-- 3. Objekte
CREATE TYPE knoten_typ AS ENUM ('haupt', 'satellit');
CREATE TABLE systemknoten (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
typ knoten_typ NOT NULL DEFAULT 'satellit'
);
CREATE TYPE objekt_status AS ENUM ('aktiv', 'ausser_dienst');
CREATE TABLE objekt (
id SERIAL PRIMARY KEY,
code TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
objekttyp_id INTEGER NOT NULL REFERENCES objekttyp(id),
vorlage_id INTEGER REFERENCES beladungsvorlage(id),
standort_id INTEGER NOT NULL REFERENCES standort(id),
status objekt_status NOT NULL DEFAULT 'aktiv',
zustaendiger_server_id INTEGER NOT NULL REFERENCES systemknoten(id)
);
CREATE TYPE objektposition_status AS ENUM ('aktiv', 'entfernt');
CREATE TABLE objektposition (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
objekt_id INTEGER NOT NULL REFERENCES objekt(id),
material_id INTEGER NOT NULL REFERENCES material(id),
sollmenge_override NUMERIC,
ist_status objektposition_status NOT NULL DEFAULT 'aktiv',
istmenge NUMERIC NOT NULL DEFAULT 0,
seriennummer TEXT,
ablaufdatum DATE,
chargennummer TEXT,
zuletzt_geaendert_am TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (objekt_id, material_id)
);
-- 4. Zuständigkeiten, Benutzer, Rollen
CREATE TABLE benutzer (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
login TEXT NOT NULL UNIQUE,
passwort_hash TEXT NOT NULL,
aktiv BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE TYPE rolle_typ AS ENUM ('mitarbeiter', 'materialverantwortlicher', 'leitungsverantwortlicher', 'administration');
CREATE TABLE benutzer_rolle (
benutzer_id INTEGER NOT NULL REFERENCES benutzer(id),
rolle rolle_typ NOT NULL,
PRIMARY KEY (benutzer_id, rolle)
);
CREATE TABLE zustaendigkeit (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
benutzer_id INTEGER NOT NULL REFERENCES benutzer(id),
standort_id INTEGER REFERENCES standort(id),
objekt_id INTEGER REFERENCES objekt(id),
CHECK (standort_id IS NOT NULL OR objekt_id IS NOT NULL)
);
CREATE TABLE kontrollverantwortung (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
objekt_id INTEGER NOT NULL REFERENCES objekt(id),
benutzer_id INTEGER REFERENCES benutzer(id),
gruppe TEXT
);
-- 5. Kontrolle
CREATE TYPE kontroll_status AS ENUM ('nicht_gestartet', 'in_bearbeitung', 'abgeschlossen', 'abgebrochen');
CREATE TABLE kontrolle (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
erzeugt_von_server_id INTEGER NOT NULL REFERENCES systemknoten(id),
objekt_id INTEGER NOT NULL REFERENCES objekt(id),
benutzer_id INTEGER NOT NULL REFERENCES benutzer(id),
status kontroll_status NOT NULL DEFAULT 'in_bearbeitung',
gestartet_am TIMESTAMPTZ NOT NULL DEFAULT now(),
beendet_am TIMESTAMPTZ,
abbruch_grund TEXT,
signatur BYTEA
);
CREATE TABLE kontrollposition (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
kontrolle_id UUID NOT NULL REFERENCES kontrolle(id),
material_id INTEGER NOT NULL REFERENCES material(id),
sollmenge_snapshot NUMERIC NOT NULL,
istmenge_erfasst NUMERIC NOT NULL,
abweichung BOOLEAN NOT NULL
);
-- 6. Fehlbestand, Nachfüllung, Mindermenge
CREATE TYPE fehlbestand_status AS ENUM ('offen', 'in_bearbeitung', 'nachgefuellt_teilweise', 'erledigt');
CREATE TABLE fehlbestand (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
erzeugt_von_server_id INTEGER NOT NULL REFERENCES systemknoten(id),
objekt_id INTEGER NOT NULL REFERENCES objekt(id),
material_id INTEGER NOT NULL REFERENCES material(id),
standort_id INTEGER NOT NULL REFERENCES standort(id),
sollmenge NUMERIC NOT NULL,
istmenge NUMERIC NOT NULL,
fehlmenge NUMERIC NOT NULL,
entstanden_am TIMESTAMPTZ NOT NULL DEFAULT now(),
festgestellt_von INTEGER NOT NULL REFERENCES benutzer(id),
kontrolle_id UUID REFERENCES kontrolle(id),
ursache TEXT,
verantwortlicher_id INTEGER REFERENCES benutzer(id),
status fehlbestand_status NOT NULL DEFAULT 'offen',
erledigt_am TIMESTAMPTZ
);
CREATE TABLE nachfuellung (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
fehlbestand_id UUID REFERENCES fehlbestand(id),
objekt_id INTEGER NOT NULL REFERENCES objekt(id),
material_id INTEGER NOT NULL REFERENCES material(id),
menge NUMERIC NOT NULL,
benutzer_id INTEGER NOT NULL REFERENCES benutzer(id),
zeitpunkt TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TYPE mindermenge_status AS ENUM ('aktiv', 'abgelaufen', 'beendet_durch_erledigung');
CREATE TABLE mindermengen_genehmigung (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
fehlbestand_id UUID NOT NULL REFERENCES fehlbestand(id),
genehmigt_von INTEGER NOT NULL REFERENCES benutzer(id),
begruendung TEXT NOT NULL,
genehmigt_am TIMESTAMPTZ NOT NULL DEFAULT now(),
ausloesende_kontrolle_id UUID NOT NULL REFERENCES kontrolle(id),
status mindermenge_status NOT NULL DEFAULT 'aktiv',
beendet_am TIMESTAMPTZ,
beendende_kontrolle_id UUID REFERENCES kontrolle(id)
);
-- 7. Historie/Audit
CREATE TABLE historie (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
erzeugt_von_server_id INTEGER NOT NULL REFERENCES systemknoten(id),
zeitpunkt TIMESTAMPTZ NOT NULL DEFAULT now(),
benutzer_id INTEGER REFERENCES benutzer(id),
ereignistyp TEXT NOT NULL,
entitaet_typ TEXT NOT NULL,
entitaet_id TEXT NOT NULL,
alter_wert JSONB,
neuer_wert JSONB,
begruendung TEXT
);
-- 8. Indizes
CREATE INDEX idx_fehlbestand_status ON fehlbestand(status);
CREATE INDEX idx_fehlbestand_objekt ON fehlbestand(objekt_id);
CREATE INDEX idx_fehlbestand_entstanden ON fehlbestand(entstanden_am);
CREATE INDEX idx_objektposition_objekt ON objektposition(objekt_id);
CREATE INDEX idx_historie_entitaet ON historie(entitaet_typ, entitaet_id);
CREATE INDEX idx_zustaendigkeit_benutzer ON zustaendigkeit(benutzer_id);
"""
DOWNGRADE_SQL = """
DROP TABLE IF EXISTS historie;
DROP TABLE IF EXISTS mindermengen_genehmigung;
DROP TABLE IF EXISTS nachfuellung;
DROP TABLE IF EXISTS fehlbestand;
DROP TABLE IF EXISTS kontrollposition;
DROP TABLE IF EXISTS kontrolle;
DROP TABLE IF EXISTS kontrollverantwortung;
DROP TABLE IF EXISTS zustaendigkeit;
DROP TABLE IF EXISTS benutzer_rolle;
DROP TABLE IF EXISTS benutzer;
DROP TABLE IF EXISTS objektposition;
DROP TABLE IF EXISTS objekt;
DROP TABLE IF EXISTS systemknoten;
DROP TABLE IF EXISTS vorlagenposition;
DROP TABLE IF EXISTS beladungsvorlage;
DROP TABLE IF EXISTS material;
DROP TABLE IF EXISTS objekttyp;
DROP TABLE IF EXISTS standort;
DROP TABLE IF EXISTS kategorie;
DROP TABLE IF EXISTS bereich;
DROP TYPE IF EXISTS mindermenge_status;
DROP TYPE IF EXISTS fehlbestand_status;
DROP TYPE IF EXISTS kontroll_status;
DROP TYPE IF EXISTS rolle_typ;
DROP TYPE IF EXISTS objektposition_status;
DROP TYPE IF EXISTS objekt_status;
DROP TYPE IF EXISTS knoten_typ;
DROP TYPE IF EXISTS vorlage_status;
DROP TYPE IF EXISTS materialtyp;
"""
def _execute_statements(sql: str) -> None:
# asyncpg lässt keine Mehrfach-Statements in einem prepared statement zu
# (Fund beim ersten realen `alembic upgrade head` gegen asyncpg) jedes
# ";"-getrennte Statement einzeln ausführen. Kein Statement in diesem
# Schema enthält ein Semikolon innerhalb eines String-/Kommentarwerts,
# daher ist ein simpler Split sicher.
for statement in sql.split(";"):
statement = statement.strip()
if statement:
op.execute(statement)
def upgrade() -> None:
_execute_statements(UPGRADE_SQL)
def downgrade() -> None:
_execute_statements(DOWNGRADE_SQL)