diff --git a/src/NestedSetQueryFactory.php b/src/NestedSetQueryFactory.php index 4e0686e..6924659 100644 --- a/src/NestedSetQueryFactory.php +++ b/src/NestedSetQueryFactory.php @@ -143,35 +143,54 @@ public function createSubtreeThroughMultipleNodesQueryBuilder(string $tableExpre $directNodeSubSelect = $this->connection->createQueryBuilder() ->select([ + "{$queryAlias}directNode." . $this->pkCol, "{$queryAlias}directNode." . $this->leftCol, "{$queryAlias}directNode." . $this->rightCol, "{$queryAlias}directNode." . $this->levelCol, + "{$queryAlias}directNode." . $this->rootCol, ]) ->from($this->connection->quoteIdentifier($tableExpression), "{$queryAlias}directNode") ->andWhere("{$queryAlias}directNode.{$this->pkCol} IN (:{$queryAlias}nodeIds)"); - $siblingQuery = $this->connection->createQueryBuilder() + + $parentQuery = $this->connection->createQueryBuilder() ->select([ + "{$queryAlias}SiblingNode." . $this->pkCol, "{$queryAlias}SiblingNode." . $this->leftCol, "{$queryAlias}SiblingNode." . $this->rightCol, "{$queryAlias}SiblingNode." . $this->levelCol, + "{$queryAlias}SiblingNode." . $this->rootCol, ]) ->from($this->connection->quoteIdentifier($tableExpression), "{$queryAlias}SiblingNode") ->innerJoin( "{$queryAlias}SiblingNode", + $this->connection->quoteIdentifier($tableExpression), + "{$queryAlias}ParentNode", + " + {$queryAlias}SiblingNode.{$this->leftCol} >= {$queryAlias}ParentNode.{$this->leftCol} + AND {$queryAlias}SiblingNode.{$this->rightCol} <= {$queryAlias}ParentNode.{$this->rightCol} + AND {$queryAlias}SiblingNode.{$this->levelCol} = {$queryAlias}ParentNode.{$this->levelCol} + 1 + AND {$queryAlias}SiblingNode.{$this->rootCol} = {$queryAlias}ParentNode.{$this->rootCol} + " + ) + ->innerJoin( + "{$queryAlias}ParentNode", '(' . $directNodeSubSelect->getSQL() . ')', "{$queryAlias}SelectedNode", " - {$queryAlias}SiblingNode.{$this->leftCol} >= {$queryAlias}SelectedNode.{$this->leftCol} - AND {$queryAlias}SiblingNode.{$this->rightCol} >= {$queryAlias}SelectedNode.{$this->rightCol} - AND {$queryAlias}SiblingNode.{$this->levelCol} = {$queryAlias}SelectedNode.{$this->levelCol}" + {$queryAlias}ParentNode.{$this->leftCol} < {$queryAlias}SelectedNode.{$this->leftCol} + AND {$queryAlias}ParentNode.{$this->rightCol} > {$queryAlias}SelectedNode.{$this->rightCol} + AND {$queryAlias}ParentNode.{$this->rootCol} = {$queryAlias}SelectedNode.{$this->rootCol} + " ); $childrenQuery = $this->connection->createQueryBuilder() ->select([ + "{$queryAlias}ChildNode." . $this->pkCol, "{$queryAlias}ChildNode." . $this->leftCol, "{$queryAlias}ChildNode." . $this->rightCol, "{$queryAlias}ChildNode." . $this->levelCol, + "{$queryAlias}ChildNode." . $this->rootCol, ]) ->from($this->connection->quoteIdentifier($tableExpression), "{$queryAlias}ChildNode") ->innerJoin( @@ -181,19 +200,35 @@ public function createSubtreeThroughMultipleNodesQueryBuilder(string $tableExpre " {$queryAlias}ChildNode.{$this->leftCol} > {$queryAlias}SelectedNode.{$this->leftCol} AND {$queryAlias}ChildNode.{$this->rightCol} < {$queryAlias}SelectedNode.{$this->rightCol} - AND {$queryAlias}ChildNode.{$this->levelCol} <= ({$queryAlias}SelectedNode.{$this->levelCol} + :{$queryAlias}maxChildLevel)" + AND {$queryAlias}ChildNode.{$this->levelCol} <= ({$queryAlias}SelectedNode.{$this->levelCol} + :{$queryAlias}maxChildLevel) + AND {$queryAlias}ChildNode.{$this->rootCol} = {$queryAlias}SelectedNode.{$this->rootCol} + " ); - $idQuery = $this->connection->createQueryBuilder() - ->select("{$queryAlias}Group.{$this->pkCol}") - ->from($this->connection->quoteIdentifier($tableExpression), "{$queryAlias}Group") + $rootQuery = $this->connection->createQueryBuilder() + ->select([ + "{$queryAlias}RootNode." . $this->pkCol, + "{$queryAlias}RootNode." . $this->leftCol, + "{$queryAlias}RootNode." . $this->rightCol, + "{$queryAlias}RootNode." . $this->levelCol, + "{$queryAlias}RootNode." . $this->rootCol, + ]) + ->from($this->connection->quoteIdentifier($tableExpression), "{$queryAlias}RootNode") ->innerJoin( - "{$queryAlias}Group", - '((' . $childrenQuery->getSQL() . ') UNION (' . $siblingQuery->getSQL() . '))', - "{$queryAlias}SourceNode", + "{$queryAlias}RootNode", + '(' . $directNodeSubSelect->getSQL() . ')', + "{$queryAlias}SelectedNode", + " + {$queryAlias}RootNode.{$this->levelCol} = 0 + AND {$queryAlias}RootNode.{$this->rootCol} = {$queryAlias}SelectedNode.{$this->rootCol} " - {$queryAlias}Group.{$this->leftCol} <= {$queryAlias}SourceNode.{$this->leftCol} - AND {$queryAlias}Group.{$this->rightCol} >= {$queryAlias}SourceNode.{$this->rightCol}" + ); + + $idQuery = $this->connection->createQueryBuilder() + ->select("{$queryAlias}Group.{$this->pkCol}") + ->from( + '((' . $childrenQuery->getSQL() . ') UNION (' . $parentQuery->getSQL() . ') UNION (' . $rootQuery->getSQL() . ')) ', + "{$queryAlias}Group" ) ->groupBy("{$queryAlias}Group.id"); diff --git a/tests/NestedSetQueryFactoryTest.php b/tests/NestedSetQueryFactoryTest.php index c6b1dcf..14b0089 100644 --- a/tests/NestedSetQueryFactoryTest.php +++ b/tests/NestedSetQueryFactoryTest.php @@ -19,6 +19,7 @@ public function setUp() $connection = \NestedSetBootstrap::getConnection(); \NestedSetBootstrap::importTable(); \NestedSetBootstrap::insertDemoTree(); + \NestedSetBootstrap::insertDemoTree(2); $this->queryFactory = NestedSetFactory::createQueryFactory($connection, new NestedSetConfig('id', 'left', 'right', 'level')); } @@ -99,11 +100,47 @@ public function test_fetch_all_roots() $rows = $qb->execute()->fetchAll(); - $this->assertCount(1, $rows); + $this->assertCount(2, $rows); $this->assertEquals('Clothing', $rows[0]['name']); + $this->assertEquals('Clothing', $rows[1]['name']); + } + + public function test_fetch_subtree_with_root_only_selected() + { + $qb = $this->queryFactory + ->createSubtreeThroughMultipleNodesQueryBuilder('tree', 't', 'root_id', [1]) + ->select('*'); + + $this->assertSubTree( + [ + 'Clothing', + 'Mens', + 'Women', + ], + $qb->execute()->fetchAll() + ); + } + + public function test_fetch_subtree_with_a_single_selected_node_slacks() + { + $qb = $this->queryFactory + ->createSubtreeThroughMultipleNodesQueryBuilder('tree', 't', 'root_id', [5]) + ->select('*'); + + $this->assertSubTree( + [ + 'Clothing', + 'Mens', + 'Suits', + 'Slacks', + 'Jackets', + 'Women', + ], + $qb->execute()->fetchAll() + ); } - public function test_fetch_subtree_with_selected_nodes() + public function test_fetch_subtree_with_selected_nodes_mens_and_dresses() { $qb = $this->queryFactory ->createSubtreeThroughMultipleNodesQueryBuilder('tree', 't', 'root_id', [2, 7]) @@ -123,7 +160,10 @@ public function test_fetch_subtree_with_selected_nodes() ], $qb->execute()->fetchAll() ); + } + public function test_fetch_subtree_with_selected_nodes_mens_and_women() + { $qb = $this->queryFactory ->createSubtreeThroughMultipleNodesQueryBuilder('tree', 't', 'root_id', [3, 2]) ->select('*'); @@ -142,7 +182,7 @@ public function test_fetch_subtree_with_selected_nodes() ); } - public function test_fetch_subtree_with_selected_nodes_uses_the_depth_parameter() + public function test_fetch_subtree_with_selected_nodes_with_a_two_as_a_depth_parameter() { $qb = $this->queryFactory ->createSubtreeThroughMultipleNodesQueryBuilder('tree', 't', 'root_id', [2, 3], 2) @@ -164,7 +204,10 @@ public function test_fetch_subtree_with_selected_nodes_uses_the_depth_parameter( ], $qb->execute()->fetchAll() ); + } + public function test_fetch_subtree_with_selected_nodes_with_a_zero_depth_parameter() + { $qb = $this->queryFactory ->createSubtreeThroughMultipleNodesQueryBuilder('tree', 't', 'root_id', [3, 2], 0) ->select('*'); @@ -181,10 +224,10 @@ public function test_fetch_subtree_with_selected_nodes_uses_the_depth_parameter( private function assertSubTree(array $expectedNames, array $rows) { - $this->assertCount(count($expectedNames), $rows, print_r($rows, true)); + $names = array_map(function (array $node) { + return $node['name']; + }, $rows); - foreach ($expectedNames as $index => $name) { - $this->assertEquals($name, $rows[$index]['name']); - } + $this->assertEquals($expectedNames, $names, 'Got: ' . print_r($names, true) . "\n and expected: " . print_r($expectedNames, true)); } } diff --git a/tests/_bootstrap.php b/tests/_bootstrap.php index b143103..a71b78f 100644 --- a/tests/_bootstrap.php +++ b/tests/_bootstrap.php @@ -109,7 +109,7 @@ public static function insertDemoTree(int $rootId = 1) foreach ($data as list($id, $left, $right, $level, $name)) { self::getConnection()->insert('tree', [ - '`id`' => $id, + '`id`' => $id + (20 * ($rootId - 1)), '`left`' => $left, '`right`' => $right, '`level`' => $level,