Pārlūkot izejas kodu

#942 OQL now supporting unions. Unions support polymorphism and can be used anywhere in the application.

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@3631 a333f486-631f-4898-b8df-5754b55c2be0
romainq 10 gadi atpakaļ
vecāks
revīzija
5ed735e8d0
40 mainītis faili ar 3757 papildinājumiem un 2851 dzēšanām
  1. 4 4
      application/ajaxwebpage.class.inc.php
  2. 4 4
      application/clipage.class.inc.php
  3. 5 3
      application/cmdbabstract.class.inc.php
  4. 4 4
      application/csvpage.class.inc.php
  5. 10 10
      application/displayblock.class.inc.php
  6. 3 3
      application/itopwebpage.class.inc.php
  7. 3 3
      application/ui.linksdirectwidget.class.inc.php
  8. 3 3
      application/ui.linkswidget.class.inc.php
  9. 3 3
      application/utils.inc.php
  10. 4 4
      application/webpage.class.inc.php
  11. 4 4
      application/xmlpage.class.inc.php
  12. 2 2
      core/bulkexport.class.inc.php
  13. 7 5
      core/cmdbobject.class.inc.php
  14. 1 2
      core/dbobject.class.php
  15. 701 414
      core/dbobjectsearch.class.php
  16. 9 9
      core/dbobjectset.class.php
  17. 724 0
      core/dbsearch.class.php
  18. 432 0
      core/dbunionsearch.class.php
  19. 69 6
      core/expression.class.inc.php
  20. 53 1085
      core/metamodel.class.php
  21. 79 73
      core/oql/oql-lexer.php
  22. 6 2
      core/oql/oql-lexer.plex
  23. 544 529
      core/oql/oql-parser.php
  24. 8 0
      core/oql/oql-parser.y
  25. 4 4
      core/oql/oqlinterpreter.class.inc.php
  26. 148 19
      core/oql/oqlquery.class.inc.php
  27. 18 5
      core/querybuildercontext.class.inc.php
  28. 587 0
      core/sqlobjectquery.class.inc.php
  29. 42 554
      core/sqlquery.class.inc.php
  30. 166 0
      core/sqlunionquery.class.inc.php
  31. 4 4
      core/userrights.class.inc.php
  32. 5 5
      core/valuesetdef.class.inc.php
  33. 3 3
      pages/UI.php
  34. 4 5
      pages/audit.php
  35. 58 56
      setup/runtimeenv.class.inc.php
  36. 1 1
      test/benchmark.php
  37. 4 4
      test/replay_query_log.php
  38. 7 5
      test/test.class.inc.php
  39. 2 0
      test/test.php
  40. 22 14
      test/testlist.inc.php

+ 4 - 4
application/ajaxwebpage.class.inc.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-// Copyright (C) 2010-2014 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //
 //   This file is part of iTop.
 //   This file is part of iTop.
 //
 //
@@ -20,7 +20,7 @@
  * Simple web page with no includes, header or fancy formatting, useful to
  * Simple web page with no includes, header or fancy formatting, useful to
  * generate HTML fragments when called by an AJAX method
  * generate HTML fragments when called by an AJAX method
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -278,9 +278,9 @@ EOF
         	echo self::FilterXSS($s_captured_output);
         	echo self::FilterXSS($s_captured_output);
         }
         }
 
 
-        if (class_exists('MetaModel'))
+        if (class_exists('DBSearch'))
         {
         {
-            MetaModel::RecordQueryTrace();
+            DBSearch::RecordQueryTrace();
         }
         }
     }
     }
 
 

+ 4 - 4
application/clipage.class.inc.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //
 //   This file is part of iTop.
 //   This file is part of iTop.
 //
 //
@@ -21,7 +21,7 @@
  * CLI page 
  * CLI page 
  * The page adds the content-type text/XML and the encoding into the headers
  * The page adds the content-type text/XML and the encoding into the headers
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -35,9 +35,9 @@ class CLIPage implements Page
 
 
     public function output()
     public function output()
     {
     {
-        if (class_exists('MetaModel'))
+        if (class_exists('DBSearch'))
         {
         {
-            MetaModel::RecordQueryTrace();
+            DBSearch::RecordQueryTrace();
         }
         }
         if (class_exists('ExecutionKPI'))
         if (class_exists('ExecutionKPI'))
         {
         {

+ 5 - 3
application/cmdbabstract.class.inc.php

@@ -21,7 +21,7 @@
  * Abstract class that implements some common and useful methods for displaying
  * Abstract class that implements some common and useful methods for displaying
  * the objects
  * the objects
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -1503,10 +1503,12 @@ EOF
 		$sHtml .= "<h2>".Dict::Format('UI:SearchFor_Class_Objects', $sClassesCombo)."</h2>\n";
 		$sHtml .= "<h2>".Dict::Format('UI:SearchFor_Class_Objects', $sClassesCombo)."</h2>\n";
 		$index = 0;
 		$index = 0;
 		$sHtml .= "<p>\n";
 		$sHtml .= "<p>\n";
-		$aFilterCriteria = $oSet->GetFilter()->GetCriteria();
+		//$aFilterCriteria = $oSet->GetFilter()->GetCriteria();
 		$aMapCriteria = array();
 		$aMapCriteria = array();
 		// Todo: Investigate... The search criteria is an expression, i.e. a tree!
 		// Todo: Investigate... The search criteria is an expression, i.e. a tree!
 		//     I wonder if that code could work... cleanup required/recommended
 		//     I wonder if that code could work... cleanup required/recommended
+		// Temporary fix (unions do fail with this)
+		$aFilterCriteria = array();
 		foreach($aFilterCriteria as $aCriteria)
 		foreach($aFilterCriteria as $aCriteria)
 		{
 		{
 			$aMapCriteria[$aCriteria['filtercode']][] = array('value' => $aCriteria['value'], 'opcode' => $aCriteria['opcode']);
 			$aMapCriteria[$aCriteria['filtercode']][] = array('value' => $aCriteria['value'], 'opcode' => $aCriteria['opcode']);
@@ -3196,7 +3198,7 @@ EOF
 		return $res;
 		return $res;
 	}
 	}
 
 
-	protected static function BulkUpdateTracked_Internal(DBObjectSearch $oFilter, array $aValues)
+	protected static function BulkUpdateTracked_Internal(DBSearch $oFilter, array $aValues)
 	{
 	{
 		// Todo - invoke the extension
 		// Todo - invoke the extension
 		return parent::BulkUpdateTracked_Internal($oFilter, $aValues);
 		return parent::BulkUpdateTracked_Internal($oFilter, $aValues);

+ 4 - 4
application/csvpage.class.inc.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-// Copyright (C) 2010-2014 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //
 //   This file is part of iTop.
 //   This file is part of iTop.
 //
 //
@@ -21,7 +21,7 @@
  * Simple web page with no includes or fancy formatting, useful to generateXML documents
  * Simple web page with no includes or fancy formatting, useful to generateXML documents
  * The page adds the content-type text/XML and the encoding into the headers
  * The page adds the content-type text/XML and the encoding into the headers
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -51,9 +51,9 @@ class CSVPage extends WebPage
         echo trim($this->s_content);
         echo trim($this->s_content);
         echo "\n";
         echo "\n";
 
 
-        if (class_exists('MetaModel'))
+        if (class_exists('DBSearch'))
         {
         {
-            MetaModel::RecordQueryTrace();
+            DBSearch::RecordQueryTrace();
         }
         }
         if (class_exists('ExecutionKPI'))
         if (class_exists('ExecutionKPI'))
         {
         {

+ 10 - 10
application/displayblock.class.inc.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-// Copyright (C) 2010-2013 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //
 //   This file is part of iTop.
 //   This file is part of iTop.
 //
 //
@@ -19,7 +19,7 @@
 /**
 /**
  * DisplayBlock and derived class
  * DisplayBlock and derived class
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -50,7 +50,7 @@ class DisplayBlock
 	protected $m_aParams;
 	protected $m_aParams;
 	protected $m_oSet;
 	protected $m_oSet;
 	
 	
-	public function __construct(DBObjectSearch $oFilter, $sStyle = 'list', $bAsynchronous = false, $aParams = array(), $oSet = null)
+	public function __construct(DBSearch $oFilter, $sStyle = 'list', $bAsynchronous = false, $aParams = array(), $oSet = null)
 	{
 	{
 		$this->m_oFilter = $oFilter->DeepClone();
 		$this->m_oFilter = $oFilter->DeepClone();
 		$this->m_aConditions = array();
 		$this->m_aConditions = array();
@@ -407,7 +407,7 @@ class DisplayBlock
 
 
 				$aGroupBy = array();
 				$aGroupBy = array();
 				$aGroupBy['grouped_by_1'] = $oGroupByExp;
 				$aGroupBy['grouped_by_1'] = $oGroupByExp;
-				$sSql = MetaModel::MakeGroupByQuery($this->m_oFilter, $aQueryParams, $aGroupBy, true);
+				$sSql = $this->m_oFilter->MakeGroupByQuery($aQueryParams, $aGroupBy, true);
 				$aRes = CMDBSource::QueryToArray($sSql);
 				$aRes = CMDBSource::QueryToArray($sSql);
 
 
 				$aGroupBy = array();
 				$aGroupBy = array();
@@ -911,7 +911,7 @@ EOF
 
 
 				$aGroupBy = array();
 				$aGroupBy = array();
 				$aGroupBy['grouped_by_1'] = $oGroupByExp;
 				$aGroupBy['grouped_by_1'] = $oGroupByExp;
-				$sSql = MetaModel::MakeGroupByQuery($this->m_oFilter, $aQueryParams, $aGroupBy, true);
+				$sSql = $this->m_oFilter->MakeGroupByQuery($aQueryParams, $aGroupBy, true);
 				$aRes = CMDBSource::QueryToArray($sSql);
 				$aRes = CMDBSource::QueryToArray($sSql);
 
 
 				$aGroupBy = array();
 				$aGroupBy = array();
@@ -986,7 +986,7 @@ EOF
 	
 	
 					$aGroupBy = array();
 					$aGroupBy = array();
 					$aGroupBy['grouped_by_1'] = $oGroupByExp;
 					$aGroupBy['grouped_by_1'] = $oGroupByExp;
-					$sSql = MetaModel::MakeGroupByQuery($this->m_oFilter, $aQueryParams, $aGroupBy, true);
+					$sSql = $this->m_oFilter->MakeGroupByQuery($aQueryParams, $aGroupBy, true);
 					$aRes = CMDBSource::QueryToArray($sSql);
 					$aRes = CMDBSource::QueryToArray($sSql);
 	
 	
 					$aGroupBy = array();
 					$aGroupBy = array();
@@ -1068,7 +1068,7 @@ EOF
 					$aGroupBy = array();
 					$aGroupBy = array();
 					$aGroupBy['grouped_by_1'] = $oGroupByExp;
 					$aGroupBy['grouped_by_1'] = $oGroupByExp;
 
 
-					$sSql = MetaModel::MakeGroupByQuery($this->m_oFilter, $aQueryParams, $aGroupBy, true);
+					$sSql = $this->m_oFilter->MakeGroupByQuery($aQueryParams, $aGroupBy, true);
 					$aRes = CMDBSource::QueryToArray($sSql);
 					$aRes = CMDBSource::QueryToArray($sSql);
 	
 	
 					$aGroupBy = array();
 					$aGroupBy = array();
@@ -1128,7 +1128,7 @@ EOF
 	}
 	}
 	
 	
 	/**
 	/**
-	 * Add a condition (restriction) to the current DBObjectSearch on which the display block is based
+	 * Add a condition (restriction) to the current DBSearch on which the display block is based
 	 * taking into account the hierarchical keys for which the condition is based on the 'below' operator
 	 * taking into account the hierarchical keys for which the condition is based on the 'below' operator
 	 */
 	 */
 	protected function AddCondition($sFilterCode, $condition, $sOpCode = null)
 	protected function AddCondition($sFilterCode, $condition, $sOpCode = null)
@@ -1216,7 +1216,7 @@ class HistoryBlock extends DisplayBlock
 	protected $iLimitCount;
 	protected $iLimitCount;
 	protected $iLimitStart;
 	protected $iLimitStart;
 	
 	
-	public function __construct(DBObjectSearch $oFilter, $sStyle = 'list', $bAsynchronous = false, $aParams = array(), $oSet = null)
+	public function __construct(DBSearch $oFilter, $sStyle = 'list', $bAsynchronous = false, $aParams = array(), $oSet = null)
 	{
 	{
 		parent::__construct($oFilter, $sStyle, $bAsynchronous, $aParams, $oSet);
 		parent::__construct($oFilter, $sStyle, $bAsynchronous, $aParams, $oSet);
 		$this->iLimitStart = 0;
 		$this->iLimitStart = 0;
@@ -1543,7 +1543,7 @@ class MenuBlock extends DisplayBlock
 					{
 					{
 						$aQueryParams = $aExtraParams['query_params'];
 						$aQueryParams = $aExtraParams['query_params'];
 					}
 					}
-					$sSql = MetaModel::MakeGroupByQuery($this->m_oFilter, $aQueryParams, $aGroupBy);
+					$sSql = $this->m_oFilter->MakeGroupByQuery($aQueryParams, $aGroupBy);
 					$aRes = CMDBSource::QueryToArray($sSql);
 					$aRes = CMDBSource::QueryToArray($sSql);
 					if (count($aRes) == 1)
 					if (count($aRes) == 1)
 					{
 					{

+ 3 - 3
application/itopwebpage.class.inc.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-// Copyright (C) 2010-2014 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //
 //   This file is part of iTop.
 //   This file is part of iTop.
 //
 //
@@ -20,7 +20,7 @@
 /**
 /**
  * Class iTopWebPage
  * Class iTopWebPage
  *
  *
- * @copyright   Copyright (C) 2010-2013 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -933,7 +933,7 @@ EOF
 				$oMPDF->Output($sOutputName, 'I');
 				$oMPDF->Output($sOutputName, 'I');
 			}
 			}
 		}
 		}
-		MetaModel::RecordQueryTrace();
+		DBSearch::RecordQueryTrace();
 		ExecutionKPI::ReportStats();
 		ExecutionKPI::ReportStats();
 	}
 	}
 
 

+ 3 - 3
application/ui.linksdirectwidget.class.inc.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //
 //   This file is part of iTop.
 //   This file is part of iTop.
 //
 //
@@ -19,7 +19,7 @@
 /**
 /**
  * Class UILinksWidgetDirect
  * Class UILinksWidgetDirect
  *  
  *  
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */ 
  */ 
 
 
@@ -382,7 +382,7 @@ class UILinksWidgetDirect
 	/**
 	/**
 	 * Initializes the default search parameters based on 1) a 'current' object and 2) the silos defined by the context
 	 * Initializes the default search parameters based on 1) a 'current' object and 2) the silos defined by the context
 	 * @param DBObject $oSourceObj
 	 * @param DBObject $oSourceObj
-	 * @param DBObjectSearch $oSearch
+	 * @param DBSearch $oSearch
 	 */
 	 */
 	protected function SetSearchDefaultFromContext($oSourceObj, &$oSearch)
 	protected function SetSearchDefaultFromContext($oSourceObj, &$oSearch)
 	{
 	{

+ 3 - 3
application/ui.linkswidget.class.inc.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //
 //   This file is part of iTop.
 //   This file is part of iTop.
 //
 //
@@ -20,7 +20,7 @@
 /**
 /**
  * Class UILinksWidget
  * Class UILinksWidget
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -452,7 +452,7 @@ EOF
 	/**
 	/**
 	 * Initializes the default search parameters based on 1) a 'current' object and 2) the silos defined by the context
 	 * Initializes the default search parameters based on 1) a 'current' object and 2) the silos defined by the context
 	 * @param DBObject $oSourceObj
 	 * @param DBObject $oSourceObj
-	 * @param DBObjectSearch $oSearch
+	 * @param DBSearch $oSearch
 	 */
 	 */
 	protected function SetSearchDefaultFromContext($oSourceObj, &$oSearch)
 	protected function SetSearchDefaultFromContext($oSourceObj, &$oSearch)
 	{
 	{

+ 3 - 3
application/utils.inc.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-// Copyright (C) 2010-2013 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //
 //   This file is part of iTop.
 //   This file is part of iTop.
 //
 //
@@ -20,7 +20,7 @@
 /**
 /**
  * Static class utils
  * Static class utils
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -334,7 +334,7 @@ class utils
 	
 	
 	/**
 	/**
 	 * Interprets the results posted by a normal or paginated list (in multiple selection mode)
 	 * Interprets the results posted by a normal or paginated list (in multiple selection mode)
-	 * @param $oFullSetFilter DBObjectSearch The criteria defining the whole sets of objects being selected
+	 * @param $oFullSetFilter DBSearch The criteria defining the whole sets of objects being selected
 	 * @return Array An arry of object IDs corresponding to the objects selected in the set
 	 * @return Array An arry of object IDs corresponding to the objects selected in the set
 	 */	
 	 */	
 	public static function ReadMultipleSelection($oFullSetFilter)
 	public static function ReadMultipleSelection($oFullSetFilter)

+ 4 - 4
application/webpage.class.inc.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-// Copyright (C) 2010-2014 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //
 //   This file is part of iTop.
 //   This file is part of iTop.
 //
 //
@@ -20,7 +20,7 @@
 /**
 /**
  * Class WebPage
  * Class WebPage
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -552,9 +552,9 @@ class WebPage implements Page
         echo "</body>\n";
         echo "</body>\n";
         echo "</html>\n";
         echo "</html>\n";
 
 
-        if (class_exists('MetaModel'))
+        if (class_exists('DBSearch'))
         {
         {
-            MetaModel::RecordQueryTrace();
+            DBSearch::RecordQueryTrace();
         }
         }
         if (class_exists('ExecutionKPI'))
         if (class_exists('ExecutionKPI'))
         {
         {

+ 4 - 4
application/xmlpage.class.inc.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-// Copyright (C) 2010-2014 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //
 //   This file is part of iTop.
 //   This file is part of iTop.
 //
 //
@@ -20,7 +20,7 @@
 /**
 /**
  * Class XMLPage
  * Class XMLPage
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -62,9 +62,9 @@ class XMLPage extends WebPage
 			}
 			}
 			echo $this->s_content;
 			echo $this->s_content;
 		}
 		}
-		if (class_exists('MetaModel'))
+		if (class_exists('DBSearch'))
 		{
 		{
-			MetaModel::RecordQueryTrace();
+			DBSearch::RecordQueryTrace();
 		}
 		}
 	}
 	}
 
 

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

@@ -147,7 +147,7 @@ abstract class BulkExport
 	/**
 	/**
 	 * Find the first class capable of exporting the data in the given format
 	 * Find the first class capable of exporting the data in the given format
 	 * @param string $sFormat The lowercase format (e.g. html, csv, spreadsheet, xlsx, xml, json, pdf...)
 	 * @param string $sFormat The lowercase format (e.g. html, csv, spreadsheet, xlsx, xml, json, pdf...)
-	 * @param DBObjectSearch $oSearch The search/filter defining the set of objects to export or null when listing the supported formats
+	 * @param DBSearch $oSearch The search/filter defining the set of objects to export or null when listing the supported formats
 	 * @return iBulkExport|NULL
 	 * @return iBulkExport|NULL
 	 */
 	 */
 	static public function FindExporter($sFormatCode, $oSearch = null)
 	static public function FindExporter($sFormatCode, $oSearch = null)
@@ -251,7 +251,7 @@ abstract class BulkExport
 	 * (non-PHPdoc)
 	 * (non-PHPdoc)
 	 * @see iBulkExport::SetObjectList()
 	 * @see iBulkExport::SetObjectList()
 	 */
 	 */
-	public function SetObjectList(DBObjectSearch $oSearch)
+	public function SetObjectList(DBSearch $oSearch)
 	{
 	{
 		$this->oSearch = $oSearch;
 		$this->oSearch = $oSearch;
 	}
 	}

+ 7 - 5
core/cmdbobject.class.inc.php

@@ -20,7 +20,7 @@
 /**
 /**
  * Class cmdbObject
  * Class cmdbObject
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -50,6 +50,8 @@ require_once('expression.class.inc.php');
 
 
 require_once('cmdbsource.class.inc.php');
 require_once('cmdbsource.class.inc.php');
 require_once('sqlquery.class.inc.php');
 require_once('sqlquery.class.inc.php');
+require_once('sqlobjectquery.class.inc.php');
+require_once('sqlunionquery.class.inc.php');
 require_once('oql/oqlquery.class.inc.php');
 require_once('oql/oqlquery.class.inc.php');
 require_once('oql/oqlexception.class.inc.php');
 require_once('oql/oqlexception.class.inc.php');
 require_once('oql/oql-parser.php');
 require_once('oql/oql-parser.php');
@@ -57,7 +59,7 @@ require_once('oql/oql-lexer.php');
 require_once('oql/oqlinterpreter.class.inc.php');
 require_once('oql/oqlinterpreter.class.inc.php');
 
 
 require_once('dbobject.class.php');
 require_once('dbobject.class.php');
-require_once('dbobjectsearch.class.php');
+require_once('dbsearch.class.php');
 require_once('dbobjectset.class.php');
 require_once('dbobjectset.class.php');
 
 
 require_once('backgroundprocess.inc.php');
 require_once('backgroundprocess.inc.php');
@@ -522,18 +524,18 @@ abstract class CMDBObject extends DBObject
 		return $ret;
 		return $ret;
 	}
 	}
 
 
-	public static function BulkUpdate(DBObjectSearch $oFilter, array $aValues)
+	public static function BulkUpdate(DBSearch $oFilter, array $aValues)
 	{
 	{
 		return $this->BulkUpdateTracked_Internal($oFilter, $aValues);
 		return $this->BulkUpdateTracked_Internal($oFilter, $aValues);
 	}
 	}
 
 
-	public static function BulkUpdateTracked(CMDBChange $oChange, DBObjectSearch $oFilter, array $aValues)
+	public static function BulkUpdateTracked(CMDBChange $oChange, DBSearch $oFilter, array $aValues)
 	{
 	{
 		self::SetCurrentChange($oChange);
 		self::SetCurrentChange($oChange);
 		$this->BulkUpdateTracked_Internal($oFilter, $aValues);
 		$this->BulkUpdateTracked_Internal($oFilter, $aValues);
 	}
 	}
 
 
-	protected static function BulkUpdateTracked_Internal(DBObjectSearch $oFilter, array $aValues)
+	protected static function BulkUpdateTracked_Internal(DBSearch $oFilter, array $aValues)
 	{
 	{
 		// $aValues is an array of $sAttCode => $value
 		// $aValues is an array of $sAttCode => $value
 
 

+ 1 - 2
core/dbobject.class.php

@@ -1862,7 +1862,7 @@ abstract class DBObject implements iDisplay
 					$oFilter = new DBObjectSearch(get_class($this));
 					$oFilter = new DBObjectSearch(get_class($this));
 					$oFilter->AddCondition('id', $this->m_iKey, '=');
 					$oFilter->AddCondition('id', $this->m_iKey, '=');
 			
 			
-					$sSQL = MetaModel::MakeUpdateQuery($oFilter, $aChanges);
+					$sSQL = $oFilter->MakeUpdateQuery($aChanges);
 					CMDBSource::Query($sSQL);
 					CMDBSource::Query($sSQL);
 				}
 				}
 			}
 			}
@@ -2627,7 +2627,6 @@ abstract class DBObject implements iDisplay
 		}
 		}
 		foreach (MetaModel::EnumRelationQueries(get_class($this), $sHackedRelCode, $bDown) as $sDummy => $aQueryInfo)
 		foreach (MetaModel::EnumRelationQueries(get_class($this), $sHackedRelCode, $bDown) as $sDummy => $aQueryInfo)
 		{
 		{
-			MetaModel::DbgTrace("object=".$this->GetKey().", depth=$iMaxDepth, rel=".$aQueryInfo['sQueryDown']);
 			$sQuery = $bDown ? $aQueryInfo['sQueryDown'] : $aQueryInfo['sQueryUp'];
 			$sQuery = $bDown ? $aQueryInfo['sQueryDown'] : $aQueryInfo['sQueryUp'];
 			//$bPropagate = $aQueryInfo["bPropagate"];
 			//$bPropagate = $aQueryInfo["bPropagate"];
 			//$iDepth = $bPropagate ? $iMaxDepth - 1 : 0;
 			//$iDepth = $bPropagate ? $iMaxDepth - 1 : 0;

+ 701 - 414
core/dbobjectsearch.class.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //
 //   This file is part of iTop.
 //   This file is part of iTop.
 //
 //
@@ -20,21 +20,12 @@
 /**
 /**
  * Define filters for a given class of objects (formerly named "filter") 
  * Define filters for a given class of objects (formerly named "filter") 
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
  
  
-define('TREE_OPERATOR_EQUALS', 0);
-define('TREE_OPERATOR_BELOW', 1);
-define('TREE_OPERATOR_BELOW_STRICT', 2);
-define('TREE_OPERATOR_NOT_BELOW', 3);
-define('TREE_OPERATOR_NOT_BELOW_STRICT', 4);
-define('TREE_OPERATOR_ABOVE', 5);
-define('TREE_OPERATOR_ABOVE_STRICT', 6);
-define('TREE_OPERATOR_NOT_ABOVE', 7);
-define('TREE_OPERATOR_NOT_ABOVE_STRICT', 8);
-
-class DBObjectSearch
+
+class DBObjectSearch extends DBSearch
 {
 {
 	private $m_aClasses; // queried classes (alias => class name), the first item is the class corresponding to this filter (the rest is coming from subfilters)
 	private $m_aClasses; // queried classes (alias => class name), the first item is the class corresponding to this filter (the rest is coming from subfilters)
 	private $m_aSelectedClasses; // selected for the output (alias => class name)
 	private $m_aSelectedClasses; // selected for the output (alias => class name)
@@ -43,15 +34,11 @@ class DBObjectSearch
 	private $m_aFullText;
 	private $m_aFullText;
 	private $m_aPointingTo;
 	private $m_aPointingTo;
 	private $m_aReferencedBy;
 	private $m_aReferencedBy;
-	private $m_aRelatedTo;
-	private $m_bDataFiltered;
-
-	// By default, some information may be hidden to the current user
-	// But it may happen that we need to disable that feature
-	private $m_bAllowAllData = false;
 
 
 	public function __construct($sClass, $sClassAlias = null)
 	public function __construct($sClass, $sClassAlias = null)
 	{
 	{
+		parent::__construct();
+
 		if (is_null($sClassAlias)) $sClassAlias = $sClass;
 		if (is_null($sClassAlias)) $sClassAlias = $sClass;
 		if(!is_string($sClass)) throw new Exception('DBObjectSearch::__construct called with a non-string parameter: $sClass = '.print_r($sClass, true));
 		if(!is_string($sClass)) throw new Exception('DBObjectSearch::__construct called with a non-string parameter: $sClass = '.print_r($sClass, true));
 		if(!MetaModel::IsValidClass($sClass)) throw new Exception('DBObjectSearch::__construct called for an invalid class: "'.$sClass.'"');
 		if(!MetaModel::IsValidClass($sClass)) throw new Exception('DBObjectSearch::__construct called for an invalid class: "'.$sClass.'"');
@@ -63,31 +50,24 @@ class DBObjectSearch
 		$this->m_aFullText = array();
 		$this->m_aFullText = array();
 		$this->m_aPointingTo = array();
 		$this->m_aPointingTo = array();
 		$this->m_aReferencedBy = array();
 		$this->m_aReferencedBy = array();
-		$this->m_aRelatedTo = array();
-		$this->m_bDataFiltered = false;
-		$this->m_aParentConditions = array();
-
-		$this->m_aModifierProperties = array();
 	}
 	}
 
 
-	/**
-	 * Perform a deep clone (as opposed to "clone" which does copy a reference to the underlying objects)
-	 **/	 	
-	public function DeepClone()
+	// Create a search definition that leads to 0 result, still a valid search object
+	static public function FromEmptySet($sClass)
 	{
 	{
-		return unserialize(serialize($this)); // Beware this serializes/unserializes the search and its parameters as well
+		$oResultFilter = new DBObjectSearch($sClass);
+		$oResultFilter->m_oSearchCondition = new FalseExpression;
+		return $oResultFilter;
 	}
 	}
 
 
-	public function AllowAllData() {$this->m_bAllowAllData = true;}
-	public function IsAllDataAllowed() {return $this->m_bAllowAllData;}
-	public function IsDataFiltered() {return $this->m_bDataFiltered; }
-	public function SetDataFiltered() {$this->m_bDataFiltered = true;}
+
+	public function GetJoinedClasses() {return $this->m_aClasses;}
 
 
 	public function GetClassName($sAlias)
 	public function GetClassName($sAlias)
 	{
 	{
-		if (array_key_exists($sAlias, $this->m_aClasses))
+		if (array_key_exists($sAlias, $this->m_aSelectedClasses))
 		{
 		{
-			return $this->m_aClasses[$sAlias];
+			return $this->m_aSelectedClasses[$sAlias];
 		}
 		}
 		else
 		else
 		{
 		{
@@ -95,8 +75,6 @@ class DBObjectSearch
 		}
 		}
 	}
 	}
 
 
-	public function GetJoinedClasses() {return $this->m_aClasses;}
-
 	public function GetClass()
 	public function GetClass()
 	{
 	{
 		return reset($this->m_aSelectedClasses);
 		return reset($this->m_aSelectedClasses);
@@ -129,13 +107,18 @@ class DBObjectSearch
 		}
 		}
 		else
 		else
 		{
 		{
-			if (!array_key_exists($sAlias, $this->m_aClasses))
+			if (!array_key_exists($sAlias, $this->m_aSelectedClasses))
 			{
 			{
 				// discard silently - necessary when recursing on the related nodes (see code below)
 				// discard silently - necessary when recursing on the related nodes (see code below)
 				return;
 				return;
 			}
 			}
 		}
 		}
 		$sCurrClass = $this->GetClassName($sAlias);
 		$sCurrClass = $this->GetClassName($sAlias);
+		if ($sNewClass == $sCurrClass)
+		{
+			// Skip silently
+			return;
+		}
 		if (!MetaModel::IsParentClass($sCurrClass, $sNewClass))
 		if (!MetaModel::IsParentClass($sCurrClass, $sNewClass))
 		{
 		{
 			throw new Exception("Could not change the search class from '$sCurrClass' to '$sNewClass'. Only child classes are permitted.");
 			throw new Exception("Could not change the search class from '$sCurrClass' to '$sNewClass'. Only child classes are permitted.");
@@ -148,10 +131,6 @@ class DBObjectSearch
 
 
 		// Change for all the related node (yes, this was necessary with some queries - strange effects otherwise)
 		// Change for all the related node (yes, this was necessary with some queries - strange effects otherwise)
 		//
 		//
-		foreach($this->m_aRelatedTo as $aRelatedTo)
-		{
-			$aRelatedTo['flt']->ChangeClass($sNewClass, $sAlias);
-		}
 		foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo)
 		foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo)
 		{
 		{
 			foreach($aPointingTo as $iOperatorCode => $aFilter)
 			foreach($aPointingTo as $iOperatorCode => $aFilter)
@@ -171,19 +150,6 @@ class DBObjectSearch
 		}
 		}
 	}
 	}
 
 
-	public function SetSelectedClasses($aNewSet)
-	{
-		$this->m_aSelectedClasses = array();
-		foreach ($aNewSet as $sAlias => $sClass)
-		{
-			if (!array_key_exists($sAlias, $this->m_aClasses))
-			{
-				throw new CoreException('Unexpected class alias', array('alias'=>$sAlias, 'expected'=>$this->m_aClasses));
-			}
-			$this->m_aSelectedClasses[$sAlias] = $sClass;
-		}
-	}
-
 	public function GetSelectedClasses()
 	public function GetSelectedClasses()
 	{
 	{
 		return $this->m_aSelectedClasses;
 		return $this->m_aSelectedClasses;
@@ -213,154 +179,8 @@ class DBObjectSearch
 		if (count($this->m_aFullText) > 0) return false;
 		if (count($this->m_aFullText) > 0) return false;
 		if (count($this->m_aPointingTo) > 0) return false;
 		if (count($this->m_aPointingTo) > 0) return false;
 		if (count($this->m_aReferencedBy) > 0) return false;
 		if (count($this->m_aReferencedBy) > 0) return false;
-		if (count($this->m_aRelatedTo) > 0) return false;
-		if (count($this->m_aParentConditions) > 0) return false;
 		return true;
 		return true;
 	}
 	}
-	
-	public function Describe()
-	{
-		// To replace __Describe
-	}
-
-	public function DescribeConditionPointTo($sExtKeyAttCode, $aPointingTo)
-	{
-		if (empty($aPointingTo)) return "";
-		foreach($aPointingTo as $iOperatorCode => $oFilter)
-		{
-			if ($oFilter->IsAny()) break;
-			$oAtt = MetaModel::GetAttributeDef($this->GetClass(), $sExtKeyAttCode);
-			$sOperator = '';
-			switch($iOperatorCode)
-			{
-				case TREE_OPERATOR_EQUALS:
-				$sOperator = 'having';
-				break;
-	
-				case TREE_OPERATOR_BELOW:
-				$sOperator = 'below';
-				break;
-	
-				case TREE_OPERATOR_BELOW_STRICT:
-				$sOperator = 'strictly below';
-				break;
-	
-				case TREE_OPERATOR_NOT_BELOW:
-				$sOperator = 'not below';
-				break;
-	
-				case TREE_OPERATOR_NOT_BELOW_STRICT:
-				$sOperator = 'strictly not below';
-				break;
-
-				case TREE_OPERATOR_ABOVE:
-				$sOperator = 'above';
-				break;
-	
-				case TREE_OPERATOR_ABOVE_STRICT:
-				$sOperator = 'strictly above';
-				break;
-	
-				case TREE_OPERATOR_NOT_ABOVE:
-				$sOperator = 'not above';
-				break;
-	
-				case TREE_OPERATOR_NOT_ABOVE_STRICT:
-				$sOperator = 'strictly not above';
-				break;
-			}
-			$aDescription[] = $oAtt->GetLabel()."$sOperator ({$oFilter->DescribeConditions()})";
-		}
-		return implode(' and ', $aDescription);
-	}
-
-	public function DescribeConditionRefBy($sForeignClass, $sForeignExtKeyAttCode)
-	{
-		if (!isset($this->m_aReferencedBy[$sForeignClass][$sForeignExtKeyAttCode])) return "";
-		$oFilter = $this->m_aReferencedBy[$sForeignClass][$sForeignExtKeyAttCode];
-		if ($oFilter->IsAny()) return "";
-		$oAtt = MetaModel::GetAttributeDef($sForeignClass, $sForeignExtKeyAttCode);
-		return "being ".$oAtt->GetLabel()." for ".$sForeignClass."s in ({$oFilter->DescribeConditions()})";
-	}
-
-	public function DescribeConditionRelTo($aRelInfo)
-	{
-		$oFilter = $aRelInfo['flt'];
-		$sRelCode = $aRelInfo['relcode'];
-		$iMaxDepth = $aRelInfo['maxdepth'];
-		return "related ($sRelCode... peut mieux faire !, $iMaxDepth dig depth) to a {$oFilter->GetClass()} ({$oFilter->DescribeConditions()})";
-	}
-
-
-	public function DescribeConditions()
-	{
-		$aConditions = array();
-
-		$aCondFT = array();
-		foreach($this->m_aFullText as $sFullText)
-		{
-			$aCondFT[] = " contain word(s) '$sFullText'";
-		}
-		if (count($aCondFT) > 0)
-		{
-			$aConditions[] = "which ".implode(" and ", $aCondFT);
-		}
-
-		// #@# todo - review textual description of the JOIN and search condition (is that still feasible?)
-		$aConditions[] = $this->RenderCondition();
-
-		$aCondPoint = array();
-		foreach($this->m_aPointingTo as $sExtKeyAttCode => $aPointingTo)
-		{
-			$aCondPoint[] = $this->DescribeConditionPointTo($sExtKeyAttCode, $aPointingTo);
-		}
-		if (count($aCondPoint) > 0)
-		{
-			$aConditions[] = implode(" and ", $aCondPoint);
-		}
-
-		$aCondReferred= array();
-		foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences)
-		{
-			foreach($aReferences as $sForeignExtKeyAttCode=>$oForeignFilter)
-			{
-				if ($oForeignFilter->IsAny()) continue;
-				$aCondReferred[] = $this->DescribeConditionRefBy($sForeignClass, $sForeignExtKeyAttCode);
-			}
-		}
-		foreach ($this->m_aRelatedTo as $aRelInfo)
-		{
-			$aCondReferred[] = $this->DescribeConditionRelTo($aRelInfo);
-		}
-		if (count($aCondReferred) > 0)
-		{
-			$aConditions[] = implode(" and ", $aCondReferred);
-		}
-
-		foreach ($this->m_aParentConditions as $aRelInfo)
-		{
-			$aCondReferred[] = $this->DescribeConditionParent($aRelInfo);
-		}
-
-		return implode(" and ", $aConditions);		
-	}
-	
-	public function __DescribeHTML()
-	{
-		try
-		{
-			$sConditionDesc = $this->DescribeConditions();
-		}
-		catch (MissingQueryArgument $e)
-		{
-			$sConditionDesc = '?missing query argument?';
-		}
-		if (!empty($sConditionDesc))
-		{
-			return "Objects of class '".$this->GetClass()."', $sConditionDesc";
-		}
-		return "Any object of class '".$this->GetClass()."'";
-	}
 
 
 	protected function TransferConditionExpression($oFilter, $aTranslation)
 	protected function TransferConditionExpression($oFilter, $aTranslation)
 	{
 	{
@@ -381,11 +201,7 @@ class DBObjectSearch
 				$oFilter->m_aParams[$sParam.$index] = $secondValue;
 				$oFilter->m_aParams[$sParam.$index] = $secondValue;
 			}
 			}
 		}
 		}
-//echo "<p>TransferConditionExpression:<br/>";
-//echo "Adding Conditions:<br/><pre>oFilter:\n".print_r($oFilter, true)."\naTranslation:\n".print_r($aTranslation, true)."</pre>\n";
-//echo "</p>";
 		$oTranslated = $oFilter->GetCriteria()->Translate($aTranslation, false, false /* leave unresolved fields */);
 		$oTranslated = $oFilter->GetCriteria()->Translate($aTranslation, false, false /* leave unresolved fields */);
-//echo "Adding Conditions (translated):<br/><pre>".print_r($oTranslated, true)."</pre>\n";
 		$this->AddConditionExpression($oTranslated);
 		$this->AddConditionExpression($oTranslated);
 		$this->m_aParams = array_merge($this->m_aParams, $oFilter->m_aParams);
 		$this->m_aParams = array_merge($this->m_aParams, $oFilter->m_aParams);
 	}
 	}
@@ -393,10 +209,6 @@ class DBObjectSearch
 	protected function RenameParam($sOldName, $sNewName)
 	protected function RenameParam($sOldName, $sNewName)
 	{
 	{
 		$this->m_oSearchCondition->RenameParam($sOldName, $sNewName);
 		$this->m_oSearchCondition->RenameParam($sOldName, $sNewName);
-		foreach($this->m_aRelatedTo as $aRelatedTo)
-		{
-			$aRelatedTo['flt']->RenameParam($sOldName, $sNewName);
-		}
 		foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo)
 		foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo)
 		{
 		{
 			foreach($aPointingTo as $iOperatorCode => $aFilter)
 			foreach($aPointingTo as $iOperatorCode => $aFilter)
@@ -414,17 +226,11 @@ class DBObjectSearch
 				$oForeignFilter->RenameParam($sOldName, $sNewName);
 				$oForeignFilter->RenameParam($sOldName, $sNewName);
 			}
 			}
 		}
 		}
-
-		foreach($this->m_aParentConditions as $aParent)
-		{
-			$aParent['expression']->RenameParam($sOldName, $sNewName);	
-		}
 	}
 	}
 	
 	
 	public function ResetCondition()
 	public function ResetCondition()
 	{
 	{
 		$this->m_oSearchCondition = new TrueExpression();
 		$this->m_oSearchCondition = new TrueExpression();
-		$this->m_aParentConditions = array();
 		// ? is that usefull/enough, do I need to rebuild the list after the subqueries ?
 		// ? is that usefull/enough, do I need to rebuild the list after the subqueries ?
 	}
 	}
 
 
@@ -603,20 +409,6 @@ class DBObjectSearch
 		$this->m_aFullText[] = $sFullText;
 		$this->m_aFullText[] = $sFullText;
 	}
 	}
 
 
-	public function AddCondition_Parent($sAttCode, $iOperatorCode, $oExpression)
-	{
-		$oAttDef = MetaModel::GetAttributeDef($this->GetClass(), $sAttCode);
-		if (!$oAttDef instanceof AttributeHierarchicalKey)
-		{
-			throw new Exception("AddCondition_Parent can only be used on hierarchical keys. '$sAttCode' is not a hierarchical key.");
-		}
-		$this->m_aParentConditions[] = array(
-			'attCode' => $sAttCode,
-			'operator' => $iOperatorCode,
-			'expression' => $oExpression,
-		);
-	}
-	
 	protected function AddToNameSpace(&$aClassAliases, &$aAliasTranslation, $bTranslateMainAlias = true)
 	protected function AddToNameSpace(&$aClassAliases, &$aAliasTranslation, $bTranslateMainAlias = true)
 	{
 	{
 		if ($bTranslateMainAlias)
 		if ($bTranslateMainAlias)
@@ -733,7 +525,7 @@ class DBObjectSearch
 			throw new CoreException("The specified tree operator $iOperatorCode is not applicable to the key '{$this->GetClass()}::$sExtKeyAttCode', which is not a HierarchicalKey");
 			throw new CoreException("The specified tree operator $iOperatorCode is not applicable to the key '{$this->GetClass()}::$sExtKeyAttCode', which is not a HierarchicalKey");
 		}
 		}
 		// Note: though it seems to be a good practice to clone the given source filter
 		// Note: though it seems to be a good practice to clone the given source filter
-		//       (as it was done and fixed an issue in MergeWith())
+		//       (as it was done and fixed an issue in Intersect())
 		//       this was not implemented here because it was causing a regression (login as admin, select an org, click on any badge)
 		//       this was not implemented here because it was causing a regression (login as admin, select an org, click on any badge)
 		//       root cause: FromOQL relies on the fact that the passed filter can be modified later 
 		//       root cause: FromOQL relies on the fact that the passed filter can be modified later 
 		// NO: $oFilter = $oFilter->DeepClone();
 		// NO: $oFilter = $oFilter->DeepClone();
@@ -767,18 +559,18 @@ class DBObjectSearch
 			throw new CoreException("The specified filter (objects referencing an object of class {$this->GetClass()}) is not compatible with the key '{$sForeignClass}::$sForeignExtKeyAttCode', which is pointing to {$oAttExtKey->GetTargetClass()}");
 			throw new CoreException("The specified filter (objects referencing an object of class {$this->GetClass()}) is not compatible with the key '{$sForeignClass}::$sForeignExtKeyAttCode', which is pointing to {$oAttExtKey->GetTargetClass()}");
 		}
 		}
 		// Note: though it seems to be a good practice to clone the given source filter
 		// Note: though it seems to be a good practice to clone the given source filter
-		//       (as it was done and fixed an issue in MergeWith())
+		//       (as it was done and fixed an issue in Intersect())
 		//       this was not implemented here because it was causing a regression (login as admin, select an org, click on any badge)
 		//       this was not implemented here because it was causing a regression (login as admin, select an org, click on any badge)
 		//       root cause: FromOQL relies on the fact that the passed filter can be modified later 
 		//       root cause: FromOQL relies on the fact that the passed filter can be modified later 
 		// NO: $oFilter = $oFilter->DeepClone();
 		// NO: $oFilter = $oFilter->DeepClone();
 		// See also: Trac #639, and self::AddCondition_PointingTo()
 		// See also: Trac #639, and self::AddCondition_PointingTo()
 		$aAliasTranslation = array();
 		$aAliasTranslation = array();
-		$res = $this->AddCondition_ReferencedBy_InNameSpace($oFilter, $sForeignExtKeyAttCode, $this->m_aClasses, $aAliasTranslation);
+		$res = $this->AddCondition_ReferencedBy_InNameSpace(DBObjectSearch, $sForeignExtKeyAttCode, $this->m_aClasses, $aAliasTranslation);
 		$this->TransferConditionExpression($oFilter, $aAliasTranslation);
 		$this->TransferConditionExpression($oFilter, $aAliasTranslation);
 		return $res;
 		return $res;
 	}
 	}
 
 
