-- Rollback for 018_projects_v2. -- -- Recreates paliad.akten (same UUIDs), restores akte_id FK columns on the -- children, renames child tables back to German (notes→notizen, deadlines→ -- fristen, appointments→termine, parties→parteien, documents→dokumente, -- project_events→akten_events), drops the new projects/team/department -- tables. Best-effort: -- * Child rows whose project_id points at a non-akten project row (i.e., -- anything created post-migration under non-'case' types) will fail -- FK re-creation. The rollback aborts in that case — this is intentional, -- since v2-only data has no v1 home. -- * project_teams memberships are folded back into akten.collaborators -- (dedup, drop role metadata); users who were only 'lead' end up in -- the array like everyone else. -- * owning_office is set to the creator's user.office when available, -- else 'munich' as a last-resort fallback (documented loss). -- 1. Recreate paliad.akten with the old shape. CREATE TABLE paliad.akten ( id uuid PRIMARY KEY, aktenzeichen text NOT NULL, title text NOT NULL, akte_type text, court text, court_ref text, status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'pending', 'closed', 'archived')), ai_summary text, owning_office text NOT NULL CHECK (owning_office IN ( 'munich', 'duesseldorf', 'hamburg', 'amsterdam', 'london', 'paris', 'milan' )), collaborators uuid[] NOT NULL DEFAULT '{}', firm_wide_visible boolean NOT NULL DEFAULT false, created_by uuid REFERENCES auth.users(id) ON DELETE SET NULL, metadata jsonb NOT NULL DEFAULT '{}', created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX akten_status_owning_office_idx ON paliad.akten (status, owning_office); CREATE INDEX akten_owning_office_idx ON paliad.akten (owning_office); CREATE INDEX akten_firm_wide_idx ON paliad.akten (firm_wide_visible) WHERE firm_wide_visible = true; CREATE INDEX akten_collaborators_gin_idx ON paliad.akten USING GIN (collaborators); -- 2. Backfill from projects (only type='case' — others have no v1 home). INSERT INTO paliad.akten ( id, aktenzeichen, title, akte_type, court, court_ref, status, ai_summary, owning_office, collaborators, firm_wide_visible, created_by, metadata, created_at, updated_at ) SELECT p.id, COALESCE(p.reference, p.id::text), p.title, NULL, p.court, p.case_number, CASE WHEN p.status IN ('active','pending','closed','archived') THEN p.status ELSE 'active' END, p.ai_summary, COALESCE( (SELECT u.office FROM paliad.users u WHERE u.id = p.created_by), 'munich' ), COALESCE( (SELECT array_agg(DISTINCT pt.user_id) FROM paliad.project_teams pt WHERE pt.project_id = p.id AND pt.user_id IS NOT NULL), '{}'::uuid[] ), false, p.created_by, p.metadata, p.created_at, p.updated_at FROM paliad.projects p WHERE p.type = 'case'; -- 3. Drop new RLS policies + helpers we installed in .up. DROP POLICY IF EXISTS parties_all ON paliad.parties; DROP POLICY IF EXISTS deadlines_all ON paliad.deadlines; DROP POLICY IF EXISTS appointments_select ON paliad.appointments; DROP POLICY IF EXISTS appointments_insert ON paliad.appointments; DROP POLICY IF EXISTS appointments_update ON paliad.appointments; DROP POLICY IF EXISTS appointments_delete ON paliad.appointments; DROP POLICY IF EXISTS documents_all ON paliad.documents; DROP POLICY IF EXISTS project_events_all ON paliad.project_events; DROP POLICY IF EXISTS notes_all ON paliad.notes; DROP POLICY IF EXISTS checklist_instances_select ON paliad.checklist_instances; DROP POLICY IF EXISTS checklist_instances_insert ON paliad.checklist_instances; DROP POLICY IF EXISTS checklist_instances_update ON paliad.checklist_instances; DROP POLICY IF EXISTS checklist_instances_delete ON paliad.checklist_instances; DROP POLICY IF EXISTS projects_select ON paliad.projects; DROP POLICY IF EXISTS projects_insert ON paliad.projects; DROP POLICY IF EXISTS projects_update ON paliad.projects; DROP POLICY IF EXISTS projects_delete ON paliad.projects; DROP POLICY IF EXISTS project_teams_select ON paliad.project_teams; DROP POLICY IF EXISTS project_teams_insert ON paliad.project_teams; DROP POLICY IF EXISTS project_teams_update ON paliad.project_teams; DROP POLICY IF EXISTS project_teams_delete ON paliad.project_teams; DROP POLICY IF EXISTS departments_select ON paliad.departments; DROP POLICY IF EXISTS departments_write ON paliad.departments; DROP POLICY IF EXISTS department_members_select ON paliad.department_members; DROP POLICY IF EXISTS department_members_write ON paliad.department_members; DROP FUNCTION IF EXISTS paliad.note_is_visible(uuid, uuid, uuid, uuid); DROP FUNCTION IF EXISTS paliad.can_see_project(uuid); -- 4. Rename child tables back to German + restore akte_id column. ALTER TABLE paliad.parties DROP CONSTRAINT IF EXISTS parties_project_id_fkey; ALTER TABLE paliad.parties RENAME COLUMN project_id TO akte_id; ALTER TABLE paliad.parties RENAME TO parteien; ALTER TABLE paliad.parteien ADD CONSTRAINT parteien_akte_id_fkey FOREIGN KEY (akte_id) REFERENCES paliad.akten(id) ON DELETE CASCADE; DROP INDEX IF EXISTS paliad.parties_project_idx; CREATE INDEX parteien_akte_idx ON paliad.parteien (akte_id); ALTER TABLE paliad.deadlines DROP CONSTRAINT IF EXISTS deadlines_project_id_fkey; ALTER TABLE paliad.deadlines RENAME COLUMN project_id TO akte_id; ALTER TABLE paliad.deadlines RENAME TO fristen; ALTER TABLE paliad.fristen ADD CONSTRAINT fristen_akte_id_fkey FOREIGN KEY (akte_id) REFERENCES paliad.akten(id) ON DELETE CASCADE; DROP INDEX IF EXISTS paliad.deadlines_project_idx; DROP INDEX IF EXISTS paliad.deadlines_status_due_date_idx; DROP INDEX IF EXISTS paliad.deadlines_due_date_idx; CREATE INDEX fristen_akte_idx ON paliad.fristen (akte_id); CREATE INDEX fristen_status_due_date_idx ON paliad.fristen (status, due_date); CREATE INDEX fristen_due_date_idx ON paliad.fristen (due_date); ALTER TABLE paliad.appointments DROP CONSTRAINT IF EXISTS appointments_project_id_fkey; ALTER TABLE paliad.appointments DROP CONSTRAINT IF EXISTS appointments_appointment_type_check; ALTER TABLE paliad.appointments RENAME COLUMN project_id TO akte_id; ALTER TABLE paliad.appointments RENAME COLUMN appointment_type TO termin_type; ALTER TABLE paliad.appointments RENAME TO termine; ALTER TABLE paliad.termine ADD CONSTRAINT termine_akte_id_fkey FOREIGN KEY (akte_id) REFERENCES paliad.akten(id) ON DELETE CASCADE, ADD CONSTRAINT termine_termin_type_check CHECK (termin_type IS NULL OR termin_type IN ( 'hearing', 'meeting', 'consultation', 'deadline_hearing' )); DROP INDEX IF EXISTS paliad.appointments_project_idx; DROP INDEX IF EXISTS paliad.appointments_start_at_idx; CREATE INDEX termine_akte_idx ON paliad.termine (akte_id); CREATE INDEX termine_start_at_idx ON paliad.termine (start_at); ALTER TABLE paliad.documents DROP CONSTRAINT IF EXISTS documents_project_id_fkey; ALTER TABLE paliad.documents RENAME COLUMN project_id TO akte_id; ALTER TABLE paliad.documents RENAME TO dokumente; ALTER TABLE paliad.dokumente ADD CONSTRAINT dokumente_akte_id_fkey FOREIGN KEY (akte_id) REFERENCES paliad.akten(id) ON DELETE CASCADE; DROP INDEX IF EXISTS paliad.documents_project_idx; CREATE INDEX dokumente_akte_idx ON paliad.dokumente (akte_id); ALTER TABLE paliad.project_events DROP CONSTRAINT IF EXISTS project_events_project_id_fkey; ALTER TABLE paliad.project_events RENAME COLUMN project_id TO akte_id; ALTER TABLE paliad.project_events RENAME TO akten_events; ALTER TABLE paliad.akten_events ADD CONSTRAINT akten_events_akte_id_fkey FOREIGN KEY (akte_id) REFERENCES paliad.akten(id) ON DELETE CASCADE; DROP INDEX IF EXISTS paliad.project_events_project_created_idx; CREATE INDEX akten_events_akte_created_idx ON paliad.akten_events (akte_id, created_at DESC); -- notes → notizen ALTER TABLE paliad.notes DROP CONSTRAINT IF EXISTS notes_exactly_one_parent; ALTER TABLE paliad.notes DROP CONSTRAINT IF EXISTS notes_project_id_fkey; ALTER TABLE paliad.notes DROP CONSTRAINT IF EXISTS notes_deadline_id_fkey; ALTER TABLE paliad.notes DROP CONSTRAINT IF EXISTS notes_appointment_id_fkey; ALTER TABLE paliad.notes DROP CONSTRAINT IF EXISTS notes_project_event_id_fkey; ALTER TABLE paliad.notes RENAME COLUMN project_id TO akte_id; ALTER TABLE paliad.notes RENAME COLUMN deadline_id TO frist_id; ALTER TABLE paliad.notes RENAME COLUMN appointment_id TO termin_id; ALTER TABLE paliad.notes RENAME COLUMN project_event_id TO akten_event_id; ALTER TABLE paliad.notes RENAME TO notizen; ALTER TABLE paliad.notizen ADD CONSTRAINT notizen_akte_id_fkey FOREIGN KEY (akte_id) REFERENCES paliad.akten(id) ON DELETE CASCADE, ADD CONSTRAINT notizen_frist_id_fkey FOREIGN KEY (frist_id) REFERENCES paliad.fristen(id) ON DELETE CASCADE, ADD CONSTRAINT notizen_termin_id_fkey FOREIGN KEY (termin_id) REFERENCES paliad.termine(id) ON DELETE CASCADE, ADD CONSTRAINT notizen_akten_event_id_fkey FOREIGN KEY (akten_event_id) REFERENCES paliad.akten_events(id) ON DELETE CASCADE, ADD CONSTRAINT notizen_exactly_one_parent CHECK ( (CASE WHEN akte_id IS NOT NULL THEN 1 ELSE 0 END) + (CASE WHEN frist_id IS NOT NULL THEN 1 ELSE 0 END) + (CASE WHEN termin_id IS NOT NULL THEN 1 ELSE 0 END) + (CASE WHEN akten_event_id IS NOT NULL THEN 1 ELSE 0 END) = 1 ); DROP INDEX IF EXISTS paliad.notes_project_idx; DROP INDEX IF EXISTS paliad.notes_deadline_idx; DROP INDEX IF EXISTS paliad.notes_appointment_idx; DROP INDEX IF EXISTS paliad.notes_project_event_idx; CREATE INDEX notizen_akte_idx ON paliad.notizen (akte_id) WHERE akte_id IS NOT NULL; CREATE INDEX notizen_frist_idx ON paliad.notizen (frist_id) WHERE frist_id IS NOT NULL; CREATE INDEX notizen_termin_idx ON paliad.notizen (termin_id) WHERE termin_id IS NOT NULL; CREATE INDEX notizen_akten_event_idx ON paliad.notizen (akten_event_id) WHERE akten_event_id IS NOT NULL; -- checklist_instances: restore akte_id column. ALTER TABLE paliad.checklist_instances DROP CONSTRAINT IF EXISTS checklist_instances_project_id_fkey; ALTER TABLE paliad.checklist_instances RENAME COLUMN project_id TO akte_id; ALTER TABLE paliad.checklist_instances ADD CONSTRAINT checklist_instances_akte_id_fkey FOREIGN KEY (akte_id) REFERENCES paliad.akten(id) ON DELETE SET NULL; DROP INDEX IF EXISTS paliad.checklist_instances_project_idx; CREATE INDEX checklist_instances_akte_idx ON paliad.checklist_instances (akte_id) WHERE akte_id IS NOT NULL; -- 5. Drop new tables + triggers. DROP TRIGGER IF EXISTS projects_rewrite_subtree_after ON paliad.projects; DROP TRIGGER IF EXISTS projects_sync_path_before ON paliad.projects; DROP FUNCTION IF EXISTS paliad.projects_rewrite_subtree(); DROP FUNCTION IF EXISTS paliad.projects_sync_path(); DROP TABLE IF EXISTS paliad.department_members; DROP TABLE IF EXISTS paliad.departments; DROP TABLE IF EXISTS paliad.project_teams; DROP TABLE IF EXISTS paliad.projects; -- 6. Restore the v1 visibility helpers + RLS policies. CREATE OR REPLACE FUNCTION paliad.can_see_akte(_akte_id uuid) RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path = paliad, public AS $$ SELECT EXISTS ( SELECT 1 FROM paliad.akten a LEFT JOIN paliad.users u ON u.id = auth.uid() WHERE a.id = _akte_id AND ( a.firm_wide_visible OR (u.office IS NOT NULL AND a.owning_office = u.office) OR auth.uid() = ANY (a.collaborators) OR (u.role = 'admin') ) ); $$; CREATE OR REPLACE FUNCTION paliad.notiz_is_visible( _akte_id uuid, _frist_id uuid, _termin_id uuid, _akten_event_id uuid ) RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path = paliad, public AS $$ SELECT CASE WHEN _akte_id IS NOT NULL THEN paliad.can_see_akte(_akte_id) WHEN _frist_id IS NOT NULL THEN paliad.can_see_akte((SELECT akte_id FROM paliad.fristen WHERE id = _frist_id)) WHEN _termin_id IS NOT NULL THEN CASE WHEN (SELECT akte_id FROM paliad.termine WHERE id = _termin_id) IS NULL THEN (SELECT created_by FROM paliad.termine WHERE id = _termin_id) = auth.uid() ELSE paliad.can_see_akte((SELECT akte_id FROM paliad.termine WHERE id = _termin_id)) END WHEN _akten_event_id IS NOT NULL THEN paliad.can_see_akte((SELECT akte_id FROM paliad.akten_events WHERE id = _akten_event_id)) ELSE false END; $$; ALTER TABLE paliad.akten ENABLE ROW LEVEL SECURITY; CREATE POLICY akten_select ON paliad.akten FOR SELECT TO authenticated USING (paliad.can_see_akte(id)); CREATE POLICY akten_insert ON paliad.akten FOR INSERT TO authenticated WITH CHECK ( owning_office = (SELECT office FROM paliad.users WHERE id = auth.uid()) OR (SELECT role FROM paliad.users WHERE id = auth.uid()) = 'admin' ); CREATE POLICY akten_update ON paliad.akten FOR UPDATE TO authenticated USING (paliad.can_see_akte(id)) WITH CHECK (paliad.can_see_akte(id)); CREATE POLICY akten_delete ON paliad.akten FOR DELETE TO authenticated USING ( paliad.can_see_akte(id) AND (SELECT role FROM paliad.users WHERE id = auth.uid()) IN ('partner', 'admin') ); CREATE POLICY parteien_all ON paliad.parteien FOR ALL TO authenticated USING (paliad.can_see_akte(akte_id)) WITH CHECK (paliad.can_see_akte(akte_id)); CREATE POLICY fristen_all ON paliad.fristen FOR ALL TO authenticated USING (paliad.can_see_akte(akte_id)) WITH CHECK (paliad.can_see_akte(akte_id)); CREATE POLICY termine_select ON paliad.termine FOR SELECT TO authenticated USING ((akte_id IS NULL AND created_by = auth.uid()) OR (akte_id IS NOT NULL AND paliad.can_see_akte(akte_id))); CREATE POLICY termine_insert ON paliad.termine FOR INSERT TO authenticated WITH CHECK ((akte_id IS NULL AND created_by = auth.uid()) OR (akte_id IS NOT NULL AND paliad.can_see_akte(akte_id))); CREATE POLICY termine_update ON paliad.termine FOR UPDATE TO authenticated USING ((akte_id IS NULL AND created_by = auth.uid()) OR (akte_id IS NOT NULL AND paliad.can_see_akte(akte_id))) WITH CHECK ((akte_id IS NULL AND created_by = auth.uid()) OR (akte_id IS NOT NULL AND paliad.can_see_akte(akte_id))); CREATE POLICY termine_delete ON paliad.termine FOR DELETE TO authenticated USING ((akte_id IS NULL AND created_by = auth.uid()) OR (akte_id IS NOT NULL AND paliad.can_see_akte(akte_id))); CREATE POLICY dokumente_all ON paliad.dokumente FOR ALL TO authenticated USING (paliad.can_see_akte(akte_id)) WITH CHECK (paliad.can_see_akte(akte_id)); CREATE POLICY akten_events_all ON paliad.akten_events FOR ALL TO authenticated USING (paliad.can_see_akte(akte_id)) WITH CHECK (paliad.can_see_akte(akte_id)); CREATE POLICY notizen_all ON paliad.notizen FOR ALL TO authenticated USING (paliad.notiz_is_visible(akte_id, frist_id, termin_id, akten_event_id)) WITH CHECK (paliad.notiz_is_visible(akte_id, frist_id, termin_id, akten_event_id)); CREATE POLICY checklist_instances_select ON paliad.checklist_instances FOR SELECT TO authenticated USING ((akte_id IS NULL AND created_by = auth.uid()) OR (akte_id IS NOT NULL AND paliad.can_see_akte(akte_id))); CREATE POLICY checklist_instances_insert ON paliad.checklist_instances FOR INSERT TO authenticated WITH CHECK (created_by = auth.uid() AND (akte_id IS NULL OR paliad.can_see_akte(akte_id))); CREATE POLICY checklist_instances_update ON paliad.checklist_instances FOR UPDATE TO authenticated USING ((akte_id IS NULL AND created_by = auth.uid()) OR (akte_id IS NOT NULL AND paliad.can_see_akte(akte_id))) WITH CHECK ((akte_id IS NULL AND created_by = auth.uid()) OR (akte_id IS NOT NULL AND paliad.can_see_akte(akte_id))); CREATE POLICY checklist_instances_delete ON paliad.checklist_instances FOR DELETE TO authenticated USING ((akte_id IS NULL AND created_by = auth.uid()) OR (akte_id IS NOT NULL AND paliad.can_see_akte(akte_id))); -- 7. Drop the users.additional_offices column we added. ALTER TABLE paliad.users DROP COLUMN IF EXISTS additional_offices;