-- 1) Schema changes ALTER TABLE departments ADD COLUMN parent_department_id UUID REFERENCES departments(id) ON DELETE CASCADE; CREATE INDEX idx_departments_parent_id ON departments(parent_department_id); -- (Optional but recommended for ltree lookups) -- CREATE EXTENSION IF NOT EXISTS ltree; -- CREATE INDEX idx_departments_path_gist ON departments USING GIST (path); -- CREATE INDEX idx_departments_path_nlevel ON departments ((nlevel(path))); -- 2) Migrate parent ids from existing ltree path -- Use ltree helpers: nlevel() and subpath() UPDATE departments d1 SET parent_department_id = d2.id FROM departments d2 WHERE nlevel(d1.path) > 1 AND d2.path = subpath(d1.path, 0, nlevel(d1.path) - 1); -- 3) Guard against cycles/self-parent via trigger CREATE OR REPLACE FUNCTION check_department_hierarchy() RETURNS TRIGGER AS $$ DECLARE current_id UUID; max_depth INT := 100; depth INT := 0; BEGIN IF NEW.parent_department_id IS NULL THEN RETURN NEW; END IF; -- self-reference IF NEW.parent_department_id = NEW.id THEN RAISE EXCEPTION 'Department cannot be its own parent'; END IF; -- walk up the tree current_id := NEW.parent_department_id; WHILE current_id IS NOT NULL AND depth < max_depth LOOP IF current_id = NEW.id THEN RAISE EXCEPTION 'Circular reference detected in department hierarchy'; END IF; SELECT parent_department_id INTO current_id FROM departments WHERE id = current_id; depth := depth + 1; END LOOP; IF depth >= max_depth THEN RAISE EXCEPTION 'Department hierarchy too deep (max: %)', max_depth; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS check_department_hierarchy_trigger ON departments; CREATE TRIGGER check_department_hierarchy_trigger BEFORE INSERT OR UPDATE OF parent_department_id ON departments FOR EACH ROW EXECUTE FUNCTION check_department_hierarchy(); -- 4) Helper to rebuild dotted text path from codes (top.down.leaf) CREATE OR REPLACE FUNCTION get_department_hierarchy_path(dept_id UUID) RETURNS TEXT AS $$ DECLARE path_text TEXT := ''; current_dept RECORD; current_id UUID := dept_id; BEGIN WHILE current_id IS NOT NULL LOOP SELECT id, name, parent_department_id, code INTO current_dept FROM departments WHERE id = current_id; IF current_dept IS NULL THEN EXIT; END IF; IF path_text = '' THEN path_text := current_dept.code; ELSE path_text := current_dept.code || '.' || path_text; END IF; current_id := current_dept.parent_department_id; END LOOP; RETURN path_text; END; $$ LANGUAGE plpgsql; -- 5) View for easy hierarchy queries CREATE OR REPLACE VIEW department_hierarchy AS WITH RECURSIVE dept_tree AS ( -- roots SELECT id, name, code, parent_department_id, path, 0 AS level, ARRAY[id] AS hierarchy_ids, ARRAY[name] AS hierarchy_names FROM departments WHERE parent_department_id IS NULL UNION ALL -- children SELECT d.id, d.name, d.code, d.parent_department_id, d.path, dt.level + 1, dt.hierarchy_ids || d.id, dt.hierarchy_names || d.name FROM departments d JOIN dept_tree dt ON d.parent_department_id = dt.id ) SELECT * FROM dept_tree;