diff --git a/application/dashlet.class.inc.php b/application/dashlet.class.inc.php index 37469603f..240a462c3 100644 --- a/application/dashlet.class.inc.php +++ b/application/dashlet.class.inc.php @@ -373,20 +373,63 @@ abstract class DashletGroupBy extends Dashlet } else { + $oFilter = DBObjectSearch::FromOQL($sQuery); + $sClassAlias = $oFilter->GetClassAlias(); + + if (preg_match('/^(.*):(.*)$/', $sGroupBy, $aMatches)) + { + $sAttCode = $aMatches[1]; + $sFunction = $aMatches[2]; + + switch($sFunction) + { + case 'hour': + $sGroupByLabel = 'Hour of '.$sAttCode. ' (0-23)'; + $sGroupByExpr = "DATE_FORMAT($sClassAlias.$sAttCode, '%H')"; // 0 -> 31 + break; + + case 'month': + $sGroupByLabel = 'Month of '.$sAttCode. ' (1 - 12)'; + $sGroupByExpr = "DATE_FORMAT($sClassAlias.$sAttCode, '%m')"; // 0 -> 31 + break; + + case 'day_of_week': + $sGroupByLabel = 'Day of week for '.$sAttCode. ' (sunday to saturday)'; + $sGroupByExpr = "DATE_FORMAT($sClassAlias.$sAttCode, '%w')"; + break; + + case 'day_of_month': + $sGroupByLabel = 'Day of month for'.$sAttCode; + $sGroupByExpr = "DATE_FORMAT($sClassAlias.$sAttCode, '%e')"; // 0 -> 31 + break; + + default: + $sGroupByLabel = 'Unknown group by function '.$sFunction; + $sGroupByExpr = $sClassAlias.'.'.$sAttCode; + } + } + else + { + $sAttCode = $sGroupBy; + + $sGroupByExpr = $sClassAlias.'.'.$sAttCode; + $sGroupByLabel = MetaModel::GetLabel($oFilter->GetClass(), $sAttCode); + } + switch($sStyle) { case 'bars': - $sXML = ''.$sQuery.''; + $sXML = ''.$sQuery.''; $sHtmlTitle = ''; // done in the itop block break; case 'pie': - $sXML = ''.$sQuery.''; + $sXML = ''.$sQuery.''; $sHtmlTitle = ''; // done in the itop block break; case 'table': default: $sHtmlTitle = htmlentities(Dict::S($sTitle), ENT_QUOTES, 'UTF-8'); // done in the itop block - $sXML = ''.$sQuery.''; + $sXML = ''.$sQuery.''; break; } @@ -429,10 +472,11 @@ abstract class DashletGroupBy extends Dashlet if ($oAttDef instanceof AttributeDateTime) { - //date_format(start_date, '%d') - $aGroupBy['date_of_'.$sAttCode] = 'Day of '.$oAttDef->GetLabel(); + $aGroupBy[$sAttCode.':hour'] = $oAttDef->GetLabel().' (hour)'; + $aGroupBy[$sAttCode.':month'] = $oAttDef->GetLabel().' (month)'; + $aGroupBy[$sAttCode.':day_of_week'] = $oAttDef->GetLabel().' (day of week)'; + $aGroupBy[$sAttCode.':day_of_month'] = $oAttDef->GetLabel().' (day of month)'; } - } @@ -719,3 +763,92 @@ class DashletBadge extends Dashlet } } + +class DashletProto extends Dashlet +{ + public function __construct($sId) + { + parent::__construct($sId); + $this->aProperties['class'] = 'Foo'; + } + + public function Render($oPage, $bEditMode = false, $aExtraParams = array()) + { + $sClass = $this->aProperties['class']; + + $oFilter = DBObjectSearch::FromOQL('SELECT FunctionalCI AS fci'); + $sGroupBy1 = 'status'; + $sGroupBy2 = 'org_id_friendlyname'; + $sHtmlTitle = "Hardcoded on $sGroupBy1 and $sGroupBy2..."; + + $sAlias = $oFilter->GetClassAlias(); + + $oGroupByExp1 = new FieldExpression($sGroupBy1, $sAlias); + $sGroupByLabel1 = MetaModel::GetLabel($oFilter->GetClass(), $sGroupBy1); + + $oGroupByExp2 = new FieldExpression($sGroupBy2, $sAlias); + $sGroupByLabel2 = MetaModel::GetLabel($oFilter->GetClass(), $sGroupBy2); + + $aGroupBy = array(); + $aGroupBy['grouped_by_1'] = $oGroupByExp1; + $aGroupBy['grouped_by_2'] = $oGroupByExp2; + $sSql = MetaModel::MakeGroupByQuery($oFilter, array(), $aGroupBy); + $aRes = CMDBSource::QueryToArray($sSql); + + $iTotalCount = 0; + $aData = array(); + $oAppContext = new ApplicationContext(); + $sParams = $oAppContext->GetForLink(); + foreach ($aRes as $aRow) + { + $iCount = $aRow['_itop_count_']; + $iTotalCount += $iCount; + + $sValue1 = $aRow['grouped_by_1']; + $sValue2 = $aRow['grouped_by_2']; + + // Build the search for this subset + $oSubsetSearch = clone $oFilter; + $oCondition = new BinaryExpression($oGroupByExp1, '=', new ScalarExpression($sValue1)); + $oSubsetSearch->AddConditionExpression($oCondition); + $oCondition = new BinaryExpression($oGroupByExp2, '=', new ScalarExpression($sValue2)); + $oSubsetSearch->AddConditionExpression($oCondition); + + $sFilter = urlencode($oSubsetSearch->serialize()); + $aData[] = array ( + 'group1' => htmlentities($sValue1, ENT_QUOTES, 'UTF-8'), + 'group2' => htmlentities($sValue2, ENT_QUOTES, 'UTF-8'), + 'value' => "$iCount" + ); // TO DO: add the context information + } + $aAttribs =array( + 'group1' => array('label' => $sGroupByLabel1, 'description' => ''), + 'group2' => array('label' => $sGroupByLabel2, 'description' => ''), + 'value' => array('label'=> Dict::S('UI:GroupBy:Count'), 'description' => Dict::S('UI:GroupBy:Count+')) + ); + + + $oPage->add('
'); + + $oPage->add('

'.$sHtmlTitle.'

'); + $oPage->p(Dict::Format('UI:Pagination:HeaderNoSelection', $iTotalCount)); + $oPage->table($aAttribs, $aData); + + $oPage->add('
'); + } + + public function GetPropertiesFields(DesignerForm $oForm) + { + $oField = new DesignerTextField('class', 'Class', $this->aProperties['class']); + $oForm->AddField($oField); + } + + static public function GetInfo() + { + return array( + 'label' => 'Test3D', + 'icon' => 'images/xxxxxx.png', + 'description' => 'Group by on two dimensions', + ); + } +} diff --git a/application/displayblock.class.inc.php b/application/displayblock.class.inc.php index 36c0b3c2a..4a449f95e 100644 --- a/application/displayblock.class.inc.php +++ b/application/displayblock.class.inc.php @@ -347,34 +347,51 @@ class DisplayBlock case 'count': if (isset($aExtraParams['group_by'])) { - $sGroupByField = $aExtraParams['group_by']; + if (isset($aExtraParams['group_by_label'])) + { + $oGroupByExp = Expression::FromOQL($aExtraParams['group_by']); + $sGroupByLabel = $aExtraParams['group_by_label']; + } + else + { + // Backward compatibility: group_by is simply a field id + $sAlias = $this->m_oFilter->GetClassAlias(); + $oGroupByExp = new FieldExpression($aExtraParams['group_by'], $sAlias); + $sGroupByLabel = MetaModel::GetLabel($this->m_oFilter->GetClass(), $aExtraParams['group_by']); + } + + $aGroupBy = array(); + $aGroupBy['grouped_by_1'] = $oGroupByExp; + $sSql = MetaModel::MakeGroupByQuery($this->m_oFilter, $aQueryParams, $aGroupBy); + $aRes = CMDBSource::QueryToArray($sSql); + $aGroupBy = array(); $sLabels = array(); - $iTotalCount = $this->m_oSet->Count(); - while($oObj = $this->m_oSet->Fetch()) + $iTotalCount = 0; + foreach ($aRes as $aRow) { - if (isset($aExtraParams['group_by_expr'])) - { - eval("\$sValue = ".sprintf($aExtraParams['group_by_expr'], $oObj->Get($sGroupByField)).';'); - } - else - { - $sValue = $oObj->Get($sGroupByField); - } - $aGroupBy[$sValue] = isset($aGroupBy[$sValue]) ? $aGroupBy[$sValue]+1 : 1; - $sLabels[$sValue] = $oObj->GetAsHtml($sGroupByField); + $sValue = $aRow['grouped_by_1']; + $sLabels[$sValue] = htmlentities($sValue, ENT_QUOTES, 'UTF-8'); + $aGroupBy[$sValue] = (int) $aRow['_itop_count_']; + $iTotalCount += $aRow['_itop_count_']; } - $sFilter = urlencode($this->m_oFilter->serialize()); + $aData = array(); $oAppContext = new ApplicationContext(); $sParams = $oAppContext->GetForLink(); foreach($aGroupBy as $sValue => $iCount) { + // Build the search for this subset + $oSubsetSearch = clone $this->m_oFilter; + $oCondition = new BinaryExpression($oGroupByExp, '=', new ScalarExpression($sValue)); + $oSubsetSearch->AddConditionExpression($oCondition); + $sFilter = urlencode($oSubsetSearch->serialize()); + $aData[] = array ( 'group' => $sLabels[$sValue], - 'value' => "$iCount"); // TO DO: add the context information + 'value' => "$iCount"); // TO DO: add the context information } $aAttribs =array( - 'group' => array('label' => MetaModel::GetLabel($this->m_oFilter->GetClass(), $sGroupByField), 'description' => ''), + 'group' => array('label' => $sGroupByLabel, 'description' => ''), 'value' => array('label'=> Dict::S('UI:GroupBy:Count'), 'description' => Dict::S('UI:GroupBy:Count+')) ); $sFormat = isset($aExtraParams['format']) ? $aExtraParams['format'] : 'UI:Pagination:HeaderNoSelection'; @@ -748,35 +765,60 @@ EOF $sFilter = $this->m_oFilter->serialize(); $sHtml .= "
If the chart does not display, install Flash
\n"; $oPage->add_script("function ofc_resize(left, width, top, height) { /* do nothing special */ }"); + if (isset($aExtraParams['group_by_label'])) + { + $sUrl = urlencode(utils::GetAbsoluteUrlAppRoot()."pages/ajax.render.php?operation=open_flash_chart¶ms[group_by]=$sGroupBy{$sGroupByExpr}¶ms[group_by_label]={$aExtraParams['group_by_label']}¶ms[chart_type]=$sChartType¶ms[chart_title]=$sTitle¶ms[currentId]=$sId&id=$sId&filter=".$sFilter); + } + else + { + $sUrl = urlencode(utils::GetAbsoluteUrlAppRoot()."pages/ajax.render.php?operation=open_flash_chart¶ms[group_by]=$sGroupBy{$sGroupByExpr}¶ms[chart_type]=$sChartType¶ms[chart_title]=$sTitle¶ms[currentId]=$sId&id=$sId&filter=".$sFilter); + } + $oPage->add_ready_script("swfobject.embedSWF(\"../images/open-flash-chart.swf\", \"my_chart_$sId{$iChartCounter}\", \"100%\", \"300\",\"9.0.0\", \"expressInstall.swf\", - {\"data-file\":\"".urlencode(utils::GetAbsoluteUrlAppRoot()."pages/ajax.render.php?operation=open_flash_chart¶ms[group_by]=$sGroupBy{$sGroupByExpr}¶ms[chart_type]=$sChartType¶ms[chart_title]=$sTitle¶ms[currentId]=$sId&id=$sId&filter=".$sFilter)."\"}, {wmode: 'transparent'} );\n"); + {\"data-file\":\"".$sUrl."\"}, {wmode: 'transparent'} );\n"); $iChartCounter++; if (isset($aExtraParams['group_by'])) { - $sGroupByField = $aExtraParams['group_by']; - $aGroupBy = array(); - while($oObj = $this->m_oSet->Fetch()) + if (isset($aExtraParams['group_by_label'])) { - if (isset($aExtraParams['group_by_expr'])) - { - eval("\$sValue = ".sprintf($aExtraParams['group_by_expr'], $oObj->Get($sGroupByField)).';'); - } - else - { - $sValue = $oObj->Get($sGroupByField); - } - $aGroupBy[$sValue] = isset($aGroupBy[$sValue]) ? $aGroupBy[$sValue]+1 : 1; + $oGroupByExp = Expression::FromOQL($aExtraParams['group_by']); + $sGroupByLabel = $aExtraParams['group_by_label']; } - $sFilter = urlencode($this->m_oFilter->serialize()); + else + { + // Backward compatibility: group_by is simply a field id + $sAlias = $this->m_oFilter->GetClassAlias(); + $oGroupByExp = new FieldExpression($aExtraParams['group_by'], $sAlias); + $sGroupByLabel = MetaModel::GetLabel($this->m_oFilter->GetClass(), $aExtraParams['group_by']); + } + + $aGroupBy = array(); + $aGroupBy['grouped_by_1'] = $oGroupByExp; + $sSql = MetaModel::MakeGroupByQuery($this->m_oFilter, $aQueryParams, $aGroupBy); + $aRes = CMDBSource::QueryToArray($sSql); + + $aGroupBy = array(); + $sLabels = array(); + $iTotalCount = 0; + foreach ($aRes as $aRow) + { + $sValue = $aRow['grouped_by_1']; + $sLabels[$sValue] = htmlentities($sValue, ENT_QUOTES, 'UTF-8'); + $aGroupBy[$sValue] = (int) $aRow['_itop_count_']; + $iTotalCount += $aRow['_itop_count_']; + } + $aData = array(); $aLabels = array(); $idx = 0; $aURLs = array(); foreach($aGroupBy as $sValue => $iValue) { - $oDrillDownFilter = clone $this->m_oFilter; - $oDrillDownFilter->AddCondition($sGroupByField, $sValue, '='); - $aURLs[$idx] = $oDrillDownFilter->serialize(); + // Build the search for this subset + $oSubsetSearch = clone $this->m_oFilter; + $oCondition = new BinaryExpression($oGroupByExp, '=', new ScalarExpression($sValue)); + $oSubsetSearch->AddConditionExpression($oCondition); + $aURLs[$idx] = $oSubsetSearch->serialize(); $idx++; } $sURLList = ''; @@ -810,21 +852,35 @@ EOF if (isset($aExtraParams['group_by'])) { - $sGroupByField = $aExtraParams['group_by']; - $aGroupBy = array(); - while($oObj = $this->m_oSet->Fetch()) + if (isset($aExtraParams['group_by_label'])) { - if (isset($aExtraParams['group_by_expr'])) - { - eval("\$sValue = ".sprintf($aExtraParams['group_by_expr'], $oObj->Get($sGroupByField)).';'); - } - else - { - $sValue = $oObj->Get($sGroupByField); - } - $aGroupBy[$sValue] = isset($aGroupBy[$sValue]) ? $aGroupBy[$sValue]+1 : 1; + $oGroupByExp = Expression::FromOQL($aExtraParams['group_by']); + $sGroupByLabel = $aExtraParams['group_by_label']; } - $sFilter = urlencode($this->m_oFilter->serialize()); + else + { + // Backward compatibility: group_by is simply a field id + $sAlias = $this->m_oFilter->GetClassAlias(); + $oGroupByExp = new FieldExpression($aExtraParams['group_by'], $sAlias); + $sGroupByLabel = MetaModel::GetLabel($this->m_oFilter->GetClass(), $aExtraParams['group_by']); + } + + $aGroupBy = array(); + $aGroupBy['grouped_by_1'] = $oGroupByExp; + $sSql = MetaModel::MakeGroupByQuery($this->m_oFilter, $aQueryParams, $aGroupBy); + $aRes = CMDBSource::QueryToArray($sSql); + + $aGroupBy = array(); + $sLabels = array(); + $iTotalCount = 0; + foreach ($aRes as $aRow) + { + $sValue = $aRow['grouped_by_1']; + $sLabels[$sValue] = htmlentities($sValue, ENT_QUOTES, 'UTF-8'); + $aGroupBy[$sValue] = (int) $aRow['_itop_count_']; + $iTotalCount += $aRow['_itop_count_']; + } + $aData = array(); $aLabels = array(); $maxValue = 0; @@ -876,21 +932,35 @@ EOF $oChartElement->set_colours( array('#FF8A00', '#909980', '#2C2B33', '#CCC08D', '#596664') ); if (isset($aExtraParams['group_by'])) { - $sGroupByField = $aExtraParams['group_by']; - $aGroupBy = array(); - while($oObj = $this->m_oSet->Fetch()) + if (isset($aExtraParams['group_by_label'])) { - if (isset($aExtraParams['group_by_expr'])) - { - eval("\$sValue = ".sprintf($aExtraParams['group_by_expr'], $oObj->Get($sGroupByField)).';'); - } - else - { - $sValue = $oObj->Get($sGroupByField); - } - $aGroupBy[$sValue] = isset($aGroupBy[$sValue]) ? $aGroupBy[$sValue]+1 : 1; + $oGroupByExp = Expression::FromOQL($aExtraParams['group_by']); + $sGroupByLabel = $aExtraParams['group_by_label']; } - $sFilter = urlencode($this->m_oFilter->serialize()); + else + { + // Backward compatibility: group_by is simply a field id + $sAlias = $this->m_oFilter->GetClassAlias(); + $oGroupByExp = new FieldExpression($aExtraParams['group_by'], $sAlias); + $sGroupByLabel = MetaModel::GetLabel($this->m_oFilter->GetClass(), $aExtraParams['group_by']); + } + + $aGroupBy = array(); + $aGroupBy['grouped_by_1'] = $oGroupByExp; + $sSql = MetaModel::MakeGroupByQuery($this->m_oFilter, $aQueryParams, $aGroupBy); + $aRes = CMDBSource::QueryToArray($sSql); + + $aGroupBy = array(); + $sLabels = array(); + $iTotalCount = 0; + foreach ($aRes as $aRow) + { + $sValue = $aRow['grouped_by_1']; + $sLabels[$sValue] = htmlentities($sValue, ENT_QUOTES, 'UTF-8'); + $aGroupBy[$sValue] = (int) $aRow['_itop_count_']; + $iTotalCount += $aRow['_itop_count_']; + } + $aData = array(); foreach($aGroupBy as $sValue => $iValue) { diff --git a/core/expression.class.inc.php b/core/expression.class.inc.php index 75f6184fa..fe414112b 100644 --- a/core/expression.class.inc.php +++ b/core/expression.class.inc.php @@ -951,12 +951,14 @@ class QueryBuilderExpressions { protected $m_oConditionExpr; protected $m_aSelectExpr; + protected $m_aGroupByExpr; protected $m_aJoinFields; - public function __construct($oCondition) + public function __construct($oCondition, $aGroupByExpr = null) { $this->m_oConditionExpr = $oCondition; $this->m_aSelectExpr = array(); + $this->m_aGroupByExpr = $aGroupByExpr; $this->m_aJoinFields = array(); } @@ -965,6 +967,11 @@ class QueryBuilderExpressions return $this->m_aSelectExpr; } + public function GetGroupBy() + { + return $this->m_aGroupByExpr; + } + public function GetCondition() { return $this->m_oConditionExpr; @@ -998,6 +1005,13 @@ class QueryBuilderExpressions { $oExpr->GetUnresolvedFields($sAlias, $aUnresolved); } + if ($this->m_aGroupByExpr) + { + foreach($this->m_aGroupByExpr as $sColAlias => $oExpr) + { + $oExpr->GetUnresolvedFields($sAlias, $aUnresolved); + } + } foreach($this->m_aJoinFields as $oExpression) { $oExpression->GetUnresolvedFields($sAlias, $aUnresolved); @@ -1011,6 +1025,13 @@ class QueryBuilderExpressions { $this->m_aSelectExpr[$sColAlias] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); } + if ($this->m_aGroupByExpr) + { + foreach($this->m_aGroupByExpr as $sColAlias => $oExpr) + { + $this->m_aGroupByExpr[$sColAlias] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); + } + } foreach($this->m_aJoinFields as $index => $oExpression) { $this->m_aJoinFields[$index] = $oExpression->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved); @@ -1024,6 +1045,13 @@ class QueryBuilderExpressions { $this->m_aSelectExpr[$sColAlias] = $oExpr->RenameParam($sOldName, $sNewName); } + if ($this->m_aGroupByExpr) + { + foreach($this->m_aGroupByExpr as $sColAlias => $oExpr) + { + $this->m_aGroupByExpr[$sColAlias] = $oExpr->RenameParam($sOldName, $sNewName); + } + } foreach($this->m_aJoinFields as $index => $oExpression) { $this->m_aJoinFields[$index] = $oExpression->RenameParam($sOldName, $sNewName); diff --git a/core/metamodel.class.php b/core/metamodel.class.php index e31dba553..973904dca 100644 --- a/core/metamodel.class.php +++ b/core/metamodel.class.php @@ -1933,7 +1933,81 @@ abstract class MetaModel return $aScalarArgs; } + public static function MakeGroupByQuery(DBObjectSearch $oFilter, $aArgs, $aGroupByExpr) + { + $aAttToLoad = array(); + $oSelect = self::MakeSelectStructure($oFilter, array(), $aArgs, $aAttToLoad, null, 0, 0, false, $aGroupByExpr); + + $aScalarArgs = array_merge(self::PrepareQueryArguments($aArgs), $oFilter->GetInternalParams()); + try + { + $sRes = $oSelect->RenderGroupBy($aScalarArgs); + } + catch (MissingQueryArgument $e) + { + // Add some information... + $e->addInfo('OQL', $sOqlQuery); + throw $e; + } + return $sRes; + } + + public static function MakeSelectQuery(DBObjectSearch $oFilter, $aOrderBy = array(), $aArgs = array(), $aAttToLoad = null, $aExtendedDataSpec = null, $iLimitCount = 0, $iLimitStart = 0, $bGetCount = false) + { + // Check the order by specification, and prefix with the class alias + // and make sure that the ordering columns are going to be selected + // + $aOrderSpec = array(); + foreach ($aOrderBy as $sFieldAlias => $bAscending) + { + if ($sFieldAlias != 'id') + { + MyHelpers::CheckValueInArray('field name in ORDER BY spec', $sFieldAlias, self::GetAttributesList($oFilter->GetFirstJoinedClass())); + } + if (!is_bool($bAscending)) + { + throw new CoreException("Wrong direction in ORDER BY spec, found '$bAscending' and expecting a boolean value"); + } + $sFirstClassAlias = $oFilter->GetFirstJoinedClassAlias(); + if (self::IsValidAttCode($oFilter->GetClass(), $sFieldAlias)) + { + $oAttDef = self::GetAttributeDef($oFilter->GetClass(), $sFieldAlias); + foreach($oAttDef->GetOrderBySQLExpressions($sFirstClassAlias) as $sSQLExpression) + { + $aOrderSpec[$sSQLExpression] = $bAscending; + } + } + else + { + $aOrderSpec['`'.$sFirstClassAlias.$sFieldAlias.'`'] = $bAscending; + } + + // Make sure that the columns used for sorting are present in the loaded columns + if (!is_null($aAttToLoad) && !isset($aAttToLoad[$sFirstClassAlias][$sFieldAlias])) + { + $aAttToLoad[$sFirstClassAlias][$sFieldAlias] = MetaModel::GetAttributeDef($oFilter->GetFirstJoinedClass(), $sFieldAlias); + } + } + + $oSelect = self::MakeSelectStructure($oFilter, $aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount); + + $aScalarArgs = array_merge(self::PrepareQueryArguments($aArgs), $oFilter->GetInternalParams()); + try + { + $sRes = $oSelect->RenderSelect($aOrderSpec, $aScalarArgs, $iLimitCount, $iLimitStart, $bGetCount); + } + catch (MissingQueryArgument $e) + { + // Add some information... + $e->addInfo('OQL', $sOqlQuery); + throw $e; + } + return $sRes; + } + + + protected static function MakeSelectStructure(DBObjectSearch $oFilter, $aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount, $aGroupByExpr = null) { // Hide objects that are not visible to the current user // @@ -1984,6 +2058,13 @@ abstract class MetaModel $sRawId = $sOqlQuery.'|'.implode(',', array_keys($aAttributes)); } } + if (!is_null($aGroupByExpr)) + { + foreach($aGroupByExpr as $sAlias => $oExpr) + { + $sRawId = 'g:'.$sAlias.'!'.$oExpr->Render(); + } + } $sOqlId = md5($sRawId); } else @@ -2029,48 +2110,25 @@ abstract class MetaModel } } - // Check the order by specification, and prefix with the class alias - // and make sure that the ordering columns are going to be selected - // - $aOrderSpec = array(); - foreach ($aOrderBy as $sFieldAlias => $bAscending) - { - if ($sFieldAlias != 'id') - { - MyHelpers::CheckValueInArray('field name in ORDER BY spec', $sFieldAlias, self::GetAttributesList($oFilter->GetFirstJoinedClass())); - } - if (!is_bool($bAscending)) - { - throw new CoreException("Wrong direction in ORDER BY spec, found '$bAscending' and expecting a boolean value"); - } - $sFirstClassAlias = $oFilter->GetFirstJoinedClassAlias(); - if (self::IsValidAttCode($oFilter->GetClass(), $sFieldAlias)) - { - $oAttDef = self::GetAttributeDef($oFilter->GetClass(), $sFieldAlias); - foreach($oAttDef->GetOrderBySQLExpressions($sFirstClassAlias) as $sSQLExpression) - { - $aOrderSpec[$sSQLExpression] = $bAscending; - } - } - else - { - $aOrderSpec['`'.$sFirstClassAlias.$sFieldAlias.'`'] = $bAscending; - } - - // Make sure that the columns used for sorting are present in the loaded columns - if (!is_null($aAttToLoad) && !isset($aAttToLoad[$sFirstClassAlias][$sFieldAlias])) - { - $aAttToLoad[$sFirstClassAlias][$sFieldAlias] = MetaModel::GetAttributeDef($oFilter->GetFirstJoinedClass(), $sFieldAlias); - } - } - if (!isset($oSelect)) { - $oBuild = new QueryBuilderContext($oFilter, $aModifierProperties); + $oBuild = new QueryBuilderContext($oFilter, $aModifierProperties, $aGroupByExpr); $oKPI = new ExecutionKPI(); $oSelect = self::MakeQuery($oBuild, $oFilter, $aAttToLoad, array(), true /* main query */); + $oSelect->SetCondition($oBuild->m_oQBExpressions->GetCondition()); $oSelect->SetSourceOQL($sOqlQuery); + if ($aGroupByExpr) + { + $aCols = $oBuild->m_oQBExpressions->GetGroupBy(); + $oSelect->SetGroupBy($aCols); + $oSelect->SetSelect($aCols); + } + else + { + $oSelect->SetSelect($oBuild->m_oQBExpressions->GetSelect()); + } + $oKPI->ComputeStats('MakeQuery (select)', $sOqlQuery); if (self::$m_bQueryCacheEnabled) @@ -2101,22 +2159,6 @@ abstract class MetaModel $oSelect->AddInnerJoin($oSelectExt, 'id', $aExtendedDataSpec['join_key'] /*, $sTableAlias*/); } - // Go - // - $aScalarArgs = array_merge(self::PrepareQueryArguments($aArgs), $oFilter->GetInternalParams()); - - try - { - $sRes = $oSelect->RenderSelect($aOrderSpec, $aScalarArgs, $iLimitCount, $iLimitStart, $bGetCount); -//echo "

MakeQuery: $sRes

"; - } - catch (MissingQueryArgument $e) - { - // Add some information... - $e->addInfo('OQL', $sOqlQuery); - throw $e; - } - if (self::$m_bTraceQueries) { $sQueryId = md5($sRes); @@ -2140,7 +2182,7 @@ abstract class MetaModel } } - return $sRes; + return $oSelect; } public static function ShowQueryTrace() @@ -2205,6 +2247,8 @@ abstract class MetaModel $aModifierProperties = self::MakeModifierProperties($oFilter); $oBuild = new QueryBuilderContext($oFilter, $aModifierProperties); $oSelect = self::MakeQuery($oBuild, $oFilter, null, array(), true /* main query */); + $oSelect->SetCondition($oBuild->m_oQBExpressions->GetCondition()); + $oSelect->SetSelect($oBuild->m_oQBExpressions->GetSelect()); $aScalarArgs = array_merge(self::PrepareQueryArguments($aArgs), $oFilter->GetInternalParams()); return $oSelect->RenderDelete($aScalarArgs); } @@ -2215,11 +2259,13 @@ abstract class MetaModel $aModifierProperties = self::MakeModifierProperties($oFilter); $oBuild = new QueryBuilderContext($oFilter, $aModifierProperties); $oSelect = self::MakeQuery($oBuild, $oFilter, null, $aValues, true /* main query */); + $oSelect->SetCondition($oBuild->m_oQBExpressions->GetCondition()); + $oSelect->SetSelect($oBuild->m_oQBExpressions->GetSelect()); $aScalarArgs = array_merge(self::PrepareQueryArguments($aArgs), $oFilter->GetInternalParams()); return $oSelect->RenderUpdate($aScalarArgs); } - private static function MakeQuery(&$oBuild, DBObjectSearch $oFilter, $aAttToLoad = null, $aValues = array(), $bIsMainQuery = false) + private static function MakeQuery(&$oBuild, DBObjectSearch $oFilter, $aAttToLoad = null, $aValues = array(), $bIsMainQueryUNUSED = false) { // Note: query class might be different than the class of the filter // -> this occurs when we are linking our class to an external class (referenced by, or pointing to) @@ -2442,14 +2488,6 @@ abstract class MetaModel } } - // Translate the conditions... and go - // - if ($bIsMainQuery) - { - $oSelectBase->SetCondition($oBuild->m_oQBExpressions->GetCondition()); - $oSelectBase->SetSelect($oBuild->m_oQBExpressions->GetSelect()); - } - // That's all... cross fingers and we'll get some working query //MyHelpers::var_dump_html($oSelectBase, true); diff --git a/core/querybuildercontext.class.inc.php b/core/querybuildercontext.class.inc.php index 8c6e88544..f4cc5f50c 100644 --- a/core/querybuildercontext.class.inc.php +++ b/core/querybuildercontext.class.inc.php @@ -32,10 +32,10 @@ class QueryBuilderContext public $m_oQBExpressions; - public function __construct($oFilter, $aModifierProperties) + public function __construct($oFilter, $aModifierProperties, $aGroupByExpr = null) { $this->m_oRootFilter = $oFilter; - $this->m_oQBExpressions = new QueryBuilderExpressions($oFilter->GetCriteria()); + $this->m_oQBExpressions = new QueryBuilderExpressions($oFilter->GetCriteria(), $aGroupByExpr); $this->m_aClassAliases = $oFilter->GetJoinedClasses(); $this->m_aTableAliases = array(); diff --git a/core/sqlquery.class.inc.php b/core/sqlquery.class.inc.php index 674ede0ab..ed0743445 100644 --- a/core/sqlquery.class.inc.php +++ b/core/sqlquery.class.inc.php @@ -41,6 +41,7 @@ class SQLQuery private $m_sTable = ''; private $m_sTableAlias = ''; private $m_aFields = array(); + private $m_aGroupBy = array(); private $m_oConditionExpr = null; private $m_bToDelete = true; // The current table must be listed for deletion ? private $m_aValues = array(); // Values to set in case of an update query @@ -62,6 +63,7 @@ class SQLQuery $this->m_sTable = $sTable; $this->m_sTableAlias = $sTableAlias; $this->m_aFields = $aFields; + $this->m_aGroupBy = null; $this->m_oConditionExpr = null; $this->m_bToDelete = $bToDelete; $this->m_aValues = $aValues; @@ -125,11 +127,12 @@ class SQLQuery } $aFrom = array(); $aFields = array(); + $aGroupBy = array(); $oCondition = null; $aDelTables = array(); $aSetValues = array(); $aSelectedIdFields = array(); - $this->privRender($aFrom, $aFields, $oCondition, $aDelTables, $aSetValues, $aSelectedIdFields); + $this->privRender($aFrom, $aFields, $aGroupBy, $oCondition, $aDelTables, $aSetValues, $aSelectedIdFields); echo "From ...
\n"; echo "
\n";
 		print_r($aFrom);
@@ -141,6 +144,11 @@ class SQLQuery
 		$this->m_aFields = $aExpressions;
 	}
 
+	public function SetGroupBy($aExpressions)
+	{
+		$this->m_aGroupBy = $aExpressions;
+	}
+
 	public function SetCondition($oConditionExpr)
 	{
 		$this->m_oConditionExpr = $oConditionExpr;
@@ -235,11 +243,12 @@ class SQLQuery
 		// The goal will be to complete the list as we build the Joins
 		$aFrom = array();
 		$aFields = array();
+		$aGroupBy = arry();
 		$oCondition = null;
 		$aDelTables = array();
 		$aSetValues = array();
 		$aSelectedIdFields = array();
-		$this->privRender($aFrom, $aFields, $oCondition, $aDelTables, $aSetValues, $aSelectedIdFields);
+		$this->privRender($aFrom, $aFields, $aGroupBy, $oCondition, $aDelTables, $aSetValues, $aSelectedIdFields);
 
 		// Target: DELETE myAlias1, myAlias2 FROM t1 as myAlias1, t2 as myAlias2, t3 as topreserve WHERE ...
 
@@ -270,11 +279,12 @@ class SQLQuery
 		// The goal will be to complete the list as we build the Joins
 		$aFrom = array();
 		$aFields = array();
+		$aGroupBy = array();
 		$oCondition = null;
 		$aDelTables = array();
 		$aSetValues = array();
 		$aSelectedIdFields = array();
-		$this->privRender($aFrom, $aFields, $oCondition, $aDelTables, $aSetValues, $aSelectedIdFields);
+		$this->privRender($aFrom, $aFields, $aGroupBy, $oCondition, $aDelTables, $aSetValues, $aSelectedIdFields);
 		$sFrom   = self::ClauseFrom($aFrom);
 		$sValues = self::ClauseValues($aSetValues);
 		$sWhere  = self::ClauseWhere($oCondition, $aArgs);
@@ -287,11 +297,12 @@ class SQLQuery
 		// The goal will be to complete the lists as we build the Joins
 		$aFrom = array();
 		$aFields = array();
+		$aGroupBy = array();
 		$oCondition = null;
 		$aDelTables = array();
 		$aSetValues = array();
 		$aSelectedIdFields = array();
-		$this->privRender($aFrom, $aFields, $oCondition, $aDelTables, $aSetValues, $aSelectedIdFields);
+		$this->privRender($aFrom, $aFields, $aGroupBy, $oCondition, $aDelTables, $aSetValues, $aSelectedIdFields);
 
 		$sFrom   = self::ClauseFrom($aFrom);
 		$sWhere  = self::ClauseWhere($oCondition, $aArgs);
@@ -328,6 +339,27 @@ class SQLQuery
 		return $sSQL;
 	}
 
+	// Interface, build the SQL query
+	public function RenderGroupBy($aArgs = array())
+	{
+		// The goal will be to complete the lists as we build the Joins
+		$aFrom = array();
+		$aFields = array();
+		$aGroupBy = array();
+		$oCondition = null;
+		$aDelTables = array();
+		$aSetValues = array();
+		$aSelectedIdFields = array();
+		$this->privRender($aFrom, $aFields, $aGroupBy, $oCondition, $aDelTables, $aSetValues, $aSelectedIdFields);
+
+		$sSelect = self::ClauseSelect($aFields);
+		$sFrom   = self::ClauseFrom($aFrom);
+		$sWhere  = self::ClauseWhere($oCondition, $aArgs);
+		$sGroupBy = self::ClauseGroupBy($aGroupBy);
+		$sSQL = "SELECT $sSelect, COUNT(*) AS _itop_count_ FROM $sFrom WHERE $sWhere GROUP BY $sGroupBy";
+		return $sSQL;
+	}
+
 	private static function ClauseSelect($aFields)
 	{
 		$aSelect = array();
@@ -339,6 +371,12 @@ class SQLQuery
 		return $sSelect;
 	}
 
+	private static function ClauseGroupBy($aGroupBy)
+	{
+		$sRes = implode(', ', $aGroupBy);
+		return $sRes;
+	}
+
 	private static function ClauseDelete($aDelTableAliases)
 	{
 		$aDelTables = array();
@@ -415,14 +453,14 @@ class SQLQuery
 	}
 
 	// Purpose: prepare the query data, once for all
-	private function privRender(&$aFrom, &$aFields, &$oCondition, &$aDelTables, &$aSetValues, &$aSelectedIdFields)
+	private function privRender(&$aFrom, &$aFields, &$aGroupBy, &$oCondition, &$aDelTables, &$aSetValues, &$aSelectedIdFields)
 	{
-		$sTableAlias = $this->privRenderSingleTable($aFrom, $aFields, $aDelTables, $aSetValues, $aSelectedIdFields, '', array('jointype' => 'first'));
+		$sTableAlias = $this->privRenderSingleTable($aFrom, $aFields, $aGroupBy, $aDelTables, $aSetValues, $aSelectedIdFields, '', array('jointype' => 'first'));
 		$oCondition = $this->m_oConditionExpr;
 		return $sTableAlias; 
 	}
 
-	private function privRenderSingleTable(&$aFrom, &$aFields, &$aDelTables, &$aSetValues, &$aSelectedIdFields, $sCallerAlias = '', $aJoinData)
+	private function privRenderSingleTable(&$aFrom, &$aFields, &$aGroupBy, &$aDelTables, &$aSetValues, &$aSelectedIdFields, $sCallerAlias = '', $aJoinData)
 	{
 		$aActualTableFields = CMDBSource::GetTableFieldsList($this->m_sTable);
 
@@ -506,6 +544,13 @@ class SQLQuery
 		{
 			$aFields["`$sAlias`"] = $oExpression->Render();
 		}
+		if ($this->m_aGroupBy)
+		{
+			foreach($this->m_aGroupBy as $sAlias => $oExpression)
+			{
+				$aGroupBy["`$sAlias`"] = $oExpression->Render();
+			}
+		}
 		if ($this->m_bToDelete)
 		{
 			$aDelTables[] = "`{$this->m_sTableAlias}`";
@@ -528,7 +573,7 @@ class SQLQuery
 		{
 			$oRightSelect = $aJoinData["select"];
 
-			$sJoinTableAlias = $oRightSelect->privRenderSingleTable($aTempFrom, $aFields, $aDelTables, $aSetValues, $aSelectedIdFields, $this->m_sTableAlias, $aJoinData);
+			$sJoinTableAlias = $oRightSelect->privRenderSingleTable($aTempFrom, $aFields, $aGroupBy, $aDelTables, $aSetValues, $aSelectedIdFields, $this->m_sTableAlias, $aJoinData);
 		}
 		$aFrom[$this->m_sTableAlias]['subfrom'] = $aTempFrom;