diff --git a/core/dbobjectsearch.class.php b/core/dbobjectsearch.class.php index b724da1ca..4d4bc761c 100644 --- a/core/dbobjectsearch.class.php +++ b/core/dbobjectsearch.class.php @@ -1101,22 +1101,39 @@ class DBObjectSearch extends DBSearch public function Filter($sClassAlias, DBSearch $oFilter) { // If the conditions are the correct ones for Intersect - if (MetaModel::IsParentClass($oFilter->GetFirstJoinedClass(),$this->GetFirstJoinedClass())) - { + if (MetaModel::IsParentClass($oFilter->GetFirstJoinedClass(), $this->GetFirstJoinedClass())) { return $this->Intersect($oFilter); } - /** @var \DBObjectSearch $oFilteredSearch */ - $oFilteredSearch = $this->DeepClone(); - $oFilterExpression = self::FilterSubClass($oFilteredSearch, $sClassAlias, $oFilter, $this->m_aClasses); - if ($oFilterExpression === false) - { - throw new CoreException("Limitation: cannot filter search"); + if ($oFilter instanceof DBUnionSearch) { + $aFilters = $oFilter->GetSearches(); + } else { + $aFilters = [$oFilter]; } - $oFilteredSearch->AddConditionExpression($oFilterExpression); + $aSearches = []; + foreach ($aFilters as $oRightFilter) { + /** @var \DBObjectSearch $oFilteredSearch */ + $oFilteredSearch = $this->DeepClone(); + $oFilterExpression = self::FilterSubClass($oFilteredSearch, $sClassAlias, $oRightFilter, $this->m_aClasses); + if ($oFilterExpression === false) { + throw new CoreException("Limitation: cannot filter search"); + } - return $oFilteredSearch; + $oFilteredSearch->AddConditionExpression($oFilterExpression); + $aSearches[] = $oFilteredSearch; + } + + if (count($aSearches) == 0) { + throw new CoreException('Filtering '.$this->ToOQL().' by '.$oFilter->ToOQL().' failed'); + } + + if (count($aSearches) == 1) { + // return a DBObjectSearch + return $aSearches[0]; + } + + return new DBUnionSearch($aSearches); } /** @@ -1182,22 +1199,10 @@ class DBObjectSearch extends DBSearch * @throws \CoreException */ public function Intersect(DBSearch $oFilter) - { - return $this->IntersectSubClass($oFilter, $this->m_aClasses); - } - - /** - * @param \DBSearch $oFilter - * @param array $aRootClasses classes of the root search (for aliases) - * - * @return \DBUnionSearch|mixed - * @throws \CoreException - */ - protected function IntersectSubClass(DBSearch $oFilter, $aRootClasses) { if ($oFilter instanceof DBUnionSearch) { - // Develop! + // Develop! $aFilters = $oFilter->GetSearches(); } else @@ -1208,56 +1213,61 @@ class DBObjectSearch extends DBSearch $aSearches = array(); foreach ($aFilters as $oRightFilter) { - // Limitation: the queried class must be the first declared class - if ($oRightFilter->GetFirstJoinedClassAlias() != $oRightFilter->GetClassAlias()) - { - throw new CoreException("Limitation: cannot merge two queries if the queried class ({$oRightFilter->GetClass()} AS {$oRightFilter->GetClassAlias()}) is not the first joined class ({$oRightFilter->GetFirstJoinedClass()} AS {$oRightFilter->GetFirstJoinedClassAlias()})"); - } - - /** @var \DBObjectSearch $oLeftFilter */ - $oLeftFilter = $this->DeepClone(); - $oRightFilter = $oRightFilter->DeepClone(); - - $bAllowAllData = ($oLeftFilter->IsAllDataAllowed() && $oRightFilter->IsAllDataAllowed()); - if ($bAllowAllData) - { - $oLeftFilter->AllowAllData(); - } - - if ($oLeftFilter->GetFirstJoinedClass() != $oRightFilter->GetClass()) - { - if (MetaModel::IsParentClass($oLeftFilter->GetFirstJoinedClass(), $oRightFilter->GetClass())) - { - // Specialize $oLeftFilter - $oLeftFilter->ChangeClass($oRightFilter->GetClass(), $oLeftFilter->GetFirstJoinedClassAlias()); - } - elseif (MetaModel::IsParentClass($oRightFilter->GetFirstJoinedClass(), $oLeftFilter->GetClass())) - { - // Specialize $oRightFilter - $oRightFilter->ChangeClass($oLeftFilter->GetFirstJoinedClass()); - } - else - { - throw new CoreException("Attempting to merge a filter of class '{$oLeftFilter->GetClass()}' with a filter of class '{$oRightFilter->GetClass()}'"); - } - } - - $aAliasTranslation = array(); - $oLeftFilter->RenameNestedQueriesAliasesInNameSpace($aRootClasses, $aAliasTranslation); - $oLeftFilter->MergeWith_InNamespace($oRightFilter, $aRootClasses, $aAliasTranslation); - $oRightFilter->RenameNestedQueriesAliasesInNameSpace($aRootClasses, $aAliasTranslation); - $oLeftFilter->TransferConditionExpression($oRightFilter, $aAliasTranslation); - $aSearches[] = $oLeftFilter; + $aSearches[] = $this->IntersectSubClass($oRightFilter, $this->m_aClasses); } + if (count($aSearches) == 1) { // return a DBObjectSearch return $aSearches[0]; } - else - { - return new DBUnionSearch($aSearches); + + return new DBUnionSearch($aSearches); + } + + /** + * @param \DBObjectSearch $oRightFilter + * @param array $aRootClasses classes of the root search (for aliases) + * + * @return \DBObjectSearch + * @throws \CoreException + */ + protected function IntersectSubClass(DBObjectSearch $oRightFilter, array $aRootClasses): DBObjectSearch + { + // Limitation: the queried class must be the first declared class + if ($oRightFilter->GetFirstJoinedClassAlias() != $oRightFilter->GetClassAlias()) { + throw new CoreException("Limitation: cannot merge two queries if the queried class ({$oRightFilter->GetClass()} AS {$oRightFilter->GetClassAlias()}) is not the first joined class ({$oRightFilter->GetFirstJoinedClass()} AS {$oRightFilter->GetFirstJoinedClassAlias()})"); } + + /** @var \DBObjectSearch $oLeftFilter */ + $oLeftFilter = $this->DeepClone(); + /** @var DBObjectSearch $oRightFilter */ + $oRightFilter = $oRightFilter->DeepClone(); + + $bAllowAllData = ($oLeftFilter->IsAllDataAllowed() && $oRightFilter->IsAllDataAllowed()); + if ($bAllowAllData) { + $oLeftFilter->AllowAllData(); + } + + if ($oLeftFilter->GetFirstJoinedClass() != $oRightFilter->GetClass()) { + if (MetaModel::IsParentClass($oLeftFilter->GetFirstJoinedClass(), $oRightFilter->GetClass())) { + // Specialize $oLeftFilter + $oLeftFilter->ChangeClass($oRightFilter->GetClass(), $oLeftFilter->GetFirstJoinedClassAlias()); + } elseif (MetaModel::IsParentClass($oRightFilter->GetFirstJoinedClass(), $oLeftFilter->GetClass())) { + // Specialize $oRightFilter + $oRightFilter->ChangeClass($oLeftFilter->GetFirstJoinedClass()); + } else { + throw new CoreException("Attempting to merge a filter of class '{$oLeftFilter->GetClass()}' with a filter of class '{$oRightFilter->GetClass()}'"); + } + } + + $aAliasTranslation = array(); + $oLeftFilter->RenameNestedQueriesAliasesInNameSpace($aRootClasses, $aAliasTranslation); + $oLeftFilter->MergeWith_InNamespace($oRightFilter, $aRootClasses, $aAliasTranslation); + $oRightFilter->RenameNestedQueriesAliasesInNameSpace($aRootClasses, $aAliasTranslation); + $oLeftFilter->TransferConditionExpression($oRightFilter, $aAliasTranslation); + + return $oLeftFilter; } /** diff --git a/tests/php-unit-tests/unitary-tests/core/DBUnionSearchTest.php b/tests/php-unit-tests/unitary-tests/core/DBUnionSearchTest.php index ba2e6c565..5a622bd25 100644 --- a/tests/php-unit-tests/unitary-tests/core/DBUnionSearchTest.php +++ b/tests/php-unit-tests/unitary-tests/core/DBUnionSearchTest.php @@ -17,10 +17,10 @@ class DBUnionSearchTest extends ItopDataTestCase public function testFilterOnFirstSelectedClass() { - $sSourceOQL = 'SELECT `Person`, `Location` FROM Person AS `Person` JOIN Location AS `Location` ON `Person`.location_id = `Location`.id WHERE 1'; + $sSourceOQL = 'SELECT `Person`, `Location` FROM Person AS `Person` JOIN Location AS `Location` ON `Person`.location_id = `Location`.id WHERE (`Location`.`id` = 1)'; $oSearch = DBSearch::FromOQL($sSourceOQL); - $sFilterOQL = 'SELECT `Person` FROM Person AS `Person` WHERE (`Person`.`id` = 1) UNION SELECT `Person` FROM Person AS `Person` WHERE (`Person`.`id` = 1)'; + $sFilterOQL = 'SELECT `Person` FROM Person AS `Person` WHERE (`Person`.`id` = 2) UNION SELECT `Person` FROM Person AS `Person` WHERE (`Person`.`id` = 3)'; $oVisibleObjects = DBSearch::FromOQL($sFilterOQL); $sClassAlias = 'Person'; $oVisibleObjects->AllowAllData(); @@ -38,6 +38,7 @@ class DBUnionSearchTest extends ItopDataTestCase /** * Ignored test (provokes PHP Error) * + * @dataProvider FilterOnSecondSelectedClassProvider * @return void * @throws \CoreException * @throws \MissingQueryArgument @@ -45,22 +46,71 @@ class DBUnionSearchTest extends ItopDataTestCase * @throws \MySQLHasGoneAwayException * @throws \OQLException */ - public function FilterOnSecondSelectedClass() + public function testFilterOnSecondSelectedClass($sSourceOQL, $sClassAlias, $sFilterOQL, $sExpected) { - $sSourceOQL = 'SELECT `Person`, `Location` FROM Person JOIN Location ON `Person`.location_id = `Location`.id'; $oSearch = DBSearch::FromOQL($sSourceOQL); - $sFilterOQL = 'SELECT Location UNION SELECT Location'; $oVisibleObjects = DBSearch::FromOQL($sFilterOQL); - $sClassAlias = 'Location'; $oVisibleObjects->AllowAllData(); $oSearch = $oSearch->Filter($sClassAlias, $oVisibleObjects); - // $oSearch->ToOQL(); + $sResult = $oSearch->ToOQL(); + $this->debug($sResult); + + $this->assertEquals($sExpected, $sResult); $oSet = new DBObjectSet($oSearch); $oSet->CountWithLimit(1); $this->assertTrue(true); } + + public function FilterOnSecondSelectedClassProvider() + { + return [ + [ + 'sSourceOQL' => "SELECT P1, L1 FROM Person AS P1 JOIN Location AS L1 ON P1.location_id = L1.id WHERE L1.id = 1", + 'sClassAlias' => "L1", + 'sFilterOQL' => "SELECT Location AS L2 WHERE L2.id = 2 UNION SELECT Location AS L3 WHERE L3.id = 3", + 'sExpected' => "SELECT `P1`, `L1` FROM Person AS `P1` JOIN Location AS `L1` ON `P1`.location_id = `L1`.id WHERE ((`L1`.`id` = 1) AND (`L1`.`id` = 2)) UNION SELECT `P1`, `L1` FROM Person AS `P1` JOIN Location AS `L1` ON `P1`.location_id = `L1`.id WHERE ((`L1`.`id` = 1) AND (`L1`.`id` = 3))", + ], + [ + 'sSourceOQL' => 'SELECT L1, P1 FROM Person AS P1 JOIN Location AS L1 ON P1.location_id = L1.id WHERE L1.id = 1', + 'sClassAlias' => 'L1', + 'sFilterOQL' => 'SELECT Location AS L2 WHERE L2.id = 2 UNION SELECT Location AS L3 WHERE L3.id = 3', + 'sExpected' => 'SELECT `L1`, `P1` FROM Person AS `P1` JOIN Location AS `L1` ON `P1`.location_id = `L1`.id WHERE ((`L1`.`id` = 1) AND (`L1`.`id` = 2)) UNION SELECT `L1`, `P1` FROM Person AS `P1` JOIN Location AS `L1` ON `P1`.location_id = `L1`.id WHERE ((`L1`.`id` = 1) AND (`L1`.`id` = 3))', + ], + [ + 'sSourceOQL' => "SELECT L1,O1 FROM Location AS L1 JOIN Organization AS O1 ON L1.org_id=O1.id JOIN Organization AS O2 ON O1.parent_id = O2.id WHERE L1.name != 'l1-name' AND O1.name != 'o1-name' AND ISNULL(O2.name) != 0", + 'sClassAlias' => "O1", + 'sFilterOQL' => "SELECT Organization AS O1 WHERE O1.id = 2 UNION SELECT Organization AS O2 WHERE O2.id = 3", + 'sExpected' => "SELECT `L1`, `O1` FROM Location AS `L1` JOIN Organization AS `O1` ON `L1`.org_id = `O1`.id JOIN Organization AS `O2` ON `O1`.parent_id = `O2`.id WHERE ((((`L1`.`name` != 'l1-name') AND (`O1`.`name` != 'o1-name')) AND (ISNULL(`O2`.`name`) != 0)) AND (`O1`.`id` = 2)) UNION SELECT `L1`, `O1` FROM Location AS `L1` JOIN Organization AS `O1` ON `L1`.org_id = `O1`.id JOIN Organization AS `O2` ON `O1`.parent_id = `O2`.id WHERE ((((`L1`.`name` != 'l1-name') AND (`O1`.`name` != 'o1-name')) AND (ISNULL(`O2`.`name`) != 0)) AND (`O1`.`id` = 3))", + ], + [ + 'sSourceOQL' => "SELECT L1,O2 FROM Location AS L1 JOIN Organization AS O1 ON L1.org_id=O1.id JOIN Organization AS O2 ON O1.parent_id = O2.id WHERE L1.name != 'l1-name' AND O1.name != 'o1-name' AND ISNULL(O2.name) != 0", + 'sClassAlias' => 'O2', + 'sFilterOQL' => 'SELECT Organization AS O1 WHERE O1.id = 2 UNION SELECT Organization AS O2 WHERE O2.id = 3', + 'sExpected' => "SELECT `L1`, `O2` FROM Location AS `L1` JOIN Organization AS `O1` ON `L1`.org_id = `O1`.id JOIN Organization AS `O2` ON `O1`.parent_id = `O2`.id WHERE ((((`L1`.`name` != 'l1-name') AND (`O1`.`name` != 'o1-name')) AND (ISNULL(`O2`.`name`) != 0)) AND (`O2`.`id` = 2)) UNION SELECT `L1`, `O2` FROM Location AS `L1` JOIN Organization AS `O1` ON `L1`.org_id = `O1`.id JOIN Organization AS `O2` ON `O1`.parent_id = `O2`.id WHERE ((((`L1`.`name` != 'l1-name') AND (`O1`.`name` != 'o1-name')) AND (ISNULL(`O2`.`name`) != 0)) AND (`O2`.`id` = 3))", + ], + [ + 'sSourceOQL' => "SELECT L1,O1 FROM Location AS L1 JOIN Organization AS O1 ON L1.org_id=O1.id JOIN Organization AS O2 ON O1.parent_id = O2.id WHERE L1.name != 'l1-name' AND O1.name != 'o1-name' AND ISNULL(O2.name) != 0", + 'sClassAlias' => 'O2', + 'sFilterOQL' => 'SELECT Organization AS O1 WHERE O1.id = 2 UNION SELECT Organization AS O2 WHERE O2.id = 3', + // This is another problem, we should not be able to filter on not selected classes + 'sExpected' => "SELECT `L1`, `O1` FROM Location AS `L1` JOIN Organization AS `O1` ON `L1`.org_id = `O1`.id JOIN Organization AS `O2` ON `O1`.parent_id = `O2`.id WHERE ((((`L1`.`name` != 'l1-name') AND (`O1`.`name` != 'o1-name')) AND (ISNULL(`O2`.`name`) != 0)) AND (`O2`.`id` = 2)) UNION SELECT `L1`, `O1` FROM Location AS `L1` JOIN Organization AS `O1` ON `L1`.org_id = `O1`.id JOIN Organization AS `O2` ON `O1`.parent_id = `O2`.id WHERE ((((`L1`.`name` != 'l1-name') AND (`O1`.`name` != 'o1-name')) AND (ISNULL(`O2`.`name`) != 0)) AND (`O2`.`id` = 3))", + ], + [ + 'sSourceOQL' => "SELECT P1, O1 FROM Person AS P1 JOIN Organization AS O1 ON P1.org_id = O1.id JOIN Location AS L1 ON P1.location_id = L1.id JOIN Organization AS O2 ON L1.org_id = O2.id WHERE L1.name != '' AND P1.name != '' AND O1.name != '' AND O2.name != ''", + 'sClassAlias' => 'O1', + 'sFilterOQL' => 'SELECT Organization AS O1 WHERE O1.id = 2 UNION SELECT Organization AS O2 WHERE O2.id = 3', + 'sExpected' => "SELECT `P1`, `O1` FROM Person AS `P1` JOIN Organization AS `O1` ON `P1`.org_id = `O1`.id JOIN Location AS `L1` ON `P1`.location_id = `L1`.id JOIN Organization AS `O2` ON `L1`.org_id = `O2`.id WHERE (((((`L1`.`name` != '') AND (`P1`.`name` != '')) AND (`O1`.`name` != '')) AND (`O2`.`name` != '')) AND (`O1`.`id` = 2)) UNION SELECT `P1`, `O1` FROM Person AS `P1` JOIN Organization AS `O1` ON `P1`.org_id = `O1`.id JOIN Location AS `L1` ON `P1`.location_id = `L1`.id JOIN Organization AS `O2` ON `L1`.org_id = `O2`.id WHERE (((((`L1`.`name` != '') AND (`P1`.`name` != '')) AND (`O1`.`name` != '')) AND (`O2`.`name` != '')) AND (`O1`.`id` = 3))", + ], + [ + 'sSourceOQL' => "SELECT P1, O2 FROM Person AS P1 JOIN Organization AS O1 ON P1.org_id = O1.id JOIN Location AS L1 ON P1.location_id = L1.id JOIN Organization AS O2 ON L1.org_id = O2.id WHERE L1.name != '' AND P1.name != '' AND O1.name != '' AND O2.name != ''", + 'sClassAlias' => 'O2', + 'sFilterOQL' => 'SELECT Organization AS O1 WHERE O1.id = 2 UNION SELECT Organization AS O2 WHERE O2.id = 3', + 'sExpected' => "SELECT `P1`, `O2` FROM Person AS `P1` JOIN Organization AS `O1` ON `P1`.org_id = `O1`.id JOIN Location AS `L1` ON `P1`.location_id = `L1`.id JOIN Organization AS `O2` ON `L1`.org_id = `O2`.id WHERE (((((`L1`.`name` != '') AND (`P1`.`name` != '')) AND (`O1`.`name` != '')) AND (`O2`.`name` != '')) AND (`O2`.`id` = 2)) UNION SELECT `P1`, `O2` FROM Person AS `P1` JOIN Organization AS `O1` ON `P1`.org_id = `O1`.id JOIN Location AS `L1` ON `P1`.location_id = `L1`.id JOIN Organization AS `O2` ON `L1`.org_id = `O2`.id WHERE (((((`L1`.`name` != '') AND (`P1`.`name` != '')) AND (`O1`.`name` != '')) AND (`O2`.`name` != '')) AND (`O2`.`id` = 3))", + ], + ]; + } }