-	protected function AddCondition_ReferencedBy_InNameSpace(DBObjectSearch $oFilter, $sForeignExtKeyAttCode, &$aClassAliases, &$aAliasTranslation)
+	protected function AddCondition_ReferencedBy_InNameSpace(DBSearch $oFilter, $sForeignExtKeyAttCode, &$aClassAliases, &$aAliasTranslation)
 	{
 	{
 		$sForeignClass = $oFilter->GetClass();
 		$sForeignClass = $oFilter->GetClass();
 
 
@@ -801,19 +593,56 @@ class DBObjectSearch
 		}
 		}
 	}
 	}
 
 
-	public function AddCondition_RelatedTo(DBObjectSearch $oFilter, $sRelCode, $iMaxDepth)
+	public function Intersect(DBSearch $oFilter)
 	{
 	{
-		MyHelpers::CheckValueInArray('relation code', $sRelCode, MetaModel::EnumRelations());
-		$this->m_aRelatedTo[] = array('flt'=>$oFilter, 'relcode'=>$sRelCode, 'maxdepth'=>$iMaxDepth);
-	}
+		if ($oFilter instanceof DBUnionSearch)
+		{
+			// Develop! 
+			$aFilters = $oFilter->GetSearches();
+		}
+		else
+		{
+			$aFilters = array($oFilter);
+		}
 
 
-	public function MergeWith($oFilter)
-	{
-		$oFilter = $oFilter->DeepClone();
-		$aAliasTranslation = array();
-		$res = $this->MergeWith_InNamespace($oFilter, $this->m_aClasses, $aAliasTranslation);
-		$this->TransferConditionExpression($oFilter, $aAliasTranslation);
-		return $res;
+		$aSearches = array();
+		foreach ($aFilters as $oRightFilter)
+		{
+			$oLeftFilter = $this->DeepClone();
+			$oRightFilter = $oRightFilter->DeepClone();
+	
+			if ($oLeftFilter->GetClass() != $oRightFilter->GetClass())
+			{
+				if (MetaModel::IsParentClass($oLeftFilter->GetClass(), $oRightFilter->GetClass()))
+				{
+					// Specialize $oLeftFilter
+					$oLeftFilter->ChangeClass($oRightFilter->GetClass());
+				}
+				elseif (MetaModel::IsParentClass($oRightFilter->GetClass(), $oLeftFilter->GetClass()))
+				{
+					// Specialize $oRightFilter
+					$oRightFilter->ChangeClass($oLeftFilter->GetClass());
+				}
+				else
+				{
+					throw new CoreException("Attempting to merge a filter of class '{$oLeftFilter->GetClass()}' with a filter of class '{$oRightFilter->GetClass()}'");
+				}
+			}
+
+			$aAliasTranslation = array();
+			$oLeftFilter->MergeWith_InNamespace($oRightFilter, $oRet->m_aClasses, $aAliasTranslation);
+			$oLeftFilter->TransferConditionExpression($oRightFilter, $aAliasTranslation);
+			$aSearches[] = $oLeftFilter;
+		}
+		if (count($aSearches) == 1)
+		{
+			// return a DBObjectSearch
+			return $aSearches[0];
+		}
+		else
+		{
+			return new DBUnionSearch($aSearches);
+		}
 	}
 	}
 
 
 	protected function MergeWith_InNamespace($oFilter, &$aClassAliases, &$aAliasTranslation)
 	protected function MergeWith_InNamespace($oFilter, &$aClassAliases, &$aAliasTranslation)
@@ -827,7 +656,6 @@ class DBObjectSearch
 		$aAliasTranslation[$oFilter->GetClassAlias()]['*'] = $this->GetClassAlias(); 
 		$aAliasTranslation[$oFilter->GetClassAlias()]['*'] = $this->GetClassAlias(); 
 
 
 		$this->m_aFullText = array_merge($this->m_aFullText, $oFilter->m_aFullText);
 		$this->m_aFullText = array_merge($this->m_aFullText, $oFilter->m_aFullText);
-		$this->m_aRelatedTo = array_merge($this->m_aRelatedTo, $oFilter->m_aRelatedTo);
 
 
 		foreach($oFilter->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo)
 		foreach($oFilter->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo)
 		{
 		{
@@ -873,10 +701,6 @@ class DBObjectSearch
 		if (!array_key_exists($sForeignExtKeyAttCode, $this->m_aReferencedBy[$sRemoteClass])) return null;
 		if (!array_key_exists($sForeignExtKeyAttCode, $this->m_aReferencedBy[$sRemoteClass])) return null;
 		return $this->m_aReferencedBy[$sRemoteClass][$sForeignExtKeyAttCode];
 		return $this->m_aReferencedBy[$sRemoteClass][$sForeignExtKeyAttCode];
 	}
 	}
-	public function GetCriteria_RelatedTo()
-	{
-		return $this->m_aRelatedTo;
-	}
 
 
 	public function SetInternalParams($aParams)
 	public function SetInternalParams($aParams)
 	{
 	{
@@ -900,11 +724,6 @@ class DBObjectSearch
 		return $this->m_oSearchCondition->ListConstantFields();
 		return $this->m_oSearchCondition->ListConstantFields();
 	}
 	}
 	
 	
-	public function RenderCondition()
-	{
-		return $this->m_oSearchCondition->Render($this->m_aParams, false);
-	}
-
 	/**
 	/**
 	 * Turn the parameters (:xxx) into scalar values in order to easily
 	 * Turn the parameters (:xxx) into scalar values in order to easily
 	 * serialize a search
 	 * serialize a search
@@ -914,106 +733,6 @@ class DBObjectSearch
 		return $this->m_oSearchCondition->ApplyParameters(array_merge($this->m_aParams, $aArgs));
 		return $this->m_oSearchCondition->ApplyParameters(array_merge($this->m_aParams, $aArgs));
 	}
 	}
 	
 	
-	public function serialize($bDevelopParams = false, $aContextParams = null)
-	{
-		$sOql = $this->ToOql($bDevelopParams, $aContextParams);
-		return base64_encode(serialize(array($sOql, $this->m_aParams, $this->m_aModifierProperties)));
-	}
-	
-	static public function unserialize($sValue)
-	{
-		$aData = unserialize(base64_decode($sValue));
-		$sOql = $aData[0];
-		$aParams = $aData[1];
-		// We've tried to use gzcompress/gzuncompress, but for some specific queries
-		// it was not working at all (See Trac #193)
-		// gzuncompress was issuing a warning "data error" and the return object was null
-		$oRetFilter = self::FromOQL($sOql, $aParams);
-		$oRetFilter->m_aModifierProperties = $aData[2];
-		return $oRetFilter;
-	}
-
-	// SImple BUt Structured Query Languag - SubuSQL
-	//
-	static private function Value2Expression($value)
-	{
-		$sRet = $value;
-		if (is_array($value))
-		{
-			$sRet = VS_START.implode(', ', $value).VS_END;
-		}
-		else if (!is_numeric($value))
-		{
-			$sRet = "'".addslashes($value)."'";
-		}
-		return $sRet;
-	}
-	static private function Expression2Value($sExpr)
-	{
-		$retValue = $sExpr;
-		if ((substr($sExpr, 0, 1) == "'") && (substr($sExpr, -1, 1) == "'"))
-		{
-			$sNoQuotes = substr($sExpr, 1, -1);
-			return stripslashes($sNoQuotes);
-		}
-		if ((substr($sExpr, 0, 1) == VS_START) && (substr($sExpr, -1, 1) == VS_END))
-		{
-			$sNoBracket = substr($sExpr, 1, -1);
-			$aRetValue = array();
-			foreach (explode(",", $sNoBracket) as $sItem)
-			{
-				$aRetValue[] = self::Expression2Value(trim($sItem));
-			}
-			return $aRetValue;
-		}
-		return $retValue;
-	}
-
-	// Alternative to object mapping: the data are transfered directly into an array
-	// This is 10 times faster than creating a set of objects, and makes sense when optimization is required
-	/**
-	 * @param hash $aOrderBy Array of '[<classalias>.]attcode' => bAscending
-	 */	
-	public function ToDataArray($aColumns = array(), $aOrderBy = array(), $aArgs = array())
-	{
-		$sSQL = MetaModel::MakeSelectQuery($this, $aOrderBy, $aArgs);
-		$resQuery = CMDBSource::Query($sSQL);
-		if (!$resQuery) return;
-
-		if (count($aColumns) == 0)
-		{
-			$aColumns = array_keys(MetaModel::ListAttributeDefs($this->GetClass()));
-			// Add the standard id (as first column)
-			array_unshift($aColumns, 'id');
-		}
-
-		$aQueryCols = CMDBSource::GetColumns($resQuery);
-
-		$sClassAlias = $this->GetClassAlias();
-		$aColMap = array();
-		foreach ($aColumns as $sAttCode)
-		{
-			$sColName = $sClassAlias.$sAttCode;
-			if (in_array($sColName, $aQueryCols))
-			{
-				$aColMap[$sAttCode] = $sColName;
-			}
-		}
-
-		$aRes = array();
-		while ($aRow = CMDBSource::FetchArray($resQuery))
-		{
-			$aMappedRow = array();
-			foreach ($aColMap as $sAttCode => $sColName)
-			{
-				$aMappedRow[$sAttCode] = $aRow[$sColName];
-			}
-			$aRes[] = $aMappedRow;
-		}
-		CMDBSource::FreeResult($resQuery);
-		return $aRes;
-	}
-
 	public function ToOQL($bDevelopParams = false, $aContextParams = null)
 	public function ToOQL($bDevelopParams = false, $aContextParams = null)
 	{
 	{
 		// Currently unused, but could be useful later
 		// Currently unused, but could be useful later
@@ -1185,58 +904,17 @@ class DBObjectSearch
 		}
 		}
 	}
 	}
 
 
-	// Create a search definition that leads to 0 result, still a valid search object
-	static public function FromEmptySet($sClass)
-	{
-		$oResultFilter = new DBObjectSearch($sClass);
-		$oResultFilter->m_oSearchCondition = new FalseExpression;
-		return $oResultFilter;
-	}
-
-	static protected $m_aOQLQueries = array();
-
-	// Do not filter out depending on user rights
-	// In particular when we are currently in the process of evaluating the user rights...
-	static public function FromOQL_AllData($sQuery, $aParams = null)
-	{
-		$oRes = self::FromOQL($sQuery, $aParams);
-		$oRes->AllowAllData();
-		return $oRes;
-	}
-
-	static public function FromOQL($sQuery, $aParams = null)
+	public function InitFromOqlQuery(OqlQuery $oOqlQuery, $sQuery)
 	{
 	{
-		if (empty($sQuery)) return null;
-
-		// Query caching
-		$bOQLCacheEnabled = true;
-		if ($bOQLCacheEnabled && array_key_exists($sQuery, self::$m_aOQLQueries))
-		{
-			// hit!
-			$oClone = self::$m_aOQLQueries[$sQuery]->DeepClone();
-			if (!is_null($aParams))
-			{
-				$oClone->m_aParams = $aParams;
-			}
-			return $oClone;
-		}
-
-		$oOql = new OqlInterpreter($sQuery);
-		$oOqlQuery = $oOql->ParseObjectQuery();
-
-		$oMetaModel = new ModelReflectionRuntime();
-		$oOqlQuery->Check($oMetaModel, $sQuery); // Exceptions thrown in case of issue
-
 		$sClass = $oOqlQuery->GetClass();
 		$sClass = $oOqlQuery->GetClass();
 		$sClassAlias = $oOqlQuery->GetClassAlias();
 		$sClassAlias = $oOqlQuery->GetClassAlias();
 
 
-		$oResultFilter = new DBObjectSearch($sClass, $sClassAlias);
 		$aAliases = array($sClassAlias => $sClass);
 		$aAliases = array($sClassAlias => $sClass);
 
 
 		// Maintain an array of filters, because the flat list is in fact referring to a tree
 		// Maintain an array of filters, because the flat list is in fact referring to a tree
 		// And this will be an easy way to dispatch the conditions
 		// And this will be an easy way to dispatch the conditions
-		// $oResultFilter will be referenced by the other filters, or the other way around...
-		$aJoinItems = array($sClassAlias => $oResultFilter);
+		// $this will be referenced by the other filters, or the other way around...
+		$aJoinItems = array($sClassAlias => $this);
 
 
 		$aJoinSpecs = $oOqlQuery->GetJoins();
 		$aJoinSpecs = $oOqlQuery->GetJoins();
 		if (is_array($aJoinSpecs))
 		if (is_array($aJoinSpecs))
@@ -1309,43 +987,652 @@ class DBObjectSearch
 		}
 		}
 
 
 		// Check and prepare the select information
 		// Check and prepare the select information
-		$aSelected = array();
+		$this->m_aSelectedClasses = array();
 		foreach ($oOqlQuery->GetSelectedClasses() as $oClassDetails)
 		foreach ($oOqlQuery->GetSelectedClasses() as $oClassDetails)
 		{
 		{
 			$sClassToSelect = $oClassDetails->GetValue();
 			$sClassToSelect = $oClassDetails->GetValue();
-			$aSelected[$sClassToSelect] = $aAliases[$sClassToSelect];
+			$this->m_aSelectedClasses[$sClassToSelect] = $aAliases[$sClassToSelect];
 		}
 		}
-		$oResultFilter->m_aClasses = $aAliases;
-		$oResultFilter->SetSelectedClasses($aSelected);
+		$this->m_aClasses = $aAliases;
 
 
 		$oConditionTree = $oOqlQuery->GetCondition();
 		$oConditionTree = $oOqlQuery->GetCondition();
 		if ($oConditionTree instanceof Expression)
 		if ($oConditionTree instanceof Expression)
 		{
 		{
-			$oResultFilter->m_oSearchCondition = $oResultFilter->OQLExpressionToCondition($sQuery, $oConditionTree, $aAliases);
+			$this->m_oSearchCondition = $this->OQLExpressionToCondition($sQuery, $oConditionTree, $aAliases);
 		}
 		}
+	}
+
+	////////////////////////////////////////////////////////////////////////////
+	//
+	// Construction of the SQL queries
+	//
+	////////////////////////////////////////////////////////////////////////////
 
 
-		if (!is_null($aParams))
+	public function MakeDeleteQuery($aArgs = array())
+	{
+		$aModifierProperties = MetaModel::MakeModifierProperties($this);
+		$oBuild = new QueryBuilderContext($this, $aModifierProperties);
+		$oSQLQuery = $this->MakeSQLObjectQuery($oBuild, null, array());
+		$oSQLQuery->SetCondition($oBuild->m_oQBExpressions->GetCondition());
+		$oSQLQuery->SetSelect($oBuild->m_oQBExpressions->GetSelect());
+		$aScalarArgs = array_merge(MetaModel::PrepareQueryArguments($aArgs), $this->GetInternalParams());
+		return $oSQLQuery->RenderDelete($aScalarArgs);
+	}
+
+	public function MakeUpdateQuery($aValues, $aArgs = array())
+	{
+		// $aValues is an array of $sAttCode => $value
+		$aModifierProperties = MetaModel::MakeModifierProperties($this);
+		$oBuild = new QueryBuilderContext($this, $aModifierProperties);
+		$oSQLQuery = $this->MakeSQLObjectQuery($oBuild, null, $aValues);
+		$oSQLQuery->SetCondition($oBuild->m_oQBExpressions->GetCondition());
+		$oSQLQuery->SetSelect($oBuild->m_oQBExpressions->GetSelect());
+		$aScalarArgs = array_merge(MetaModel::PrepareQueryArguments($aArgs), $this->GetInternalParams());
+		return $oSQLQuery->RenderUpdate($aScalarArgs);
+	}
+
+	public function MakeSQLQuery($aAttToLoad, $bGetCount, $aModifierProperties, $aGroupByExpr = null, $aSelectedClasses = null)
+	{
+		$oBuild = new QueryBuilderContext($this, $aModifierProperties, $aGroupByExpr, $aSelectedClasses);
+
+		$oSQLQuery = $this->MakeSQLObjectQuery($oBuild, $aAttToLoad, array());
+		$oSQLQuery->SetCondition($oBuild->m_oQBExpressions->GetCondition());
+		if ($aGroupByExpr)
+		{
+			$aCols = $oBuild->m_oQBExpressions->GetGroupBy();
+			$oSQLQuery->SetGroupBy($aCols);
+			$oSQLQuery->SetSelect($aCols);
+		}
+		else
 		{
 		{
-			$oResultFilter->m_aParams = $aParams;
+			$oSQLQuery->SetSelect($oBuild->m_oQBExpressions->GetSelect());
 		}
 		}
 
 
-		if ($bOQLCacheEnabled)
+		if (self::$m_bOptimizeQueries)
 		{
 		{
-			self::$m_aOQLQueries[$sQuery] = $oResultFilter->DeepClone();
+			if ($bGetCount)
+			{
+				// Simplify the query if just getting the count
+				$oSQLQuery->SetSelect(array());
+			}
+			$oBuild->m_oQBExpressions->GetMandatoryTables($aMandatoryTables);
+			$oSQLQuery->OptimizeJoins($aMandatoryTables);
 		}
 		}
 
 
-		return $oResultFilter;
+		return $oSQLQuery;
 	}
 	}
 
 
