← hierarchy-data-closure-table
Refactors
+881 −233 · 28 files changed
-
.gitignore+1 −0 -
README.md+36 −35 -
sql/tables.sql => mysql/create_tables.sql+0 −0 -
mysql/p_get_tree.sql+35 −0 -
mysql/p_node_add.sql+34 −0 -
mysql/p_node_hide.sql+21 −0 -
mysql/p_node_move.sql+47 −0 -
{sql => mysql}/sample_data.sql+0 −0 -
mysql/trigger_add_node.sql+14 −0 -
mysql/trigger_move_node.sql+22 −0 -
postgres/README.md+7 −0 -
postgres/add_new_paths_for_insert.sql+65 −0 -
postgres/add_node.sql+34 −0 -
postgres/categories_data.sql+65 −0 -
postgres/delete_subtree.sql+20 −0 -
postgres/get_child_nodes.sql+26 −0 -
postgres/get_subtree.sql+40 −0 -
postgres/json_output.sql+64 −0 -
postgres/move_subtree.sql+31 −0 -
postgres/postgresql_create_tables.sql+75 −0 -
postgres/table.sql+244 −0 -
sql/p_prefix_nodes_add_new_paths_after_insert.sql+0 −34 -
sql/p_prefix_nodes_delete_nodes.sql+0 −22 -
sql/p_prefix_nodes_delete_nodes_after_update.sql+0 −22 -
sql/p_prefix_nodes_get_subtree_by_node_id.sql+0 −35 -
sql/p_prefix_nodes_move_old_paths_after_update.sql+0 −46 -
sql/prefix_nodes_add_new_paths_after_insert.sql+0 −15 -
sql/prefix_nodes_move_old_paths_after_update.sql+0 −24
@@ -0,0 +1 @@ +.test @@ -1,7 +1,8 @@ -Closure Table -============= +# Closure Table -This is a mysql store procedure and trigger implementation of closure table in RDBMS about hierarchy data model. + +This is a mysql store procedure and trigger implementation of closure table in +RDBMS about hierarchy data model. <h3>Closure Table Node Paths</h3> @@ -11,60 +12,60 @@ This is a mysql store procedure and trigger implementation of closure table in R  +## Features + + +* Automatically add new paths when you insert a new node + +* Automatically update(`DELETE` old paths and `INSERT` new paths) paths when you +update `parent_id` of a node. (This means move a node/subtree to a new parent) + +* A store procedure that is used to select a whole subtree by a `node_id` +(if the `node_id` has descendant) -Features --------- -* Automatically add new paths relationship information between ancester and descendant nodes when you insert node to table `prefix_nodes` -* Automatically update(`DELETE` old paths and `INSERT` new paths) paths relationship information when you update the `parent_id` column of `prefix_nodes` table if the new value parent_id if different(This means move a node/subtree to a new parent node) -* A store procedure that is used to select a whole subtree by a `node_id` (if the `node_id` has descendant) +## Triggers +* `trigger_add_paths` -Triggers --------- +The trigger is execute when insert a node into `prefix_nodes` table, and call `p_node_add` to add update paths. -* `prefix_nodes_add_new_paths_after_insert` -The trigger is execute when insert a node into `prefix_nodes` table, and call `p_prefix_nodes_add_new_paths_after_insert` to add new paths info. +* `prefix_node_move`: -* `prefix_nodes_move_old_paths_after_update`: -The trigger is execute when update the `parent_id` column of `prefix_nodes` table only if `OLD.parent_id != NEW.parent_id` +The trigger is execute when update the `parent_id` column of `prefix_nodes` +table only if `OLD.parent_id != NEW.parent_id` -Store Procedures ----------------- +## Store Procedures -It's very clear of means just like the name of the procedures. -* `p_prefix_nodes_add_new_paths_after_insert(param_node_new_id INT UNSIGNED,param_node_parent_id INT UNSIGNED)` +* `p_node_add(param_node_new_id INT UNSIGNED,param_node_parent_id INT UNSIGNED)` Add new paths when insert a node to `prefix_nodes` table - -* `p_prefix_nodes_get_subtree_by_node_id(node_id INT UNSIGNED)` + +* `p_get_tree(node_id INT UNSIGNED)` Get subtree by a node id - -* `p_prefix_nodes_move_old_paths_after_update(node_old_parent_id INT UNSIGNED,node_new_parent_id INT UNSIGNED)` + +* `p_node_move(node_old_parent_id INT UNSIGNED,node_new_parent_id INT UNSIGNED)` Update paths when move a node to a new parent node - -* `p_prefix_nodes_delete_nodes(node_id INT UNSIGNED, is_deleted INT UNSIGNED)` - Hidden or show nodes from subtree, explains as following: +* `p_node_hide(node_id INT UNSIGNED, is_deleted INT UNSIGNED)` - - First `call p_prefix_nodes_get_subtree_by_node_id(6)` get a HARDWARE subtree, - - Second `call p_prefix_nodes_delete_nodes(6, 0)` to hidden subtree, - - Third `call p_prefix_nodes_get_subtree_by_node_id(6)` again get HARDWARE subtree, but this time the HARDWARE subtree was disappeared - - Fourth `call p_prefix_nodes_delete_nodes(6, 1)` show HARDWARE subtree + Hide or show nodes from subtree, explains as following: -Files ------ + - Step 1. `call p_get_tree(6)` get the `HARDWARE` subtree, + - Step 2. `call p_node_hide(6, 0)` to hide a subtree, + - Step 3. `call p_get_tree(6)` get the `HARDWARE` subtree, when you get a subtree, it is not show in the result. + - Step 4. `call p_node_hide(6, 1)` show `HARDWARE` subtree -* `sql/tables.sql` +## Files + +* `./sql/tables.sql` Create tables. -* `sql/sample_data.sql` +* `./sql/sample_data.sql` Some insert statements for testing - - @@ -0,0 +1,35 @@ +DELIMITER $$ + +USE `hierarchy_data`$$ + +DROP PROCEDURE IF EXISTS `p_get_tree`$$ + +CREATE PROCEDURE `p_get_tree`( + node_id INT UNSIGNED +) COMMENT 'Query all descendants nodes by a node id, return as a result set' +BEGIN + SELECT + d.`id`, + d.`is_deleted`, + d.`parent_id`, + CONCAT( + REPEAT('-', p.`path_length`), + d.`name` + ) AS tree, + p.`path_length`, + GROUP_CONCAT( + crumbs.`ancestor_id` SEPARATOR ',' + ) AS breadcrumbs + FROM + `prefix_nodes` AS d + JOIN `prefix_nodes_paths` AS p + ON d.`id` = p.`descendant_id` + JOIN `prefix_nodes_paths` AS crumbs + ON crumbs.`descendant_id` = p.`descendant_id` + WHERE p.`ancestor_id` = `node_id` + AND d.`is_deleted` = 0 + GROUP BY d.`id` + ORDER BY breadcrumbs ; +END$$ + +DELIMITER ; @@ -0,0 +1,34 @@ +DELIMITER $$ + +USE `hierarchy_data`$$ + +DROP PROCEDURE IF EXISTS `p_node_add`$$ + +CREATE PROCEDURE `p_node_add`( + param_node_new_id INT UNSIGNED, + param_node_parent_id INT UNSIGNED +) +COMMENT 'Adding new paths prefix_nodes_paths table' +BEGIN + -- Update paths information + INSERT INTO `prefix_nodes_paths` ( + `ancestor_id`, + `descendant_id`, + `path_length` + ) + SELECT + `ancestor_id`, + `param_node_new_id`, + `path_length` + 1 + FROM + `prefix_nodes_paths` + WHERE `descendant_id` = `param_node_parent_id` + UNION + ALL + SELECT + `param_node_new_id`, + `param_node_new_id`, + 0 ; +END$$ + +DELIMITER ; @@ -0,0 +1,21 @@ +DELIMITER $$ + +USE `hierarchy_data`$$ + +DROP PROCEDURE IF EXISTS `p_node_hide`$$ + +CREATE PROCEDURE `p_node_hide` ( + `node_id` INT UNSIGNED, + `deleted` INT UNSIGNED +) COMMENT 'Delete a node and its descendant nodes(update is_deleted = 1)' +BEGIN + UPDATE + `prefix_nodes` AS d + JOIN `prefix_nodes_paths` AS p + ON d.`id` = p.`descendant_id` + JOIN `prefix_nodes_paths` AS crumbs + ON crumbs.`descendant_id` = p.`descendant_id` SET d.`is_deleted` = deleted + WHERE p.`ancestor_id` = node_id; +END $$ + +DELIMITER ; @@ -0,0 +1,47 @@ +DELIMITER $$ + +USE `hierarchy_data`$$ + +DROP PROCEDURE IF EXISTS `p_node_move`$$ + +CREATE PROCEDURE `p_node_move` ( + `node_old_parent_id` INT UNSIGNED, + `node_new_parent_id` INT UNSIGNED +) COMMENT 'Update paths when parent_id column changed' +BEGIN +-- References: +-- http://www.mysqlperformanceblog.com/2011/02/14/moving-subtrees-in-closure-table/ +-- The store procedure is used to update paths informations when the value of parent_id columns is changed (when move a node to a new parent) +-- If parent_id has chanaged +-- 1. Delete the paths between moved node and old ancestors +-- 2. Add the paths between moved node and new ancestors + DELETE + a + FROM + `prefix_nodes_paths` AS a + JOIN `prefix_nodes_paths` AS d + ON a.`descendant_id` = d.`descendant_id` + LEFT JOIN `prefix_nodes_paths` AS x + ON x.`ancestor_id` = d.`ancestor_id` + AND x.`descendant_id` = a.`ancestor_id` + WHERE d.`ancestor_id` = `node_old_parent_id` + AND x.`ancestor_id` IS NULL ; + + -- Add the node to its new parent + INSERT `prefix_nodes_paths` ( + `ancestor_id`, + `descendant_id`, + `path_length` + ) + SELECT + supertree.`ancestor_id`, + subtree.`descendant_id`, + supertree.`path_length` + subtree.`path_length` + 1 + FROM + `prefix_nodes_paths` AS supertree + JOIN `prefix_nodes_paths` AS subtree + WHERE subtree.`ancestor_id` = `node_old_parent_id` + AND supertree.`descendant_id` = `node_new_parent_id` ; +END$$ + +DELIMITER ; @@ -0,0 +1,14 @@ +DELIMITER $$ + +USE `hierarchy_data`$$ + +DROP TRIGGER IF EXISTS `trigger_add_node`$$ + +CREATE + TRIGGER `trigger_add_node` AFTER INSERT ON `prefix_nodes` + FOR EACH ROW BEGIN + CALL p_node_add(NEW.`id`, NEW.`parent_id`); +END; +$$ + +DELIMITER ; @@ -0,0 +1,22 @@ +DELIMITER $$ + +USE `hierarchy_data`$$ + +DROP TRIGGER IF EXISTS `trigger_move_node`$$ + +CREATE TRIGGER `trigger_move_node` AFTER UPDATE ON `prefix_nodes` + FOR EACH ROW BEGIN + IF OLD.`parent_id` != NEW.`parent_id` THEN + -- http://www.mysqlperformanceblog.com/2011/02/14/moving-subtrees-in-closure-table/ + -- in the example, when change node D's parent to B. + -- its sql has only D & B. so I think it should currentNode.id& newParent.id + CALL p_node_move(NEW.`id`, NEW.`parent_id`); + END IF; + + IF OLD.`is_deleted` != NEW.`is_deleted` THEN + CALL p_node_hide(NEW.`parent_id`, NEW.`is_deleted`); + END IF; +END; +$$ + +DELIMITER ; @@ -0,0 +1,7 @@ +## + + +## References + + +[Building a nested hierarchy of JSON in a relational DB](https://lakshminp.com/building-nested-hierarchy-json-relational-db) @@ -0,0 +1,65 @@ +----------------------------------------------- +-- This function is used to update paths when +-- inserted a new node to a exist parent node. +----------------------------------------------- + +-- Drop function +DROP FUNCTION IF EXISTS after_change_node(); + +-- Step 1: Create a function used to update paths +CREATE OR REPLACE FUNCTION after_change_node() RETURNS TRIGGER LANGUAGE PLPGSQL AS $$ +BEGIN + IF (TG_OP = 'INSERT') THEN + INSERT INTO prefix_nodes_paths(ancestor_id,descendant_id,depth) + SELECT ancestor_id, NEW.id, depth + 1 + FROM prefix_nodes_paths WHERE descendant_id = NEW.parent_id + UNION ALL SELECT NEW.id, NEW.id, 0; + ELSE IF (TG_OP = 'UPDATE') THEN + -- Move Subtree + -- References: + -- https://codegists.com/snippet/sql/closure_treesql_x2002uwh_sql + IF OLD.parent_id != NEW.parent_id THEN + -- Step 1: Disconnect from current ancestors + DELETE FROM prefix_nodes_paths + WHERE descendant_id IN (SELECT descendant_id FROM prefix_nodes_paths WHERE ancestor_id = OLD.parent_id) + AND ancestor_id IN (SELECT ancestor_id FROM prefix_nodes_paths WHERE descendant_id = OLD.parent_id AND ancestor_id != descendant_id); + + -- Step 2: Insert rows matching ancestors of insertion point and descendants of subtree + INSERT INTO prefix_nodes_paths (ancestor, descendant) + SELECT supertree.ancestor, subtree.descendant + FROM prefix_nodes_paths AS supertree + CROSS JOIN prefix_nodes_paths AS subtree + WHERE supertree.descendant = 3 + AND subtree.ancestor = 6; + + + -- Add new paths + INSERT prefix_nodes_paths (ancestor_id, descendant_id, depth) + SELECT supertree.ancestor_id, subtree.descendant_id, supertree.path_length + subtree.path_length + 1 + FROM prefix_nodes_paths AS supertree + JOIN prefix_nodes_paths AS subtree + WHERE subtree.ancestor_id = OLD.parent_id + AND supertree.descendant_id = NEW.parent_id ; + + END IF; + END IF; + RETURN NULL; +END; +$$; + + +-- Add trigger function on prefix_nodes table +CREATE TRIGGER after_change_node AFTER INSERT OR UPDATE + ON prefix_nodes FOR EACH ROW EXECUTE PROCEDURE after_change_node(); + +-- +-- If you want modify the `after_change_node` function, +-- you must delete the trigger from `prefix_nodes` table +-- first, and re-create it. +-- + +-- Drop trigger function on a table +DROP TRIGGER IF EXISTS after_change_node ON prefix_nodes; + + + @@ -0,0 +1,34 @@ +----------------------------------------------- +-- Add a new node to existing parent node. +-- +-- If you want modify the `after_change_node` function, +-- you must delete the trigger from `prefix_nodes` table +-- first, and re-create it. +-- +----------------------------------------------- + +-- Drop trigger on a table +DROP TRIGGER IF EXISTS after_change_node ON prefix_nodes; + +-- Drop trigger function +DROP FUNCTION IF EXISTS after_change_node(); + +-- Step 1: Create a function used to update paths +CREATE OR REPLACE FUNCTION after_change_node() RETURNS TRIGGER LANGUAGE PLPGSQL AS $$ +BEGIN + IF (TG_OP = 'INSERT') THEN + INSERT INTO prefix_nodes_paths(ancestor_id,descendant_id,depth) + SELECT ancestor_id, NEW.id, depth + 1 + FROM prefix_nodes_paths WHERE descendant_id = NEW.parent_id + UNION ALL SELECT NEW.id, NEW.id, 0; + END IF; + RETURN NULL; +END; +$$; + +-- Add trigger function on prefix_nodes table +CREATE TRIGGER after_change_node AFTER INSERT OR UPDATE + ON prefix_nodes FOR EACH ROW EXECUTE PROCEDURE after_change_node(); + + + @@ -0,0 +1,65 @@ + +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, 'HOME', false, NULL); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, 'PRODUCT', false, 1); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, 'CONTACT', false, 1); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, 'DOCUMENTATION', false, 1); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, 'SOFTWARE', false, 2); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, 'HARDWARE', false, 2); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, 'DEMO', false, 1); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, 'JAVA', false, 4); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, 'PHP', false, 4); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, 'Asia', false, NULL); + +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, 'China', false, 10); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, 'Korea', false, 10); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, 'Japan', false, 10); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, 'CPU', false, 6); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, 'HARD DISK', false, 6); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, 'SSD', false, 15); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, 'HDD', false, 15); + + + +INSERT INTO prefix_nodes(id, "order", name, is_deleted, parent_id) VALUES('29f9d3a1-068f-4132-bf5e-4792add8489b', 0, 'Subtree Test', false, NULL); + +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, 'Child Node 1', false, '29f9d3a1-068f-4132-bf5e-4792add8489b'); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, 'Child Node 2', false, '29f9d3a1-068f-4132-bf5e-4792add8489b'); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, 'Child Node 3', false, '29f9d3a1-068f-4132-bf5e-4792add8489b'); + + +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, 'Child Node 3 - Child', false, '72850f82-7dab-45d9-8560-dc183abf3817'); + + +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, 'Child Node 3 - Child', false, '5efc1e96-6931-4310-ac09-ad791f22d7e3'); + + + +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, '春季焕新', false, 'e632b593-8b1b-4966-8427-3273f2d43f4c'); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, '商场同款', false, 'e632b593-8b1b-4966-8427-3273f2d43f4c'); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, '修身夹克', false, 'e632b593-8b1b-4966-8427-3273f2d43f4c'); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, '牛仔夹克', false, 'e632b593-8b1b-4966-8427-3273f2d43f4c'); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, '潮流卫衣', false, 'e632b593-8b1b-4966-8427-3273f2d43f4c'); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, '长袖衬衫', false, 'e632b593-8b1b-4966-8427-3273f2d43f4c'); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, '长袖T恤', false, 'e632b593-8b1b-4966-8427-3273f2d43f4c'); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, '针织开衫', false, 'e632b593-8b1b-4966-8427-3273f2d43f4c'); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, '单西', false, 'e632b593-8b1b-4966-8427-3273f2d43f4c'); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, '束脚裤', false, 'e632b593-8b1b-4966-8427-3273f2d43f4c'); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, '水洗牛仔裤', false, 'e632b593-8b1b-4966-8427-3273f2d43f4c'); + + + +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, '精选上装', false, 'a498140e-be81-494f-926f-b907614ac3fe'); + +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, '羽绒服', false, 'e462c5b8-3674-4a06-9abb-047e9cb4a0d8'); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, '毛呢外套', false, 'e462c5b8-3674-4a06-9abb-047e9cb4a0d8'); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, '毛衣', false, 'e462c5b8-3674-4a06-9abb-047e9cb4a0d8'); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, '针织衫', false, 'e462c5b8-3674-4a06-9abb-047e9cb4a0d8'); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, '衬衫', false, 'e462c5b8-3674-4a06-9abb-047e9cb4a0d8'); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, '风衣', false, 'e462c5b8-3674-4a06-9abb-047e9cb4a0d8'); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, '短外套', false, 'e462c5b8-3674-4a06-9abb-047e9cb4a0d8'); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, '卫衣', false, 'e462c5b8-3674-4a06-9abb-047e9cb4a0d8'); +INSERT INTO prefix_nodes("order", name, is_deleted, parent_id) VALUES(0, '小西装', false, 'e462c5b8-3674-4a06-9abb-047e9cb4a0d8'); + + + + @@ -0,0 +1,20 @@ +-------------------------------------------- +-- Delete a leaf +-------------------------------------------- + + + +-------------------------------------------- +-- Delete a subtree +-------------------------------------------- + +DROP FUNCTION IF EXISTS delete_subtree(INTEGER); + +CREATE FUNCTION delete_subtree(INTEGER) +RETURNS VOID +AS $$ + DELETE FROM prefix_nodes_paths + WHERE descendant_id IN ( + SELECT descendant_id FROM prefix_nodes_paths WHERE ancestor_id = $1 + ); +$$ LANGUAGE sql @@ -0,0 +1,26 @@ +---------------- +-- Get child nodes +---------------- + +DROP FUNCTION IF EXISTS get_child_nodes(INTEGER); + +-- Create function +CREATE FUNCTION get_child_nodes(INTEGER) + RETURNS SETOF RECORD + LANGUAGE SQL +AS $$ +SELECT d.id, + d.is_deleted, + d.parent_id, + concat(repeat('-', p.depth), d.name) AS tree, + p.depth, + array_to_string(array_agg(crumbs.ancestor_id::CHARACTER VARYING ORDER BY crumbs.ancestor_id),',','*') breadcrumbs + FROM prefix_nodes AS d + JOIN prefix_nodes_paths AS p ON d.id = p.descendant_id + JOIN prefix_nodes_paths AS crumbs ON crumbs.descendant_id = p.descendant_id + WHERE p.ancestor_id = $1 AND d.is_deleted = false + GROUP BY d.id, p.depth + ORDER BY d.id ASC + ; +$$ + @@ -0,0 +1,40 @@ +----------------------------------------------------------------------- +-- Get child nodes +----------------------------------------------------------------------- + + +DROP FUNCTION IF EXISTS get_child_nodes(UUID); + +-- Create function +CREATE FUNCTION get_child_nodes(UUID) + RETURNS SETOF RECORD +AS $$ +SELECT d.id, + d.is_deleted, + d.parent_id, + concat(repeat('-', p.depth), d.name) AS name, + p.depth, + array_to_string(array_agg(crumbs.ancestor_id::CHARACTER VARYING),',','*') breadcrumbs + FROM prefix_nodes AS d + JOIN prefix_nodes_paths AS p ON d.id = p.descendant_id + JOIN prefix_nodes_paths AS crumbs ON crumbs.descendant_id = p.descendant_id + WHERE p.ancestor_id = $1 AND d.is_deleted = false + GROUP BY d.id, p.depth + ORDER BY d.id ASC + ; +$$ LANGUAGE SQL; + + + +----------------------------------------------------------------------- +-- Get subtree(Depend on `get_child_nodes` user defined function above) +-- (include node itself, such as following statement, node #2) +-- TODO +-- 1. Add a switch(boolean value) if include node itself +----------------------------------------------------------------------- + + +SELECT * FROM get_child_nodes('29f9d3a1-068f-4132-bf5e-4792add8489b') + AS (id UUID,is_deleted BOOLEAN,parent_id UUID,name VARCHAR(255),depth INTEGER,breadcrumbs VARCHAR(255)) + ORDER BY breadcrumbs; + @@ -0,0 +1,64 @@ +------------------------------------------ +-- Get tree +------------------------------------------ + +DROP FUNCTION IF EXISTS get_tree(data json); + +CREATE OR REPLACE FUNCTION get_tree(data json) +RETURNS json +AS $$ +var root = []; +for(var i in data) { + build_tree(data[i]['id'], data[i]['name'], data[i]['children']); +} +function build_tree(id, name, children) { + var exists = getObject(root, id); + if(exists) { + exists['children'] = children; + } + else { + root.push({'id': id, 'name': name, 'children': children, 'toggled': true}); + } +} +function getObject(theObject, id) { + var result = null; + if(theObject instanceof Array) { + for(var i = 0; i < theObject.length; i++) { + result = getObject(theObject[i], id); + if (result) { + break; + } + } + } + else + { + theObject['toggled'] = true; + for(var prop in theObject) { + if(prop == 'id') { + if(theObject[prop] === id) { + return theObject; + } + } + if(theObject[prop] instanceof Object || theObject[prop] instanceof Array) { + result = getObject(theObject[prop], id); + if (result) { + break; + } + } + } + } + return result; +} +return JSON.stringify(root); +$$ LANGUAGE plv8 IMMUTABLE STRICT; + +------------------------------------------ +-- Format to JSON Object(The Whole Trees) +------------------------------------------ + +WITH data AS( +SELECT array_to_json(array_agg(row_to_json(t))) AS data + FROM ( + SELECT id, name, COALESCE(get_children_by_uuid(id), '[]') AS children, 'true' FROM prefix_nodes + ) t +) SELECT get_tree(data) FROM data; @@ -0,0 +1,31 @@ +DROP FUNCTION IF EXISTS move(UUID, UUID); + +CREATE FUNCTION move( + UUID, + UUID +) RETURNS VOID +AS $$ +-------------------------------------------- +-- Step 1: Update parent node id +-------------------------------------------- +UPDATE prefix_nodes SET parent_id = $2 WHERE id = $1; + +-------------------------------------------- +-- Step 2: Disconnect from current ancestors +-- Delete all paths that end at descendants in the subtree +-------------------------------------------- +DELETE FROM prefix_nodes_paths +WHERE descendant_id IN (SELECT descendant_id FROM prefix_nodes_paths WHERE ancestor_id = $1) +AND ancestor_id IN (SELECT ancestor_id FROM prefix_nodes_paths WHERE descendant_id = $1 AND ancestor_id != descendant_id); + +-------------------------------------------- +-- Step 2: Mount subtree to new ancestors +-- Insert rows matching ancestors of insertion point and descendants of subtree +-------------------------------------------- +INSERT INTO prefix_nodes_paths (ancestor_id, descendant_id, depth) +SELECT supertree.ancestor_id, subtree.descendant_id, supertree.depth + subtree.depth + 1 +FROM prefix_nodes_paths AS supertree +CROSS JOIN prefix_nodes_paths AS subtree +WHERE supertree.descendant_id = $2 +AND subtree.ancestor_id = $1; +$$ LANGUAGE SQL; @@ -0,0 +1,75 @@ +-- Table: public.prefix_nodes + +-- DROP TABLE public.prefix_nodes; + +CREATE TABLE public.prefix_nodes +( + id integer NOT NULL DEFAULT nextval('prefix_nodes_id_seq'::regclass), + parent_id integer, + name character varying(255), + "order" integer, + is_deleted boolean NOT NULL DEFAULT false, + CONSTRAINT prefix_nodes_pkey1 PRIMARY KEY (id), + CONSTRAINT prefix_nodes_parent_id_fkey FOREIGN KEY (parent_id) + REFERENCES public.prefix_nodes (id) MATCH SIMPLE + ON UPDATE NO ACTION ON DELETE CASCADE +) +WITH ( + OIDS=FALSE +); +ALTER TABLE public.prefix_nodes + OWNER TO postgres; + +-- Index: public.prefix_nodes_parent_id_index + +-- DROP INDEX public.prefix_nodes_parent_id_index; + +CREATE INDEX prefix_nodes_parent_id_index + ON public.prefix_nodes + USING btree + (parent_id); + + +-- Table: public.prefix_nodes_paths + +-- DROP TABLE public.prefix_nodes_paths; + +CREATE TABLE public.prefix_nodes_paths +( + id integer NOT NULL DEFAULT nextval('prefix_nodes_paths_id_seq'::regclass), + ancestor_id integer, + descendant_id integer, + depth integer, + CONSTRAINT prefix_nodes_paths_pkey PRIMARY KEY (id), + CONSTRAINT prefix_nodes_paths_ancestor_id_fkey FOREIGN KEY (ancestor_id) + REFERENCES public.prefix_nodes (id) MATCH SIMPLE + ON UPDATE NO ACTION ON DELETE CASCADE, + CONSTRAINT prefix_nodes_paths_descendant_id_fkey FOREIGN KEY (descendant_id) + REFERENCES public.prefix_nodes (id) MATCH SIMPLE + ON UPDATE NO ACTION ON DELETE CASCADE +) +WITH ( + OIDS=FALSE +); +ALTER TABLE public.prefix_nodes_paths + OWNER TO postgres; + +-- Index: public.prefix_nodes_paths_ancestor_id_index + +-- DROP INDEX public.prefix_nodes_paths_ancestor_id_index; + +CREATE INDEX prefix_nodes_paths_ancestor_id_index + ON public.prefix_nodes_paths + USING btree + (ancestor_id); + +-- Index: public.prefix_nodes_paths_descendant_id_index + +-- DROP INDEX public.prefix_nodes_paths_descendant_id_index; + +CREATE INDEX prefix_nodes_paths_descendant_id_index + ON public.prefix_nodes_paths + USING btree + (descendant_id); + + @@ -0,0 +1,244 @@ +-- Table: public.prefix_nodes + +-- DROP TABLE public.prefix_nodes; + +CREATE TABLE public.prefix_nodes +( + id uuid NOT NULL DEFAULT gen_random_uuid(), + "order" integer, + name character varying(255), + is_deleted boolean NOT NULL DEFAULT false, + parent_id uuid, + CONSTRAINT prefix_nodes_pkey PRIMARY KEY (id), + CONSTRAINT prefix_nodes_parent_id_fkey FOREIGN KEY (parent_id) + REFERENCES public.prefix_nodes (id) MATCH SIMPLE + ON UPDATE NO ACTION ON DELETE CASCADE +) +WITH ( + OIDS=FALSE +); +ALTER TABLE public.prefix_nodes + OWNER TO postgres; + +-- Index: public.prefix_nodes_parent_id_index + +-- DROP INDEX public.prefix_nodes_parent_id_index; + +CREATE INDEX prefix_nodes_parent_id_index + ON public.prefix_nodes + USING btree + (parent_id); + + +-- Trigger: after_change_node on public.prefix_nodes + +-- DROP TRIGGER after_change_node ON public.prefix_nodes; + +CREATE TRIGGER after_change_node + AFTER INSERT OR UPDATE + ON public.prefix_nodes + FOR EACH ROW + EXECUTE PROCEDURE public.after_change_node(); + +------------------------------------------------------------------------------------------------------ + +-- Table: public.prefix_nodes_paths + +-- DROP TABLE public.prefix_nodes_paths; + +CREATE TABLE public.prefix_nodes_paths +( + ancestor_id uuid NOT NULL, + descendant_id uuid NOT NULL, + depth integer, + CONSTRAINT prefix_nodes_paths_pkey PRIMARY KEY (ancestor_id, descendant_id), + CONSTRAINT prefix_nodes_paths_ancestor_id_fkey FOREIGN KEY (ancestor_id) + REFERENCES public.prefix_nodes (id) MATCH SIMPLE + ON UPDATE NO ACTION ON DELETE CASCADE, + CONSTRAINT prefix_nodes_paths_descendant_id_fkey FOREIGN KEY (descendant_id) + REFERENCES public.prefix_nodes (id) MATCH SIMPLE + ON UPDATE NO ACTION ON DELETE CASCADE +) +WITH ( + OIDS=FALSE +); +ALTER TABLE public.prefix_nodes_paths + OWNER TO postgres; + +-- Index: public.prefix_nodes_paths_ancestor_id_index + +-- DROP INDEX public.prefix_nodes_paths_ancestor_id_index; + +CREATE INDEX prefix_nodes_paths_ancestor_id_index + ON public.prefix_nodes_paths + USING btree + (ancestor_id); + +-- Index: public.prefix_nodes_paths_descendant_id_index + +-- DROP INDEX public.prefix_nodes_paths_descendant_id_index; + +CREATE INDEX prefix_nodes_paths_descendant_id_index + ON public.prefix_nodes_paths + USING btree + (descendant_id); + + +------------------------------------------------------------------------------------------------------ + + +-- Function: public.get_child_nodes(uuid) + +-- DROP FUNCTION public.get_child_nodes(uuid); + +CREATE OR REPLACE FUNCTION public.get_child_nodes(uuid) + RETURNS SETOF record AS +$BODY$ +SELECT d.id, + d.is_deleted, + d.parent_id, + concat(repeat('-', p.depth), d.name) AS name, + p.depth, + array_to_string(array_agg(crumbs.ancestor_id::CHARACTER VARYING),',','*') breadcrumbs + FROM prefix_nodes AS d + JOIN prefix_nodes_paths AS p ON d.id = p.descendant_id + JOIN prefix_nodes_paths AS crumbs ON crumbs.descendant_id = p.descendant_id + WHERE p.ancestor_id = $1 AND d.is_deleted = false + GROUP BY d.id, p.depth + ORDER BY d.id ASC + ; +$BODY$ + LANGUAGE sql VOLATILE + COST 100 + ROWS 1000; +ALTER FUNCTION public.get_child_nodes(uuid) + OWNER TO postgres; + + +------------------------------------------------------------------------------------------------------ + + +-- Function: public.get_children_by_uuid(uuid) + +-- DROP FUNCTION public.get_children_by_uuid(uuid); + +CREATE OR REPLACE FUNCTION public.get_children_by_uuid(uuid) + RETURNS json AS +$BODY$ +DECLARE + result json; +BEGIN +SELECT array_to_json(array_agg(row_to_json(t))) INTO result -- inject output into result variable +FROM ( -- same CTE as above + WITH RECURSIVE genres_materialized_path AS ( + SELECT id, name, ARRAY[]::UUID[] AS path + FROM prefix_nodes WHERE parent_id IS NULL + + UNION ALL + + SELECT prefix_nodes.id, prefix_nodes.name, genres_materialized_path.path || prefix_nodes.parent_id::UUID + FROM prefix_nodes, genres_materialized_path + WHERE prefix_nodes.parent_id = genres_materialized_path.id + ) SELECT id, name, ARRAY[]::UUID[] AS children + FROM genres_materialized_path WHERE $1 = genres_materialized_path.path[array_upper(genres_materialized_path.path,1)] -- some column polish for a cleaner JSON +) t; +RETURN result; +END; +$BODY$ + LANGUAGE plpgsql VOLATILE + COST 100; +ALTER FUNCTION public.get_children_by_uuid(uuid) + OWNER TO postgres; + + + +------------------------------------------------------------------------------------------------------ + + + +-- Function: public.get_tree(json) + +-- DROP FUNCTION public.get_tree(json); + +CREATE OR REPLACE FUNCTION public.get_tree(data json) + RETURNS json AS +$BODY$ +var root = []; +for(var i in data) { + build_tree(data[i]['id'], data[i]['name'], data[i]['children']); +} +function build_tree(id, name, children) { + var exists = getObject(root, id); + if(exists) { + exists['children'] = children; + } + else { + root.push({'id': id, 'name': name, 'children': children}); + } +} +function getObject(theObject, id) { + var result = null; + if(theObject instanceof Array) { + for(var i = 0; i < theObject.length; i++) { + result = getObject(theObject[i], id); + if (result) { + break; + } + } + } + else + { + for(var prop in theObject) { + if(prop == 'id') { + if(theObject[prop] === id) { + return theObject; + } + } + if(theObject[prop] instanceof Object || theObject[prop] instanceof Array) { + result = getObject(theObject[prop], id); + if (result) { + break; + } + } + } + } + return result; +} +return JSON.stringify(root); +$BODY$ + LANGUAGE plv8 IMMUTABLE STRICT + COST 100; +ALTER FUNCTION public.get_tree(json) + OWNER TO postgres; + + +------------------------------------------------------------------------------------------------------ + + + +-- Function: public.move(uuid, uuid) + +-- DROP FUNCTION public.move(uuid, uuid); + +CREATE OR REPLACE FUNCTION public.move( + uuid, + uuid) + RETURNS void AS +$BODY$ +UPDATE prefix_nodes SET parent_id = $2 WHERE id = $1; + +DELETE FROM prefix_nodes_paths +WHERE descendant_id IN (SELECT descendant_id FROM prefix_nodes_paths WHERE ancestor_id = $1) +AND ancestor_id IN (SELECT ancestor_id FROM prefix_nodes_paths WHERE descendant_id = $1 AND ancestor_id != descendant_id); + +INSERT INTO prefix_nodes_paths (ancestor_id, descendant_id, depth) +SELECT supertree.ancestor_id, subtree.descendant_id, supertree.depth + subtree.depth + 1 +FROM prefix_nodes_paths AS supertree +CROSS JOIN prefix_nodes_paths AS subtree +WHERE supertree.descendant_id = $2 +AND subtree.ancestor_id = $1; +$BODY$ + LANGUAGE sql VOLATILE + COST 100; +ALTER FUNCTION public.move(uuid, uuid) + OWNER TO postgres; @@ -1,34 +0,0 @@ -DELIMITER $$ - -USE `_category`$$ - -DROP PROCEDURE IF EXISTS `p_prefix_nodes_add_new_paths_after_insert`$$ - -CREATE DEFINER=`root`@`localhost` PROCEDURE `p_prefix_nodes_add_new_paths_after_insert`( - param_node_new_id INT UNSIGNED, - param_node_parent_id INT UNSIGNED -) - COMMENT 'Adding new paths information to prefix_nodes_paths table' -BEGIN - -- Update paths information - INSERT INTO `prefix_nodes_paths` ( - `ancestor_id`, - `descendant_id`, - `path_length` - ) - SELECT - `ancestor_id`, - `param_node_new_id`, - `path_length` + 1 - FROM - `prefix_nodes_paths` - WHERE `descendant_id` = `param_node_parent_id` - UNION - ALL - SELECT - `param_node_new_id`, - `param_node_new_id`, - 0 ; -END$$ - -DELIMITER ; @@ -1,22 +0,0 @@ -DELIMITER $$ - -USE `_category`$$ - -DROP PROCEDURE IF EXISTS `p_prefix_nodes_delete_nodes_after_update`$$ - -CREATE DEFINER = `root` @`localhost` PROCEDURE `p_prefix_nodes_delete_nodes_after_update` ( - `node_id` INT UNSIGNED, - `deleted` INT UNSIGNED -) COMMENT 'Delete a node and its descendant nodes(update is_deleted = 1)' -BEGIN - UPDATE - `prefix_nodes` AS d - JOIN `prefix_nodes_paths` AS p - ON d.`id` = p.`descendant_id` - JOIN `prefix_nodes_paths` AS crumbs - ON crumbs.`descendant_id` = p.`descendant_id` SET d.`is_deleted` = deleted - WHERE p.`ancestor_id` = node_id; -END $$ - -DELIMITER ; - @@ -1,22 +0,0 @@ -DELIMITER $$ - -USE `_category`$$ - -DROP PROCEDURE IF EXISTS `p_prefix_nodes_delete_nodes_after_update`$$ - -CREATE DEFINER = `root` @`localhost` PROCEDURE `p_prefix_nodes_delete_nodes_after_update` ( - `node_id` INT UNSIGNED, - `deleted` INT UNSIGNED -) COMMENT 'Delete a node and its descendant nodes(update is_deleted = 1)' -BEGIN - UPDATE - `prefix_nodes` AS d - JOIN `prefix_nodes_paths` AS p - ON d.`id` = p.`descendant_id` - JOIN `prefix_nodes_paths` AS crumbs - ON crumbs.`descendant_id` = p.`descendant_id` SET d.`is_deleted` = deleted - WHERE p.`ancestor_id` = node_id; -END $$ - -DELIMITER ; - @@ -1,35 +0,0 @@ -DELIMITER $$ - -USE `_category`$$ - -DROP PROCEDURE IF EXISTS `p_prefix_nodes_get_subtree_by_node_id`$$ - -CREATE DEFINER=`root`@`localhost` PROCEDURE `p_prefix_nodes_get_subtree_by_node_id`( - node_id INT UNSIGNED -) COMMENT 'Query all descendants nodes by a node id, return as a result set' -BEGIN - SELECT - d.`id`, - d.`is_deleted`, - d.`parent_id`, - CONCAT( - REPEAT('-', p.`path_length`), - d.`name` - ) AS tree, - p.`path_length`, - GROUP_CONCAT( - crumbs.`ancestor_id` SEPARATOR ',' - ) AS breadcrumbs - FROM - `prefix_nodes` AS d - JOIN `prefix_nodes_paths` AS p - ON d.`id` = p.`descendant_id` - JOIN `prefix_nodes_paths` AS crumbs - ON crumbs.`descendant_id` = p.`descendant_id` - WHERE p.`ancestor_id` = `node_id` - AND d.`is_deleted` = 0 - GROUP BY d.`id` - ORDER BY breadcrumbs ; -END$$ - -DELIMITER ; @@ -1,46 +0,0 @@ -DELIMITER $$ - -USE `_category`$$ - -DROP PROCEDURE IF EXISTS `p_prefix_nodes_move_old_paths_after_update`$$ - -CREATE DEFINER = `root` @`localhost` PROCEDURE `p_prefix_nodes_move_old_paths_after_update` ( - `node_old_parent_id` INT UNSIGNED, - `node_new_parent_id` INT UNSIGNED -) COMMENT 'Update paths when parent_id column changed' -BEGIN - -- References: - -- http://www.mysqlperformanceblog.com/2011/02/14/moving-subtrees-in-closure-table/ - -- The store procedure is used to update paths informations when the value of parent_id columns is changed (when move a node to a new parent) - -- If parent_id has chanaged - -- 1. Delete the paths between moved node and old ancestors - -- 2. Add the paths between moved node and new ancestors - DELETE - a - FROM - `prefix_nodes_paths` AS a - JOIN `prefix_nodes_paths` AS d - ON a.`descendant_id` = d.`descendant_id` - LEFT JOIN `prefix_nodes_paths` AS x - ON x.`ancestor_id` = d.`ancestor_id` - AND x.`descendant_id` = a.`ancestor_id` - WHERE d.`ancestor_id` = `node_old_parent_id` - AND x.`ancestor_id` IS NULL ; - -- Add the node to its new parent - INSERT `prefix_nodes_paths` ( - `ancestor_id`, - `descendant_id`, - `path_length` - ) - SELECT - supertree.`ancestor_id`, - subtree.`descendant_id`, - supertree.`path_length` + subtree.`path_length` + 1 - FROM - `prefix_nodes_paths` AS supertree - JOIN `prefix_nodes_paths` AS subtree - WHERE subtree.`ancestor_id` = `node_old_parent_id` - AND supertree.`descendant_id` = `node_new_parent_id` ; -END$$ - -DELIMITER ; @@ -1,15 +0,0 @@ -DELIMITER $$ - -USE `_category`$$ - -DROP TRIGGER /*!50032 IF EXISTS */ `prefix_nodes_add_new_paths_after_insert`$$ - -CREATE - /*!50017 DEFINER = 'root'@'localhost' */ - TRIGGER `prefix_nodes_add_new_paths_after_insert` AFTER INSERT ON `prefix_nodes` - FOR EACH ROW BEGIN - CALL p_prefix_nodes_add_new_paths_after_insert(NEW.`id`, NEW.`parent_id`); -END; -$$ - -DELIMITER ; @@ -1,24 +0,0 @@ -DELIMITER $$ - -USE `_category`$$ - -DROP TRIGGER /*!50032 IF EXISTS */ `prefix_nodes_move_old_paths_after_update`$$ - -CREATE - /*!50017 DEFINER = 'root'@'localhost' */ - TRIGGER `prefix_nodes_move_old_paths_after_update` AFTER UPDATE ON `prefix_nodes` - FOR EACH ROW BEGIN - IF OLD.`parent_id` != NEW.`parent_id` THEN - --http://www.mysqlperformanceblog.com/2011/02/14/moving-subtrees-in-closure-table/ - --in the example, when change node D's parent to B. - --its sql has only D & B. so I think it should currentNode.id& newParent.id - CALL p_prefix_nodes_move_old_paths_after_update(NEW.`id`, NEW.`parent_id`); - END IF; - - IF OLD.`is_deleted` != NEW.`is_deleted` THEN - CALL p_prefix_nodes_delete_nodes_after_update(NEW.`parent_id`, NEW.`is_deleted`); - END IF; -END; -$$ - -DELIMITER ;