فهرست منبع

Implementation of a new type of ExternalKey attribute: HierarchicalKey. This attribute implements the "nested set" model and is used to define a hierarchy of an arbitrary depth of objects of the same class. With this new feature it is possible to retrieve in one OQL query (and in one sql query as well) all the children of a given organization.

I'm still keeping (commented out) some of the traces helpful for debugging the construction of the OQL queries.

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@1349 a333f486-631f-4898-b8df-5754b55c2be0
dflaven 14 سال پیش
والد
کامیت
b90aa3b9c4

+ 110 - 0
core/attributedef.class.inc.php

@@ -167,6 +167,7 @@ abstract class AttributeDefinition
 	public function IsScalar() {return false;} 
 	public function IsLinkSet() {return false;} 
 	public function IsExternalKey($iType = EXTKEY_RELATIVE) {return false;} 
+	public function IsHierarchicalKey() {return false;}
 	public function IsExternalField() {return false;} 
 	public function IsWritable() {return false;} 
 	public function IsNullAllowed() {return true;} 
@@ -2423,6 +2424,115 @@ class AttributeExternalKey extends AttributeDBFieldVoid
 }
 
 /**
+ * Special kind of External Key to manage a hierarchy of objects
+ */
+class AttributeHierarchicalKey extends AttributeExternalKey
+{
+	static protected function ListExpectedParams()
+	{
+		$aParams = parent::ListExpectedParams();
+		//unset($aParams[array_search('targetclass', $aParams)]);
+		
+		//print_r($aParams);
+		return $aParams; // TODO: mettre les bons parametres ici !!
+	}
+
+	public function GetEditClass() {return "ExtKey";}
+	public function RequiresIndex()
+	{
+		return true;
+	}
+
+	public function IsHierarchicalKey() {return true;}
+	public function GetKeyAttDef($iType = EXTKEY_RELATIVE){return $this;}
+	public function GetKeyAttCode() {return $this->GetCode();} 
+	
+
+	public function GetBasicFilterOperators()
+	{
+		return parent::GetBasicFilterOperators();
+	}
+	public function GetBasicFilterLooseOperator()
+	{
+		return parent::GetBasicFilterLooseOperator();
+	}
+
+	public function GetSQLColumns()
+	{
+		$aColumns = array();
+		$aColumns[$this->GetCode()] = 'INT(11)';
+		$aColumns[$this->GetSQLLeft()] = 'INT(11)';
+		$aColumns[$this->GetSQLRight()] = 'INT(11)';
+		return $aColumns;
+	}
+	public function GetSQLRight()
+	{
+		return $this->GetCode().'_right';
+	}
+	public function GetSQLLeft()
+	{
+		return $this->GetCode().'_left';
+	}
+
+	public function GetSQLValues($value)
+	{
+		if (!is_array($value))
+		{
+			$aValues[$this->GetCode()] = $value;
+		}
+		else
+		{
+			$aValues = array();
+			$aValues[$this->GetCode()] = $value[$this->GetCode()];
+			$aValues[$this->GetSQLRight()] = $value[$this->GetSQLRight()];
+			$aValues[$this->GetSQLLeft()] = $value[$this->GetSQLLeft()];
+		}
+		return $aValues;
+	}
+
+	public function GetAllowedValues($aArgs = array(), $sContains = '')
+	{
+		if (array_key_exists('this', $aArgs))
+		{
+			// Hierarchical keys have one more constraint: the "parent value" cannot be
+			// "under" themselves
+			$iRootId = $aArgs['this']->GetKey();
+			if ($iRootId > 0) // ignore objects that do no exist in the database...
+			{
+				$oValSetDef = $this->GetValuesDef();
+				$sClass = $this->GetHostClass(); // host class  == target class for HK
+				$oFilter = DBObjectSearch::FromOQL("SELECT $sClass AS node JOIN $sClass AS root ON node.".$this->GetCode()." NOT BELOW root.id WHERE root.id = $iRootId");
+				$oValSetDef->AddCondition($oFilter);
+			}
+		}
+		else
+		{
+			return parent::GetAllowedValues($aArgs, $sContains);
+		}
+	}
+
+	public function GetAllowedValuesAsObjectSet($aArgs = array(), $sContains = '')
+	{
+		$oValSetDef = $this->GetValuesDef();
+		if (array_key_exists('this', $aArgs))
+		{
+			// Hierarchical keys have one more constraint: the "parent value" cannot be
+			// "under" themselves
+			$iRootId = $aArgs['this']->GetKey();
+			if ($iRootId > 0) // ignore objects that do no exist in the database...
+			{
+				$aValuesSetDef = $this->GetValuesDef();
+				$sClass = $this->GetHostClass(); // host class  == target class for HK
+				$oFilter = DBObjectSearch::FromOQL("SELECT $sClass AS node JOIN $sClass AS root ON node.".$this->GetCode()." NOT BELOW root.id WHERE root.id = $iRootId");
+				$oValSetDef->AddCondition($oFilter);
+			}
+		}
+		$oSet = $oValSetDef->ToObjectSet($aArgs, $sContains);
+		return $oSet;
+	}
+}
+
+/**
  * An attribute which corresponds to an external key (direct or indirect) 
  *
  * @package     iTopORM

+ 2 - 0
core/cmdbchangeop.class.inc.php

@@ -212,6 +212,8 @@ class CMDBChangeOpSetAttributeScalar extends CMDBChangeOpSetAttribute
 		$oMonoObjectSet = new DBObjectSet($oTargetSearch);
 		if (UserRights::IsActionAllowedOnAttribute($this->Get('objclass'), $this->Get('attcode'), UR_ACTION_READ, $oMonoObjectSet) == UR_ALLOWED_YES)
 		{
+			if (!MetaModel::IsValidAttCode($this->Get('objclass'), $this->Get('attcode'))) return ''; // Protects against renammed attributes...
+
 			$oAttDef = MetaModel::GetAttributeDef($this->Get('objclass'), $this->Get('attcode'));
 			$sAttName = $oAttDef->GetLabel();
 			$sNewValue = $this->Get('newvalue');

+ 106 - 7
core/dbobject.class.php

@@ -719,6 +719,14 @@ abstract class DBObject
 					return "Target object not found ($sTargetClass::$toCheck)";
 				}
 			}
+			if ($oAtt->IsHierarchicalKey())
+			{
+				// This check cannot be deactivated since otherwise the user may break things by a CSV import of a bulk modify
+				if ($toCheck == $this->GetKey())
+				{
+					return "An object can not be its own parent in a hierarchy (".$oAtt->Getlabel()." = $toCheck)";
+				}
+			}
 		}
 		elseif ($oAtt->IsScalar())
 		{
@@ -1067,6 +1075,8 @@ abstract class DBObject
 			$aValuesToWrite[] = CMDBSource::Quote($this->m_iKey);
 		}
 
+		$aHierarchicalKeys = array();
+		
 		foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef)
 		{
 			// Skip this attribute if not defined in this table
@@ -1077,6 +1087,10 @@ abstract class DBObject
 				$aFieldsToWrite[] = "`$sColumn`"; 
 				$aValuesToWrite[] = CMDBSource::Quote($sValue);
 			}
+			if ($oAttDef->IsHierarchicalKey())
+			{
+				$aHierarchicalKeys[$sAttCode] = $oAttDef;
+			}
 		}
 
 		if (count($aValuesToWrite) == 0) return false;
@@ -1099,6 +1113,17 @@ abstract class DBObject
 			}
 			else
 			{
+				if (count($aHierarchicalKeys) > 0)
+				{
+					foreach($aHierarchicalKeys as $sAttCode => $oAttDef)
+					{
+						$aValues = MetaModel::HKInsertChildUnder($this->m_aCurrValues[$sAttCode], $oAttDef, $sTable);
+						$aFieldsToWrite[] = '`'.$oAttDef->GetSQLRight().'`';
+						$aValuesToWrite[] = $aValues[$oAttDef->GetSQLRight()];
+						$aFieldsToWrite[] = '`'.$oAttDef->GetSQLLeft().'`';
+						$aValuesToWrite[] = $aValues[$oAttDef->GetSQLLeft()];
+					}
+				}
 				$sInsertSQL = "INSERT INTO `$sTable` (".join(",", $aFieldsToWrite).") VALUES (".join(", ", $aValuesToWrite).")";
 				$iNewKey = CMDBSource::InsertInto($sInsertSQL);
 			}
@@ -1250,22 +1275,68 @@ abstract class DBObject
 		}
 
 		$bHasANewExternalKeyValue = false;
+		$aHierarchicalKeys = array();
 		foreach($aChanges as $sAttCode => $valuecurr)
 		{
 			$oAttDef = MetaModel::GetAttributeDef(get_class($this), $sAttCode);
 			if ($oAttDef->IsExternalKey()) $bHasANewExternalKeyValue = true;
 			if (!$oAttDef->IsDirectField()) unset($aChanges[$sAttCode]);
+			if ($oAttDef->IsHierarchicalKey())
+			{
+				$aHierarchicalKeys[$sAttCode] = $oAttDef;
+			}
 		}
 
-		// Update scalar attributes
-		if (count($aChanges) != 0)
+		if (!MetaModel::DBIsReadOnly())
 		{
-			$oFilter = new DBObjectSearch(get_class($this));
-			$oFilter->AddCondition('id', $this->m_iKey, '=');
-	
-			$sSQL = MetaModel::MakeUpdateQuery($oFilter, $aChanges);
-			if (!MetaModel::DBIsReadOnly())
+			// Update the left & right indexes for each hierarchical key
+			foreach($aHierarchicalKeys as $sAttCode => $oAttDef)
 			{
+				$sTable = $sTable = MetaModel::DBGetTable(get_class($this), $sAttCode);
+				$sSQL = "SELECT `".$oAttDef->GetSQLRight()."` AS `right`, `".$oAttDef->GetSQLLeft()."` AS `left` FROM `$sTable` WHERE id=".$this->GetKey();
+				$aRes = CMDBSource::QueryToArray($sSQL);
+				$iMyLeft = $aRes[0]['left'];
+				$iMyRight = $aRes[0]['right'];
+				$iDelta =$iMyRight - $iMyLeft + 1;
+				MetaModel::HKTemporaryCutBranch($iMyLeft, $iMyRight, $oAttDef, $sTable);
+				
+				if ($aChanges[$sAttCode] == 0)
+				{
+					// No new parent, insert completely at the right of the tree
+					$sSQL = "SELECT max(`".$oAttDef->GetSQLRight()."`) AS max FROM `$sTable`";
+					$aRes = CMDBSource::QueryToArray($sSQL);
+					if (count($aRes) == 0)
+					{
+						$iNewLeft = 1;
+					}
+					else
+					{
+						$iNewLeft = $aRes[0]['max']+1;
+					}
+				}
+				else
+				{
+					// Insert at the right of the specified parent
+					$sSQL = "SELECT `".$oAttDef->GetSQLRight()."` FROM `$sTable` WHERE id=".((int)$aChanges[$sAttCode]);
+					$iNewLeft = CMDBSource::QueryToScalar($sSQL);
+				}
+
+				MetaModel::HKReplugBranch($iNewLeft, $iNewLeft + $iDelta - 1, $oAttDef, $sTable);
+
+				$aHKChanges = array();
+				$aHKChanges[$sAttCode] = $aChanges[$sAttCode];
+				$aHKChanges[$oAttDef->GetSQLLeft()] = $iNewLeft;
+				$aHKChanges[$oAttDef->GetSQLRight()] = $iNewLeft + $iDelta - 1;
+				$aChanges[$sAttCode] = $aHKChanges; // the 3 values will be stored by MakeUpdateQuery below
+			}
+			
+			// Update scalar attributes
+			if (count($aChanges) != 0)
+			{
+				$oFilter = new DBObjectSearch(get_class($this));
+				$oFilter->AddCondition('id', $this->m_iKey, '=');
+		
+				$sSQL = MetaModel::MakeUpdateQuery($oFilter, $aChanges);
 				CMDBSource::Query($sSQL);
 			}
 		}
@@ -1321,6 +1392,34 @@ abstract class DBObject
 
 		if (!MetaModel::DBIsReadOnly())
 		{
+			foreach(MetaModel::ListAttributeDefs(get_class($this)) as $sAttCode => $oAttDef)
+			{
+				if ($oAttDef->IsHierarchicalKey())
+				{
+					// Update the left & right indexes for each hierarchical key
+					$sTable = $sTable = MetaModel::DBGetTable(get_class($this), $sAttCode);
+					$sSQL = "SELECT `".$oAttDef->GetSQLRight()."` AS `right`, `".$oAttDef->GetSQLLeft()."` AS `left` FROM `$sTable` WHERE id=".$this->GetKey();
+					$aRes = CMDBSource::QueryToArray($sSQL);
+					$iMyLeft = $aRes[0]['left'];
+					$iMyRight = $aRes[0]['right'];
+					$iDelta =$iMyRight - $iMyLeft + 1;
+					MetaModel::HKTemporaryCutBranch($iMyLeft, $iMyRight, $oAttDef, $sTable);
+
+					// No new parent, insert completely at the right of the tree
+					$sSQL = "SELECT max(`".$oAttDef->GetSQLRight()."`) AS max FROM `$sTable`";
+					$aRes = CMDBSource::QueryToArray($sSQL);
+					if (count($aRes) == 0)
+					{
+						$iNewLeft = 1;
+					}
+					else
+					{
+						$iNewLeft = $aRes[0]['max']+1;
+					}
+					MetaModel::HKReplugBranch($iNewLeft, $iNewLeft + $iDelta - 1, $oAttDef, $sTable);
+				}
+			}
+
 			foreach(MetaModel::EnumParentClasses(get_class($this), ENUM_PARENT_CLASSES_ALL) as $sParentClass)
 			{
 				$this->DBDeleteSingleTable($sParentClass);

+ 153 - 23
core/dbobjectsearch.class.php

@@ -22,6 +22,12 @@
  * @author      Denis Flaven <denis.flaven@combodo.com>
  * @license     http://www.opensource.org/licenses/gpl-3.0.html LGPL
  */
