"""Initial schema — full application schema, collapsed from the prior 13-migration chain. Migrations 001-013 are collapsed into this single one now that the chain has never shipped to a production database (dev-only so far). This file must stay byte-for-byte equivalent, schema-wise, to `_FULL_SCHEMA` in `app/db/connection.py` — `tests/integration/test_schema_parity.py` asserts exactly that by building a database from each and diffing tables, columns, CHECKs, foreign keys and indexes. The CREATE TABLE / CREATE INDEX statements below are copied verbatim from `_FULL_SCHEMA` so the two can never drift. Revision ID: 001 Revises: None Create Date: 2026-09-18 """ from __future__ import annotations from alembic import op revision: str = "001" down_revision: str | None = None branch_labels: str | None = None depends_on: str | None = None # Tables in FK-dependency order (workspace first); indexes last. Kept in sync # with `_FULL_SCHEMA` in `app/db/connection.py` — see module docstring. _UPGRADE_SQL = """ CREATE TABLE IF NOT EXISTS workspace ( id TEXT PRIMARY KEY, name TEXT NOT NULL, created_at TIMESTAMP NOT NULL ); CREATE TABLE IF NOT EXISTS personal_data ( uuid TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, name TEXT NOT NULL, salary_type TEXT NOT NULL DEFAULT 'monthly' CHECK (salary_type IN ('monthly','hourly')), employment_status TEXT NOT NULL DEFAULT 'employed' CHECK (employment_status IN ('employed','jobseeking','none')), min_vacation_days_per_year INTEGER NOT NULL DEFAULT 20 CHECK (min_vacation_days_per_year >= 0 AND min_vacation_days_per_year <= 60), personnummer_enc TEXT, created_at TIMESTAMP NOT NULL, deleted_at TIMESTAMP, FOREIGN KEY (workspace_id) REFERENCES workspace(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS "user" ( id TEXT PRIMARY KEY, personal_data_uuid TEXT NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE, hashed_password TEXT NOT NULL, created_at TIMESTAMP NOT NULL, is_active INTEGER NOT NULL DEFAULT 1, FOREIGN KEY (personal_data_uuid) REFERENCES personal_data(uuid) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS workspace_invitation ( id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, token_hash TEXT NOT NULL UNIQUE, invited_by_user_id TEXT NOT NULL, status TEXT NOT NULL CHECK (status IN ('pending','accepted','revoked','expired')), expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP NOT NULL, accepted_at TIMESTAMP, FOREIGN KEY (workspace_id) REFERENCES workspace(id) ON DELETE CASCADE, FOREIGN KEY (invited_by_user_id) REFERENCES "user"(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS child ( id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, name TEXT NOT NULL, birth_date DATE NOT NULL, preschool_start_date DATE, preschool_planning_days_per_year INTEGER NOT NULL DEFAULT 6 CHECK (preschool_planning_days_per_year >= 0 AND preschool_planning_days_per_year <= 30), created_at TIMESTAMP NOT NULL, FOREIGN KEY (workspace_id) REFERENCES workspace(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS leave_wish ( id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, child_id TEXT, name TEXT NOT NULL, name_message_key TEXT, window_start_month INTEGER NOT NULL DEFAULT 1 CHECK (window_start_month >= 1 AND window_start_month <= 12), window_start_day INTEGER NOT NULL DEFAULT 1 CHECK (window_start_day >= 1 AND window_start_day <= 31), window_end_month INTEGER NOT NULL DEFAULT 1 CHECK (window_end_month >= 1 AND window_end_month <= 12), window_end_day INTEGER NOT NULL DEFAULT 1 CHECK (window_end_day >= 1 AND window_end_day <= 31), target_weeks INTEGER NOT NULL, target_days INTEGER, recurrence TEXT NOT NULL DEFAULT 'once' CHECK (recurrence IN ('yearly','once')), first_year INTEGER NOT NULL DEFAULT 2026, last_year INTEGER, priority INTEGER NOT NULL DEFAULT 1 CHECK (priority >= 1), parent_mode TEXT NOT NULL DEFAULT 'either' CHECK (parent_mode IN ('either','both','specific')), parent_personal_data_uuid TEXT, window_mode TEXT NOT NULL DEFAULT 'fixed' CHECK (window_mode IN ('fixed','flexible')), wants_dubbeldag INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL, FOREIGN KEY (workspace_id) REFERENCES workspace(id) ON DELETE CASCADE, FOREIGN KEY (child_id) REFERENCES child(id) ON DELETE CASCADE, FOREIGN KEY (parent_personal_data_uuid) REFERENCES personal_data(uuid) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS parental_leave_fetch ( id TEXT PRIMARY KEY, personal_data_uuid TEXT NOT NULL, workspace_id TEXT NOT NULL, child_id TEXT NOT NULL, source TEXT NOT NULL CHECK (source IN ('scraped','manual')), fetched_at TIMESTAMP NOT NULL, total_days INTEGER NOT NULL, days_used INTEGER NOT NULL, days_left INTEGER NOT NULL, reserved_days_total INTEGER NOT NULL DEFAULT 90, reserved_days_left INTEGER NOT NULL, transferable_days_left INTEGER NOT NULL, dubbeldagar_left INTEGER NOT NULL, tier_data TEXT NOT NULL, created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL, FOREIGN KEY (personal_data_uuid) REFERENCES personal_data(uuid) ON DELETE CASCADE, FOREIGN KEY (workspace_id) REFERENCES workspace(id) ON DELETE CASCADE, FOREIGN KEY (child_id) REFERENCES child(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS vacation_days ( id TEXT PRIMARY KEY, personal_data_uuid TEXT NOT NULL, workspace_id TEXT NOT NULL, year INTEGER NOT NULL, total_days INTEGER NOT NULL, saved_days_in INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL, UNIQUE(personal_data_uuid, year), FOREIGN KEY (personal_data_uuid) REFERENCES personal_data(uuid) ON DELETE CASCADE, FOREIGN KEY (workspace_id) REFERENCES workspace(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS planned_leave_day ( id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, child_id TEXT NOT NULL, parent_personal_data_uuid TEXT NOT NULL, date DATE NOT NULL, day_type TEXT NOT NULL CHECK (day_type IN ('sjukpenning','lagsta','vacation')), is_dubbeldag INTEGER NOT NULL DEFAULT 0, wish_id TEXT, is_pinned INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL, UNIQUE(workspace_id, child_id, date, parent_personal_data_uuid), FOREIGN KEY (workspace_id) REFERENCES workspace(id) ON DELETE CASCADE, FOREIGN KEY (child_id) REFERENCES child(id) ON DELETE CASCADE, FOREIGN KEY (parent_personal_data_uuid) REFERENCES personal_data(uuid) ON DELETE CASCADE, FOREIGN KEY (wish_id) REFERENCES leave_wish(id) ON DELETE SET NULL ); CREATE TABLE IF NOT EXISTS day_override ( id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, date DATE NOT NULL, is_working_day INTEGER NOT NULL, note TEXT, created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL, UNIQUE(workspace_id, date), FOREIGN KEY (workspace_id) REFERENCES workspace(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS tip_dismissal ( id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, tip_id TEXT NOT NULL, data_hash TEXT NOT NULL, dismissed_at TIMESTAMP NOT NULL, UNIQUE(workspace_id, tip_id), FOREIGN KEY (workspace_id) REFERENCES workspace(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_pld_parent_child_type_date ON planned_leave_day (parent_personal_data_uuid, child_id, day_type, date); CREATE INDEX IF NOT EXISTS idx_pld_child_dubbeldag_date ON planned_leave_day (child_id, is_dubbeldag, date); CREATE INDEX IF NOT EXISTS idx_pld_wish ON planned_leave_day (wish_id); CREATE INDEX IF NOT EXISTS idx_plf_parent_child_fetched ON parental_leave_fetch (personal_data_uuid, child_id, fetched_at); CREATE INDEX IF NOT EXISTS idx_plf_workspace ON parental_leave_fetch (workspace_id); CREATE INDEX IF NOT EXISTS idx_plf_child ON parental_leave_fetch (child_id); CREATE INDEX IF NOT EXISTS idx_leave_wish_workspace ON leave_wish (workspace_id); CREATE INDEX IF NOT EXISTS idx_leave_wish_child ON leave_wish (child_id); CREATE INDEX IF NOT EXISTS idx_leave_wish_parent ON leave_wish (parent_personal_data_uuid); CREATE INDEX IF NOT EXISTS idx_child_workspace_birth ON child (workspace_id, birth_date); CREATE INDEX IF NOT EXISTS idx_personal_data_workspace_deleted ON personal_data (workspace_id, deleted_at); CREATE INDEX IF NOT EXISTS idx_invitation_workspace_status_created ON workspace_invitation (workspace_id, status, created_at); CREATE INDEX IF NOT EXISTS idx_invitation_invited_by ON workspace_invitation (invited_by_user_id); CREATE INDEX IF NOT EXISTS idx_vacation_days_workspace ON vacation_days (workspace_id); """ # Reverse FK-dependency order: drop dependents before their targets. Dropping # a table also drops its own indexes, so no separate DROP INDEX statements # are needed. _DOWNGRADE_SQL = """ DROP TABLE IF EXISTS tip_dismissal; DROP TABLE IF EXISTS day_override; DROP TABLE IF EXISTS planned_leave_day; DROP TABLE IF EXISTS vacation_days; DROP TABLE IF EXISTS parental_leave_fetch; DROP TABLE IF EXISTS leave_wish; DROP TABLE IF EXISTS child; DROP TABLE IF EXISTS workspace_invitation; DROP TABLE IF EXISTS "user"; DROP TABLE IF EXISTS personal_data; DROP TABLE IF EXISTS workspace; """ 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))