-	public function toxpath()
+
+	protected function MakeSQLObjectQuery(&$oBuild, $aAttToLoad = null, $aValues = array())
 	{
 	{
-		// #@# a voir...
+		// 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)
+		$sClass = $this->GetFirstJoinedClass();
+		$sClassAlias = $this->GetFirstJoinedClassAlias();
+
+		$bIsOnQueriedClass = array_key_exists($sClassAlias, $oBuild->GetRootFilter()->GetSelectedClasses());
+
+		self::DbgTrace("Entering: ".$this->ToOQL().", ".($bIsOnQueriedClass ? "MAIN" : "SECONDARY"));
+
+		$sRootClass = MetaModel::GetRootClass($sClass);
+		$sKeyField = MetaModel::DBGetKey($sClass);
+
+		if ($bIsOnQueriedClass)
+		{
+			// default to the whole list of attributes + the very std id/finalclass
+			$oBuild->m_oQBExpressions->AddSelect($sClassAlias.'id', new FieldExpression('id', $sClassAlias));
+			if (is_null($aAttToLoad) || !array_key_exists($sClassAlias, $aAttToLoad))
+			{
+				$sSelectedClass = $oBuild->GetSelectedClass($sClassAlias);
+				$aAttList = MetaModel::ListAttributeDefs($sSelectedClass);
+			}
+			else
+			{
+				$aAttList = $aAttToLoad[$sClassAlias];
+			}
+			foreach ($aAttList as $sAttCode => $oAttDef)
+			{
+				if (!$oAttDef->IsScalar()) continue;
+				// keep because it can be used for sorting - if (!$oAttDef->LoadInObject()) continue;
+				
+				foreach ($oAttDef->GetSQLExpressions() as $sColId => $sSQLExpr)
+				{
+					$oBuild->m_oQBExpressions->AddSelect($sClassAlias.$sAttCode.$sColId, new FieldExpression($sAttCode.$sColId, $sClassAlias));
+				}
+			}
+
+			// Transform the full text condition into additional condition expression
+			$aFullText = $this->GetCriteria_FullText();
+			if (count($aFullText) > 0)
+			{
+				$aFullTextFields = array();
+				foreach (MetaModel::ListAttributeDefs($sClass) as $sAttCode => $oAttDef)
+				{
+					if (!$oAttDef->IsScalar()) continue;
+					if ($oAttDef->IsExternalKey()) continue;
+					$aFullTextFields[] = new FieldExpression($sAttCode, $sClassAlias);
+				}
+				$oTextFields = new CharConcatWSExpression(' ', $aFullTextFields);
+				
+				foreach($aFullText as $sFTNeedle)
+				{
+					$oNewCond = new BinaryExpression($oTextFields, 'LIKE', new ScalarExpression("%$sFTNeedle%"));
+					$oBuild->m_oQBExpressions->AddCondition($oNewCond);
+				}
+			}
+		}
+//echo "<p>oQBExpr ".__LINE__.": <pre>\n".print_r($oBuild->m_oQBExpressions, true)."</pre></p>\n";
+		$aExpectedAtts = array(); // array of (attcode => fieldexpression)
+//echo "<p>".__LINE__.": GetUnresolvedFields($sClassAlias, ...)</p>\n";
+		$oBuild->m_oQBExpressions->GetUnresolvedFields($sClassAlias, $aExpectedAtts);
+
+		// Compute a clear view of required joins (from the current class)
+		// Build the list of external keys:
+		// -> ext keys required by an explicit join
+		// -> ext keys mentionned in a 'pointing to' condition
+		// -> ext keys required for an external field
+		// -> ext keys required for a friendly name
+		//
+		$aExtKeys = array(); // array of sTableClass => array of (sAttCode (keys) => array of (sAttCode (fields)=> oAttDef))
+		//
+		// Optimization: could be partially computed once for all (cached) ?
+		//  
+
+		if ($bIsOnQueriedClass)
+		{
+			// Get all Ext keys for the queried class (??)
+			foreach(MetaModel::GetKeysList($sClass) as $sKeyAttCode)
+			{
+				$sKeyTableClass = MetaModel::GetAttributeOrigin($sClass, $sKeyAttCode);
+				$aExtKeys[$sKeyTableClass][$sKeyAttCode] = array();
+			}
+		}
+		// Get all Ext keys used by the filter
+		foreach ($this->GetCriteria_PointingTo() as $sKeyAttCode => $aPointingTo)
+		{
+			if (array_key_exists(TREE_OPERATOR_EQUALS, $aPointingTo))
+			{
+				$sKeyTableClass = MetaModel::GetAttributeOrigin($sClass, $sKeyAttCode);
+				$aExtKeys[$sKeyTableClass][$sKeyAttCode] = array();
+			}
+		}
+
+		$aFNJoinAlias = array(); // array of (subclass => alias)
+		if (array_key_exists('friendlyname', $aExpectedAtts))
+		{
+			// To optimize: detect a restriction on child classes in the condition expression
+			//    e.g. SELECT FunctionalCI WHERE finalclass IN ('Server', 'VirtualMachine')
+			$oNameExpression = self::GetExtendedNameExpression($sClass);
+
+			$aNameFields = array();
+			$oNameExpression->GetUnresolvedFields('', $aNameFields);
+			$aTranslateNameFields = array();
+			foreach($aNameFields as $sSubClass => $aFields)
+			{
+				foreach($aFields as $sAttCode => $oField)
+				{
+					$oAttDef = MetaModel::GetAttributeDef($sSubClass, $sAttCode);
+					if ($oAttDef->IsExternalKey())
+					{
+						$sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sAttCode);
+						$aExtKeys[$sClassOfAttribute][$sAttCode] = array();
+					}				
+					elseif ($oAttDef->IsExternalField() || ($oAttDef instanceof AttributeFriendlyName))
+					{
+						$sKeyAttCode = $oAttDef->GetKeyAttCode();
+						$sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sKeyAttCode);
+						$aExtKeys[$sClassOfAttribute][$sKeyAttCode][$sAttCode] = $oAttDef;
+					}
+					else
+					{
+						$sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sAttCode);
+					}
+
+					if (MetaModel::IsParentClass($sClassOfAttribute, $sClass))
+					{
+						// The attribute is part of the standard query
+						//
+						$sAliasForAttribute = $sClassAlias;
+					}
+					else
+					{
+						// The attribute will be available from an additional outer join
+						// For each subclass (table) one single join is enough
+						//
+						if (!array_key_exists($sClassOfAttribute, $aFNJoinAlias))
+						{
+							$sAliasForAttribute = $oBuild->GenerateClassAlias($sClassAlias.'_fn_'.$sClassOfAttribute, $sClassOfAttribute);
+							$aFNJoinAlias[$sClassOfAttribute] = $sAliasForAttribute;
+						}
+						else
+						{
+							$sAliasForAttribute = $aFNJoinAlias[$sClassOfAttribute];
+						}
+					}
+
+					$aTranslateNameFields[$sSubClass][$sAttCode] = new FieldExpression($sAttCode, $sAliasForAttribute);
+				}
+			}
+			$oNameExpression = $oNameExpression->Translate($aTranslateNameFields, false);
+
+			$aTranslateNow = array();
+			$aTranslateNow[$sClassAlias]['friendlyname'] = $oNameExpression;
+			$oBuild->m_oQBExpressions->Translate($aTranslateNow, false);
+		}
+
+		// Add the ext fields used in the select (eventually adds an external key)
+		foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode=>$oAttDef)
+		{
+			if ($oAttDef->IsExternalField() || ($oAttDef instanceof AttributeFriendlyName))
+			{
+				if (array_key_exists($sAttCode, $aExpectedAtts))
+				{
+					$sKeyAttCode = $oAttDef->GetKeyAttCode();
+					if ($sKeyAttCode != 'id')
+					{
+						// Add the external attribute
+						$sKeyTableClass = MetaModel::GetAttributeOrigin($sClass, $sKeyAttCode);
+						$aExtKeys[$sKeyTableClass][$sKeyAttCode][$sAttCode] = $oAttDef;
+					}
+				}
+			}
+		}
+
+		// First query built upon on the leaf (ie current) class
+		//
+		self::DbgTrace("Main (=leaf) class, call MakeSQLObjectQuerySingleTable()");
+		if (MetaModel::HasTable($sClass))
+		{
+			$oSelectBase = $this->MakeSQLObjectQuerySingleTable($oBuild, $sClass, $aExtKeys, $aValues);
+		}
+		else
+		{
+			$oSelectBase = null;
+
+			// As the join will not filter on the expected classes, we have to specify it explicitely
+			$sExpectedClasses = implode("', '", MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL));
+			$oFinalClassRestriction = Expression::FromOQL("`$sClassAlias`.finalclass IN ('$sExpectedClasses')");
+			$oBuild->m_oQBExpressions->AddCondition($oFinalClassRestriction);
+		}
+
+		// Then we join the queries of the eventual parent classes (compound model)
+		foreach(MetaModel::EnumParentClasses($sClass) as $sParentClass)
+		{
+			if (!MetaModel::HasTable($sParentClass)) continue;
+
+			self::DbgTrace("Parent class: $sParentClass... let's call MakeSQLObjectQuerySingleTable()");
+			$oSelectParentTable = $this->MakeSQLObjectQuerySingleTable($oBuild, $sParentClass, $aExtKeys, $aValues);
+			if (is_null($oSelectBase))
+			{
+				$oSelectBase = $oSelectParentTable;
+			}
+			else
+			{
+				$oSelectBase->AddInnerJoin($oSelectParentTable, $sKeyField, MetaModel::DBGetKey($sParentClass));
+			}
+		}
+
+		// Filter on objects referencing me
+		foreach ($this->GetCriteria_ReferencedBy() as $sForeignClass => $aKeysAndFilters)
+		{
+			foreach ($aKeysAndFilters as $sForeignKeyAttCode => $oForeignFilter)
+			{
+				$oForeignKeyAttDef = MetaModel::GetAttributeDef($sForeignClass, $sForeignKeyAttCode);
+	
+				self::DbgTrace("Referenced by foreign key: $sForeignKeyAttCode... let's call MakeSQLObjectQuery()");
+				//self::DbgTrace($oForeignFilter);
+				//self::DbgTrace($oForeignFilter->ToOQL());
+				//self::DbgTrace($oSelectForeign);
+				//self::DbgTrace($oSelectForeign->RenderSelect(array()));
+
+				$sForeignClassAlias = $oForeignFilter->GetFirstJoinedClassAlias();
+				$oBuild->m_oQBExpressions->PushJoinField(new FieldExpression($sForeignKeyAttCode, $sForeignClassAlias));
+
+				if ($oForeignKeyAttDef instanceof AttributeObjectKey)
+				{
+					$sClassAttCode = $oForeignKeyAttDef->Get('class_attcode');
+
+					// Add the condition: `$sForeignClassAlias`.$sClassAttCode IN (subclasses of $sClass')
+					$oClassListExpr = ListExpression::FromScalars(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL));
+					$oClassExpr = new FieldExpression($sClassAttCode, $sForeignClassAlias);
+					$oClassRestriction = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr);
+					$oBuild->m_oQBExpressions->AddCondition($oClassRestriction);
+				}
+
+				$oSelectForeign = $oForeignFilter->MakeSQLObjectQuery($oBuild, $aAttToLoad);
+
+				$oJoinExpr = $oBuild->m_oQBExpressions->PopJoinField();
+				$sForeignKeyTable = $oJoinExpr->GetParent();
+				$sForeignKeyColumn = $oJoinExpr->GetName();
+				$oSelectBase->AddInnerJoin($oSelectForeign, $sKeyField, $sForeignKeyColumn, $sForeignKeyTable);
+			}
+		}
+
+		// Additional JOINS for Friendly names
+		//
+		foreach ($aFNJoinAlias as $sSubClass => $sSubClassAlias)
+		{
+			$oSubClassFilter = new DBObjectSearch($sSubClass, $sSubClassAlias);
+			$oSelectFN = $oSubClassFilter->MakeSQLObjectQuerySingleTable($oBuild, $sSubClass, $aExtKeys, array());
+			$oSelectBase->AddLeftJoin($oSelectFN, $sKeyField, MetaModel::DBGetKey($sSubClass));
+		}
+
+		// That's all... cross fingers and we'll get some working query
+
+		//MyHelpers::var_dump_html($oSelectBase, true);
+		//MyHelpers::var_dump_html($oSelectBase->RenderSelect(), true);
+		if (self::$m_bDebugQuery) $oSelectBase->DisplayHtml();
+		return $oSelectBase;
 	}
 	}
-	static public function fromxpath()
+
+	protected function MakeSQLObjectQuerySingleTable(&$oBuild, $sTableClass, $aExtKeys, $aValues)
 	{
 	{
-		// #@# a voir...
+		// $aExtKeys is an array of sTableClass => array of (sAttCode (keys) => array of sAttCode (fields))
+//echo "MakeSQLObjectQuery($sTableClass)-liste des clefs externes($sTableClass): <pre>".print_r($aExtKeys, true)."</pre><br/>\n";
+
+		// Prepare the query for a single table (compound objects)
+		// Ignores the items (attributes/filters) that are not on the target table
+		// Perform an (inner or left) join for every external key (and specify the expected fields)
+		//
+		// Returns an SQLQuery
+		//
+		$sTargetClass = $this->GetFirstJoinedClass();
+		$sTargetAlias = $this->GetFirstJoinedClassAlias();
+		$sTable = MetaModel::DBGetTable($sTableClass);
+		$sTableAlias = $oBuild->GenerateTableAlias($sTargetAlias.'_'.$sTable, $sTable);
+
+		$aTranslation = array();
+		$aExpectedAtts = array();
+		$oBuild->m_oQBExpressions->GetUnresolvedFields($sTargetAlias, $aExpectedAtts);
+		
+		$bIsOnQueriedClass = array_key_exists($sTargetAlias, $oBuild->GetRootFilter()->GetSelectedClasses());
+		
+		self::DbgTrace("Entering: tableclass=$sTableClass, filter=".$this->ToOQL().", ".($bIsOnQueriedClass ? "MAIN" : "SECONDARY"));
+
+		// 1 - SELECT and UPDATE
+		//
+		// Note: no need for any values nor fields for foreign Classes (ie not the queried Class)
+		//
+		$aUpdateValues = array();
+
+
+		// 1/a - Get the key and friendly name
+		//
+		// We need one pkey to be the key, let's take the first one available
+		$oSelectedIdField = null;
+		$oIdField = new FieldExpressionResolved(MetaModel::DBGetKey($sTableClass), $sTableAlias);
+		$aTranslation[$sTargetAlias]['id'] = $oIdField;
+
+		if ($bIsOnQueriedClass)
+		{
+			// Add this field to the list of queried fields (required for the COUNT to work fine)
+			$oSelectedIdField = $oIdField;
+		}
+
+		// 1/b - Get the other attributes
+		// 
+		foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef)
+		{
+			// Skip this attribute if not defined in this table
+			if (MetaModel::GetAttributeOrigin($sTargetClass, $sAttCode) != $sTableClass) continue;
+
+			// Skip this attribute if not made of SQL columns 
+			if (count($oAttDef->GetSQLExpressions()) == 0) continue;
+
+			// Update...
+			//
+			if ($bIsOnQueriedClass && array_key_exists($sAttCode, $aValues))
+			{
+				assert ($oAttDef->IsDirectField());
+				foreach ($oAttDef->GetSQLValues($aValues[$sAttCode]) as $sColumn => $sValue)
+				{
+					$aUpdateValues[$sColumn] = $sValue;
+				}
+			}
+		}
+
+		// 2 - The SQL query, for this table only
+		//
+		$oSelectBase = new SQLObjectQuery($sTable, $sTableAlias, array(), $bIsOnQueriedClass, $aUpdateValues, $oSelectedIdField);
+
+		// 3 - Resolve expected expressions (translation table: alias.attcode => table.column)
+		//
+		foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef)
+		{
+			// Skip this attribute if not defined in this table
+			if (MetaModel::GetAttributeOrigin($sTargetClass, $sAttCode) != $sTableClass) continue;
+
+			// Select...
+			//
+			if ($oAttDef->IsExternalField())
+			{
+				// skip, this will be handled in the joined tables (done hereabove)
+			}
+			else
+			{
+//echo "<p>MakeSQLObjectQuerySingleTable: Field $sAttCode is part of the table $sTable (named: $sTableAlias)</p>";
+				// standard field, or external key
+				// add it to the output
+				foreach ($oAttDef->GetSQLExpressions() as $sColId => $sSQLExpr)
+				{
+					if (array_key_exists($sAttCode.$sColId, $aExpectedAtts))
+					{
+						$oFieldSQLExp = new FieldExpressionResolved($sSQLExpr, $sTableAlias);
+						foreach (MetaModel::EnumPlugins('iQueryModifier') as $sPluginClass => $oQueryModifier)
+						{
+							$oFieldSQLExp = $oQueryModifier->GetFieldExpression($oBuild, $sTargetClass, $sAttCode, $sColId, $oFieldSQLExp, $oSelectBase);
+						}
+						$aTranslation[$sTargetAlias][$sAttCode.$sColId] = $oFieldSQLExp;
+					}
+				}
+			}
+		}
+
+//echo "MakeSQLObjectQuery- Classe $sTableClass<br/>\n";
+		// 4 - The external keys -> joins...
+		//
+		$aAllPointingTo = $this->GetCriteria_PointingTo();
+
+		if (array_key_exists($sTableClass, $aExtKeys))
+		{
+			foreach ($aExtKeys[$sTableClass] as $sKeyAttCode => $aExtFields)
+			{
+				$oKeyAttDef = MetaModel::GetAttributeDef($sTableClass, $sKeyAttCode);
+
+				$aPointingTo = $this->GetCriteria_PointingTo($sKeyAttCode);
+//echo "MakeSQLObjectQuery-Cle '$sKeyAttCode'<br/>\n";
+				if (!array_key_exists(TREE_OPERATOR_EQUALS, $aPointingTo))
+				{
+//echo "MakeSQLObjectQuery-Ajoutons l'operateur TREE_OPERATOR_EQUALS pour $sKeyAttCode<br/>\n";
+					// The join was not explicitely defined in the filter,
+					// we need to do it now
+					$sKeyClass =  $oKeyAttDef->GetTargetClass();
+					$sKeyClassAlias = $oBuild->GenerateClassAlias($sKeyClass.'_'.$sKeyAttCode, $sKeyClass);
+					$oExtFilter = new DBObjectSearch($sKeyClass, $sKeyClassAlias);
+
+					$aAllPointingTo[$sKeyAttCode][TREE_OPERATOR_EQUALS][$sKeyClassAlias] = $oExtFilter;
+				}
+			}
+		}
+//echo "MakeSQLObjectQuery-liste des clefs de jointure: <pre>".print_r(array_keys($aAllPointingTo), true)."</pre><br/>\n";
+				
+		foreach ($aAllPointingTo as $sKeyAttCode => $aPointingTo)
+		{
+			foreach($aPointingTo as $iOperatorCode => $aFilter)
+			{
+				foreach($aFilter as $oExtFilter)
+				{
+					if (!MetaModel::IsValidAttCode($sTableClass, $sKeyAttCode)) continue; // Not defined in the class, skip it
+					// The aliases should not conflict because normalization occured while building the filter
+					$oKeyAttDef = MetaModel::GetAttributeDef($sTableClass, $sKeyAttCode);
+					$sKeyClass =  $oExtFilter->GetFirstJoinedClass();
+					$sKeyClassAlias = $oExtFilter->GetFirstJoinedClassAlias();
+
+//echo "MakeSQLObjectQuery-$sTableClass::$sKeyAttCode Foreach PointingTo($iOperatorCode) <span style=\"color:red\">$sKeyClass (alias:$sKeyClassAlias)</span><br/>\n";
+				
+					// Note: there is no search condition in $oExtFilter, because normalization did merge the condition onto the top of the filter tree 
+
+//echo "MakeSQLObjectQuery-array_key_exists($sTableClass, \$aExtKeys)<br/>\n";
+					if ($iOperatorCode == TREE_OPERATOR_EQUALS)
+					{
+						if (array_key_exists($sTableClass, $aExtKeys) && array_key_exists($sKeyAttCode, $aExtKeys[$sTableClass]))
+						{
+							// Specify expected attributes for the target class query
+							// ... and use the current alias !
+							$aTranslateNow = array(); // Translation for external fields - must be performed before the join is done (recursion...)
+							foreach($aExtKeys[$sTableClass][$sKeyAttCode] as $sAttCode => $oAtt)
+							{
+//echo "MakeSQLObjectQuery aExtKeys[$sTableClass][$sKeyAttCode] => $sAttCode-oAtt: <pre>".print_r($oAtt, true)."</pre><br/>\n";
+								if ($oAtt instanceof AttributeFriendlyName)
+								{
+									// Note: for a given ext key, there is one single attribute "friendly name"
+									$aTranslateNow[$sTargetAlias][$sAttCode] = new FieldExpression('friendlyname', $sKeyClassAlias);
+//echo "<p><b>aTranslateNow[$sTargetAlias][$sAttCode] = new FieldExpression('friendlyname', $sKeyClassAlias);</b></p>\n";
+								}
+								else
+								{
+									$sExtAttCode = $oAtt->GetExtAttCode();
+									// Translate mainclass.extfield => remoteclassalias.remotefieldcode
+									$oRemoteAttDef = MetaModel::GetAttributeDef($sKeyClass, $sExtAttCode);
+									foreach ($oRemoteAttDef->GetSQLExpressions() as $sColID => $sRemoteAttExpr)
+									{
+										$aTranslateNow[$sTargetAlias][$sAttCode.$sColId] = new FieldExpression($sExtAttCode, $sKeyClassAlias);
+//echo "<p><b>aTranslateNow[$sTargetAlias][$sAttCode.$sColId] = new FieldExpression($sExtAttCode, $sKeyClassAlias);</b></p>\n";
+									}
+//echo "<p><b>ExtAttr2: $sTargetAlias.$sAttCode to $sKeyClassAlias.$sRemoteAttExpr (class: $sKeyClass)</b></p>\n";
+								}
+							}
+
+							if ($oKeyAttDef instanceof AttributeObjectKey)
+							{
+								// Add the condition: `$sTargetAlias`.$sClassAttCode IN (subclasses of $sKeyClass')
+								$sClassAttCode = $oKeyAttDef->Get('class_attcode');
+								$oClassAttDef = MetaModel::GetAttributeDef($sTargetClass, $sClassAttCode);
+								foreach ($oClassAttDef->GetSQLExpressions() as $sColID => $sSQLExpr)
+								{
+									$aTranslateNow[$sTargetAlias][$sClassAttCode.$sColId] = new FieldExpressionResolved($sSQLExpr, $sTableAlias);
+								}
+
+								$oClassListExpr = ListExpression::FromScalars(MetaModel::EnumChildClasses($sKeyClass, ENUM_CHILD_CLASSES_ALL));
+								$oClassExpr = new FieldExpression($sClassAttCode, $sTargetAlias);
+								$oClassRestriction = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr);
+								$oBuild->m_oQBExpressions->AddCondition($oClassRestriction);
+							}
+
+							// Translate prior to recursing
+							//
+//echo "<p>oQBExpr ".__LINE__.": <pre>\n".print_r($oBuild->m_oQBExpressions, true)."\n".print_r($aTranslateNow, true)."</pre></p>\n";
+							$oBuild->m_oQBExpressions->Translate($aTranslateNow, false);
+//echo "<p>oQBExpr ".__LINE__.": <pre>\n".print_r($oBuild->m_oQBExpressions, true)."</pre></p>\n";
+		
+//echo "<p>External key $sKeyAttCode (class: $sKeyClass), call MakeSQLObjectQuery()/p>\n";
+							self::DbgTrace("External key $sKeyAttCode (class: $sKeyClass), call MakeSQLObjectQuery()");
+							$oBuild->m_oQBExpressions->PushJoinField(new FieldExpression('id', $sKeyClassAlias));
+			
+//echo "<p>Recursive MakeSQLObjectQuery ".__LINE__.": <pre>\n".print_r($oBuild->GetRootFilter()->GetSelectedClasses(), true)."</pre></p>\n";
+							$oSelectExtKey = $oExtFilter->MakeSQLObjectQuery($oBuild);
+			
+							$oJoinExpr = $oBuild->m_oQBExpressions->PopJoinField();
+							$sExternalKeyTable = $oJoinExpr->GetParent();
+							$sExternalKeyField = $oJoinExpr->GetName();
+			
+							$aCols = $oKeyAttDef->GetSQLExpressions(); // Workaround a PHP bug: sometimes issuing a Notice if invoking current(somefunc())
+							$sLocalKeyField = current($aCols); // get the first column for an external key
+			
+							self::DbgTrace("External key $sKeyAttCode, Join on $sLocalKeyField = $sExternalKeyField");
+							if ($oKeyAttDef->IsNullAllowed())
+							{
+								$oSelectBase->AddLeftJoin($oSelectExtKey, $sLocalKeyField, $sExternalKeyField, $sExternalKeyTable);
+							}
+							else
+							{
+								$oSelectBase->AddInnerJoin($oSelectExtKey, $sLocalKeyField, $sExternalKeyField, $sExternalKeyTable);
+							}
+						}
+					}
+					elseif(MetaModel::GetAttributeOrigin($sKeyClass, $sKeyAttCode) == $sTableClass)
+					{
+						$oBuild->m_oQBExpressions->PushJoinField(new FieldExpression($sKeyAttCode, $sKeyClassAlias));
+						$oSelectExtKey = $oExtFilter->MakeSQLObjectQuery($oBuild);
+						$oJoinExpr = $oBuild->m_oQBExpressions->PopJoinField();
+						$sExternalKeyTable = $oJoinExpr->GetParent();
+						$sExternalKeyField = $oJoinExpr->GetName();
+						$sLeftIndex = $sExternalKeyField.'_left'; // TODO use GetSQLLeft()
+						$sRightIndex = $sExternalKeyField.'_right'; // TODO use GetSQLRight()
+	
+						$LocalKeyLeft = $oKeyAttDef->GetSQLLeft();
+						$LocalKeyRight = $oKeyAttDef->GetSQLRight();
+	
+						$oSelectBase->AddInnerJoinTree($oSelectExtKey, $LocalKeyLeft, $LocalKeyRight, $sLeftIndex, $sRightIndex, $sExternalKeyTable, $iOperatorCode);
+					}
+				}
+			}
+		}
+
+		// Translate the selected columns
+		//
+//echo "<p>oQBExpr ".__LINE__.": <pre>\n".print_r($oBuild->m_oQBExpressions, true)."</pre></p>\n";
+		$oBuild->m_oQBExpressions->Translate($aTranslation, false);
+//echo "<p>oQBExpr ".__LINE__.": <pre>\n".print_r($oBuild->m_oQBExpressions, true)."</pre></p>\n";
+
+		//MyHelpers::var_dump_html($oSelectBase->RenderSelect());
+		return $oSelectBase;
 	}
 	}
-}
 
 
+	/**
+	 *	Get the friendly name for the class and its subclasses (if finalclass = 'subclass' ...)
+	 *	Simplifies the final expression by grouping classes having the same name expression	 
+	 *	Used when querying a parent class 	 
+	*/
+	static protected function GetExtendedNameExpression($sClass)
+	{
+		// 1st step - get all of the required expressions (instantiable classes)
+		//            and group them using their OQL representation
+		//
+		$aFNExpressions = array(); // signature => array('expression' => oExp, 'classes' => array of classes)
+		foreach (MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL) as $sSubClass)
+		{
+			if (($sSubClass != $sClass) && MetaModel::IsAbstract($sSubClass)) continue;
+
+			$oSubClassName = MetaModel::GetNameExpression($sSubClass);
+			$sSignature = $oSubClassName->Render();
+			if (!array_key_exists($sSignature, $aFNExpressions))
+			{
+				$aFNExpressions[$sSignature] = array(
+					'expression' => $oSubClassName,
+					'classes' => array(),
+				);
+			}
+			$aFNExpressions[$sSignature]['classes'][] = $sSubClass;
+		}
+
+		// 2nd step - build the final name expression depending on the finalclass
+		//
+		if (count($aFNExpressions) == 1)
+		{
+			$aExpData = reset($aFNExpressions);
+			$oNameExpression = $aExpData['expression'];
+		}
+		else
+		{
+			$oNameExpression = null;
+			foreach ($aFNExpressions as $sSignature => $aExpData)
+			{
+				$oClassListExpr = ListExpression::FromScalars($aExpData['classes']);
+				$oClassExpr = new FieldExpression('finalclass', $sClass);
+				$oClassInList = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr);
+
+				if (is_null($oNameExpression))
+				{
+					$oNameExpression = $aExpData['expression'];
+				}
+				else
+				{
+					$oNameExpression = new FunctionExpression('IF', array($oClassInList, $aExpData['expression'], $oNameExpression));
+				}
+			}
+		}
+		return $oNameExpression;
+	}
 
 
-?>
+}

+ 9 - 9
core/dbobjectset.class.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-// Copyright (C) 2010-2014 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //
 //   This file is part of iTop.
 //   This file is part of iTop.
 //
 //
@@ -20,7 +20,7 @@
 /**
 /**
  * Object set management
  * Object set management
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -47,14 +47,14 @@ class DBObjectSet
 	/**
 	/**
 	 * Create a new set based on a Search definition.
 	 * Create a new set based on a Search definition.
 	 * 
 	 * 
-	 * @param DBObjectSearch $oFilter The search filter defining the objects which are part of the set (multiple columns/objects per row are supported)
+	 * @param DBSearch $oFilter The search filter defining the objects which are part of the set (multiple columns/objects per row are supported)
 	 * @param hash $aOrderBy Array of '[<classalias>.]attcode' => bAscending
 	 * @param hash $aOrderBy Array of '[<classalias>.]attcode' => bAscending
 	 * @param hash $aArgs Values to substitute for the search/query parameters (if any). Format: param_name => value
 	 * @param hash $aArgs Values to substitute for the search/query parameters (if any). Format: param_name => value
 	 * @param hash $aExtendedDataSpec
 	 * @param hash $aExtendedDataSpec
 	 * @param int $iLimitCount Maximum number of rows to load (i.e. equivalent to MySQL's LIMIT start, count)
 	 * @param int $iLimitCount Maximum number of rows to load (i.e. equivalent to MySQL's LIMIT start, count)
 	 * @param int $iLimitStart Index of the first row to load (i.e. equivalent to MySQL's LIMIT start, count)
 	 * @param int $iLimitStart Index of the first row to load (i.e. equivalent to MySQL's LIMIT start, count)
 	 */
 	 */
