From 03f9a9fcac5485ca74ce8b46843d07a0f85cecf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eric=20Espi=C3=A9?= Date: Fri, 16 Feb 2018 12:59:35 +0000 Subject: [PATCH] =?UTF-8?q?N=C2=B01161=20-=20Add=20functions,=20order=20by?= =?UTF-8?q?=20and=20limits=20to=20DBSearch::MakeGroupByQuery()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SVN:trunk[5350] --- core/dbobjectsearch.class.php | 39 +- core/dbsearch.class.php | 59 ++- core/dbunionsearch.class.php | 48 ++- core/oql/expression.class.inc.php | 15 +- core/querybuildercontext.class.inc.php | 4 +- core/sqlobjectquery.class.inc.php | 32 +- core/sqlquery.class.inc.php | 14 +- core/sqlunionquery.class.inc.php | 52 ++- test/GroupByAndFunctions.php | 343 ++++++++++++++++++ test/core/DBSearchTest.php | 483 +++++++++++++++++++++++++ 10 files changed, 1044 insertions(+), 45 deletions(-) create mode 100644 test/GroupByAndFunctions.php create mode 100644 test/core/DBSearchTest.php diff --git a/core/dbobjectsearch.class.php b/core/dbobjectsearch.class.php index f1295407d..dbd40e605 100644 --- a/core/dbobjectsearch.class.php +++ b/core/dbobjectsearch.class.php @@ -1434,7 +1434,7 @@ class DBObjectSearch extends DBSearch return $sRet; } - public function GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr = null, $aSelectedClasses = null) + public function GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null) { // Hide objects that are not visible to the current user // @@ -1517,7 +1517,15 @@ class DBObjectSearch extends DBSearch $sRawId .= 'g:'.$sAlias.'!'.$oExpr->Render(); } } + if (!is_null($aSelectExpr)) + { + foreach($aSelectExpr as $sAlias => $oExpr) + { + $sRawId .= 'se:'.$sAlias.'!'.$oExpr->Render(); + } + } $aContextData['aGroupByExpr'] = $aGroupByExpr; + $aContextData['aSelectExpr'] = $aSelectExpr; $sRawId .= $bGetCount; $aContextData['bGetCount'] = $bGetCount; if (is_array($aSelectedClasses)) @@ -1542,6 +1550,7 @@ class DBObjectSearch extends DBSearch // Query caching // + $sOqlAPCCacheId = null; if (self::$m_bQueryCacheEnabled) { // Warning: using directly the query string as the key to the hash array can FAIL if the string @@ -1581,7 +1590,7 @@ class DBObjectSearch extends DBSearch if (!isset($oSQLQuery)) { $oKPI = new ExecutionKPI(); - $oSQLQuery = $oSearch->BuildSQLQueryStruct($aAttToLoad, $bGetCount, $aModifierProperties, $aGroupByExpr, $aSelectedClasses); + $oSQLQuery = $oSearch->BuildSQLQueryStruct($aAttToLoad, $bGetCount, $aModifierProperties, $aGroupByExpr, $aSelectedClasses, $aSelectExpr); $oKPI->ComputeStats('BuildSQLQueryStruct', $sOqlQuery); if (self::$m_bQueryCacheEnabled) @@ -1601,16 +1610,17 @@ class DBObjectSearch extends DBSearch } /** - * @param $aAttToLoad - * @param $bGetCount - * @param $aModifierProperties - * @param null $aGroupByExpr - * @param null $aSelectedClasses + * @param array $aAttToLoad + * @param bool $bGetCount + * @param array $aModifierProperties + * @param array $aGroupByExpr + * @param array $aSelectedClasses + * @param array $aSelectExpr * @return null|SQLObjectQuery */ - protected function BuildSQLQueryStruct($aAttToLoad, $bGetCount, $aModifierProperties, $aGroupByExpr = null, $aSelectedClasses = null) + protected function BuildSQLQueryStruct($aAttToLoad, $bGetCount, $aModifierProperties, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null) { - $oBuild = new QueryBuilderContext($this, $aModifierProperties, $aGroupByExpr, $aSelectedClasses); + $oBuild = new QueryBuilderContext($this, $aModifierProperties, $aGroupByExpr, $aSelectedClasses, $aSelectExpr); $oSQLQuery = $this->MakeSQLObjectQuery($oBuild, $aAttToLoad, array()); $oSQLQuery->SetCondition($oBuild->m_oQBExpressions->GetCondition()); @@ -1624,6 +1634,17 @@ class DBObjectSearch extends DBSearch { $oSQLQuery->SetSelect($oBuild->m_oQBExpressions->GetSelect()); } + if ($aSelectExpr) + { + // Get the fields corresponding to the select expressions + foreach($oBuild->m_oQBExpressions->GetSelect() as $sAlias => $oExpr) + { + if (key_exists($sAlias, $aSelectExpr)) + { + $oSQLQuery->AddSelect($sAlias, $oExpr); + } + } + } $aMandatoryTables = null; if (self::$m_bOptimizeQueries) diff --git a/core/dbsearch.class.php b/core/dbsearch.class.php index f81d6a79a..8b8b9fb6f 100644 --- a/core/dbsearch.class.php +++ b/core/dbsearch.class.php @@ -429,8 +429,53 @@ abstract class DBSearch protected static $m_aQueryStructCache = array(); - public function MakeGroupByQuery($aArgs, $aGroupByExpr, $bExcludeNullValues = false) + /** Generate a Group By SQL request from a search + * @param array $aArgs + * @param array $aGroupByExpr array('alias' => Expression) + * @param bool $bExcludeNullValues + * @param array $aSelectExpr array('alias' => Expression) Additional expressions added to the request + * @param array $aOrderBy array('alias' => bool) true = ASC false = DESC + * @param int $iLimitCount + * @param int $iLimitStart + * @return string SQL query generated + * @throws Exception + */ + public function MakeGroupByQuery($aArgs, $aGroupByExpr, $bExcludeNullValues = false, $aSelectExpr = array(), $aOrderBy = array(), $iLimitCount = 0, $iLimitStart = 0) { + // Sanity check + foreach($aGroupByExpr as $sAlias => $oExpr) + { + if (!($oExpr instanceof Expression)) + { + throw new CoreException("Wrong parameter for 'Group By' for [$sAlias] (an array('alias' => Expression) is awaited)"); + } + } + foreach($aSelectExpr as $sAlias => $oExpr) + { + if (array_key_exists($sAlias, $aGroupByExpr)) + { + throw new CoreException("Alias collision between 'Group By' and 'Select Expressions' [$sAlias]"); + } + if (!($oExpr instanceof Expression)) + { + throw new CoreException("Wrong parameter for 'Select Expressions' for [$sAlias] (an array('alias' => Expression) is awaited)"); + } + } + foreach($aOrderBy as $sAlias => $bAscending) + { + if (!array_key_exists($sAlias, $aGroupByExpr) && !array_key_exists($sAlias, $aSelectExpr) && ($sAlias != '_itop_count_')) + { + $aAllowedAliases = array_keys($aSelectExpr); + $aAllowedAliases = array_merge($aAllowedAliases, array_keys($aGroupByExpr)); + $aAllowedAliases[] = '_itop_count_'; + throw new CoreException("Wrong alias [$sAlias] for 'Order By'. Allowed values are: ", null, implode(", ", $aAllowedAliases)); + } + if (!is_bool($bAscending)) + { + throw new CoreException("Wrong direction in ORDER BY spec, found '$bAscending' and expecting a boolean value for '$sAlias''"); + } + } + if ($bExcludeNullValues) { // Null values are not handled (though external keys set to 0 are allowed) @@ -448,15 +493,15 @@ abstract class DBSearch } $aAttToLoad = array(); - $oSQLQuery = $oQueryFilter->GetSQLQuery(array(), $aArgs, $aAttToLoad, null, 0, 0, false, $aGroupByExpr); + $oSQLQuery = $oQueryFilter->GetSQLQuery(array(), $aArgs, $aAttToLoad, null, 0, 0, false, $aGroupByExpr, $aSelectExpr); $aScalarArgs = MetaModel::PrepareQueryArguments($aArgs, $this->GetInternalParams()); try { $bBeautifulSQL = self::$m_bTraceQueries || self::$m_bDebugQuery || self::$m_bIndentQueries; - $sRes = $oSQLQuery->RenderGroupBy($aScalarArgs, $bBeautifulSQL); + $sRes = $oSQLQuery->RenderGroupBy($aScalarArgs, $bBeautifulSQL, $aOrderBy, $iLimitCount, $iLimitStart); } - catch (MissingQueryArgument $e) + catch (Exception $e) { // Add some information... $e->addInfo('OQL', $this->ToOQL()); @@ -563,9 +608,9 @@ abstract class DBSearch } - protected function GetSQLQuery($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount, $aGroupByExpr = null) + protected function GetSQLQuery($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount, $aGroupByExpr = null, $aSelectExpr = null) { - $oSQLQuery = $this->GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr); + $oSQLQuery = $this->GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr, null, $aSelectExpr); $oSQLQuery->SetSourceOQL($this->ToOQL()); // Join to an additional table, if required... @@ -587,7 +632,7 @@ abstract class DBSearch } public abstract function GetSQLQueryStructure( - $aAttToLoad, $bGetCount, $aGroupByExpr = null, $aSelectedClasses = null + $aAttToLoad, $bGetCount, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null ); //////////////////////////////////////////////////////////////////////////// diff --git a/core/dbunionsearch.class.php b/core/dbunionsearch.class.php index 27e370fc0..d0c967671 100644 --- a/core/dbunionsearch.class.php +++ b/core/dbunionsearch.class.php @@ -143,7 +143,7 @@ class DBUnionSearch extends DBSearch /** * Limited to the selected classes - */ + */ public function GetClassName($sAlias) { if (array_key_exists($sAlias, $this->aSelectedClasses)) @@ -474,15 +474,17 @@ class DBUnionSearch extends DBSearch throw new Exception('MakeUpdateQuery is not implemented for the unions!'); } - public function GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr = null, $aSelectedClasses = null) + public function GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null) { if (count($this->aSearches) == 1) { - return $this->aSearches[0]->GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr); + return $this->aSearches[0]->GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr, $aSelectExpr); } $aSQLQueries = array(); $aAliases = array_keys($this->aSelectedClasses); + $aQueryAttToLoad = null; + $aUnionQuerySelectExpr = array(); foreach ($this->aSearches as $iSearch => $oSearch) { $aSearchAliases = array_keys($oSearch->GetSelectedClasses()); @@ -544,7 +546,43 @@ class DBUnionSearch extends DBSearch $aQueryGroupByExpr[$sExpressionAlias] = $oExpression->Translate($aTranslationData, false, false); } } - $oSubQuery = $oSearch->GetSQLQueryStructure($aQueryAttToLoad, false, $aQueryGroupByExpr, $aSearchSelectedClasses); + + if (is_null($aSelectExpr)) + { + $aQuerySelectExpr = null; + } + else + { + $aQuerySelectExpr = array(); + $aTranslationData = array(); + $aQueryColumns = array_keys($oSearch->GetSelectedClasses()); + foreach($aAliases as $iColumn => $sAlias) + { + $sQueryAlias = $aQueryColumns[$iColumn]; + $aTranslationData[$sAlias]['*'] = $sQueryAlias; + } + foreach($aSelectExpr as $sExpressionAlias => $oExpression) + { + $oExpression->Browse(function ($oNode) use (&$aQuerySelectExpr, &$aTranslationData) + { + if ($oNode instanceof FieldExpression) + { + $sAlias = $oNode->GetParent()."__".$oNode->GetName(); + if (!key_exists($sAlias, $aQuerySelectExpr)) + { + $aQuerySelectExpr[$sAlias] = $oNode->Translate($aTranslationData, false, false); + } + $aTranslationData[$oNode->GetParent()][$oNode->GetName()] = new FieldExpression($sAlias); + } + }); + // Only done for the first select as aliases are named after the first query + if (!array_key_exists($sExpressionAlias, $aUnionQuerySelectExpr)) + { + $aUnionQuerySelectExpr[$sExpressionAlias] = $oExpression->Translate($aTranslationData, false, false); + } + } + } + $oSubQuery = $oSearch->GetSQLQueryStructure($aQueryAttToLoad, false, $aQueryGroupByExpr, $aSearchSelectedClasses, $aQuerySelectExpr); if (count($aSearchAliases) > 1) { // Necessary to make sure that selected columns will match throughout all the queries @@ -554,7 +592,7 @@ class DBUnionSearch extends DBSearch $aSQLQueries[] = $oSubQuery; } - $oSQLQuery = new SQLUnionQuery($aSQLQueries, $aGroupByExpr); + $oSQLQuery = new SQLUnionQuery($aSQLQueries, $aGroupByExpr, $aUnionQuerySelectExpr); //MyHelpers::var_dump_html($oSQLQuery, true); //MyHelpers::var_dump_html($oSQLQuery->RenderSelect(), true); if (self::$m_bDebugQuery) $oSQLQuery->DisplayHtml(); diff --git a/core/oql/expression.class.inc.php b/core/oql/expression.class.inc.php index 3438eb2c5..80db27c07 100644 --- a/core/oql/expression.class.inc.php +++ b/core/oql/expression.class.inc.php @@ -70,7 +70,7 @@ abstract class Expression { return base64_encode($this->Render()); } - + static public function unserialize($sValue) { return self::FromOQL(base64_decode($sValue)); @@ -1373,7 +1373,7 @@ class QueryBuilderExpressions */ protected $m_aClassIds; - public function __construct(DBObjectSearch $oSearch, $aGroupByExpr = null) + public function __construct(DBObjectSearch $oSearch, $aGroupByExpr = null, $aSelectExpr = null) { $this->m_oConditionExpr = $oSearch->GetCriteria(); if (!$oSearch->GetShowObsoleteData()) @@ -1387,7 +1387,7 @@ class QueryBuilderExpressions } } } - $this->m_aSelectExpr = array(); + $this->m_aSelectExpr = is_null($aSelectExpr) ? array() : $aSelectExpr; $this->m_aGroupByExpr = $aGroupByExpr; $this->m_aJoinFields = array(); @@ -1448,8 +1448,10 @@ class QueryBuilderExpressions /** * Get tables representing the queried objects - * Could be further optimized: when the first join is an outer join, then the rest can be omitted - */ + * Could be further optimized: when the first join is an outer join, then the rest can be omitted + * @param array $aTables + * @return array + */ public function GetMandatoryTables(&$aTables = null) { if (is_null($aTables)) $aTables = array(); @@ -1458,6 +1460,8 @@ class QueryBuilderExpressions { $oExpression->CollectUsedParents($aTables); } + + return $aTables; } public function GetUnresolvedFields($sAlias, &$aUnresolved) @@ -1498,7 +1502,6 @@ class QueryBuilderExpressions { $this->m_aJoinFields[$index] = $oExpression->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); } - foreach($this->m_aClassIds as $sClass => $oExpression) { $this->m_aClassIds[$sClass] = $oExpression->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); diff --git a/core/querybuildercontext.class.inc.php b/core/querybuildercontext.class.inc.php index 71f456502..87cd5ed27 100644 --- a/core/querybuildercontext.class.inc.php +++ b/core/querybuildercontext.class.inc.php @@ -34,10 +34,10 @@ class QueryBuilderContext public $m_oQBExpressions; - public function __construct($oFilter, $aModifierProperties, $aGroupByExpr = null, $aSelectedClasses = null) + public function __construct($oFilter, $aModifierProperties, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null) { $this->m_oRootFilter = $oFilter; - $this->m_oQBExpressions = new QueryBuilderExpressions($oFilter, $aGroupByExpr); + $this->m_oQBExpressions = new QueryBuilderExpressions($oFilter, $aGroupByExpr, $aSelectExpr); $this->m_aClassAliases = $oFilter->GetJoinedClasses(); $this->m_aTableAliases = array(); diff --git a/core/sqlobjectquery.class.inc.php b/core/sqlobjectquery.class.inc.php index ab7136a4b..47e89ae39 100644 --- a/core/sqlobjectquery.class.inc.php +++ b/core/sqlobjectquery.class.inc.php @@ -293,11 +293,12 @@ class SQLObjectQuery extends SQLQuery * Needed for the unions * @param $aOrderBy * @return string + * @throws CoreException */ public function RenderOrderByClause($aOrderBy) { $this->PrepareRendering(); - $sOrderBy = self::ClauseOrderBy($aOrderBy); + $sOrderBy = self::ClauseOrderBy($aOrderBy, $this->__aFields); return $sOrderBy; } @@ -357,8 +358,8 @@ class SQLObjectQuery extends SQLQuery } else { - $sSelect = self::ClauseSelect($this->__aFields); - $sOrderBy = self::ClauseOrderBy($aOrderBy); + $sSelect = self::ClauseSelect($this->__aFields, $sLineSep); + $sOrderBy = self::ClauseOrderBy($aOrderBy, $this->__aFields); if (!empty($sOrderBy)) { $sOrderBy = "ORDER BY $sOrderBy$sLineSep"; @@ -381,21 +382,42 @@ class SQLObjectQuery extends SQLQuery /** * @param array $aArgs * @param bool $bBeautifulQuery + * @param array $aOrderBy + * @param int $iLimitCount + * @param int $iLimitStart * @return string * @throws CoreException */ - public function RenderGroupBy($aArgs = array(), $bBeautifulQuery = false) + public function RenderGroupBy($aArgs = array(), $bBeautifulQuery = false, $aOrderBy = array(), $iLimitCount = 0, $iLimitStart = 0) { $this->m_bBeautifulQuery = $bBeautifulQuery; $sLineSep = $this->m_bBeautifulQuery ? "\n" : ''; $sIndent = $this->m_bBeautifulQuery ? " " : null; $this->PrepareRendering(); + $sSelect = self::ClauseSelect($this->__aFields); $sFrom = self::ClauseFrom($this->__aFrom, $sIndent); $sWhere = self::ClauseWhere($this->m_oConditionExpr, $aArgs); $sGroupBy = self::ClauseGroupBy($this->__aGroupBy); - $sSQL = "SELECT $sSelect,$sLineSep COUNT(*) AS _itop_count_$sLineSep FROM $sFrom$sLineSep WHERE $sWhere$sLineSep GROUP BY $sGroupBy"; + $sOrderBy = self::ClauseOrderBy($aOrderBy, $this->__aFields); + if (!empty($sGroupBy)) + { + $sGroupBy = "GROUP BY $sGroupBy$sLineSep"; + } + if (!empty($sOrderBy)) + { + $sOrderBy = "ORDER BY $sOrderBy$sLineSep"; + } + if ($iLimitCount > 0) + { + $sLimit = 'LIMIT '.$iLimitStart.', '.$iLimitCount; + } + else + { + $sLimit = ''; + } + $sSQL = "SELECT $sSelect,$sLineSep COUNT(*) AS _itop_count_$sLineSep FROM $sFrom$sLineSep WHERE $sWhere$sLineSep $sGroupBy $sOrderBy$sLineSep $sLimit"; return $sSQL; } diff --git a/core/sqlquery.class.inc.php b/core/sqlquery.class.inc.php index 4370f8c8a..f759b09be 100644 --- a/core/sqlquery.class.inc.php +++ b/core/sqlquery.class.inc.php @@ -69,18 +69,18 @@ abstract class SQLQuery abstract public function RenderDelete($aArgs = array()); abstract public function RenderUpdate($aArgs = array()); abstract public function RenderSelect($aOrderBy = array(), $aArgs = array(), $iLimitCount = 0, $iLimitStart = 0, $bGetCount = false, $bBeautifulQuery = false); - abstract public function RenderGroupBy($aArgs = array(), $bBeautifulQuery = false); + abstract public function RenderGroupBy($aArgs = array(), $bBeautifulQuery = false, $aOrderBy = array(), $iLimitCount = 0, $iLimitStart = 0); abstract public function OptimizeJoins($aUsedTables, $bTopCall = true); - protected static function ClauseSelect($aFields) + protected static function ClauseSelect($aFields, $sLineSep = '') { $aSelect = array(); foreach ($aFields as $sFieldAlias => $sSQLExpr) { $aSelect[] = "$sSQLExpr AS $sFieldAlias"; } - $sSelect = implode(', ', $aSelect); + $sSelect = implode(",$sLineSep ", $aSelect); return $sSelect; } @@ -181,7 +181,13 @@ abstract class SQLQuery } } - protected static function ClauseOrderBy($aOrderBy) + /** + * @param array $aOrderBy + * @param array $aExistingFields + * @return string + * @throws CoreException + */ + protected static function ClauseOrderBy($aOrderBy, $aExistingFields) { $aOrderBySpec = array(); foreach($aOrderBy as $sFieldAlias => $bAscending) diff --git a/core/sqlunionquery.class.inc.php b/core/sqlunionquery.class.inc.php index 64192fbd4..5d3e81320 100644 --- a/core/sqlunionquery.class.inc.php +++ b/core/sqlunionquery.class.inc.php @@ -38,8 +38,9 @@ class SQLUnionQuery extends SQLQuery { protected $aQueries; protected $aGroupBy; + protected $aSelectExpr; - public function __construct($aQueries, $aGroupBy) + public function __construct($aQueries, $aGroupBy, $aSelectExpr = array()) { parent::__construct(); @@ -49,6 +50,7 @@ class SQLUnionQuery extends SQLQuery $this->aQueries[] = $oSQLQuery->DeepClone(); } $this->aGroupBy = $aGroupBy; + $this->aSelectExpr = $aSelectExpr; } public function DisplayHtml() @@ -129,7 +131,17 @@ class SQLUnionQuery extends SQLQuery } // Interface, build the SQL query - public function RenderGroupBy($aArgs = array(), $bBeautifulQuery = false) + + /** + * @param array $aArgs + * @param bool $bBeautifulQuery + * @param array $aOrderBy + * @param int $iLimitCount + * @param int $iLimitStart + * @return string + * @throws CoreException + */ + public function RenderGroupBy($aArgs = array(), $bBeautifulQuery = false, $aOrderBy = array(), $iLimitCount = 0, $iLimitStart = 0) { $this->m_bBeautifulQuery = $bBeautifulQuery; $sLineSep = $this->m_bBeautifulQuery ? "\n" : ''; @@ -143,15 +155,41 @@ class SQLUnionQuery extends SQLQuery $sSelects = '('.implode(")$sLineSep UNION$sLineSep(", $aSelects).')'; $sFrom = "($sLineSep$sSelects$sLineSep) as __selects__"; - $aAliases = array(); + $aSelectAliases = array(); + $aGroupAliases = array(); foreach ($this->aGroupBy as $sGroupAlias => $trash) { - $aAliases[] = "`$sGroupAlias`"; + $aSelectAliases[$sGroupAlias] = "`$sGroupAlias`"; + $aGroupAliases[] = "`$sGroupAlias`"; + } + foreach($this->aSelectExpr as $sSelectAlias => $oExpr) + { + $aSelectAliases[$sSelectAlias] = $oExpr->Render()." AS `$sSelectAlias`"; } - $sSelect = implode(', ', $aAliases); - $sGroupBy = implode(', ', $aAliases); - $sSQL = "SELECT $sSelect,$sLineSep COUNT(*) AS _itop_count_$sLineSep FROM $sFrom$sLineSep GROUP BY $sGroupBy"; + $sSelect = implode(",$sLineSep ", $aSelectAliases); + $sGroupBy = implode(', ', $aGroupAliases); + + $sOrderBy = self::ClauseOrderBy($aOrderBy, $aSelectAliases); + if (!empty($sGroupBy)) + { + $sGroupBy = "GROUP BY $sGroupBy$sLineSep"; + } + if (!empty($sOrderBy)) + { + $sOrderBy = "ORDER BY $sOrderBy$sLineSep"; + } + if ($iLimitCount > 0) + { + $sLimit = 'LIMIT '.$iLimitStart.', '.$iLimitCount; + } + else + { + $sLimit = ''; + } + + + $sSQL = "SELECT $sSelect,$sLineSep COUNT(*) AS _itop_count_$sLineSep FROM $sFrom$sLineSep $sGroupBy $sOrderBy$sLineSep $sLimit"; return $sSQL; } diff --git a/test/GroupByAndFunctions.php b/test/GroupByAndFunctions.php new file mode 100644 index 000000000..015b8b673 --- /dev/null +++ b/test/GroupByAndFunctions.php @@ -0,0 +1,343 @@ + +// + +require_once ('../approot.inc.php'); +require_once(APPROOT.'application/application.inc.php'); +require_once(APPROOT.'application/itopwebpage.class.inc.php'); +require_once(APPROOT.'application/startup.inc.php'); +require_once(APPROOT.'application/loginwebpage.class.inc.php'); + + +///////////////////////////////////////////////////////////////////// +// Main program +// +LoginWebPage::DoLogin(true); // Check user rights and prompt if needed + + +$sSubmit = utils::ReadParam('submit', '', false, 'raw_data'); +if ($sSubmit != 'Reset') +{ + $sOQL = utils::ReadParam('OQL_Request', '', false, 'raw_data'); +} +else +{ + $sOQL = ''; +} +$bError = false; +$oP = new iTopWebPage('Database inconsistencies'); +$oP->set_base(utils::GetAbsoluteUrlAppRoot().'test/'); +$oP->set_title('Grouping with functions'); +$oP->add('

Grouping with functions

'); +$oP->add('
'); +try +{ + if (!empty($sOQL)) + { + // Getting class attributes + $oSearch = DBSearch::FromOQL($sOQL); + $aSearches = $oSearch->GetSearches(); + if ($oSearch instanceof DBUnionSearch) + { + $sClass = $aSearches[0]->GetClassAlias(); + $sRealClass = $aSearches[0]->GetClass(); + } + else + { + $sClass = $oSearch->GetClassAlias(); + $sRealClass = $oSearch->GetClass(); + } + + $sGroupBy1 = utils::ReadParam('groupby_1', ''); + $sGroupBy2 = utils::ReadParam('groupby_2', ''); + $sOrderBy1 = utils::ReadParam('orderby_1', ''); + $sOrderBy2 = utils::ReadParam('orderby_2', ''); + + $sAttributesOptions1 = ''; + $sAttributesOptions2 = ''; + $sAttributesOptions3 = ''; + $sAttributesOptions4 = ''; + + foreach(array('_itop_sum_', '_itop_avg_', '_itop_min_', '_itop_max_', '_itop_count_', 'group1', 'group2') as $sAttCode) + { + $sAttributesOptions3 .= ''; + $sAttributesOptions4 .= ''; + } + + foreach(MetaModel::ListAttributeDefs($sRealClass) as $sAttCode => $oAttDef) + { + // Skip this attribute if not defined in this table + if ($oSearch instanceof DBUnionSearch) + { + foreach($aSearches as $oSubQuery) + { + $sSubClass = $oSubQuery->GetClass(); + if (!MetaModel::IsValidAttCode($sSubClass, $sAttCode)) + { + continue 2; + } + } + } + $sAttributesOptions1 .= ''; + $sAttributesOptions2 .= ''; + } + + $iLimit = intval(utils::ReadParam('top', '0')); + + $sInvOrder1 = utils::ReadParam('desc1', ''); + $sCheck1 = ($sInvOrder1 == 'on' ? 'checked' : ''); + + $sInvOrder2 = utils::ReadParam('desc2', ''); + $sCheck2 = ($sInvOrder2 == 'on' ? 'checked' : ''); + + $sFuncField = utils::ReadParam('funcfield', ''); + + $sFuncFieldOption = ''; + foreach(MetaModel::ListAttributeDefs($sRealClass) as $sAttCode => $oAttDef) + { + // Skip this attribute if not defined in this table + if ($oSearch instanceof DBUnionSearch) + { + foreach($aSearches as $oSubQuery) + { + $sSubClass = $oSubQuery->GetClass(); + if (!MetaModel::IsValidAttCode($sSubClass, $sAttCode)) + { + continue 2; + } + } + } + switch (get_class($oAttDef)) + { + case 'Integer': + case 'AttributeDecimal': + case 'AttributeDuration': + case 'AttributeSubItem': + case 'AttributePercentage': + $sFuncFieldOption .= ''; + break; + } + } + } +} +catch (Exception $e) +{ + $oP->p('
'.$e->getMessage().'
'); + $bError = true; +} +$oP->add("
"); +$oP->add("\n"); +$oP->add("
"); + +$oP->add("
"); + +$oP->add( + << + +
+ +
+
+EOF +); + +if (!empty($sOQL) && !$bError) +{ + $oP->add( + << + +
+ + +
+
+
+ +
+ + +
+
+ + +
+
+
+ +
+ +
+
+
+ +
+
+
+ +EOF + ); +} + +$oP->add("\n"); + +$oP->add(""); + +$sSQL = ''; + + +if (empty($sOQL) || empty($sGroupBy1)) +{ + $oP->output(); + return; +} +try +{ + $iLimitStart = 0; + $aOrderBy = array(); + if (!empty($sOrderBy1)) + { + $aOrderBy[$sOrderBy1] = ($sInvOrder1 != 'on'); + } + if (!empty($sOrderBy2)) + { + $aOrderBy[$sOrderBy2] = ($sInvOrder2 != 'on'); + } + + $aGroupBy = array(); + $oExpr1 = Expression::FromOQL($sClass.'.'.$sGroupBy1); + $aGroupBy["group1"] = $oExpr1; + + if (!empty($sGroupBy2)) + { + $oExpr2 = Expression::FromOQL($sClass.'.'.$sGroupBy2); + $aGroupBy["group2"] = $oExpr2; + } + + $aArgs = array(); + + if (empty($sFuncField)) + { + $aFunctions = array(); + } + else + { + $oTimeExpr = Expression::FromOQL($sClass.'.'.$sFuncField); + $oSumExpr = new FunctionExpression('SUM', array($oTimeExpr)); + $oAvgExpr = new FunctionExpression('AVG', array($oTimeExpr)); + $oMinExpr = new FunctionExpression('MIN', array($oTimeExpr)); + $oMaxExpr = new FunctionExpression('MAX', array($oTimeExpr)); + // Alias => Expression + $aFunctions = array( + '_itop_sum_' => $oSumExpr, + '_itop_avg_' => $oAvgExpr, + '_itop_min_' => $oMinExpr, + '_itop_max_' => $oMaxExpr, + ); + } + + $sSQL = $oSearch->MakeGroupByQuery($aArgs, $aGroupBy, false, $aFunctions, $aOrderBy, $iLimit, $iLimitStart); + + $aRes = CMDBSource::QueryToArray($sSQL); + + // Display results + if (!empty($aRes)) + { + $oP->add('
'); + $oP->add(''); + $aLine = $aRes[0]; + $aCols = array(); + $oP->add(''); + foreach(array_keys($aLine) as $item) + { + if (!is_numeric($item)) + { + $aCols[] = $item; + $oP->add(""); + } + } + $oP->add(''); + + foreach($aRes as $aLine) + { + $oP->add(''); + foreach($aCols as $sCol) + { + $oP->add(""); + } + $oP->add(''); + } + + $oP->add('
$item
".$aLine[$sCol]."
'); + $oP->add('
'); + } + else + { + $oP->add("

No Result

\n"); + } +} +catch (Exception $e) +{ + $oP->p('
'.$e->getMessage().'
'); + $bError = true; +} + +$oP->add("
$sSQL
\n"); + +$oP->output(); + +return; + +/* +echo "
";
+$aClassSelection = MetaModel::GetClasses();
+foreach($aClassSelection as $sClass)
+{
+	if (!MetaModel::HasTable($sClass))
+	{
+		continue;
+	}
+	foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
+	{
+		// Skip this attribute if not defined in this table
+		if (!MetaModel::IsAttributeOrigin($sClass, $sAttCode))
+		{
+			continue;
+		}
+		switch (get_class($oAttDef))
+		{
+			case 'Integer':
+			case 'AttributeDecimal':
+			case 'AttributeDuration':
+			case 'AttributeSubItem':
+			case 'AttributePercentage':
+			echo "$sClass:$sAttCode = ".get_class($oAttDef)."\n";
+				break;
+		}
+	}
+}
+*/
\ No newline at end of file
diff --git a/test/core/DBSearchTest.php b/test/core/DBSearchTest.php
new file mode 100644
index 000000000..79c847075
--- /dev/null
+++ b/test/core/DBSearchTest.php
@@ -0,0 +1,483 @@
+
+//
+
+/**
+ * Created by PhpStorm.
+ * User: Eric
+ * Date: 06/02/2018
+ * Time: 09:58
+ */
+
+namespace Combodo\iTop\Test\UnitTest\Core;
+
+
+use CMDBSource;
+use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
+use DBSearch;
+use Exception;
+use Expression;
+use FunctionExpression;
+use PHPUnit\Framework\TestCase;
+
+
+/**
+ * Tests of the DBSearch class.
+ * 
+ *
+ * @runTestsInSeparateProcesses
+ * @preserveGlobalState disabled
+ * @backupGlobals disabled
+ */
+class DBSearchTest extends ItopDataTestCase
+{
+
+	/**
+	 * @throws \Exception
+	 */
+	protected function setUp()
+	{
+		parent::setUp();
+	}
+
+	/**
+	 * @dataProvider UReqProvider
+	 * @param $iOrgNb
+	 * @param $iPersonNb
+	 * @param $aReq
+	 * @param $iLimit
+	 * @param $aCountRes
+	 * @throws Exception
+	 */
+	public function testMakeGroupByQuery($iOrgNb, $iPersonNb, $aReq, $iLimit, $aCountRes)
+	{
+		$sOrgs = $this->init_db($iOrgNb, $iPersonNb, $aReq);
+
+		$oSearch = DBSearch::FromOQL("SELECT UserRequest WHERE org_id IN ($sOrgs)");
+		$this->assertNotNull($oSearch);
+		$oExpr1 = Expression::FromOQL('UserRequest.org_id');
+
+		// Alias => Expression
+		$aGroupBy = array('org_id' => $oExpr1);
+
+		$oTimeExpr = Expression::FromOQL('UserRequest.time_spent');
+		$oSumExpr = new FunctionExpression('SUM', array($oTimeExpr));
+		$oAvgExpr = new FunctionExpression('AVG', array($oTimeExpr));
+		$oMinExpr = new FunctionExpression('MIN', array($oTimeExpr));
+		$oMaxExpr = new FunctionExpression('MAX', array($oTimeExpr));
+		// Alias => Expression
+		$aFunctions = array(
+			'_itop_sum_' => $oSumExpr,
+			'_itop_avg_' => $oAvgExpr,
+			'_itop_min_' => $oMinExpr,
+			'_itop_max_' => $oMaxExpr,
+			);
+
+		// Alias => Order
+		$aOrderBy = array('_itop_sum_' => true, '_itop_count_' => true);
+
+		$aArgs = array();
+
+		$sSQL = $oSearch->MakeGroupByQuery($aArgs, $aGroupBy, false, $aFunctions, $aOrderBy, $iLimit);
+		$this->debug($sSQL);
+
+		$aRes = CMDBSource::QueryToArray($sSQL);
+		$this->debug($aRes);
+
+		$this->assertEquals(count($aCountRes), count($aRes));
+		for ($i = 0; $i < count($aCountRes); $i++)
+		{
+			$this->assertEquals($aCountRes[$i], $aRes[$i]['_itop_count_']);
+		}
+	}
+
+
+	public function UReqProvider()
+	{
+		return array(
+			"1 line" => array(1, 1, array(array(1, 0, 0)), 0, array('1')),
+			"2 same lines" => array(1, 1, array(array(1, 0, 0), array(1, 0, 0)), 0, array('2')),
+			"2 diff lines" => array(2, 2, array(array(1, 0, 0), array(1, 1, 1)), 0, array('1', '1')),
+			"4 lines" => array(2, 2, array(array(1, 0, 0), array(1, 1, 1), array(1, 0, 0), array(1, 1, 1)), 0, array('2', '2')),
+			"5 lines" => array(2, 2, array(array(1, 0, 0), array(1, 0, 0), array(1, 1, 1), array(1, 0, 0), array(1, 1, 1)), 0, array('2', '3')),
+			"6 lines" => array(2, 4, array(array(1, 0, 0), array(1, 1, 3), array(1, 1, 1), array(1, 1, 3), array(1, 0, 2), array(1, 1, 1)), 0, array('2', '4')),
+			"6 lines limit" => array(2, 4, array(array(1, 0, 0), array(1, 1, 3), array(1, 1, 1), array(1, 1, 1), array(1, 0, 0), array(1, 1, 1)), 1, array('2')),
+		);
+	}
+
+	/**
+	 * @param int $iOrgNb Number of Organization to create
+	 * @param int $iPersonNb Number of Person to create
+	 * @param array $aReq  UserRequests to create: array(array([time_spent value], [org index], [person index]))
+	 * @return string organization list for select
+	 * @throws Exception
+	 */
+	private function init_db($iOrgNb, $iPersonNb, $aReq)
+	{
+		$aOrgIds = array();
+		$sOrgs = '';
+		for($i = 0; $i < $iOrgNb; $i++)
+		{
+			$oObj = $this->CreateOrganization('UnitTest_Org'.$i);
+			$sKey = $oObj->GetKey();
+			$aOrgIds[] = $sKey;
+			if ($i > 0)
+			{
+				$sOrgs .= ",";
+			}
+			$sOrgs .= $sKey;
+		}
+		$this->assertEquals($iOrgNb, count($aOrgIds));
+
+		$aPersonIds = array();
+		for($i = 0; $i < $iPersonNb; $i++)
+		{
+			$oObj = $this->CreatePerson($i, $aOrgIds[$i % $iOrgNb]);
+			$aPersonIds[] = $oObj->GetKey();
+		}
+		$this->assertEquals($iPersonNb, count($aPersonIds));
+
+		$i = 0;
+		foreach($aReq as $aParams)
+		{
+			$oObj = $this->CreateUserRequest($i, $aParams[0], $aOrgIds[$aParams[1]], $aPersonIds[$aParams[2]]);
+			$this->assertNotNull($oObj);
+			$i++;
+		}
+		return $sOrgs;
+	}
+
+	/**
+	 * @throws Exception
+	 */
+	public function testGroupByUnion()
+	{
+		$oServer = $this->CreateServer(1);
+
+		$this->CreatePhysicalInterface(1, 1000, $oServer->GetKey());
+		$this->CreateFiberChannelInterface(1, 1000, $oServer->GetKey());
+
+
+		$oSearch = DBSearch::FromOQL("SELECT FiberChannelInterface AS FCI WHERE FCI.name = '1' UNION SELECT PhysicalInterface AS PHI WHERE PHI.name = '1'");
+		$this->assertNotNull($oSearch);
+		$oExpr1 = Expression::FromOQL('FCI.name');
+
+		// Alias => Expression (first select reference)
+		$aGroupBy = array('group1' => $oExpr1);
+
+		$oTimeExpr = Expression::FromOQL('FCI.speed');
+		$oSumExpr = new FunctionExpression('SUM', array($oTimeExpr));
+		$oAvgExpr = new FunctionExpression('AVG', array($oTimeExpr));
+		$oMinExpr = new FunctionExpression('MIN', array($oTimeExpr));
+		$oMaxExpr = new FunctionExpression('MAX', array($oTimeExpr));
+		// Alias => Expression
+		$aFunctions = array(
+			'_itop_sum_' => $oSumExpr,
+			'_itop_avg_' => $oAvgExpr,
+			'_itop_min_' => $oMinExpr,
+			'_itop_max_' => $oMaxExpr,
+		);
+
+		// Alias => Order
+		$aOrderBy = array('group1' => true, '_itop_count_' => true);
+
+		$aArgs = array();
+
+		$sSQL = $oSearch->MakeGroupByQuery($aArgs, $aGroupBy, false, $aFunctions, $aOrderBy);
+		$this->debug($sSQL);
+
+		$aRes = CMDBSource::QueryToArray($sSQL);
+		$this->debug($aRes);
+	}
+
+	/**
+	 * @throws Exception
+	 */
+	public function testOrderBy_1()
+	{
+
+		$oSearch = DBSearch::FromOQL("SELECT FiberChannelInterface");
+		$this->assertNotNull($oSearch);
+
+		// Alias => Expression (first select reference)
+		$oExpr1 = Expression::FromOQL('FiberChannelInterface.name');
+		$aGroupBy = array('group1' => $oExpr1);
+
+		$oTimeExpr = Expression::FromOQL('FiberChannelInterface.speed');
+		$oSumExpr = new FunctionExpression('SUM', array($oTimeExpr));
+		$oAvgExpr = new FunctionExpression('AVG', array($oTimeExpr));
+		$oMinExpr = new FunctionExpression('MIN', array($oTimeExpr));
+		$oMaxExpr = new FunctionExpression('MAX', array($oTimeExpr));
+		// Alias => Expression
+		$aFunctions = array(
+			'_itop_sum_' => $oSumExpr,
+			'_itop_avg_' => $oAvgExpr,
+			'_itop_min_' => $oMinExpr,
+			'_itop_max_' => $oMaxExpr,
+		);
+		$aArgs = array();
+
+		// Alias => Order
+		$aOrderBy = array(
+			'group1' => true,
+			'_itop_sum_' => true,
+			'_itop_avg_' => true,
+			'_itop_min_' => true,
+			'_itop_max_' => true);
+		$sSQL = $oSearch->MakeGroupByQuery($aArgs, $aGroupBy, false, $aFunctions, $aOrderBy);
+		$this->assertNotEmpty($sSQL);
+
+		// Alias => Order
+		$aOrderBy = array('nothing_good' => true);
+		$this->expectException("CoreException");
+		$oSearch->MakeGroupByQuery($aArgs, $aGroupBy, false, $aFunctions, $aOrderBy);
+
+		$this->assertTrue(false);
+	}
+
+	/**
+	 * @throws Exception
+	 */
+	public function testSanity_1()
+	{
+		$oSearch = DBSearch::FromOQL("SELECT FiberChannelInterface AS FCI WHERE FCI.name = '1' UNION SELECT PhysicalInterface AS PHI WHERE PHI.name = '1'");
+		$this->assertNotNull($oSearch);
+		$oExpr1 = Expression::FromOQL('FCI.name');
+
+		// Alias => Expression (first select reference)
+		$aGroupBy = array('group1' => $oExpr1);
+
+		$oTimeExpr = Expression::FromOQL('FCI.speed');
+		$oSumExpr = new FunctionExpression('SUM', array($oTimeExpr));
+		$oAvgExpr = new FunctionExpression('AVG', array($oTimeExpr));
+		$oMinExpr = new FunctionExpression('MIN', array($oTimeExpr));
+		$oMaxExpr = new FunctionExpression('MAX', array($oTimeExpr));
+		// Alias => Expression
+		$aFunctions = array(
+			'_itop_sum_' => $oSumExpr,
+			'_itop_avg_' => $oAvgExpr,
+			'group1' => $oMinExpr,
+			'_itop_max_' => $oMaxExpr,
+		);
+		$aArgs = array();
+
+		// Alias => Order
+		$aOrderBy = array(
+			'group1' => true,
+			'_itop_sum_' => true,
+			'_itop_avg_' => true,
+			'_itop_min_' => true,
+			'_itop_max_' => true);
+
+		$this->expectException("CoreException");
+		$oSearch->MakeGroupByQuery($aArgs, $aGroupBy, false, $aFunctions, $aOrderBy);
+
+		$this->assertTrue(false);
+	}
+
+	/**
+	 * @throws Exception
+	 */
+	public function testSanity_2()
+	{
+		$oSearch = DBSearch::FromOQL("SELECT FiberChannelInterface AS FCI WHERE FCI.name = '1' UNION SELECT PhysicalInterface AS PHI WHERE PHI.name = '1'");
+		$this->assertNotNull($oSearch);
+
+		// Alias => Expression (first select reference)
+		$oExpr1 = Expression::FromOQL('FCI.name');
+		$aGroupBy = array('group1' => $oExpr1);
+
+		$oTimeExpr = Expression::FromOQL('FCI.speed');
+		$oSumExpr = new FunctionExpression('SUM', array($oTimeExpr));
+		$oAvgExpr = new FunctionExpression('AVG', array($oTimeExpr));
+		$oMinExpr = new FunctionExpression('MIN', array($oTimeExpr));
+		$oMaxExpr = new FunctionExpression('MAX', array($oTimeExpr));
+		// Alias => Expression
+		$aFunctions = array(
+			'_itop_sum_' => $oSumExpr,
+			'_itop_avg_' => $oAvgExpr,
+			'_itop_min_' => $oMinExpr,
+			'_itop_max_' => $oMaxExpr,
+		);
+		$aArgs = array();
+
+		// Alias => Order
+		$aOrderBy = array(
+			'group1' => true,
+			'_itop_sum_' => true,
+			'_itop_avg_' => true,
+			'_itop_min_' => true,
+			'_itop_max_' => true);
+
+		$sSQL = $oSearch->MakeGroupByQuery($aArgs, $aGroupBy, false, $aFunctions, $aOrderBy);
+		$this->assertNotEmpty($sSQL);
+
+		$aGroupBy = array('group1' => 'FCI.name');
+		$this->expectException("CoreException");
+		$oSearch->MakeGroupByQuery($aArgs, $aGroupBy, false, $aFunctions, $aOrderBy);
+
+		$this->assertTrue(false);
+	}
+
+	/**
+	 * @throws Exception
+	 */
+	public function testSanity_3()
+	{
+		$oSearch = DBSearch::FromOQL("SELECT FiberChannelInterface AS FCI WHERE FCI.name = '1' UNION SELECT PhysicalInterface AS PHI WHERE PHI.name = '1'");
+		$this->assertNotNull($oSearch);
+
+		// Alias => Expression (first select reference)
+		$oExpr1 = Expression::FromOQL('FCI.name');
+		$aGroupBy = array('group1' => $oExpr1);
+
+		$oTimeExpr = Expression::FromOQL('FCI.speed');
+		$oSumExpr = new FunctionExpression('SUM', array($oTimeExpr));
+		$oAvgExpr = new FunctionExpression('AVG', array($oTimeExpr));
+		$oMinExpr = new FunctionExpression('MIN', array($oTimeExpr));
+		$oMaxExpr = new FunctionExpression('MAX', array($oTimeExpr));
+		// Alias => Expression
+		$aFunctions = array(
+			'_itop_sum_' => $oSumExpr,
+			'_itop_avg_' => $oAvgExpr,
+			'_itop_min_' => $oMinExpr,
+			'_itop_max_' => $oMaxExpr,
+		);
+		$aArgs = array();
+
+		// Alias => Order
+		$aOrderBy = array(
+			'group1' => true,
+			'_itop_sum_' => true,
+			'_itop_avg_' => true,
+			'_itop_min_' => true,
+			'_itop_max_' => true);
+
+		$sSQL = $oSearch->MakeGroupByQuery($aArgs, $aGroupBy, false, $aFunctions, $aOrderBy);
+		$this->assertNotEmpty($sSQL);
+
+		$aFunctions = array(
+			'_itop_sum_' => 'SumExpr',
+			'_itop_avg_' => $oAvgExpr,
+			'_itop_min_' => $oMinExpr,
+			'_itop_max_' => $oMaxExpr,
+		);
+
+		$this->expectException("CoreException");
+		$oSearch->MakeGroupByQuery($aArgs, $aGroupBy, false, $aFunctions, $aOrderBy);
+
+		$this->assertTrue(false);
+	}
+
+	/**
+	 * @throws Exception
+	 */
+	public function testSanity_4()
+	{
+		$oSearch = DBSearch::FromOQL("SELECT FiberChannelInterface AS FCI WHERE FCI.name = '1' UNION SELECT PhysicalInterface AS PHI WHERE PHI.name = '1'");
+		$this->assertNotNull($oSearch);
+
+		// Alias => Expression (first select reference)
+		$oExpr1 = Expression::FromOQL('FCI.name');
+		$aGroupBy = array('group1' => $oExpr1);
+
+		$oTimeExpr = Expression::FromOQL('FCI.speed');
+		$oSumExpr = new FunctionExpression('SUM', array($oTimeExpr));
+		$oAvgExpr = new FunctionExpression('AVG', array($oTimeExpr));
+		$oMinExpr = new FunctionExpression('MIN', array($oTimeExpr));
+		$oMaxExpr = new FunctionExpression('MAX', array($oTimeExpr));
+		// Alias => Expression
+		$aFunctions = array(
+			'_itop_sum_' => $oSumExpr,
+			'_itop_avg_' => $oAvgExpr,
+			'_itop_min_' => $oMinExpr,
+			'_itop_max_' => $oMaxExpr,
+		);
+		$aArgs = array();
+
+		// Alias => Order
+		$aOrderBy = array(
+			'group1' => true,
+			'_itop_sum_' => true,
+			'_itop_avg_' => true,
+			'_itop_min_' => true,
+			'_itop_max_' => true);
+
+		$sSQL = $oSearch->MakeGroupByQuery($aArgs, $aGroupBy, false, $aFunctions, $aOrderBy);
+		$this->assertNotEmpty($sSQL);
+
+		$aOrderBy = array(
+			'group1' => true,
+			'_itop_sum_' => true,
+			'_itop_avg_' => 'ASC',
+			'_itop_min_' => true,
+			'_itop_max_' => true);
+
+		$this->expectException("CoreException");
+		$oSearch->MakeGroupByQuery($aArgs, $aGroupBy, false, $aFunctions, $aOrderBy);
+
+		$this->assertTrue(false);
+	}
+
+	/**
+	 * @throws Exception
+	 */
+	public function testSanity_5()
+	{
+		$oSearch = DBSearch::FromOQL("SELECT FiberChannelInterface AS FCI WHERE FCI.name = '1' UNION SELECT PhysicalInterface AS PHI WHERE PHI.name = '1'");
+		$this->assertNotNull($oSearch);
+
+		// Alias => Expression (first select reference)
+		$oExpr1 = Expression::FromOQL('FCI.name');
+		$aGroupBy = array('group1' => $oExpr1);
+
+		$oTimeExpr = Expression::FromOQL('FCI.speed');
+		$oSumExpr = new FunctionExpression('SUM', array($oTimeExpr));
+		$oAvgExpr = new FunctionExpression('AVG', array($oTimeExpr));
+		$oMinExpr = new FunctionExpression('MIN', array($oTimeExpr));
+		$oMaxExpr = new FunctionExpression('MAX', array($oTimeExpr));
+		// Alias => Expression
+		$aFunctions = array(
+			'_itop_sum_' => $oSumExpr,
+			'_itop_avg_' => $oAvgExpr,
+			'_itop_min_' => $oMinExpr,
+			'_itop_max_' => $oMaxExpr,
+		);
+		$aArgs = array();
+
+		// Alias => Order
+		$aOrderBy = array(
+			'group1' => true,
+			'_itop_sum_' => true,
+			'_itop_avg_' => true,
+			'_itop_min_' => true,
+			'_itop_max_' => true);
+		$sSQL = $oSearch->MakeGroupByQuery($aArgs, $aGroupBy, false, $aFunctions, $aOrderBy);
+		$this->assertNotEmpty($sSQL);
+
+		// Alias => Order
+		$aOrderBy = array('nothing_good' => true);
+		$this->expectException("CoreException");
+		$oSearch->MakeGroupByQuery($aArgs, $aGroupBy, false, $aFunctions, $aOrderBy);
+
+		$this->assertTrue(false);
+	}
+
+}