Browse Source

Dashboard: optimized group by (done by MySQL) + proto of a group by on 2 dimensions

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@2041 a333f486-631f-4898-b8df-5754b55c2be0
romainq 13 years ago
parent
commit
c000cddae5

+ 139 - 6
application/dashlet.class.inc.php

@@ -373,20 +373,63 @@ abstract class DashletGroupBy extends Dashlet
 		}
 		}
 		else
 		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)
 			switch($sStyle)
 			{
 			{
 			case 'bars':
 			case 'bars':
-				$sXML = '<itopblock BlockClass="DisplayBlock" type="open_flash_chart" parameters="chart_type:bars;chart_title:'.$sTitle.';group_by:'.$sGroupBy.'" asynchronous="false" encoding="text/oql">'.$sQuery.'</itopblock>';
+				$sXML = '<itopblock BlockClass="DisplayBlock" type="open_flash_chart" parameters="chart_type:bars;chart_title:'.$sGroupByLabel.';group_by:'.$sGroupByExpr.';group_by_label:'.$sGroupByLabel.'" asynchronous="false" encoding="text/oql">'.$sQuery.'</itopblock>';
 				$sHtmlTitle = ''; // done in the itop block
 				$sHtmlTitle = ''; // done in the itop block
 				break;
 				break;
 			case 'pie':
 			case 'pie':
-				$sXML = '<itopblock BlockClass="DisplayBlock" type="open_flash_chart" parameters="chart_type:pie;chart_title:'.$sTitle.';group_by:'.$sGroupBy.'" asynchronous="false" encoding="text/oql">'.$sQuery.'</itopblock>';
+				$sXML = '<itopblock BlockClass="DisplayBlock" type="open_flash_chart" parameters="chart_type:pie;chart_title:'.$sGroupByLabel.';group_by:'.$sGroupByExpr.';group_by_label:'.$sGroupByLabel.'" asynchronous="false" encoding="text/oql">'.$sQuery.'</itopblock>';
 				$sHtmlTitle = ''; // done in the itop block
 				$sHtmlTitle = ''; // done in the itop block
 				break;
 				break;
 			case 'table':
 			case 'table':
 			default:
 			default:
 				$sHtmlTitle = htmlentities(Dict::S($sTitle), ENT_QUOTES, 'UTF-8'); // done in the itop block
 				$sHtmlTitle = htmlentities(Dict::S($sTitle), ENT_QUOTES, 'UTF-8'); // done in the itop block
-				$sXML = '<itopblock BlockClass="DisplayBlock" type="count" parameters="group_by:'.$sGroupBy.'" asynchronous="false" encoding="text/oql">'.$sQuery.'</itopblock>';
+				$sXML = '<itopblock BlockClass="DisplayBlock" type="count" parameters="group_by:'.$sGroupByExpr.';group_by_label:'.$sGroupByLabel.'" asynchronous="false" encoding="text/oql">'.$sQuery.'</itopblock>';
 				break;
 				break;
 			}
 			}
 	
 	
@@ -429,10 +472,11 @@ abstract class DashletGroupBy extends Dashlet
 
 
 			if ($oAttDef instanceof AttributeDateTime)
 			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' => "<a href=\"".utils::GetAbsoluteUrlAppRoot()."pages/UI.php?operation=search&dosearch=1&$sParams&filter=$sFilter\">$iCount</a>"
+			); // 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('<div style="text-align:center" class="dashlet-content">');
+
+		$oPage->add('<h1>'.$sHtmlTitle.'</h1>');
+		$oPage->p(Dict::Format('UI:Pagination:HeaderNoSelection', $iTotalCount));
+		$oPage->table($aAttribs, $aData);
+
+		$oPage->add('</div>');
+	}
+
+	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',
+		);
+	}
+}

+ 126 - 56
application/displayblock.class.inc.php