-	public function __construct(DBObjectSearch $oFilter, $aOrderBy = array(), $aArgs = array(), $aExtendedDataSpec = null, $iLimitCount = 0, $iLimitStart = 0)
+	public function __construct(DBSearch $oFilter, $aOrderBy = array(), $aArgs = array(), $aExtendedDataSpec = null, $iLimitCount = 0, $iLimitStart = 0)
 	{
 	{
 		$this->m_oFilter = $oFilter->DeepClone();
 		$this->m_oFilter = $oFilter->DeepClone();
 		$this->m_aAddedIds = array();
 		$this->m_aAddedIds = array();
@@ -86,7 +86,7 @@ class DBObjectSet
 		$sRet = '';
 		$sRet = '';
 		$this->Rewind();
 		$this->Rewind();
 		$sRet .= "Set (".$this->m_oFilter->ToOQL().")<br/>\n";
 		$sRet .= "Set (".$this->m_oFilter->ToOQL().")<br/>\n";
-		$sRet .= "Query: <pre style=\"font-size: smaller; display:inline;\">".MetaModel::MakeSelectQuery($this->m_oFilter, array()).")</pre>\n";
+		$sRet .= "Query: <pre style=\"font-size: smaller; display:inline;\">".$this->m_oFilter->MakeSelectQuery().")</pre>\n";
 		
 		
 		$sRet .= $this->Count()." records<br/>\n";
 		$sRet .= $this->Count()." records<br/>\n";
 		if ($this->Count() > 0)
 		if ($this->Count() > 0)
@@ -353,7 +353,7 @@ class DBObjectSet
 	}
 	}
 
 
 	/**
 	/**
-	 * Retrieve the DBObjectSearch corresponding to the objects present in this set
+	 * Retrieve the DBSearch corresponding to the objects present in this set
 	 * 
 	 * 
 	 * Limitation:
 	 * Limitation:
 	 * This method will NOT work for sets with several columns (i.e. several objects per row)
 	 * This method will NOT work for sets with several columns (i.e. several objects per row)
@@ -513,11 +513,11 @@ class DBObjectSet
 
 
 		if ($this->m_iLimitCount > 0)
 		if ($this->m_iLimitCount > 0)
 		{
 		{
-			$sSQL = MetaModel::MakeSelectQuery($this->m_oFilter, $this->GetRealSortOrder(), $this->m_aArgs, $this->m_aAttToLoad, $this->m_aExtendedDataSpec, $this->m_iLimitCount, $this->m_iLimitStart);
+			$sSQL = $this->m_oFilter->MakeSelectQuery($this->GetRealSortOrder(), $this->m_aArgs, $this->m_aAttToLoad, $this->m_aExtendedDataSpec, $this->m_iLimitCount, $this->m_iLimitStart);
 		}
 		}
 		else
 		else
 		{
 		{
-			$sSQL = MetaModel::MakeSelectQuery($this->m_oFilter, $this->GetRealSortOrder(), $this->m_aArgs, $this->m_aAttToLoad, $this->m_aExtendedDataSpec);
+			$sSQL = $this->m_oFilter->MakeSelectQuery($this->GetRealSortOrder(), $this->m_aArgs, $this->m_aAttToLoad, $this->m_aExtendedDataSpec);
 		}
 		}
 		
 		
 		if (is_object($this->m_oSQLResult))
 		if (is_object($this->m_oSQLResult))
@@ -549,7 +549,7 @@ class DBObjectSet
 	{
 	{
 		if (is_null($this->m_iNumTotalDBRows))
 		if (is_null($this->m_iNumTotalDBRows))
 		{
 		{
-			$sSQL = MetaModel::MakeSelectQuery($this->m_oFilter, array(), $this->m_aArgs, null, null, 0, 0, true);
+			$sSQL = $this->m_oFilter->MakeSelectQuery(array(), $this->m_aArgs, null, null, 0, 0, true);
 			$resQuery = CMDBSource::Query($sSQL);
 			$resQuery = CMDBSource::Query($sSQL);
 			if (!$resQuery) return 0;
 			if (!$resQuery) return 0;
 	
 	

+ 724 - 0
core/dbsearch.class.php

@@ -0,0 +1,724 @@
+<?php
+// Copyright (C) 2015 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify	
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+
+require_once('dbobjectsearch.class.php');
+require_once('dbunionsearch.class.php');
+
+
+define('TREE_OPERATOR_EQUALS', 0);
+define('TREE_OPERATOR_BELOW', 1);
+define('TREE_OPERATOR_BELOW_STRICT', 2);
+define('TREE_OPERATOR_NOT_BELOW', 3);
+define('TREE_OPERATOR_NOT_BELOW_STRICT', 4);
+define('TREE_OPERATOR_ABOVE', 5);
+define('TREE_OPERATOR_ABOVE_STRICT', 6);
+define('TREE_OPERATOR_NOT_ABOVE', 7);
+define('TREE_OPERATOR_NOT_ABOVE_STRICT', 8);
+
+/**
+ * An object search
+ * 
+ * Note: in the ancient times of iTop, a search was named after DBObjectSearch.
+ *  When the UNION has been introduced, it has been decided to:
+ *  - declare a hierarchy of search classes, with two leafs :
+ *    - one class to cope with a single query (A JOIN B... WHERE...)
+ *    - and the other to cope with several queries (query1 UNION query2)
+ *  - in order to preserve forward/backward compatibility of the existing modules 
+ *    - keep the name of DBObjectSearch even if it a little bit confusing
+ *    - do not provide a type-hint for function parameters defined in the modules
+ *    - leave the statements DBObjectSearch::FromOQL in the modules, though DBSearch is more relevant 
+ *
+ * @copyright   Copyright (C) 2015 Combodo SARL
+ * @license     http://opensource.org/licenses/AGPL-3.0
+ */
+ 
+abstract class DBSearch
+{
+	protected $m_bDataFiltered = false;
+	protected $m_aModifierProperties = array();
+
+	// By default, some information may be hidden to the current user
+	// But it may happen that we need to disable that feature
+	protected $m_bAllowAllData = false;
+
+	public function __construct()
+	{
+	}
+
+	/**
+	 * Perform a deep clone (as opposed to "clone" which does copy a reference to the underlying objects)
+	 **/	 	
+	public function DeepClone()
+	{
+		return unserialize(serialize($this)); // Beware this serializes/unserializes the search and its parameters as well
+	}
+
+	public function AllowAllData() {$this->m_bAllowAllData = true;}
+	public function IsAllDataAllowed() {return $this->m_bAllowAllData;}
+	public function IsDataFiltered() {return $this->m_bDataFiltered; }
+	public function SetDataFiltered() {$this->m_bDataFiltered = true;}
+
+	public function SetModifierProperty($sPluginClass, $sProperty, $value)
+	{
+		$this->m_aModifierProperties[$sPluginClass][$sProperty] = $value;
+	}
+
+	public function GetModifierProperties($sPluginClass)
+	{
+		if (array_key_exists($sPluginClass, $this->m_aModifierProperties))
+		{
+			return $this->m_aModifierProperties[$sPluginClass];
+		}
+		else
+		{
+			return array();
+		}
+	}
+
+	abstract public function GetClassName($sAlias);
+	abstract public function GetClass();
+	abstract public function GetClassAlias();
+
+	/**
+	 * Change the class (only subclasses are supported as of now, because the conditions must fit the new class)
+	 * Defaults to the first selected class (most of the time it is also the first joined class	 
+	 */	 	
+	abstract public function ChangeClass($sNewClass, $sAlias = null);
+	abstract public function GetSelectedClasses();
+
+	abstract public function IsAny();
+
+	public function Describe(){return 'deprecated - use ToOQL() instead';}
+	public function DescribeConditionPointTo($sExtKeyAttCode, $aPointingTo){return 'deprecated - use ToOQL() instead';}
+	public function DescribeConditionRefBy($sForeignClass, $sForeignExtKeyAttCode){return 'deprecated - use ToOQL() instead';}
+	public function DescribeConditionRelTo($aRelInfo){return 'deprecated - use ToOQL() instead';}
+	public function DescribeConditions(){return 'deprecated - use ToOQL() instead';}
+	public function __DescribeHTML(){return 'deprecated - use ToOQL() instead';}
+
+	abstract public function ResetCondition();
+	abstract public function MergeConditionExpression($oExpression);
+	abstract public function AddConditionExpression($oExpression);
+  	abstract public function AddNameCondition($sName);
+	abstract public function AddCondition($sFilterCode, $value, $sOpCode = null);
+	/**
+	 * Specify a condition on external keys or link sets
+	 * @param sAttSpec Can be either an attribute code or extkey->[sAttSpec] or linkset->[sAttSpec] and so on, recursively
+	 *                 Example: infra_list->ci_id->location_id->country	 
+	 * @param value The value to match (can be an array => IN(val1, val2...)
+	 * @return void
+	 */
+	abstract public function AddConditionAdvanced($sAttSpec, $value);
+	abstract public function AddCondition_FullText($sFullText);
+
+	abstract public function AddCondition_PointingTo(DBObjectSearch $oFilter, $sExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS);
+	abstract public function AddCondition_ReferencedBy(DBObjectSearch $oFilter, $sForeignExtKeyAttCode);
+	abstract public function Intersect(DBSearch $oFilter);
+
+	abstract public function SetInternalParams($aParams);
+	abstract public function GetInternalParams();
+	abstract public function GetQueryParams();
+	abstract public function ListConstantFields();
+	
+	/**
+	 * Turn the parameters (:xxx) into scalar values in order to easily
+	 * serialize a search
+	 */
+	abstract public function ApplyParameters($aArgs);
+	
+	public function serialize($bDevelopParams = false, $aContextParams = null)
+	{
+		$sOql = $this->ToOql($bDevelopParams, $aContextParams);
+		return base64_encode(serialize(array($sOql, $this->GetInternalParams(), $this->m_aModifierProperties)));
+	}
+	
+	static public function unserialize($sValue)
+	{
+		$aData = unserialize(base64_decode($sValue));
+		$sOql = $aData[0];
+		$aParams = $aData[1];
+		// We've tried to use gzcompress/gzuncompress, but for some specific queries
+		// it was not working at all (See Trac #193)
+		// gzuncompress was issuing a warning "data error" and the return object was null
+		$oRetFilter = self::FromOQL($sOql, $aParams);
+		$oRetFilter->m_aModifierProperties = $aData[2];
+		return $oRetFilter;
+	}
+
+	abstract public function ToOQL($bDevelopParams = false, $aContextParams = null);
+
+	static protected $m_aOQLQueries = array();
+
+	// Do not filter out depending on user rights
+	// In particular when we are currently in the process of evaluating the user rights...
+	static public function FromOQL_AllData($sQuery, $aParams = null)
+	{
+		$oRes = self::FromOQL($sQuery, $aParams);
+		$oRes->AllowAllData();
+		return $oRes;
+	}
+
+	static public function FromOQL($sQuery, $aParams = null)
+	{
+		if (empty($sQuery)) return null;
+
+		// Query caching
+		$bOQLCacheEnabled = true;
+		if ($bOQLCacheEnabled && array_key_exists($sQuery, self::$m_aOQLQueries))
+		{
+			// hit!
+			$oClone = self::$m_aOQLQueries[$sQuery]->DeepClone();
+			if (!is_null($aParams))
+			{
+				$oClone->SetInternalParams($aParams);
+			}
+			return $oClone;
+		}
+
+		$oOql = new OqlInterpreter($sQuery);
+		$oOqlQuery = $oOql->ParseQuery();
+
+		$oMetaModel = new ModelReflectionRuntime();
+		$oOqlQuery->Check($oMetaModel, $sQuery); // Exceptions thrown in case of issue
+
+		$oResultFilter = $oOqlQuery->ToDBSearch($sQuery);
+
+		if (!is_null($aParams))
+		{
+			$oResultFilter->SetInternalParams($aParams);
+		}
+
+		if ($bOQLCacheEnabled)
+		{
+			self::$m_aOQLQueries[$sQuery] = $oResultFilter->DeepClone();
+		}
+
+		return $oResultFilter;
+	}
+
+	// Alternative to object mapping: the data are transfered directly into an array
+	// This is 10 times faster than creating a set of objects, and makes sense when optimization is required
+	/**
+	 * @param hash $aOrderBy Array of '[<classalias>.]attcode' => bAscending
+	 */	
+	public function ToDataArray($aColumns = array(), $aOrderBy = array(), $aArgs = array())
+	{
+		$sSQL = $this->MakeSelectQuery($aOrderBy, $aArgs);
+		$resQuery = CMDBSource::Query($sSQL);
+		if (!$resQuery) return;
+
+		if (count($aColumns) == 0)
+		{
+			$aColumns = array_keys(MetaModel::ListAttributeDefs($this->GetClass()));
+			// Add the standard id (as first column)
+			array_unshift($aColumns, 'id');
+		}
+
+		$aQueryCols = CMDBSource::GetColumns($resQuery);
+
+		$sClassAlias = $this->GetClassAlias();
+		$aColMap = array();
+		foreach ($aColumns as $sAttCode)
+		{
+			$sColName = $sClassAlias.$sAttCode;
+			if (in_array($sColName, $aQueryCols))
+			{
+				$aColMap[$sAttCode] = $sColName;
+			}
+		}
+
+		$aRes = array();
+		while ($aRow = CMDBSource::FetchArray($resQuery))
+		{
+			$aMappedRow = array();
+			foreach ($aColMap as $sAttCode => $sColName)
+			{
+				$aMappedRow[$sAttCode] = $aRow[$sColName];
+			}
+			$aRes[] = $aMappedRow;
+		}
+		CMDBSource::FreeResult($resQuery);
+		return $aRes;
+	}
+
+	////////////////////////////////////////////////////////////////////////////
+	//
+	// Construction of the SQL queries
+	//
+	////////////////////////////////////////////////////////////////////////////
+	protected static $m_aQueryStructCache = array();
+
+
+	public function MakeGroupByQuery($aArgs, $aGroupByExpr, $bExcludeNullValues = false)
+	{
+		if ($bExcludeNullValues)
+		{
+			// Null values are not handled (though external keys set to 0 are allowed)
+			$oQueryFilter = $this->DeepClone();
+			foreach ($aGroupByExpr as $oGroupByExp)
+			{
+				$oNull = new FunctionExpression('ISNULL', array($oGroupByExp));
+				$oNotNull = new BinaryExpression($oNull, '!=', new TrueExpression());
+				$oQueryFilter->AddConditionExpression($oNotNull);
+			}
+		}
+		else
+		{
+			$oQueryFilter = $this;
+		}
+
+		$aAttToLoad = array();
+		$oSQLQuery = $oQueryFilter->GetSQLQuery(array(), $aArgs, $aAttToLoad, null, 0, 0, false, $aGroupByExpr);
+
+		$aScalarArgs = array_merge(MetaModel::PrepareQueryArguments($aArgs), $this->GetInternalParams());
+		try
+		{
+			$bBeautifulSQL = self::$m_bTraceQueries || self::$m_bDebugQuery || self::$m_bIndentQueries;
+			$sRes = $oSQLQuery->RenderGroupBy($aScalarArgs, $bBeautifulSQL);
+		}
+		catch (MissingQueryArgument $e)
+		{
+			// Add some information...
+			$e->addInfo('OQL', $this->ToOQL());
+			throw $e;
+		}
+		$this->AddQueryTraceGroupBy($aArgs, $aGroupByExpr, $sRes);
+		return $sRes;
+	}
+
+
+	/**
+	 * @param hash $aOrderBy Array of '[<classalias>.]attcode' => bAscending
+	 */	
+	public function MakeSelectQuery($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
+		//
+		$sClass = $this->GetClass();
+		$sClassAlias = $this->GetClassAlias();
+		$aOrderSpec = array();
+		foreach ($aOrderBy as $sFieldAlias => $bAscending)
+		{
+			if (!is_bool($bAscending))
+			{
+				throw new CoreException("Wrong direction in ORDER BY spec, found '$bAscending' and expecting a boolean value");
+			}
+
+			$iDotPos = strpos($sFieldAlias, '.');
+			if ($iDotPos === false)
+			{
+				$sAttClass = $sClass;
+				$sAttClassAlias = $sClassAlias;
+				$sAttCode = $sFieldAlias;
+			}
+			else
+			{
+				$sAttClassAlias = substr($sFieldAlias, 0, $iDotPos);
+				$sAttClass = $this->GetClassName($sAttClassAlias);
+				$sAttCode = substr($sFieldAlias, $iDotPos + 1);
+			}
+
+			if ($sAttCode != 'id')
+			{
+				MyHelpers::CheckValueInArray('field name in ORDER BY spec', $sAttCode, MetaModel::GetAttributesList($sAttClass));
+
+				$oAttDef = MetaModel::GetAttributeDef($sAttClass, $sAttCode);
+				foreach($oAttDef->GetOrderBySQLExpressions($sAttClassAlias) as $sSQLExpression)
+				{
+					$aOrderSpec[$sSQLExpression] = $bAscending;
+				}
+			}
+			else
+			{
+				$aOrderSpec['`'.$sAttClassAlias.$sAttCode.'`'] = $bAscending;
+			}
+
+			// Make sure that the columns used for sorting are present in the loaded columns
+			if (!is_null($aAttToLoad) && !isset($aAttToLoad[$sAttClassAlias][$sAttCode]))
+			{
+				$aAttToLoad[$sAttClassAlias][$sAttCode] = MetaModel::GetAttributeDef($sAttClass, $sAttCode);
+			}			
+		}
+
+		$oSQLQuery = $this->GetSQLQuery($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount);
+
+		$aScalarArgs = array_merge(MetaModel::PrepareQueryArguments($aArgs), $this->GetInternalParams());
+		try
+		{
+			$bBeautifulSQL = self::$m_bTraceQueries || self::$m_bDebugQuery || self::$m_bIndentQueries;
+			$sRes = $oSQLQuery->RenderSelect($aOrderSpec, $aScalarArgs, $iLimitCount, $iLimitStart, $bGetCount, $bBeautifulSQL);
+			if ($sClassAlias == '_itop_')
+			{
+				echo $sRes."<br/>\n";
+			}
+		}
+		catch (MissingQueryArgument $e)
+		{
+			// Add some information...
+			$e->addInfo('OQL', $this->ToOQL());
+			throw $e;
+		}
+		$this->AddQueryTraceSelect($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount, $sRes);
+		return $sRes;
+	}
+
+
+	protected function GetSQLQuery($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount, $aGroupByExpr = null)
+	{
+		// Hide objects that are not visible to the current user
+		//
+		$oSearch = $this;
+		if (!$this->IsAllDataAllowed() && !$this->IsDataFiltered())
+		{
+			$oVisibleObjects = UserRights::GetSelectFilter($this->GetClass(), $this->GetModifierProperties('UserRightsGetSelectFilter'));
+			if ($oVisibleObjects === false)
+			{
+				// Make sure this is a valid search object, saying NO for all
+				$oVisibleObjects = DBObjectSearch::FromEmptySet($this->GetClass());
+			}
+			if (is_object($oVisibleObjects))
+			{
+				$oSearch = $this->Intersect($oVisibleObjects);
+				$oSearch->SetDataFiltered();
+			}
+			else
+			{
+				// should be true at this point, meaning that no additional filtering
+				// is required
+			}
+		}
+
+		// Compute query modifiers properties (can be set in the search itself, by the context, etc.)
+		//
+		$aModifierProperties = MetaModel::MakeModifierProperties($oSearch);
+
+		// Create a unique cache id
+		//
+		if (self::$m_bQueryCacheEnabled || self::$m_bTraceQueries)
+		{
+			// Need to identify the query
+			$sOqlQuery = $oSearch->ToOql();
+
+			if (count($aModifierProperties))
+			{
+				array_multisort($aModifierProperties);
+				$sModifierProperties = json_encode($aModifierProperties);
+			}
+			else
+			{
+				$sModifierProperties = '';
+			}
+
+			$sRawId = $sOqlQuery.$sModifierProperties;
+			if (!is_null($aAttToLoad))
+			{
+				$sRawId .= json_encode($aAttToLoad);
+			}
+			if (!is_null($aGroupByExpr))
+			{
+				foreach($aGroupByExpr as $sAlias => $oExpr)
+				{
+					$sRawId .= 'g:'.$sAlias.'!'.$oExpr->Render();
+				}
+			}
+			$sRawId .= $bGetCount;
+			$sOqlId = md5($sRawId);
+		}
+		else
+		{
+			$sOqlQuery = "SELECTING... ".$oSearch->GetClass();
+			$sOqlId = "query id ? n/a";
+		}
+
+
+		// Query caching
+		//
+		if (self::$m_bQueryCacheEnabled)
+		{
+			// Warning: using directly the query string as the key to the hash array can FAIL if the string
+			// is long and the differences are only near the end... so it's safer (but not bullet proof?)
+			// to use a hash (like md5) of the string as the key !
+			//
+			// Example of two queries that were found as similar by the hash array:
+			// SELECT SLT JOIN lnkSLTToSLA AS L1 ON L1.slt_id=SLT.id JOIN SLA ON L1.sla_id = SLA.id JOIN lnkContractToSLA AS L2 ON L2.sla_id = SLA.id JOIN CustomerContract ON L2.contract_id = CustomerContract.id WHERE SLT.ticket_priority = 1 AND SLA.service_id = 3 AND SLT.metric = 'TTO' AND CustomerContract.customer_id = 2
+			// and	
+			// SELECT SLT JOIN lnkSLTToSLA AS L1 ON L1.slt_id=SLT.id JOIN SLA ON L1.sla_id = SLA.id JOIN lnkContractToSLA AS L2 ON L2.sla_id = SLA.id JOIN CustomerContract ON L2.contract_id = CustomerContract.id WHERE SLT.ticket_priority = 1 AND SLA.service_id = 3 AND SLT.metric = 'TTR' AND CustomerContract.customer_id = 2
+			// the only difference is R instead or O at position 285 (TTR instead of TTO)...
+			//
+			if (array_key_exists($sOqlId, self::$m_aQueryStructCache))
+			{
+				// hit!
+
+				$oSQLQuery = unserialize(serialize(self::$m_aQueryStructCache[$sOqlId]));
+				// Note: cloning is not enough because the subtree is made of objects
+			}
+			elseif (self::$m_bUseAPCCache)
+			{
+				// Note: For versions of APC older than 3.0.17, fetch() accepts only one parameter
+				//
+				$sOqlAPCCacheId = 'itop-'.MetaModel::GetEnvironmentId().'-query-cache-'.$sOqlId;
+				$oKPI = new ExecutionKPI();
+				$result = apc_fetch($sOqlAPCCacheId);
+				$oKPI->ComputeStats('Query APC (fetch)', $sOqlQuery);
+
+				if (is_object($result))
+				{
+					$oSQLQuery = $result;
+					self::$m_aQueryStructCache[$sOqlId] = $oSQLQuery;
+				}
+			}
+		}
+
+		if (!isset($oSQLQuery))
+		{
+			$oKPI = new ExecutionKPI();
+			$oSQLQuery = $oSearch->MakeSQLQuery($aAttToLoad, $bGetCount, $aModifierProperties, $aGroupByExpr);
+			$oSQLQuery->SetSourceOQL($sOqlQuery);
+			$oKPI->ComputeStats('MakeSQLQuery', $sOqlQuery);
+
+			if (self::$m_bQueryCacheEnabled)
+			{
+				if (self::$m_bUseAPCCache)
+				{
+					$oKPI = new ExecutionKPI();
+					apc_store($sOqlAPCCacheId, $oSQLQuery, self::$m_iQueryCacheTTL);
+					$oKPI->ComputeStats('Query APC (store)', $sOqlQuery);
+				}
+
+				self::$m_aQueryStructCache[$sOqlId] = $oSQLQuery->DeepClone();
+			}
+		}
+
+		// Join to an additional table, if required...
+		//
+		if ($aExtendedDataSpec != null)
+		{
+			$sTableAlias = '_extended_data_';
+			$aExtendedFields = array();
+			foreach($aExtendedDataSpec['fields'] as $sColumn)
+			{
+				$sColRef = $oSearch->GetClassAlias().'_extdata_'.$sColumn;
+				$aExtendedFields[$sColRef] = new FieldExpressionResolved($sColumn, $sTableAlias);
+			}
+			$oSQLQueryExt = new SQLObjectQuery($aExtendedDataSpec['table'], $sTableAlias, $aExtendedFields);
+			$oSQLQuery->AddInnerJoin($oSQLQueryExt, 'id', $aExtendedDataSpec['join_key'] /*, $sTableAlias*/);
+		}
+		
+		return $oSQLQuery;
+	}
+
+	////////////////////////////////////////////////////////////////////////////
+	//
+	// Cache/Trace/Log queries
+	//
+	////////////////////////////////////////////////////////////////////////////
+	protected static $m_bDebugQuery = false;
+	protected static $m_aQueriesLog = array();
+	protected static $m_bQueryCacheEnabled = false;
+	protected static $m_bUseAPCCache = false;
+	protected static $m_iQueryCacheTTL = 3600;
+	protected static $m_bTraceQueries = false;
+	protected static $m_bIndentQueries = false;
+	protected static $m_bOptimizeQueries = false;
+
+	public static function StartDebugQuery()
+	{
+		$aBacktrace = debug_backtrace();
+		self::$m_bDebugQuery = true;
+	}
+	public static function StopDebugQuery()
+	{
+		self::$m_bDebugQuery = false;
+	}
+	
+	public static function EnableQueryCache($bEnabled, $bUseAPC, $iTimeToLive = 3600)
+	{
+		self::$m_bQueryCacheEnabled = $bEnabled;
+		self::$m_bUseAPCCache = $bUseAPC;
+		self::$m_iQueryCacheTTL = $iTimeToLive;
+	}
+	public static function EnableQueryTrace($bEnabled)
+	{
+		self::$m_bTraceQueries = $bEnabled;
+	}
+	public static function EnableQueryIndentation($bEnabled)
+	{
+		self::$m_bIndentQueries = $bEnabled;
+	}
+	public static function EnableOptimizeQuery($bEnabled)
+	{
+		self::$m_bOptimizeQueries = $bEnabled;
+	}
+
+
+	protected function AddQueryTraceSelect($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount, $sSql)
+	{
+		if (self::$m_bTraceQueries)
+		{
+			$aQueryData = array(
+				'type' => 'select',
+				'filter' => $this,
+				'order_by' => $aOrderBy,
+				'args' => $aArgs,
+				'att_to_load' => $aAttToLoad,
+				'extended_data_spec' => $aExtendedDataSpec,
+				'limit_count' => $iLimitCount,
+				'limit_start' => $iLimitStart,
+				'is_count' => $bGetCount
+			);
+			$sOql = $this->ToOQL(true, $aArgs);
+			self::AddQueryTrace($aQueryData, $sOql, $sSql);
+		}
+	}
+	
+	protected function AddQueryTraceGroupBy($aArgs, $aGroupByExpr, $sSql)
+	{
+		if (self::$m_bTraceQueries)
+		{
+			$aQueryData = array(
+				'type' => 'group_by',
+				'filter' => $this,
+				'args' => $aArgs,
+				'group_by_expr' => $aGroupByExpr
+			);
+			$sOql = $this->ToOQL(true, $aArgs);
+			self::AddQueryTrace($aQueryData, $sOql, $sSql);
+		}
+	}
+
+	protected static function AddQueryTrace($aQueryData, $sOql, $sSql)
+	{
+		if (self::$m_bTraceQueries)
+		{
+			$sQueryId = md5(serialize($aQueryData));
+			$sMySQLQueryId = md5($sSql);
+			if(!isset(self::$m_aQueriesLog[$sQueryId]))
+			{
+				self::$m_aQueriesLog[$sQueryId]['data'] = serialize($aQueryData);
+				self::$m_aQueriesLog[$sQueryId]['oql'] = $sOql;
+				self::$m_aQueriesLog[$sQueryId]['hits'] = 1;
+			}
+			else
+			{
+				self::$m_aQueriesLog[$sQueryId]['hits']++;
+			}
+			if(!isset(self::$m_aQueriesLog[$sQueryId]['queries'][$sMySQLQueryId]))
+			{
+				self::$m_aQueriesLog[$sQueryId]['queries'][$sMySQLQueryId]['sql'] = $sSql;
+				self::$m_aQueriesLog[$sQueryId]['queries'][$sMySQLQueryId]['count'] = 1;
+				$iTableCount = count(CMDBSource::ExplainQuery($sSql));
+				self::$m_aQueriesLog[$sQueryId]['queries'][$sMySQLQueryId]['table_count'] = $iTableCount;
+			}
+			else
+			{
+				self::$m_aQueriesLog[$sQueryId]['queries'][$sMySQLQueryId]['count']++;
+			}
+		}
+	}
+
+	public static function RecordQueryTrace()
+	{
+		if (!self::$m_bTraceQueries) return;
+
+		$iOqlCount = count(self::$m_aQueriesLog);
+		$iSqlCount = 0;
+		foreach (self::$m_aQueriesLog as $sQueryId => $aOqlData)
+		{
+			$iSqlCount += $aOqlData['hits'];
+		}
+		$sHtml = "<h2>Stats on SELECT queries: OQL=$iOqlCount, SQL=$iSqlCount</h2>\n";
+		foreach (self::$m_aQueriesLog as $sQueryId => $aOqlData)
+		{
+			$sOql = $aOqlData['oql'];
+			$sHits = $aOqlData['hits'];
+
+			$sHtml .= "<p><b>$sHits</b> hits for OQL query: $sOql</p>\n";
+			$sHtml .= "<ul id=\"ClassesRelationships\" class=\"treeview\">\n";
+			foreach($aOqlData['queries'] as $aSqlData)
+			{
+				$sQuery = $aSqlData['sql'];
+				$sSqlHits = $aSqlData['count'];
+				$iTableCount = $aSqlData['table_count'];
+				$sHtml .= "<li><b>$sSqlHits</b> hits for SQL ($iTableCount tables): <pre style=\"font-size:60%\">$sQuery</pre></li>\n";
+			}
+			$sHtml .= "</ul>\n";
+		}
+
+		$sLogFile = 'queries.latest';
+		file_put_contents(APPROOT.'data/'.$sLogFile.'.html', $sHtml);
+
+		$sLog = "<?php\n\$aQueriesLog = ".var_export(self::$m_aQueriesLog, true).";";
+		file_put_contents(APPROOT.'data/'.$sLogFile.'.log', $sLog);
+
+		// Cumulate the queries
+		$sAllQueries = APPROOT.'data/queries.log';
+		if (file_exists($sAllQueries))
+		{
+			// Merge the new queries into the existing log
+			include($sAllQueries);
+			foreach (self::$m_aQueriesLog as $sQueryId => $aOqlData)
+			{
+				if (!array_key_exists($sQueryId, $aQueriesLog))
+				{
+					$aQueriesLog[$sQueryId] = $aOqlData;
+				}
+			}
+		}
+		else
+		{
+			$aQueriesLog = self::$m_aQueriesLog;
+		}
+		$sLog = "<?php\n\$aQueriesLog = ".var_export($aQueriesLog, true).";";
+		file_put_contents($sAllQueries, $sLog);
+	}
+
+	protected static function DbgTrace($value)
+	{
+		if (!self::$m_bDebugQuery) return;
+		$aBacktrace = debug_backtrace();
+		$iCallStackPos = count($aBacktrace) - self::$m_bDebugQuery;
+		$sIndent = ""; 
+		for ($i = 0 ; $i < $iCallStackPos ; $i++)
+		{
+			$sIndent .= " .-=^=-. ";
+		}
+		$aCallers = array();
+		foreach($aBacktrace as $aStackInfo)
+		{
+			$aCallers[] = $aStackInfo["function"];
+		}
+		$sCallers = "Callstack: ".implode(', ', $aCallers);
+		$sFunction = "<b title=\"$sCallers\">".$aBacktrace[1]["function"]."</b>";
+
+		if (is_string($value))
+		{
+			echo "$sIndent$sFunction: $value<br/>\n";
+		}
+		else if (is_object($value))
+		{
+			echo "$sIndent$sFunction:\n<pre>\n";
+			print_r($value);
+			echo "</pre>\n";
+		}
+		else
+		{
+			echo "$sIndent$sFunction: $value<br/>\n";
+		}
+	}
+}

+ 432 - 0
core/dbunionsearch.class.php

