UMBRA/backend/alembic/versions/007_add_recurrence_fields.py
Kyle Pope d89758fedf Add materialized recurring events backend
- Migration 007: parent_event_id (self-ref FK CASCADE), is_recurring, original_start columns on calendar_events
- CalendarEvent model: three new Mapped[] columns for recurrence tracking
- RecurrenceRule Pydantic model: typed schema for every_n_days, weekly, monthly_nth_weekday, monthly_date
- CalendarEventCreate/Update: accept structured RecurrenceRule (router serializes to JSON string for DB)
- CalendarEventUpdate: edit_scope field (this | this_and_future)
- CalendarEventResponse: exposes parent_event_id, is_recurring, original_start
- recurrence.py service: generates unsaved child ORM objects from parent rule up to 365-day horizon
- GET /: excludes parent template rows (children are displayed instead)
- POST /: creates parent + bulk children when recurrence_rule provided
- PUT /: scope=this detaches occurrence; scope=this_and_future deletes future siblings and regenerates
- DELETE /: scope=this deletes one; scope=this_and_future deletes future siblings; no scope deletes all (CASCADE)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 00:37:21 +08:00

63 lines
1.6 KiB
Python

"""Add recurrence fields to calendar_events
Revision ID: 007
Revises: 006
Create Date: 2026-02-22 00:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '007'
down_revision: Union[str, None] = '006'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# parent_event_id: self-referential FK, nullable, ON DELETE CASCADE
op.add_column(
'calendar_events',
sa.Column('parent_event_id', sa.Integer(), nullable=True)
)
op.create_foreign_key(
'fk_calendar_events_parent_event_id',
'calendar_events',
'calendar_events',
['parent_event_id'],
['id'],
ondelete='CASCADE',
)
# is_recurring: tracks whether this row is part of a recurring series
op.add_column(
'calendar_events',
sa.Column(
'is_recurring',
sa.Boolean(),
nullable=False,
server_default='false',
)
)
# original_start: the originally computed occurrence datetime (naive, no TZ)
op.add_column(
'calendar_events',
sa.Column('original_start', sa.DateTime(), nullable=True)
)
def downgrade() -> None:
op.drop_column('calendar_events', 'original_start')
op.drop_column('calendar_events', 'is_recurring')
op.drop_constraint(
'fk_calendar_events_parent_event_id',
'calendar_events',
type_='foreignkey',
)
op.drop_column('calendar_events', 'parent_event_id')