@@ -347,34 +347,51 @@ class DisplayBlock
 			case 'count':
 			case 'count':
 			if (isset($aExtraParams['group_by']))
 			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();
 				$aGroupBy = array();
 				$sLabels = 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();
 				$aData = array();
 				$oAppContext = new ApplicationContext();
 				$oAppContext = new ApplicationContext();
 				$sParams = $oAppContext->GetForLink();
 				$sParams = $oAppContext->GetForLink();
 				foreach($aGroupBy as $sValue => $iCount)
 				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],
 					$aData[] = array ( 'group' => $sLabels[$sValue],
-									  'value' => "<a href=\"".utils::GetAbsoluteUrlAppRoot()."pages/UI.php?operation=search&dosearch=1&$sParams&filter=$sFilter&$sGroupByField=".urlencode($sValue)."\">$iCount</a>"); // TO DO: add the context information
+									  'value' => "<a href=\"".utils::GetAbsoluteUrlAppRoot()."pages/UI.php?operation=search&dosearch=1&$sParams&filter=$sFilter\">$iCount</a>"); // TO DO: add the context information
 				}
 				}
 				$aAttribs =array(
 				$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+'))
 					'value' => array('label'=> Dict::S('UI:GroupBy:Count'), 'description' => Dict::S('UI:GroupBy:Count+'))
 				);
 				);
 				$sFormat = isset($aExtraParams['format']) ? $aExtraParams['format'] : 'UI:Pagination:HeaderNoSelection';
 				$sFormat = isset($aExtraParams['format']) ? $aExtraParams['format'] : 'UI:Pagination:HeaderNoSelection';
@@ -748,35 +765,60 @@ EOF
 			$sFilter = $this->m_oFilter->serialize();
 			$sFilter = $this->m_oFilter->serialize();
 			$sHtml .= "<div id=\"my_chart_$sId{$iChartCounter}\">If the chart does not display, <a href=\"http://get.adobe.com/flash/\" target=\"_blank\">install Flash</a></div>\n";
 			$sHtml .= "<div id=\"my_chart_$sId{$iChartCounter}\">If the chart does not display, <a href=\"http://get.adobe.com/flash/\" target=\"_blank\">install Flash</a></div>\n";
 			$oPage->add_script("function ofc_resize(left, width, top, height) { /* do nothing special */ }");
 			$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&params[group_by]=$sGroupBy{$sGroupByExpr}&params[group_by_label]={$aExtraParams['group_by_label']}&params[chart_type]=$sChartType&params[chart_title]=$sTitle&params[currentId]=$sId&id=$sId&filter=".$sFilter);
+			}
+			else
+			{
+				$sUrl = urlencode(utils::GetAbsoluteUrlAppRoot()."pages/ajax.render.php?operation=open_flash_chart&params[group_by]=$sGroupBy{$sGroupByExpr}&params[chart_type]=$sChartType&params[chart_title]=$sTitle&params[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\",
 			$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&params[group_by]=$sGroupBy{$sGroupByExpr}&params[chart_type]=$sChartType&params[chart_title]=$sTitle&params[currentId]=$sId&id=$sId&filter=".$sFilter)."\"}, {wmode: 'transparent'} );\n");
+				{\"data-file\":\"".$sUrl."\"}, {wmode: 'transparent'} );\n");
 			$iChartCounter++;
 			$iChartCounter++;
 			if (isset($aExtraParams['group_by']))
 			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 = array();
-				while($oObj = $this->m_oSet->Fetch())
+				$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)
 				{
 				{
-					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;
+					$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();
 				$aData = array();
 				$aLabels = array();
 				$aLabels = array();
 				$idx = 0;
 				$idx = 0;
 				$aURLs = array();
 				$aURLs = array();
 				foreach($aGroupBy as $sValue => $iValue)
 				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++;
 					$idx++;
 				}
 				}
 				$sURLList = '';
 				$sURLList = '';
@@ -810,21 +852,35 @@ EOF
 
 
 				if (isset($aExtraParams['group_by']))
 				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();
 					$aGroupBy = array();
-					while($oObj = $this->m_oSet->Fetch())
+					$sLabels = array();
+					$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;
+						$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();
 					$aData = array();
 					$aLabels = array();
 					$aLabels = array();
 					$maxValue = 0;
 					$maxValue = 0;
