Move node in nested set
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:
- Change positions of node and all it's sub nodes into negative values,which are equal to current ones by module.
- Move all positions "up", which are more, that pos_right of current node.
- Move all positions "down", which are more, that pos_right of new parent node.
- Change positions of current node and all it's subnodes, so that it's now will be exactly "after" (or "down") of new parent node.
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 nodeUPDATE `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_sizeWHERE `pos_left` > @node_pos_right;UPDATE `list_items`SET `pos_right` = `pos_right` - @node_sizeWHERE `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_sizeWHERE `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_sizeWHERE `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 idUPDATE `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_idWHERE `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.
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:
- Create new space for the subtree.
- Move the subtree into this space.
- Remove the old space vacated by the subtree.
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 variablesint width = node.getRpos() - node.getLpos() + 1;int distance = newlpos - node.getLpos();int tmppos = node.getLpos(); // backwards movement must account for new spaceif (distance < 0) { distance -= width; tmppos += width;}
For a complete code example, see my blog at
https://rogerkeays.com/how-to-move-a-node-in-nested-sets-with-sql
If you like this solution, please up-vote.
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 uniqueidentifierASBEGIN 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