dukcapil/migrations/000035_add_parent_department_id.up.sql
2025-09-20 16:31:22 +07:00

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;