+ 
+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);
 
 class DBObjectSearch
 {
@@ -107,6 +113,7 @@ class DBObjectSearch
 		if (count($this->m_aPointingTo) > 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;
 	}
 	
@@ -115,13 +122,39 @@ class DBObjectSearch
 		// To replace __Describe
 	}
 
-	public function DescribeConditionPointTo($sExtKeyAttCode)
+	public function DescribeConditionPointTo($sExtKeyAttCode, $aPointingTo)
 	{
-		if (!isset($this->m_aPointingTo[$sExtKeyAttCode])) return "";
-		$oFilter = $this->m_aPointingTo[$sExtKeyAttCode];
-		if ($oFilter->IsAny()) return "";
-		$oAtt = MetaModel::GetAttributeDef($this->GetClass(), $sExtKeyAttCode);
-		return $oAtt->GetLabel()." having ({$oFilter->DescribeConditions()})";
+		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;
+			}
+			$aDescription[] = $oAtt->GetLabel()."$sOperator ({$oFilter->DescribeConditions()})";
+		}
+		return implode(' and ', $aDescription);
 	}
 
 	public function DescribeConditionRefBy($sForeignClass, $sForeignExtKeyAttCode)
@@ -141,6 +174,7 @@ class DBObjectSearch
 		return "related ($sRelCode... peut mieux faire !, $iMaxDepth dig depth) to a {$oFilter->GetClass()} ({$oFilter->DescribeConditions()})";
 	}
 
+
 	public function DescribeConditions()
 	{
 		$aConditions = array();
@@ -159,10 +193,9 @@ class DBObjectSearch
 		$aConditions[] = $this->RenderCondition();
 
 		$aCondPoint = array();
-		foreach($this->m_aPointingTo as $sExtKeyAttCode=>$oFilter)
+		foreach($this->m_aPointingTo as $sExtKeyAttCode => $aPointingTo)
 		{
-			if ($oFilter->IsAny()) continue;
-			$aCondPoint[] = $this->DescribeConditionPointTo($sExtKeyAttCode);
+			$aCondPoint[] = $this->DescribeConditionPointTo($sExtKeyAttCode, $aPointingTo);
 		}
 		if (count($aCondPoint) > 0)
 		{
@@ -187,6 +220,11 @@ class DBObjectSearch
 			$aConditions[] = implode(" and ", $aCondReferred);
 		}
 
+		foreach ($this->m_aParentConditions as $aRelInfo)
+		{
+			$aCondReferred[] = $this->DescribeConditionParent($aRelInfo);
+		}
+
 		return implode(" and ", $aConditions);		
 	}
 	
@@ -210,6 +248,9 @@ class DBObjectSearch
 	protected function TransferConditionExpression($oFilter, $aTranslation)
 	{
 		$oTranslated = $oFilter->GetCriteria()->Translate($aTranslation, false, false /* leave unresolved fields */);
+//echo "<p>TransferConditionExpression:<br/>";
+//echo "Adding Conditions:<br/><pre>".print_r($oTranslated, true)."</pre>\n";
+//echo "</p>";
 		$this->AddConditionExpression($oTranslated);
 		// #@# what about collisions in parameter names ???
 		$this->m_aParams = array_merge($this->m_aParams, $oFilter->m_aParams);
@@ -218,6 +259,7 @@ class DBObjectSearch
 	public function ResetCondition()
 	{
 		$this->m_oSearchCondition = new TrueExpression();
+		$this->m_aParentConditions = array();
 		// ? is that usefull/enough, do I need to rebuild the list after the subqueries ?
 	}
 
@@ -385,25 +427,47 @@ class DBObjectSearch
 		$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)
 	{
 		$sOrigAlias = $this->GetClassAlias();
 		if (array_key_exists($sOrigAlias, $aClassAliases))
 		{
 			$sNewAlias = MetaModel::GenerateUniqueAlias($aClassAliases, $sOrigAlias, $this->GetClass());
+//echo "<h1>Generating a new alias for $sOrigAlias (already used). It is now: $sNewAlias</h1>\n";
 			$this->m_aSelectedClasses[$sNewAlias] = $this->GetClass();
 			unset($this->m_aSelectedClasses[$sOrigAlias]);
 
+			$this->m_aClasses[$sNewAlias] = $this->GetClass();
+			unset($this->m_aClasses[$sOrigAlias]);
+
 			// Translate the condition expression with the new alias
 			$aAliasTranslation[$sOrigAlias]['*'] = $sNewAlias;
 		}
 
+//echo "<h1>Adding the alias for ".$this->GetClass().". as ".$this->GetClassAlias()."</h1>\n";
 		// add the alias into the filter aliases list
 		$aClassAliases[$this->GetClassAlias()] = $this->GetClass();
 		
-		foreach($this->m_aPointingTo as $sExtKeyAttCode=>$oFilter)
+		foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo)
 		{
-			$oFilter->AddToNameSpace($aClassAliases, $aAliasTranslation);
+			foreach($aPointingTo as $iOperatorCode => $oFilter)
+			{
+				$oFilter->AddToNameSpace($aClassAliases, $aAliasTranslation);
+			}
 		}
 
 		foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences)
