129 lines
3.3 KiB
PL/PgSQL
129 lines
3.3 KiB
PL/PgSQL
-- 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;
|