I\'d need a MySQL query that moves a node and all its children within a nested set. I found this site, but that function just seems so illogical - there\'s no universe
I know this is an old question, but I've just used the answer myself but for SQL Server. Should anyone want it, here is the code for a SQL Server Stored Proc based on the accepted answer.
CREATE PROCEDURE [dbo].[Item_Move]
@id uniqueidentifier,
@destinationId uniqueidentifier
AS
BEGIN
SET NOCOUNT ON;
declare @moverLeft int,
@moverRight int,
@destinationRight int,
@node_size int
-- step 0: Initialize parameters.
SELECT
@moverLeft = leftExtent,
@moverRight = rightExtent
FROM
Item
WHERE
id = @id
SELECT
@destinationRight = rightExtent
FROM
Item
WHERE
id = @destinationId
SELECT
@node_size = @moverRight - @moverLeft + 1; -- 'size' of moving node (including all it's sub nodes)
-- step 1: temporary "remove" moving node
UPDATE Item
SET leftExtent = 0-(leftExtent), rightExtent = 0-(rightExtent), updatedDate = GETDATE()
WHERE leftExtent >= @moverLeft AND rightExtent <= @moverRight;
-- step 2: decrease left and/or right position values of currently 'lower' items (and parents)
UPDATE Item
SET leftExtent = leftExtent - @node_size, updatedDate = GETDATE()
WHERE leftExtent > @moverRight;
UPDATE Item
SET rightExtent = rightExtent - @node_size, updatedDate = GETDATE()
WHERE rightExtent > @moverRight;
-- step 3: increase left and/or right position values of future 'lower' items (and parents)
UPDATE Item
SET leftExtent = leftExtent + @node_size, updatedDate = GETDATE()
WHERE leftExtent >= CASE WHEN @destinationRight > @moverRight THEN @destinationRight - @node_size ELSE @destinationRight END;
UPDATE Item
SET rightExtent = rightExtent + @node_size, updatedDate = GETDATE()
WHERE rightExtent >= CASE WHEN @destinationRight > @moverRight THEN @destinationRight - @node_size ELSE @destinationRight END;
-- step 4: move node (and it's subnodes) and update it's parent item id
UPDATE Item
SET
leftExtent = 0-(leftExtent) + CASE WHEN @destinationRight > @moverRight THEN @destinationRight - @moverRight - 1 ELSE @destinationRight - @moverRight - 1 + @node_size END,
rightExtent = 0-(rightExtent) + CASE WHEN @destinationRight > @moverRight THEN @destinationRight - @moverRight - 1 ELSE @destinationRight - @moverRight - 1 + @node_size END,
updatedDate = GETDATE()
WHERE leftExtent <= 0-@moverLeft AND rightExtent >= 0-@moverRight;
UPDATE Item
SET parentId = @destinationId, updatedDate = GETDATE()
WHERE id = @id;
END
Moving subtrees around is very expensive and complex in the Nested Sets design.
You should consider a different design for representing trees.
For example, if you use the Path Enumeration design, you store the list of direct ancestors of each node as a concatenated string.
id path
1 1/
2 1/2/
3 1/3/
4 1/3/4/
5 1/3/5/
Then moving a subtree (say node 3 moves to be a child of node 2):
UPDATE Tree t
JOIN Tree node2 ON (node2.id = 2)
JOIN Tree node3 ON (node3.id = 3)
SET t.path = CONCAT(node2.path, REPLACE(t.path, node3.path, node2.path))
WHERE t.path LIKE CONCAT(node3.path, '%');
I see, that this topic is quite old, but anyway it's still unanswered. I got here from Google, and found no direct answer to this question.
So, after a little research I found quite easy solution.
Everything, what we gonna need to move our node is: node left and right positions, new parent node right position. The node to the new position then can be moved in four easy steps:
That's theory, now - this algorithm realization in MySQL (example using PHP):
-- step 0: Initialize parameters.
SELECT
@node_id := 1, --put there id of moving node
@node_pos_left := 0, --put there left position of moving node
@node_pos_right := 1, --put there right position of moving node
@parent_id := 2, --put there id of new parent node (there moving node should be moved)
@parent_pos_right := 4; --put there right position of new parent node (there moving node should be moved)
SELECT
@node_size := @node_pos_right - @node_pos_left + 1; -- 'size' of moving node (including all it's sub nodes)
-- step 1: temporary "remove" moving node
UPDATE `list_items`
SET `pos_left` = 0-(`pos_left`), `pos_right` = 0-(`pos_right`)
WHERE `pos_left` >= @node_pos_left AND `pos_right` <= @node_pos_right;
-- step 2: decrease left and/or right position values of currently 'lower' items (and parents)
UPDATE `list_items`
SET `pos_left` = `pos_left` - @node_size
WHERE `pos_left` > @node_pos_right;
UPDATE `list_items`
SET `pos_right` = `pos_right` - @node_size
WHERE `pos_right` > @node_pos_right;
-- step 3: increase left and/or right position values of future 'lower' items (and parents)
UPDATE `list_items`
SET `pos_left` = `pos_left` + @node_size
WHERE `pos_left` >= IF(@parent_pos_right > @node_pos_right, @parent_pos_right - @node_size, @parent_pos_right);
UPDATE `list_items`
SET `pos_right` = `pos_right` + @node_size
WHERE `pos_right` >= IF(@parent_pos_right > @node_pos_right, @parent_pos_right - @node_size, @parent_pos_right);
-- step 4: move node (ant it's subnodes) and update it's parent item id
UPDATE `list_items`
SET
`pos_left` = 0-(`pos_left`)+IF(@parent_pos_right > @node_pos_right, @parent_pos_right - @node_pos_right - 1, @parent_pos_right - @node_pos_right - 1 + @node_size),
`pos_right` = 0-(`pos_right`)+IF(@parent_pos_right > @node_pos_right, @parent_pos_right - @node_pos_right - 1, @parent_pos_right - @node_pos_right - 1 + @node_size)
WHERE `pos_left` <= 0-@node_pos_left AND `pos_right` >= 0-@node_pos_right;
UPDATE `list_items`
SET `parent_item_id` = @parent_id
WHERE `item_id` = @node_id;
Please beware - there still may be some syntax errors in SQL code, because I actually use this algorithm in PHP like this:
$iItemId = 1;
$iItemPosLeft = 0;
$iItemPosRight = 1;
$iParentId = 2;
$iParentPosRight = 4;
$iSize = $iPosRight - $iPosLeft + 1;
$sql = array(
// step 1: temporary "remove" moving node
'UPDATE `list_items`
SET `pos_left` = 0-(`pos_left`), `pos_right` = 0-(`pos_right`)
WHERE `pos_left` >= "'.$iItemPosLeft.'" AND `pos_right` <= "'.$iItemPosRight.'"',
// step 2: decrease left and/or right position values of currently 'lower' items (and parents)
'UPDATE `list_items`
SET `pos_left` = `pos_left` - '.$iSize.'
WHERE `pos_left` > "'.$iItemPosRight.'"',
'UPDATE `list_items`
SET `pos_right` = `pos_right` - '.$iSize.'
WHERE `pos_right` > "'.$iItemPosRight.'"',
// step 3: increase left and/or right position values of future 'lower' items (and parents)
'UPDATE `list_items`
SET `pos_left` = `pos_left` + '.$iSize.'
WHERE `pos_left` >= "'.($iParentPosRight > $iItemPosRight ? $iParentPosRight - $iSize : $iParentPosRight).'"',
'UPDATE `list_items`
SET `pos_right` = `pos_right` + '.$iSize.'
WHERE `pos_right` >= "'.($iParentPosRight > $iItemPosRight ? $iParentPosRight - $iSize : $iParentPosRight).'"',
// step 4: move node (ant it's subnodes) and update it's parent item id
'UPDATE `list_items`
SET
`pos_left` = 0-(`pos_left`)+'.($iParentPosRight > $iItemPosRight ? $iParentPosRight - $iItemPosRight - 1 : $iParentPosRight - $iItemPosRight - 1 + $iSize).',
`pos_right` = 0-(`pos_right`)+'.($iParentPosRight > $iItemPosRight ? $iParentPosRight - $iItemPosRight - 1 : $iParentPosRight - $iItemPosRight - 1 + $iSize).'
WHERE `pos_left` <= "'.(0-$iItemPosLeft).'" AND i.`pos_right` >= "'.(0-$iItemPosRight).'"',
'UPDATE `list_items`
SET `parent_item_id` = "'.$iParentItemId.'"
WHERE `item_id`="'.$iItemId.'"'
);
foreach($sql as $sqlQuery){
mysql_query($sqlQuery);
}
Please note also, that code may be optimized, but I going to leave it like that for better readability. Also consider table locking if you are using nested sets in multi-user systems.
Hope that my message will help to anyone, who will search for a solution after me. Any comments and corrections are also welcome.
I know this post is old but im posting this solution for every one else that will get here to see a solution.I found this @ sedna-soft.de . I tested id and works perfectly
-- moves a subtree before the specified position
-- if the position is the rgt of a node, the subtree will be its last child
-- if the position is the lft of a node, the subtree will be inserted before
-- @param l the lft of the subtree to move
-- @param r the rgt of the subtree to move
-- @param p the position to move the subtree before
SET @r: , @l: , @p:
update tree
set
lft = lft + if (@p > @r,
if (@r < lft and lft < @p,
@l - @r - 1,
if (@l <= lft and lft < @r,
@p - @r - 1,
0
)
),
if (@p <= lft and lft < @l,
@r - @l + 1,
if (@l <= lft and lft < @r,
@p - @l,
0
)
)
),
rgt = rgt + if (@p > @r,
if (@r < rgt and rgt < @p,
@l - @r - 1,
if (@l < rgt and rgt <= @r,
@p - @r - 1,
0
)
),
if (@p <= rgt and rgt < @l,
@r - @l + 1,
if (@l < rgt and rgt <= @r,
@p - @l,
0
)
)
)
where @r < @p or @p < @l;
Here is a solution that lets you move a node to any position in the tree, either as a sibling or a child with just a single input parameter - the new left position (newlpos) of the node.
Fundamentally there are three steps:
In psuedo-sql, it looks like this:
//
* -- create new space for subtree
* UPDATE tags SET lpos = lpos + :width WHERE lpos >= :newlpos
* UPDATE tags SET rpos = rpos + :width WHERE rpos >= :newlpos
*
* -- move subtree into new space
* UPDATE tags SET lpos = lpos + :distance, rpos = rpos + :distance
* WHERE lpos >= :tmppos AND rpos < :tmppos + :width
*
* -- remove old space vacated by subtree
* UPDATE tags SET lpos = lpos - :width WHERE lpos > :oldrpos
* UPDATE tags SET rpos = rpos - :width WHERE rpos > :oldrpos
*/
The :distance variable is the distance between the new and old positions, the :width is the size of the subtree, and :tmppos is used to keep track of the subtree being moved during the updates. These variables are defined as:
// calculate position adjustment variables
int width = node.getRpos() - node.getLpos() + 1;
int distance = newlpos - node.getLpos();
int tmppos = node.getLpos();
// backwards movement must account for new space
if (distance < 0) {
distance -= width;
tmppos += width;
}
For a complete code example, see my blog at
http://www.ninthavenue.com.au/how-to-move-a-node-in-nested-sets-with-sql
If you like this solution, please up-vote.
There are many answers already, but I feel like mine can be useful for someone. Based on answer of Roger Keays (thank you very much!), I wrote stored procedures for mySQL database:
-- to move target before specified node
CREATE DEFINER=`root`@`%` PROCEDURE `move_before`(IN target_id int, before_id int)
BEGIN
SELECT @new_pos := lft FROM dirs WHERE id = before_id;
CALL move(target_id, @new_pos);
END
-- to move target after specified node
CREATE DEFINER=`root`@`%` PROCEDURE `move_after`(IN target_id int, after_id int)
BEGIN
SELECT @new_pos := rgt + 1 FROM dirs WHERE id = after_id;
CALL move(target_id, @new_pos);
END
-- to move target to the specified node
CREATE DEFINER=`root`@`%` PROCEDURE `move_in`(IN target_id int, parent_id int)
BEGIN
SELECT @new_pos := rgt FROM dirs WHERE id = parent_id;
CALL move(target_id, @new_pos);
END
--main procedure to move target before position
CREATE DEFINER=`root`@`%` PROCEDURE `move`(in target_id int, in new_pos int)
BEGIN
SELECT @oldlft := lft, @oldrgt := rgt
FROM dirs
WHERE target_id = id;
SET @width := @oldrgt - @oldlft +1;
SET @distance := new_pos - @oldlft;
SET @tmppos := @oldlft;
IF (@distance <0)
THEN
SELECT @distance := @distance - @width;
SELECT @tmppos := @tmppos + @width;
END IF;
-- create new space for subtree
UPDATE dirs SET lft = lft + @width WHERE lft >= new_pos;
UPDATE dirs SET rgt = rgt + @width WHERE rgt >= new_pos;
-- move subtree into new space
UPDATE dirs SET lft = lft + @distance, rgt = rgt + @distance
WHERE lft >= @tmppos AND rgt < @tmppos + @width;
-- remove old space vacated by subtree
UPDATE dirs SET lft = lft - @width WHERE lft > @oldrgt;
UPDATE dirs SET rgt = rgt - @width WHERE rgt > @oldrgt;
END