Files
OAMC-Auswertung/migrations/versions/d7d7d8a2cf90_initiales_schema.py
linusandClaude Sonnet 5 c33aaa1a4b Meisterschaft: eine pro Saison, mit zugeordneten Turnieren
Auf Vorgabe des Auftraggebers umgestellt:
* genau eine Meisterschaft je Saison (unique auf saison_jahr)
* turnier.meisterschaft_id (nullable FK) statt bool zaehlt_meisterschaft;
  NULL = "o. M.". Kompat-Property turnier.zaehlt_meisterschaft bleibt.
* Teilwertungen (Erwachsene-Gesamt, Jugend J1-J5) sind (Kategorie, Klasse)-
  Sichten auf die eine Meisterschaft, keine eigenen Zeilen mehr
* meisterschaft_stand: kategorie + klasse_id + Unique je Wertung/Fahrer
* berechne_stand(kategorie=, klasse_id=), wertungen_fuer(), berechne_alle_wertungen()
* Read-API: /meisterschaften/{id}/stand[?wertung=], .../endlauf-qualifikation?wertung=
* Ingest: POST /turniere haengt zaehlende Turniere an die Jahres-Meisterschaft
* Initialmigration neu erzeugt (Projekt noch nicht ausgerollt); Seed: eine
  "HTH-Meisterschaft <jahr>"
* Tests angepasst/ergaenzt (61 gruen)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 19:52:25 +02:00

300 lines
19 KiB
Python