@@ -876,21 +932,35 @@ EOF
 				$oChartElement->set_colours( array('#FF8A00', '#909980', '#2C2B33', '#CCC08D', '#596664') );
 				$oChartElement->set_colours( array('#FF8A00', '#909980', '#2C2B33', '#CCC08D', '#596664') );
 				if (isset($aExtraParams['group_by']))
 				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 = array();
-					while($oObj = $this->m_oSet->Fetch())
+					$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)
 					{
 					{
-						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;
+						$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();
 					$aData = array();
 					foreach($aGroupBy as $sValue => $iValue)
 					foreach($aGroupBy as $sValue => $iValue)
 					{
 					{

+ 29 - 1
core/expression.class.inc.php

@@ -951,12 +951,14 @@ class QueryBuilderExpressions
 {
 {
 	protected $m_oConditionExpr;
 	protected $m_oConditionExpr;
 	protected $m_aSelectExpr;
 	protected $m_aSelectExpr;
+	protected $m_aGroupByExpr;
 	protected $m_aJoinFields;
 	protected $m_aJoinFields;
 
 
-	public function __construct($oCondition)
+	public function __construct($oCondition, $aGroupByExpr = null)
 	{
 	{
 		$this->m_oConditionExpr = $oCondition;
 		$this->m_oConditionExpr = $oCondition;
 		$this->m_aSelectExpr = array();
 		$this->m_aSelectExpr = array();
+		$this->m_aGroupByExpr = $aGroupByExpr;
 		$this->m_aJoinFields = array();
 		$this->m_aJoinFields = array();
 	}
 	}
 
 
@@ -965,6 +967,11 @@ class QueryBuilderExpressions
 		return $this->m_aSelectExpr;
 		return $this->m_aSelectExpr;
 	}
 	}
 
 
+	public function GetGroupBy()
+	{
+		return $this->m_aGroupByExpr;
+	}
+
 	public function GetCondition()
 	public function GetCondition()
 	{
 	{
 		return $this->m_oConditionExpr;
 		return $this->m_oConditionExpr;
@@ -998,6 +1005,13 @@ class QueryBuilderExpressions
 		{
 		{
 			$oExpr->GetUnresolvedFields($sAlias, $aUnresolved);
 			$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)
 		foreach($this->m_aJoinFields as $oExpression)
 		{
 		{
 			$oExpression->GetUnresolvedFields($sAlias, $aUnresolved);
 			$oExpression->GetUnresolvedFields($sAlias, $aUnresolved);
@@ -1011,6 +1025,13 @@ class QueryBuilderExpressions
 		{
 		{
 			$this->m_aSelectExpr[$sColAlias] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
 			$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)
 		foreach($this->m_aJoinFields as $index => $oExpression)
 		{
 		{
 			$this->m_aJoinFields[$index] = $oExpression->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
 			$this->m_aJoinFields[$index] = $oExpression->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
@@ -1024,6 +1045,13 @@ class QueryBuilderExpressions
 		{
 		{
 			$this->m_aSelectExpr[$sColAlias] = $oExpr->RenameParam($sOldName, $sNewName);
 			$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)
 		foreach($this->m_aJoinFields as $index => $oExpression)
 		{
 		{
 			$this->m_aJoinFields[$index] = $oExpression->RenameParam($sOldName, $sNewName);
 			$this->m_aJoinFields[$index] = $oExpression->RenameParam($sOldName, $sNewName);

+ 99 - 61
core/metamodel.class.php

@@ -1933,8 +1933,82 @@ abstract class MetaModel
 		return $aScalarArgs;
 		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)
 	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
 		// Hide objects that are not visible to the current user
 		//
 		//
 		if (!$oFilter->IsAllDataAllowed() && !$oFilter->IsDataFiltered())
 		if (!$oFilter->IsAllDataAllowed() && !$oFilter->IsDataFiltered())
@@ -1984,6 +2058,13 @@ abstract class MetaModel
 					$sRawId = $sOqlQuery.'|'.implode(',', array_keys($aAttributes));
 					$sRawId = $sOqlQuery.'|'.implode(',', array_keys($aAttributes));
 				}
 				}
 			}
 			}
+			if (!is_null($aGroupByExpr))
+			{
+				foreach($aGroupByExpr as $sAlias => $oExpr)
+				{
+					$sRawId = 'g:'.$sAlias.'!'.$oExpr->Render();
+				}
+			}
 			$sOqlId = md5($sRawId);
 			$sOqlId = md5($sRawId);
 		}
 		}
 		else
 		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 (!isset($oSelect))
 		{
 		{
-			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))
+			$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)
 			{
 			{
-				$oAttDef = self::GetAttributeDef($oFilter->GetClass(), $sFieldAlias);
-				foreach($oAttDef->GetOrderBySQLExpressions($sFirstClassAlias) as $sSQLExpression)
-				{
-					$aOrderSpec[$sSQLExpression] = $bAscending;
-				}
+				$aCols = $oBuild->m_oQBExpressions->GetGroupBy();
+				$oSelect->SetGroupBy($aCols);
+				$oSelect->SetSelect($aCols);
 			}
 			}
 			else
 			else
 			{
 			{
-				$aOrderSpec['`'.$sFirstClassAlias.$sFieldAlias.'`'] = $bAscending;
+				$oSelect->SetSelect($oBuild->m_oQBExpressions->GetSelect());
 			}
 			}
 
 
-			// 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);
-
-			$oKPI = new ExecutionKPI();
-			$oSelect = self::MakeQuery($oBuild, $oFilter, $aAttToLoad, array(), true /* main query */);
-			$oSelect->SetSourceOQL($sOqlQuery);
 			$oKPI->ComputeStats('MakeQuery (select)', $sOqlQuery);
 			$oKPI->ComputeStats('MakeQuery (select)', $sOqlQuery);
 
 
 			if (self::$m_bQueryCacheEnabled)
 			if (self::$m_bQueryCacheEnabled)
@@ -2101,22 +2159,6 @@ abstract class MetaModel
 			$oSelect->AddInnerJoin($oSelectExt, 'id', $aExtendedDataSpec['join_key'] /*, $sTableAlias*/);
 			$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 "<p>MakeQuery: $sRes</p>";
-		}
-		catch (MissingQueryArgument $e)
-		{
-			// Add some information...
-			$e->addInfo('OQL', $sOqlQuery);
-			throw $e;
-		}
-
 		if (self::$m_bTraceQueries)
 		if (self::$m_bTraceQueries)
 		{
 		{
 			$sQueryId = md5($sRes);
 			$sQueryId = md5($sRes);
@@ -2140,7 +2182,7 @@ abstract class MetaModel
 			}
 			}
 		}
 		}
 
 
-		return $sRes;
+		return $oSelect;
 	}
 	}
 
 
 	public static function ShowQueryTrace()
 	public static function ShowQueryTrace()
@@ -2205,6 +2247,8 @@ abstract class MetaModel
 		$aModifierProperties = self::MakeModifierProperties($oFilter);
 		$aModifierProperties = self::MakeModifierProperties($oFilter);
 		$oBuild = new QueryBuilderContext($oFilter, $aModifierProperties);
 		$oBuild = new QueryBuilderContext($oFilter, $aModifierProperties);
 		$oSelect = self::MakeQuery($oBuild, $oFilter, null, array(), true /* main query */);
 		$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());
 		$aScalarArgs = array_merge(self::PrepareQueryArguments($aArgs), $oFilter->GetInternalParams());
 		return $oSelect->RenderDelete($aScalarArgs);
 		return $oSelect->RenderDelete($aScalarArgs);
 	}
 	}
