"""add calendars & calendar_entries Revision ID: 215330c5ec15 Revises: 20251102_223123 Create Date: 2025-11-03 13:07:10.387189 """ from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision = "215330c5ec15" down_revision = "20251102_223123" branch_labels = None depends_on = None def upgrade(): # calendars op.create_table( "calendars", sa.Column("id", sa.Integer(), primary_key=True), sa.Column("post_id", sa.Integer(), sa.ForeignKey("posts.id", ondelete="CASCADE"), nullable=False), sa.Column("name", sa.String(length=255), nullable=False), sa.Column("slug", sa.String(length=255), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), # no hard UniqueConstraint; we enforce soft-delete-aware uniqueness with a partial index below ) # helpful lookup indexes op.create_index("ix_calendars_post_id", "calendars", ["post_id"], unique=False) op.create_index("ix_calendars_name", "calendars", ["name"], unique=False) op.create_index("ix_calendars_slug", "calendars", ["slug"], unique=False) # calendar_entries op.create_table( "calendar_entries", sa.Column("id", sa.Integer(), primary_key=True), sa.Column("calendar_id", sa.Integer(), sa.ForeignKey("calendars.id", ondelete="CASCADE"), nullable=False), sa.Column("name", sa.String(length=255), nullable=False), sa.Column("start_at", sa.DateTime(timezone=True), nullable=False), sa.Column("end_at", sa.DateTime(timezone=True), nullable=True), # <- allow open-ended sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), sa.CheckConstraint("(end_at IS NULL) OR (end_at >= start_at)", name="ck_calendar_entries_end_after_start"), ) op.create_index("ix_calendar_entries_calendar_id", "calendar_entries", ["calendar_id"], unique=False) op.create_index("ix_calendar_entries_start_at", "calendar_entries", ["start_at"], unique=False) op.create_index("ix_calendar_entries_name", "calendar_entries", ["name"], unique=False) # ---- Soft-delete-aware uniqueness for calendars (Postgres) ---- # One active calendar per (post_id, lower(slug)) if op.get_bind().dialect.name == "postgresql": # cleanup any active duplicates (defensive; table is new on fresh runs) op.execute(""" WITH ranked AS ( SELECT id, ROW_NUMBER() OVER ( PARTITION BY post_id, lower(slug) ORDER BY updated_at DESC, created_at DESC, id DESC ) AS rn FROM calendars WHERE deleted_at IS NULL ) UPDATE calendars c SET deleted_at = NOW() FROM ranked r WHERE c.id = r.id AND r.rn > 1; """) op.execute(""" CREATE UNIQUE INDEX IF NOT EXISTS ux_calendars_post_slug_active ON calendars (post_id, lower(slug)) WHERE deleted_at IS NULL; """) def downgrade(): # drop in reverse dependency order op.drop_index("ix_calendar_entries_name", table_name="calendar_entries") op.drop_index("ix_calendar_entries_start_at", table_name="calendar_entries") op.drop_index("ix_calendar_entries_calendar_id", table_name="calendar_entries") op.drop_table("calendar_entries") if op.get_bind().dialect.name == "postgresql": op.execute("DROP INDEX IF EXISTS ux_calendars_post_slug_active;") op.drop_index("ix_calendars_slug", table_name="calendars") op.drop_index("ix_calendars_name", table_name="calendars") op.drop_index("ix_calendars_post_id", table_name="calendars") op.drop_table("calendars")