"""TEMPLATE – Mustervorlage für Migration mit NEUER Tabelle + RLS-Policy. Abgeleitet vom Muster in migrations/versions/0035_absence_comments.py. WICHTIG (siehe project_dsgvo_multitenant_priority): - JEDE neue company-bezogene Tabelle braucht company_id-Spalte + RLS-Policy - NACH dieser Migration: tests/conftest.py muss die RLS-Policy manuell replizieren (Test-DB nutzt keine echten Migrationen) – sonst schlagen Company-Isolation-Tests unbemerkt fehl (false green) - Nur neue SPALTEN auf bereits RLS-gefencten Tabellen brauchen KEINE RLS-Änderung (siehe 0042_payout_requests.py als Gegenbeispiel) Anleitung: 1. Nach backend/migrations/versions/00XX_xxx_things.py kopieren 2. revision/down_revision auf nächste freie Nummer setzen (siehe "Datenbank-Migrationen – Chronik" in docmost für die aktuell höchste Nummer) 3. Tabellenname/Spalten anpassen 4. conftest.py RLS-Replikation ergänzen 5. Lokal (auf 137!) testen: alembic upgrade head, dann pytest """ from alembic import op from sqlalchemy import text revision = "00XX" # TODO: nächste freie Nummer down_revision = "00XX-1" # TODO: vorherige Migration branch_labels = None depends_on = None _BYPASS = "COALESCE(current_setting('app.bypass_rls', true), 'off') = 'on'" _CID = "company_id = NULLIF(current_setting('app.company_id', true), '')::uuid" _USING = f"({_BYPASS} OR {_CID})" def _exec(sql: str) -> None: op.execute(text(sql)) def upgrade() -> None: # 1) Tabelle anlegen _exec(""" CREATE TABLE IF NOT EXISTS xxx_things ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, note TEXT, status VARCHAR(12) NOT NULL DEFAULT 'requested', rejection_reason TEXT, created_by UUID NOT NULL REFERENCES users(id) ON DELETE SET NULL, decided_by UUID REFERENCES users(id) ON DELETE SET NULL, decided_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ) """) _exec("CREATE INDEX IF NOT EXISTS ix_xxx_things_company_id ON xxx_things(company_id)") _exec("CREATE INDEX IF NOT EXISTS ix_xxx_things_user_id ON xxx_things(user_id)") # 2) RLS (company_id-gefenced) _exec("ALTER TABLE xxx_things ENABLE ROW LEVEL SECURITY") _exec("ALTER TABLE xxx_things FORCE ROW LEVEL SECURITY") for cmd in ("select", "insert", "update", "delete"): _exec(f"DROP POLICY IF EXISTS rls_xxx_things_{cmd} ON xxx_things") _exec(f"CREATE POLICY rls_xxx_things_select ON xxx_things FOR SELECT USING {_USING}") _exec(f"CREATE POLICY rls_xxx_things_insert ON xxx_things FOR INSERT WITH CHECK {_USING}") _exec(f"CREATE POLICY rls_xxx_things_update ON xxx_things FOR UPDATE USING {_USING} WITH CHECK {_USING}") _exec(f"CREATE POLICY rls_xxx_things_delete ON xxx_things FOR DELETE USING {_USING}") def downgrade() -> None: _exec("DROP TABLE IF EXISTS xxx_things")