-- 0002_path_trigger.sql -- Maintain projax.items.path as dot-joined slug walk from root. -- Enforce: areas at root only, projects not at root, no cycles. -- See docs/design.md ยง3.1 create or replace function projax.compute_item_path(p_parent_id uuid, p_slug text, p_self_id uuid) returns text language plpgsql stable as $$ declare parts text[] := array[p_slug]; cur_id uuid := p_parent_id; cur_slug text; cur_parent uuid; hops int := 0; begin while cur_id is not null loop hops := hops + 1; if hops > 64 then raise exception 'projax.items: path depth exceeds 64 (cycle or pathological tree?)'; end if; if p_self_id is not null and cur_id = p_self_id then raise exception 'projax.items: cycle detected (item % is ancestor of itself)', p_self_id using errcode = 'check_violation'; end if; select slug, parent_id into cur_slug, cur_parent from projax.items where id = cur_id; if cur_slug is null then raise exception 'projax.items: parent % not found', cur_id; end if; parts := array_prepend(cur_slug, parts); cur_id := cur_parent; end loop; return array_to_string(parts, '.'); end; $$; create or replace function projax.items_before_write() returns trigger language plpgsql as $$ begin -- Structural rules if 'area' = any(new.kind) and new.parent_id is not null then raise exception 'projax.items: area must have parent_id = NULL (got %)', new.parent_id using errcode = 'check_violation'; end if; if 'project' = any(new.kind) and new.parent_id is null then raise exception 'projax.items: project must have a non-null parent_id' using errcode = 'check_violation'; end if; -- Cycle check on UPDATE: a node cannot be its own ancestor if tg_op = 'UPDATE' and new.parent_id is not null and new.parent_id = new.id then raise exception 'projax.items: parent_id cannot equal id' using errcode = 'check_violation'; end if; -- Compute path new.path := projax.compute_item_path(new.parent_id, new.slug, case when tg_op = 'UPDATE' then new.id else null end); return new; end; $$; drop trigger if exists items_before_write on projax.items; create trigger items_before_write before insert or update of slug, parent_id, kind on projax.items for each row execute function projax.items_before_write(); -- After update of slug or parent_id: recompute paths of all descendants. create or replace function projax.items_after_reparent() returns trigger language plpgsql as $$ begin if (tg_op = 'UPDATE') and (old.slug is distinct from new.slug or old.parent_id is distinct from new.parent_id) then -- Use recursive CTE to refresh descendant paths with recursive descendants as ( select id, parent_id, slug from projax.items where parent_id = new.id union all select i.id, i.parent_id, i.slug from projax.items i join descendants d on i.parent_id = d.id ) update projax.items i set path = projax.compute_item_path(i.parent_id, i.slug, i.id) from descendants d where i.id = d.id; end if; return null; end; $$; drop trigger if exists items_after_reparent on projax.items; create trigger items_after_reparent after update of slug, parent_id on projax.items for each row execute function projax.items_after_reparent();