diff --git a/application/displayblock.class.inc.php b/application/displayblock.class.inc.php index ad3c75cc4..d03dfd291 100644 --- a/application/displayblock.class.inc.php +++ b/application/displayblock.class.inc.php @@ -1898,8 +1898,8 @@ class MenuBlock extends DisplayBlock } // Any style actions - // - Bulk actions on objects set (except unions...) - if ($iSetCount > 1 && !$this->GetFilter() instanceof DBUnionSearch) { + // - Bulk actions on objects set + if ($iSetCount > 1) { // Bulk actions for each selected classes (eg. "link" and "remote" on n:n relations) foreach ($aSelectedClasses as $sSelectedAlias => $sSelectedClass) { $sSelectedClassName = MetaModel::GetName($sSelectedClass); diff --git a/core/dbunionsearch.class.php b/core/dbunionsearch.class.php index 454333c94..485162476 100644 --- a/core/dbunionsearch.class.php +++ b/core/dbunionsearch.class.php @@ -25,7 +25,7 @@ class DBUnionSearch extends DBSearch { protected $aSearches; // source queries protected $aSelectedClasses; // alias => classes (lowest common ancestors) computed at construction - + protected $aColumnToAliases; /** * DBUnionSearch constructor. * @@ -94,11 +94,14 @@ class DBUnionSearch extends DBSearch /** * Find the lowest common ancestor for each of the selected class + * + * @throws \Exception */ protected function ComputeSelectedClasses() { // 1 - Collect all the column/classes - $aColumnToClasses = array(); + $aColumnToClasses = []; + $this->aColumnToAliases = []; foreach ($this->aSearches as $iPos => $oSearch) { $aSelected = array_values($oSearch->GetSelectedClasses()); @@ -117,7 +120,14 @@ class DBUnionSearch extends DBSearch foreach ($aSelected as $iColumn => $sClass) { - $aColumnToClasses[$iColumn][] = $sClass; + $aColumnToClasses[$iColumn][$iPos] = $sClass; + } + + // Store the aliases by column to map them later (the first query impose the aliases) + $aAliases = array_keys($oSearch->GetSelectedClasses()); + foreach ($aAliases as $iColumn => $sAlias) + { + $this->aColumnToAliases[$iColumn][$iPos] = $sAlias; } } @@ -126,7 +136,7 @@ class DBUnionSearch extends DBSearch $aColumnToAlias = array_keys($oFirstSearch->GetSelectedClasses()); // 3 - Compute alias => lowest common ancestor - $this->aSelectedClasses = array(); + $this->aSelectedClasses = []; foreach ($aColumnToClasses as $iColumn => $aClasses) { $sAlias = $aColumnToAlias[$iColumn]; @@ -215,15 +225,32 @@ class DBUnionSearch extends DBSearch /** * @param array $aSelectedClasses array of aliases - * @throws CoreException + * + * @throws \Exception */ public function SetSelectedClasses($aSelectedClasses) { + // Get the columns corresponding the given aliases + $aSelectedColumns = []; + $oFirstSearch = $this->aSearches[0]; + $aAliasesToColumn = array_flip(array_keys($oFirstSearch->GetSelectedClasses())); + foreach ($aSelectedClasses as $sSelectedAlias) { + if (!isset($aAliasesToColumn[$sSelectedAlias])) { + throw new CoreException("SetSelectedClasses: Invalid class alias $sSelectedAlias"); + } + $aSelectedColumns[] = $aAliasesToColumn[$sSelectedAlias]; + } + // 1 - change for each search - foreach ($this->aSearches as $oSearch) + foreach ($this->aSearches as $iPos => $oSearch) { + $aCurrentSelectedAliases = []; + foreach ($aSelectedColumns as $iColumn) { + $aCurrentSelectedAliases[] = $this->aColumnToAliases[$iColumn][$iPos]; + } + // Throws an exception if not valid - $oSearch->SetSelectedClasses($aSelectedClasses); + $oSearch->SetSelectedClasses($aCurrentSelectedAliases); } // 2 - update the lowest common ancestors $this->ComputeSelectedClasses(); diff --git a/tests/php-unit-tests/unitary-tests/core/DBSearchIntersectTest.php b/tests/php-unit-tests/unitary-tests/core/DBSearchIntersectTest.php index 5cb13ed19..b87c44de3 100644 --- a/tests/php-unit-tests/unitary-tests/core/DBSearchIntersectTest.php +++ b/tests/php-unit-tests/unitary-tests/core/DBSearchIntersectTest.php @@ -58,12 +58,11 @@ class DBSearchIntersectTest extends ItopTestCase 'alias' => "ApplicationSolution", 'result' => "SELECT `ApplicationSolution` FROM ApplicationSolution AS `ApplicationSolution` WHERE (`ApplicationSolution`.`org_id` = 3) UNION SELECT `BusinessProcess` FROM BusinessProcess AS `BusinessProcess` WHERE (`BusinessProcess`.`org_id` = 3)"); -// Bug to fix -// $aTests['Test union #2902'] = array( -// 'left' => "SELECT `L-1` FROM ServiceFamily AS `L-1` WHERE 1", -// 'right' => "SELECT `sf` FROM ServiceFamily AS `sf` JOIN Service AS `s` ON `s`.servicefamily_id = `sf`.id JOIN lnkCustomerContractToService AS `l1` ON `l1`.service_id = `s`.id JOIN CustomerContract AS `cc` ON `l1`.customercontract_id = `cc`.id WHERE (`cc`.`org_id` = 3) UNION SELECT `sf` FROM ServiceFamily AS `sf` JOIN Service AS `s` ON `s`.servicefamily_id = `sf`.id JOIN lnkCustomerContractToService AS `l1` ON `l1`.service_id = `s`.id JOIN CustomerContract AS `cc` ON `l1`.customercontract_id = `cc`.id JOIN Organization AS `child` ON `cc`.org_id = `child`.id JOIN Organization AS `root` ON `child`.parent_id BELOW `root`.id WHERE (`root`.`id` = 3)", -// 'alias' => "L-1", -// 'result' => "SELECT `L-1` FROM ServiceFamily AS `L-1` JOIN Service AS `s` ON `s`.servicefamily_id = `L-1`.id JOIN lnkCustomerContractToService AS `l1` ON `l1`.service_id = `s`.id JOIN CustomerContract AS `cc` ON `l1`.customercontract_id = `cc`.id WHERE (`cc`.`org_id` = 3) UNION SELECT `L-1` FROM ServiceFamily AS `L-1` JOIN Service AS `s` ON `s`.servicefamily_id = `L-1`.id JOIN lnkCustomerContractToService AS `l1` ON `l1`.service_id = `s`.id JOIN CustomerContract AS `cc` ON `l1`.customercontract_id = `cc`.id JOIN Organization AS `child` ON `cc`.org_id = `child`.id JOIN Organization AS `root` ON `child`.parent_id BELOW `root`.id WHERE (`root`.`id` = 3)"); + $aTests['Test union #2902'] = array( + 'left' => "SELECT `L-1` FROM ServiceFamily AS `L-1` WHERE 1", + 'right' => "SELECT `sf` FROM ServiceFamily AS `sf` JOIN Service AS `s` ON `s`.servicefamily_id = `sf`.id JOIN lnkCustomerContractToService AS `l1` ON `l1`.service_id = `s`.id JOIN CustomerContract AS `cc` ON `l1`.customercontract_id = `cc`.id WHERE (`cc`.`org_id` = 3) UNION SELECT `sf` FROM ServiceFamily AS `sf` JOIN Service AS `s` ON `s`.servicefamily_id = `sf`.id JOIN lnkCustomerContractToService AS `l1` ON `l1`.service_id = `s`.id JOIN CustomerContract AS `cc` ON `l1`.customercontract_id = `cc`.id JOIN Organization AS `child` ON `cc`.org_id = `child`.id JOIN Organization AS `root` ON `child`.parent_id BELOW `root`.id WHERE (`root`.`id` = 3)", + 'alias' => "L-1", + 'result' => "SELECT `L-1` FROM ServiceFamily AS `L-1` JOIN Service AS `s` ON `s`.servicefamily_id = `L-1`.id JOIN lnkCustomerContractToService AS `l1` ON `l1`.service_id = `s`.id JOIN CustomerContract AS `cc` ON `l1`.customercontract_id = `cc`.id WHERE (`cc`.`org_id` = 3) UNION SELECT `L-1` FROM ServiceFamily AS `L-1` JOIN Service AS `s` ON `s`.servicefamily_id = `L-1`.id JOIN lnkCustomerContractToService AS `l1` ON `l1`.service_id = `s`.id JOIN CustomerContract AS `cc` ON `l1`.customercontract_id = `cc`.id JOIN Organization AS `child` ON `cc`.org_id = `child`.id JOIN Organization AS `root` ON `child`.parent_id BELOW `root`.id WHERE (`root`.`id` = 3)"); $aTests['Multiple selected classes inverted'] = array( 'left' => "SELECT `L`, `P` FROM Person AS `P` JOIN Location AS `L` ON `P`.location_id = `L`.id WHERE 1", diff --git a/tests/php-unit-tests/unitary-tests/core/DBSearchTest.php b/tests/php-unit-tests/unitary-tests/core/DBSearchTest.php index 954c87e71..1d11700f7 100644 --- a/tests/php-unit-tests/unitary-tests/core/DBSearchTest.php +++ b/tests/php-unit-tests/unitary-tests/core/DBSearchTest.php @@ -30,6 +30,7 @@ namespace Combodo\iTop\Test\UnitTest\Core; use CMDBSource; use Combodo\iTop\Test\UnitTest\ItopDataTestCase; use CoreOqlMultipleResultsForbiddenException; +use DBObjectSet; use DBSearch; use Exception; use Expression; @@ -745,4 +746,30 @@ class DBSearchTest extends ItopDataTestCase $oSearch->MakeSelectQuery(); self::assertTrue(true); } + + /** + * @dataProvider QueriesProvider + * @param $sOQL + * + * @return void + */ + public function testQueries($sOQL) + { + $oSearch = DBSearch::FromOQL($sOQL); + $oSet = new DBObjectSet($oSearch); + if ($oSet->Count() > 0) { + $aSelectedAliases = array_keys($oSearch->GetSelectedClasses()); + $aFirstRow = $oSet->FetchAssoc(); + $aAliases = array_keys($aFirstRow); + $this->assertEquals($aSelectedAliases, $aAliases); + } + } + + public function QueriesProvider() + { + return [ + ['SELECT L,P FROM Person AS P JOIN Location AS L ON P.location_id=L.id'], + ['SELECT P,L FROM Person AS P JOIN Location AS L ON P.location_id=L.id'], + ]; + } } diff --git a/tests/php-unit-tests/unitary-tests/core/DBUnionSearchTest.php b/tests/php-unit-tests/unitary-tests/core/DBUnionSearchTest.php index 5a622bd25..73d29859b 100644 --- a/tests/php-unit-tests/unitary-tests/core/DBUnionSearchTest.php +++ b/tests/php-unit-tests/unitary-tests/core/DBUnionSearchTest.php @@ -15,6 +15,52 @@ use Combodo\iTop\Test\UnitTest\ItopDataTestCase; class DBUnionSearchTest extends ItopDataTestCase { + /** + * @dataProvider UnionSearchProvider + * + * @param $sOQL + * + * @return void + * @throws \CoreException + * @throws \OQLException + */ + public function testUnionSearch($sOQL) + { + $oSearch = DBSearch::FromOQL($sOQL); + + $oSet = new DBObjectSet($oSearch); + if ($oSet->Count() > 0) { + $aSelectedAliases = array_keys($oSearch->GetSelectedClasses()); + $aFirstRow = $oSet->FetchAssoc(); + $aAliases = array_keys($aFirstRow); + $this->assertEquals($aSelectedAliases, $aAliases); + } + + $aSelectedClasses = $oSearch->GetSelectedClasses(); + foreach ($aSelectedClasses as $sSelectedAlias => $sSelectedClass) { + $oSearchTest = $oSearch->DeepClone(); + $oSearchTest->SetSelectedClasses([$sSelectedAlias]); + $oSet = new DBObjectSet($oSearchTest); + if ($oSet->Count() > 0) { + $aSelectedAliases = array_keys($oSearchTest->GetSelectedClasses()); + $aFirstRow = $oSet->FetchAssoc(); + $aAliases = array_keys($aFirstRow); + $this->assertEquals($aSelectedAliases, $aAliases); + } + } + } + + public function UnionSearchProvider() + { + return [ + 'Same class' => ["SELECT Server UNION SELECT Server"], + 'different class same alias' => ['SELECT Server AS fci UNION SELECT VirtualMachine AS fci'], + 'different class no alias' => ['SELECT Server UNION SELECT VirtualMachine'], + 'multiple classes same alias' => ['SELECT `L`, `P` FROM Location AS `L` JOIN Person AS `P` ON `P`.location_id = `L`.id WHERE (`L`.`org_id` = 3) UNION SELECT `L`, `P` FROM Location AS `L` JOIN Person AS `P` ON `P`.location_id = `L`.id WHERE (`L`.`org_id` = 2)'], + 'multiple classes' => ['SELECT `L`, `P` FROM Location AS `L` JOIN Person AS `P` ON `P`.location_id = `L`.id WHERE (`L`.`org_id` = 3) UNION SELECT `L1`, `P1` FROM Person AS `P1` JOIN Location AS `L1` ON `P1`.location_id = `L1`.id WHERE (`P1`.`org_id` = 2)'], + ]; + } + public function testFilterOnFirstSelectedClass() { $sSourceOQL = 'SELECT `Person`, `Location` FROM Person AS `Person` JOIN Location AS `Location` ON `Person`.location_id = `Location`.id WHERE (`Location`.`id` = 1)';