diff --git a/core/dbobjectsearch.class.php b/core/dbobjectsearch.class.php index 2a8c2891e..7ab30c58e 100644 --- a/core/dbobjectsearch.class.php +++ b/core/dbobjectsearch.class.php @@ -339,7 +339,7 @@ class DBObjectSearch { foreach($aPointingTo as $iOperatorCode => $aFilter) { - foreach($aFilter as $sAlias => $oExtFilter) + foreach($aFilter as $oExtFilter) { $oExtFilter->RenameParam($sOldName, $sNewName); } @@ -586,7 +586,7 @@ class DBObjectSearch { foreach($aPointingTo as $iOperatorCode => $aFilter) { - foreach($aFilter as $sAlias => $oFilter) + foreach($aFilter as $oFilter) { $oFilter->AddToNameSpace($aClassAliases, $aAliasTranslation); } @@ -602,6 +602,48 @@ class DBObjectSearch } } + + // Browse the tree nodes recursively + // + protected function GetNode($sAlias) + { + if ($this->GetFirstJoinedClassAlias() == $sAlias) + { + return $this; + } + else + { + foreach($this->m_aPointingTo as $sExtKeyAttCode=>$aPointingTo) + { + foreach($aPointingTo as $iOperatorCode => $aFilter) + { + foreach($aFilter as $oFilter) + { + $ret = $oFilter->GetNode($sAlias); + if (is_object($ret)) + { + return $ret; + } + } + } + } + foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences) + { + foreach($aReferences as $sForeignExtKeyAttCode=>$oForeignFilter) + { + $ret = $oForeignFilter->GetNode($sAlias); + if (is_object($ret)) + { + return $ret; + } + } + } + } + // Not found + return null; + } + + public function AddCondition_PointingTo(DBObjectSearch $oFilter, $sExtKeyAttCode, $iOperatorCode = TREE_OPERATOR_EQUALS) { $aAliasTranslation = array(); @@ -626,29 +668,11 @@ class DBObjectSearch throw new CoreException("The specified tree operator $iOperatorCode is not applicable to the key '{$this->GetClass()}::$sExtKeyAttCode', which is not a HierarchicalKey"); } - $bSamePointingTo = false; - if (array_key_exists($sExtKeyAttCode, $this->m_aPointingTo)) - { - if (array_key_exists($iOperatorCode, $this->m_aPointingTo[$sExtKeyAttCode])) - { - if (array_key_exists($oFilter->GetClassAlias(), $this->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode])) - { - $bSamePointingTo = true; - } - } - } + // Find the node on which the new tree must be attached (most of the time it is "this") + $oReceivingFilter = $this->GetNode($this->GetClassAlias()); - if ($bSamePointingTo) - { - // Same ext key, alias and same operator, merge the filters together - $oFilter->AddToNamespace($aClassAliases, $aAliasTranslation, true /* Don't translate the main alias */); - $this->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode][$oFilter->GetClassAlias()] = $oFilter; - } - else - { - $oFilter->AddToNamespace($aClassAliases, $aAliasTranslation); - $this->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode][$oFilter->GetClassAlias()] = $oFilter; - } + $oFilter->AddToNamespace($aClassAliases, $aAliasTranslation); + $oReceivingFilter->m_aPointingTo[$sExtKeyAttCode][$iOperatorCode][] = $oFilter; } public function AddCondition_ReferencedBy(DBObjectSearch $oFilter, $sForeignExtKeyAttCode) @@ -672,9 +696,13 @@ 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()}"); } + + // 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])) { - $this->m_aReferencedBy[$sForeignClass][$sForeignExtKeyAttCode]->MergeWith_InNamespace($oFilter, $aClassAliases, $aAliasTranslation); + $oReceivingFilter->m_aReferencedBy[$sForeignClass][$sForeignExtKeyAttCode]->MergeWith_InNamespace($oFilter, $aClassAliases, $aAliasTranslation); } else { @@ -684,7 +712,7 @@ class DBObjectSearch //$oNewFilter = clone $oFilter; //$oNewFilter->ResetCondition(); - $this->m_aReferencedBy[$sForeignClass][$sForeignExtKeyAttCode]= $oFilter; + $oReceivingFilter->m_aReferencedBy[$sForeignClass][$sForeignExtKeyAttCode]= $oFilter; } } @@ -728,7 +756,7 @@ class DBObjectSearch { foreach($aPointingTo as $iOperatorCode => $aFilter) { - foreach($aFilter as $sAlias => $oExtFilter) + foreach($aFilter as $oExtFilter) { $this->AddCondition_PointingTo_InNamespace($oExtFilter, $sExtKeyAttCode, $aClassAliases, $aAliasTranslation, $iOperatorCode); } @@ -951,7 +979,7 @@ class DBObjectSearch { foreach($aPointingTo as $iOperatorCode => $aFilter) { - foreach($aFilter as $sAlias => $oFilter) + foreach($aFilter as $oFilter) { switch($iOperatorCode) { diff --git a/core/metamodel.class.php b/core/metamodel.class.php index 8d747bfa7..18aea7dab 100644 --- a/core/metamodel.class.php +++ b/core/metamodel.class.php @@ -2096,36 +2096,37 @@ abstract class MetaModel // Check the order by specification, and prefix with the class alias // and make sure that the ordering columns are going to be selected // + $sClass = $oFilter->GetClass(); + $sClassAlias = $oFilter->GetClassAlias(); $aOrderSpec = array(); foreach ($aOrderBy as $sFieldAlias => $bAscending) { if ($sFieldAlias != 'id') { - MyHelpers::CheckValueInArray('field name in ORDER BY spec', $sFieldAlias, self::GetAttributesList($oFilter->GetFirstJoinedClass())); + MyHelpers::CheckValueInArray('field name in ORDER BY spec', $sFieldAlias, self::GetAttributesList($sClass)); } if (!is_bool($bAscending)) { throw new CoreException("Wrong direction in ORDER BY spec, found '$bAscending' and expecting a boolean value"); } - $sFirstClassAlias = $oFilter->GetClassAlias(); - if (self::IsValidAttCode($oFilter->GetClass(), $sFieldAlias)) + if (self::IsValidAttCode($sClass, $sFieldAlias)) { - $oAttDef = self::GetAttributeDef($oFilter->GetClass(), $sFieldAlias); - foreach($oAttDef->GetOrderBySQLExpressions($sFirstClassAlias) as $sSQLExpression) + $oAttDef = self::GetAttributeDef($sClass, $sFieldAlias); + foreach($oAttDef->GetOrderBySQLExpressions($sClassAlias) as $sSQLExpression) { $aOrderSpec[$sSQLExpression] = $bAscending; } } else { - $aOrderSpec['`'.$sFirstClassAlias.$sFieldAlias.'`'] = $bAscending; + $aOrderSpec['`'.$sClassAlias.$sFieldAlias.'`'] = $bAscending; } // Make sure that the columns used for sorting are present in the loaded columns - if (!is_null($aAttToLoad) && !isset($aAttToLoad[$sFirstClassAlias][$sFieldAlias])) + if (!is_null($aAttToLoad) && !isset($aAttToLoad[$sClassAlias][$sFieldAlias])) { - $aAttToLoad[$sFirstClassAlias][$sFieldAlias] = MetaModel::GetAttributeDef($oFilter->GetFirstJoinedClass(), $sFieldAlias); + $aAttToLoad[$sClassAlias][$sFieldAlias] = MetaModel::GetAttributeDef($sClass, $sFieldAlias); } } @@ -2135,7 +2136,7 @@ abstract class MetaModel try { $sRes = $oSelect->RenderSelect($aOrderSpec, $aScalarArgs, $iLimitCount, $iLimitStart, $bGetCount); - if ($oFilter->GetClassAlias() == 'itop') + if ($sClassAlias == 'itop') { echo $sRes."
\n"; } @@ -2827,7 +2828,7 @@ abstract class MetaModel { foreach($aPointingTo as $iOperatorCode => $aFilter) { - foreach($aFilter as $sAlias => $oExtFilter) + 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 diff --git a/test/testlist.inc.php b/test/testlist.inc.php index c62af6e4d..313c5bf6b 100644 --- a/test/testlist.inc.php +++ b/test/testlist.inc.php @@ -1100,7 +1100,7 @@ class TestItopEfficiency extends TestBizModel return 'Measure time to perform the queries'; } - static public function GetConfigFile() {return '/config-itop.php';} + static public function GetConfigFile() {return 'conf/production/config-itop.php';} protected function DoBenchmark($sOqlQuery) { @@ -1225,7 +1225,7 @@ class TestQueries extends TestBizModel return 'Try as many queries as possible'; } - static public function GetConfigFile() {return '/config-itop.php';} + static public function GetConfigFile() {return 'conf/production/config-itop.php';} protected function DoBenchmark($sOqlQuery) { @@ -1321,6 +1321,106 @@ class TestQueries extends TestBizModel } } +/////////////////////////////////////////////////////////////////////////// +// Check programmaticaly built queries +/////////////////////////////////////////////////////////////////////////// + +class TestQueriesByAPI extends TestBizModel +{ + static public function GetName() + { + return 'Itop - queries build programmaticaly'; + } + + static public function GetDescription() + { + return 'Validate the DBObjectSearch API, through a set of complex (though realistic cases)'; + } + + static public function GetConfigFile() {return 'conf/production/config-itop.php';} + + protected function DoExecute() + { + // Note: relying on eval() - after upgrading to PHP 5.3 we can move to closure (aka anonymous functions) + $aQueries = array( + 'Basic (validate the test)' => array( + 'search' => ' +$oSearch = DBObjectSearch::FromOQL("SELECT P FROM Organization AS O JOIN Person AS P ON P.org_id = O.id WHERE org_id = 2"); + ', + 'oql' => 'SELECT P FROM Organization AS O JOIN Person AS P ON P.org_id = O.id WHERE P.org_id = 2' + ), + 'Double constraint' => array( + 'search' => ' +$oSearch = DBObjectSearch::FromOQL("SELECT Contact AS c"); +$sClass = $oSearch->GetClass(); +$sFilterCode = "org_id"; + +$oAttDef = MetaModel::GetAttributeDef($sClass, $sFilterCode); + +if ($oAttDef->IsExternalKey()) +{ + $sHierarchicalKeyCode = MetaModel::IsHierarchicalClass($oAttDef->GetTargetClass()); + + if ($sHierarchicalKeyCode !== false) + { + $oFilter = new DBObjectSearch($oAttDef->GetTargetClass(), "ORGA"); + $oFilter->AddCondition("id", 2); + $oHKFilter = new DBObjectSearch($oAttDef->GetTargetClass(), "ORGA"); + $oHKFilter->AddCondition_PointingTo(clone $oFilter, $sHierarchicalKeyCode, TREE_OPERATOR_BELOW); + + $oSearch->AddCondition_PointingTo(clone $oHKFilter, $sFilterCode); + + $oFilter = new DBObjectSearch($oAttDef->GetTargetClass(), "ORGA"); + $oFilter->AddCondition("id", 2); + $oHKFilter = new DBObjectSearch($oAttDef->GetTargetClass(), "ORGA"); + $oHKFilter->AddCondition_PointingTo(clone $oFilter, $sHierarchicalKeyCode, TREE_OPERATOR_BELOW); + + $oSearch->AddCondition_PointingTo(clone $oHKFilter, $sFilterCode); + } +} + ', + 'oql' => 'SELECT Contact AS C JOIN Organization ???' + ), + 'Simplified issue' => array( + 'search' => ' +$oSearch = DBObjectSearch::FromOQL("SELECT P FROM Organization AS O JOIN Person AS P ON P.org_id = O.id WHERE O.id = 2"); +$oOrgSearch = new DBObjectSearch("Organization", "O2"); +$oOrgSearch->AddCondition("id", 2); +$oSearch->AddCondition_PointingTo($oOrgSearch, "org_id"); + ', + 'oql' => 'SELECT P FROM Organization AS O JOIN Person AS P ON P.org_id = O.id JOIN Organization AS O2 ON P.org_id = O2.id WHERE O.id = 2 AND O2.id = 2' + ), + ); + foreach ($aQueries as $sQueryDesc => $aQuerySpec) + { + echo "

Query $sQueryDesc

\n"; + echo "

Using code: ".highlight_string("', true)."

\n"; + echo "

Expected OQL: ".$aQuerySpec['oql']."

\n"; + + if (isset($oSearch)) + { + unset($oSearch); + } + eval($aQuerySpec['search']); + $sResOQL = $oSearch->ToOQL(); + echo "

Resulting OQL: ".$sResOQL."

\n"; + + echo "
";
+			print_r($oSearch);
+			echo "
"; + + $sSQL = MetaModel::MakeSelectQuery($oSearch); + $res = CMDBSource::Query($sSQL); + foreach (CMDBSource::ExplainQuery($sSQL) as $aRow) + { + } + } + +// throw new UnitTestException("Expecting result '{$aWebService['expected result']}', but got '$res'"); + + } +} + /////////////////////////////////////////////////////////////////////////// // Test bulk load API /////////////////////////////////////////////////////////////////////////// @@ -1337,7 +1437,7 @@ class TestItopBulkLoad extends TestBizModel return 'Execute a bulk change at the Core API level'; } - static public function GetConfigFile() {return '/config-itop.php';} + static public function GetConfigFile() {return 'conf/production/config-itop.php';} protected function DoExecute() @@ -1906,7 +2006,7 @@ class TestDataExchange extends TestBizModel return 'Test REST services: synchro_import and synchro_exec'; } - static public function GetConfigFile() {return '/config-itop.php';} + static public function GetConfigFile() {return 'conf/production/config-itop.php';} protected function DoExecScenario($aSingleScenario) { @@ -2910,7 +3010,7 @@ abstract class TestSoapDirect extends TestBizModel static public function GetName() {return 'Test web services locally';} static public function GetDescription() {return 'Invoke the service directly (troubleshooting)';} - static public function GetConfigFile() {return '/config-itop.php';} + static public function GetConfigFile() {return 'conf/production/config-itop.php';} protected $m_aTestSpecs; @@ -3010,7 +3110,7 @@ class TestTriggerAndEmail extends TestBizModel static public function GetName() {return 'Test trigger and email';} static public function GetDescription() {return 'Create a trigger and an email, then activates the trigger';} - static public function GetConfigFile() {return '/config-itop.php';} + static public function GetConfigFile() {return 'conf/production/config-itop.php';} protected function CreateEmailSpec($oTrigger, $sStatus, $sTo, $sCC, $sTesterEmail) { @@ -3154,7 +3254,7 @@ class TestDBProperties extends TestBizModel return 'Write and read a dummy property'; } - static public function GetConfigFile() {return '/config-itop.php';} + static public function GetConfigFile() {return 'conf/production/config-itop.php';} protected function DoExecute() { @@ -3177,7 +3277,7 @@ class TestCreateObjects extends TestBizModel return 'Create weird objects (reproduce a bug?)'; } - static public function GetConfigFile() {return '/config-itop.php';} + static public function GetConfigFile() {return 'conf/production/config-itop.php';} protected function DoExecute() { @@ -3238,7 +3338,7 @@ class TestSetLinkset extends TestBizModel return 'Create a user account, setting its profile by the mean of a string (prerequisite to CSV import of linksets)'; } - static public function GetConfigFile() {return '/config-itop.php';} + static public function GetConfigFile() {return 'conf/production/config-itop.php';} protected function DoExecute() { @@ -3272,7 +3372,7 @@ class TestEmailAsynchronous extends TestBizModel return 'Queues a request to send an email'; } - static public function GetConfigFile() {return '/config-itop.php';} + static public function GetConfigFile() {return 'conf/production/config-itop.php';} protected function DoExecute() {