Browse Source

OQL: fixed an old limitation, hierarchies can now be expressed both ways. Example of a query that now works fine: SELECT Organization AS root JOIN Organization AS child ON child.parent_id BELOW root.id WHERE child.name LIKE 'Combodo'. In the previous implementation, the operator was interpreted as '='.

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@4233 a333f486-631f-4898-b8df-5754b55c2be0
romainq 9 years ago
parent
commit
1d69c5f27c

+ 173 - 131
core/dbobjectsearch.class.php

@@ -143,9 +143,15 @@ class DBObjectSearch extends DBSearch
 		}
 		foreach($this->m_aReferencedBy as $sForeignClass => $aReferences)
 		{
-			foreach($aReferences as $sForeignExtKeyAttCode => $oForeignFilter)
+			foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator)
 			{
-				$oForeignFilter->ChangeClass($sNewClass, $sAlias);
+				foreach ($aFiltersByOperator as $iOperatorCode => $aFilters)
+				{
+					foreach ($aFilters as $oForeignFilter)
+					{
+						$oForeignFilter->ChangeClass($sNewClass, $sAlias);
+					}
+				}
 			}
 		}
 	}
@@ -237,9 +243,15 @@ class DBObjectSearch extends DBSearch
 		}
 		foreach($this->m_aReferencedBy as $sForeignClass => $aReferences)
 		{
-			foreach($aReferences as $sForeignExtKeyAttCode => $oForeignFilter)
+			foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator)
 			{
-				$oForeignFilter->RenameParam($sOldName, $sNewName);
+				foreach ($aFiltersByOperator as $iOperatorCode => $aFilters)
+				{
+					foreach ($aFilters as $oForeignFilter)
+					{
+						$oForeignFilter->RenameParam($sOldName, $sNewName);
+					}
+				}
 			}
 		}
 	}
@@ -482,9 +494,15 @@ class DBObjectSearch extends DBSearch
 
 		foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences)
 		{
-			foreach($aReferences as $sForeignExtKeyAttCode=>$oForeignFilter)
+			foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator)
 			{
-				$oForeignFilter->AddToNameSpace($aClassAliases, $aAliasTranslation);
+				foreach ($aFiltersByOperator as $iOperatorCode => $aFilters)
+				{
+					foreach ($aFilters as $oForeignFilter)
+					{
+						$oForeignFilter->AddToNameSpace($aClassAliases, $aAliasTranslation);
+					}
+				}
 			}
 		}
 	}
@@ -516,12 +534,18 @@ class DBObjectSearch extends DBSearch
 			}
 			foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences)
 			{
-				foreach($aReferences as $sForeignExtKeyAttCode=>$oForeignFilter)
+				foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator)
 				{
-					$ret = $oForeignFilter->GetNode($sAlias);
-					if (is_object($ret))
+					foreach ($aFiltersByOperator as $iOperatorCode => $aFilters)
 					{
-						return $ret;
+						foreach ($aFilters as $oForeignFilter)
+						{
+							$ret = $oForeignFilter->GetNode($sAlias);
+							if (is_object($ret))
+							{
+								return $ret;
+							}
+						}
 					}
 				}
 			}
@@ -567,7 +591,7 @@ class DBObjectSearch extends DBSearch
 		$oReceivingFilter->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode][] = $oFilter;
 	}
 
-	public function AddCondition_ReferencedBy(DBObjectSearch $oFilter, $sForeignExtKeyAttCode)
+	public function AddCondition_ReferencedBy(DBObjectSearch $oFilter, $sForeignExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS)
 	{
 		$sForeignClass = $oFilter->GetClass();
 		if (!MetaModel::IsValidKeyAttCode($sForeignClass, $sForeignExtKeyAttCode))
@@ -587,32 +611,20 @@ class DBObjectSearch extends DBSearch
 		// NO: $oFilter = $oFilter->DeepClone();
 		// See also: Trac #639, and self::AddCondition_PointingTo()
 		$aAliasTranslation = array();
-		$res = $this->AddCondition_ReferencedBy_InNameSpace($oFilter, $sForeignExtKeyAttCode, $this->m_aClasses, $aAliasTranslation);
+		$res = $this->AddCondition_ReferencedBy_InNameSpace($oFilter, $sForeignExtKeyAttCode, $this->m_aClasses, $aAliasTranslation, $iOperatorCode);
 		$this->TransferConditionExpression($oFilter, $aAliasTranslation);
 		return $res;
 	}
 
