/** * A union of DBObjectSearches * * @copyright Copyright (C) 2015-2017 Combodo SARL * @license http://opensource.org/licenses/AGPL-3.0 */ class DBUnionSearch extends DBSearch { protected $aSearches; // source queries protected $aSelectedClasses; // alias => classes (lowest common ancestors) computed at construction public function __construct($aSearches) { if (count ($aSearches) == 0) { throw new CoreException('A DBUnionSearch must be made of at least one search'); } $this->aSearches = array(); foreach ($aSearches as $oSearch) { if ($oSearch instanceof DBUnionSearch) { foreach ($oSearch->aSearches as $oSubSearch) { $this->aSearches[] = $oSubSearch->DeepClone(); } } else { $this->aSearches[] = $oSearch->DeepClone(); } } $this->ComputeSelectedClasses(); } public function AllowAllData() { foreach ($this->aSearches as $oSearch) { $oSearch->AllowAllData(); } } public function IsAllDataAllowed() { foreach ($this->aSearches as $oSearch) { if ($oSearch->IsAllDataAllowed() === false) return false; } return true; } public function SetArchiveMode($bEnable) { foreach ($this->aSearches as $oSearch) { $oSearch->SetArchiveMode($bEnable); } parent::SetArchiveMode($bEnable); } public function SetShowObsoleteData($bShow) { foreach ($this->aSearches as $oSearch) { $oSearch->SetShowObsoleteData($bShow); } parent::SetShowObsoleteData($bShow); } /** * Find the lowest common ancestor for each of the selected class */ protected function ComputeSelectedClasses() { // 1 - Collect all the column/classes $aColumnToClasses = array(); foreach ($this->aSearches as $iPos => $oSearch) { $aSelected = array_values($oSearch->GetSelectedClasses()); if ($iPos != 0) { if (count($aSelected) < count($aColumnToClasses)) { throw new Exception('Too few selected classes in the subquery #'.($iPos+1)); } if (count($aSelected) > count($aColumnToClasses)) { throw new Exception('Too many selected classes in the subquery #'.($iPos+1)); } } foreach ($aSelected as $iColumn => $sClass) { $aColumnToClasses[$iColumn][] = $sClass; } } // 2 - Build the index column => alias $oFirstSearch = $this->aSearches[0]; $aColumnToAlias = array_keys($oFirstSearch->GetSelectedClasses()); // 3 - Compute alias => lowest common ancestor $this->aSelectedClasses = array(); foreach ($aColumnToClasses as $iColumn => $aClasses) { $sAlias = $aColumnToAlias[$iColumn]; $sAncestor = MetaModel::GetLowestCommonAncestor($aClasses); if (is_null($sAncestor)) { throw new Exception('Could not find a common ancestor for the column '.($iColumn+1).' (Classes: '.implode(', ', $aClasses).')'); } $this->aSelectedClasses[$sAlias] = $sAncestor; } } public function GetSearches() { return $this->aSearches; } /** * Limited to the selected classes */ public function GetClassName($sAlias) { if (array_key_exists($sAlias, $this->aSelectedClasses)) { return $this->aSelectedClasses[$sAlias]; } else { throw new CoreException("Invalid class alias '$sAlias'"); } } public function GetClass() { return reset($this->aSelectedClasses); } public function GetClassAlias() { reset($this->aSelectedClasses); return key($this->aSelectedClasses); } /** * Change the class (only subclasses are supported as of now, because the conditions must fit the new class) * Defaults to the first selected class * Only the selected classes can be changed */ public function ChangeClass($sNewClass, $sAlias = null) { if (is_null($sAlias)) { $sAlias = $this->GetClassAlias(); } elseif (!array_key_exists($sAlias, $this->aSelectedClasses)) { // discard silently - necessary when recursing (??? copied from DBObjectSearch) return; } // 1 - identify the impacted column $iColumn = array_search($sAlias, array_keys($this->aSelectedClasses)); // 2 - change for each search foreach ($this->aSearches as $oSearch) { $aSearchAliases = array_keys($oSearch->GetSelectedClasses()); $sSearchAlias = $aSearchAliases[$iColumn]; $oSearch->ChangeClass($sNewClass, $sSearchAlias); } // 3 - record the change $this->aSelectedClasses[$sAlias] = $sNewClass; } public function GetSelectedClasses() { return $this->aSelectedClasses; } /** * @param array $aSelectedClasses array of aliases * @throws CoreException */ public function SetSelectedClasses($aSelectedClasses) { // 1 - change for each search foreach ($this->aSearches as $oSearch) { // Throws an exception if not valid $oSearch->SetSelectedClasses($aSelectedClasses); } // 2 - update the lowest common ancestors $this->ComputeSelectedClasses(); } /** * Change any alias of the query tree * * @param $sOldName * @param $sNewName * @return bool True if the alias has been found and changed */ public function RenameAlias($sOldName, $sNewName) { $bRet = false; foreach ($this->aSearches as $oSearch) { $bRet = $oSearch->RenameAlias($sOldName, $sNewName) || $bRet; } return $bRet; } public function IsAny() { $bIsAny = true; foreach ($this->aSearches as $oSearch) { if (!$oSearch->IsAny()) { $bIsAny = false; break; } } return $bIsAny; } public function ResetCondition() { foreach ($this->aSearches as $oSearch) { $oSearch->ResetCondition(); } } public function MergeConditionExpression($oExpression) { $aAliases = array_keys($this->aSelectedClasses); foreach ($this->aSearches as $iSearchIndex => $oSearch) { $oClonedExpression = $oExpression->DeepClone(); if ($iSearchIndex != 0) { foreach (array_keys($oSearch->GetSelectedClasses()) as $iColumn => $sSearchAlias) { $oClonedExpression->RenameAlias($aAliases[$iColumn], $sSearchAlias); } } $oSearch->MergeConditionExpression($oClonedExpression); } } public function AddConditionExpression($oExpression) { $aAliases = array_keys($this->aSelectedClasses); foreach ($this->aSearches as $iSearchIndex => $oSearch) { $oClonedExpression = $oExpression->DeepClone(); if ($iSearchIndex != 0) { foreach (array_keys($oSearch->GetSelectedClasses()) as $iColumn => $sSearchAlias) { $oClonedExpression->RenameAlias($aAliases[$iColumn], $sSearchAlias); } } $oSearch->AddConditionExpression($oClonedExpression); } } public function AddNameCondition($sName) { foreach ($this->aSearches as $oSearch) { $oSearch->AddNameCondition($sName); } } public function AddCondition($sFilterCode, $value, $sOpCode = null) { foreach ($this->aSearches as $oSearch) { $oSearch->AddCondition($sFilterCode, $value, $sOpCode); } } /** * Specify a condition on external keys or link sets * @param sAttSpec Can be either an attribute code or extkey->[sAttSpec] or linkset->[sAttSpec] and so on, recursively * Example: infra_list->ci_id->location_id->country * @param value The value to match (can be an array => IN(val1, val2...) * @return void */ public function AddConditionAdvanced($sAttSpec, $value) { foreach ($this->aSearches as $oSearch) { $oSearch->AddConditionAdvanced($sAttSpec, $value); } } public function AddCondition_FullText($sFullText) { foreach ($this->aSearches as $oSearch) { $oSearch->AddCondition_FullText($sFullText); } } /** * @param DBObjectSearch $oFilter * @param $sExtKeyAttCode * @param int $iOperatorCode * @param null $aRealiasingMap array of => , for each alias that has changed * @throws CoreException * @throws CoreWarning */ public function AddCondition_PointingTo(DBObjectSearch $oFilter, $sExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS, &$aRealiasingMap = null) { foreach ($this->aSearches as $oSearch) { $oSearch->AddCondition_PointingTo($oFilter, $sExtKeyAttCode, $iOperatorCode, $aRealiasingMap); } } /** * @param DBObjectSearch $oFilter * @param $sForeignExtKeyAttCode * @param int $iOperatorCode * @param null $aRealiasingMap array of => , for each alias that has changed */ public function AddCondition_ReferencedBy(DBObjectSearch $oFilter, $sForeignExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS, &$aRealiasingMap = null) { foreach ($this->aSearches as $oSearch) { $oSearch->AddCondition_ReferencedBy($oFilter, $sForeignExtKeyAttCode, $iOperatorCode, $aRealiasingMap); } } public function Intersect(DBSearch $oFilter) { $aSearches = array(); foreach ($this->aSearches as $oSearch) { $aSearches[] = $oSearch->Intersect($oFilter); } return new DBUnionSearch($aSearches); } public function SetInternalParams($aParams) { foreach ($this->aSearches as $oSearch) { $oSearch->SetInternalParams($aParams); } } public function GetInternalParams() { $aParams = array(); foreach ($this->aSearches as $oSearch) { $aParams = array_merge($oSearch->GetInternalParams(), $aParams); } return $aParams; } public function GetQueryParams($bExcludeMagicParams = true) { $aParams = array(); foreach ($this->aSearches as $oSearch) { $aParams = array_merge($oSearch->GetQueryParams($bExcludeMagicParams), $aParams); } return $aParams; } public function ListConstantFields() { // Somewhat complex to implement for unions, for a poor benefit return array(); } /** * Turn the parameters (:xxx) into scalar values in order to easily * serialize a search */ public function ApplyParameters($aArgs) { foreach ($this->aSearches as $oSearch) { $oSearch->ApplyParameters($aArgs); } } /** * Overloads for query building */ public function ToOQL($bDevelopParams = false, $aContextParams = null, $bWithAllowAllFlag = false) { $aSubQueries = array(); foreach ($this->aSearches as $oSearch) { $aSubQueries[] = $oSearch->ToOQL($bDevelopParams, $aContextParams, $bWithAllowAllFlag); } $sRet = implode(' UNION ', $aSubQueries); return $sRet; } /** * Returns a new DBUnionSearch object where duplicates queries have been removed based on their OQLs * * @return \DBUnionSearch */ public function RemoveDuplicateQueries() { $aQueries = array(); $aSearches = array(); foreach ($this->GetSearches() as $oTmpSearch) { $sQuery = $oTmpSearch->ToOQL(true); if (!in_array($sQuery, $aQueries)) { $aQueries[] = $sQuery; $aSearches[] = $oTmpSearch; } } $oNewSearch = new DBUnionSearch($aSearches); return $oNewSearch; } //////////////////////////////////////////////////////////////////////////// // // Construction of the SQL queries // //////////////////////////////////////////////////////////////////////////// public function MakeDeleteQuery($aArgs = array()) { throw new Exception('MakeDeleteQuery is not implemented for the unions!'); } public function MakeUpdateQuery($aValues, $aArgs = array()) { throw new Exception('MakeUpdateQuery is not implemented for the unions!'); } protected function GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr = null, $aSelectedClasses = null) { if (count($this->aSearches) == 1) { return $this->aSearches[0]->GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr); } $aSQLQueries = array(); $aAliases = array_keys($this->aSelectedClasses); foreach ($this->aSearches as $iSearch => $oSearch) { $aSearchAliases = array_keys($oSearch->GetSelectedClasses()); // The selected classes from the query build perspective are the lowest common ancestors amongst the various queries // (used when it comes to determine which attributes must be selected) $aSearchSelectedClasses = array(); foreach ($aSearchAliases as $iColumn => $sSearchAlias) { $sAlias = $aAliases[$iColumn]; $aSearchSelectedClasses[$sSearchAlias] = $this->aSelectedClasses[$sAlias]; } if (is_null($aAttToLoad)) { $aQueryAttToLoad = null; } else { // (Eventually) Transform the aliases $aQueryAttToLoad = array(); foreach ($aAttToLoad as $sAlias => $aAttributes) { $iColumn = array_search($sAlias, $aAliases); $sQueryAlias = ($iColumn === false) ? $sAlias : $aSearchAliases[$iColumn]; $aQueryAttToLoad[$sQueryAlias] = $aAttributes; } } if (is_null($aGroupByExpr)) { $aQueryGroupByExpr = null; } else { // Clone (and eventually transform) the group by expressions $aQueryGroupByExpr = array(); $aTranslationData = array(); $aQueryColumns = array_keys($oSearch->GetSelectedClasses()); foreach ($aAliases as $iColumn => $sAlias) { $sQueryAlias = $aQueryColumns[$iColumn]; $aTranslationData[$sAlias]['*'] = $sQueryAlias; $aQueryGroupByExpr[$sAlias.'id'] = new FieldExpression('id', $sQueryAlias); } foreach ($aGroupByExpr as $sExpressionAlias => $oExpression) { $aQueryGroupByExpr[$sExpressionAlias] = $oExpression->Translate($aTranslationData, false, false); } } $oSubQuery = $oSearch->GetSQLQueryStructure($aQueryAttToLoad, false, $aQueryGroupByExpr, $aSearchSelectedClasses); if (count($aSearchAliases) > 1) { // Necessary to make sure that selected columns will match throughout all the queries // (default order of selected fields depending on the order of JOINS) $oSubQuery->SortSelectedFields(); } $aSQLQueries[] = $oSubQuery; } $oSQLQuery = new SQLUnionQuery($aSQLQueries, $aGroupByExpr); //MyHelpers::var_dump_html($oSQLQuery, true); //MyHelpers::var_dump_html($oSQLQuery->RenderSelect(), true); if (self::$m_bDebugQuery) $oSQLQuery->DisplayHtml(); return $oSQLQuery; } }