@@ -415,16 +479,17 @@ class DBObjectSearch
 		}
 	}
 
-	public function AddCondition_PointingTo(DBObjectSearch $oFilter, $sExtKeyAttCode)
+	public function AddCondition_PointingTo(DBObjectSearch $oFilter, $sExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS)
 	{
 		$aAliasTranslation = array();
-		$res = $this->AddCondition_PointingTo_InNameSpace($oFilter, $sExtKeyAttCode, $this->m_aClasses, $aAliasTranslation);
+		$res = $this->AddCondition_PointingTo_InNameSpace($oFilter, $sExtKeyAttCode, $this->m_aClasses, $aAliasTranslation, $iOperatorCode);
 		$this->TransferConditionExpression($oFilter, $aAliasTranslation);
 		return $res;
 	}
 
-	protected function AddCondition_PointingTo_InNameSpace(DBObjectSearch $oFilter, $sExtKeyAttCode, &$aClassAliases, &$aAliasTranslation)
+	protected function AddCondition_PointingTo_InNameSpace(DBObjectSearch $oFilter, $sExtKeyAttCode, &$aClassAliases, &$aAliasTranslation, $iOperatorCode)
 	{
+//echo "<p style=\"color:green\">Calling: AddCondition_PointingTo_InNameSpace([".implode(',', $aClassAliases)."], [".implode(',', $aAliasTranslation)."]);</p>";
 		if (!MetaModel::IsValidKeyAttCode($this->GetClass(), $sExtKeyAttCode))
 		{
 			throw new CoreWarning("The attribute code '$sExtKeyAttCode' is not an external key of the class '{$this->GetClass()}' - the condition will be ignored");
@@ -434,20 +499,36 @@ class DBObjectSearch
 		{
 			throw new CoreException("The specified filter (pointing to {$oFilter->GetClass()}) is not compatible with the key '{$this->GetClass()}::$sExtKeyAttCode', which is pointing to {$oAttExtKey->GetTargetClass()}");
 		}
+		if(($iOperatorCode != TREE_OPERATOR_EQUALS) && !($oAttExtKey instanceof AttributeHierarchicalKey))
+		{
+			throw new CoreException("The specified tree operator $isOperatorCode is not applicable to the key '{$this->GetClass()}::$sExtKeyAttCode', which is not a HierarchicalKey");
+		}
 
 		if (array_key_exists($sExtKeyAttCode, $this->m_aPointingTo))
 		{
-			$this->m_aPointingTo[$sExtKeyAttCode]->MergeWith_InNamespace($oFilter, $aClassAliases, $aAliasTranslation);
+			if (array_key_exists($iOperatorCode, $this->m_aPointingTo[$sExtKeyAttCode]))
+			{
+				// Same ext key and same operator, merge the filters together
+				$this->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode]->MergeWith_InNamespace($oFilter, $aClassAliases, $aAliasTranslation);
+			}
+			else
+			{
+//echo "<p style=\"color:red\">Calling: AddToNameSpace([".implode(',', $aClassAliases)."], [".implode(',', $aAliasTranslation)."]);</p>";
+				$oFilter->AddToNamespace($aClassAliases, $aAliasTranslation);
+	
+				$this->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode] = $oFilter;
+			}
 		}
 		else
 		{
+//echo "<p style=\"color:red\">Calling: AddToNameSpace([".implode(',', $aClassAliases)."], [".implode(',', $aAliasTranslation)."]);</p>";
 			$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 = clone $oFilter;
 			// $oNewFilter->ResetCondition();
 
-			$this->m_aPointingTo[$sExtKeyAttCode] = $oFilter;
+			$this->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode] = $oFilter;
 		}
 	}
 
@@ -491,6 +572,7 @@ class DBObjectSearch
 	public function AddCondition_LinkedTo(DBObjectSearch $oLinkFilter, $sExtKeyAttCodeToMe, $sExtKeyAttCodeTarget, DBObjectSearch $oFilterTarget)
 	{
 		$oLinkFilterFinal = clone $oLinkFilter;
+		// todo : new function prototype
 		$oLinkFilterFinal->AddCondition_PointingTo($sExtKeyAttCodeToMe);
 
 		$this->AddCondition_ReferencedBy($oLinkFilterFinal, $sExtKeyAttCodeToMe);
@@ -523,9 +605,12 @@ class DBObjectSearch
 		$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=>$oExtFilter)
+		foreach($oFilter->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo)
 		{
-			$this->AddCondition_PointingTo_InNamespace($oExtFilter, $sExtKeyAttCode, $aClassAliases, $aAliasTranslation);
+			foreach($aPointingTo as $iOperatorCode => $oExtFilter)
+			{
+				$this->AddCondition_PointingTo_InNamespace($oExtFilter, $sExtKeyAttCode, $aClassAliases, $aAliasTranslation, $iOperatorCode);
+			}
 		}
 		foreach($oFilter->m_aReferencedBy as $sForeignClass => $aReferences)
 		{
@@ -544,7 +629,7 @@ class DBObjectSearch
 		{
 			return $this->m_aPointingTo;
 		}
-		if (!array_key_exists($sKeyAttCode, $this->m_aPointingTo)) return null;
+		if (!array_key_exists($sKeyAttCode, $this->m_aPointingTo)) return array();
 		return $this->m_aPointingTo[$sKeyAttCode];
 	}
 	public function GetCriteria_ReferencedBy($sRemoteClass = "", $sForeignExtKeyAttCode = "")
@@ -710,10 +795,36 @@ class DBObjectSearch
 	protected function ToOQL_Joins()
 	{
 		$sRes = '';
-		foreach($this->m_aPointingTo as $sExtKey=>$oFilter)
+		foreach($this->m_aPointingTo as $sExtKey => $aPointingTo)
 		{
-			$sRes .= ' JOIN '.$oFilter->GetClass().' AS '.$oFilter->GetClassAlias().' ON '.$this->GetClassAlias().'.'.$sExtKey.' = '.$oFilter->GetClassAlias().'.id';
-			$sRes .= $oFilter->ToOQL_Joins();
+			foreach($aPointingTo as $iOperatorCode => $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;
+					
+				}
+				$sRes .= ' JOIN '.$oFilter->GetClass().' AS '.$oFilter->GetClassAlias().' ON '.$this->GetClassAlias().'.'.$sExtKey.$sOperator.$oFilter->GetClassAlias().'.id';
+				$sRes .= $oFilter->ToOQL_Joins();				
+			}
 		}
 		foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences)
 		{
@@ -926,7 +1037,26 @@ class DBObjectSearch
 				}
 				else
 				{
-					$aJoinItems[$sFromClass]->AddCondition_PointingTo($aJoinItems[$sToClass], $sExtKeyAttCode);
+					$sOperator = $oJoinSpec->GetOperator();
+					switch($sOperator)
+					{
+						case '=':
+						$iOperatorCode = TREE_OPERATOR_EQUALS;
+						break;
+						case 'BELOW':
+						$iOperatorCode = TREE_OPERATOR_BELOW;
+						break;
+						case 'BELOW_STRICT':
+						$iOperatorCode = TREE_OPERATOR_BELOW_STRICT;
+						break;
+						case 'NOT_BELOW':
+						$iOperatorCode = TREE_OPERATOR_NOT_BELOW;
+						break;
+						case 'NOT_BELOW_STRICT':
+						$iOperatorCode = TREE_OPERATOR_NOT_BELOW_STRICT;
+						break;
+					}
+					$aJoinItems[$sFromClass]->AddCondition_PointingTo($aJoinItems[$sToClass], $sExtKeyAttCode, $iOperatorCode);
 				}
 			}
 		}

+ 254 - 58
core/metamodel.class.php

@@ -699,7 +699,11 @@ abstract class MetaModel
 
 	final static public function GetAttributeDef($sClass, $sAttCode)
 	{
-		self::_check_subclass($sClass);	
+		self::_check_subclass($sClass);
+if (!array_key_exists($sAttCode, self::$m_aAttribDefs[$sClass]))
+{
+	echo "<p>$sAttCode is NOT a valid attribute of class $sClass.</p>";
+}
 		return self::$m_aAttribDefs[$sClass][$sAttCode];
 	}
 
@@ -1666,6 +1670,23 @@ abstract class MetaModel
 		self::_check_subclass($sClass);
 		return (self::GetRootClass($sClass) == $sClass);
 	}