-	protected function AddCondition_ReferencedBy_InNameSpace(DBSearch $oFilter, $sForeignExtKeyAttCode, &$aClassAliases, &$aAliasTranslation)
+	protected function AddCondition_ReferencedBy_InNameSpace(DBSearch $oFilter, $sForeignExtKeyAttCode, &$aClassAliases, &$aAliasTranslation, $iOperatorCode)
 	{
 		$sForeignClass = $oFilter->GetClass();
 
 		// Find the node on which the new tree must be attached (most of the time it is "this")
 		$oReceivingFilter = $this->GetNode($this->GetClassAlias());
 
-		if (array_key_exists($sForeignClass, $this->m_aReferencedBy) && array_key_exists($sForeignExtKeyAttCode, $this->m_aReferencedBy[$sForeignClass]))
-		{
-			$oReceivingFilter->m_aReferencedBy[$sForeignClass][$sForeignExtKeyAttCode]->MergeWith_InNamespace($oFilter, $aClassAliases, $aAliasTranslation);
-		}
-		else
-		{
-			$oFilter->AddToNamespace($aClassAliases, $aAliasTranslation);
-
-			// #@# The condition expression found in that filter should not be used - could be another kind of structure like a join spec tree !!!!
-			//$oNewFilter = $oFilter->DeepClone();
-			//$oNewFilter->ResetCondition();
-
-			$oReceivingFilter->m_aReferencedBy[$sForeignClass][$sForeignExtKeyAttCode]= $oFilter;
-		}
+		$oFilter->AddToNamespace($aClassAliases, $aAliasTranslation);
+		$oReceivingFilter->m_aReferencedBy[$sForeignClass][$sForeignExtKeyAttCode][$iOperatorCode][] = $oFilter;
 	}
 
 	public function Intersect(DBSearch $oFilter)
@@ -701,16 +713,22 @@ class DBObjectSearch extends DBSearch
 		}
 		foreach($oFilter->m_aReferencedBy as $sForeignClass => $aReferences)
 		{
-			foreach($aReferences as $sForeignExtKeyAttCode => $oForeignFilter)
+			foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator)
 			{
-				$this->AddCondition_ReferencedBy_InNamespace($oForeignFilter, $sForeignExtKeyAttCode, $aClassAliases, $aAliasTranslation);
+				foreach ($aFiltersByOperator as $iOperatorCode => $aFilters)
+				{
+					foreach ($aFilters as $oForeignFilter)
+					{
+						$this->AddCondition_ReferencedBy_InNamespace($oForeignFilter, $sForeignExtKeyAttCode, $aClassAliases, $aAliasTranslation, $iOperatorCode);
+					}
+				}
 			}
 		}
 	}
 
 	public function GetCriteria() {return $this->m_oSearchCondition;}
 	public function GetCriteria_FullText() {return $this->m_aFullText;}
-	public function GetCriteria_PointingTo($sKeyAttCode = "")
+	protected function GetCriteria_PointingTo($sKeyAttCode = "")
 	{
 		if (empty($sKeyAttCode))
 		{
@@ -719,19 +737,9 @@ class DBObjectSearch extends DBSearch
 		if (!array_key_exists($sKeyAttCode, $this->m_aPointingTo)) return array();
 		return $this->m_aPointingTo[$sKeyAttCode];
 	}
-	public function GetCriteria_ReferencedBy($sRemoteClass = "", $sForeignExtKeyAttCode = "")
+	protected function GetCriteria_ReferencedBy()
 	{
-		if (empty($sRemoteClass))
-		{
-			return $this->m_aReferencedBy;
-		}
-		if (!array_key_exists($sRemoteClass, $this->m_aReferencedBy)) return null;
-		if (empty($sForeignExtKeyAttCode))
-		{
-			return $this->m_aReferencedBy[$sRemoteClass];
-		}
-		if (!array_key_exists($sForeignExtKeyAttCode, $this->m_aReferencedBy[$sRemoteClass])) return null;
-		return $this->m_aReferencedBy[$sRemoteClass][$sForeignExtKeyAttCode];
+		return $this->m_aReferencedBy;
 	}
 
 	public function SetInternalParams($aParams)
@@ -845,6 +853,50 @@ class DBObjectSearch extends DBSearch
 		return $sRes;
 	}
 