@@ -0,0 +1,432 @@
+<?php
+// Copyright (C) 2015 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify	
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+
+/**
+ * A union of DBObjectSearches 
+ *
+ * @copyright   Copyright (C) 2015 Combodo SARL
+ * @license     http://opensource.org/licenses/AGPL-3.0
+ */
+ 
+class DBUnionSearch extends DBSearch
+{
+	protected $aSearches; // source queries
+	protected $aSelectedClasses; // alias => classes (lowest common ancestors) computed at construction
+
+	public function __construct($aSearches)
+	{
+		if (count ($aSearches) == 0)
+		{
+			throw new CoreException('A DBUnionSearch must be made of at least one search');
+		}
+
+		$this->aSearches = array();
+		foreach ($aSearches as $oSearch)
+		{
+			if ($oSearch instanceof DBUnionSearch)
+			{
+				foreach ($oSearch->aSearches as $oSubSearch)
+				{
+					$this->aSearches[] = $oSubSearch->DeepClone();
+				}
+			}
+			else
+			{
+				$this->aSearches[] = $oSearch->DeepClone();
+			}
+		}
+
+		// 1 - Collect all the column/classes
+		$aColumnToClasses = array();
+		foreach ($this->aSearches as $iPos => $oSearch)
+		{
+			$aSelected = array_values($oSearch->GetSelectedClasses());
+
+			if ($iPos != 0)
+			{
+				if (count($aSelected) < count($aColumnToClasses))
+				{
+					throw new Exception('Too few selected classes in the subquery #'.($iPos+1));
+				}
+				if (count($aSelected) > count($aColumnToClasses))
+				{
+					throw new Exception('Too many selected classes in the subquery #'.($iPos+1));
+				}
+			}
+
+			foreach ($aSelected as $iColumn => $sClass)
+			{
+				$aColumnToClasses[$iColumn][] = $sClass;
+			}
+		}
+
+		// 2 - Build the index column => alias
+		$oFirstSearch = $this->aSearches[0];
+		$aColumnToAlias = array_keys($oFirstSearch->GetSelectedClasses());
+
+		// 3 - Compute alias => lowest common ancestor
+		$this->aSelectedClasses = array();
+		foreach ($aColumnToClasses as $iColumn => $aClasses)
+		{
+			$sAlias = $aColumnToAlias[$iColumn];
+			$sAncestor = MetaModel::GetLowestCommonAncestor($aClasses);
+			if (is_null($sAncestor))
+			{
+				throw new Exception('Could not find a common ancestor for the column '.($iColumn+1).' (Classes: '.implode(', ', $aClasses).')');
+			}
+			$this->aSelectedClasses[$sAlias] = $sAncestor;
+		}
+	}
+
+	public function GetSearches()
+	{
+		return $this->aSearches;
+	}
+
+	/**
+	 * Limited to the selected classes
+	 */	 	
+	public function GetClassName($sAlias)
+	{
+		if (array_key_exists($sAlias, $this->aSelectedClasses))
+		{
+			return $this->aSelectedClasses[$sAlias];
+		}
+		else
+		{
+			throw new CoreException("Invalid class alias '$sAlias'");
+		}
+	}
+
+	public function GetClass()
+	{
+		return reset($this->aSelectedClasses);
+	}
+
+	public function GetClassAlias()
+	{
+		reset($this->aSelectedClasses);
+		return key($this->aSelectedClasses);
+	}
+
+
+	/**
+	 * Change the class (only subclasses are supported as of now, because the conditions must fit the new class)
+	 * Defaults to the first selected class
+	 * Only the selected classes can be changed
+	 */	 	
+	public function ChangeClass($sNewClass, $sAlias = null)
+	{
+		if (is_null($sAlias))
+		{
+			$sAlias = $this->GetClassAlias();
+		}
+		elseif (!array_key_exists($sAlias, $this->aSelectedClasses))
+		{
+			// discard silently - necessary when recursing (??? copied from DBObjectSearch)
+			return;
+		}
+
+		// 1 - identify the impacted column
+		$iColumn = array_search($sAlias, array_keys($this->aSelectedClasses));
+
+		// 2 - change for each search
+		foreach ($this->aSearches as $oSearch)
+		{
+			$aSearchAliases = array_keys($oSearch->GetSelectedClasses());
+			$sSearchAlias = $aSearchAliases[$iColumn];
+			$oSearch->ChangeClass($sNewClass, $sSearchAlias);
+		}
+
+		// 3 - record the change
+		$this->aSelectedClasses[$sAlias] = $sNewClass;
+	}
+
+	public function GetSelectedClasses()
+	{
+		return $this->aSelectedClasses;
+	}
+
+	public function IsAny()
+	{
+		$bIsAny = true;
+		foreach ($this->aSearches as $oSearch)
+		{
+			if (!$oSearch->IsAny())
+			{
+				$bIsAny = false;
+				break;
+			}
+		}
+		return $bIsAny;
+	}
+
+	public function ResetCondition()
+	{
+		foreach ($this->aSearches as $oSearch)
+		{
+			$oSearch->ResetCondition();
+		}
+	}
+
+	public function MergeConditionExpression($oExpression)
+	{
+		$aAliases = array_keys($this->aSelectedClasses);
+		foreach ($this->aSearches as $iSearchIndex => $oSearch)
+		{
+			$oClonedExpression = $oExpression->DeepClone();
+			if ($iSearchIndex != 0)
+			{
+				foreach (array_keys($oSearch->GetSelectedClasses()) as $iColumn => $sSearchAlias)
+				{
+					$oClonedExpression->RenameAlias($aAliases[$iColumn], $sSearchAlias);
+				}
+			}
+			$oSearch->MergeConditionExpression($oClonedExpression);
+		}
+	}
+
+	public function AddConditionExpression($oExpression)
+	{
+		$aAliases = array_keys($this->aSelectedClasses);
+		foreach ($this->aSearches as $iSearchIndex => $oSearch)
+		{
+			$oClonedExpression = $oExpression->DeepClone();
+			if ($iSearchIndex != 0)
+			{
+				foreach (array_keys($oSearch->GetSelectedClasses()) as $iColumn => $sSearchAlias)
+				{
+					$oClonedExpression->RenameAlias($aAliases[$iColumn], $sSearchAlias);
+				}
+			}
+			$oSearch->AddConditionExpression($oClonedExpression);
+		}
+	}
+
+  	public function AddNameCondition($sName)
+	{
+		foreach ($this->aSearches as $oSearch)
+		{
+			$oSearch->AddNameCondition($sName);
+		}
+	}
+
+	public function AddCondition($sFilterCode, $value, $sOpCode = null)
+	{
+		foreach ($this->aSearches as $oSearch)
+		{
+			$oSearch->AddCondition($sFilterCode, $value, $sOpCode);
+		}
+	}
+
+	/**
+	 * Specify a condition on external keys or link sets
+	 * @param sAttSpec Can be either an attribute code or extkey->[sAttSpec] or linkset->[sAttSpec] and so on, recursively
+	 *                 Example: infra_list->ci_id->location_id->country	 
+	 * @param value The value to match (can be an array => IN(val1, val2...)
+	 * @return void
+	 */
+	public function AddConditionAdvanced($sAttSpec, $value)
+	{
+		foreach ($this->aSearches as $oSearch)
+		{
+			$oSearch->AddConditionAdvanced($sAttSpec, $value);
+		}
+	}
+
+	public function AddCondition_FullText($sFullText)
+	{
+		foreach ($this->aSearches as $oSearch)
+		{
+			$oSearch->AddCondition_FullText($sFullText);
+		}
+	}
+
+	public function AddCondition_PointingTo(DBObjectSearch $oFilter, $sExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS)
+	{
+		foreach ($this->aSearches as $oSearch)
+		{
+			$oSearch->AddCondition_PointingTo($oFilter, $sExtKeyAttCode, $iOperatorCode);
+		}
+	}
+
+	public function AddCondition_ReferencedBy(DBObjectSearch $oFilter, $sForeignExtKeyAttCode)
+	{
+		foreach ($this->aSearches as $oSearch)
+		{
+			$oSearch->AddCondition_ReferencedBy($oFilter, $sForeignExtKeyAttCode);
+		}
+	}
+
+	public function Intersect(DBSearch $oFilter)
+	{
+		$aSearches = array();
+		foreach ($this->aSearches as $oSearch)
+		{
+			$aSearches[] = $oSearch->Intersect($oFilter);
+		}
+		return new DBUnionSearch($aSearches);
+	}
+
+	public function SetInternalParams($aParams)
+	{
+		foreach ($this->aSearches as $oSearch)
+		{
+			$oSearch->SetInternalParams($aParams);
+		}
+	}
+
+	public function GetInternalParams()
+	{
+		$aParams = array();
+		foreach ($this->aSearches as $oSearch)
+		{
+			$aParams = array_merge($oSearch->GetInternalParams(), $aParams);
+		}
+		return $aParams;
+	}
+
+	public function GetQueryParams()
+	{
+		$aParams = array();
+		foreach ($this->aSearches as $oSearch)
+		{
+			$aParams = array_merge($oSearch->GetQueryParams(), $aParams);
+		}
+		return $aParams;
+	}
+
+	public function ListConstantFields()
+	{
+		// Somewhat complex to implement for unions, for a poor benefit
+		return array();
+	}
+
+	/**
+	 * Turn the parameters (:xxx) into scalar values in order to easily
+	 * serialize a search
+	 */
+	public function ApplyParameters($aArgs)
+	{
+		foreach ($this->aSearches as $oSearch)
+		{
+			$oSearch->ApplyParameters($aArgs);
+		}
+	}
+
+	/**
+	 * Overloads for query building
+	 */ 
+	public function ToOQL($bDevelopParams = false, $aContextParams = null)
+	{
+		$aSubQueries = array();
+		foreach ($this->aSearches as $oSearch)
+		{
+			$aSubQueries[] = $oSearch->ToOQL($bDevelopParams, $aContextParams);
+		}
+		$sRet = implode(' UNION ', $aSubQueries);
+		return $sRet;
+	}
+
+	////////////////////////////////////////////////////////////////////////////
+	//
+	// Construction of the SQL queries
+	//
+	////////////////////////////////////////////////////////////////////////////
+
+	public function MakeDeleteQuery($aArgs = array())
+	{
+		throw new Exception('MakeDeleteQuery is not implemented for the unions!');
+	}
+
+	public function MakeUpdateQuery($aValues, $aArgs = array())
+	{
+		throw new Exception('MakeUpdateQuery is not implemented for the unions!');
+	}
+
+	protected function MakeSQLQuery($aAttToLoad, $bGetCount, $aModifierProperties, $aGroupByExpr = null, $aSelectedClasses = null)
+	{
+		if (count($this->aSearches) == 1)
+		{
+			return $this->aSearches[0]->MakeSQLQuery($aAttToLoad, $bGetCount, $aModifierProperties, $aGroupByExpr);
+		}
+
+		$aSQLQueries = array();
+		$aAliases = array_keys($this->aSelectedClasses);
+		foreach ($this->aSearches as $iSearch => $oSearch)
+		{
+			$aSearchAliases = array_keys($oSearch->GetSelectedClasses());
+
+			// The selected classes from the query build perspective are the lowest common ancestors amongst the various queries
+			// (used when it comes to determine which attributes must be selected)
+			$aSearchSelectedClasses = array();
+			foreach ($aSearchAliases as $iColumn => $sSearchAlias)
+			{
+				$sAlias = $aAliases[$iColumn];
+				$aSearchSelectedClasses[$sSearchAlias] = $this->aSelectedClasses[$sAlias];
+			}
+
+			if (is_null($aAttToLoad))
+			{
+				$aQueryAttToLoad = null;
+			}
+			else
+			{
+					// (Eventually) Transform the aliases
+				$aQueryAttToLoad = array();
+				foreach ($aAttToLoad as $sAlias => $aAttributes)
+				{
+					$iColumn = array_search($sAlias, $aAliases);
+					$sQueryAlias = ($iColumn === false) ? $sAlias : $aSearchAliases[$iColumn];
+					$aQueryAttToLoad[$sQueryAlias] = $aAttributes;
+				}
+			}
+
+			if (is_null($aGroupByExpr))
+			{
+				$aQueryGroupByExpr = null;
+			}
+			else
+			{
+				// Clone (and eventually transform) the group by expressions
+				$aQueryGroupByExpr = array();
+				$aTranslationData = array();
+				$aQueryColumns = array_keys($oSearch->GetSelectedClasses());
+				foreach ($aAliases as $iColumn => $sAlias)
+				{
+					$sQueryAlias = $aQueryColumns[$iColumn];
+					$aTranslationData[$sAlias]['*'] = $sQueryAlias;
+					$aQueryGroupByExpr[$sAlias.'id'] = new FieldExpression('id', $sQueryAlias);
+				}
+				foreach ($aGroupByExpr as $sExpressionAlias => $oExpression)
+				{
+					$aQueryGroupByExpr[$sExpressionAlias] = $oExpression->Translate($aTranslationData, false, false);
+				}
+			}
+			$oSubQuery = $oSearch->MakeSQLQuery($aQueryAttToLoad, false, $aModifierProperties, $aQueryGroupByExpr, $aSearchSelectedClasses);
+			$aSQLQueries[] = $oSubQuery;
+		}
+
+		$oSQLQuery = new SQLUnionQuery($aSQLQueries, $aGroupByExpr);
+		//MyHelpers::var_dump_html($oSQLQuery, true);
+		//MyHelpers::var_dump_html($oSQLQuery->RenderSelect(), true);
+		if (self::$m_bDebugQuery) $oSQLQuery->DisplayHtml();
+		return $oSQLQuery;
+	}
+}

+ 69 - 6
core/expression.class.inc.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //
 //   This file is part of iTop.
 //   This file is part of iTop.
 //
 //
@@ -20,7 +20,7 @@
 /**
 /**
  * General definition of an expression tree (could be OQL, SQL or whatever) 
  * General definition of an expression tree (could be OQL, SQL or whatever) 
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -30,6 +30,14 @@ class MissingQueryArgument extends CoreException
 
 
 abstract class Expression
 abstract class Expression
 {
 {
+	/**
+	 * Perform a deep clone (as opposed to "clone" which does copy a reference to the underlying objects)
+	 **/	 	
+	public function DeepClone()
+	{
+		return unserialize(serialize($this));
+	}
+
 	// recursive translation of identifiers
 	// recursive translation of identifiers
 	abstract public function GetUnresolvedFields($sAlias, &$aUnresolved);
 	abstract public function GetUnresolvedFields($sAlias, &$aUnresolved);
 	abstract public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true);
 	abstract public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true);
@@ -95,11 +103,12 @@ abstract class Expression
 	}
 	}
 	
 	
 	abstract public function RenameParam($sOldName, $sNewName);
 	abstract public function RenameParam($sOldName, $sNewName);
+	abstract public function RenameAlias($sOldName, $sNewName);
 
 
 	/**
 	/**
 	 * Make the most relevant label, given the value of the expression
 	 * Make the most relevant label, given the value of the expression
 	 * 	 
 	 * 	 
-	 * @param DBObjectSearch oFilter The context in which this expression has been used	 	
+	 * @param DBSearch oFilter The context in which this expression has been used	 	
 	 * @param string sValue The value returned by the query, for this expression	 	
 	 * @param string sValue The value returned by the query, for this expression	 	
 	 * @param string sDefault The default value if no relevant label could be computed	 	
 	 * @param string sDefault The default value if no relevant label could be computed	 	
 	 * @return The label
 	 * @return The label
@@ -161,6 +170,11 @@ class SQLExpression extends Expression
 	{
 	{
 		// Do nothing, since there is nothing to rename
 		// Do nothing, since there is nothing to rename
 	}
 	}
+
+	public function RenameAlias($sOldName, $sNewName)
+	{
+		// Do nothing, since there is nothing to rename
+	}
 }
 }
 
 
 
 
@@ -313,6 +327,12 @@ class BinaryExpression extends Expression
 		$this->GetLeftExpr()->RenameParam($sOldName, $sNewName);
 		$this->GetLeftExpr()->RenameParam($sOldName, $sNewName);
 		$this->GetRightExpr()->RenameParam($sOldName, $sNewName);
 		$this->GetRightExpr()->RenameParam($sOldName, $sNewName);
 	}
 	}
+
+	public function RenameAlias($sOldName, $sNewName)
+	{
+		$this->GetLeftExpr()->RenameAlias($sOldName, $sNewName);
+		$this->GetRightExpr()->RenameAlias($sOldName, $sNewName);
+	}
 }
 }
 
 
 
 
@@ -374,6 +394,11 @@ class UnaryExpression extends Expression
 		// Do nothing
 		// Do nothing
 		// really ? what about :param{$iParamIndex} ??
 		// really ? what about :param{$iParamIndex} ??
 	}
 	}
+
+	public function RenameAlias($sOldName, $sNewName)
+	{
+		// Do nothing
+	}
 }
 }
 
 
 class ScalarExpression extends UnaryExpression
 class ScalarExpression extends UnaryExpression
@@ -526,7 +551,7 @@ class FieldExpression extends UnaryExpression
 	/**
 	/**
 	 * Make the most relevant label, given the value of the expression
 	 * Make the most relevant label, given the value of the expression
 	 * 	 
 	 * 	 
-	 * @param DBObjectSearch oFilter The context in which this expression has been used	 	
+	 * @param DBSearch oFilter The context in which this expression has been used	 	
 	 * @param string sValue The value returned by the query, for this expression	 	
 	 * @param string sValue The value returned by the query, for this expression	 	
 	 * @param string sDefault The default value if no relevant label could be computed	 	
 	 * @param string sDefault The default value if no relevant label could be computed	 	
 	 * @return The label
 	 * @return The label
@@ -568,6 +593,14 @@ class FieldExpression extends UnaryExpression
 		}
 		}
 		return $sRes;
 		return $sRes;
 	}
 	}
+
+	public function RenameAlias($sOldName, $sNewName)
+	{
+		if ($this->m_sParent == $sOldName)
+		{
+			$this->m_sParent = $sNewName;
+		}
+	}
 }
 }
 
 
 // Has been resolved into an SQL expression
 // Has been resolved into an SQL expression
@@ -792,6 +825,15 @@ class ListExpression extends Expression
 			$this->m_aExpressions[$key] = $oExpr->RenameParam($sOldName, $sNewName);
 			$this->m_aExpressions[$key] = $oExpr->RenameParam($sOldName, $sNewName);
 		}
 		}
 	}	
 	}	
+
+	public function RenameAlias($sOldName, $sNewName)
+	{
+		$aRes = array();
+		foreach ($this->m_aExpressions as $key => $oExpr)
+		{
+			$oExpr->RenameAlias($sOldName, $sNewName);
+		}
+	}	
 }
 }
 
 
 
 
@@ -826,7 +868,7 @@ class FunctionExpression extends Expression
 	public function Render(&$aArgs = null, $bRetrofitParams = false)
 	public function Render(&$aArgs = null, $bRetrofitParams = false)
 	{
 	{
 		$aRes = array();
 		$aRes = array();
-		foreach ($this->m_aArgs as $oExpr)
+		foreach ($this->m_aArgs as $iPos => $oExpr)
 		{
 		{
 			$aRes[] = $oExpr->Render($aArgs, $bRetrofitParams);
 			$aRes[] = $oExpr->Render($aArgs, $bRetrofitParams);
 		}
 		}
@@ -903,10 +945,18 @@ class FunctionExpression extends Expression
 		}
 		}
 	}
 	}
 
 
+	public function RenameAlias($sOldName, $sNewName)
+	{
+		foreach ($this->m_aArgs as $key => $oExpr)
+		{
+			$oExpr->RenameAlias($sOldName, $sNewName);
+		}
+	}
+
 	/**
 	/**
 	 * Make the most relevant label, given the value of the expression
 	 * Make the most relevant label, given the value of the expression
 	 * 	 
 	 * 	 
-	 * @param DBObjectSearch oFilter The context in which this expression has been used	 	
+	 * @param DBSearch oFilter The context in which this expression has been used	 	
 	 * @param string sValue The value returned by the query, for this expression	 	
 	 * @param string sValue The value returned by the query, for this expression	 	
 	 * @param string sDefault The default value if no relevant label could be computed	 	
 	 * @param string sDefault The default value if no relevant label could be computed	 	
 	 * @return The label
 	 * @return The label
@@ -1048,6 +1098,11 @@ class IntervalExpression extends Expression
 	{
 	{
 		$this->m_oValue->RenameParam($sOldName, $sNewName);
 		$this->m_oValue->RenameParam($sOldName, $sNewName);
 	}	
 	}	
+
+	public function RenameAlias($sOldName, $sNewName)
+	{
+		$this->m_oValue->RenameAlias($sOldName, $sNewName);
+	}	
 }
 }
 
 
 class CharConcatExpression extends Expression
 class CharConcatExpression extends Expression
@@ -1152,6 +1207,14 @@ class CharConcatExpression extends Expression
 			$this->m_aExpressions[$key] = $oExpr->RenameParam($sOldName, $sNewName);
 			$this->m_aExpressions[$key] = $oExpr->RenameParam($sOldName, $sNewName);
 		}
 		}
 	}	
 	}	
+
+	public function RenameAlias($sOldName, $sNewName)
+	{
+		foreach ($this->m_aExpressions as $key => $oExpr)
+		{
+			$oExpr->RenameAlias($sOldName, $sNewName);
+		}
+	}	
 }
 }
 
 
 
 

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 53 - 1085
core/metamodel.class.php


+ 79 - 73
core/oql/oql-lexer.php

@@ -1,6 +1,6 @@
 <?php
 <?php
 
 
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //
 //   This file is part of iTop.
 //   This file is part of iTop.
 //
 //
@@ -21,7 +21,7 @@
 /**
 /**
  * OQL syntax analyzer, to be used prior to run the lexical analyzer
  * OQL syntax analyzer, to be used prior to run the lexical analyzer
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -108,6 +108,7 @@ class OQLLexerRaw
         do {
         do {
             $rules = array(
             $rules = array(
                 '/\G[ \t\n\r]+/ ',
                 '/\G[ \t\n\r]+/ ',
+                '/\GUNION/ ',
                 '/\GSELECT/ ',
                 '/\GSELECT/ ',
                 '/\GFROM/ ',
                 '/\GFROM/ ',
                 '/\GAS/ ',
                 '/\GAS/ ',
@@ -280,361 +281,366 @@ class OQLLexerRaw
     function yy_r1_1($yy_subpatterns)
     function yy_r1_1($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::SELECT;
+	$this->token = OQLParser::UNION;
     }
     }
     function yy_r1_2($yy_subpatterns)
     function yy_r1_2($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::FROM;
+	$this->token = OQLParser::SELECT;
     }
     }
     function yy_r1_3($yy_subpatterns)
     function yy_r1_3($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::AS_ALIAS;
+	$this->token = OQLParser::FROM;
     }
     }
     function yy_r1_4($yy_subpatterns)
     function yy_r1_4($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::WHERE;
+	$this->token = OQLParser::AS_ALIAS;
     }
     }
     function yy_r1_5($yy_subpatterns)
     function yy_r1_5($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::JOIN;
+	$this->token = OQLParser::WHERE;
     }
     }
     function yy_r1_6($yy_subpatterns)
     function yy_r1_6($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::ON;
+	$this->token = OQLParser::JOIN;
     }
     }
     function yy_r1_7($yy_subpatterns)
     function yy_r1_7($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::MATH_DIV;
+	$this->token = OQLParser::ON;
     }
     }
     function yy_r1_8($yy_subpatterns)
     function yy_r1_8($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::MATH_MULT;
+	$this->token = OQLParser::MATH_DIV;
     }
     }
     function yy_r1_9($yy_subpatterns)
     function yy_r1_9($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::MATH_PLUS;
+	$this->token = OQLParser::MATH_MULT;
     }
     }
     function yy_r1_10($yy_subpatterns)
     function yy_r1_10($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::MATH_MINUS;
+	$this->token = OQLParser::MATH_PLUS;
     }
     }
     function yy_r1_11($yy_subpatterns)
     function yy_r1_11($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::LOG_AND;
+	$this->token = OQLParser::MATH_MINUS;
     }
     }
     function yy_r1_12($yy_subpatterns)
     function yy_r1_12($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::LOG_OR;
+	$this->token = OQLParser::LOG_AND;
     }
     }
     function yy_r1_13($yy_subpatterns)
     function yy_r1_13($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::BITWISE_OR;
+	$this->token = OQLParser::LOG_OR;
     }
     }
     function yy_r1_14($yy_subpatterns)
     function yy_r1_14($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::BITWISE_AND;
+	$this->token = OQLParser::BITWISE_OR;
     }
     }
     function yy_r1_15($yy_subpatterns)
     function yy_r1_15($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::BITWISE_XOR;
+	$this->token = OQLParser::BITWISE_AND;
     }
     }
     function yy_r1_16($yy_subpatterns)
     function yy_r1_16($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::BITWISE_LEFT_SHIFT;
+	$this->token = OQLParser::BITWISE_XOR;
     }
     }
     function yy_r1_17($yy_subpatterns)
     function yy_r1_17($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::BITWISE_RIGHT_SHIFT;
+	$this->token = OQLParser::BITWISE_LEFT_SHIFT;
     }
     }
     function yy_r1_18($yy_subpatterns)
     function yy_r1_18($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::COMA;
+	$this->token = OQLParser::BITWISE_RIGHT_SHIFT;
     }
     }
     function yy_r1_19($yy_subpatterns)
     function yy_r1_19($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::PAR_OPEN;
+	$this->token = OQLParser::COMA;
     }
     }
     function yy_r1_20($yy_subpatterns)
     function yy_r1_20($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::PAR_CLOSE;
+	$this->token = OQLParser::PAR_OPEN;
     }
     }
     function yy_r1_21($yy_subpatterns)
     function yy_r1_21($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::REGEXP;
+	$this->token = OQLParser::PAR_CLOSE;
     }
     }
     function yy_r1_22($yy_subpatterns)
     function yy_r1_22($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::EQ;
+	$this->token = OQLParser::REGEXP;
     }
     }
     function yy_r1_23($yy_subpatterns)
     function yy_r1_23($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::NOT_EQ;
+	$this->token = OQLParser::EQ;
     }
     }
     function yy_r1_24($yy_subpatterns)
     function yy_r1_24($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::GT;
+	$this->token = OQLParser::NOT_EQ;
     }
     }
     function yy_r1_25($yy_subpatterns)
     function yy_r1_25($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::LT;
+	$this->token = OQLParser::GT;
     }
     }
     function yy_r1_26($yy_subpatterns)
     function yy_r1_26($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::GE;
+	$this->token = OQLParser::LT;
     }
     }
     function yy_r1_27($yy_subpatterns)
     function yy_r1_27($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::LE;
+	$this->token = OQLParser::GE;
     }
     }
     function yy_r1_28($yy_subpatterns)
     function yy_r1_28($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::LIKE;
+	$this->token = OQLParser::LE;
     }
     }
     function yy_r1_29($yy_subpatterns)
     function yy_r1_29($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::NOT_LIKE;
+	$this->token = OQLParser::LIKE;
     }
     }
     function yy_r1_30($yy_subpatterns)
     function yy_r1_30($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::IN;
+	$this->token = OQLParser::NOT_LIKE;
     }
     }
     function yy_r1_31($yy_subpatterns)
     function yy_r1_31($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::NOT_IN;
+	$this->token = OQLParser::IN;
     }
     }
     function yy_r1_32($yy_subpatterns)
     function yy_r1_32($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::INTERVAL;
+	$this->token = OQLParser::NOT_IN;
     }
     }
     function yy_r1_33($yy_subpatterns)
     function yy_r1_33($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_IF;
+	$this->token = OQLParser::INTERVAL;
     }
     }
     function yy_r1_34($yy_subpatterns)
     function yy_r1_34($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_ELT;
+	$this->token = OQLParser::F_IF;
     }
     }
     function yy_r1_35($yy_subpatterns)
     function yy_r1_35($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_COALESCE;
+	$this->token = OQLParser::F_ELT;
     }
     }
     function yy_r1_36($yy_subpatterns)
     function yy_r1_36($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_ISNULL;
+	$this->token = OQLParser::F_COALESCE;
     }
     }
     function yy_r1_37($yy_subpatterns)
     function yy_r1_37($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_CONCAT;
+	$this->token = OQLParser::F_ISNULL;
     }
     }
     function yy_r1_38($yy_subpatterns)
     function yy_r1_38($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_SUBSTR;
+	$this->token = OQLParser::F_CONCAT;
     }
     }
     function yy_r1_39($yy_subpatterns)
     function yy_r1_39($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_TRIM;
+	$this->token = OQLParser::F_SUBSTR;
     }
     }
     function yy_r1_40($yy_subpatterns)
     function yy_r1_40($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_DATE;
+	$this->token = OQLParser::F_TRIM;
     }
     }
     function yy_r1_41($yy_subpatterns)
     function yy_r1_41($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_DATE_FORMAT;
+	$this->token = OQLParser::F_DATE;
     }
     }
     function yy_r1_42($yy_subpatterns)
     function yy_r1_42($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_CURRENT_DATE;
+	$this->token = OQLParser::F_DATE_FORMAT;
     }
     }
     function yy_r1_43($yy_subpatterns)
     function yy_r1_43($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_NOW;
+	$this->token = OQLParser::F_CURRENT_DATE;
     }
     }
     function yy_r1_44($yy_subpatterns)
     function yy_r1_44($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_TIME;
+	$this->token = OQLParser::F_NOW;
     }
     }
     function yy_r1_45($yy_subpatterns)
     function yy_r1_45($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_TO_DAYS;
+	$this->token = OQLParser::F_TIME;
     }
     }
     function yy_r1_46($yy_subpatterns)
     function yy_r1_46($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_FROM_DAYS;
+	$this->token = OQLParser::F_TO_DAYS;
     }
     }
     function yy_r1_47($yy_subpatterns)
     function yy_r1_47($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_YEAR;
+	$this->token = OQLParser::F_FROM_DAYS;
     }
     }
     function yy_r1_48($yy_subpatterns)
     function yy_r1_48($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_MONTH;
+	$this->token = OQLParser::F_YEAR;
     }
     }
     function yy_r1_49($yy_subpatterns)
     function yy_r1_49($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_DAY;
+	$this->token = OQLParser::F_MONTH;
     }
     }
     function yy_r1_50($yy_subpatterns)
     function yy_r1_50($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_HOUR;
+	$this->token = OQLParser::F_DAY;
     }
     }
     function yy_r1_51($yy_subpatterns)
     function yy_r1_51($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_MINUTE;
+	$this->token = OQLParser::F_HOUR;
     }
     }
     function yy_r1_52($yy_subpatterns)
     function yy_r1_52($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_SECOND;
+	$this->token = OQLParser::F_MINUTE;
     }
     }
     function yy_r1_53($yy_subpatterns)
     function yy_r1_53($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_DATE_ADD;
+	$this->token = OQLParser::F_SECOND;
     }
     }
     function yy_r1_54($yy_subpatterns)
     function yy_r1_54($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_DATE_SUB;
+	$this->token = OQLParser::F_DATE_ADD;
     }
     }
     function yy_r1_55($yy_subpatterns)
     function yy_r1_55($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_ROUND;
+	$this->token = OQLParser::F_DATE_SUB;
     }
     }
     function yy_r1_56($yy_subpatterns)
     function yy_r1_56($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_FLOOR;
+	$this->token = OQLParser::F_ROUND;
     }
     }
     function yy_r1_57($yy_subpatterns)
     function yy_r1_57($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_INET_ATON;
+	$this->token = OQLParser::F_FLOOR;
     }
     }
     function yy_r1_58($yy_subpatterns)
     function yy_r1_58($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::F_INET_NTOA;
+	$this->token = OQLParser::F_INET_ATON;
     }
     }
     function yy_r1_59($yy_subpatterns)
     function yy_r1_59($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::BELOW;
+	$this->token = OQLParser::F_INET_NTOA;
     }
     }
     function yy_r1_60($yy_subpatterns)
     function yy_r1_60($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::BELOW_STRICT;
+	$this->token = OQLParser::BELOW;
     }
     }
     function yy_r1_61($yy_subpatterns)
     function yy_r1_61($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::NOT_BELOW;
+	$this->token = OQLParser::BELOW_STRICT;
     }
     }
     function yy_r1_62($yy_subpatterns)
     function yy_r1_62($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::NOT_BELOW_STRICT;
+	$this->token = OQLParser::NOT_BELOW;
     }
     }
     function yy_r1_63($yy_subpatterns)
     function yy_r1_63($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::ABOVE;
+	$this->token = OQLParser::NOT_BELOW_STRICT;
     }
     }
     function yy_r1_64($yy_subpatterns)
     function yy_r1_64($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::ABOVE_STRICT;
+	$this->token = OQLParser::ABOVE;
     }
     }
     function yy_r1_65($yy_subpatterns)
     function yy_r1_65($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::NOT_ABOVE;
+	$this->token = OQLParser::ABOVE_STRICT;
     }
     }
     function yy_r1_66($yy_subpatterns)
     function yy_r1_66($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::NOT_ABOVE_STRICT;
+	$this->token = OQLParser::NOT_ABOVE;
     }
     }
     function yy_r1_67($yy_subpatterns)
     function yy_r1_67($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::HEXVAL;
+	$this->token = OQLParser::NOT_ABOVE_STRICT;
     }
     }
     function yy_r1_68($yy_subpatterns)
     function yy_r1_68($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::NUMVAL;
+	$this->token = OQLParser::HEXVAL;
     }
     }
     function yy_r1_69($yy_subpatterns)
     function yy_r1_69($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::STRVAL;
+	$this->token = OQLParser::NUMVAL;
     }
     }
     function yy_r1_70($yy_subpatterns)
     function yy_r1_70($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::NAME;
+	$this->token = OQLParser::STRVAL;
     }
     }
     function yy_r1_71($yy_subpatterns)
     function yy_r1_71($yy_subpatterns)
     {
     {
 
 
-	$this->token = OQLParser::VARNAME;
+	$this->token = OQLParser::NAME;
     }
     }
     function yy_r1_72($yy_subpatterns)
     function yy_r1_72($yy_subpatterns)
     {
     {
 
 
+	$this->token = OQLParser::VARNAME;
+    }
+    function yy_r1_73($yy_subpatterns)
+    {
+
 	$this->token = OQLParser::DOT;
 	$this->token = OQLParser::DOT;
     }
     }
 
 

+ 6 - 2
core/oql/oql-lexer.plex

@@ -1,6 +1,6 @@
 <?php
 <?php
 
 
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //
 //   This file is part of iTop.
 //   This file is part of iTop.
 //
 //
@@ -21,7 +21,7 @@
 /**
 /**
  * OQL syntax analyzer, to be used prior to run the lexical analyzer
  * OQL syntax analyzer, to be used prior to run the lexical analyzer
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -80,6 +80,7 @@ class OQLLexerRaw
 %line    $this->line
 %line    $this->line
 %matchlongest 1
 %matchlongest 1
 whitespace = /[ \t\n\r]+/
 whitespace = /[ \t\n\r]+/
+union      = "UNION"
 select     = "SELECT"
 select     = "SELECT"
 from       = "FROM"
 from       = "FROM"
 as_alias   = "AS"
 as_alias   = "AS"
@@ -176,6 +177,9 @@ dot       = "."
 whitespace {
 whitespace {
 	return false;
 	return false;
 }
 }
+union {
+	$this->token = OQLParser::UNION;
+}
 select {
 select {
 	$this->token = OQLParser::SELECT;
 	$this->token = OQLParser::SELECT;
 }
 }

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 544 - 529
core/oql/oql-parser.php


+ 8 - 0
core/oql/oql-parser.y

@@ -26,9 +26,17 @@ TODO : solve the 2 remaining shift-reduce conflicts (JOIN)
 throw new OQLParserException($this->m_sSourceQuery, $this->m_iLine, $this->m_iCol, $this->tokenName($yymajor), $TOKEN);
 throw new OQLParserException($this->m_sSourceQuery, $this->m_iLine, $this->m_iCol, $this->tokenName($yymajor), $TOKEN);
 }
 }
 
 
+result ::= union(X). { $this->my_result = X; }
 result ::= query(X). { $this->my_result = X; }
 result ::= query(X). { $this->my_result = X; }
 result ::= condition(X). { $this->my_result = X; }
 result ::= condition(X). { $this->my_result = X; }
 
 
+union(A) ::= query(X) UNION query(Y). {
+	A = new OqlUnionQuery(X, Y);
+}
+union(A) ::= query(X) UNION union(Y). {
+	A = new OqlUnionQuery(X, Y);
+}
+
 query(A) ::= SELECT class_name(X) join_statement(J) where_statement(W). {
 query(A) ::= SELECT class_name(X) join_statement(J) where_statement(W). {
 	A = new OqlObjectQuery(X, X, W, J, array(X));
 	A = new OqlObjectQuery(X, X, W, J, array(X));
 }
 }

+ 4 - 4
core/oql/oqlinterpreter.class.inc.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //
 //   This file is part of iTop.
 //   This file is part of iTop.
 //
 //
@@ -20,7 +20,7 @@
 /**
 /**
  * Wrapper to execute the parser, lexical analyzer and normalization of an OQL query
  * Wrapper to execute the parser, lexical analyzer and normalization of an OQL query
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -83,10 +83,10 @@ class OqlInterpreter
 		return $res;
 		return $res;
 	}
 	}
 
 
-	public function ParseObjectQuery()
+	public function ParseQuery()
 	{
 	{
 		$oRes = $this->Parse();
 		$oRes = $this->Parse();
-		if (!$oRes instanceof OqlObjectQuery)
+		if (!$oRes instanceof OqlQuery)
 		{
 		{
 			throw new OQLException('Expecting an OQL query', $this->m_sQuery, 0, 0, get_class($oRes));
 			throw new OQLException('Expecting an OQL query', $this->m_sQuery, 0, 0, get_class($oRes));
 		}
 		}

+ 148 - 19
core/oql/oqlquery.class.inc.php

@@ -20,7 +20,7 @@
 /**
 /**
  * Classes defined for lexical analyze (see oql-parser.y)
  * Classes defined for lexical analyze (see oql-parser.y)
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -266,23 +266,18 @@ class IntervalOqlExpression extends IntervalExpression implements CheckableExpre
 
 
 abstract class OqlQuery
 abstract class OqlQuery
 {
 {
-	protected $m_aJoins; // array of OqlJoinSpec
-	protected $m_oCondition; // condition tree (expressions)
-
-	public function __construct($oCondition = null, $aJoins = null)
+	public function __construct()
 	{
 	{
-		$this->m_aJoins = $aJoins;
-		$this->m_oCondition = $oCondition;
 	}
 	}
 
 
-	public function GetJoins()
-	{
-		return $this->m_aJoins;
-	}
-	public function GetCondition()
-	{
-		return $this->m_oCondition;
-	}
+	/**
+	 * Check the validity of the expression with regard to the data model
+	 * and the query in which it is used
+	 * 	 	 
+	 * @param ModelReflection $oModelReflection MetaModel to consider	 	
+	 * @throws OqlNormalizeException
+	 */	 	
+	abstract public function Check(ModelReflection $oModelReflection, $sSourceQuery);
 }
 }
 
 
 class OqlObjectQuery extends OqlQuery
 class OqlObjectQuery extends OqlQuery