+	/**
+	 * Tells if a class contains a hierarchical key, and if so what is its AttCode
+	 * @return mixed String = sAttCode or false if the class is not part of a hierarchy
+	 */
+	public static function IsHierarchicalClass($sClass)
+	{
+		$sHierarchicalKeyCode = false;
+		foreach (self::ListAttributeDefs($sClass) as $sAttCode => $oAtt)
+		{
+			if ($oAtt->IsHierarchicalKey())
+			{
+				$sHierarchicalKeyCode = $sAttCode; // Found the hierarchical key, no need to continue
+				break;
+			}
+		}
+		return $sHierarchicalKeyCode;
+	}
 	public static function EnumRootClasses()
 	{
 		return array_unique(self::$m_aRootClasses);
@@ -1943,6 +1964,7 @@ abstract class MetaModel
 		try
 		{
 			$sRes = $oSelect->RenderSelect($aOrderSpec, $aScalarArgs, $iLimitCount, $iLimitStart, $bGetCount);
+//echo "<p>MakeQuery: $sRes</p>";
 		}
 		catch (MissingQueryArgument $e)
 		{
@@ -2094,8 +2116,9 @@ abstract class MetaModel
 				}
 			}
 		}
-
+//echo "<p>oQBExpr ".__LINE__.": <pre>\n".print_r($oQBExpr, true)."</pre></p>\n";
 		$aExpectedAtts = array(); // array of (attcode => fieldexpression)
+//echo "<p>".__LINE__.": GetUnresolvedFields($sClassAlias, ...)</p>\n";
 		$oQBExpr->GetUnresolvedFields($sClassAlias, $aExpectedAtts);
 
 		// Compute a clear view of required joins (from the current class)
@@ -2120,17 +2143,22 @@ abstract class MetaModel
 			}
 		}
 		// Get all Ext keys used by the filter
-		foreach ($oFilter->GetCriteria_PointingTo() as $sKeyAttCode => $trash)
+		foreach ($oFilter->GetCriteria_PointingTo() as $sKeyAttCode => $aPointingTo)
 		{
-			$sKeyTableClass = self::$m_aAttribOrigins[$sClass][$sKeyAttCode];
-			$aExtKeys[$sKeyTableClass][$sKeyAttCode] = array();
+			if (array_key_exists(TREE_OPERATOR_EQUALS, $aPointingTo))
+			{
+				$sKeyTableClass = self::$m_aAttribOrigins[$sClass][$sKeyAttCode];
+				$aExtKeys[$sKeyTableClass][$sKeyAttCode] = array();
+			}
 		}
 
 		if (array_key_exists('friendlyname', $aExpectedAtts))
 		{
 			$aTranslateNow = array();
 			$aTranslateNow[$sClassAlias]['friendlyname'] = self::GetNameExpression($sClass, $sClassAlias);
+//echo "<p>oQBExpr ".__LINE__.": <pre>\n".print_r($oQBExpr, true)."</pre></p>\n";
 			$oQBExpr->Translate($aTranslateNow, false);
+//echo "<p>oQBExpr ".__LINE__.": <pre>\n".print_r($oQBExpr, true)."</pre></p>\n";
 
 			$aNameSpec = self::GetNameSpec($sClass);
 			foreach($aNameSpec[1] as $i => $sAttCode)
@@ -2188,6 +2216,7 @@ abstract class MetaModel
 		foreach(self::EnumParentClasses($sClass) as $sParentClass)
 		{
 			if (!self::HasTable($sParentClass)) continue;
+//echo "<p>Parent class: $sParentClass... let's call MakeQuerySingleTable()</p>";
 			self::DbgTrace("Parent class: $sParentClass... let's call MakeQuerySingleTable()");
 			$oSelectParentTable = self::MakeQuerySingleTable($aSelectedClasses, $oQBExpr, $aClassAliases, $aTableAliases, $oFilter, $sParentClass, $aExtKeys, $aValues);
 			if (is_null($oSelectBase))
@@ -2272,6 +2301,7 @@ abstract class MetaModel
 	protected static function MakeQuerySingleTable($aSelectedClasses, &$oQBExpr, &$aClassAliases, &$aTableAliases, $oFilter, $sTableClass, $aExtKeys, $aValues)
 	{
 		// $aExtKeys is an array of sTableClass => array of (sAttCode (keys) => array of sAttCode (fields))
+//echo "MAKEQUERY($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
@@ -2346,6 +2376,7 @@ abstract class MetaModel
 			}
 			else
 			{
+//echo "<p>MakeQuerySingleTable: 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)
@@ -2362,92 +2393,257 @@ abstract class MetaModel
 		//
 		$oSelectBase = new SQLQuery($sTable, $sTableAlias, array(), $bIsOnQueriedClass, $aUpdateValues, $oSelectedIdField);
 
+//echo "MAKEQUERY- Classe $sTableClass<br/>\n";
 		// 4 - The external keys -> joins...
 		//
+		$aAllPointingTo = $oFilter->GetCriteria_PointingTo();
+
 		if (array_key_exists($sTableClass, $aExtKeys))
 		{
 			foreach ($aExtKeys[$sTableClass] as $sKeyAttCode => $aExtFields)
 			{
-				$oKeyAttDef = self::GetAttributeDef($sTargetClass, $sKeyAttCode);
-
-				$oExtFilter = $oFilter->GetCriteria_PointingTo($sKeyAttCode);
+				$oKeyAttDef = self::GetAttributeDef($sTableClass, $sKeyAttCode);
 
-				// In case the join was not explicitely defined in the filter,
-				// we need to do it now
-				if (empty($oExtFilter))
+				$aPointingTo = $oFilter->GetCriteria_PointingTo($sKeyAttCode);
+//echo "MAKEQUERY-Cle '$sKeyAttCode'<br/>\n";
+				if (!array_key_exists(TREE_OPERATOR_EQUALS, $aPointingTo))
 				{
+//echo "MAKEQUERY-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 = self::GenerateUniqueAlias($aClassAliases, $sKeyClass.'_'.$sKeyAttCode, $sKeyClass);
 					$oExtFilter = new DBObjectSearch($sKeyClass, $sKeyClassAlias);
+
+					$aAllPointingTo[$sKeyAttCode][TREE_OPERATOR_EQUALS] = $oExtFilter;
 				}
-				else
-				{
-					// The aliases should not conflict because normalization occured while building the filter
-					$sKeyClass =  $oExtFilter->GetFirstJoinedClass();
-					$sKeyClassAlias = $oExtFilter->GetFirstJoinedClassAlias();
-					
-					// Note: there is no search condition in $oExtFilter, because normalization did merge the condition onto the top of the filter tree 
-				}
+			}
+		}
+//echo "MAKEQUERY-liste des clefs de jointure: <pre>".print_r(array_keys($aAllPointingTo), true)."</pre><br/>\n";
+				
+		foreach ($aAllPointingTo as $sKeyAttCode => $aPointingTo)
+		{
+			foreach($aPointingTo as $iOperatorCode => $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 = self::GetAttributeDef($sTableClass, $sKeyAttCode);
+				$sKeyClass =  $oExtFilter->GetFirstJoinedClass();
+				$sKeyClassAlias = $oExtFilter->GetFirstJoinedClassAlias();
+
+//echo "MAKEQUERY-$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 
 
-				// 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($aExtFields as $sAttCode => $oAtt)
+				if ($iOperatorCode == TREE_OPERATOR_EQUALS)
 				{
-					if ($oAtt instanceof AttributeFriendlyName)
-					{
-						// Note: for a given ext key, there is one single attribute "friendly name"
-						$aTranslateNow[$sTargetAlias][$sAttCode] = new FieldExpression('friendlyname', $sKeyClassAlias);
-					}
-					else
+					// 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...)
+//echo "MAKEQUERY-array_key_exists($sTableClass, \$aExtKeys)<br/>\n";
+					if (array_key_exists($sTableClass, $aExtKeys) && array_key_exists($sKeyAttCode, $aExtKeys[$sTableClass]))
 					{
-						$sExtAttCode = $oAtt->GetExtAttCode();
-						// Translate mainclass.extfield => remoteclassalias.remotefieldcode
-						$oRemoteAttDef = self::GetAttributeDef($sKeyClass, $sExtAttCode);
-						foreach ($oRemoteAttDef->GetSQLExpressions() as $sColID => $sRemoteAttExpr)
+						foreach($aExtKeys[$sTableClass][$sKeyAttCode] as $sAttCode => $oAtt)
+						{
+//echo "MAKEQUERY 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 = self::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";
+							}
+						}
+						// Translate prior to recursing
+						//
+//echo "<p>oQBExpr ".__LINE__.": <pre>\n".print_r($oQBExpr, true)."\n".print_r($aTranslateNow, true)."</pre></p>\n";
+						$oQBExpr->Translate($aTranslateNow, false);
+//echo "<p>oQBExpr ".__LINE__.": <pre>\n".print_r($oQBExpr, true)."</pre></p>\n";
+		
+//echo "<p>External key $sKeyAttCode (class: $sKeyClass), call MakeQuery()/p>\n";
+						self::DbgTrace("External key $sKeyAttCode (class: $sKeyClass), call MakeQuery()");
+						$oQBExpr->PushJoinField(new FieldExpression('id', $sKeyClassAlias));
+		
+//echo "<p>Recursive MakeQuery ".__LINE__.": <pre>\n".print_r($aSelectedClasses, true)."</pre></p>\n";
+						$oSelectExtKey = self::MakeQuery($aSelectedClasses, $oQBExpr, $aClassAliases, $aTableAliases, $oExtFilter);
+		
+						$oJoinExpr = $oQBExpr->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
 						{
-							$aTranslateNow[$sTargetAlias][$sAttCode.$sColId] = new FieldExpression($sExtAttCode, $sKeyClassAlias);
+							$oSelectBase->AddInnerJoin($oSelectExtKey, $sLocalKeyField, $sExternalKeyField, $sExternalKeyTable);
 						}
-						//#@# debug - echo "<p>$sTargetAlias.$sAttCode to $sKeyClassAlias.$sRemoteAttExpr (class: $sKeyClass)</p>\n";
 					}
 				}
-				// Translate prior to recursing
-				//
-				$oQBExpr->Translate($aTranslateNow, false);
-
-				self::DbgTrace("External key $sKeyAttCode (class: $sKeyClass), call MakeQuery()");
-
-				$oQBExpr->PushJoinField(new FieldExpression('id', $sKeyClassAlias));
-
-				$oSelectExtKey = self::MakeQuery($aSelectedClasses, $oQBExpr, $aClassAliases, $aTableAliases, $oExtFilter);
-
-				$oJoinExpr = $oQBExpr->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);
+					$oQBExpr->PushJoinField(new FieldExpression($sKeyAttCode, $sKeyClassAlias));
+					$oSelectExtKey = self::MakeQuery($aSelectedClasses, $oQBExpr, $aClassAliases, $aTableAliases, $oExtFilter);
+					$oJoinExpr = $oQBExpr->PopJoinField();
+//echo "MAKEQUERY-PopJoinField pour $sKeyAttCode, $sKeyClassAlias: <pre>".print_r($oJoinExpr, true)."</pre><br/>\n";
+					$sExternalKeyTable = $oJoinExpr->GetParent();
+					$sExternalKeyField = $oJoinExpr->GetName();
+					$sLeftIndex = $sExternalKeyField.'_left'; // TODO use GetSQLLeft()
+					$sRightIndex = $sExternalKeyField.'_right'; // TODO use GetSQLRight()
+
+					$LocalKeyLeft = $oKeyAttDef->GetSQLLeft();
+//echo "MAKEQUERY-LocalKeyLeft pour $sKeyAttCode => $LocalKeyLeft<br/>\n";
+
+					$oSelectBase->AddInnerJoinTree($oSelectExtKey, $LocalKeyLeft, $sLeftIndex, $sRightIndex, $sExternalKeyTable, $iOperatorCode);
 				}
 			}
 		}
 
 		// Translate the selected columns
 		//