+	protected function OperatorCodeToOQL($iOperatorCode)
+	{
+		switch($iOperatorCode)
+		{
+			case TREE_OPERATOR_EQUALS:
+				$sOperator = ' = ';
+				break;
+
+			case TREE_OPERATOR_BELOW:
+				$sOperator = ' BELOW ';
+				break;
+
+			case TREE_OPERATOR_BELOW_STRICT:
+				$sOperator = ' BELOW STRICT ';
+				break;
+
+			case TREE_OPERATOR_NOT_BELOW:
+				$sOperator = ' NOT BELOW ';
+				break;
+
+			case TREE_OPERATOR_NOT_BELOW_STRICT:
+				$sOperator = ' NOT BELOW STRICT ';
+				break;
+
+			case TREE_OPERATOR_ABOVE:
+				$sOperator = ' ABOVE ';
+				break;
+
+			case TREE_OPERATOR_ABOVE_STRICT:
+				$sOperator = ' ABOVE STRICT ';
+				break;
+
+			case TREE_OPERATOR_NOT_ABOVE:
+				$sOperator = ' NOT ABOVE ';
+				break;
+
+			case TREE_OPERATOR_NOT_ABOVE_STRICT:
+				$sOperator = ' NOT ABOVE STRICT ';
+				break;
+
+		}
+		return $sOperator;
+	}
+
 	protected function ToOQL_Joins()
 	{
 		$sRes = '';
@@ -852,47 +904,9 @@ class DBObjectSearch extends DBSearch
 		{
 			foreach($aPointingTo as $iOperatorCode => $aFilter)
 			{
+				$sOperator = $this->OperatorCodeToOQL($iOperatorCode);
 				foreach($aFilter as $oFilter)
 				{
-					switch($iOperatorCode)
-					{
-						case TREE_OPERATOR_EQUALS:
-						$sOperator = ' = ';
-						break;
-						
-						case TREE_OPERATOR_BELOW:
-						$sOperator = ' BELOW ';
-						break;
-						
-						case TREE_OPERATOR_BELOW_STRICT:
-						$sOperator = ' BELOW STRICT ';
-						break;
-						
-						case TREE_OPERATOR_NOT_BELOW:
-						$sOperator = ' NOT BELOW ';
-						break;
-						
-						case TREE_OPERATOR_NOT_BELOW_STRICT:
-						$sOperator = ' NOT BELOW STRICT ';
-						break;
-						
-						case TREE_OPERATOR_ABOVE:
-						$sOperator = ' ABOVE ';
-						break;
-						
-						case TREE_OPERATOR_ABOVE_STRICT:
-						$sOperator = ' ABOVE STRICT ';
-						break;
-						
-						case TREE_OPERATOR_NOT_ABOVE:
-						$sOperator = ' NOT ABOVE ';
-						break;
-						
-						case TREE_OPERATOR_NOT_ABOVE_STRICT:
-						$sOperator = ' NOT ABOVE STRICT ';
-						break;
-						
-					}
 					$sRes .= ' JOIN ' . $oFilter->GetFirstJoinedClass() . ' AS `' . $oFilter->GetFirstJoinedClassAlias() . '` ON `' . $this->GetFirstJoinedClassAlias() . '`.' . $sExtKey . $sOperator . '`' . $oFilter->GetFirstJoinedClassAlias() . '`.id';
 					$sRes .= $oFilter->ToOQL_Joins();				
 				}
@@ -900,10 +914,17 @@ class DBObjectSearch extends DBSearch
 		}
 		foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences)
 		{
-			foreach($aReferences as $sForeignExtKeyAttCode=>$oForeignFilter)
+			foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator)
 			{
-				$sRes .= ' JOIN ' . $oForeignFilter->GetFirstJoinedClass() . ' AS `' . $oForeignFilter->GetFirstJoinedClassAlias() . '` ON `' . $oForeignFilter->GetFirstJoinedClassAlias() . '`.' . $sForeignExtKeyAttCode . ' = `' . $this->GetFirstJoinedClassAlias() . '`.id';
-				$sRes .= $oForeignFilter->ToOQL_Joins();
+				foreach ($aFiltersByOperator as $iOperatorCode => $aFilters)
+				{
+					$sOperator = $this->OperatorCodeToOQL($iOperatorCode);
+					foreach ($aFilters as $oForeignFilter)
+					{
+						$sRes .= ' JOIN ' . $oForeignFilter->GetFirstJoinedClass() . ' AS `' . $oForeignFilter->GetFirstJoinedClassAlias() . '` ON `' . $oForeignFilter->GetFirstJoinedClassAlias() . '`.' . $sForeignExtKeyAttCode . $sOperator . '`' . $this->GetFirstJoinedClassAlias() . '`.id';
+						$sRes .= $oForeignFilter->ToOQL_Joins();
+					}
+				}
 			}
 		}
 		return $sRes;
