diff --git a/backend/alembic/versions/054_add_calendar_event_indexes.py b/backend/alembic/versions/054_add_calendar_event_indexes.py new file mode 100644 index 0000000..e69a8c2 --- /dev/null +++ b/backend/alembic/versions/054_add_calendar_event_indexes.py @@ -0,0 +1,36 @@ +"""Add performance indexes to calendar_events + +Revision ID: 054 +Revises: 053 +""" +from alembic import op + +revision = "054" +down_revision = "053" + + +def upgrade(): + # Covers range queries in dashboard today's events and events list + op.create_index( + "ix_calendar_events_calendar_start", + "calendar_events", + ["calendar_id", "start_datetime"], + ) + # Covers bulk DELETE on recurrence edit/regeneration and sibling lookups + op.create_index( + "ix_calendar_events_parent_event_id", + "calendar_events", + ["parent_event_id"], + ) + # Covers starred widget query (calendar_id + is_starred + start_datetime) + op.create_index( + "ix_calendar_events_calendar_starred_start", + "calendar_events", + ["calendar_id", "is_starred", "start_datetime"], + ) + + +def downgrade(): + op.drop_index("ix_calendar_events_calendar_starred_start", table_name="calendar_events") + op.drop_index("ix_calendar_events_parent_event_id", table_name="calendar_events") + op.drop_index("ix_calendar_events_calendar_start", table_name="calendar_events") diff --git a/backend/app/routers/dashboard.py b/backend/app/routers/dashboard.py index e69a7e6..b131e77 100644 --- a/backend/app/routers/dashboard.py +++ b/backend/app/routers/dashboard.py @@ -35,8 +35,12 @@ async def get_dashboard( today = client_date or date.today() upcoming_cutoff = today + timedelta(days=current_settings.upcoming_days) - # Subquery: calendar IDs belonging to this user (for event scoping) - user_calendar_ids = select(Calendar.id).where(Calendar.user_id == current_user.id) + # Fetch calendar IDs once as a plain list — PostgreSQL handles IN (1,2,3) more + # efficiently than re-evaluating a correlated subquery for each of the 3 queries below. + calendar_id_result = await db.execute( + select(Calendar.id).where(Calendar.user_id == current_user.id) + ) + user_calendar_ids = [row[0] for row in calendar_id_result.all()] # Today's events (exclude parent templates — they are hidden, children are shown) today_start = datetime.combine(today, datetime.min.time()) @@ -173,8 +177,11 @@ async def get_upcoming( overdue_floor = today - timedelta(days=30) overdue_floor_dt = datetime.combine(overdue_floor, datetime.min.time()) - # Subquery: calendar IDs belonging to this user - user_calendar_ids = select(Calendar.id).where(Calendar.user_id == current_user.id) + # Fetch calendar IDs once as a plain list (same rationale as /dashboard handler) + calendar_id_result = await db.execute( + select(Calendar.id).where(Calendar.user_id == current_user.id) + ) + user_calendar_ids = [row[0] for row in calendar_id_result.all()] # Build queries — include overdue todos (up to 30 days back) and snoozed reminders todos_query = select(Todo).where(