+//echo "<p>oQBExpr ".__LINE__.": <pre>\n".print_r($oQBExpr, true)."</pre></p>\n";
 		$oQBExpr->Translate($aTranslation, false);
+//echo "<p>oQBExpr ".__LINE__.": <pre>\n".print_r($oQBExpr, true)."</pre></p>\n";
 
 		//MyHelpers::var_dump_html($oSelectBase->RenderSelect());
 		return $oSelectBase;
 	}
 
+	/**
+	 * Special processing for the hierarchical keys stored as nested sets
+	 * @param $iId integer The identifier of the parent
+	 * @param $oAttDef AttributeDefinition The attribute corresponding to the hierarchical key
+	 * @param $stable string The name of the database table containing the hierarchical key 
+	 */
+	public static function HKInsertChildUnder($iId, $oAttDef, $sTable)
+	{
+		// Get the parent id.right value
+		if ($iId == 0)
+		{
+			// No parent, insert completely at the right of the tree
+			$sSQL = "SELECT max(`".$oAttDef->GetSQLRight()."`) AS max FROM `$sTable`";
+			$aRes = CMDBSource::QueryToArray($sSQL);
+			if (count($aRes) == 0)
+			{
+				$iMyRight = 1;
+			}
+			else
+			{
+				$iMyRight = $aRes[0]['max']+1;
+			}
+		}
+		else
+		{
+			$sSQL = "SELECT `".$oAttDef->GetSQLRight()."` FROM `$sTable` WHERE id=".$iId;
+			$iMyRight = CMDBSource::QueryToScalar($sSQL);
+			$sSQLUpdateRight = "UPDATE `$sTable` SET `".$oAttDef->GetSQLRight()."` = `".$oAttDef->GetSQLRight()."` + 2 WHERE `".$oAttDef->GetSQLRight()."` >= $iMyRight";
+			CMDBSource::Query($sSQLUpdateRight);
+			$sSQLUpdateLeft = "UPDATE `$sTable` SET `".$oAttDef->GetSQLLeft()."` = `".$oAttDef->GetSQLLeft()."` + 2 WHERE `".$oAttDef->GetSQLLeft()."` > $iMyRight";
+			CMDBSource::Query($sSQLUpdateLeft);
+		}
+		return array($oAttDef->GetSQLRight() =>  $iMyRight+1, $oAttDef->GetSQLLeft() => $iMyRight);
+	}
+
+	/**
+	 * Special processing for the hierarchical keys stored as nested sets: temporary remove the branch
+	 * @param $iId integer The identifier of the parent
+	 * @param $oAttDef AttributeDefinition The attribute corresponding to the hierarchical key
+	 * @param $sTable string The name of the database table containing the hierarchical key 
+	 */
+	public static function HKTemporaryCutBranch($iMyLeft, $iMyRight, $oAttDef, $sTable)
+	{
+		$iDelta = $iMyRight - $iMyLeft + 1;
+		$sSQL = "UPDATE `$sTable` SET `".$oAttDef->GetSQLRight()."` = $iMyLeft - `".$oAttDef->GetSQLRight()."`, `".$oAttDef->GetSQLLeft()."` = $iMyLeft - `".$oAttDef->GetSQLLeft();
+		$sSQL .= "` WHERE  `".$oAttDef->GetSQLLeft()."`> $iMyLeft AND `".$oAttDef->GetSQLRight()."`< $iMyRight";
+		CMDBSource::Query($sSQL);
+		$sSQL = "UPDATE `$sTable` SET `".$oAttDef->GetSQLLeft()."` = `".$oAttDef->GetSQLLeft()."` - $iDelta WHERE `".$oAttDef->GetSQLLeft()."` > $iMyRight";
+		CMDBSource::Query($sSQL);
+		$sSQL = "UPDATE `$sTable` SET `".$oAttDef->GetSQLRight()."` = `".$oAttDef->GetSQLRight()."` - $iDelta WHERE `".$oAttDef->GetSQLRight()."` > $iMyRight";
+		CMDBSource::Query($sSQL);
+	}
+
+	/**
+	 * Special processing for the hierarchical keys stored as nested sets: replug the temporary removed branch
+	 * @param $iId integer The identifier of the parent
+	 * @param $oAttDef AttributeDefinition The attribute corresponding to the hierarchical key
+	 * @param $sTable string The name of the database table containing the hierarchical key 
+	 */
+	public static function HKReplugBranch($iNewLeft, $iNewRight, $oAttDef, $sTable)
+	{
+		$iDelta = $iNewRight - $iNewLeft + 1;
+		$sSQL = "UPDATE `$sTable` SET `".$oAttDef->GetSQLLeft()."` = `".$oAttDef->GetSQLLeft()."` + $iDelta WHERE `".$oAttDef->GetSQLLeft()."` > $iNewLeft";
+		CMDBSource::Query($sSQL);
+		$sSQL = "UPDATE `$sTable` SET `".$oAttDef->GetSQLRight()."` = `".$oAttDef->GetSQLRight()."` + $iDelta WHERE `".$oAttDef->GetSQLRight()."` >= $iNewLeft";
+		CMDBSource::Query($sSQL);
+		$sSQL = "UPDATE `$sTable` SET `".$oAttDef->GetSQLRight()."` = $iNewLeft - `".$oAttDef->GetSQLRight()."`, `".$oAttDef->GetSQLLeft()."` = $iNewLeft - `".$oAttDef->GetSQLLeft()."` WHERE `".$oAttDef->GetSQLRight()."`< 0";
+		CMDBSource::Query($sSQL);
+	}
+
+	/**
+	 * Initializes (i.e converts) a hierarchy stored using a 'parent_id' external key
+	 * into a hierarchy stored with a HierarchicalKey, by initializing the _left and _right values
+	 * to correspond to the existing hierarchy in the database
+	 * @param $sClass string Name of the class to process
+	 * @param $sAttCode string Code of the attribute to process
+	 */
+	public static function HKInit($sClass, $sAttCode)
+	{
+		$idx = 1;
+		$oAttDef = self::GetAttributeDef($sClass, $sAttCode);
+		$sTable = self::DBGetTable($sClass, $sAttCode);
+		if ($oAttDef->IsHierarchicalKey())
+		{
+			try
+			{
+				CMDBSource::Query('START TRANSACTION');
+				self::HKInitChildren($sTable, $sAttCode, $oAttDef, 0, $idx);
+				CMDBSource::Query('COMMIT');
+			}
+			catch(Exception $e)
+			{
+				CMDBSource::Query('ROLLBACK');
+				throw new Exception("An error occured (".$e->getMessage().") while initializing the hierarchy for ($sClass, $sAttCode). The database was not modified.");
+			}
+		}
+	}
+	
+	/**
+	 * Recursive helper function called by HKInit
+	 */
+	protected static function HKInitChildren($sTable, $sAttCode, $oAttDef, $iId, &$iCurrIndex)
+	{
+		$sSQL = "SELECT id FROM `$sTable` WHERE `$sAttCode` = $iId";
+		$aRes = CMDBSource::QueryToArray($sSQL);
+		$aTree = array();
+		$sLeft = $oAttDef->GetSQLLeft();
+		$sRight = $oAttDef->GetSQLRight();
+		foreach($aRes as $aValues)
+		{
+			$iChildId = $aValues['id'];
+			$iLeft = $iCurrIndex++;
+			$aChildren = self::HKInitChildren($sTable, $sAttCode, $oAttDef, $iChildId, $iCurrIndex);
+			$iRight = $iCurrIndex++;
+			$sSQL = "UPDATE `$sTable` SET `$sLeft` = $iLeft, `$sRight` = $iRight WHERE id= $iChildId";
+			CMDBSource::Query($sSQL);
+		}
+	}
+	
 	public static function GenerateUniqueAlias(&$aAliases, $sNewName, $sRealName)
 	{
 		if (!array_key_exists($sNewName, $aAliases))

+ 4 - 0
core/oql/build.bash

@@ -0,0 +1,4 @@
+#!/bin/bash
+php /usr/share/php/PHP/LexerGenerator/cli.php oql-lexer.plex
+php /usr/share/php/PHP/ParserGenerator/cli.php oql-parser.y
+

+ 177 - 153
core/oql/oql-lexer.php

@@ -104,155 +104,159 @@ class OQLLexerRaw
         if ($this->count >= strlen($this->data)) {
             return false; // end of input
         }
-    	do {
-	    	$rules = array(
-    			'/^[ \t\n\r]+/',
-    			'/^SELECT/',
-    			'/^FROM/',
-    			'/^AS/',
-    			'/^WHERE/',
-    			'/^JOIN/',
-    			'/^ON/',
-    			'/^\//',
-    			'/^\\*/',
-    			'/^\\+/',
-    			'/^-/',
-    			'/^AND/',
-    			'/^OR/',
-    			'/^,/',
-    			'/^\\(/',
-    			'/^\\)/',
-    			'/^REGEXP/',
-    			'/^=/',
-    			'/^!=/',
-    			'/^>/',
-    			'/^</',
-    			'/^>=/',
-    			'/^<=/',
-    			'/^LIKE/',
-    			'/^NOT LIKE/',
-    			'/^IN/',
-    			'/^NOT IN/',
-    			'/^INTERVAL/',
-    			'/^IF/',
-    			'/^ELT/',
-    			'/^COALESCE/',
-    			'/^ISNULL/',
-    			'/^CONCAT/',
-    			'/^SUBSTR/',
-    			'/^TRIM/',
-    			'/^DATE/',
-    			'/^DATE_FORMAT/',
-    			'/^CURRENT_DATE/',
-    			'/^NOW/',
-    			'/^TIME/',
-    			'/^TO_DAYS/',
-    			'/^FROM_DAYS/',
-    			'/^YEAR/',
-    			'/^MONTH/',
-    			'/^DAY/',
-    			'/^HOUR/',
-    			'/^MINUTE/',
-    			'/^SECOND/',
-    			'/^DATE_ADD/',
-    			'/^DATE_SUB/',
-    			'/^ROUND/',
-    			'/^FLOOR/',
-    			'/^INET_ATON/',
-    			'/^INET_NTOA/',
-    			'/^[0-9]+|0x[0-9a-fA-F]+/',
-    			'/^\"([^\\\\\"]|\\\\\"|\\\\\\\\)*\"|'.chr(94).chr(39).'([^\\\\'.chr(39).']|\\\\'.chr(39).'|\\\\\\\\)*'.chr(39).'/',
-    			'/^([_a-zA-Z][_a-zA-Z0-9]*|`[^`]+`)/',
-    			'/^:([_a-zA-Z][_a-zA-Z0-9]*->[_a-zA-Z][_a-zA-Z0-9]*|[_a-zA-Z][_a-zA-Z0-9]*)/',
-    			'/^\\./',
-	    	);
-	    	$match = false;
-	    	foreach ($rules as $index => $rule) {
-	    		if (preg_match($rule, substr($this->data, $this->count), $yymatches)) {
-	            	if ($match) {
-	            	    if (strlen($yymatches[0]) > strlen($match[0][0])) {
-	            	    	$match = array($yymatches, $index); // matches, token
-	            	    }
-	            	} else {
-	            		$match = array($yymatches, $index);
-	            	}
-	            }
-	    	}
-	    	if (!$match) {
-	            throw new Exception('Unexpected input at line' . $this->line .
-	                ': ' . $this->data[$this->count]);
-	    	}
-	    	$this->token = $match[1];
-	    	$this->value = $match[0][0];
-	    	$yysubmatches = $match[0];
-	    	array_shift($yysubmatches);
-	    	if (!$yysubmatches) {
-	    		$yysubmatches = array();
-	    	}
-	        $r = $this->{'yy_r1_' . $this->token}($yysubmatches);
-	        if ($r === null) {
-	            $this->count += strlen($this->value);
-	            $this->line += substr_count($this->value, "\n");
-	            // accept this token
-	            return true;
-	        } elseif ($r === true) {
-	            // we have changed state
-	            // process this token in the new state
-	            return $this->yylex();
-	        } elseif ($r === false) {
-	            $this->count += strlen($this->value);
-	            $this->line += substr_count($this->value, "\n");
-	            if ($this->count >= strlen($this->data)) {
-	                return false; // end of input
-	            }
-	            // skip this token
-	            continue;
-	        } else {
-	            $yy_yymore_patterns = array_slice($rules, $this->token, true);
-	            // yymore is needed
-	            do {
-	                if (!isset($yy_yymore_patterns[$this->token])) {
-	                    throw new Exception('cannot do yymore for the last token');
-	                }
-			    	$match = false;
-	                foreach ($yy_yymore_patterns[$this->token] as $index => $rule) {
-	                	if (preg_match('/' . $rule . '/',
-	                      	  substr($this->data, $this->count), $yymatches)) {
-	                    	$yymatches = array_filter($yymatches, 'strlen'); // remove empty sub-patterns
-			            	if ($match) {
-			            	    if (strlen($yymatches[0]) > strlen($match[0][0])) {
-			            	    	$match = array($yymatches, $index); // matches, token
-			            	    }
-			            	} else {
-			            		$match = array($yymatches, $index);
-			            	}
-			            }
-			    	}
-			    	if (!$match) {
-			            throw new Exception('Unexpected input at line' . $this->line .
-			                ': ' . $this->data[$this->count]);
-			    	}
-			    	$this->token = $match[1];
-			    	$this->value = $match[0][0];
-			    	$yysubmatches = $match[0];
-			    	array_shift($yysubmatches);
-			    	if (!$yysubmatches) {
-			    		$yysubmatches = array();
-			    	}
-	                $this->line = substr_count($this->value, "\n");
-	                $r = $this->{'yy_r1_' . $this->token}();
-	            } while ($r !== null || !$r);
-		        if ($r === true) {
-		            // we have changed state
-		            // process this token in the new state
-		            return $this->yylex();
-		        } else {
-	                // accept
-	                $this->count += strlen($this->value);
-	                $this->line += substr_count($this->value, "\n");
-	                return true;
-		        }
-	        }
+        do {
+            $rules = array(
+                '/\G[ \t\n\r]+/ ',
+                '/\GSELECT/ ',
+                '/\GFROM/ ',
+                '/\GAS/ ',
+                '/\GWHERE/ ',
+                '/\GJOIN/ ',
+                '/\GON/ ',
+                '/\G\// ',
+                '/\G\\*/ ',
+                '/\G\\+/ ',
+                '/\G-/ ',
+                '/\GAND/ ',
+                '/\GOR/ ',
+                '/\G,/ ',
+                '/\G\\(/ ',
+                '/\G\\)/ ',
+                '/\GREGEXP/ ',
+                '/\G=/ ',
+                '/\G!=/ ',
+                '/\G>/ ',
+                '/\G</ ',
+                '/\G>=/ ',
+                '/\G<=/ ',
+                '/\GLIKE/ ',
+                '/\GNOT LIKE/ ',
+                '/\GIN/ ',
+                '/\GNOT IN/ ',
+                '/\GINTERVAL/ ',
+                '/\GIF/ ',
+                '/\GELT/ ',
+                '/\GCOALESCE/ ',
+                '/\GISNULL/ ',
+                '/\GCONCAT/ ',
+                '/\GSUBSTR/ ',
+                '/\GTRIM/ ',
+                '/\GDATE/ ',
+                '/\GDATE_FORMAT/ ',
+                '/\GCURRENT_DATE/ ',
+                '/\GNOW/ ',
+                '/\GTIME/ ',
+                '/\GTO_DAYS/ ',
+                '/\GFROM_DAYS/ ',
+                '/\GYEAR/ ',
+                '/\GMONTH/ ',
+                '/\GDAY/ ',
+                '/\GHOUR/ ',
+                '/\GMINUTE/ ',
+                '/\GSECOND/ ',
+                '/\GDATE_ADD/ ',
+                '/\GDATE_SUB/ ',
+                '/\GROUND/ ',
+                '/\GFLOOR/ ',
+                '/\GINET_ATON/ ',
+                '/\GINET_NTOA/ ',
+                '/\GBELOW/ ',
+                '/\GBELOW STRICT/ ',
+                '/\GNOT BELOW/ ',
+                '/\GNOT BELOW STRICT/ ',
+                '/\G[0-9]+|0x[0-9a-fA-F]+/ ',
+                '/\G\"([^\\\\\"]|\\\\\"|\\\\\\\\)*\"|'.chr(94).chr(39).'([^\\\\'.chr(39).']|\\\\'.chr(39).'|\\\\\\\\)*'.chr(39).'/ ',
+                '/\G([_a-zA-Z][_a-zA-Z0-9]*|`[^`]+`)/ ',
+                '/\G:([_a-zA-Z][_a-zA-Z0-9]*->[_a-zA-Z][_a-zA-Z0-9]*|[_a-zA-Z][_a-zA-Z0-9]*)/ ',
+                '/\G\\./ ',
+            );
+            $match = false;
+            foreach ($rules as $index => $rule) {
+                if (preg_match($rule, substr($this->data, $this->count), $yymatches)) {
+                    if ($match) {
+                        if (strlen($yymatches[0]) > strlen($match[0][0])) {
+                            $match = array($yymatches, $index); // matches, token
+                        }
+                    } else {
+                        $match = array($yymatches, $index);
+                    }
+                }
+            }
+            if (!$match) {
+                throw new Exception('Unexpected input at line ' . $this->line .
+                    ': ' . $this->data[$this->count]);
+            }
+            $this->token = $match[1];
+            $this->value = $match[0][0];
+            $yysubmatches = $match[0];
+            array_shift($yysubmatches);
+            if (!$yysubmatches) {
+                $yysubmatches = array();
+            }
+            $r = $this->{'yy_r1_' . $this->token}($yysubmatches);
+            if ($r === null) {
+                $this->count += strlen($this->value);
+                $this->line += substr_count($this->value, "\n");
+                // accept this token
+                return true;
+            } elseif ($r === true) {
+                // we have changed state
+                // process this token in the new state
+                return $this->yylex();
+            } elseif ($r === false) {
+                $this->count += strlen($this->value);
+                $this->line += substr_count($this->value, "\n");
+                if ($this->count >= strlen($this->data)) {
+                    return false; // end of input
+                }
+                // skip this token
+                continue;
+            } else {
+                $yy_yymore_patterns = array_slice($rules, $this->token, true);
+                // yymore is needed
+                do {
+                    if (!isset($yy_yymore_patterns[$this->token])) {
+                        throw new Exception('cannot do yymore for the last token');
+                    }
+                    $match = false;
+                    foreach ($yy_yymore_patterns[$this->token] as $index => $rule) {
+                        if (preg_match('/' . $rule . '/',
+                                $this->data, $yymatches, null, $this->count)) {
+                            $yymatches = array_filter($yymatches, 'strlen'); // remove empty sub-patterns
+                            if ($match) {
+                                if (strlen($yymatches[0]) > strlen($match[0][0])) {
+                                    $match = array($yymatches, $index); // matches, token
+                                }
+                            } else {
+                                $match = array($yymatches, $index);
+                            }
+                        }
+                    }
+                    if (!$match) {
+                        throw new Exception('Unexpected input at line ' . $this->line .
+                            ': ' . $this->data[$this->count]);
+                    }
+                    $this->token = $match[1];
+                    $this->value = $match[0][0];
+                    $yysubmatches = $match[0];
+                    array_shift($yysubmatches);
+                    if (!$yysubmatches) {
+                        $yysubmatches = array();
+                    }
+                    $this->line = substr_count($this->value, "\n");
+                    $r = $this->{'yy_r1_' . $this->token}();
+                } while ($r !== null || !$r);
+                if ($r === true) {
+                    // we have changed state
+                    // process this token in the new state
+                    return $this->yylex();
+                } else {
+                    // accept
+                    $this->count += strlen($this->value);
+                    $this->line += substr_count($this->value, "\n");
+                    return true;
+                }
+            }
         } while (true);
 
     } // end function
@@ -530,26 +534,46 @@ class OQLLexerRaw
     function yy_r1_54($yy_subpatterns)
     {
 
-	$this->token = OQLParser::NUMVAL;
+	$this->token = OQLParser::BELOW;
     }
     function yy_r1_55($yy_subpatterns)
     {
 
-	$this->token = OQLParser::STRVAL;
+	$this->token = OQLParser::BELOW_STRICT;
     }
     function yy_r1_56($yy_subpatterns)
     {
 
-	$this->token = OQLParser::NAME;
+	$this->token = OQLParser::NOT_BELOW;
     }
     function yy_r1_57($yy_subpatterns)
     {
 
-	$this->token = OQLParser::VARNAME;
+	$this->token = OQLParser::NOT_BELOW_STRICT;
     }
     function yy_r1_58($yy_subpatterns)
     {
 
+	$this->token = OQLParser::NUMVAL;
+    }
+    function yy_r1_59($yy_subpatterns)
+    {
+
+	$this->token = OQLParser::STRVAL;
+    }
+    function yy_r1_60($yy_subpatterns)
+    {
+
+	$this->token = OQLParser::NAME;
+    }
+    function yy_r1_61($yy_subpatterns)
+    {
+
+	$this->token = OQLParser::VARNAME;
+    }
+    function yy_r1_62($yy_subpatterns)
+    {
+
 	$this->token = OQLParser::DOT;
     }
 

+ 16 - 0
core/oql/oql-lexer.plex

@@ -132,6 +132,10 @@ f_round    = "ROUND"
 f_floor    = "FLOOR"
 f_inet_aton = "INET_ATON"
 f_inet_ntoa = "INET_NTOA"
+below            = "BELOW"
+below_strict     = "BELOW STRICT"
+not_below        = "NOT BELOW"
+not_below_strict = "NOT BELOW STRICT"
 numval     = /[0-9]+|0x[0-9a-fA-F]+/
 strval     = /"([^\\"]|\\"|\\\\)*"|'.chr(94).chr(39).'([^\\'.chr(39).']|\\'.chr(39).'|\\\\)*'.chr(39).'/
 name       = /([_a-zA-Z][_a-zA-Z0-9]*|`[^`]+`)/
@@ -302,6 +306,18 @@ f_inet_aton {
 f_inet_ntoa {
 	$this->token = OQLParser::F_INET_NTOA;
 }
+below {
+	$this->token = OQLParser::BELOW;
+}
+below_strict {
+	$this->token = OQLParser::BELOW_STRICT;
+}
+not_below {
+	$this->token = OQLParser::NOT_BELOW;
+}
+not_below_strict {
+	$this->token = OQLParser::NOT_BELOW_STRICT;
+}
 numval {
 	$this->token = OQLParser::NUMVAL;
 }

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 524 - 487
core/oql/oql-parser.php


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

@@ -78,6 +78,10 @@ join_item(A) ::= JOIN class_name(X) ON join_condition(C).
 }
 
 join_condition(A) ::= field_id(X) EQ field_id(Y). { A = new BinaryOqlExpression(X, '=', Y); }
+join_condition(A) ::= field_id(X) BELOW field_id(Y). { A = new BinaryOqlExpression(X, 'BELOW', Y); }
+join_condition(A) ::= field_id(X) BELOW_STRICT field_id(Y). { A = new BinaryOqlExpression(X, 'BELOW_STRICT', Y); }
+join_condition(A) ::= field_id(X) NOT_BELOW field_id(Y). { A = new BinaryOqlExpression(X, 'NOT_BELOW', Y); }
+join_condition(A) ::= field_id(X) NOT_BELOW_STRICT field_id(Y). { A = new BinaryOqlExpression(X, 'NOT_BELOW_STRICT', Y); }
 
 condition(A) ::= expression_prio4(X). { A = X; }
 

+ 7 - 0
core/oql/oqlquery.class.inc.php

@@ -59,6 +59,7 @@ class OqlJoinSpec
 	protected $m_oClassAlias;
 	protected $m_oLeftField;
 	protected $m_oRightField;
+	protected $m_sOperator;
 
 	protected $m_oNextJoinspec;
 
@@ -68,6 +69,8 @@ class OqlJoinSpec
 		$this->m_oClassAlias = $oClassAlias;
 		$this->m_oLeftField = $oExpression->GetLeftExpr();
 		$this->m_oRightField = $oExpression->GetRightExpr();
+		$this->m_oRightField = $oExpression->GetRightExpr();
+		$this->m_sOperator = $oExpression->GetOperator();
 	}
 
 	public function GetClass()
@@ -96,6 +99,10 @@ class OqlJoinSpec
 	{
 		return $this->m_oRightField;
 	}
+	public function GetOperator()
+	{
+		return $this->m_sOperator;
+	}
 }
 
 class BinaryOqlExpression extends BinaryExpression

+ 60 - 14
core/sqlquery.class.inc.php

@@ -172,9 +172,25 @@ class SQLQuery
 			"righttablealias" => $sRightTableAlias
 		);
 	}
-	public function AddInnerJoin($oSQLQuery, $sLeftField, $sRightField, $sRigthtTable = '')
+	public function AddInnerJoin($oSQLQuery, $sLeftField, $sRightField, $sRightTable = '')
 	{
-		$this->AddJoin("inner", $oSQLQuery, $sLeftField, $sRightField, $sRigthtTable);
+		$this->AddJoin("inner", $oSQLQuery, $sLeftField, $sRightField, $sRightTable);
+	}
+	public function AddInnerJoinTree($oSQLQuery, $sLeftField, $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" => $sLeftField,
+			"rightfield_left" => $sRightFieldLeft,
+			"rightfield_right" => $sRightFieldRight,
+			"righttablealias" => $sRightTableAlias,
+			"tree_operator" => $iOperatorCode);
 	}
 	public function AddLeftJoin($oSQLQuery, $sLeftField, $sRightField)
 	{
@@ -227,7 +243,6 @@ class SQLQuery
 		$aSetValues = array();
 		$aSelectedIdFields = array();
 		$this->privRender($aFrom, $aFields, $oCondition, $aDelTables, $aSetValues, $aSelectedIdFields);
-
 		$sFrom   = self::ClauseFrom($aFrom);
 		$sValues = self::ClauseValues($aSetValues);
 		$sWhere  = self::ClauseWhere($oCondition, $aArgs);
@@ -315,6 +330,7 @@ class SQLQuery
 					$sFrom .= " ".self::ClauseFrom($aJoinInfo["subfrom"]);
 					break;
 				case "inner":
+				case "inner_tree":
 					$sFrom .= " INNER JOIN (`".$aJoinInfo["tablename"]."` AS `$sTableAlias`";
 					$sFrom .= " ".self::ClauseFrom($aJoinInfo["subfrom"]);
 					$sFrom .= ") ON ".$aJoinInfo["joincondition"];
@@ -368,12 +384,12 @@ class SQLQuery
 	// Purpose: prepare the query data, once for all
 	private function privRender(&$aFrom, &$aFields, &$oCondition, &$aDelTables, &$aSetValues, &$aSelectedIdFields)
 	{
-		$sTableAlias = $this->privRenderSingleTable($aFrom, $aFields, $aDelTables, $aSetValues, $aSelectedIdFields);
+		$sTableAlias = $this->privRenderSingleTable($aFrom, $aFields, $aDelTables, $aSetValues, $aSelectedIdFields, '', array('jointype' => 'first'));
 		$oCondition = $this->m_oConditionExpr;
 		return $sTableAlias; 
 	}
 
-	private function privRenderSingleTable(&$aFrom, &$aFields, &$aDelTables, &$aSetValues, &$aSelectedIdFields, $sJoinType = 'first', $sCallerAlias = '', $sLeftField = '', $sRightField = '', $sRightTableAlias = '')
+	private function privRenderSingleTable(&$aFrom, &$aFields, &$aDelTables, &$aSetValues, &$aSelectedIdFields, $sCallerAlias = '', $aJoinData)
 	{
 		$aActualTableFields = CMDBSource::GetTableFieldsList($this->m_sTable);
 
@@ -381,12 +397,15 @@ class SQLQuery
 
 		// Handle the various kinds of join (or first table in the list)
 		//
-		if (empty($sRightTableAlias))
+		if (empty($aJoinData['righttablealias']))
 		{
 			$sRightTableAlias = $this->m_sTableAlias;
 		}
-		$sJoinCond = "`$sCallerAlias`.`$sLeftField` = `$sRightTableAlias`.`$sRightField`";
-		switch ($sJoinType)
+		else
+		{
+			$sRightTableAlias = $aJoinData['righttablealias'];
+		}
+		switch ($aJoinData['jointype'])
 		{
 			case "first":
 				$aFrom[$this->m_sTableAlias] = array("jointype"=>"first", "tablename"=>$this->m_sTable, "joincondition"=>"");
@@ -394,7 +413,33 @@ class SQLQuery
 			case "inner":
 			case "left":
 			// table or tablealias ???
-				$aFrom[$this->m_sTableAlias] = array("jointype"=>$sJoinType, "tablename"=>$this->m_sTable, "joincondition"=>"$sJoinCond");
+				$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']}`";
+				$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;
+					
+				}
+				$aFrom[$this->m_sTableAlias] = array("jointype"=>$aJoinData['jointype'], "tablename"=>$this->m_sTable, "joincondition"=>"$sJoinCond");
 				break;
 		}
 