@@ -1011,47 +1032,49 @@ class DBObjectSearch extends DBSearch
 				$aAliases[$sJoinClassAlias] = $sJoinClass;
 				$aJoinItems[$sJoinClassAlias] = new DBObjectSearch($sJoinClass, $sJoinClassAlias);
 
-				if ($sFromClass == $sJoinClassAlias)
-				{
-					$oReceiver = $aJoinItems[$sToClass];
-					$oNewComer = $aJoinItems[$sFromClass];
-
-					$aAliasTranslation = array();
-					$oReceiver->AddCondition_ReferencedBy_InNameSpace($oNewComer, $sExtKeyAttCode, $oReceiver->m_aClasses, $aAliasTranslation);
-				}
-				else
+				$sOperator = $oJoinSpec->GetOperator();
+				switch($sOperator)
 				{
-					$sOperator = $oJoinSpec->GetOperator();
-					switch($sOperator)
-					{
-						case '=':
+					case '=':
+					default:
 						$iOperatorCode = TREE_OPERATOR_EQUALS;
 						break;
-						case 'BELOW':
+					case 'BELOW':
 						$iOperatorCode = TREE_OPERATOR_BELOW;
 						break;
-						case 'BELOW_STRICT':
+					case 'BELOW_STRICT':
 						$iOperatorCode = TREE_OPERATOR_BELOW_STRICT;
 						break;
-						case 'NOT_BELOW':
+					case 'NOT_BELOW':
 						$iOperatorCode = TREE_OPERATOR_NOT_BELOW;
 						break;
-						case 'NOT_BELOW_STRICT':
+					case 'NOT_BELOW_STRICT':
 						$iOperatorCode = TREE_OPERATOR_NOT_BELOW_STRICT;
 						break;
-						case 'ABOVE':
+					case 'ABOVE':
 						$iOperatorCode = TREE_OPERATOR_ABOVE;
 						break;
-						case 'ABOVE_STRICT':
+					case 'ABOVE_STRICT':
 						$iOperatorCode = TREE_OPERATOR_ABOVE_STRICT;
 						break;
-						case 'NOT_ABOVE':
+					case 'NOT_ABOVE':
 						$iOperatorCode = TREE_OPERATOR_NOT_ABOVE;
 						break;
-						case 'NOT_ABOVE_STRICT':
+					case 'NOT_ABOVE_STRICT':
 						$iOperatorCode = TREE_OPERATOR_NOT_ABOVE_STRICT;
 						break;
-					}
+				}
+
+				if ($sFromClass == $sJoinClassAlias)
+				{
+					$oReceiver = $aJoinItems[$sToClass];
+					$oNewComer = $aJoinItems[$sFromClass];
+
+					$aAliasTranslation = array();
+					$oReceiver->AddCondition_ReferencedBy_InNameSpace($oNewComer, $sExtKeyAttCode, $oReceiver->m_aClasses, $aAliasTranslation, $iOperatorCode);
+				}
+				else
+				{
 					$oReceiver = $aJoinItems[$sFromClass];
 					$oNewComer = $aJoinItems[$sToClass];
 
@@ -1348,38 +1371,57 @@ class DBObjectSearch extends DBSearch
 		}
 
 		// Filter on objects referencing me