@@ -2215,11 +2259,13 @@ abstract class MetaModel
 		$aModifierProperties = self::MakeModifierProperties($oFilter);
 		$aModifierProperties = self::MakeModifierProperties($oFilter);
 		$oBuild = new QueryBuilderContext($oFilter, $aModifierProperties);
 		$oBuild = new QueryBuilderContext($oFilter, $aModifierProperties);
 		$oSelect = self::MakeQuery($oBuild, $oFilter, null, $aValues, true /* main query */);
 		$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());
 		$aScalarArgs = array_merge(self::PrepareQueryArguments($aArgs), $oFilter->GetInternalParams());
 		return $oSelect->RenderUpdate($aScalarArgs);
 		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
 		// 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)
 		// -> 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
 		// That's all... cross fingers and we'll get some working query
 
 
 		//MyHelpers::var_dump_html($oSelectBase, true);
 		//MyHelpers::var_dump_html($oSelectBase, true);

+ 2 - 2
core/querybuildercontext.class.inc.php

@@ -32,10 +32,10 @@ class QueryBuilderContext
 
 
 	public $m_oQBExpressions;
 	public $m_oQBExpressions;
 
 
-	public function __construct($oFilter, $aModifierProperties)
+	public function __construct($oFilter, $aModifierProperties, $aGroupByExpr = null)
 	{
 	{
 		$this->m_oRootFilter = $oFilter;
 		$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_aClassAliases = $oFilter->GetJoinedClasses();
 		$this->m_aTableAliases = array();
 		$this->m_aTableAliases = array();

+ 53 - 8
core/sqlquery.class.inc.php

@@ -41,6 +41,7 @@ class SQLQuery
 	private $m_sTable = '';
 	private $m_sTable = '';
 	private $m_sTableAlias = '';
 	private $m_sTableAlias = '';
 	private $m_aFields = array();
 	private $m_aFields = array();
+	private $m_aGroupBy = array();
 	private $m_oConditionExpr = null;
 	private $m_oConditionExpr = null;
 	private $m_bToDelete = true; // The current table must be listed for deletion ?
 	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
 	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_sTable = $sTable;
 		$this->m_sTableAlias = $sTableAlias;
 		$this->m_sTableAlias = $sTableAlias;
 		$this->m_aFields = $aFields;
 		$this->m_aFields = $aFields;
+		$this->m_aGroupBy = null;
 		$this->m_oConditionExpr = null;
 		$this->m_oConditionExpr = null;
 		$this->m_bToDelete = $bToDelete;
 		$this->m_bToDelete = $bToDelete;
 		$this->m_aValues = $aValues;
 		$this->m_aValues = $aValues;
@@ -125,11 +127,12 @@ class SQLQuery
 		}
 		}
 		$aFrom = array();
 		$aFrom = array();
 		$aFields = array();
 		$aFields = array();