@@ -409,6 +454,7 @@ class SQLQuery
 		{
 			$aDelTables[] = "`{$this->m_sTableAlias}`";
 		}
+//echo "<p>in privRenderSingleTable this->m_aValues<pre>".print_r($this->m_aValues, true)."</pre></p>\n";	
 		foreach($this->m_aValues as $sFieldName=>$value)
 		{
 			$aSetValues["`{$this->m_sTableAlias}`.`$sFieldName`"] = $value; // quoted further!
@@ -424,13 +470,13 @@ class SQLQuery
 		$aTempFrom = array(); // temporary subset of 'from' specs, to be grouped in the final query
 		foreach ($this->m_aJoinSelects as $aJoinData)
 		{
-			$sJoinType = $aJoinData["jointype"];
 			$oRightSelect = $aJoinData["select"];
-			$sLeftField = $aJoinData["leftfield"];
-			$sRightField = $aJoinData["rightfield"];
-			$sRightTableAlias = $aJoinData["righttablealias"];
+//			$sJoinType = $aJoinData["jointype"];
+//			$sLeftField = $aJoinData["leftfield"];
+//			$sRightField = $aJoinData["rightfield"];
+//			$sRightTableAlias = $aJoinData["righttablealias"];
 
-			$sJoinTableAlias = $oRightSelect->privRenderSingleTable($aTempFrom, $aFields, $aDelTables, $aSetValues, $aSelectedIdFields, $sJoinType, $this->m_sTableAlias, $sLeftField, $sRightField, $sRightTableAlias);
+			$sJoinTableAlias = $oRightSelect->privRenderSingleTable($aTempFrom, $aFields, $aDelTables, $aSetValues, $aSelectedIdFields, $this->m_sTableAlias, $aJoinData);
 		}
 		$aFrom[$this->m_sTableAlias]['subfrom'] = $aTempFrom;
 

+ 42 - 0
core/valuesetdef.class.inc.php

@@ -95,6 +95,7 @@ class ValueSetObjects extends ValueSetDefinition
 	protected $m_sFilterExpr; // in OQL
 	protected $m_sValueAttCode;
 	protected $m_aOrderBy;
+	protected $m_aExtraConditions;
 	private $m_bAllowAllData;
 
 	public function __construct($sFilterExp, $sValueAttCode = '', $aOrderBy = array(), $bAllowAllData = false)
@@ -104,8 +105,13 @@ class ValueSetObjects extends ValueSetDefinition
 		$this->m_sValueAttCode = $sValueAttCode;
 		$this->m_aOrderBy = $aOrderBy;
 		$this->m_bAllowAllData = $bAllowAllData;
+		$this->m_aExtraConditions = array();
 	}
 