-		foreach ($this->GetCriteria_ReferencedBy() as $sForeignClass => $aKeysAndFilters)
+		//
+		foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences)
 		{
-			foreach ($aKeysAndFilters as $sForeignKeyAttCode => $oForeignFilter)
+			foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator)
 			{
-				$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()));
+				foreach ($aFiltersByOperator as $iOperatorCode => $aFilters)
+				{
+					foreach ($aFilters as $oForeignFilter)
+					{
+						$oForeignKeyAttDef = MetaModel::GetAttributeDef($sForeignClass, $sForeignExtKeyAttCode);
 
-				$sForeignClassAlias = $oForeignFilter->GetFirstJoinedClassAlias();
-				$oBuild->m_oQBExpressions->PushJoinField(new FieldExpression($sForeignKeyAttCode, $sForeignClassAlias));
+						self::DbgTrace("Referenced by foreign key: $sForeignExtKeyAttCode... let's call MakeSQLObjectQuery()");
+						//self::DbgTrace($oForeignFilter);
+						//self::DbgTrace($oForeignFilter->ToOQL());
+						//self::DbgTrace($oSelectForeign);
+						//self::DbgTrace($oSelectForeign->RenderSelect(array()));
 
-				if ($oForeignKeyAttDef instanceof AttributeObjectKey)
-				{
-					$sClassAttCode = $oForeignKeyAttDef->Get('class_attcode');
+						$sForeignClassAlias = $oForeignFilter->GetFirstJoinedClassAlias();
+						$oBuild->m_oQBExpressions->PushJoinField(new FieldExpression($sForeignExtKeyAttCode, $sForeignClassAlias));
 
-					// 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);
-				}
+						if ($oForeignKeyAttDef instanceof AttributeObjectKey)
+						{
+							$sClassAttCode = $oForeignKeyAttDef->Get('class_attcode');
 
-				$oSelectForeign = $oForeignFilter->MakeSQLObjectQuery($oBuild, $aAttToLoad);
+							// 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();
+
+						if ($iOperatorCode == TREE_OPERATOR_EQUALS)
+						{
+							$oSelectBase->AddInnerJoin($oSelectForeign, $sKeyField, $sForeignKeyColumn, $sForeignKeyTable);
+						}
+						else
+						{
+							// Hierarchical key
+							$KeyLeft = $oForeignKeyAttDef->GetSQLLeft();
+							$KeyRight = $oForeignKeyAttDef->GetSQLRight();
 
-				$oJoinExpr = $oBuild->m_oQBExpressions->PopJoinField();
-				$sForeignKeyTable = $oJoinExpr->GetParent();
-				$sForeignKeyColumn = $oJoinExpr->GetName();
-				$oSelectBase->AddInnerJoin($oSelectForeign, $sKeyField, $sForeignKeyColumn, $sForeignKeyTable);
+							$oSelectBase->AddInnerJoinTree($oSelectForeign, $KeyLeft, $KeyRight, $KeyLeft, $KeyRight, $sForeignKeyTable, $iOperatorCode, true);
+						}
+					}
+				}
 			}
 		}
 

+ 1 - 1
core/dbsearch.class.php

