From 82ce592f170870cd9a45787a5aff924d60d3a620 Mon Sep 17 00:00:00 2001 From: patrick Date: Mon, 25 May 2026 00:57:29 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20migration=200029=20idempotent=20(IF=20NO?= =?UTF-8?q?T=20EXISTS=20f=C3=BCr=20Enum=20+=20Tabelle)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../versions/0029_special_assignments.py | 67 ++++++++++--------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/backend/migrations/versions/0029_special_assignments.py b/backend/migrations/versions/0029_special_assignments.py index 60ca8d6..fe7dd12 100644 --- a/backend/migrations/versions/0029_special_assignments.py +++ b/backend/migrations/versions/0029_special_assignments.py @@ -12,9 +12,7 @@ Neue Tabelle special_assignments: - label / description (optional) - Overlap-Check per Constraint (date_from <= date_to) + App-seitige Prüfung """ -import sqlalchemy as sa from alembic import op -from sqlalchemy.dialects import postgresql revision = "0029" down_revision = "0028" @@ -25,37 +23,46 @@ depends_on = None def upgrade() -> None: op.execute("CREATE EXTENSION IF NOT EXISTS btree_gist") - # Enum erzeugen - op.execute("CREATE TYPE assignment_mode AS ENUM ('fza', 'payroll', 'both')") + # Enum erzeugen (IF NOT EXISTS: falls create_all in lifespan den Typ schon angelegt hat) + op.execute("CREATE TYPE IF NOT EXISTS assignment_mode AS ENUM ('fza', 'payroll', 'both')") - op.create_table( - "special_assignments", - sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")), - sa.Column("user_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True), - sa.Column("company_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("companies.id", ondelete="CASCADE"), nullable=False, index=True), - sa.Column("date_from", sa.Date, nullable=False), - sa.Column("date_to", sa.Date, nullable=False), - sa.Column("factor", sa.Numeric(5, 3), nullable=False), - sa.Column("mode", sa.Enum("fza", "payroll", "both", name="assignment_mode", create_type=False), nullable=False, server_default="both"), - sa.Column("label", sa.String(100), nullable=True), - sa.Column("description", sa.Text, nullable=True), - sa.CheckConstraint("factor > 0 AND factor <= 10", name="ck_special_assignment_factor"), - sa.CheckConstraint("date_from <= date_to", name="ck_special_assignment_dates"), - ) - - # Exclusion Constraint: kein überlappender Zeitraum pro User - op.execute( - """ - ALTER TABLE special_assignments - ADD CONSTRAINT special_assignments_no_overlap - EXCLUDE USING gist ( - user_id WITH =, - daterange(date_from, date_to, '[]') WITH && + # Tabelle nur anlegen falls noch nicht vorhanden (idempotent) + op.execute(""" + CREATE TABLE IF NOT EXISTS special_assignments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE, + date_from DATE NOT NULL, + date_to DATE NOT NULL, + factor NUMERIC(5,3) NOT NULL, + mode assignment_mode NOT NULL DEFAULT 'both', + label VARCHAR(100), + description TEXT, + CONSTRAINT ck_special_assignment_factor CHECK (factor > 0 AND factor <= 10), + CONSTRAINT ck_special_assignment_dates CHECK (date_from <= date_to) ) - """ - ) + """) + op.execute("CREATE INDEX IF NOT EXISTS ix_special_assignments_user_id ON special_assignments(user_id)") + op.execute("CREATE INDEX IF NOT EXISTS ix_special_assignments_company_id ON special_assignments(company_id)") + + # Exclusion Constraint (nur falls noch nicht vorhanden) + op.execute(""" + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'special_assignments_no_overlap' + ) THEN + ALTER TABLE special_assignments + ADD CONSTRAINT special_assignments_no_overlap + EXCLUDE USING gist ( + user_id WITH =, + daterange(date_from, date_to, '[]') WITH && + ); + END IF; + END $$ + """) def downgrade() -> None: - op.drop_table("special_assignments") + op.execute("DROP TABLE IF EXISTS special_assignments") op.execute("DROP TYPE IF EXISTS assignment_mode")