+	public function AddCondition(DBObjectSearch $oFilter)
+	{
+		$this->m_aExtraConditions[] = $oFilter;		
+	}
 
 	public function ToObjectSet($aArgs = array(), $sContains = '')
 	{
@@ -117,6 +123,10 @@ class ValueSetObjects extends ValueSetDefinition
 		{
 			$oFilter = DBObjectSearch::FromOQL($this->m_sFilterExpr);
 		}
+		foreach($this->m_aExtraConditions as $oExtraFilter)
+		{
+			$oFilter->MergeWith($oExtraFilter);
+		}
 
 		return new DBObjectSet($oFilter, $this->m_aOrderBy, $aArgs);
 	}
@@ -148,6 +158,10 @@ class ValueSetObjects extends ValueSetDefinition
 			$oFilter = DBObjectSearch::FromOQL($this->m_sFilterExpr);
 		}
 		if (!$oFilter) return false;
+		foreach($this->m_aExtraConditions as $oExtraFilter)
+		{
+			$oFilter->MergeWith($oExtraFilter);
+		}
 
 		$oValueExpr = new ScalarExpression('%'.$sContains.'%');
 		$oNameExpr = new FieldExpression('friendlyname', $oFilter->GetClassAlias());
@@ -289,6 +303,34 @@ class ValueSetEnum extends ValueSetDefinition
 	}
 }
 
+/**
+ * Fixed set values, defined as a range: 0..59 (with an optional increment)
+ *
+ * @package     iTopORM
+ */
+class ValueSetRange extends ValueSetDefinition
+{
+	protected $m_iStart;
+	protected $m_iEnd;
+
+	public function __construct($iStart, $iEnd, $iStep = 1)
+	{
+		$this->m_iStart = $iStart;
+		$this->m_iEnd = $iEnd;
+		$this->m_iStep = $iStep;
+	}
+
+	protected function LoadValues($aArgs)
+	{
+		$iValue = $this->m_iStart;
+		for($iValue = $this->m_iStart; $iValue <= $this->m_iEnd; $iValue += $this->m_iStep)
+		{
+			$this->m_aValues[$iValue] = $iValue;
+		}
+		return true;
+	}
+}
+
 
 /**
  * Data model classes 

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است