mirror of
https://github.com/Combodo/iTop.git
synced 2026-02-13 07:24:13 +01:00
553 lines
15 KiB
PHP
553 lines
15 KiB
PHP
<?php
|
|
// Copyright (C) 2015-2017 Combodo SARL
|
|
//
|
|
// This file is part of iTop.
|
|
//
|
|
// iTop is free software; you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// iTop is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with iTop. If not, see <http://www.gnu.org/licenses/>
|
|
|
|
|
|
/**
|
|
* 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 <old-alias> => <new-alias>, 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 <old-alias> => <new-alias>, 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;
|
|
}
|
|
}
|