"""Initiales Schema
Revision ID: d7d7d8a2cf90
Revises:
Create Date: 2026-09-05 19:48:05.733306+02:00
"""
from __future__ import annotations
from collections.abc import Sequence
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision: str = 'd7d7d8a2cf90'
down_revision: str | None = None
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.execute("CREATE EXTENSION IF NOT EXISTS pg_trgm")
op.create_table('benutzer',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('benutzername', sa.String(length=80), nullable=False),
sa.Column('anzeigename', sa.String(length=120), nullable=False),
sa.Column('passwort_hash', sa.String(length=255), nullable=False),
sa.Column('rolle', sa.Enum('admin', 'auswerter', 'lesend', name='benutzer_rolle'), nullable=False),
sa.Column('aktiv', sa.Boolean(), nullable=False),
sa.Column('letzter_login_am', sa.DateTime(timezone=True), nullable=True),
sa.Column('erstellt_am', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('geaendert_am', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_benutzer')),
sa.UniqueConstraint('benutzername', name=op.f('uq_benutzer_benutzername'))
)
op.create_table('geraet',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('bezeichnung', sa.String(length=120), nullable=False),
sa.Column('typ', sa.Enum('lichtschranke', 'anzeige', name='geraet_typ'), nullable=False),
sa.Column('token_hash', sa.String(length=255), nullable=False),
sa.Column('aktiv', sa.Boolean(), nullable=False),
sa.Column('letzte_meldung_am', sa.DateTime(timezone=True), nullable=True),
sa.Column('firmware_version', sa.String(length=60), nullable=True),
sa.Column('queue_laenge_gemeldet', sa.Integer(), nullable=True),
sa.Column('notiz', sa.Text(), nullable=True),
sa.Column('konfiguration', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('erstellt_am', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('geaendert_am', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_geraet')),
sa.UniqueConstraint('bezeichnung', name=op.f('uq_geraet_bezeichnung'))
)
op.create_table('import_job',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('quelle', sa.String(length=120), nullable=False),
sa.Column('idempotenz_schluessel', sa.String(length=200), nullable=True),
sa.Column('dateiname', sa.String(length=300), nullable=True),
sa.Column('roh_payload', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('ergebnis_bericht', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('status', sa.Enum('angenommen', 'vorschau', 'fehler', 'teilweise', name='import_status'), nullable=False),
sa.Column('fehler', sa.Text(), nullable=True),
sa.Column('angelegt_von', sa.String(length=120), nullable=True),
sa.Column('angelegt_am', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_import_job'))
)
op.create_index(op.f('ix_import_job_idempotenz_schluessel'), 'import_job', ['idempotenz_schluessel'], unique=False)
op.create_table('saison',
sa.Column('jahr', sa.Integer(), autoincrement=False, nullable=False),
sa.Column('turnierordnung_version', sa.String(length=60), nullable=True),
sa.Column('streichresultate_tabelle', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('nenngeld_cent', sa.Integer(), nullable=False),
sa.Column('aktiv', sa.Boolean(), nullable=False),
sa.PrimaryKeyConstraint('jahr', name=op.f('pk_saison'))
)
op.create_table('verein',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=200), nullable=False),
sa.Column('kurzname', sa.String(length=60), nullable=True),
sa.Column('adac_gau', sa.String(length=120), nullable=True),
sa.Column('aktiv', sa.Boolean(), nullable=False),
sa.Column('notiz', sa.Text(), nullable=True),
sa.Column('erstellt_am', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('geaendert_am', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_verein')),
sa.UniqueConstraint('name', name=op.f('uq_verein_name'))
)
op.create_table('fahrer',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('nachname', sa.String(length=120), nullable=False),
sa.Column('vorname', sa.String(length=120), nullable=False),
sa.Column('geburtsjahr', sa.Integer(), nullable=True),
sa.Column('geburtsdatum', sa.Date(), nullable=True),
sa.Column('geschlecht', sa.Enum('M', 'W', 'D', name='geschlecht'), nullable=True),
sa.Column('verein_id', sa.Integer(), nullable=True),
sa.Column('adac_mitgliedsnr', sa.String(length=40), nullable=True),
sa.Column('jugendausweis_gueltig_bis', sa.Date(), nullable=True),
sa.Column('anzeige_anonymisiert', sa.Boolean(), nullable=False),
sa.Column('aktiv', sa.Boolean(), nullable=False),
sa.Column('notiz', sa.Text(), nullable=True),
sa.Column('erstellt_am', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('geaendert_am', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['verein_id'], ['verein.id'], name=op.f('fk_fahrer_verein_id_verein')),
sa.PrimaryKeyConstraint('id', name=op.f('pk_fahrer'))
)
op.create_index(op.f('ix_fahrer_geburtsjahr'), 'fahrer', ['geburtsjahr'], unique=False)
op.create_index(op.f('ix_fahrer_verein_id'), 'fahrer', ['verein_id'], unique=False)
op.execute(
"CREATE INDEX ix_fahrer_name_trgm ON fahrer USING gin "
"((lower(nachname || ' ' || vorname)) gin_trgm_ops)"
)
op.create_table('klasse',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('saison_jahr', sa.Integer(), nullable=False),
sa.Column('code', sa.String(length=10), nullable=False),
sa.Column('kategorie', sa.Enum('J', 'S', 'A', name='kategorie'), nullable=False),
sa.Column('bezeichnung', sa.String(length=200), nullable=False),
sa.Column('jahrgaenge', postgresql.ARRAY(sa.Integer()), nullable=True),
sa.Column('ccm_max', sa.Integer(), nullable=True),
sa.Column('kw_max', sa.Numeric(precision=5, scale=2), nullable=True),
sa.Column('fahrzeugtyp', sa.String(length=200), nullable=True),
sa.Column('mindestalter_am_veranstaltungstag', sa.Integer(), nullable=True),
sa.Column('sortierung', sa.Integer(), nullable=False),
sa.Column('notiz', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['saison_jahr'], ['saison.jahr'], name=op.f('fk_klasse_saison_jahr_saison'), ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_klasse')),
sa.UniqueConstraint('saison_jahr', 'code', name='klasse_saison_code')
)
op.create_index(op.f('ix_klasse_saison_jahr'), 'klasse', ['saison_jahr'], unique=False)
op.create_table('meisterschaft',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('saison_jahr', sa.Integer(), nullable=False),
sa.Column('bezeichnung', sa.String(length=200), nullable=False),
sa.Column('regelwerk', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.ForeignKeyConstraint(['saison_jahr'], ['saison.jahr'], name=op.f('fk_meisterschaft_saison_jahr_saison')),
sa.PrimaryKeyConstraint('id', name=op.f('pk_meisterschaft')),
sa.UniqueConstraint('saison_jahr', name='meisterschaft_saison_eindeutig')
)
op.create_index(op.f('ix_meisterschaft_saison_jahr'), 'meisterschaft', ['saison_jahr'], unique=True)
op.create_table('veranstaltung',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('datum', sa.Date(), nullable=False),
sa.Column('titel', sa.String(length=300), nullable=False),
sa.Column('ort', sa.String(length=200), nullable=True),
sa.Column('ausrichter_verein_id', sa.Integer(), nullable=True),
sa.Column('ist_doppelturnier', sa.Boolean(), nullable=False),
sa.Column('veranstaltungsleiter', sa.String(length=200), nullable=True),
sa.Column('turnierleiter', sa.String(length=200), nullable=True),
sa.Column('status', sa.Enum('geplant', 'laufend', 'vorlaeufig', 'final', name='veranstaltung_status'), nullable=False),
sa.Column('notiz', sa.Text(), nullable=True),
sa.Column('erstellt_am', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('geaendert_am', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['ausrichter_verein_id'], ['verein.id'], name=op.f('fk_veranstaltung_ausrichter_verein_id_verein')),
sa.PrimaryKeyConstraint('id', name=op.f('pk_veranstaltung'))
)
op.create_index(op.f('ix_veranstaltung_datum'), 'veranstaltung', ['datum'], unique=False)
op.create_table('meisterschaft_stand',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('meisterschaft_id', sa.Integer(), nullable=False),
sa.Column('kategorie', sa.Enum('J', 'S', 'A', name='meisterschaft_stand_kategorie'), nullable=False),
sa.Column('klasse_id', sa.Integer(), nullable=True),
sa.Column('fahrer_id', sa.Integer(), nullable=False),
sa.Column('punkte_gesamt', sa.Numeric(precision=9, scale=2), nullable=False),
sa.Column('punkte_je_lauf', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('gestrichene_laeufe', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('anzahl_siege', sa.Integer(), nullable=False),
sa.Column('platz', sa.Integer(), nullable=True),
sa.Column('berechnet_am', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['fahrer_id'], ['fahrer.id'], name=op.f('fk_meisterschaft_stand_fahrer_id_fahrer')),
sa.ForeignKeyConstraint(['klasse_id'], ['klasse.id'], name=op.f('fk_meisterschaft_stand_klasse_id_klasse')),
sa.ForeignKeyConstraint(['meisterschaft_id'], ['meisterschaft.id'], name=op.f('fk_meisterschaft_stand_meisterschaft_id_meisterschaft'), ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_meisterschaft_stand')),
sa.UniqueConstraint('meisterschaft_id', 'kategorie', 'klasse_id', 'fahrer_id', name='meisterschaft_stand_eindeutig')
)
op.create_index(op.f('ix_meisterschaft_stand_fahrer_id'), 'meisterschaft_stand', ['fahrer_id'], unique=False)
op.create_index(op.f('ix_meisterschaft_stand_meisterschaft_id'), 'meisterschaft_stand', ['meisterschaft_id'], unique=False)
op.create_table('turnier',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('veranstaltung_id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=120), nullable=False),
sa.Column('veranstalter_verein_id', sa.Integer(), nullable=True),
sa.Column('meisterschaft_id', sa.Integer(), nullable=True),
sa.Column('ist_aktiv', sa.Boolean(), nullable=False),
sa.Column('status', sa.Enum('geplant', 'laufend', 'vorlaeufig', 'final', name='turnier_status'), nullable=False),
sa.Column('erstellt_am', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('geaendert_am', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['meisterschaft_id'], ['meisterschaft.id'], name=op.f('fk_turnier_meisterschaft_id_meisterschaft'), ondelete='SET NULL'),
sa.ForeignKeyConstraint(['veranstalter_verein_id'], ['verein.id'], name=op.f('fk_turnier_veranstalter_verein_id_verein')),
sa.ForeignKeyConstraint(['veranstaltung_id'], ['veranstaltung.id'], name=op.f('fk_turnier_veranstaltung_id_veranstaltung'), ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_turnier'))
)
op.create_index(op.f('ix_turnier_meisterschaft_id'), 'turnier', ['meisterschaft_id'], unique=False)
op.create_index(op.f('ix_turnier_veranstaltung_id'), 'turnier', ['veranstaltung_id'], unique=False)
op.create_index('uq_turnier_ist_aktiv', 'turnier', ['ist_aktiv'], unique=True, postgresql_where=sa.text('ist_aktiv'))
op.create_table('wertungsklasse',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('turnier_id', sa.Integer(), nullable=False),
sa.Column('bezeichnung', sa.String(length=200), nullable=False),
sa.Column('klasse_ids', postgresql.ARRAY(sa.Integer()), nullable=False),
sa.Column('sortierung', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['turnier_id'], ['turnier.id'], name=op.f('fk_wertungsklasse_turnier_id_turnier'), ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_wertungsklasse'))
)
op.create_index(op.f('ix_wertungsklasse_turnier_id'), 'wertungsklasse', ['turnier_id'], unique=False)
op.create_table('start',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('turnier_id', sa.Integer(), nullable=False),
sa.Column('fahrer_id', sa.Integer(), nullable=False),
sa.Column('klasse_id', sa.Integer(), nullable=False),
sa.Column('wertungsklasse_id', sa.Integer(), nullable=True),
sa.Column('startnummer', sa.Integer(), nullable=False),
sa.Column('fahrzeug', sa.String(length=200), nullable=True),
sa.Column('nenngeld_bezahlt', sa.Boolean(), nullable=False),
sa.Column('status', sa.Enum('genannt', 'gestartet', 'DNF', 'DNS', 'DSQ', name='start_status'), nullable=False),
sa.Column('nennreihenfolge', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['fahrer_id'], ['fahrer.id'], name=op.f('fk_start_fahrer_id_fahrer')),
sa.ForeignKeyConstraint(['klasse_id'], ['klasse.id'], name=op.f('fk_start_klasse_id_klasse')),
sa.ForeignKeyConstraint(['turnier_id'], ['turnier.id'], name=op.f('fk_start_turnier_id_turnier'), ondelete='CASCADE'),
sa.ForeignKeyConstraint(['wertungsklasse_id'], ['wertungsklasse.id'], name=op.f('fk_start_wertungsklasse_id_wertungsklasse'), ondelete='SET NULL'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_start')),
sa.UniqueConstraint('turnier_id', 'startnummer', name='start_turnier_startnummer')
)
op.create_index(op.f('ix_start_fahrer_id'), 'start', ['fahrer_id'], unique=False)
op.create_index(op.f('ix_start_turnier_id'), 'start', ['turnier_id'], unique=False)
op.create_index(op.f('ix_start_wertungsklasse_id'), 'start', ['wertungsklasse_id'], unique=False)
op.create_table('ergebnis',
sa.Column('start_id', sa.Integer(), nullable=False),
sa.Column('summe_zeit', sa.Numeric(precision=7, scale=2), nullable=True),
sa.Column('summe_fehler', sa.Integer(), nullable=True),
sa.Column('gesamt', sa.Numeric(precision=9, scale=2), nullable=True),
sa.Column('platzierung', sa.Integer(), nullable=True),
sa.Column('zeit_vollstaendig', sa.Boolean(), nullable=False),
sa.Column('fehler_vollstaendig', sa.Boolean(), nullable=False),
sa.Column('erfasst_von', sa.String(length=120), nullable=True),
sa.Column('erfasst_am', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('korrigiert_am', sa.DateTime(timezone=True), nullable=True),
sa.Column('korrektur_grund', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['start_id'], ['start.id'], name=op.f('fk_ergebnis_start_id_start'), ondelete='CASCADE'),
sa.PrimaryKeyConstraint('start_id', name=op.f('pk_ergebnis'))
)
op.create_table('zeitmessung',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('geraet_id', sa.Integer(), nullable=False),
sa.Column('turnier_id', sa.Integer(), nullable=False),
sa.Column('start_id', sa.Integer(), nullable=True),
sa.Column('startnummer_gemeldet', sa.Integer(), nullable=True),
sa.Column('messpunkt', sa.String(length=80), nullable=False),
sa.Column('dauer_sekunden', sa.Numeric(precision=7, scale=2), nullable=False),
sa.Column('gemessen_am', sa.DateTime(timezone=True), nullable=False),
sa.Column('monotonic_ns', sa.BigInteger(), nullable=True),
sa.Column('empfangen_am', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('nachgeliefert', sa.Boolean(), nullable=False),
sa.Column('status', sa.Enum('gueltig', 'verworfen', 'unzugeordnet', 'unplausibel', name='zeitmessung_status'), nullable=False),
sa.Column('verworfen_grund', sa.Text(), nullable=True),
sa.Column('roh', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.ForeignKeyConstraint(['geraet_id'], ['geraet.id'], name=op.f('fk_zeitmessung_geraet_id_geraet')),
sa.ForeignKeyConstraint(['start_id'], ['start.id'], name=op.f('fk_zeitmessung_start_id_start'), ondelete='SET NULL'),
sa.ForeignKeyConstraint(['turnier_id'], ['turnier.id'], name=op.f('fk_zeitmessung_turnier_id_turnier'), ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_zeitmessung'))
)
op.create_index('ix_zeitmessung_start', 'zeitmessung', ['start_id'], unique=False)
op.create_index('ix_zeitmessung_turnier_status', 'zeitmessung', ['turnier_id', 'status'], unique=False)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('ix_zeitmessung_turnier_status', table_name='zeitmessung')
op.drop_index('ix_zeitmessung_start', table_name='zeitmessung')
op.drop_table('zeitmessung')
op.drop_table('ergebnis')
op.drop_index(op.f('ix_start_wertungsklasse_id'), table_name='start')
op.drop_index(op.f('ix_start_turnier_id'), table_name='start')
op.drop_index(op.f('ix_start_fahrer_id'), table_name='start')
op.drop_table('start')
op.drop_index(op.f('ix_wertungsklasse_turnier_id'), table_name='wertungsklasse')
op.drop_table('wertungsklasse')
op.drop_index('uq_turnier_ist_aktiv', table_name='turnier', postgresql_where=sa.text('ist_aktiv'))
op.drop_index(op.f('ix_turnier_veranstaltung_id'), table_name='turnier')
op.drop_index(op.f('ix_turnier_meisterschaft_id'), table_name='turnier')
op.drop_table('turnier')
op.drop_index(op.f('ix_meisterschaft_stand_meisterschaft_id'), table_name='meisterschaft_stand')
op.drop_index(op.f('ix_meisterschaft_stand_fahrer_id'), table_name='meisterschaft_stand')
op.drop_table('meisterschaft_stand')
op.drop_index(op.f('ix_veranstaltung_datum'), table_name='veranstaltung')
op.drop_table('veranstaltung')
op.drop_index(op.f('ix_meisterschaft_saison_jahr'), table_name='meisterschaft')
op.drop_table('meisterschaft')
op.drop_index(op.f('ix_klasse_saison_jahr'), table_name='klasse')
op.drop_table('klasse')
op.drop_index(op.f('ix_fahrer_verein_id'), table_name='fahrer')
op.drop_index(op.f('ix_fahrer_geburtsjahr'), table_name='fahrer')
op.drop_table('fahrer')
op.drop_table('verein')
op.drop_table('saison')
op.drop_index(op.f('ix_import_job_idempotenz_schluessel'), table_name='import_job')
op.drop_table('import_job')
op.drop_table('geraet')
op.drop_table('benutzer')
# ### end Alembic commands ###