@@ -128,7 +128,7 @@ abstract class DBSearch
 	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 AddCondition_ReferencedBy(DBObjectSearch $oFilter, $sForeignExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS);
 	abstract public function Intersect(DBSearch $oFilter);
 
 	/**

+ 2 - 2
core/dbunionsearch.class.php

@@ -290,11 +290,11 @@ class DBUnionSearch extends DBSearch
 		}
 	}
 
-	public function AddCondition_ReferencedBy(DBObjectSearch $oFilter, $sForeignExtKeyAttCode)
+	public function AddCondition_ReferencedBy(DBObjectSearch $oFilter, $sForeignExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS)
 	{
 		foreach ($this->aSearches as $oSearch)
 		{
-			$oSearch->AddCondition_ReferencedBy($oFilter, $sForeignExtKeyAttCode);
+			$oSearch->AddCondition_ReferencedBy($oFilter, $sForeignExtKeyAttCode, $iOperatorCode);
 		}
 	}
 

+ 20 - 8
core/sqlobjectquery.class.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2015 Combodo SARL
+// Copyright (C) 2015-2016 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -21,7 +21,7 @@
  * SQLObjectQuery
  * build a mySQL compatible SQL query
  *
- * @copyright   Copyright (C) 2015 Combodo SARL
+ * @copyright   Copyright (C) 2015-2016 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -196,7 +196,7 @@ class SQLObjectQuery extends SQLQuery
 	{
 		$this->AddJoin("inner", $oSQLQuery, $sLeftField, $sRightField, $sRightTable);
 	}
-	public function AddInnerJoinTree($oSQLQuery, $sLeftFieldLeft, $sLeftFieldRight, $sRightFieldLeft, $sRightFieldRight, $sRightTableAlias = '', $iOperatorCode = TREE_OPERATOR_BELOW)
+	public function AddInnerJoinTree($oSQLQuery, $sLeftFieldLeft, $sLeftFieldRight, $sRightFieldLeft, $sRightFieldRight, $sRightTableAlias = '', $iOperatorCode = TREE_OPERATOR_BELOW, $bInvertOnClause = false)
 	{
 		assert((get_class($oSQLQuery) == __CLASS__) || is_subclass_of($oSQLQuery, __CLASS__));
 		if (empty($sRightTableAlias))
@@ -211,7 +211,9 @@ class SQLObjectQuery extends SQLQuery
 			"rightfield_left" => $sRightFieldLeft,
 			"rightfield_right" => $sRightFieldRight,
 			"righttablealias" => $sRightTableAlias,
-			"tree_operator" => $iOperatorCode);
+			"tree_operator" => $iOperatorCode,
+			'invert_on_clause' => $bInvertOnClause
+		);
 	}
 	public function AddLeftJoin($oSQLQuery, $sLeftField, $sRightField)
 	{
@@ -406,10 +408,20 @@ class SQLObjectQuery extends SQLQuery
 				$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']}`";
+				if ($aJoinData['invert_on_clause'])
+				{
+					$sRootLeft = "`$sCallerAlias`.`{$aJoinData['leftfield']}`";
+					$sRootRight = "`$sCallerAlias`.`{$aJoinData['rightfield']}`";
+					$sNodeLeft = "`$sRightTableAlias`.`{$aJoinData['rightfield_left']}`";
+					$sNodeRight = "`$sRightTableAlias`.`{$aJoinData['rightfield_right']}`";
+				}
+				else
+				{
+					$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:

+ 6 - 2
test/testlist.inc.php

@@ -1,5 +1,5 @@
 <?php
-// Copyright (C) 2010-2015 Combodo SARL
+// Copyright (C) 2010-2016 Combodo SARL
 //
 //   This file is part of iTop.
 //
@@ -19,7 +19,7 @@
 /**
  * Core test list
  *
- * @copyright   Copyright (C) 2010-2015 Combodo SARL
+ * @copyright   Copyright (C) 2010-2016 Combodo SARL
  * @license     http://opensource.org/licenses/AGPL-3.0
  */
 
@@ -263,6 +263,7 @@ class TestOQLParser extends TestFunction
 			'SELECT  A, B,C FROM A JOIN B ON A.myB = B.id' => true,
 			'SELECT C FROM A JOIN B ON A.myB = B.id WHERE A.col1 = 2' => true,
 			'SELECT A JOIN B ON A.myB BELOW B.id WHERE A.col1 = 2' => true,
+			'SELECT A JOIN B ON B.myA BELOW A.id WHERE A.col1 = 2' => true,
 			'SELECT A JOIN B ON A.myB = B.id JOIN C ON C.parent_id 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 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 B.id WHERE A.col1 = 2 AND B.id = 3' => true,
@@ -374,6 +375,9 @@ class TestOQLNormalization extends TestBizModel
 			'SELECT Contact AS c WHERE Contact.name = "foo"' => false,
 			'SELECT Contact AS c WHERE x.name = "foo"' => false,
 
+			'SELECT Organization AS child JOIN Organization AS root ON child.parent_id BELOW root.id' => true,
+			'SELECT Organization AS root JOIN Organization AS child ON child.parent_id BELOW root.id' => true,
+
 			'SELECT RelationProfessionnelle' => false,
 			'SELECT RelationProfessionnelle AS c WHERE name = "foo"' => false,