+		$aGroupBy = array();
 		$oCondition = null;
 		$oCondition = null;
 		$aDelTables = array();
 		$aDelTables = array();
 		$aSetValues = array();
 		$aSetValues = array();
 		$aSelectedIdFields = array();
 		$aSelectedIdFields = array();
-		$this->privRender($aFrom, $aFields, $oCondition, $aDelTables, $aSetValues, $aSelectedIdFields);
+		$this->privRender($aFrom, $aFields, $aGroupBy, $oCondition, $aDelTables, $aSetValues, $aSelectedIdFields);
 		echo "From ...<br/>\n";
 		echo "From ...<br/>\n";
 		echo "<pre style=\"font-size: smaller;\">\n";
 		echo "<pre style=\"font-size: smaller;\">\n";
 		print_r($aFrom);
 		print_r($aFrom);
@@ -141,6 +144,11 @@ class SQLQuery
 		$this->m_aFields = $aExpressions;
 		$this->m_aFields = $aExpressions;
 	}
 	}
 
 
+	public function SetGroupBy($aExpressions)
+	{
+		$this->m_aGroupBy = $aExpressions;
+	}
+
 	public function SetCondition($oConditionExpr)
 	public function SetCondition($oConditionExpr)
 	{
 	{
 		$this->m_oConditionExpr = $oConditionExpr;
 		$this->m_oConditionExpr = $oConditionExpr;
@@ -235,11 +243,12 @@ class SQLQuery
 		// The goal will be to complete the list as we build the Joins
 		// The goal will be to complete the list as we build the Joins
 		$aFrom = array();
 		$aFrom = array();
 		$aFields = array();
 		$aFields = array();
+		$aGroupBy = arry();
 		$oCondition = null;
 		$oCondition = null;
 		$aDelTables = array();
 		$aDelTables = array();
 		$aSetValues = array();
 		$aSetValues = array();
 		$aSelectedIdFields = 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 ...
 		// 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
 		// The goal will be to complete the list as we build the Joins
 		$aFrom = array();
 		$aFrom = array();
 		$aFields = array();
 		$aFields = array();
+		$aGroupBy = array();
 		$oCondition = null;
 		$oCondition = null;
 		$aDelTables = array();
 		$aDelTables = array();
 		$aSetValues = array();
 		$aSetValues = array();
 		$aSelectedIdFields = array();
 		$aSelectedIdFields = array();
-		$this->privRender($aFrom, $aFields, $oCondition, $aDelTables, $aSetValues, $aSelectedIdFields);
+		$this->privRender($aFrom, $aFields, $aGroupBy, $oCondition, $aDelTables, $aSetValues, $aSelectedIdFields);
 		$sFrom   = self::ClauseFrom($aFrom);
 		$sFrom   = self::ClauseFrom($aFrom);
 		$sValues = self::ClauseValues($aSetValues);
 		$sValues = self::ClauseValues($aSetValues);
 		$sWhere  = self::ClauseWhere($oCondition, $aArgs);
 		$sWhere  = self::ClauseWhere($oCondition, $aArgs);
@@ -287,11 +297,12 @@ class SQLQuery
 		// The goal will be to complete the lists as we build the Joins
 		// The goal will be to complete the lists as we build the Joins
 		$aFrom = array();
 		$aFrom = array();
 		$aFields = array();
 		$aFields = array();
+		$aGroupBy = array();
 		$oCondition = null;
 		$oCondition = null;
 		$aDelTables = array();
 		$aDelTables = array();
 		$aSetValues = array();
 		$aSetValues = array();
 		$aSelectedIdFields = array();
 		$aSelectedIdFields = array();
-		$this->privRender($aFrom, $aFields, $oCondition, $aDelTables, $aSetValues, $aSelectedIdFields);
+		$this->privRender($aFrom, $aFields, $aGroupBy, $oCondition, $aDelTables, $aSetValues, $aSelectedIdFields);
 
 
 		$sFrom   = self::ClauseFrom($aFrom);
 		$sFrom   = self::ClauseFrom($aFrom);
 		$sWhere  = self::ClauseWhere($oCondition, $aArgs);
 		$sWhere  = self::ClauseWhere($oCondition, $aArgs);
@@ -328,6 +339,27 @@ class SQLQuery
 		return $sSQL;
 		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)
 	private static function ClauseSelect($aFields)
 	{
 	{
 		$aSelect = array();
 		$aSelect = array();
@@ -339,6 +371,12 @@ class SQLQuery
 		return $sSelect;
 		return $sSelect;
 	}
 	}
 
 
+	private static function ClauseGroupBy($aGroupBy)
+	{
+		$sRes = implode(', ', $aGroupBy);
+		return $sRes;
+	}
+
 	private static function ClauseDelete($aDelTableAliases)
 	private static function ClauseDelete($aDelTableAliases)
 	{
 	{
 		$aDelTables = array();
 		$aDelTables = array();
@@ -415,14 +453,14 @@ class SQLQuery
 	}
 	}
 
 
 	// Purpose: prepare the query data, once for all
 	// 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;
 		$oCondition = $this->m_oConditionExpr;
 		return $sTableAlias; 
 		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);
 		$aActualTableFields = CMDBSource::GetTableFieldsList($this->m_sTable);
 
 
@@ -506,6 +544,13 @@ class SQLQuery
 		{
 		{
 			$aFields["`$sAlias`"] = $oExpression->Render();
 			$aFields["`$sAlias`"] = $oExpression->Render();
 		}
 		}
+		if ($this->m_aGroupBy)
+		{
+			foreach($this->m_aGroupBy as $sAlias => $oExpression)
+			{
+				$aGroupBy["`$sAlias`"] = $oExpression->Render();
+			}
+		}
 		if ($this->m_bToDelete)
 		if ($this->m_bToDelete)
 		{
 		{
 			$aDelTables[] = "`{$this->m_sTableAlias}`";
 			$aDelTables[] = "`{$this->m_sTableAlias}`";
@@ -528,7 +573,7 @@ class SQLQuery
 		{
 		{
 			$oRightSelect = $aJoinData["select"];
 			$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;
 		$aFrom[$this->m_sTableAlias]['subfrom'] = $aTempFrom;