@@ -290,13 +285,18 @@ class OqlObjectQuery extends OqlQuery
 	protected $m_aSelect; // array of selected classes
 	protected $m_aSelect; // array of selected classes
 	protected $m_oClass;
 	protected $m_oClass;
 	protected $m_oClassAlias;
 	protected $m_oClassAlias;
+	protected $m_aJoins; // array of OqlJoinSpec
+	protected $m_oCondition; // condition tree (expressions)
 
 
 	public function __construct($oClass, $oClassAlias, $oCondition = null, $aJoins = null, $aSelect = null)
 	public function __construct($oClass, $oClassAlias, $oCondition = null, $aJoins = null, $aSelect = null)
 	{
 	{
 		$this->m_aSelect = $aSelect;
 		$this->m_aSelect = $aSelect;
 		$this->m_oClass = $oClass;
 		$this->m_oClass = $oClass;
 		$this->m_oClassAlias = $oClassAlias;
 		$this->m_oClassAlias = $oClassAlias;
-		parent::__construct($oCondition, $aJoins);
+		$this->m_aJoins = $aJoins;
+		$this->m_oCondition = $oCondition;
+
+		parent::__construct();
 	}
 	}
 
 
 	public function GetSelectedClasses()
 	public function GetSelectedClasses()
@@ -321,6 +321,15 @@ class OqlObjectQuery extends OqlQuery
 		return $this->m_oClassAlias;
 		return $this->m_oClassAlias;
 	}
 	}
 
 
+	public function GetJoins()
+	{
+		return $this->m_aJoins;
+	}
+	public function GetCondition()
+	{
+		return $this->m_oCondition;
+	}
+
 	/**
 	/**
 	 * Recursively check the validity of the expression with regard to the data model
 	 * Recursively check the validity of the expression with regard to the data model
 	 * and the query in which it is used
 	 * and the query in which it is used
@@ -459,7 +468,6 @@ class OqlObjectQuery extends OqlQuery
 
 
 		// Check the select information
 		// Check the select information
 		//
 		//
-		$aSelected = array();
 		foreach ($this->GetSelectedClasses() as $oClassDetails)
 		foreach ($this->GetSelectedClasses() as $oClassDetails)
 		{
 		{
 			$sClassToSelect = $oClassDetails->GetValue();
 			$sClassToSelect = $oClassDetails->GetValue();
@@ -467,7 +475,6 @@ class OqlObjectQuery extends OqlQuery
 			{
 			{
 				throw new OqlNormalizeException('Unknown class [alias]', $sSourceQuery, $oClassDetails, array_keys($aAliases));
 				throw new OqlNormalizeException('Unknown class [alias]', $sSourceQuery, $oClassDetails, array_keys($aAliases));
 			}
 			}
-			$aSelected[$sClassToSelect] = $aAliases[$sClassToSelect];
 		}
 		}
 
 
 		// Check the condition tree
 		// Check the condition tree
@@ -477,6 +484,128 @@ class OqlObjectQuery extends OqlQuery
 			$this->m_oCondition->Check($oModelReflection, $aAliases, $sSourceQuery);
 			$this->m_oCondition->Check($oModelReflection, $aAliases, $sSourceQuery);
 		}
 		}
 	}
 	}
+
+	/**
+	 * Make the relevant DBSearch instance (FromOQL)
+	 */	 	
+	public function ToDBSearch($sQuery)
+	{
+		$sClass = $this->GetClass();
+		$sClassAlias = $this->GetClassAlias();
+
+		$oSearch = new DBObjectSearch($sClass, $sClassAlias);
+		$oSearch->InitFromOqlQuery($this, $sQuery);
+		return $oSearch;
+	}
 }
 }
 
 
-?>
+class OqlUnionQuery extends OqlQuery
+{
+	protected $aQueries;
+
+	public function __construct(OqlObjectQuery $oLeftQuery, OqlQuery $oRightQueryOrUnion)
+	{
+		$this->aQueries[] = $oLeftQuery;
+		if ($oRightQueryOrUnion instanceof OqlUnionQuery)
+		{
+			foreach ($oRightQueryOrUnion->GetQueries() as $oSingleQuery)
+			{
+				$this->aQueries[] = $oSingleQuery;
+			}
+		}
+		else
+		{
+			$this->aQueries[] = $oRightQueryOrUnion;
+		}
+	}
+	
+	public function GetQueries()
+	{
+		return $this->aQueries;
+	}
+
+	/**
+	 * Check the validity of the expression with regard to the data model
+	 * and the query in which it is used
+	 * 	 	 
+	 * @param ModelReflection $oModelReflection MetaModel to consider	 	
+	 * @throws OqlNormalizeException
+	 */	 	
+	public function Check(ModelReflection $oModelReflection, $sSourceQuery)
+	{
+		$aColumnToClasses = array();
+		foreach ($this->aQueries as $iQuery => $oQuery)
+		{
+			$oQuery->Check($oModelReflection, $sSourceQuery);
+
+			$aAliasToClass = array($oQuery->GetClassAlias() => $oQuery->GetClass());
+			$aJoinSpecs = $oQuery->GetJoins();
+			if (is_array($aJoinSpecs))
+			{
+				foreach ($aJoinSpecs as $oJoinSpec)
+				{
+					$aAliasToClass[$oJoinSpec->GetClassAlias()] = $oJoinSpec->GetClass();
+				}
+			}
+
+			$aSelectedClasses = $oQuery->GetSelectedClasses();
+			if ($iQuery != 0)
+			{
+				if (count($aSelectedClasses) < count($aColumnToClasses))
+				{
+					$oLastClass = end($aSelectedClasses);
+					throw new OqlNormalizeException('Too few selected classes in the subquery', $sSourceQuery, $oLastClass);
+				}
+				if (count($aSelectedClasses) > count($aColumnToClasses))
+				{
+					$oLastClass = end($aSelectedClasses);
+					throw new OqlNormalizeException('Too many selected classes in the subquery', $sSourceQuery, $oLastClass);
+				}
+			}
+			foreach ($aSelectedClasses as $iColumn => $oClassDetails)
+			{
+				$sAlias = $oClassDetails->GetValue();
+				$sClass = $aAliasToClass[$sAlias];
+				$aColumnToClasses[$iColumn][] = array(
+					'alias' => $sAlias,
+					'class' => $sClass,
+					'class_name' => $oClassDetails,
+				);
+			}
+		}
+		foreach ($aColumnToClasses as $iColumn => $aClasses)
+		{
+			foreach ($aClasses as $iQuery => $aData)
+			{
+				if ($iQuery == 0)
+				{
+					// Establish the reference
+					$sRootClass = MetaModel::GetRootClass($aData['class']);
+				}
+				else
+				{
+					if (MetaModel::GetRootClass($aData['class']) != $sRootClass)
+					{
+						$aSubclasses = MetaModel::EnumChildClasses($sRootClass, ENUM_CHILD_CLASSES_ALL);
+						throw new OqlNormalizeException('Incompatible classes: could not find a common ancestor', $sSourceQuery, $aData['class_name'], $aSubclasses);
+					}
+				}
+			}
+		}
+	}
+
+	/**
+	 * Make the relevant DBSearch instance (FromOQL)
+	 */	 	
+	public function ToDBSearch($sQuery)
+	{
+		$aSearches = array();
+		foreach ($this->aQueries as $oQuery)
+		{
+			$aSearches[] = $oQuery->ToDBSearch($sQuery);
+		}
+
+		$oSearch = new DBUnionSearch($aSearches);
+		return $oSearch;
+	}
+}

+ 18 - 5
core/querybuildercontext.class.inc.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //
 //   This file is part of iTop.
 //   This file is part of iTop.
 //
 //
@@ -19,7 +19,7 @@
 /**
 /**
  * Associated with the metamodel -> MakeQuery/MakeQuerySingleTable
  * Associated with the metamodel -> MakeQuery/MakeQuerySingleTable
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -29,10 +29,11 @@ class QueryBuilderContext
 	protected $m_aClassAliases;
 	protected $m_aClassAliases;
 	protected $m_aTableAliases;
 	protected $m_aTableAliases;
 	protected $m_aModifierProperties;
 	protected $m_aModifierProperties;
+	protected $m_aSelectedClasses;
 
 
 	public $m_oQBExpressions;
 	public $m_oQBExpressions;
 
 
-	public function __construct($oFilter, $aModifierProperties, $aGroupByExpr = null)
+	public function __construct($oFilter, $aModifierProperties, $aGroupByExpr = null, $aSelectedClasses = null)
 	{
 	{
 		$this->m_oRootFilter = $oFilter;
 		$this->m_oRootFilter = $oFilter;
 		$this->m_oQBExpressions = new QueryBuilderExpressions($oFilter, $aGroupByExpr);
 		$this->m_oQBExpressions = new QueryBuilderExpressions($oFilter, $aGroupByExpr);
@@ -41,6 +42,15 @@ class QueryBuilderContext
 		$this->m_aTableAliases = array();
 		$this->m_aTableAliases = array();
 
 
 		$this->m_aModifierProperties = $aModifierProperties;
 		$this->m_aModifierProperties = $aModifierProperties;
+		if (is_null($aSelectedClasses))
+		{
+			$this->m_aSelectedClasses = $oFilter->GetSelectedClasses();
+		}
+		else
+		{
+			// For the unions, the selected classes can be upper in the hierarchy (lowest common ancestor)
+			$this->m_aSelectedClasses = $aSelectedClasses;
+		}
 	}
 	}
 
 
 	public function GetRootFilter()
 	public function GetRootFilter()
@@ -69,6 +79,9 @@ class QueryBuilderContext
 			return array();
 			return array();
 		}
 		}
 	}
 	}
-}
 
 
-?>
+	public function GetSelectedClass($sAlias)
+	{
+		return $this->m_aSelectedClasses[$sAlias];
+	}
+}

+ 587 - 0
core/sqlobjectquery.class.inc.php

@@ -0,0 +1,587 @@
+<?php
+// Copyright (C) 2015 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify	
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+
+/**
+ * SQLObjectQuery
+ * build a mySQL compatible SQL query
+ *
+ * @copyright   Copyright (C) 2015 Combodo SARL
+ * @license     http://opensource.org/licenses/AGPL-3.0
+ */
+
+
+/**
+ * SQLObjectQuery
+ * build a mySQL compatible SQL query
+ *
+ * @package     iTopORM
+ */
+
+
+class SQLObjectQuery extends SQLQuery
+{
+	private $m_SourceOQL = '';
+	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
+	private $m_oSelectedIdField = null;
+	private $m_aJoinSelects = array();
+	private $m_bBeautifulQuery = false;
+
+	// Data set by PrepareRendering()
+	private $__aFrom;
+	private $__aFields;
+	private $__aGroupBy;
+	private $__aDelTables;
+	private $__aSetValues;
+	private $__aSelectedIdFields;
+
+
+	public function __construct($sTable, $sTableAlias, $aFields, $bToDelete = true, $aValues = array(), $oSelectedIdField = null)
+	{
+		parent::__construct();
+
+		// This check is not needed but for developping purposes
+		//if (!CMDBSource::IsTable($sTable))
+		//{
+		//	throw new CoreException("Unknown table '$sTable'");
+		//}
+
+		// $aFields must be an array of "alias"=>"expr"
+		// $oConditionExpr must be a condition tree
+		// $aValues is an array of "alias"=>value
+
+		$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;
+		$this->m_oSelectedIdField = $oSelectedIdField;
+	}
+
+	public function GetTableAlias()
+	{
+		return $this->m_sTableAlias;
+	}
+
+	public function DisplayHtml()
+	{
+		if (count($this->m_aFields) == 0) $sFields = "";
+		else
+		{
+			$aFieldDesc = array();
+			foreach ($this->m_aFields as $sAlias => $oExpression)
+			{
+				$aFieldDesc[] = $oExpression->Render()." as <em>$sAlias</em>";
+			}
+			$sFields = " =&gt; ".implode(', ', $aFieldDesc);
+		}
+		echo "<b>$this->m_sTable</b>$sFields<br/>\n";
+		// #@# todo - display html of an expression tree
+		//$this->m_oConditionExpr->DisplayHtml()
+		if (count($this->m_aJoinSelects) > 0)
+		{
+			echo "Joined to...<br/>\n";
+			echo "<ul class=\"treeview\">\n";
+			foreach ($this->m_aJoinSelects as $aJoinInfo)
+			{
+				$sJoinType = $aJoinInfo["jointype"];
+				$oSQLQuery = $aJoinInfo["select"];
+				if (isset($aJoinInfo["on_expression"]))
+				{
+					$sOnCondition = $aJoinInfo["on_expression"]->Render();
+
+					echo "<li>Join '$sJoinType', ON ($sOnCondition)".$oSQLQuery->DisplayHtml()."</li>\n";
+				}
+				else
+				{
+					$sLeftField = $aJoinInfo["leftfield"];
+					$sRightField = $aJoinInfo["rightfield"];
+					$sRightTableAlias = $aJoinInfo["righttablealias"];
+	
+					echo "<li>Join '$sJoinType', $sLeftField, $sRightTableAlias.$sRightField".$oSQLQuery->DisplayHtml()."</li>\n";
+				}
+			}
+			echo "</ul>";
+		}
+		$this->PrepareRendering();
+		echo "From ...<br/>\n";
+		echo "<pre style=\"font-size: smaller;\">\n";
+		print_r($this->__aFrom);
+		echo "</pre>";
+	}
+
+	public function SetSelect($aExpressions)
+	{
+		$this->m_aFields = $aExpressions;
+	}
+
+	public function AddSelect($sAlias, $oExpression)
+	{
+		$this->m_aFields[$sAlias] = $oExpression;
+	}
+
+	public function SetGroupBy($aExpressions)
+	{
+		$this->m_aGroupBy = $aExpressions;
+	}
+
+	public function SetCondition($oConditionExpr)
+	{
+		$this->m_oConditionExpr = $oConditionExpr;
+	}
+
+	public function AddCondition($oConditionExpr)
+	{
+		if (is_null($this->m_oConditionExpr))
+		{
+			$this->m_oConditionExpr = $oConditionExpr;
+		}
+		else
+		{
+			$this->m_oConditionExpr->LogAnd($oConditionExpr);
+		}
+	}
+
+	private function AddJoin($sJoinType, $oSQLQuery, $sLeftField, $sRightField, $sRightTableAlias = '')
+	{
+		assert((get_class($oSQLQuery) == __CLASS__) || is_subclass_of($oSQLQuery, __CLASS__));
+		// No need to check this here but for development purposes
+		//if (!CMDBSource::IsField($this->m_sTable, $sLeftField))
+		//{
+		//	throw new CoreException("Unknown field '$sLeftField' in table '".$this->m_sTable);
+		//}
+
+		if (empty($sRightTableAlias))
+		{
+			$sRightTableAlias = $oSQLQuery->m_sTableAlias;
+		}
+// #@# Could not be verified here because the namespace is unknown - do we need to check it there?
+//
+//		if (!CMDBSource::IsField($sRightTable, $sRightField))
+//		{
+//			throw new CoreException("Unknown field '$sRightField' in table '".$sRightTable."'");
+//		}
+		$this->m_aJoinSelects[] = array(
+			"jointype" => $sJoinType,
+			"select" => $oSQLQuery,
+			"leftfield" => $sLeftField,
+			"rightfield" => $sRightField,
+			"righttablealias" => $sRightTableAlias
+		);
+	}
+	public function AddInnerJoin($oSQLQuery, $sLeftField, $sRightField, $sRightTable = '')
+	{
+		$this->AddJoin("inner", $oSQLQuery, $sLeftField, $sRightField, $sRightTable);
+	}
+	public function AddInnerJoinTree($oSQLQuery, $sLeftFieldLeft, $sLeftFieldRight, $sRightFieldLeft, $sRightFieldRight, $sRightTableAlias = '', $iOperatorCode = TREE_OPERATOR_BELOW)
+	{
+		assert((get_class($oSQLQuery) == __CLASS__) || is_subclass_of($oSQLQuery, __CLASS__));
+		if (empty($sRightTableAlias))
+		{
+			$sRightTableAlias = $oSQLQuery->m_sTableAlias;
+		}
+		$this->m_aJoinSelects[] = array(
+			"jointype" => 'inner_tree',
+			"select" => $oSQLQuery,
+			"leftfield" => $sLeftFieldLeft,
+			"rightfield" => $sLeftFieldRight,
+			"rightfield_left" => $sRightFieldLeft,
+			"rightfield_right" => $sRightFieldRight,
+			"righttablealias" => $sRightTableAlias,
+			"tree_operator" => $iOperatorCode);
+	}
+	public function AddLeftJoin($oSQLQuery, $sLeftField, $sRightField)
+	{
+		return $this->AddJoin("left", $oSQLQuery, $sLeftField, $sRightField);
+	}
+
+	public function AddInnerJoinEx(SQLQuery $oSQLQuery, Expression $oOnExpression)
+	{
+		$this->m_aJoinSelects[] = array(
+			"jointype" => 'inner',
+			"select" => $oSQLQuery,
+			"on_expression" => $oOnExpression
+		);
+	}
+
+	public function AddLeftJoinEx(SQLQuery $oSQLQuery, Expression $oOnExpression)
+	{
+		$this->m_aJoinSelects[] = array(
+			"jointype" => 'left',
+			"select" => $oSQLQuery,
+			"on_expression" => $oOnExpression
+		);
+	}
+	
+	// Interface, build the SQL query
+	public function RenderDelete($aArgs = array())
+	{
+		$this->PrepareRendering();
+
+		// Target: DELETE myAlias1, myAlias2 FROM t1 as myAlias1, t2 as myAlias2, t3 as topreserve WHERE ...
+
+		$sDelete = self::ClauseDelete($this->__aDelTables);
+		$sFrom   = self::ClauseFrom($this->__aFrom);
+		// #@# safety net to redo ?
+		/*
+		if ($this->m_oConditionExpr->IsAny())
+		-- if (count($aConditions) == 0) --
+		{
+			throw new CoreException("Building a request wich will delete every object of a given table -looks suspicious- please use truncate instead...");
+		}
+		*/
+		if (is_null($this->m_oConditionExpr))
+		{
+			// Delete all !!!
+		}
+		else
+		{
+			$sWhere  = self::ClauseWhere($this->m_oConditionExpr, $aArgs);
+			return "DELETE $sDelete FROM $sFrom WHERE $sWhere";
+		}
+	}
+
+	/**
+	 *	Needed for the unions
+	 */
+	public function RenderSelectClause()
+	{
+		$this->PrepareRendering();
+		$sSelect = self::ClauseSelect($this->__aFields);
+		return $sSelect;
+	}
+
+	/**
+	 *	Needed for the unions
+	 */
+	public function RenderOrderByClause($aOrderBy)
+	{
+		$this->PrepareRendering();
+		$sOrderBy = self::ClauseOrderBy($aOrderBy);
+		return $sOrderBy;
+	}
+
+	// Interface, build the SQL query
+	public function RenderUpdate($aArgs = array())
+	{
+		$this->PrepareRendering();
+		$sFrom   = self::ClauseFrom($this->__aFrom);
+		$sValues = self::ClauseValues($this->__aSetValues);
+		$sWhere  = self::ClauseWhere($this->m_oConditionExpr, $aArgs);
+		return "UPDATE $sFrom SET $sValues WHERE $sWhere";
+	}
+
+	// Interface, build the SQL query
+	public function RenderSelect($aOrderBy = array(), $aArgs = array(), $iLimitCount = 0, $iLimitStart = 0, $bGetCount = false, $bBeautifulQuery = false)
+	{
+		$this->m_bBeautifulQuery = $bBeautifulQuery;
+		$sLineSep = $this->m_bBeautifulQuery ? "\n" : '';
+		$sIndent = $this->m_bBeautifulQuery ? "   " : null;
+
+		$this->PrepareRendering();
+		$sFrom   = self::ClauseFrom($this->__aFrom, $sIndent);
+		$sWhere  = self::ClauseWhere($this->m_oConditionExpr, $aArgs);
+		if ($bGetCount)
+		{
+			if (count($this->__aSelectedIdFields) > 0)
+			{
+				$aCountFields = array();
+				foreach ($this->__aSelectedIdFields as $sFieldExpr)
+				{
+					$aCountFields[] = "COALESCE($sFieldExpr, 0)"; // Null values are excluded from the count
+				}
+				$sCountFields = implode(', ', $aCountFields);
+				$sSQL = "SELECT$sLineSep COUNT(DISTINCT $sCountFields) AS COUNT$sLineSep FROM $sFrom$sLineSep WHERE $sWhere";
+			}
+			else
+			{
+				$sSQL = "SELECT$sLineSep COUNT(*) AS COUNT$sLineSep FROM $sFrom$sLineSep WHERE $sWhere";
+			}
+		}
+		else
+		{
+			$sSelect = self::ClauseSelect($this->__aFields);
+			$sOrderBy = self::ClauseOrderBy($aOrderBy);
+			if (!empty($sOrderBy))
+			{
+				$sOrderBy = "ORDER BY $sOrderBy$sLineSep";
+			}
+			if ($iLimitCount > 0)
+			{
+				$sLimit = 'LIMIT '.$iLimitStart.', '.$iLimitCount;
+			}
+			else
+			{
+				$sLimit = '';
+			}
+			$sSQL = "SELECT$sLineSep DISTINCT $sSelect$sLineSep FROM $sFrom$sLineSep WHERE $sWhere$sLineSep $sOrderBy $sLimit";
+		}
+		return $sSQL;
+	}
+
+	// Interface, build the SQL query
+	public function RenderGroupBy($aArgs = array(), $bBeautifulQuery = false)
+	{
+		$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";
+		return $sSQL;
+	}
+
+	// Purpose: prepare the query data, once for all
+	private function PrepareRendering()
+	{
+		if (is_null($this->__aFrom))
+		{
+			$this->__aFrom = array();
+			$this->__aFields = array();
+			$this->__aGroupBy = array();
+			$this->__aDelTables = array();
+			$this->__aSetValues = array();
+			$this->__aSelectedIdFields = array();
+	
+			$this->PrepareSingleTable($this, $this->__aFrom, '', array('jointype' => 'first'));
+		}
+	}
+
+	private function PrepareSingleTable(SQLObjectQuery $oRootQuery, &$aFrom, $sCallerAlias = '', $aJoinData)
+	{
+		$aTranslationTable[$this->m_sTable]['*'] = $this->m_sTableAlias;
+
+		// Handle the various kinds of join (or first table in the list)
+		//
+		if (empty($aJoinData['righttablealias']))
+		{
+			$sRightTableAlias = $this->m_sTableAlias;
+		}
+		else
+		{
+			$sRightTableAlias = $aJoinData['righttablealias'];
+		}
+		switch ($aJoinData['jointype'])
+		{
+			case "first":
+				$aFrom[$this->m_sTableAlias] = array("jointype"=>"first", "tablename"=>$this->m_sTable, "joincondition"=>"");
+				break;
+			case "inner":
+			case "left":
+				if (isset($aJoinData["on_expression"]))
+				{
+					$sJoinCond = $aJoinData["on_expression"]->Render();
+				}
+				else
+				{
+					$sJoinCond = "`$sCallerAlias`.`{$aJoinData['leftfield']}` = `$sRightTableAlias`.`{$aJoinData['rightfield']}`";
+				}
+				$aFrom[$this->m_sTableAlias] = array("jointype"=>$aJoinData['jointype'], "tablename"=>$this->m_sTable, "joincondition"=>"$sJoinCond");
+				break;
+			case "inner_tree":
+				$sNodeLeft = "`$sCallerAlias`.`{$aJoinData['leftfield']}`";
+				$sNodeRight = "`$sCallerAlias`.`{$aJoinData['rightfield']}`";
+				$sRootLeft = "`$sRightTableAlias`.`{$aJoinData['rightfield_left']}`";
+				$sRootRight = "`$sRightTableAlias`.`{$aJoinData['rightfield_right']}`";
+				switch($aJoinData['tree_operator'])
+				{
+					case TREE_OPERATOR_BELOW:
+					$sJoinCond = "$sNodeLeft >= $sRootLeft AND $sNodeLeft <= $sRootRight";
+					break;
+					
+					case TREE_OPERATOR_BELOW_STRICT:
+					$sJoinCond = "$sNodeLeft > $sRootLeft AND $sNodeLeft < $sRootRight";
+					break;
+					
+					case TREE_OPERATOR_NOT_BELOW: // Complementary of 'BELOW'
+					$sJoinCond = "$sNodeLeft < $sRootLeft OR $sNodeLeft > $sRootRight";
+					break;
+					
+					case TREE_OPERATOR_NOT_BELOW_STRICT: // Complementary of BELOW_STRICT
+					$sJoinCond = "$sNodeLeft <= $sRootLeft OR $sNodeLeft >= $sRootRight";
+					break;
+
+					case TREE_OPERATOR_ABOVE:
+					$sJoinCond = "$sNodeLeft <= $sRootLeft AND $sNodeRight >= $sRootRight";
+					break;
+					
+					case TREE_OPERATOR_ABOVE_STRICT:
+					$sJoinCond = "$sNodeLeft < $sRootLeft AND $sNodeRight > $sRootRight";
+					break;
+					
+					case TREE_OPERATOR_NOT_ABOVE: // Complementary of 'ABOVE'
+					$sJoinCond = "$sNodeLeft > $sRootLeft OR $sNodeRight < $sRootRight";
+					break;
+					
+					case TREE_OPERATOR_NOT_ABOVE_STRICT: // Complementary of ABOVE_STRICT
+					$sJoinCond = "$sNodeLeft >= $sRootLeft OR $sNodeRight <= $sRootRight";
+					break;
+					
+				}
+				$aFrom[$this->m_sTableAlias] = array("jointype"=>$aJoinData['jointype'], "tablename"=>$this->m_sTable, "joincondition"=>"$sJoinCond");
+				break;
+		}
+
+		// Given the alias, modify the fields and conditions
+		// before adding them into the current lists
+		//
+		foreach($this->m_aFields as $sAlias => $oExpression)
+		{
+			$oRootQuery->__aFields["`$sAlias`"] = $oExpression->Render();
+		}
+		if ($this->m_aGroupBy)
+		{
+			foreach($this->m_aGroupBy as $sAlias => $oExpression)
+			{
+				$oRootQuery->__aGroupBy["`$sAlias`"] = $oExpression->Render();
+			}
+		}
+		if ($this->m_bToDelete)
+		{
+			$oRootQuery->__aDelTables[] = "`{$this->m_sTableAlias}`";
+		}
+		foreach($this->m_aValues as $sFieldName=>$value)
+		{
+			$oRootQuery->__aSetValues["`{$this->m_sTableAlias}`.`$sFieldName`"] = $value; // quoted further!
+		}
+
+  		if (!is_null($this->m_oSelectedIdField))
+  		{
+  			$oRootQuery->__aSelectedIdFields[] = $this->m_oSelectedIdField->Render();
+		}
+
+		// loop on joins, to complete the list of tables/fields/conditions
+		//
+		$aTempFrom = array(); // temporary subset of 'from' specs, to be grouped in the final query
+		foreach ($this->m_aJoinSelects as $aJoinData)
+		{
+			$oRightSelect = $aJoinData["select"];
+
+			$sJoinTableAlias = $oRightSelect->PrepareSingleTable($oRootQuery, $aTempFrom, $this->m_sTableAlias, $aJoinData);
+		}
+		$aFrom[$this->m_sTableAlias]['subfrom'] = $aTempFrom;
+
+		return $this->m_sTableAlias;
+	}
+
+	public function OptimizeJoins($aUsedTables, $bTopCall = true)
+	{
+		if ($bTopCall)
+		{
+			// Top call: complete the list of tables absolutely required to perform the right query
+			$this->CollectUsedTables($aUsedTables);
+		}
+
+		$aToDiscard = array();
+		foreach ($this->m_aJoinSelects as $i => $aJoinInfo)
+		{
+			$oSQLQuery = $aJoinInfo["select"];
+			$sTableAlias = $oSQLQuery->GetTableAlias();
+			if ($oSQLQuery->OptimizeJoins($aUsedTables, false) && !array_key_exists($sTableAlias, $aUsedTables))
+			{
+				$aToDiscard[] = $i;
+			}
+		}
+		foreach ($aToDiscard as $i)
+		{
+			unset($this->m_aJoinSelects[$i]);
+		}
+
+		return (count($this->m_aJoinSelects) == 0);
+	}
+
+	protected function CollectUsedTables(&$aTables)
+	{
+		$this->m_oConditionExpr->CollectUsedParents($aTables);
+		foreach($this->m_aFields as $sFieldAlias => $oField)
+		{
+			$oField->CollectUsedParents($aTables);
+		}
+		if ($this->m_aGroupBy)
+		{
+			foreach($this->m_aGroupBy as $sAlias => $oExpression)
+			{
+				$oExpression->CollectUsedParents($aTables);
+			}
+		}
+  		if (!is_null($this->m_oSelectedIdField))
+  		{
+  			$this->m_oSelectedIdField->CollectUsedParents($aTables);
+		}
+
+		foreach ($this->m_aJoinSelects as $i => $aJoinInfo)
+		{
+			$oSQLQuery = $aJoinInfo["select"];
+			if ($oSQLQuery->HasRequiredTables($aTables))
+			{
+				// There is something required in the branch, then this node is a MUST
+				if (isset($aJoinInfo['righttablealias']))
+				{
+					$aTables[$aJoinInfo['righttablealias']] = true;
+				}
+				if (isset($aJoinInfo["on_expression"]))
+				{
+					$sJoinCond = $aJoinInfo["on_expression"]->CollectUsedParents($aTables);
+				}
+			}
+		}
+
+		return $aTables;
+	}
+
+	// Is required in the JOIN, and therefore we must ensure that the join expression will be valid
+	protected function HasRequiredTables(&$aTables)
+	{
+		$bResult = false;
+		if (array_key_exists($this->m_sTableAlias, $aTables))
+		{
+			$bResult = true;
+		}
+		foreach ($this->m_aJoinSelects as $i => $aJoinInfo)
+		{
+			$oSQLQuery = $aJoinInfo["select"];
+			if ($oSQLQuery->HasRequiredTables($aTables))
+			{
+				// There is something required in the branch, then this node is a MUST
+				if (isset($aJoinInfo['righttablealias']))
+				{
+					$aTables[$aJoinInfo['righttablealias']] = true;
+				}
+				if (isset($aJoinInfo["on_expression"]))
+				{
+					$sJoinCond = $aJoinInfo["on_expression"]->CollectUsedParents($aTables);
+				}
+				$bResult = true;
+			}
+		}
+		// None of the tables is in the list of required tables
+		return $bResult;
+	}
+}

