"""Add indexes on hot filter/FK-cascade columns, and user.is_active. Backend-perf audit (`scratchpad/audit/backend-perf.md` §4, M1): every migration to date created zero indexes; `EXPLAIN QUERY PLAN` against the full schema shows several hot paths doing a full table SCAN once the DB carries more than a handful of rows per table (multi-workspace deployment, years of FK-fetch history) — `planned_leave_day.count_by_parent_type` (2x per parent+child in analytics and plan/apply), `count_dubbeldagar`, `parental_leave_fetch.get_latest_by_parent`, and every FK-cascade column that `ON DELETE CASCADE` walks on a GDPR delete. These 14 indexes cover exactly those columns; see the audit for the full per-index rationale. Every `CREATE INDEX` below is mirrored verbatim (same columns, same order, no partial indexes) in `_FULL_SCHEMA` (`app/db/connection.py`) so `tests/integration/test_schema_parity.py` — which compares index sets between the Alembic chain and that hand-maintained DDL — stays green. Also adds `user.is_active` (default 1): `AuthService.login` and `get_user_from_token` already check `User.is_active` (account deactivation), but the column never existed — every account was unconditionally treated as active. This migration adds it and `SQLiteUserRepository` now reads/writes it. Revision ID: 011 Revises: 010 Create Date: 2026-09-18 """ from __future__ import annotations from alembic import op revision: str = "011" down_revision: str | None = "010" branch_labels: str | None = None depends_on: str | None = None _UPGRADE_SQL = """ ALTER TABLE "user" ADD COLUMN is_active INTEGER NOT NULL DEFAULT 1; CREATE INDEX idx_pld_parent_child_type_date ON planned_leave_day (parent_personal_data_uuid, child_id, day_type, date); CREATE INDEX idx_pld_child_dubbeldag_date ON planned_leave_day (child_id, is_dubbeldag, date); CREATE INDEX idx_pld_wish ON planned_leave_day (wish_id); CREATE INDEX idx_plf_parent_child_fetched ON parental_leave_fetch (personal_data_uuid, child_id, fetched_at); CREATE INDEX idx_plf_workspace ON parental_leave_fetch (workspace_id); CREATE INDEX idx_plf_child ON parental_leave_fetch (child_id); CREATE INDEX idx_leave_wish_workspace ON leave_wish (workspace_id); CREATE INDEX idx_leave_wish_child ON leave_wish (child_id); CREATE INDEX idx_leave_wish_parent ON leave_wish (parent_personal_data_uuid); CREATE INDEX idx_child_workspace_birth ON child (workspace_id, birth_date); CREATE INDEX idx_personal_data_workspace_deleted ON personal_data (workspace_id, deleted_at); CREATE INDEX idx_invitation_workspace_status_created ON workspace_invitation (workspace_id, status, created_at); CREATE INDEX idx_invitation_invited_by ON workspace_invitation (invited_by_user_id); CREATE INDEX idx_vacation_days_workspace ON vacation_days (workspace_id); """ _DOWNGRADE_SQL = """ DROP INDEX idx_vacation_days_workspace; DROP INDEX idx_invitation_invited_by; DROP INDEX idx_invitation_workspace_status_created; DROP INDEX idx_personal_data_workspace_deleted; DROP INDEX idx_child_workspace_birth; DROP INDEX idx_leave_wish_parent; DROP INDEX idx_leave_wish_child; DROP INDEX idx_leave_wish_workspace; DROP INDEX idx_plf_child; DROP INDEX idx_plf_workspace; DROP INDEX idx_plf_parent_child_fetched; DROP INDEX idx_pld_wish; DROP INDEX idx_pld_child_dubbeldag_date; DROP INDEX idx_pld_parent_child_type_date; ALTER TABLE "user" DROP COLUMN is_active; """ def upgrade() -> None: from sqlalchemy import text for stmt in [s.strip() for s in _UPGRADE_SQL.split(";") if s.strip()]: op.execute(text(stmt)) def downgrade() -> None: from sqlalchemy import text for stmt in [s.strip() for s in _DOWNGRADE_SQL.split(";") if s.strip()]: op.execute(text(stmt))