From 3fc9edf87166b9a659c8a3eea90833c0950c45d6 Mon Sep 17 00:00:00 2001 From: zYne Date: Mon, 15 Jan 2007 20:12:22 +0000 Subject: [PATCH] Hierarchical data handling implementation moved from draft to main lib --- lib/Doctrine/Node.php | 157 ++++ lib/Doctrine/Node/AdjacencyList.php | 34 + .../Node/AdjacencyList/LevelOrderIterator.php | 33 + .../Node/AdjacencyList/PostOrderIterator.php | 33 + .../Node/AdjacencyList/PreOrderIterator.php | 33 + lib/Doctrine/Node/Exception.php | 33 + lib/Doctrine/Node/Interface.php | 267 +++++++ lib/Doctrine/Node/MaterializedPath.php | 34 + .../MaterializedPath/LevelOrderIterator.php | 67 ++ .../MaterializedPath/PostOrderIterator.php | 67 ++ .../MaterializedPath/PreOrderIterator.php | 67 ++ lib/Doctrine/Node/NestedSet.php | 723 ++++++++++++++++++ .../Node/NestedSet/LevelOrderIterator.php | 33 + .../Node/NestedSet/PostOrderIterator.php | 33 + .../Node/NestedSet/PreOrderIterator.php | 177 +++++ lib/Doctrine/Tree/AdjacencyList.php | 33 + lib/Doctrine/Tree/Exception.php | 33 + lib/Doctrine/Tree/Interface.php | 64 ++ lib/Doctrine/Tree/MaterializedPath.php | 33 + lib/Doctrine/Tree/NestedSet.php | 241 ++++++ 20 files changed, 2195 insertions(+) create mode 100644 lib/Doctrine/Node.php create mode 100644 lib/Doctrine/Node/AdjacencyList.php create mode 100644 lib/Doctrine/Node/AdjacencyList/LevelOrderIterator.php create mode 100644 lib/Doctrine/Node/AdjacencyList/PostOrderIterator.php create mode 100644 lib/Doctrine/Node/AdjacencyList/PreOrderIterator.php create mode 100644 lib/Doctrine/Node/Exception.php create mode 100644 lib/Doctrine/Node/Interface.php create mode 100644 lib/Doctrine/Node/MaterializedPath.php create mode 100644 lib/Doctrine/Node/MaterializedPath/LevelOrderIterator.php create mode 100644 lib/Doctrine/Node/MaterializedPath/PostOrderIterator.php create mode 100644 lib/Doctrine/Node/MaterializedPath/PreOrderIterator.php create mode 100644 lib/Doctrine/Node/NestedSet.php create mode 100644 lib/Doctrine/Node/NestedSet/LevelOrderIterator.php create mode 100644 lib/Doctrine/Node/NestedSet/PostOrderIterator.php create mode 100644 lib/Doctrine/Node/NestedSet/PreOrderIterator.php create mode 100644 lib/Doctrine/Tree/AdjacencyList.php create mode 100644 lib/Doctrine/Tree/Exception.php create mode 100644 lib/Doctrine/Tree/Interface.php create mode 100644 lib/Doctrine/Tree/MaterializedPath.php create mode 100644 lib/Doctrine/Tree/NestedSet.php diff --git a/lib/Doctrine/Node.php b/lib/Doctrine/Node.php new file mode 100644 index 000000000..4ff072bf4 --- /dev/null +++ b/lib/Doctrine/Node.php @@ -0,0 +1,157 @@ +. + */ +/** + * Doctrine_Node + * + * @package Doctrine + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @category Object Relational Mapping + * @link www.phpdoctrine.com + * @since 1.0 + * @version $Revision$ + * @author Joe Simms + */ +class Doctrine_Node implements IteratorAggregate +{ + /** + * @param object $record reference to associated Doctrine_Record instance + */ + protected $record; + + /** + * @param array $options + */ + protected $options; + + /** + * @param string $iteratorType (Pre | Post | Level) + */ + protected $iteratorType; + + /** + * @param array $iteratorOptions + */ + protected $iteratorOptions; + + /** + * contructor, creates node with reference to record and any options + * + * @param object $record instance of Doctrine_Record + * @param array $options options + */ + public function __construct(Doctrine_Record $record, $options) + { + $this->record = $record; + $this->options = $options; + } + + /** + * factory method to return node instance based upon chosen implementation + * + * @param object $record instance of Doctrine_Record + * @param string $impName implementation (NestedSet, AdjacencyList, MaterializedPath) + * @param array $options options + * @return object $options instance of Doctrine_Node + */ + public static function factory(Doctrine_Record $record, $implName, $options = array()) + { + $class = 'Doctrine_Node_' . $implName; + + if (!class_exists($class)) { + throw new Doctrine_Node_Exception("The class $class must exist and extend Doctrine_Node"); + } + + return new $class($record, $options); + } + + /** + * setter for record attribute + * + * @param object $record instance of Doctrine_Record + */ + public function setRecord(Doctrine_Record $record) + { + $this->record = $record; + } + + /** + * getter for record attribute + * + * @return object instance of Doctrine_Record + */ + public function getRecord() + { + return $this->record; + } + + /** + * convenience function for getIterator + * + * @param string $type type of iterator (Pre | Post | Level) + * @param array $options options + */ + public function traverse($type = 'Pre', $options = array()) + { + return $this->getIterator($type, $options); + } + + /** + * get iterator + * + * @param string $type type of iterator (Pre | Post | Level) + * @param array $options options + */ + public function getIterator($type = null, $options = null) + { + if ($type === null) { + $type = (isset($this->iteratorType) ? $this->iteratorType : 'Pre'); + } + + if ($options === null) { + $options = (isset($this->iteratorOptions) ? $this->iteratorOptions : array()); + } + + $implName = $this->record->getTable()->getTreeImplName(); + $iteratorClass = 'Doctrine_Node_' . $implName . '_' . ucfirst(strtolower($type)) . 'OrderIterator'; + + return new $iteratorClass($this->record, $options); + } + + /** + * sets node's iterator type + * + * @param int + */ + public function setIteratorType($type) + { + $this->iteratorType = $type; + } + + /** + * sets node's iterator options + * + * @param int + */ + public function setIteratorOptions($options) + { + $this->iteratorOptions = $options; + } +} diff --git a/lib/Doctrine/Node/AdjacencyList.php b/lib/Doctrine/Node/AdjacencyList.php new file mode 100644 index 000000000..8dc1138a6 --- /dev/null +++ b/lib/Doctrine/Node/AdjacencyList.php @@ -0,0 +1,34 @@ +. + */ +/** + * Doctrine_Node_AdjacencyList + * + * @package Doctrine + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @category Object Relational Mapping + * @link www.phpdoctrine.com + * @since 1.0 + * @version $Revision$ + * @author Joe Simms + */ +class Doctrine_Node_AdjacencyList extends Doctrine_Node implements Doctrine_Node_Interface +{} + diff --git a/lib/Doctrine/Node/AdjacencyList/LevelOrderIterator.php b/lib/Doctrine/Node/AdjacencyList/LevelOrderIterator.php new file mode 100644 index 000000000..cb0521775 --- /dev/null +++ b/lib/Doctrine/Node/AdjacencyList/LevelOrderIterator.php @@ -0,0 +1,33 @@ +. + */ +/** + * Doctrine_Node_AdjacencyList_LevelOrderIterator + * + * @package Doctrine + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @category Object Relational Mapping + * @link www.phpdoctrine.com + * @since 1.0 + * @version $Revision$ + * @author Joe Simms + */ +class Doctrine_Node_AdjacencyList_LevelOrderIterator implements Iterator +{} diff --git a/lib/Doctrine/Node/AdjacencyList/PostOrderIterator.php b/lib/Doctrine/Node/AdjacencyList/PostOrderIterator.php new file mode 100644 index 000000000..6da50ea39 --- /dev/null +++ b/lib/Doctrine/Node/AdjacencyList/PostOrderIterator.php @@ -0,0 +1,33 @@ +. + */ +/** + * Doctrine_Node_AdjacencyList_PostOrderIterator + * + * @package Doctrine + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @category Object Relational Mapping + * @link www.phpdoctrine.com + * @since 1.0 + * @version $Revision$ + * @author Joe Simms + */ +class Doctrine_Node_AdjacencyList_PostOrderIterator implements Iterator +{} diff --git a/lib/Doctrine/Node/AdjacencyList/PreOrderIterator.php b/lib/Doctrine/Node/AdjacencyList/PreOrderIterator.php new file mode 100644 index 000000000..4bf3bd865 --- /dev/null +++ b/lib/Doctrine/Node/AdjacencyList/PreOrderIterator.php @@ -0,0 +1,33 @@ +. + */ +/** + * Doctrine_Node_AdjacencyList_PreOrderIterator + * + * @package Doctrine + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @category Object Relational Mapping + * @link www.phpdoctrine.com + * @since 1.0 + * @version $Revision$ + * @author Joe Simms + */ +class Doctrine_Node_AdjacencyList_PreOrderIterator implements Iterator +{} diff --git a/lib/Doctrine/Node/Exception.php b/lib/Doctrine/Node/Exception.php new file mode 100644 index 000000000..099bbe673 --- /dev/null +++ b/lib/Doctrine/Node/Exception.php @@ -0,0 +1,33 @@ +. + */ +/** + * Doctrine_Node_Exception + * + * @package Doctrine + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @category Object Relational Mapping + * @link www.phpdoctrine.com + * @since 1.0 + * @version $Revision$ + * @author Joe Simms + */ +class Doctrine_Node_Exception extends Doctrine_Exception +{} diff --git a/lib/Doctrine/Node/Interface.php b/lib/Doctrine/Node/Interface.php new file mode 100644 index 000000000..c6a6ce394 --- /dev/null +++ b/lib/Doctrine/Node/Interface.php @@ -0,0 +1,267 @@ +. + */ +/** + * Doctrine_Node_Interface + * + * @package Doctrine + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @category Object Relational Mapping + * @link www.phpdoctrine.com + * @since 1.0 + * @version $Revision$ + * @author Joe Simms + */ +interface Doctrine_Node_Interface { + + /** + * test if node has previous sibling + * + * @return bool + */ + public function hasPrevSibling(); + + /** + * test if node has next sibling + * + * @return bool + */ + public function hasNextSibling(); + + /** + * test if node has children + * + * @return bool + */ + public function hasChildren(); + + /** + * test if node has parent + * + * @return bool + */ + public function hasParent(); + + /** + * gets record of prev sibling or empty record + * + * @return object Doctrine_Record + */ + public function getPrevSibling(); + + /** + * gets record of next sibling or empty record + * + * @return object Doctrine_Record + */ + public function getNextSibling(); + + /** + * gets siblings for node + * + * @return array array of sibling Doctrine_Record objects + */ + public function getSiblings($includeNode = false); + + /** + * gets record of first child or empty record + * + * @return object Doctrine_Record + */ + public function getFirstChild(); + + /** + * gets record of last child or empty record + * + * @return object Doctrine_Record + */ + public function getLastChild(); + + /** + * gets children for node (direct descendants only) + * + * @return array array of sibling Doctrine_Record objects + */ + public function getChildren(); + + /** + * gets descendants for node (direct descendants only) + * + * @return iterator iterator to traverse descendants from node + */ + public function getDescendants(); + + /** + * gets record of parent or empty record + * + * @return object Doctrine_Record + */ + public function getParent(); + + /** + * gets ancestors for node + * + * @return object Doctrine_Collection + */ + public function getAncestors(); + + /** + * gets path to node from root, uses record::toString() method to get node names + * + * @param string $seperator path seperator + * @param bool $includeNode whether or not to include node at end of path + * @return string string representation of path + */ + public function getPath($seperator = ' > ', $includeNode = false); + + /** + * gets level (depth) of node in the tree + * + * @return int + */ + public function getLevel(); + + /** + * gets number of children (direct descendants) + * + * @return int + */ + public function getNumberChildren(); + + /** + * gets number of descendants (children and their children) + * + * @return int + */ + public function getNumberDescendants(); + + /** + * inserts node as parent of dest record + * + * @return bool + */ + public function insertAsParentOf(Doctrine_Record $dest); + + /** + * inserts node as previous sibling of dest record + * + * @return bool + */ + public function insertAsPrevSiblingOf(Doctrine_Record $dest); + + /** + * inserts node as next sibling of dest record + * + * @return bool + */ + public function insertAsNextSiblingOf(Doctrine_Record $dest); + + /** + * inserts node as first child of dest record + * + * @return bool + */ + public function insertAsFirstChildOf(Doctrine_Record $dest); + + /** + * inserts node as first child of dest record + * + * @return bool + */ + public function insertAsLastChildOf(Doctrine_Record $dest); + + /** + * moves node as prev sibling of dest record + * + */ + public function moveAsPrevSiblingOf(Doctrine_Record $dest); + + /** + * moves node as next sibling of dest record + * + */ + public function moveAsNextSiblingOf(Doctrine_Record $dest); + + /** + * moves node as first child of dest record + * + */ + public function moveAsFirstChildOf(Doctrine_Record $dest); + + /** + * moves node as last child of dest record + * + */ + public function moveAsLastChildOf(Doctrine_Record $dest); + + /** + * adds node as last child of record + * + */ + public function addChild(Doctrine_Record $record); + + /** + * determines if node is leaf + * + * @return bool + */ + public function isLeaf(); + + /** + * determines if node is root + * + * @return bool + */ + public function isRoot(); + + /** + * determines if node is equal to subject node + * + * @return bool + */ + public function isEqualTo(Doctrine_Record $subj); + + /** + * determines if node is child of subject node + * + * @return bool + */ + public function isDescendantOf(Doctrine_Record $subj); + + /** + * determines if node is child of or sibling to subject node + * + * @return bool + */ + public function isDescendantOfOrEqualTo(Doctrine_Record $subj); + + /** + * determines if node is valid + * + * @return bool + */ + public function isValidNode(); + + /** + * deletes node and it's descendants + * + */ + public function delete(); +} diff --git a/lib/Doctrine/Node/MaterializedPath.php b/lib/Doctrine/Node/MaterializedPath.php new file mode 100644 index 000000000..1887bf37d --- /dev/null +++ b/lib/Doctrine/Node/MaterializedPath.php @@ -0,0 +1,34 @@ +. + */ +/** + * Doctrine_Node_MaterializedPath + * + * @package Doctrine + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @category Object Relational Mapping + * @link www.phpdoctrine.com + * @since 1.0 + * @version $Revision$ + * @author Joe Simms + */ +class Doctrine_Node_MaterializedPath extends Doctrine_Node implements Doctrine_Node_Interface +{} + diff --git a/lib/Doctrine/Node/MaterializedPath/LevelOrderIterator.php b/lib/Doctrine/Node/MaterializedPath/LevelOrderIterator.php new file mode 100644 index 000000000..fe43dc6c5 --- /dev/null +++ b/lib/Doctrine/Node/MaterializedPath/LevelOrderIterator.php @@ -0,0 +1,67 @@ +. + */ +/** + * Doctrine_Node_MaterializedPath_LevelOrderIterator + * + * @package Doctrine + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @category Object Relational Mapping + * @link www.phpdoctrine.com + * @since 1.0 + * @version $Revision$ + * @author Joe Simms + */ +class Doctrine_Node_MaterializedPath_LevelOrderIterator implements Iterator +{ + private $topNode = null; + + private $curNode = null; + + public function __construct($node, $opts) + { + throw new Doctrine_Exception('Not yet implemented'); + } + + public function rewind() + { + throw new Doctrine_Exception('Not yet implemented'); + } + + public function valid() + { + throw new Doctrine_Exception('Not yet implemented'); + } + + public function current() + { + throw new Doctrine_Exception('Not yet implemented'); + } + + public function key() + { + throw new Doctrine_Exception('Not yet implemented'); + } + + public function next() + { + throw new Doctrine_Exception('Not yet implemented'); + } +} diff --git a/lib/Doctrine/Node/MaterializedPath/PostOrderIterator.php b/lib/Doctrine/Node/MaterializedPath/PostOrderIterator.php new file mode 100644 index 000000000..ae65bd11c --- /dev/null +++ b/lib/Doctrine/Node/MaterializedPath/PostOrderIterator.php @@ -0,0 +1,67 @@ +. + */ +/** + * Doctrine_Node_MaterializedPath_PostOrderIterator + * + * @package Doctrine + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @category Object Relational Mapping + * @link www.phpdoctrine.com + * @since 1.0 + * @version $Revision$ + * @author Joe Simms + */ +class Doctrine_Node_MaterializedPath_PostOrderIterator implements Iterator +{ + private $topNode = null; + + private $curNode = null; + + public function __construct($node, $opts) + { + throw new Doctrine_Exception('Not yet implemented'); + } + + public function rewind() + { + throw new Doctrine_Exception('Not yet implemented'); + } + + public function valid() + { + throw new Doctrine_Exception('Not yet implemented'); + } + + public function current() + { + throw new Doctrine_Exception('Not yet implemented'); + } + + public function key() + { + throw new Doctrine_Exception('Not yet implemented'); + } + + public function next() + { + throw new Doctrine_Exception('Not yet implemented'); + } +} diff --git a/lib/Doctrine/Node/MaterializedPath/PreOrderIterator.php b/lib/Doctrine/Node/MaterializedPath/PreOrderIterator.php new file mode 100644 index 000000000..e76420d67 --- /dev/null +++ b/lib/Doctrine/Node/MaterializedPath/PreOrderIterator.php @@ -0,0 +1,67 @@ +. + */ +/** + * Doctrine_Node_MaterializedPath_PreOrderIterator + * + * @package Doctrine + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @category Object Relational Mapping + * @link www.phpdoctrine.com + * @since 1.0 + * @version $Revision$ + * @author Joe Simms + */ +class Doctrine_Node_MaterializedPath_PreOrderIterator implements Iterator +{ + private $topNode = null; + + private $curNode = null; + + public function __construct($node, $opts) + { + throw new Doctrine_Exception('Not yet implemented'); + } + + public function rewind() + { + throw new Doctrine_Exception('Not yet implemented'); + } + + public function valid() + { + throw new Doctrine_Exception('Not yet implemented'); + } + + public function current() + { + throw new Doctrine_Exception('Not yet implemented'); + } + + public function key() + { + throw new Doctrine_Exception('Not yet implemented'); + } + + public function next() + { + throw new Doctrine_Exception('Not yet implemented'); + } +} diff --git a/lib/Doctrine/Node/NestedSet.php b/lib/Doctrine/Node/NestedSet.php new file mode 100644 index 000000000..82a86a5dd --- /dev/null +++ b/lib/Doctrine/Node/NestedSet.php @@ -0,0 +1,723 @@ +. + */ +/** + * Doctrine_Node_NestedSet + * + * @package Doctrine + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @category Object Relational Mapping + * @link www.phpdoctrine.com + * @since 1.0 + * @version $Revision$ + * @author Joe Simms + */ +class Doctrine_Node_NestedSet extends Doctrine_Node implements Doctrine_Node_Interface +{ + /** + * test if node has previous sibling + * + * @return bool + */ + public function hasPrevSibling() + { + return $this->isValidNode($this->getPrevSibling()); + } + + /** + * test if node has next sibling + * + * @return bool + */ + public function hasNextSibling() + { + return $this->isValidNode($this->getNextSibling()); + } + + /** + * test if node has children + * + * @return bool + */ + public function hasChildren() + { + return (($this->getRightValue() - $this->getLeftValue() ) >1 ); + } + + /** + * test if node has parent + * + * @return bool + */ + public function hasParent() + { + return !$this->isRoot(); + } + + /** + * gets record of prev sibling or empty record + * + * @return object Doctrine_Record + */ + public function getPrevSibling() + { + $q = $this->record->getTable()->createQuery(); + $result = $q->where('rgt = ?', $this->getLeftValue() - 1)->execute()->getFirst(); + + if(!$result) + $result = $this->record->getTable()->create(); + + return $result; + } + + /** + * gets record of next sibling or empty record + * + * @return object Doctrine_Record + */ + public function getNextSibling() + { + $q = $this->record->getTable()->createQuery(); + $result = $q->where('lft = ?', $this->getRightValue() + 1)->execute()->getFirst(); + + if(!$result) + $result = $this->record->getTable()->create(); + + return $result; + } + + /** + * gets siblings for node + * + * @return array array of sibling Doctrine_Record objects + */ + public function getSiblings($includeNode = false) + { + $parent = $this->getParent(); + $siblings = array(); + if($parent->exists()) + { + foreach($parent->getNode()->getChildren() as $child) + { + if($this->isEqualTo($child) && !$includeNode) + continue; + + $siblings[] = $child; + } + } + + return $siblings; + } + + /** + * gets record of first child or empty record + * + * @return object Doctrine_Record + */ + public function getFirstChild() + { + $q = $this->record->getTable()->createQuery(); + $result = $q->where('lft = ?', $this->getLeftValue() + 1)->execute()->getFirst(); + + if(!$result) + $result = $this->record->getTable()->create(); + + return $result; + } + + /** + * gets record of last child or empty record + * + * @return object Doctrine_Record + */ + public function getLastChild() + { + $q = $this->record->getTable()->createQuery(); + $result = $q->where('rgt = ?', $this->getRightValue() - 1)->execute()->getFirst(); + + if(!$result) + $result = $this->record->getTable()->create(); + + return $result; + } + + /** + * gets children for node (direct descendants only) + * + * @return array array of sibling Doctrine_Record objects + */ + public function getChildren() + { + return $this->getIterator('Pre', array('depth' => 1)); + } + + /** + * gets descendants for node (direct descendants only) + * + * @return iterator iterator to traverse descendants from node + */ + public function getDescendants() + { + return $this->getIterator(); + } + + /** + * gets record of parent or empty record + * + * @return object Doctrine_Record + */ + public function getParent() + { + $q = $this->record->getTable()->createQuery(); + + $parent = $q->where('lft < ? AND rgt > ?', array($this->getLeftValue(), $this->getRightValue())) + ->orderBy('rgt asc') + ->execute() + ->getFirst(); + + if(!$parent) + $parent = $this->record->getTable()->create(); + + return $parent; + } + + /** + * gets ancestors for node + * + * @return object Doctrine_Collection + */ + public function getAncestors() + { + $q = $this->record->getTable()->createQuery(); + + $ancestors = $q->where('lft < ? AND rgt > ?', array($this->getLeftValue(), $this->getRightValue())) + ->orderBy('lft asc') + ->execute(); + + return $ancestors; + } + + /** + * gets path to node from root, uses record::toString() method to get node names + * + * @param string $seperator path seperator + * @param bool $includeNode whether or not to include node at end of path + * @return string string representation of path + */ + public function getPath($seperator = ' > ', $includeRecord = false) + { + $path = array(); + $ancestors = $this->getAncestors(); + foreach($ancestors as $ancestor) + { + $path[] = $ancestor->__toString(); + } + if($includeRecord) + $path[] = $this->getRecord()->__toString(); + + return implode($seperator, $path); + } + + /** + * gets number of children (direct descendants) + * + * @return int + */ + public function getNumberChildren() + { + $count = 0; + $children = $this->getChildren(); + + while($children->next()) + { + $count++; + } + return $count; + } + + /** + * gets number of descendants (children and their children) + * + * @return int + */ + public function getNumberDescendants() + { + return ($this->getRightValue() - $this->getLeftValue() - 1) / 2; + } + + /** + * inserts node as parent of dest record + * + * @return bool + */ + public function insertAsParentOf(Doctrine_Record $dest) + { + // cannot insert a node that has already has a place within the tree + if($this->isValidNode()) + return false; + + // cannot insert as parent of root + if($dest->getNode()->isRoot()) + return false; + + $this->shiftRLValues($dest->getNode()->getLeftValue(), 1); + $this->shiftRLValues($dest->getNode()->getRightValue() + 2, 1); + + $newLeft = $dest->getNode()->getLeftValue(); + $newRight = $dest->getNode()->getRightValue() + 2; + $newRoot = $dest->getNode()->getRootValue(); + + $this->insertNode($newLeft, $newRight, $newRoot); + + return true; + } + + /** + * inserts node as previous sibling of dest record + * + * @return bool + */ + public function insertAsPrevSiblingOf(Doctrine_Record $dest) + { + // cannot insert a node that has already has a place within the tree + if($this->isValidNode()) + return false; + + $newLeft = $dest->getNode()->getLeftValue(); + $newRight = $dest->getNode()->getLeftValue() + 1; + $newRoot = $dest->getNode()->getRootValue(); + + $this->shiftRLValues($newLeft, 2, $newRoot); + $this->insertNode($newLeft, $newRight, $newRoot); + // update destination left/right values to prevent a refresh + // $dest->getNode()->setLeftValue($dest->getNode()->getLeftValue() + 2); + // $dest->getNode()->setRightValue($dest->getNode()->getRightValue() + 2); + + return true; + } + + /** + * inserts node as next sibling of dest record + * + * @return bool + */ + public function insertAsNextSiblingOf(Doctrine_Record $dest) + { + // cannot insert a node that has already has a place within the tree + if($this->isValidNode()) + return false; + + $newLeft = $dest->getNode()->getRightValue() + 1; + $newRight = $dest->getNode()->getRightValue() + 2; + $newRoot = $dest->getNode()->getRootValue(); + + $this->shiftRLValues($newLeft, 2, $newRoot); + $this->insertNode($newLeft, $newRight, $newRoot); + + // update destination left/right values to prevent a refresh + // no need, node not affected + + return true; + } + + /** + * inserts node as first child of dest record + * + * @return bool + */ + public function insertAsFirstChildOf(Doctrine_Record $dest) + { + // cannot insert a node that has already has a place within the tree + if($this->isValidNode()) + return false; + + $newLeft = $dest->getNode()->getLeftValue() + 1; + $newRight = $dest->getNode()->getLeftValue() + 2; + $newRoot = $dest->getNode()->getRootValue(); + + $this->shiftRLValues($newLeft, 2, $newRoot); + $this->insertNode($newLeft, $newRight, $newRoot); + + // update destination left/right values to prevent a refresh + // $dest->getNode()->setRightValue($dest->getNode()->getRightValue() + 2); + + return true; + } + + /** + * inserts node as last child of dest record + * + * @return bool + */ + public function insertAsLastChildOf(Doctrine_Record $dest) + { + // cannot insert a node that has already has a place within the tree + if($this->isValidNode()) + return false; + + $newLeft = $dest->getNode()->getRightValue(); + $newRight = $dest->getNode()->getRightValue() + 1; + $newRoot = $dest->getNode()->getRootValue(); + + $this->shiftRLValues($newLeft, 2, $newRoot); + $this->insertNode($newLeft, $newRight, $newRoot); + + // update destination left/right values to prevent a refresh + // $dest->getNode()->setRightValue($dest->getNode()->getRightValue() + 2); + + return true; + } + + /** + * moves node as prev sibling of dest record + * + */ + public function moveAsPrevSiblingOf(Doctrine_Record $dest) + { + $this->updateNode($dest->getNode()->getLeftValue()); + } + + /** + * moves node as next sibling of dest record + * + */ + public function moveAsNextSiblingOf(Doctrine_Record $dest) + { + $this->updateNode($dest->getNode()->getRightValue() + 1); + } + + /** + * moves node as first child of dest record + * + */ + public function moveAsFirstChildOf(Doctrine_Record $dest) + { + $this->updateNode($dest->getNode()->getLeftValue() + 1); + } + + /** + * moves node as last child of dest record + * + */ + public function moveAsLastChildOf(Doctrine_Record $dest) + { + $this->updateNode($dest->getNode()->getRightValue()); + } + + /** + * adds node as last child of record + * + */ + public function addChild(Doctrine_Record $record) + { + $record->getNode()->insertAsLastChildOf($this->getRecord()); + } + + /** + * determines if node is leaf + * + * @return bool + */ + public function isLeaf() + { + return (($this->getRightValue()-$this->getLeftValue())==1); + } + + /** + * determines if node is root + * + * @return bool + */ + public function isRoot() + { + return ($this->getLeftValue()==1); + } + + /** + * determines if node is equal to subject node + * + * @return bool + */ + public function isEqualTo(Doctrine_Record $subj) + { + return (($this->getLeftValue()==$subj->getNode()->getLeftValue()) and ($this->getRightValue()==$subj->getNode()->getRightValue()) and ($this->getRootValue() == $subj->getNode()->getRootValue())); + } + + /** + * determines if node is child of subject node + * + * @return bool + */ + public function isDescendantOf(Doctrine_Record $subj) + { + return (($this->getLeftValue()>$subj->getNode()->getLeftValue()) and ($this->getRightValue()<$subj->getNode()->getRightValue()) and ($this->getRootValue() == $subj->getNode()->getRootValue())); + } + + /** + * determines if node is child of or sibling to subject node + * + * @return bool + */ + public function isDescendantOfOrEqualTo(Doctrine_Record $subj) + { + return (($this->getLeftValue()>=$subj->getNode()->getLeftValue()) and ($this->getRightValue()<=$subj->getNode()->getRightValue()) and ($this->getRootValue() == $subj->getNode()->getRootValue())); + } + + /** + * determines if node is valid + * + * @return bool + */ + public function isValidNode() + { + return ($this->getRightValue() > $this->getLeftValue()); + } + + /** + * deletes node and it's descendants + * + */ + public function delete() + { + // TODO: add the setting whether or not to delete descendants or relocate children + + $q = $this->record->getTable()->createQuery(); + + $componentName = $this->record->getTable()->getComponentName(); + + $q = $q->where("$componentName.lft >= ? AND $componentName.rgt <= ?", array($this->getLeftValue(), $this->getRightValue())); + + $q = $this->record->getTable()->getTree()->returnQueryWithRootId($q, $this->getRootValue()); + + $coll = $q->execute(); + + $coll->delete(); + + $first = $this->getRightValue() + 1; + $delta = $this->getLeftValue() - $this->getRightValue() - 1; + $this->shiftRLValues($first, $delta); + + return true; + } + + /** + * sets node's left and right values and save's it + * + * @param int $destLeft node left value + * @param int $destRight node right value + */ + private function insertNode($destLeft = 0, $destRight = 0, $destRoot = 1) + { + $this->setLeftValue($destLeft); + $this->setRightValue($destRight); + $this->setRootValue($destRoot); + $this->record->save(); + } + + /** + * move node's and its children to location $destLeft and updates rest of tree + * + * @param int $destLeft destination left value + */ + private function updateNode($destLeft) + { + $left = $this->getLeftValue(); + $right = $this->getRightValue(); + + $treeSize = $right - $left + 1; + + $this->shiftRLValues($destLeft, $treeSize); + + if($left >= $destLeft){ // src was shifted too? + $left += $treeSize; + $right += $treeSize; + } + + // now there's enough room next to target to move the subtree + $this->shiftRLRange($left, $right, $destLeft - $left); + + // correct values after source + $this->shiftRLValues($right + 1, -$treeSize); + + $this->record->save(); + $this->record->refresh(); + } + + /** + * adds '$delta' to all Left and Right values that are >= '$first'. '$delta' can also be negative. + * + * @param int $first First node to be shifted + * @param int $delta Value to be shifted by, can be negative + */ + private function shiftRLValues($first, $delta, $root_id = 1) + { + $qLeft = $this->record->getTable()->createQuery(); + $qRight = $this->record->getTable()->createQuery(); + + // TODO: Wrap in transaction + + // shift left columns + $qLeft = $qLeft->update($this->record->getTable()->getComponentName()) + ->set('lft', "lft + $delta") + ->where('lft >= ?', $first); + + $qLeft = $this->record->getTable()->getTree()->returnQueryWithRootId($qLeft, $root_id); + + $resultLeft = $qLeft->execute(); + + // shift right columns + $resultRight = $qRight->update($this->record->getTable()->getComponentName()) + ->set('rgt', "rgt + $delta") + ->where('rgt >= ?', $first); + + $qRight = $this->record->getTable()->getTree()->returnQueryWithRootId($qRight, $root_id); + + $resultRight = $qRight->execute(); + } + + /** + * adds '$delta' to all Left and Right values that are >= '$first' and <= '$last'. + * '$delta' can also be negative. + * + * @param int $first First node to be shifted (L value) + * @param int $last Last node to be shifted (L value) + * @param int $delta Value to be shifted by, can be negative + */ + private function shiftRLRange($first, $last, $delta, $root_id = 1) + { + $qLeft = $this->record->getTable()->createQuery(); + $qRight = $this->record->getTable()->createQuery(); + + // TODO : Wrap in transaction + + // shift left column values + $qLeft = $qLeft->update($this->record->getTable()->getComponentName()) + ->set('lft', "lft + $delta") + ->where('lft >= ? AND lft <= ?', array($first, $last)); + + $qLeft = $this->record->getTable()->getTree()->returnQueryWithRootId($qLeft, $root_id); + + $resultLeft = $qLeft->execute(); + + // shift right column values + $qRight = $qRight->update($this->record->getTable()->getComponentName()) + ->set('rgt', "rgt + $delta") + ->where('rgt >= ? AND rgt <= ?', array($first, $last)); + + $qRight = $this->record->getTable()->getTree()->returnQueryWithRootId($qRight, $root_id); + + $resultRight = $qRight->execute(); + } + + /** + * gets record's left value + * + * @return int + */ + public function getLeftValue() + { + return $this->record->get('lft'); + } + + /** + * sets record's left value + * + * @param int + */ + public function setLeftValue($lft) + { + $this->record->set('lft', $lft); + } + + /** + * gets record's right value + * + * @return int + */ + public function getRightValue() + { + return $this->record->get('rgt'); + } + + /** + * sets record's right value + * + * @param int + */ + public function setRightValue($rgt) + { + $this->record->set('rgt', $rgt); + } + + /** + * gets level (depth) of node in the tree + * + * @return int + */ + public function getLevel() + { + if(!isset($this->level)) + { + $q = $this->record->getTable()->createQuery(); + $q = $q->where('lft < ? AND rgt > ?', array($this->getLeftValue(), $this->getRightValue())); + + $q = $this->record->getTable()->getTree()->returnQueryWithRootId($q, $this->getRootValue()); + + $coll = $q->execute(); + + $this->level = $coll->count() ? $coll->count() : 0; + } + + return $this->level; + } + + /** + * sets node's level + * + * @param int + */ + public function setLevel($level) + { + $this->level = $level; + } + + /** + * get records root id value + * + */ + public function getRootValue() + { + if($this->record->getTable()->getTree()->getAttribute('has_many_roots')) + return $this->record->get($this->record->getTable()->getTree()->getAttribute('root_column_name')); + + return 1; + } + + /** + * sets records root id value + * + * @param int + */ + public function setRootValue($value) + { + if($this->record->getTable()->getTree()->getAttribute('has_many_roots')) + $this->record->set($this->record->getTable()->getTree()->getAttribute('root_column_name'), $value); + } +} diff --git a/lib/Doctrine/Node/NestedSet/LevelOrderIterator.php b/lib/Doctrine/Node/NestedSet/LevelOrderIterator.php new file mode 100644 index 000000000..cadaec3e2 --- /dev/null +++ b/lib/Doctrine/Node/NestedSet/LevelOrderIterator.php @@ -0,0 +1,33 @@ +. + */ +/** + * Doctrine_Node_NestedSet_LevelOrderIterator + * + * @package Doctrine + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @category Object Relational Mapping + * @link www.phpdoctrine.com + * @since 1.0 + * @version $Revision$ + * @author Joe Simms + */ +class Doctrine_Node_NestedSet_LevelOrderIterator implements Iterator +{} diff --git a/lib/Doctrine/Node/NestedSet/PostOrderIterator.php b/lib/Doctrine/Node/NestedSet/PostOrderIterator.php new file mode 100644 index 000000000..12d9d8670 --- /dev/null +++ b/lib/Doctrine/Node/NestedSet/PostOrderIterator.php @@ -0,0 +1,33 @@ +. + */ +/** + * Doctrine_Node_NestedSet_PostOrderIterator + * + * @package Doctrine + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @category Object Relational Mapping + * @link www.phpdoctrine.com + * @since 1.0 + * @version $Revision$ + * @author Joe Simms + */ +class Doctrine_Node_NestedSet_PostOrderIterator implements Iterator +{} diff --git a/lib/Doctrine/Node/NestedSet/PreOrderIterator.php b/lib/Doctrine/Node/NestedSet/PreOrderIterator.php new file mode 100644 index 000000000..bc2e87d5a --- /dev/null +++ b/lib/Doctrine/Node/NestedSet/PreOrderIterator.php @@ -0,0 +1,177 @@ +. + */ +/** + * Doctrine_Node_NestedSet_PreOrderIterator + * + * @package Doctrine + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @category Object Relational Mapping + * @link www.phpdoctrine.com + * @since 1.0 + * @version $Revision$ + * @author Joe Simms + */ +class Doctrine_Node_NestedSet_PreOrderIterator implements Iterator +{ + /** + * @var Doctrine_Collection $collection + */ + protected $collection; + /** + * @var array $keys + */ + protected $keys; + /** + * @var mixed $key + */ + protected $key; + /** + * @var integer $index + */ + protected $index; + /** + * @var integer $index + */ + protected $prevIndex; + /** + * @var integer $index + */ + protected $traverseLevel; + /** + * @var integer $count + */ + protected $count; + + public function __construct($record, $opts) + { + $componentName = $record->getTable()->getComponentName(); + + $q = $record->getTable()->createQuery(); + + $params = array($record->get('lft'), $record->get('rgt')); + if (isset($opts['include_record']) && $opts['include_record']) { + $query = $q->where("$componentName.lft >= ? AND $componentName.rgt <= ?", $params)->orderBy('lft asc'); + } else { + $query = $q->where("$componentName.lft > ? AND $componentName.rgt < ?", $params)->orderBy('lft asc'); + } + + $query = $record->getTable()->getTree()->returnQueryWithRootId($query, $record->getNode()->getRootValue()); + + $this->maxLevel = isset($opts['depth']) ? ($opts['depth'] + $record->getNode()->getLevel()) : 0; + $this->options = $opts; + $this->collection = isset($opts['collection']) ? $opts['collection'] : $query->execute(); + $this->keys = $this->collection->getKeys(); + $this->count = $this->collection->count(); + $this->index = -1; + $this->level = $record->getNode()->getLevel(); + $this->prevLeft = $record->getNode()->getLeftValue(); + + echo $this->maxDepth; + // clear the table identity cache + $record->getTable()->clear(); + } + + /** + * rewinds the iterator + * + * @return void + */ + public function rewind() + { + $this->index = -1; + $this->key = null; + } + + /** + * returns the current key + * + * @return integer + */ + public function key() + { + return $this->key; + } + + /** + * returns the current record + * + * @return Doctrine_Record + */ + public function current() + { + $record = $this->collection->get($this->key); + $record->getNode()->setLevel($this->level); + return $record; + } + + /** + * advances the internal pointer + * + * @return void + */ + public function next() + { + while ($current = $this->advanceIndex()) { + if ($this->maxLevel && ($this->level > $this->maxLevel)) { + continue; + } + + return $current; + } + + return false; + } + + /** + * @return boolean whether or not the iteration will continue + */ + public function valid() + { + return ($this->index < $this->count); + } + + public function count() + { + return $this->count; + } + + private function updateLevel() + { + if (!(isset($this->options['include_record']) && $this->options['include_record'] && $this->index == 0)) { + $left = $this->collection->get($this->key)->getNode()->getLeftValue(); + $this->level += $this->prevLeft - $left + 2; + $this->prevLeft = $left; + } + } + + private function advanceIndex() + { + $this->index++; + $i = $this->index; + if (isset($this->keys[$i])) { + $this->key = $this->keys[$i]; + $this->updateLevel(); + return $this->current(); + } + + return false; + } +} diff --git a/lib/Doctrine/Tree/AdjacencyList.php b/lib/Doctrine/Tree/AdjacencyList.php new file mode 100644 index 000000000..9ddefbe71 --- /dev/null +++ b/lib/Doctrine/Tree/AdjacencyList.php @@ -0,0 +1,33 @@ +. + */ +/** + * Doctrine_Tree_AdjacencyList + * + * @package Doctrine + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @category Object Relational Mapping + * @link www.phpdoctrine.com + * @since 1.0 + * @version $Revision$ + * @author Joe Simms + */ +class Doctrine_Tree_AdjacencyList extends Doctrine_Tree implements Doctrine_Tree_Interface +{} diff --git a/lib/Doctrine/Tree/Exception.php b/lib/Doctrine/Tree/Exception.php new file mode 100644 index 000000000..423d0df0c --- /dev/null +++ b/lib/Doctrine/Tree/Exception.php @@ -0,0 +1,33 @@ +. + */ +/** + * Doctrine_Tree_Exception + * + * @package Doctrine + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @category Object Relational Mapping + * @link www.phpdoctrine.com + * @since 1.0 + * @version $Revision$ + * @author Konsta Vesterinen + */ +class Doctrine_Tree_Exception extends Doctrine_Exception +{} diff --git a/lib/Doctrine/Tree/Interface.php b/lib/Doctrine/Tree/Interface.php new file mode 100644 index 000000000..7c365fee5 --- /dev/null +++ b/lib/Doctrine/Tree/Interface.php @@ -0,0 +1,64 @@ +. + */ +/** + * Doctrine_Tree_Interface + * + * @package Doctrine + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @category Object Relational Mapping + * @link www.phpdoctrine.com + * @since 1.0 + * @version $Revision$ + * @author Joe Simms + */ +interface Doctrine_Tree_Interface { + + /** + * creates root node from given record or from a new record + * + * @param object $record instance of Doctrine_Record + */ + public function createRoot(Doctrine_Record $record = null); + + /** + * returns root node + * + * @return object $record instance of Doctrine_Record + */ + public function findRoot($root_id = 1); + + /** + * optimised method to returns iterator for traversal of the entire tree from root + * + * @param array $options options + * @return object $iterator instance of Doctrine_Node__PreOrderIterator + */ + public function fetchTree($options = array()); + + /** + * optimised method that returns iterator for traversal of the tree from the given record primary key + * + * @param mixed $pk primary key as used by table::find() to locate node to traverse tree from + * @param array $options options + * @return iterator instance of Doctrine_Node__PreOrderIterator + */ + public function fetchBranch($pk, $options = array()); +} diff --git a/lib/Doctrine/Tree/MaterializedPath.php b/lib/Doctrine/Tree/MaterializedPath.php new file mode 100644 index 000000000..dd0609b16 --- /dev/null +++ b/lib/Doctrine/Tree/MaterializedPath.php @@ -0,0 +1,33 @@ +. + */ +/** + * Doctrine_Tree_MaterializedPath + * + * @package Doctrine + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @category Object Relational Mapping + * @link www.phpdoctrine.com + * @since 1.0 + * @version $Revision$ + * @author Joe Simms + */ +class Doctrine_Tree_MaterializedPath extends Doctrine_Tree implements Doctrine_Tree_Interface +{} diff --git a/lib/Doctrine/Tree/NestedSet.php b/lib/Doctrine/Tree/NestedSet.php new file mode 100644 index 000000000..7d790045a --- /dev/null +++ b/lib/Doctrine/Tree/NestedSet.php @@ -0,0 +1,241 @@ +. + */ +/** + * Doctrine_Tree_NestedSet + * + * @package Doctrine + * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @category Object Relational Mapping + * @link www.phpdoctrine.com + * @since 1.0 + * @version $Revision$ + * @author Joe Simms + */ +class Doctrine_Tree_NestedSet extends Doctrine_Tree implements Doctrine_Tree_Interface +{ + /** + * constructor, creates tree with reference to table and sets default root options + * + * @param object $table instance of Doctrine_Table + * @param array $options options + */ + public function __construct(Doctrine_Table $table, $options) + { + // set default many root attributes + $options['has_many_roots'] = isset($options['has_many_roots']) ? $options['has_many_roots'] : false; + $options['root_column_name'] = isset($options['root_column_name']) ? $options['root_column_name'] : 'root_id'; + + parent::__construct($table, $options); + } + + /** + * used to define table attributes required for the NestetSet implementation + * adds lft and rgt columns for corresponding left and right values + * + */ + public function setTableDefinition() + { + if ($this->getAttribute('has_many_roots')) { + $this->table->setColumn($this->getAttribute('root_column_name'),"integer",11); + } + + $this->table->setColumn("lft","integer",11); + $this->table->setColumn("rgt","integer",11); + } + + /** + * creates root node from given record or from a new record + * + * @param object $record instance of Doctrine_Record + */ + public function createRoot(Doctrine_Record $record = null) + { + if ( ! $record) { + $record = $this->table->create(); + } + + // if tree is many roots, then get next root id + if ($this->getAttribute('has_many_roots')) { + $record->getNode()->setRootValue($this->getNextRootId()); + } + + $record->set('lft', '1'); + $record->set('rgt', '2'); + + $record->save(); + + return $record; + } + + /** + * returns root node + * + * @return object $record instance of Doctrine_Record + */ + public function findRoot($rootId = 1) + { + $q = $this->table->createQuery(); + $q = $q->where('lft = ?', 1); + + // if tree has many roots, then specify root id + $q = $this->returnQueryWithRootId($q, $rootId); + + $root = $q->execute()->getFirst(); + + // if no record is returned, create record + if ( ! $root) { + $root = $this->table->create(); + } + + // set level to prevent additional query to determine level + $root->getNode()->setLevel(0); + + return $root; + } + + /** + * optimised method to returns iterator for traversal of the entire tree from root + * + * @param array $options options + * @return object $iterator instance of Doctrine_Node_NestedSet_PreOrderIterator + */ + public function fetchTree($options = array()) + { + // fetch tree + $q = $this->table->createQuery(); + + $q = $q->where('lft >= ?', 1) + ->orderBy('lft asc'); + + // if tree has many roots, then specify root id + $rootId = isset($options['root_id']) ? $options['root_id'] : '1'; + $q = $this->returnQueryWithRootId($q, $rootId); + + $tree = $q->execute(); + + $root = $tree->getFirst(); + + // if no record is returned, create record + if ( ! $root) { + $root = $this->table->create(); + } + + if ($root->exists()) { + // set level to prevent additional query + $root->getNode()->setLevel(0); + + // default to include root node + $options = array_merge(array('include_record'=>true), $options); + + // remove root node from collection if not required + if ($options['include_record'] == false) { + $tree->remove(0); + } + + // set collection for iterator + $options['collection'] = $tree; + + return $root->getNode()->traverse('Pre', $options); + } + + // TODO: no default return value or exception thrown? + } + + /** + * optimised method that returns iterator for traversal of the tree from the given record primary key + * + * @param mixed $pk primary key as used by table::find() to locate node to traverse tree from + * @param array $options options + * @return iterator instance of Doctrine_Node__PreOrderIterator + */ + public function fetchBranch($pk, $options = array()) + { + $record = $this->table->find($pk); + if ($record->exists()) { + $options = array_merge(array('include_record'=>true), $options); + return $record->getNode()->traverse('Pre', $options); + } + + // TODO: if record doesn't exist, throw exception or similar? + } + + /** + * fetch root nodes + * + * @return collection Doctrine_Collection + */ + public function fetchRoots() + { + $q = $this->table->createQuery(); + $q = $q->where('lft = ?', 1); + return $q->execute(); + } + + /** + * calculates the next available root id + * + * @return integer + */ + public function getNextRootId() + { + return $this->getMaxRootId() + 1; + } + + /** + * calculates the current max root id + * + * @return integer + */ + public function getMaxRootId() + { + $component = $this->table->getComponentName(); + $column = $this->getAttribute('root_column_name'); + + // cannot get this dql to work, cannot retrieve result using $coll[0]->max + //$dql = "SELECT MAX(c.$column) FROM $component c"; + + $dql = 'SELECT c.' . $column . ' FROM ' . $component . ' c ORDER BY c.' . $column . ' DESC LIMIT 1'; + + $coll = $this->table->getConnection()->query($dql); + + $max = $coll[0]->get($column); + + $max = !is_null($max) ? $max : 0; + + return $max; + } + + /** + * returns parsed query with root id where clause added if applicable + * + * @param object $query Doctrine_Query + * @param integer $root_id id of destination root + * @return object Doctrine_Query + */ + public function returnQueryWithRootId($query, $rootId = 1) + { + if($this->getAttribute('has_many_roots')) { + $query->addWhere($this->getAttribute('root_column_name') . ' = ?', $rootId); + } + + return $query; + } +}