+ 42 - 554
core/sqlquery.class.inc.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //
 //   This file is part of iTop.
 //   This file is part of iTop.
 //
 //
@@ -21,7 +21,7 @@
  * SQLQuery
  * SQLQuery
  * build an mySQL compatible SQL query
  * build an mySQL compatible SQL query
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -36,40 +36,13 @@
 require_once('cmdbsource.class.inc.php');
 require_once('cmdbsource.class.inc.php');
 
 
 
 
-class SQLQuery
+abstract class SQLQuery
 {
 {
 	private $m_SourceOQL = '';
 	private $m_SourceOQL = '';
-	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
-	private $m_oSelectedIdField = null;
-	private $m_aJoinSelects = array();
 	private $m_bBeautifulQuery = false;
 	private $m_bBeautifulQuery = false;
 
 
-	public function __construct($sTable, $sTableAlias, $aFields, $bToDelete = true, $aValues = array(), $oSelectedIdField = null)
+	public function __construct()
 	{
 	{
-		// This check is not needed but for developping purposes
-		//if (!CMDBSource::IsTable($sTable))
-		//{
-		//	throw new CoreException("Unknown table '$sTable'");
-		//}
-
-		// $aFields must be an array of "alias"=>"expr"
-		// $oConditionExpr must be a condition tree
-		// $aValues is an array of "alias"=>value
-
-		$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;
-		$this->m_oSelectedIdField = $oSelectedIdField;
 	}
 	}
 
 
 	/**
 	/**
@@ -80,11 +53,6 @@ class SQLQuery
 		return unserialize(serialize($this));
 		return unserialize(serialize($this));
 	}
 	}
 
 
-	public function GetTableAlias()
-	{
-		return $this->m_sTableAlias;
-	}
-
 	public function SetSourceOQL($sOQL)
 	public function SetSourceOQL($sOQL)
 	{
 	{
 		$this->m_SourceOQL = $sOQL;
 		$this->m_SourceOQL = $sOQL;
@@ -95,295 +63,17 @@ class SQLQuery
 		return $this->m_SourceOQL;
 		return $this->m_SourceOQL;
 	}
 	}
 
 
-	public function DisplayHtml()
-	{
-		if (count($this->m_aFields) == 0) $sFields = "";
-		else
-		{
-			$aFieldDesc = array();
-			foreach ($this->m_aFields as $sAlias => $oExpression)
-			{
-				$aFieldDesc[] = $oExpression->Render()." as <em>$sAlias</em>";
-			}
-			$sFields = " =&gt; ".implode(', ', $aFieldDesc);
-		}
-		echo "<b>$this->m_sTable</b>$sFields<br/>\n";
-		// #@# todo - display html of an expression tree
-		//$this->m_oConditionExpr->DisplayHtml()
-		if (count($this->m_aJoinSelects) > 0)
-		{
-			echo "Joined to...<br/>\n";
-			echo "<ul class=\"treeview\">\n";
-			foreach ($this->m_aJoinSelects as $aJoinInfo)
-			{
-				$sJoinType = $aJoinInfo["jointype"];
-				$oSQLQuery = $aJoinInfo["select"];
-				if (isset($aJoinInfo["on_expression"]))
-				{
-					$sOnCondition = $aJoinInfo["on_expression"]->Render();
-
-					echo "<li>Join '$sJoinType', ON ($sOnCondition)".$oSQLQuery->DisplayHtml()."</li>\n";
-				}
-				else
-				{
-					$sLeftField = $aJoinInfo["leftfield"];
-					$sRightField = $aJoinInfo["rightfield"];
-					$sRightTableAlias = $aJoinInfo["righttablealias"];
-	
-					echo "<li>Join '$sJoinType', $sLeftField, $sRightTableAlias.$sRightField".$oSQLQuery->DisplayHtml()."</li>\n";
-				}
-			}
-			echo "</ul>";
-		}
-		$aFrom = array();
-		$aFields = array();
-		$aGroupBy = array();
-		$oCondition = null;
-		$aDelTables = array();
-		$aSetValues = array();
-		$aSelectedIdFields = array();
-		$this->privRender($aFrom, $aFields, $aGroupBy, $oCondition, $aDelTables, $aSetValues, $aSelectedIdFields);
-		echo "From ...<br/>\n";
-		echo "<pre style=\"font-size: smaller;\">\n";
-		print_r($aFrom);
-		echo "</pre>";
-	}
-
-	public function SetSelect($aExpressions)
-	{
-		$this->m_aFields = $aExpressions;
-	}
-
-	public function SetGroupBy($aExpressions)
-	{
-		$this->m_aGroupBy = $aExpressions;
-	}
+	abstract public function AddInnerJoin($oSQLQuery, $sLeftField, $sRightField, $sRightTable = '');
 
 
-	public function SetCondition($oConditionExpr)
-	{
-		$this->m_oConditionExpr = $oConditionExpr;
-	}
+	abstract public function DisplayHtml();
+	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);
 
 
-	public function AddCondition($oConditionExpr)
-	{
-		if (is_null($this->m_oConditionExpr))
-		{
-			$this->m_oConditionExpr = $oConditionExpr;
-		}
-		else
-		{
-			$this->m_oConditionExpr->LogAnd($oConditionExpr);
-		}
-	}
+	abstract public function OptimizeJoins($aUsedTables, $bTopCall = true);
 
 
-	private function AddJoin($sJoinType, $oSQLQuery, $sLeftField, $sRightField, $sRightTableAlias = '')
-	{
-		assert((get_class($oSQLQuery) == __CLASS__) || is_subclass_of($oSQLQuery, __CLASS__));
-		// No need to check this here but for development purposes
-		//if (!CMDBSource::IsField($this->m_sTable, $sLeftField))
-		//{
-		//	throw new CoreException("Unknown field '$sLeftField' in table '".$this->m_sTable);
-		//}
-
-		if (empty($sRightTableAlias))
-		{
-			$sRightTableAlias = $oSQLQuery->m_sTableAlias;
-		}
-// #@# Could not be verified here because the namespace is unknown - do we need to check it there?
-//
-//		if (!CMDBSource::IsField($sRightTable, $sRightField))
-//		{
-//			throw new CoreException("Unknown field '$sRightField' in table '".$sRightTable."'");
-//		}
-		$this->m_aJoinSelects[] = array(
-			"jointype" => $sJoinType,
-			"select" => $oSQLQuery,
-			"leftfield" => $sLeftField,
-			"rightfield" => $sRightField,
-			"righttablealias" => $sRightTableAlias
-		);
-	}
-	public function AddInnerJoin($oSQLQuery, $sLeftField, $sRightField, $sRightTable = '')
-	{
-		$this->AddJoin("inner", $oSQLQuery, $sLeftField, $sRightField, $sRightTable);
-	}
-	public function AddInnerJoinTree($oSQLQuery, $sLeftFieldLeft, $sLeftFieldRight, $sRightFieldLeft, $sRightFieldRight, $sRightTableAlias = '', $iOperatorCode = TREE_OPERATOR_BELOW)
-	{
-		assert((get_class($oSQLQuery) == __CLASS__) || is_subclass_of($oSQLQuery, __CLASS__));
-		if (empty($sRightTableAlias))
-		{
-			$sRightTableAlias = $oSQLQuery->m_sTableAlias;
-		}
-		$this->m_aJoinSelects[] = array(
-			"jointype" => 'inner_tree',
-			"select" => $oSQLQuery,
-			"leftfield" => $sLeftFieldLeft,
-			"rightfield" => $sLeftFieldRight,
-			"rightfield_left" => $sRightFieldLeft,
-			"rightfield_right" => $sRightFieldRight,
-			"righttablealias" => $sRightTableAlias,
-			"tree_operator" => $iOperatorCode);
-	}
-	public function AddLeftJoin($oSQLQuery, $sLeftField, $sRightField)
-	{
-		return $this->AddJoin("left", $oSQLQuery, $sLeftField, $sRightField);
-	}
-
-	public function AddInnerJoinEx(SQLQuery $oSQLQuery, Expression $oOnExpression)
-	{
-		$this->m_aJoinSelects[] = array(
-			"jointype" => 'inner',
-			"select" => $oSQLQuery,
-			"on_expression" => $oOnExpression
-		);
-	}
-
-	public function AddLeftJoinEx(SQLQuery $oSQLQuery, Expression $oOnExpression)
-	{
-		$this->m_aJoinSelects[] = array(
-			"jointype" => 'left',
-			"select" => $oSQLQuery,
-			"on_expression" => $oOnExpression
-		);
-	}
-	
-	// Interface, build the SQL query
-	public function RenderDelete($aArgs = array())
-	{
-		// 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, $aGroupBy, $oCondition, $aDelTables, $aSetValues, $aSelectedIdFields);
-
-		// Target: DELETE myAlias1, myAlias2 FROM t1 as myAlias1, t2 as myAlias2, t3 as topreserve WHERE ...
-
-		$sDelete = self::ClauseDelete($aDelTables);
-		$sFrom   = self::ClauseFrom($aFrom);
-		// #@# safety net to redo ?
-		/*
-		if ($this->m_oConditionExpr->IsAny())
-		-- if (count($aConditions) == 0) --
-		{
-			throw new CoreException("Building a request wich will delete every object of a given table -looks suspicious- please use truncate instead...");
-		}
-		*/
-		if (is_null($oCondition))
-		{
-			// Delete all !!!
-		}
-		else
-		{
-			$sWhere  = self::ClauseWhere($oCondition, $aArgs);
-			return "DELETE $sDelete FROM $sFrom WHERE $sWhere";
-		}
-	}
-
-	// Interface, build the SQL query
-	public function RenderUpdate($aArgs = array())
-	{
-		// 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, $aGroupBy, $oCondition, $aDelTables, $aSetValues, $aSelectedIdFields);
-		$sFrom   = self::ClauseFrom($aFrom);
-		$sValues = self::ClauseValues($aSetValues);
-		$sWhere  = self::ClauseWhere($oCondition, $aArgs);
-		return "UPDATE $sFrom SET $sValues WHERE $sWhere";
-	}
-
-	// Interface, build the SQL query
-	public function RenderSelect($aOrderBy = array(), $aArgs = array(), $iLimitCount = 0, $iLimitStart = 0, $bGetCount = false, $bBeautifulQuery = false)
-	{
-		$this->m_bBeautifulQuery = $bBeautifulQuery;
-		$sLineSep = $this->m_bBeautifulQuery ? "\n" : '';
-		$sIndent = $this->m_bBeautifulQuery ? "   " : null;
-
-		// 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);
-
-		$sFrom   = self::ClauseFrom($aFrom, $sIndent);
-		$sWhere  = self::ClauseWhere($oCondition, $aArgs);
-		if ($bGetCount)
-		{
-			if (count($aSelectedIdFields) > 0)
-			{
-				$aCountFields = array();
-				foreach ($aSelectedIdFields as $sFieldExpr)
-				{
-					$aCountFields[] = "COALESCE($sFieldExpr, 0)"; // Null values are excluded from the count
-				}
-				$sCountFields = implode(', ', $aCountFields);
-				$sSQL = "SELECT$sLineSep COUNT(DISTINCT $sCountFields) AS COUNT$sLineSep FROM $sFrom$sLineSep WHERE $sWhere";
-			}
-			else
-			{
-				$sSQL = "SELECT$sLineSep COUNT(*) AS COUNT$sLineSep FROM $sFrom$sLineSep WHERE $sWhere";
-			}
-		}
-		else
-		{
-			$sSelect = self::ClauseSelect($aFields);
-			$sOrderBy = self::ClauseOrderBy($aOrderBy);
-			if (!empty($sOrderBy))
-			{
-				$sOrderBy = "ORDER BY $sOrderBy";
-			}
-			if ($iLimitCount > 0)
-			{
-				$sLimit = 'LIMIT '.$iLimitStart.', '.$iLimitCount;
-			}
-			else
-			{
-				$sLimit = '';
-			}
-			$sSQL = "SELECT$sLineSep DISTINCT $sSelect$sLineSep FROM $sFrom$sLineSep WHERE $sWhere$sLineSep $sOrderBy$sLineSep $sLimit";
-		}
-		return $sSQL;
-	}
-
-	// Interface, build the SQL query
-	public function RenderGroupBy($aArgs = array(), $bBeautifulQuery = false)
-	{
-		$this->m_bBeautifulQuery = $bBeautifulQuery;
-		$sLineSep = $this->m_bBeautifulQuery ? "\n" : '';
-		$sIndent = $this->m_bBeautifulQuery ? "   " : null;
-
-		// 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, $sIndent);
-		$sWhere  = self::ClauseWhere($oCondition, $aArgs);
-		$sGroupBy = self::ClauseGroupBy($aGroupBy);
-		$sSQL = "SELECT $sSelect,$sLineSep COUNT(*) AS _itop_count_$sLineSep FROM $sFrom$sLineSep WHERE $sWhere$sLineSep GROUP BY $sGroupBy";
-		return $sSQL;
-	}
-
-	private static function ClauseSelect($aFields)
+	protected static function ClauseSelect($aFields)
 	{
 	{
 		$aSelect = array();
 		$aSelect = array();
 		foreach ($aFields as $sFieldAlias => $sSQLExpr)
 		foreach ($aFields as $sFieldAlias => $sSQLExpr)
@@ -394,13 +84,13 @@ class SQLQuery
 		return $sSelect;
 		return $sSelect;
 	}
 	}
 
 
-	private static function ClauseGroupBy($aGroupBy)
+	protected static function ClauseGroupBy($aGroupBy)
 	{
 	{
 		$sRes = implode(', ', $aGroupBy);
 		$sRes = implode(', ', $aGroupBy);
 		return $sRes;
 		return $sRes;
 	}
 	}
 
 
-	private static function ClauseDelete($aDelTableAliases)
+	protected static function ClauseDelete($aDelTableAliases)
 	{
 	{
 		$aDelTables = array();
 		$aDelTables = array();
 		foreach ($aDelTableAliases as $sTableAlias)
 		foreach ($aDelTableAliases as $sTableAlias)
@@ -411,7 +101,7 @@ class SQLQuery
 		return $sDelTables;
 		return $sDelTables;
 	}
 	}
 
 
-	private static function ClauseFrom($aFrom, $sIndent = null, $iIndentLevel = 0)
+	protected static function ClauseFrom($aFrom, $sIndent = null, $iIndentLevel = 0)
 	{
 	{
 		$sLineBreakLong = $sIndent ? "\n".str_repeat($sIndent, $iIndentLevel + 1) : '';
 		$sLineBreakLong = $sIndent ? "\n".str_repeat($sIndent, $iIndentLevel + 1) : '';
 		$sLineBreak = $sIndent ? "\n".str_repeat($sIndent, $iIndentLevel) : '';
 		$sLineBreak = $sIndent ? "\n".str_repeat($sIndent, $iIndentLevel) : '';
@@ -427,14 +117,32 @@ class SQLQuery
 					break;
 					break;
 				case "inner":
 				case "inner":
 				case "inner_tree":
 				case "inner_tree":
-					$sFrom .= $sLineBreak."INNER JOIN ($sLineBreakLong`".$aJoinInfo["tablename"]."` AS `$sTableAlias`";
-					$sFrom .= " ".self::ClauseFrom($aJoinInfo["subfrom"], $sIndent, $iIndentLevel + 1);
-					$sFrom .= $sLineBreak.") ON ".$aJoinInfo["joincondition"];
+					if (count($aJoinInfo["subfrom"]) > 0)
+					{
+						$sFrom .= $sLineBreak."INNER JOIN ($sLineBreakLong`".$aJoinInfo["tablename"]."` AS `$sTableAlias`";
+						$sFrom .= " ".self::ClauseFrom($aJoinInfo["subfrom"], $sIndent, $iIndentLevel + 1);
+						$sFrom .= $sLineBreak.") ON ".$aJoinInfo["joincondition"];
+					}
+					else
+					{
+						// Unions do not suffer parenthesis around the "table AS alias"
+						$sFrom .= $sLineBreak."INNER JOIN $sLineBreakLong`".$aJoinInfo["tablename"]."` AS `$sTableAlias`";
+						$sFrom .= $sLineBreak." ON ".$aJoinInfo["joincondition"];
+					}
 					break;
 					break;
 				case "left":
 				case "left":
-					$sFrom .= $sLineBreak."LEFT JOIN ($sLineBreakLong`".$aJoinInfo["tablename"]."` AS `$sTableAlias`";
-					$sFrom .= " ".self::ClauseFrom($aJoinInfo["subfrom"], $sIndent, $iIndentLevel + 1);
-					$sFrom .= $sLineBreak.") ON ".$aJoinInfo["joincondition"];
+					if (count($aJoinInfo["subfrom"]) > 0)
+					{
+						$sFrom .= $sLineBreak."LEFT JOIN ($sLineBreakLong`".$aJoinInfo["tablename"]."` AS `$sTableAlias`";
+						$sFrom .= " ".self::ClauseFrom($aJoinInfo["subfrom"], $sIndent, $iIndentLevel + 1);
+						$sFrom .= $sLineBreak.") ON ".$aJoinInfo["joincondition"];
+					}
+					else
+					{
+						// Unions do not suffer parenthesis around the "table AS alias"
+						$sFrom .= $sLineBreak."LEFT JOIN $sLineBreakLong`".$aJoinInfo["tablename"]."` AS `$sTableAlias`";
+						$sFrom .= $sLineBreak." ON ".$aJoinInfo["joincondition"];
+					}
 					break;
 					break;
 				default:
 				default:
 					throw new CoreException("Unknown jointype: '".$aJoinInfo["jointype"]."'");
 					throw new CoreException("Unknown jointype: '".$aJoinInfo["jointype"]."'");
@@ -443,7 +151,7 @@ class SQLQuery
 		return $sFrom;
 		return $sFrom;
 	}
 	}
 
 
-	private static function ClauseValues($aValues)
+	protected static function ClauseValues($aValues)
 	{
 	{
 		$aSetValues = array();
 		$aSetValues = array();
 		foreach ($aValues as $sFieldSpec => $value)
 		foreach ($aValues as $sFieldSpec => $value)
@@ -454,7 +162,7 @@ class SQLQuery
 		return $sSetValues;
 		return $sSetValues;
 	}
 	}
 
 
-	private static function ClauseWhere($oConditionExpr, $aArgs = array())
+	protected static function ClauseWhere($oConditionExpr, $aArgs = array())
 	{
 	{
 		if (is_null($oConditionExpr))
 		if (is_null($oConditionExpr))
 		{
 		{
@@ -466,7 +174,7 @@ class SQLQuery
 		}
 		}
 	}
 	}
 
 
-	private static function ClauseOrderBy($aOrderBy)
+	protected static function ClauseOrderBy($aOrderBy)
 	{
 	{
 		$aOrderBySpec = array();
 		$aOrderBySpec = array();
 		foreach($aOrderBy as $sFieldAlias => $bAscending)
 		foreach($aOrderBy as $sFieldAlias => $bAscending)
@@ -477,224 +185,4 @@ class SQLQuery
 		$sOrderBy = implode(", ", $aOrderBySpec);
 		$sOrderBy = implode(", ", $aOrderBySpec);
 		return $sOrderBy;
 		return $sOrderBy;
 	}
 	}
-
-	// Purpose: prepare the query data, once for all
-	private function privRender(&$aFrom, &$aFields, &$aGroupBy, &$oCondition, &$aDelTables, &$aSetValues, &$aSelectedIdFields)
-	{
-		$sTableAlias = $this->privRenderSingleTable($aFrom, $aFields, $aGroupBy, $aDelTables, $aSetValues, $aSelectedIdFields, '', array('jointype' => 'first'));
-		$oCondition = $this->m_oConditionExpr;
-		return $sTableAlias; 
-	}
-
-	private function privRenderSingleTable(&$aFrom, &$aFields, &$aGroupBy, &$aDelTables, &$aSetValues, &$aSelectedIdFields, $sCallerAlias = '', $aJoinData)
-	{
-		$aTranslationTable[$this->m_sTable]['*'] = $this->m_sTableAlias;
-
-		// Handle the various kinds of join (or first table in the list)
-		//
-		if (empty($aJoinData['righttablealias']))
-		{
-			$sRightTableAlias = $this->m_sTableAlias;
-		}
-		else
-		{
-			$sRightTableAlias = $aJoinData['righttablealias'];
-		}
-		switch ($aJoinData['jointype'])
-		{
-			case "first":
-				$aFrom[$this->m_sTableAlias] = array("jointype"=>"first", "tablename"=>$this->m_sTable, "joincondition"=>"");
-				break;
-			case "inner":
-			case "left":
-				if (isset($aJoinData["on_expression"]))
-				{
-					$sJoinCond = $aJoinData["on_expression"]->Render();
-				}
-				else
-				{
-					$sJoinCond = "`$sCallerAlias`.`{$aJoinData['leftfield']}` = `$sRightTableAlias`.`{$aJoinData['rightfield']}`";
-				}
-				$aFrom[$this->m_sTableAlias] = array("jointype"=>$aJoinData['jointype'], "tablename"=>$this->m_sTable, "joincondition"=>"$sJoinCond");
-				break;
-			case "inner_tree":
-				$sNodeLeft = "`$sCallerAlias`.`{$aJoinData['leftfield']}`";
-				$sNodeRight = "`$sCallerAlias`.`{$aJoinData['rightfield']}`";
-				$sRootLeft = "`$sRightTableAlias`.`{$aJoinData['rightfield_left']}`";
-				$sRootRight = "`$sRightTableAlias`.`{$aJoinData['rightfield_right']}`";
-				switch($aJoinData['tree_operator'])
-				{
-					case TREE_OPERATOR_BELOW:
-					$sJoinCond = "$sNodeLeft >= $sRootLeft AND $sNodeLeft <= $sRootRight";
-					break;
-					
-					case TREE_OPERATOR_BELOW_STRICT:
-					$sJoinCond = "$sNodeLeft > $sRootLeft AND $sNodeLeft < $sRootRight";
-					break;
-					
-					case TREE_OPERATOR_NOT_BELOW: // Complementary of 'BELOW'
-					$sJoinCond = "$sNodeLeft < $sRootLeft OR $sNodeLeft > $sRootRight";
-					break;
-					
-					case TREE_OPERATOR_NOT_BELOW_STRICT: // Complementary of BELOW_STRICT
-					$sJoinCond = "$sNodeLeft <= $sRootLeft OR $sNodeLeft >= $sRootRight";
-					break;
-
-					case TREE_OPERATOR_ABOVE:
-					$sJoinCond = "$sNodeLeft <= $sRootLeft AND $sNodeRight >= $sRootRight";
-					break;
-					
-					case TREE_OPERATOR_ABOVE_STRICT:
-					$sJoinCond = "$sNodeLeft < $sRootLeft AND $sNodeRight > $sRootRight";
-					break;
-					
-					case TREE_OPERATOR_NOT_ABOVE: // Complementary of 'ABOVE'
-					$sJoinCond = "$sNodeLeft > $sRootLeft OR $sNodeRight < $sRootRight";
-					break;
-					
-					case TREE_OPERATOR_NOT_ABOVE_STRICT: // Complementary of ABOVE_STRICT
-					$sJoinCond = "$sNodeLeft >= $sRootLeft OR $sNodeRight <= $sRootRight";
-					break;
-					
-				}
-				$aFrom[$this->m_sTableAlias] = array("jointype"=>$aJoinData['jointype'], "tablename"=>$this->m_sTable, "joincondition"=>"$sJoinCond");
-				break;
-		}
-
-		// Given the alias, modify the fields and conditions
-		// before adding them into the current lists
-		//
-		foreach($this->m_aFields as $sAlias => $oExpression)
-		{
-			$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}`";
-		}
-		foreach($this->m_aValues as $sFieldName=>$value)
-		{
-			$aSetValues["`{$this->m_sTableAlias}`.`$sFieldName`"] = $value; // quoted further!
-		}
-
-  		if (!is_null($this->m_oSelectedIdField))
-  		{
-  			$aSelectedIdFields[] = $this->m_oSelectedIdField->Render();
-		}
-
-		// loop on joins, to complete the list of tables/fields/conditions
-		//
-		$aTempFrom = array(); // temporary subset of 'from' specs, to be grouped in the final query
-		foreach ($this->m_aJoinSelects as $aJoinData)
-		{
-			$oRightSelect = $aJoinData["select"];
-
-			$sJoinTableAlias = $oRightSelect->privRenderSingleTable($aTempFrom, $aFields, $aGroupBy, $aDelTables, $aSetValues, $aSelectedIdFields, $this->m_sTableAlias, $aJoinData);
-		}
-		$aFrom[$this->m_sTableAlias]['subfrom'] = $aTempFrom;
-
-		return $this->m_sTableAlias;
-	}
-
-	public function OptimizeJoins($aUsedTables, $bTopCall = true)
-	{
-		if ($bTopCall)
-		{
-			// Top call: complete the list of tables absolutely required to perform the right query
-			$this->CollectUsedTables($aUsedTables);
-		}
-
-		$aToDiscard = array();
-		foreach ($this->m_aJoinSelects as $i => $aJoinInfo)
-		{
-			$oSQLQuery = $aJoinInfo["select"];
-			$sTableAlias = $oSQLQuery->GetTableAlias();
-			if ($oSQLQuery->OptimizeJoins($aUsedTables, false) && !array_key_exists($sTableAlias, $aUsedTables))
-			{
-				$aToDiscard[] = $i;
-			}
-		}
-		foreach ($aToDiscard as $i)
-		{
-			unset($this->m_aJoinSelects[$i]);
-		}
-
-		return (count($this->m_aJoinSelects) == 0);
-	}
-
-	protected function CollectUsedTables(&$aTables)
-	{
-		$this->m_oConditionExpr->CollectUsedParents($aTables);
-		foreach($this->m_aFields as $sFieldAlias => $oField)
-		{
-			$oField->CollectUsedParents($aTables);
-		}
-		if ($this->m_aGroupBy)
-		{
-			foreach($this->m_aGroupBy as $sAlias => $oExpression)
-			{
-				$oExpression->CollectUsedParents($aTables);
-			}
-		}
-  		if (!is_null($this->m_oSelectedIdField))
-  		{
-  			$this->m_oSelectedIdField->CollectUsedParents($aTables);
-		}
-
-		foreach ($this->m_aJoinSelects as $i => $aJoinInfo)
-		{
-			$oSQLQuery = $aJoinInfo["select"];
-			if ($oSQLQuery->HasRequiredTables($aTables))
-			{
-				// There is something required in the branch, then this node is a MUST
-				if (isset($aJoinInfo['righttablealias']))
-				{
-					$aTables[$aJoinInfo['righttablealias']] = true;
-				}
-				if (isset($aJoinInfo["on_expression"]))
-				{
-					$sJoinCond = $aJoinInfo["on_expression"]->CollectUsedParents($aTables);
-				}
-			}
-		}
-
-		return $aTables;
-	}
-
-	// Is required in the JOIN, and therefore we must ensure that the join expression will be valid
-	protected function HasRequiredTables(&$aTables)
-	{
-		$bResult = false;
-		if (array_key_exists($this->m_sTableAlias, $aTables))
-		{
-			$bResult = true;
-		}
-		foreach ($this->m_aJoinSelects as $i => $aJoinInfo)
-		{
-			$oSQLQuery = $aJoinInfo["select"];
-			if ($oSQLQuery->HasRequiredTables($aTables))
-			{
-				// There is something required in the branch, then this node is a MUST
-				if (isset($aJoinInfo['righttablealias']))
-				{
-					$aTables[$aJoinInfo['righttablealias']] = true;
-				}
-				if (isset($aJoinInfo["on_expression"]))
-				{
-					$sJoinCond = $aJoinInfo["on_expression"]->CollectUsedParents($aTables);
-				}
-				$bResult = true;
-			}
-		}
-		// None of the tables is in the list of required tables
-		return $bResult;
-	}
 }
 }
-?>

+ 166 - 0
core/sqlunionquery.class.inc.php

@@ -0,0 +1,166 @@
+<?php
+// Copyright (C) 2015 Combodo SARL
+//
+//   This file is part of iTop.
+//
+//   iTop is free software; you can redistribute it and/or modify	
+//   it under the terms of the GNU Affero General Public License as published by
+//   the Free Software Foundation, either version 3 of the License, or
+//   (at your option) any later version.
+//
+//   iTop is distributed in the hope that it will be useful,
+//   but WITHOUT ANY WARRANTY; without even the implied warranty of
+//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+//   GNU Affero General Public License for more details.
+//
+//   You should have received a copy of the GNU Affero General Public License
+//   along with iTop. If not, see <http://www.gnu.org/licenses/>
+
+
+/**
+ * SQLUnionQuery
+ * build a mySQL compatible SQL query
+ *
+ * @copyright   Copyright (C) 2015 Combodo SARL
+ * @license     http://opensource.org/licenses/AGPL-3.0
+ */
+
+
+/**
+ * SQLUnionQuery
+ * build a mySQL compatible SQL query
+ *
+ * @package     iTopORM
+ */
+
+
+class SQLUnionQuery extends SQLQuery
+{
+	protected $aQueries;
+	protected $aGroupBy;
+
+	public function __construct($aQueries, $aGroupBy)
+	{
+		parent::__construct();
+
+		$this->aQueries = array();
+		foreach ($aQueries as $oSQLQuery)
+		{
+			$this->aQueries[] = $oSQLQuery->DeepClone();
+		}
+		$this->aGroupBy = $aGroupBy;
+	}
+
+	public function DisplayHtml()
+	{
+		$aQueriesHtml = array();
+		foreach ($this->aQueries as $oSQLQuery)
+		{
+			$aQueriesHtml[] = '<p>'.$oSQLQuery->DisplayHtml().'</p>';
+		}
+		echo implode('UNION', $aQueries);
+	}
+
+	public function AddInnerJoin($oSQLQuery, $sLeftField, $sRightField, $sRightTable = '')
+	{
+		foreach ($this->aQueries as $oSubSQLQuery)
+		{
+			$oSubSQLQuery->AddInnerJoin($oSQLQuery->DeepClone(), $sLeftField, $sRightField, $sRightTable = '');
+		}
+	}
+
+	public function RenderDelete($aArgs = array())
+	{
+		throw new Exception(__class__.'::'.__function__.'Not implemented !');
+	}
+
+	// Interface, build the SQL query
+	public function RenderUpdate($aArgs = array())
+	{
+		throw new Exception(__class__.'::'.__function__.'Not implemented !');
+	}
+
+	// Interface, build the SQL query
+	public function RenderSelect($aOrderBy = array(), $aArgs = array(), $iLimitCount = 0, $iLimitStart = 0, $bGetCount = false, $bBeautifulQuery = false)
+	{
+		$this->m_bBeautifulQuery = $bBeautifulQuery;
+		$sLineSep = $this->m_bBeautifulQuery ? "\n" : '';
+		$sIndent = $this->m_bBeautifulQuery ? "   " : null;
+
+		$aSelects = array();
+		foreach ($this->aQueries as $oSQLQuery)
+		{
+			// Render SELECTS without orderby/limit/count
+			$aSelects[] = $oSQLQuery->RenderSelect(array(), $aArgs, 0, 0, false, $bBeautifulQuery);
+		}
+		$sSelects = '('.implode(")$sLineSep UNION$sLineSep(", $aSelects).')';
+
+		if ($bGetCount)
+		{
+			$sFrom = "($sLineSep$sSelects$sLineSep) as __selects__";
+			$sSQL = "SELECT$sLineSep COUNT(*) AS COUNT$sLineSep FROM $sFrom$sLineSep";
+		}
+		else
+		{
+			$aSelects = array();
+			foreach ($this->aQueries as $oSQLQuery)
+			{
+				// Render SELECT without orderby/limit/count
+				$aSelects[] = $oSQLQuery->RenderSelect(array(), $aArgs, 0, 0, false, $bBeautifulQuery);
+			}
+			$sSelect = $this->aQueries[0]->RenderSelectClause();
+			$sOrderBy = $this->aQueries[0]->RenderOrderByClause($aOrderBy);
+			if (!empty($sOrderBy))
+			{
+				$sOrderBy = "ORDER BY $sOrderBy$sLineSep";
+			}
+			if ($iLimitCount > 0)
+			{
+				$sLimit = 'LIMIT '.$iLimitStart.', '.$iLimitCount;
+			}
+			else
+			{
+				$sLimit = '';
+			}
+			$sSQL = $sSelects.$sLineSep.$sOrderBy.' '.$sLimit;
+		}
+		return $sSQL;
+	}
+
+	// Interface, build the SQL query
+	public function RenderGroupBy($aArgs = array(), $bBeautifulQuery = false)
+	{
+		$this->m_bBeautifulQuery = $bBeautifulQuery;
+		$sLineSep = $this->m_bBeautifulQuery ? "\n" : '';
+		$sIndent = $this->m_bBeautifulQuery ? "   " : null;
+
+		$aSelects = array();
+		foreach ($this->aQueries as $oSQLQuery)
+		{
+			// Render SELECTS without orderby/limit/count
+			$aSelects[] = $oSQLQuery->RenderSelect(array(), $aArgs, 0, 0, false, $bBeautifulQuery);
+		}
+		$sSelects = '('.implode(")$sLineSep UNION$sLineSep(", $aSelects).')';
+		$sFrom = "($sLineSep$sSelects$sLineSep) as __selects__";
+
+		$aAliases = array();
+		foreach ($this->aGroupBy as $sGroupAlias => $trash)
+		{
+			$aAliases[] = "`$sGroupAlias`";
+		}
+		$sSelect = implode(', ', $aAliases);
+		$sGroupBy = implode(', ', $aAliases);
+
+		$sSQL = "SELECT $sSelect,$sLineSep COUNT(*) AS _itop_count_$sLineSep FROM $sFrom$sLineSep GROUP BY $sGroupBy";
+		return $sSQL;
+	}
+
+
+	public function OptimizeJoins($aUsedTables, $bTopCall = true)
+	{
+		foreach ($this->aQueries as $oSQLQuery)
+		{
+			$oSQLQuery->OptimizeJoins($aUsedTables);
+		}
+	}
+}

+ 4 - 4
core/userrights.class.inc.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-// Copyright (C) 2010-2013 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //
 //   This file is part of iTop.
 //   This file is part of iTop.
 //
 //
@@ -20,7 +20,7 @@
 /**
 /**
  * User rights management API
  * User rights management API
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -1045,7 +1045,7 @@ class ActionChecker
 	var $iAllowedCount = null;
 	var $iAllowedCount = null;
 	var $aAllowedIDs = null;
 	var $aAllowedIDs = null;
 	
 	
-	public function __construct(DBObjectSearch $oFilter, $iActionCode)
+	public function __construct(DBSearch $oFilter, $iActionCode)
 	{
 	{
 		$this->oFilter = $oFilter;
 		$this->oFilter = $oFilter;
 		$this->iActionCode = $iActionCode;
 		$this->iActionCode = $iActionCode;
@@ -1125,7 +1125,7 @@ class StimulusChecker extends ActionChecker
 {
 {
 	var $sState = null;
 	var $sState = null;
 	
 	
-	public function __construct(DBObjectSearch $oFilter, $sState, $iStimulusCode)
+	public function __construct(DBSearch $oFilter, $sState, $iStimulusCode)
 	{
 	{
 		parent::__construct($oFilter, $iStimulusCode);
 		parent::__construct($oFilter, $iStimulusCode);
 		$this->sState = $sState;
 		$this->sState = $sState;

+ 5 - 5
core/valuesetdef.class.inc.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //
 //   This file is part of iTop.
 //   This file is part of iTop.
 //
 //
@@ -20,7 +20,7 @@
 /**
 /**
  * Value set definitions (from a fixed list or from a query, etc.)
  * Value set definitions (from a fixed list or from a query, etc.)
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -119,7 +119,7 @@ class ValueSetObjects extends ValueSetDefinition
 		$this->m_aModifierProperties[$sPluginClass][$sProperty] = $value;
 		$this->m_aModifierProperties[$sPluginClass][$sProperty] = $value;
 	}
 	}
 
 
-	public function AddCondition(DBObjectSearch $oFilter)
+	public function AddCondition(DBSearch $oFilter)
 	{
 	{
 		$this->m_aExtraConditions[] = $oFilter;		
 		$this->m_aExtraConditions[] = $oFilter;		
 	}
 	}
@@ -136,7 +136,7 @@ class ValueSetObjects extends ValueSetDefinition
 		}
 		}
 		foreach($this->m_aExtraConditions as $oExtraFilter)
 		foreach($this->m_aExtraConditions as $oExtraFilter)
 		{
 		{
-			$oFilter->MergeWith($oExtraFilter);
+			$oFilter = $oFilter->Intersect($oExtraFilter);
 		}
 		}
 		foreach($this->m_aModifierProperties as $sPluginClass => $aProperties)
 		foreach($this->m_aModifierProperties as $sPluginClass => $aProperties)
 		{
 		{
@@ -178,7 +178,7 @@ class ValueSetObjects extends ValueSetDefinition
 		if (!$oFilter) return false;
 		if (!$oFilter) return false;
 		foreach($this->m_aExtraConditions as $oExtraFilter)
 		foreach($this->m_aExtraConditions as $oExtraFilter)
 		{
 		{
-			$oFilter->MergeWith($oExtraFilter);
+			$oFilter = $oFilter->Intersect($oExtraFilter);
 		}
 		}
 		foreach($this->m_aModifierProperties as $sPluginClass => $aProperties)
 		foreach($this->m_aModifierProperties as $sPluginClass => $aProperties)
 		{
 		{

+ 3 - 3
pages/UI.php

@@ -20,7 +20,7 @@
 /**
 /**
  * Main page of iTop
  * Main page of iTop
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -144,7 +144,7 @@ function DisplayDetails($oP, $sClass, $oObj, $id)
 /**
 /**
  * Displays the result of a search request
  * Displays the result of a search request
  * @param $oP WebPage Web page for the output
  * @param $oP WebPage Web page for the output
- * @param $oFilter DBObjectSearch The search of objects to display
+ * @param $oFilter DBSearch The search of objects to display
  * @param $bSearchForm boolean Whether or not to display the search form at the top the page
  * @param $bSearchForm boolean Whether or not to display the search form at the top the page
  * @param $sBaseClass string The base class for the search (can be different from the actual class of the results)
  * @param $sBaseClass string The base class for the search (can be different from the actual class of the results)
  * @param $sFormat string The format to use for the output: csv or html
  * @param $sFormat string The format to use for the output: csv or html
@@ -182,7 +182,7 @@ function DisplaySearchSet($oP, $oFilter, $bSearchForm = true, $sBaseClass = '',
  * Displays a form (checkboxes) to select the objects for which to apply a given action
  * Displays a form (checkboxes) to select the objects for which to apply a given action
  * Only the objects for which the action is valid can be checked. By default all valid objects are checked
  * Only the objects for which the action is valid can be checked. By default all valid objects are checked
  * @param $oP WebPage The page for output
  * @param $oP WebPage The page for output
- * @param $oFilter DBObjectSearch The filter that defines the list of objects
+ * @param $oFilter DBSearch The filter that defines the list of objects
   * @param $sNextOperation string The next operation (code) to be executed when the form is submitted
   * @param $sNextOperation string The next operation (code) to be executed when the form is submitted
  * @param $oChecker ActionChecker The helper class/instance used to check for which object the action is valid
  * @param $oChecker ActionChecker The helper class/instance used to check for which object the action is valid
  * @return none
  * @return none

+ 4 - 5
pages/audit.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //
 //   This file is part of iTop.
 //   This file is part of iTop.
 //
 //
@@ -19,13 +19,13 @@
 /**
 /**
  * Execute and shows the data quality audit
  * Execute and shows the data quality audit
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 /**
 /**
  * Adds the context parameters to the audit query
  * Adds the context parameters to the audit query
  */
  */
-function FilterByContext(DBObjectSearch &$oFilter, ApplicationContext $oAppContext)
+function FilterByContext(DBSearch &$oFilter, ApplicationContext $oAppContext)
 {
 {
 	$sObjClass = $oFilter->GetClass();		
 	$sObjClass = $oFilter->GetClass();		
 	$aContextParams = $oAppContext->GetNames();
 	$aContextParams = $oAppContext->GetNames();
@@ -93,8 +93,7 @@ function GetRuleResultFilter($iRuleId, $oDefinitionFilter, $oAppContext)
 	if ($oRule->Get('valid_flag') == 'false')
 	if ($oRule->Get('valid_flag') == 'false')
 	{
 	{
 		// The query returns directly the invalid elements
 		// The query returns directly the invalid elements
-		$oFilter = $oRuleFilter;
-		$oFilter->MergeWith($oDefinitionFilter);
+		$oFilter = $oRuleFilter->Intersect($oDefinitionFilter);
 	}
 	}
 	else
 	else
 	{
 	{

+ 58 - 56
setup/runtimeenv.class.inc.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //
 //   This file is part of iTop.
 //   This file is part of iTop.
 //
 //
@@ -20,7 +20,7 @@
 /**
 /**
  * Manage a runtime environment
  * Manage a runtime environment
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -78,8 +78,10 @@ class RunTimeEnvironment
 		require_once(APPROOT.'/core/expression.class.inc.php');
 		require_once(APPROOT.'/core/expression.class.inc.php');
 		require_once(APPROOT.'/core/cmdbsource.class.inc.php');
 		require_once(APPROOT.'/core/cmdbsource.class.inc.php');
 		require_once(APPROOT.'/core/sqlquery.class.inc.php');
 		require_once(APPROOT.'/core/sqlquery.class.inc.php');
+		require_once(APPROOT.'/core/sqlobjectquery.class.inc.php');
+		require_once(APPROOT.'/core/sqlunionquery.class.inc.php');
 		require_once(APPROOT.'/core/dbobject.class.php');
 		require_once(APPROOT.'/core/dbobject.class.php');
-		require_once(APPROOT.'/core/dbobjectsearch.class.php');
+		require_once(APPROOT.'/core/dbsearch.class.php');
 		require_once(APPROOT.'/core/dbobjectset.class.php');
 		require_once(APPROOT.'/core/dbobjectset.class.php');
 		require_once(APPROOT.'/application/cmdbabstract.class.inc.php');
 		require_once(APPROOT.'/application/cmdbabstract.class.inc.php');
 		require_once(APPROOT.'/core/userrights.class.inc.php');
 		require_once(APPROOT.'/core/userrights.class.inc.php');
@@ -498,59 +500,59 @@ class RunTimeEnvironment
 			}
 			}
 		}
 		}
 		return true;
 		return true;
-	}
-
-	public function UpdatePredefinedObjects()
-	{
-		// Constant classes (e.g. User profiles)
-		//
-		foreach (MetaModel::GetClasses() as $sClass)
-		{
-			$aPredefinedObjects = call_user_func(array(
-				$sClass,
-				'GetPredefinedObjects'
-			));
-			if ($aPredefinedObjects != null)
-			{
-				$this->log_info("$sClass::GetPredefinedObjects() returned " . count($aPredefinedObjects) . " elements.");
-				
-				// Create/Delete/Update objects of this class,
-				// according to the given constant values
-				//
-				$aDBIds = array();
-				$oAll = new DBObjectSet(new DBObjectSearch($sClass));
-				while ($oObj = $oAll->Fetch())
-				{
-					if (array_key_exists($oObj->GetKey(), $aPredefinedObjects))
-					{
-						$aObjValues = $aPredefinedObjects[$oObj->GetKey()];
-						foreach ($aObjValues as $sAttCode => $value)
-						{
-							$oObj->Set($sAttCode, $value);
-						}
-						$oObj->DBUpdate();
-						$aDBIds[$oObj->GetKey()] = true;
-					}
-					else
-					{
-						$oObj->DBDelete();
-					}
-				}
-				foreach ($aPredefinedObjects as $iRefId => $aObjValues)
-				{
-					if (! array_key_exists($iRefId, $aDBIds))
-					{
-						$oNewObj = MetaModel::NewObject($sClass);
-						$oNewObj->SetKey($iRefId);
-						foreach ($aObjValues as $sAttCode => $value)
-						{
-							$oNewObj->Set($sAttCode, $value);
-						}
-						$oNewObj->DBInsert();
-					}
-				}
-			}
-		}
+	}
+
+	public function UpdatePredefinedObjects()
+	{
+		// Constant classes (e.g. User profiles)
+		//
+		foreach (MetaModel::GetClasses() as $sClass)
+		{
+			$aPredefinedObjects = call_user_func(array(
+				$sClass,
+				'GetPredefinedObjects'
+			));
+			if ($aPredefinedObjects != null)
+			{
+				$this->log_info("$sClass::GetPredefinedObjects() returned " . count($aPredefinedObjects) . " elements.");
+				
+				// Create/Delete/Update objects of this class,
+				// according to the given constant values
+				//
+				$aDBIds = array();
+				$oAll = new DBObjectSet(new DBObjectSearch($sClass));
+				while ($oObj = $oAll->Fetch())
+				{
+					if (array_key_exists($oObj->GetKey(), $aPredefinedObjects))
+					{
+						$aObjValues = $aPredefinedObjects[$oObj->GetKey()];
+						foreach ($aObjValues as $sAttCode => $value)
+						{
+							$oObj->Set($sAttCode, $value);
+						}
+						$oObj->DBUpdate();
+						$aDBIds[$oObj->GetKey()] = true;
+					}
+					else
+					{
+						$oObj->DBDelete();
+					}
+				}
+				foreach ($aPredefinedObjects as $iRefId => $aObjValues)
+				{
+					if (! array_key_exists($iRefId, $aDBIds))
+					{
+						$oNewObj = MetaModel::NewObject($sClass);
+						$oNewObj->SetKey($iRefId);
+						foreach ($aObjValues as $sAttCode => $value)
+						{
+							$oNewObj->Set($sAttCode, $value);
+						}
+						$oNewObj->DBInsert();
+					}
+				}
+			}
+		}
 	}
 	}
 	
 	
 	public function RecordInstallation(Config $oConfig, $sDataModelVersion, $aSelectedModules, $sModulesRelativePath, $sShortComment = null)
 	public function RecordInstallation(Config $oConfig, $sDataModelVersion, $aSelectedModules, $sModulesRelativePath, $sShortComment = null)

+ 1 - 1
test/benchmark.php

@@ -866,6 +866,6 @@ catch(ZZCoreException $e)
 	$oP->error("Error: '".$e->getHtmlDesc()."'");	
 	$oP->error("Error: '".$e->getHtmlDesc()."'");	
 }
 }
 $oKPI->ComputeAndReport('Total execution');
 $oKPI->ComputeAndReport('Total execution');
-//MetaModel::RecordQueryTrace();
+//DBSearch::RecordQueryTrace();
 $oP->output();
 $oP->output();
 ?>
 ?>

+ 4 - 4
test/replay_query_log.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //
 //   This file is part of iTop.
 //   This file is part of iTop.
 //
 //
@@ -19,7 +19,7 @@
 /**
 /**
  * Replay the query log made when log_queries = 1
  * Replay the query log made when log_queries = 1
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -104,7 +104,7 @@ class QueryLogEntry
 			{
 			{
 				for($i = 0 ; $i < $iRepeat ; $i++)
 				for($i = 0 ; $i < $iRepeat ; $i++)
 				{
 				{
-					$this->sSql = MetaModel::MakeSelectQuery($this->oFilter, $this->aOrderBy, $this->aArgs, $this->aAttToLoad, $this->aExtendedDataSpec, $this->iLimitCount, $this->iLimitStart, $this->bGetCount);
+					$this->sSql = $this->oFilter->MakeSelectQuery($this->aOrderBy, $this->aArgs, $this->aAttToLoad, $this->aExtendedDataSpec, $this->iLimitCount, $this->iLimitStart, $this->bGetCount);
 				}
 				}
 			}
 			}
 			catch(Exception $e)
 			catch(Exception $e)
@@ -130,7 +130,7 @@ class QueryLogEntry
 			{
 			{
 				for($i = 0 ; $i < $iRepeat ; $i++)
 				for($i = 0 ; $i < $iRepeat ; $i++)
 				{
 				{
-					$this->sSql = MetaModel::MakeGroupByQuery($this->oFilter, $this->aArgs, $this->aGroupByExpr);
+					$this->sSql = $this->oFilter->MakeGroupByQuery($this->aArgs, $this->aGroupByExpr);
 				}
 				}
 			}
 			}
 			catch(Exception $e)
 			catch(Exception $e)

+ 7 - 5
test/test.class.inc.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //
 //   This file is part of iTop.
 //   This file is part of iTop.
 //
 //
@@ -19,7 +19,7 @@
 /**
 /**
  * Core automated tests - basics
  * Core automated tests - basics
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -33,12 +33,14 @@ require_once(APPROOT.'/core/MyHelpers.class.inc.php');
 require_once(APPROOT.'/core/expression.class.inc.php');
 require_once(APPROOT.'/core/expression.class.inc.php');
 require_once(APPROOT.'/core/cmdbsource.class.inc.php');
 require_once(APPROOT.'/core/cmdbsource.class.inc.php');
 require_once(APPROOT.'/core/sqlquery.class.inc.php');
 require_once(APPROOT.'/core/sqlquery.class.inc.php');
+require_once(APPROOT.'/core/sqlobjectquery.class.inc.php');
+require_once(APPROOT.'/core/sqlunionquery.class.inc.php');
 
 
 require_once(APPROOT.'/core/log.class.inc.php');
 require_once(APPROOT.'/core/log.class.inc.php');
 require_once(APPROOT.'/core/kpi.class.inc.php');
 require_once(APPROOT.'/core/kpi.class.inc.php');
 
 
 require_once(APPROOT.'/core/dbobject.class.php');
 require_once(APPROOT.'/core/dbobject.class.php');
-require_once(APPROOT.'/core/dbobjectsearch.class.php');
+require_once(APPROOT.'/core/dbsearch.class.php');
 require_once(APPROOT.'/core/dbobjectset.class.php');
 require_once(APPROOT.'/core/dbobjectset.class.php');
 
 
 require_once(APPROOT.'/application/cmdbabstract.class.inc.php');
 require_once(APPROOT.'/application/cmdbabstract.class.inc.php');
@@ -508,10 +510,10 @@ abstract class TestBizModel extends TestHandler
 		echo MyHelpers::make_table_from_assoc_array($aData);
 		echo MyHelpers::make_table_from_assoc_array($aData);
 	}
 	}
 
 
-	static protected function search_and_show_list(DBObjectSearch $oMyFilter)
+	static protected function search_and_show_list(DBSearch $oMyFilter)
 	{
 	{
 		$oObjSet = new CMDBObjectSet($oMyFilter);
 		$oObjSet = new CMDBObjectSet($oMyFilter);
-		echo $oMyFilter->__DescribeHTML()."' - Found ".$oObjSet->Count()." items.</br>\n";
+		echo $oMyFilter->ToOQL()."' - Found ".$oObjSet->Count()." items.</br>\n";
 		self::show_list($oObjSet);
 		self::show_list($oObjSet);
 	}
 	}
 
 

+ 2 - 0
test/test.php

@@ -90,6 +90,7 @@ function DisplayEvents($aEvents, $sTitle)
 // Main
 // Main
 ///////////////////////////////////////////////////////////////////////////////
 ///////////////////////////////////////////////////////////////////////////////
 
 
+date_default_timezone_set('Europe/Paris');
 
 
 require_once('../approot.inc.php');
 require_once('../approot.inc.php');
 require_once(APPROOT.'/application/utils.inc.php');
 require_once(APPROOT.'/application/utils.inc.php');
@@ -98,6 +99,7 @@ require_once('./testlist.inc.php');
 
 
 require_once(APPROOT.'/core/cmdbobject.class.inc.php');
 require_once(APPROOT.'/core/cmdbobject.class.inc.php');
 
 
+
 $sTodo = utils::ReadParam("todo", "");
 $sTodo = utils::ReadParam("todo", "");
 if ($sTodo == '')
 if ($sTodo == '')
 {
 {

+ 22 - 14
test/testlist.inc.php

@@ -1,5 +1,5 @@
 <?php
 <?php
-// Copyright (C) 2010-2012 Combodo SARL
+// Copyright (C) 2010-2015 Combodo SARL
 //
 //
 //   This file is part of iTop.
 //   This file is part of iTop.
 //
 //
@@ -19,7 +19,7 @@
 /**
 /**
  * Core test list
  * Core test list
  *
  *
- * @copyright   Copyright (C) 2010-2012 Combodo SARL
+ * @copyright   Copyright (C) 2010-2015 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
  */
 
 
@@ -46,7 +46,7 @@ class TestSQLQuery extends TestScenarioOnDB
 
 
 	protected function DoExecute()
 	protected function DoExecute()
 	{
 	{
-		$oQuery = new SQLQuery(
+		$oQuery = new SQLObjectQuery(
 			$sTable = 'myTable',
 			$sTable = 'myTable',
 			$sTableAlias = 'myTableAlias',
 			$sTableAlias = 'myTableAlias',
 			$aFields = array('column1'=>new FieldExpression('column1', 'myTableAlias'), 'column2'=>new FieldExpression('column2', 'myTableAlias')),
 			$aFields = array('column1'=>new FieldExpression('column1', 'myTableAlias'), 'column2'=>new FieldExpression('column2', 'myTableAlias')),
@@ -56,7 +56,7 @@ class TestSQLQuery extends TestScenarioOnDB
 		);
 		);
 		$oQuery->AddCondition(Expression::FromOQL('DATE(NOW() - 1200 * 2) > \'2008-07-31\''));
 		$oQuery->AddCondition(Expression::FromOQL('DATE(NOW() - 1200 * 2) > \'2008-07-31\''));
 
 
-		$oSubQuery1 = new SQLQuery(
+		$oSubQuery1 = new SQLObjectQuery(
 			$sTable = 'myTable1',
 			$sTable = 'myTable1',
 			$sTableAlias = 'myTable1Alias',
 			$sTableAlias = 'myTable1Alias',
 			$aFields = array('column1_1'=>new FieldExpression('column1', 'myTableAlias'), 'column1_2'=>new FieldExpression('column1', 'myTableAlias')),
 			$aFields = array('column1_1'=>new FieldExpression('column1', 'myTableAlias'), 'column1_2'=>new FieldExpression('column1', 'myTableAlias')),
@@ -65,7 +65,7 @@ class TestSQLQuery extends TestScenarioOnDB
 			$aValues = array()
 			$aValues = array()
 		);
 		);
 
 
-		$oSubQuery2 = new SQLQuery(
+		$oSubQuery2 = new SQLObjectQuery(
 			$sTable = 'myTable2',
 			$sTable = 'myTable2',
 			$sTableAlias = 'myTable2Alias',
 			$sTableAlias = 'myTable2Alias',
 			$aFields = array('column2_1'=>new FieldExpression('column2', 'myTableAlias'), 'column2_2'=>new FieldExpression('column2', 'myTableAlias')),
 			$aFields = array('column2_1'=>new FieldExpression('column2', 'myTableAlias'), 'column2_2'=>new FieldExpression('column2', 'myTableAlias')),
@@ -231,6 +231,14 @@ class TestOQLParser extends TestFunction
 			'SELECT A JOIN B ON A.myB = B.id JOIN C ON C.parent_id NOT BELOW B.id WHERE A.col1 = 2 AND B.id = 3' => true,
 			'SELECT A JOIN B ON A.myB = B.id JOIN C ON C.parent_id NOT BELOW B.id WHERE A.col1 = 2 AND B.id = 3' => true,
 			'SELECT A JOIN B ON A.myB = B.id JOIN C ON C.parent_id NOT BELOW STRICT B.id WHERE A.col1 = 2 AND B.id = 3' => true,
 			'SELECT A JOIN B ON A.myB = B.id JOIN C ON C.parent_id NOT BELOW STRICT B.id WHERE A.col1 = 2 AND B.id = 3' => true,
 			'SELECT A JOIN B ON A.myB = B.id JOIN C ON C.parent_id = B.id WHERE A.col1 BELOW 2 AND B.id = 3' => false,
 			'SELECT A JOIN B ON A.myB = B.id JOIN C ON C.parent_id = B.id WHERE A.col1 BELOW 2 AND B.id = 3' => false,
+
+			// Unions
+			//
+			'SELECT A UNION SELECT B' => true,
+			'SELECT A WHERE A.b = "sdf" UNION SELECT B WHERE B.a = "sfde"' => true,
+			'SELECT A UNION SELECT B UNION SELECT C' => true,
+			'SELECT A UNION SELECT B UNION SELECT C UNION SELECT D' => true,
+			'SELECT A JOIN B ON A.myB = B.id JOIN C ON C.parent_id NOT BELOW B.id WHERE A.col1 = 2 AND B.id = 3 UNION SELECT Device JOIN Site ON Device.site = Site.id JOIN Country ON Site.location = Country.id' => true,
 		);
 		);
 
 
 		$iErrors = 0;
 		$iErrors = 0;
@@ -622,9 +630,9 @@ class TestMyBizModel extends TestBizModel
 		$oMyChange->Set("userinfo", "test_setattribute / Made by robot #".rand(1,100));
 		$oMyChange->Set("userinfo", "test_setattribute / Made by robot #".rand(1,100));
 		$iChangeId = $oMyChange->DBInsert();
 		$iChangeId = $oMyChange->DBInsert();
 	
 	
-		//MetaModel::StartDebugQuery();
+		//DBSearch::StartDebugQuery();
 		$team->DBUpdateTracked($oMyChange);
 		$team->DBUpdateTracked($oMyChange);
-		//MetaModel::StopDebugQuery();
+		//DBSearch::StopDebugQuery();
 	
 	
 		echo "<h4>Check the modified team</h4>";
 		echo "<h4>Check the modified team</h4>";
 		$oTeam = MetaModel::GetObject("cmdbTeam", "2");
 		$oTeam = MetaModel::GetObject("cmdbTeam", "2");
@@ -954,7 +962,7 @@ class TestQueriesOnFarm extends MyFarm
 		try
 		try
 		{
 		{
 			//$oOql = new OqlInterpreter($sQuery);
 			//$oOql = new OqlInterpreter($sQuery);
-			//$oTrash = $oOql->ParseObjectQuery();
+			//$oTrash = $oOql->ParseQuery();
 			//self::DumpVariable($oTrash, true);
 			//self::DumpVariable($oTrash, true);
 			$oMyFilter = DBObjectSearch::FromOQL($sQuery);
 			$oMyFilter = DBObjectSearch::FromOQL($sQuery);
 		}
 		}
@@ -991,17 +999,17 @@ class TestQueriesOnFarm extends MyFarm
 		
 		
 		//echo "<p>first pass<p>\n";
 		//echo "<p>first pass<p>\n";
 		//self::DumpVariable($oMyFilter, true);
 		//self::DumpVariable($oMyFilter, true);
-		$sQuery1 = MetaModel::MakeSelectQuery($oMyFilter);
+		$sQuery1 = $oMyFilter->MakeSelectQuery();
 		//echo "<p>second pass<p>\n";
 		//echo "<p>second pass<p>\n";
 		//self::DumpVariable($oMyFilter, true);
 		//self::DumpVariable($oMyFilter, true);
-		//$sQuery1 = MetaModel::MakeSelectQuery($oMyFilter);
+		//$sQuery1 = $oMyFilter->MakeSelectQuery();
 		
 		
 		$sSerialize = $oMyFilter->serialize();
 		$sSerialize = $oMyFilter->serialize();
 		echo "<p>Serialized:$sSerialize</p>\n";
 		echo "<p>Serialized:$sSerialize</p>\n";
 		$oFilter2 = DBObjectSearch::unserialize($sSerialize);
 		$oFilter2 = DBObjectSearch::unserialize($sSerialize);
 		try
 		try
 		{
 		{
-			$sQuery2 = MetaModel::MakeSelectQuery($oFilter2);
+			$sQuery2 = $oMyFilter2->MakeSelectQuery();
 		}
 		}
 		catch (Exception $e)
 		catch (Exception $e)
 		{
 		{
@@ -1282,7 +1290,7 @@ class TestItopEfficiency extends TestBizModel
 		$fStart = MyHelpers::getmicrotime();
 		$fStart = MyHelpers::getmicrotime();
 		for($i=0 ; $i < COUNT_BENCHMARK ; $i++)
 		for($i=0 ; $i < COUNT_BENCHMARK ; $i++)
 		{
 		{
-			$sSQL = MetaModel::MakeSelectQuery($oFilter);
+			$sSQL = $oFilter->MakeSelectQuery();
 		}
 		}
 		$fDuration = MyHelpers::getmicrotime() - $fStart;
 		$fDuration = MyHelpers::getmicrotime() - $fStart;
 		$fBuildDuration = $fDuration / COUNT_BENCHMARK;
 		$fBuildDuration = $fDuration / COUNT_BENCHMARK;
@@ -1399,7 +1407,7 @@ class TestQueries extends TestBizModel
 		$fParsingDuration = MyHelpers::getmicrotime() - $fStart;
 		$fParsingDuration = MyHelpers::getmicrotime() - $fStart;
 
 
 		$fStart = MyHelpers::getmicrotime();
 		$fStart = MyHelpers::getmicrotime();
-		$sSQL = MetaModel::MakeSelectQuery($oFilter);
+		$sSQL = $oFilter->MakeSelectQuery();
 		$fBuildDuration = MyHelpers::getmicrotime() - $fStart;
 		$fBuildDuration = MyHelpers::getmicrotime() - $fStart;
 
 
 		$fStart = MyHelpers::getmicrotime();
 		$fStart = MyHelpers::getmicrotime();
@@ -1570,7 +1578,7 @@ $oSearch->AddCondition_PointingTo($oOrgSearch, "org_id");
 			print_r($oSearch);
 			print_r($oSearch);
 			echo "</pre>";
 			echo "</pre>";
 
 
-			$sSQL = MetaModel::MakeSelectQuery($oSearch);
+			$sSQL = $oSearch->MakeSelectQuery();
 			$res = CMDBSource::Query($sSQL);
 			$res = CMDBSource::Query($sSQL);
 			foreach (CMDBSource::ExplainQuery($sSQL) as $aRow)
 			foreach (CMDBSource::ExplainQuery($sSQL) as $aRow)
 			{
 			{

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels