N°2272 - OQL performance (testability)

This commit is contained in:
Eric
2019-09-05 09:37:28 +02:00
parent 3fde778c0c
commit aac6ab0fc6
18 changed files with 4335 additions and 861 deletions

View File

@@ -50,6 +50,14 @@
"classmap": [
"core",
"application"
],
"exclude-from-classmap": [
"core/dbobjectsearch.class.php",
"core/legacy/dbobjectsearchlegacy.class.php",
"core/querybuildercontext.class.inc.php",
"core/legacy/querybuildercontextlegacy.class.inc.php",
"core/querybuilderexpressions.class.inc.php",
"core/legacy/querybuilderexpressionslegacy.class.inc.php"
]
},
"conflict": {

View File

@@ -59,7 +59,6 @@ require_once('sqlobjectquery.class.inc.php');
require_once('sqlunionquery.class.inc.php');
require_once('dbobject.class.php');
require_once('dbsearch.class.php');
require_once('dbobjectset.class.php');
require_once('backgroundprocess.inc.php');

View File

@@ -1168,6 +1168,30 @@ class Config
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'use_legacy_dbsearch' => array(
'type' => 'bool',
'description' => 'If set, DBSearch will use legacy SQL query generation',
'default' => false,
'value' => false,
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'query_cache_enabled' => array(
'type' => 'bool',
'description' => 'If set, DBSearch will use cache for query generation',
'default' => true,
'value' => true,
'source_of_value' => '',
'show_in_conf_sample' => false,
),
'log_kpi_record_oql' => array(
'type' => 'integer',
'description' => '1 => Record OQL requests and parameters',
'default' => 0,
'value' => 0,
'source_of_value' => '',
'show_in_conf_sample' => false,
),
);
public function IsProperty($sPropCode)
@@ -1225,7 +1249,6 @@ class Config
}
$this->m_aSettings[$sPropCode]['value'] = $value;
$this->m_aSettings[$sPropCode]['source_of_value'] = $sSourceDesc;
}
/**
@@ -1822,7 +1845,6 @@ class Config
'log_notification' => $this->m_bLogNotification,
'log_issue' => $this->m_bLogIssue,
'log_web_service' => $this->m_bLogWebService,
'query_cache_enabled' => $this->m_bQueryCacheEnabled,
'secure_connection_required' => $this->m_bSecureConnectionRequired,
);
foreach ($aBoolValues as $sKey => $bValue)

View File

@@ -1096,7 +1096,12 @@ class DBObjectSearch extends DBSearch
if (!array_key_exists($sKeyAttCode, $this->m_aPointingTo)) return array();
return $this->m_aPointingTo[$sKeyAttCode];
}
protected function GetCriteria_ReferencedBy()
/**
* @internal
* @return array
*/
public function GetCriteria_ReferencedBy()
{
return $this->m_aReferencedBy;
}
@@ -1528,36 +1533,30 @@ class DBObjectSearch extends DBSearch
public function MakeDeleteQuery($aArgs = array())
{
$aModifierProperties = MetaModel::MakeModifierProperties($this);
$oBuild = new QueryBuilderContext($this, $aModifierProperties);
$oSQLQuery = $this->MakeSQLObjectQuery($oBuild, array($this->GetClassAlias() => array()), array());
$oSQLQuery->SetCondition($oBuild->m_oQBExpressions->GetCondition());
$oSQLQuery->SetSelect($oBuild->m_oQBExpressions->GetSelect());
$oSQLQuery->OptimizeJoins(array());
$oSQLObjectQueryBuilder = new SQLObjectQueryBuilder($this, self::$m_bOptimizeQueries, self::$m_bDebugQuery);
$oSQLQuery = $oSQLObjectQueryBuilder->MakeSQLObjectDeleteQuery();
$aScalarArgs = MetaModel::PrepareQueryArguments($aArgs, $this->GetInternalParams());
$sRet = $oSQLQuery->RenderDelete($aScalarArgs);
return $sRet;
}
/**
* @param array $aValues is an array of $sAttCode => $value
* @param array $aArgs
*
* @return string
* @throws \CoreException
*/
public function MakeUpdateQuery($aValues, $aArgs = array())
{
// $aValues is an array of $sAttCode => $value
$aModifierProperties = MetaModel::MakeModifierProperties($this);
$oBuild = new QueryBuilderContext($this, $aModifierProperties);
$aRequested = array(); // Requested attributes are the updated attributes
foreach ($aValues as $sAttCode => $value)
{
$aRequested[$sAttCode] = MetaModel::GetAttributeDef($this->GetClass(), $sAttCode);
}
$oSQLQuery = $this->MakeSQLObjectQuery($oBuild, array($this->GetClassAlias() => $aRequested), $aValues);
$oSQLQuery->SetCondition($oBuild->m_oQBExpressions->GetCondition());
$oSQLQuery->SetSelect($oBuild->m_oQBExpressions->GetSelect());
$oSQLQuery->OptimizeJoins(array());
$oSQLObjectQueryBuilder = new SQLObjectQueryBuilder($this, self::$m_bOptimizeQueries, self::$m_bDebugQuery);
$oSQLQuery = $oSQLObjectQueryBuilder->MakeSQLObjectUpdateQuery($aValues);
$aScalarArgs = MetaModel::PrepareQueryArguments($aArgs, $this->GetInternalParams());
$sRet = $oSQLQuery->RenderUpdate($aScalarArgs);
return $sRet;
}
// ENTRY POINT API à conserver
public function GetSQLQueryStructure($aAttToLoad, $bGetCount, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null)
{
// Hide objects that are not visible to the current user
@@ -1709,7 +1708,8 @@ class DBObjectSearch extends DBSearch
if (!isset($oSQLQuery))
{
$oKPI = new ExecutionKPI();
$oSQLQuery = $oSearch->BuildSQLQueryStruct($aAttToLoad, $bGetCount, $aModifierProperties, $aGroupByExpr, $aSelectedClasses, $aSelectExpr);
$oSQLObjectQueryBuilder = new SQLObjectQueryBuilder($oSearch, self::$m_bOptimizeQueries, self::$m_bDebugQuery);
$oSQLQuery = $oSQLObjectQueryBuilder->BuildSQLQueryStruct($aAttToLoad, $bGetCount, $aModifierProperties, $aGroupByExpr, $aSelectedClasses, $aSelectExpr);
$oKPI->ComputeStats('BuildSQLQueryStruct', $sOqlQuery);
if (self::$m_bQueryCacheEnabled)
@@ -1728,649 +1728,6 @@ class DBObjectSearch extends DBSearch
return $oSQLQuery;
}
/**
* @param array $aAttToLoad
* @param bool $bGetCount
* @param array $aModifierProperties
* @param array $aGroupByExpr
* @param array $aSelectedClasses
* @param array $aSelectExpr
*
* @return null|SQLObjectQuery
* @throws \CoreException
*/
protected function BuildSQLQueryStruct($aAttToLoad, $bGetCount, $aModifierProperties, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null)
{
$oBuild = new QueryBuilderContext($this, $aModifierProperties, $aGroupByExpr, $aSelectedClasses, $aSelectExpr);
$oSQLQuery = $this->MakeSQLObjectQuery($oBuild, $aAttToLoad, array());
$oSQLQuery->SetCondition($oBuild->m_oQBExpressions->GetCondition());
if (is_array($aGroupByExpr))
{
$aCols = $oBuild->m_oQBExpressions->GetGroupBy();
$oSQLQuery->SetGroupBy($aCols);
$oSQLQuery->SetSelect($aCols);
}
else
{
$oSQLQuery->SetSelect($oBuild->m_oQBExpressions->GetSelect());
}
if ($aSelectExpr)
{
// Get the fields corresponding to the select expressions
foreach($oBuild->m_oQBExpressions->GetSelect() as $sAlias => $oExpr)
{
if (key_exists($sAlias, $aSelectExpr))
{
$oSQLQuery->AddSelect($sAlias, $oExpr);
}
}
}
$aMandatoryTables = null;
if (self::$m_bOptimizeQueries)
{
if ($bGetCount)
{
// Simplify the query if just getting the count
$oSQLQuery->SetSelect(array());
}
$oBuild->m_oQBExpressions->GetMandatoryTables($aMandatoryTables);
$oSQLQuery->OptimizeJoins($aMandatoryTables);
}
// Filter tables as late as possible: do not interfere with the optimization process
foreach ($oBuild->GetFilteredTables() as $sTableAlias => $aConditions)
{
if ($aMandatoryTables && array_key_exists($sTableAlias, $aMandatoryTables))
{
foreach ($aConditions as $oCondition)
{
$oSQLQuery->AddCondition($oCondition);
}
}
}
return $oSQLQuery;
}
/**
* @param $oBuild
* @param null $aAttToLoad
* @param array $aValues
* @return null|SQLObjectQuery
* @throws \CoreException
*/
protected function MakeSQLObjectQuery(&$oBuild, $aAttToLoad = null, $aValues = array())
{
// Note: query class might be different than the class of the filter
// -> this occurs when we are linking our class to an external class (referenced by, or pointing to)
$sClass = $this->GetFirstJoinedClass();
$sClassAlias = $this->GetFirstJoinedClassAlias();
$bIsOnQueriedClass = array_key_exists($sClassAlias, $oBuild->GetRootFilter()->GetSelectedClasses());
//self::DbgTrace("Entering: ".$this->ToOQL().", ".($bIsOnQueriedClass ? "MAIN" : "SECONDARY"));
//$sRootClass = MetaModel::GetRootClass($sClass);
$sKeyField = MetaModel::DBGetKey($sClass);
if ($bIsOnQueriedClass)
{
// default to the whole list of attributes + the very std id/finalclass
$oBuild->m_oQBExpressions->AddSelect($sClassAlias.'id', new FieldExpression('id', $sClassAlias));
if (is_null($aAttToLoad) || !array_key_exists($sClassAlias, $aAttToLoad))
{
$sSelectedClass = $oBuild->GetSelectedClass($sClassAlias);
$aAttList = MetaModel::ListAttributeDefs($sSelectedClass);
}
else
{
$aAttList = $aAttToLoad[$sClassAlias];
}
foreach ($aAttList as $sAttCode => $oAttDef)
{
if (!$oAttDef->IsScalar()) continue;
// keep because it can be used for sorting - if (!$oAttDef->LoadInObject()) continue;
if ($oAttDef->IsBasedOnOQLExpression())
{
$oBuild->m_oQBExpressions->AddSelect($sClassAlias.$sAttCode, new FieldExpression($sAttCode, $sClassAlias));
}
else
{
foreach ($oAttDef->GetSQLExpressions() as $sColId => $sSQLExpr)
{
$oBuild->m_oQBExpressions->AddSelect($sClassAlias.$sAttCode.$sColId, new FieldExpression($sAttCode.$sColId, $sClassAlias));
}
}
}
}
//echo "<p>oQBExpr ".__LINE__.": <pre>\n".print_r($oBuild->m_oQBExpressions, true)."</pre></p>\n";
$aExpectedAtts = array(); // array of (attcode => fieldexpression)
//echo "<p>".__LINE__.": GetUnresolvedFields($sClassAlias, ...)</p>\n";
$oBuild->m_oQBExpressions->GetUnresolvedFields($sClassAlias, $aExpectedAtts);
// Compute a clear view of required joins (from the current class)
// Build the list of external keys:
// -> ext keys required by an explicit join
// -> ext keys mentionned in a 'pointing to' condition
// -> ext keys required for an external field
// -> ext keys required for a friendly name
//
$aExtKeys = array(); // array of sTableClass => array of (sAttCode (keys) => array of (sAttCode (fields)=> oAttDef))
//
// Optimization: could be partially computed once for all (cached) ?
//
if ($bIsOnQueriedClass)
{
// Get all Ext keys for the queried class (??)
foreach(MetaModel::GetKeysList($sClass) as $sKeyAttCode)
{
$sKeyTableClass = MetaModel::GetAttributeOrigin($sClass, $sKeyAttCode);
$aExtKeys[$sKeyTableClass][$sKeyAttCode] = array();
}
}
// Get all Ext keys used by the filter
foreach ($this->GetCriteria_PointingTo() as $sKeyAttCode => $aPointingTo)
{
if (array_key_exists(TREE_OPERATOR_EQUALS, $aPointingTo))
{
$sKeyTableClass = MetaModel::GetAttributeOrigin($sClass, $sKeyAttCode);
$aExtKeys[$sKeyTableClass][$sKeyAttCode] = array();
}
}
$aFNJoinAlias = array(); // array of (subclass => alias)
foreach ($aExpectedAtts as $sExpectedAttCode => $oExpression)
{
if (!MetaModel::IsValidAttCode($sClass, $sExpectedAttCode)) continue;
$oAttDef = MetaModel::GetAttributeDef($sClass, $sExpectedAttCode);
if ($oAttDef->IsBasedOnOQLExpression())
{
// To optimize: detect a restriction on child classes in the condition expression
// e.g. SELECT FunctionalCI WHERE finalclass IN ('Server', 'VirtualMachine')
$oExpression = static::GetPolymorphicExpression($sClass, $sExpectedAttCode);
$aRequiredFields = array();
$oExpression->GetUnresolvedFields('', $aRequiredFields);
$aTranslateFields = array();
foreach($aRequiredFields as $sSubClass => $aFields)
{
foreach($aFields as $sAttCode => $oField)
{
$oAttDef = MetaModel::GetAttributeDef($sSubClass, $sAttCode);
if ($oAttDef->IsExternalKey())
{
$sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sAttCode);
$aExtKeys[$sClassOfAttribute][$sAttCode] = array();
}
elseif ($oAttDef->IsExternalField())
{
$sKeyAttCode = $oAttDef->GetKeyAttCode();
$sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sKeyAttCode);
$aExtKeys[$sClassOfAttribute][$sKeyAttCode][$sAttCode] = $oAttDef;
}
else
{
$sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sAttCode);
}
if (MetaModel::IsParentClass($sClassOfAttribute, $sClass))
{
// The attribute is part of the standard query
//
$sAliasForAttribute = $sClassAlias;
}
else
{
// The attribute will be available from an additional outer join
// For each subclass (table) one single join is enough
//
if (!array_key_exists($sClassOfAttribute, $aFNJoinAlias))
{
$sAliasForAttribute = $oBuild->GenerateClassAlias($sClassAlias.'_fn_'.$sClassOfAttribute, $sClassOfAttribute);
$aFNJoinAlias[$sClassOfAttribute] = $sAliasForAttribute;
}
else
{
$sAliasForAttribute = $aFNJoinAlias[$sClassOfAttribute];
}
}
$aTranslateFields[$sSubClass][$sAttCode] = new FieldExpression($sAttCode, $sAliasForAttribute);
}
}
$oExpression = $oExpression->Translate($aTranslateFields, false);
$aTranslateNow = array();
$aTranslateNow[$sClassAlias][$sExpectedAttCode] = $oExpression;
$oBuild->m_oQBExpressions->Translate($aTranslateNow, false);
}
}
// Add the ext fields used in the select (eventually adds an external key)
foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode=>$oAttDef)
{
if ($oAttDef->IsExternalField())
{
if (array_key_exists($sAttCode, $aExpectedAtts))
{
// Add the external attribute
$sKeyAttCode = $oAttDef->GetKeyAttCode();
$sKeyTableClass = MetaModel::GetAttributeOrigin($sClass, $sKeyAttCode);
$aExtKeys[$sKeyTableClass][$sKeyAttCode][$sAttCode] = $oAttDef;
}
}
}
$bRootFirst = MetaModel::GetConfig()->Get('optimize_requests_for_join_count');
if ($bRootFirst)
{
// First query built from the root, adding all tables including the leaf
// Before N.1065 we were joining from the leaf first, but this wasn't a good choice :
// most of the time (obsolescence, friendlyname, ...) we want to get a root attribute !
//
$oSelectBase = null;
$aClassHierarchy = MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL, true);
$bIsClassStandaloneClass = (count($aClassHierarchy) == 1);
foreach($aClassHierarchy as $sSomeClass)
{
if (!MetaModel::HasTable($sSomeClass))
{
continue;
}
self::DbgTrace("Adding join from root to leaf: $sSomeClass... let's call MakeSQLObjectQuerySingleTable()");
$oSelectParentTable = $this->MakeSQLObjectQuerySingleTable($oBuild, $aAttToLoad, $sSomeClass, $aExtKeys, $aValues);
if (is_null($oSelectBase))
{
$oSelectBase = $oSelectParentTable;
if (!$bIsClassStandaloneClass && (MetaModel::IsRootClass($sSomeClass)))
{
// As we're linking to root class first, we're adding a where clause on the finalClass attribute :
// COALESCE($sRootClassFinalClass IN ('$sExpectedClasses'), 1)
// If we don't, the child classes can be removed in the query optimisation phase, including the leaf that was queried
// So we still need to filter records to only those corresponding to the child classes !
// The coalesce is mandatory if we have a polymorphic query (left join)
$oClassListExpr = ListExpression::FromScalars(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL));
$sFinalClassSqlColumnName = MetaModel::DBGetClassField($sSomeClass);
$oClassExpr = new FieldExpression($sFinalClassSqlColumnName, $oSelectBase->GetTableAlias());
$oInExpression = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr);
$oTrueExpression = new TrueExpression();
$aCoalesceAttr = array($oInExpression, $oTrueExpression);
$oFinalClassRestriction = new FunctionExpression("COALESCE", $aCoalesceAttr);
$oBuild->m_oQBExpressions->AddCondition($oFinalClassRestriction);
}
}
else
{
$oSelectBase->AddInnerJoin($oSelectParentTable, $sKeyField, MetaModel::DBGetKey($sSomeClass));
}
}
}
else
{
// First query built upon on the leaf (ie current) class
//
self::DbgTrace("Main (=leaf) class, call MakeSQLObjectQuerySingleTable()");
if (MetaModel::HasTable($sClass))
{
$oSelectBase = $this->MakeSQLObjectQuerySingleTable($oBuild, $aAttToLoad, $sClass, $aExtKeys, $aValues);
}
else
{
$oSelectBase = null;
// As the join will not filter on the expected classes, we have to specify it explicitely
$sExpectedClasses = implode("', '", MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL));
$oFinalClassRestriction = Expression::FromOQL("`$sClassAlias`.finalclass IN ('$sExpectedClasses')");
$oBuild->m_oQBExpressions->AddCondition($oFinalClassRestriction);
}
// Then we join the queries of the eventual parent classes (compound model)
foreach(MetaModel::EnumParentClasses($sClass) as $sParentClass)
{
if (!MetaModel::HasTable($sParentClass)) continue;
self::DbgTrace("Parent class: $sParentClass... let's call MakeSQLObjectQuerySingleTable()");
$oSelectParentTable = $this->MakeSQLObjectQuerySingleTable($oBuild, $aAttToLoad, $sParentClass, $aExtKeys, $aValues);
if (is_null($oSelectBase))
{
$oSelectBase = $oSelectParentTable;
}
else
{
$oSelectBase->AddInnerJoin($oSelectParentTable, $sKeyField, MetaModel::DBGetKey($sParentClass));
}
}
}
// Filter on objects referencing me
//
foreach($this->m_aReferencedBy as $sForeignClass=>$aReferences)
{
foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator)
{
foreach ($aFiltersByOperator as $iOperatorCode => $aFilters)
{
foreach ($aFilters as $oForeignFilter)
{
$oForeignKeyAttDef = MetaModel::GetAttributeDef($sForeignClass, $sForeignExtKeyAttCode);
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()));
$sForeignClassAlias = $oForeignFilter->GetFirstJoinedClassAlias();
$oBuild->m_oQBExpressions->PushJoinField(new FieldExpression($sForeignExtKeyAttCode, $sForeignClassAlias));
if ($oForeignKeyAttDef instanceof AttributeObjectKey)
{
$sClassAttCode = $oForeignKeyAttDef->Get('class_attcode');
// Add the condition: `$sForeignClassAlias`.$sClassAttCode IN (subclasses of $sClass')
$oClassListExpr = ListExpression::FromScalars(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL));
$oClassExpr = new FieldExpression($sClassAttCode, $sForeignClassAlias);
$oClassRestriction = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr);
$oBuild->m_oQBExpressions->AddCondition($oClassRestriction);
}
$oSelectForeign = $oForeignFilter->MakeSQLObjectQuery($oBuild, $aAttToLoad);
$oJoinExpr = $oBuild->m_oQBExpressions->PopJoinField();
$sForeignKeyTable = $oJoinExpr->GetParent();
$sForeignKeyColumn = $oJoinExpr->GetName();
if ($iOperatorCode == TREE_OPERATOR_EQUALS)
{
$oSelectBase->AddInnerJoin($oSelectForeign, $sKeyField, $sForeignKeyColumn, $sForeignKeyTable);
}
else
{
// Hierarchical key
$KeyLeft = $oForeignKeyAttDef->GetSQLLeft();
$KeyRight = $oForeignKeyAttDef->GetSQLRight();
$oSelectBase->AddInnerJoinTree($oSelectForeign, $KeyLeft, $KeyRight, $KeyLeft, $KeyRight, $sForeignKeyTable, $iOperatorCode, true);
}
}
}
}
}
// Additional JOINS for Friendly names
//
foreach ($aFNJoinAlias as $sSubClass => $sSubClassAlias)
{
$oSubClassFilter = new DBObjectSearch($sSubClass, $sSubClassAlias);
$oSelectFN = $oSubClassFilter->MakeSQLObjectQuerySingleTable($oBuild, $aAttToLoad, $sSubClass, $aExtKeys, array());
$oSelectBase->AddLeftJoin($oSelectFN, $sKeyField, MetaModel::DBGetKey($sSubClass));
}
// That's all... cross fingers and we'll get some working query
//MyHelpers::var_dump_html($oSelectBase, true);
//MyHelpers::var_dump_html($oSelectBase->RenderSelect(), true);
if (self::$m_bDebugQuery) $oSelectBase->DisplayHtml();
return $oSelectBase;
}
protected function MakeSQLObjectQuerySingleTable(&$oBuild, $aAttToLoad, $sTableClass, $aExtKeys, $aValues)
{
// $aExtKeys is an array of sTableClass => array of (sAttCode (keys) => array of sAttCode (fields))
// Prepare the query for a single table (compound objects)
// Ignores the items (attributes/filters) that are not on the target table
// Perform an (inner or left) join for every external key (and specify the expected fields)
//
// Returns an SQLQuery
//
$sTargetClass = $this->GetFirstJoinedClass();
$sTargetAlias = $this->GetFirstJoinedClassAlias();
$sTable = MetaModel::DBGetTable($sTableClass);
$sTableAlias = $oBuild->GenerateTableAlias($sTargetAlias.'_'.$sTable, $sTable);
$aTranslation = array();
$aExpectedAtts = array();
$oBuild->m_oQBExpressions->GetUnresolvedFields($sTargetAlias, $aExpectedAtts);
$bIsOnQueriedClass = array_key_exists($sTargetAlias, $oBuild->GetRootFilter()->GetSelectedClasses());
self::DbgTrace("Entering: tableclass=$sTableClass, filter=".$this->ToOQL().", ".($bIsOnQueriedClass ? "MAIN" : "SECONDARY"));
// 1 - SELECT and UPDATE
//
// Note: no need for any values nor fields for foreign Classes (ie not the queried Class)
//
$aUpdateValues = array();
// 1/a - Get the key and friendly name
//
// We need one pkey to be the key, let's take the first one available
$oSelectedIdField = null;
$oIdField = new FieldExpressionResolved(MetaModel::DBGetKey($sTableClass), $sTableAlias);
$aTranslation[$sTargetAlias]['id'] = $oIdField;
if ($bIsOnQueriedClass)
{
// Add this field to the list of queried fields (required for the COUNT to work fine)
$oSelectedIdField = $oIdField;
}
// 1/b - Get the other attributes
//
foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef)
{
// Skip this attribute if not defined in this table
if (MetaModel::GetAttributeOrigin($sTargetClass, $sAttCode) != $sTableClass) continue;
// Skip this attribute if not made of SQL columns
if (count($oAttDef->GetSQLExpressions()) == 0) continue;
// Update...
//
if ($bIsOnQueriedClass && array_key_exists($sAttCode, $aValues))
{
assert ($oAttDef->IsBasedOnDBColumns());
foreach ($oAttDef->GetSQLValues($aValues[$sAttCode]) as $sColumn => $sValue)
{
$aUpdateValues[$sColumn] = $sValue;
}
}
}
// 2 - The SQL query, for this table only
//
$oSelectBase = new SQLObjectQuery($sTable, $sTableAlias, array(), $bIsOnQueriedClass, $aUpdateValues, $oSelectedIdField);
// 3 - Resolve expected expressions (translation table: alias.attcode => table.column)
//
foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef)
{
// Skip this attribute if not defined in this table
if (MetaModel::GetAttributeOrigin($sTargetClass, $sAttCode) != $sTableClass) continue;
// Select...
//
if ($oAttDef->IsExternalField())
{
// skip, this will be handled in the joined tables (done hereabove)
}
else
{
// standard field, or external key
// add it to the output
foreach ($oAttDef->GetSQLExpressions() as $sColId => $sSQLExpr)
{
if (array_key_exists($sAttCode.$sColId, $aExpectedAtts))
{
$oFieldSQLExp = new FieldExpressionResolved($sSQLExpr, $sTableAlias);
foreach (MetaModel::EnumPlugins('iQueryModifier') as $sPluginClass => $oQueryModifier)
{
$oFieldSQLExp = $oQueryModifier->GetFieldExpression($oBuild, $sTargetClass, $sAttCode, $sColId, $oFieldSQLExp, $oSelectBase);
}
$aTranslation[$sTargetAlias][$sAttCode.$sColId] = $oFieldSQLExp;
}
}
}
}
// 4 - The external keys -> joins...
//
$aAllPointingTo = $this->GetCriteria_PointingTo();
if (array_key_exists($sTableClass, $aExtKeys))
{
foreach ($aExtKeys[$sTableClass] as $sKeyAttCode => $aExtFields)
{
$oKeyAttDef = MetaModel::GetAttributeDef($sTableClass, $sKeyAttCode);
$aPointingTo = $this->GetCriteria_PointingTo($sKeyAttCode);
if (!array_key_exists(TREE_OPERATOR_EQUALS, $aPointingTo))
{
// The join was not explicitely defined in the filter,
// we need to do it now
$sKeyClass = $oKeyAttDef->GetTargetClass();
$sKeyClassAlias = $oBuild->GenerateClassAlias($sKeyClass.'_'.$sKeyAttCode, $sKeyClass);
$oExtFilter = new DBObjectSearch($sKeyClass, $sKeyClassAlias);
$aAllPointingTo[$sKeyAttCode][TREE_OPERATOR_EQUALS][$sKeyClassAlias] = $oExtFilter;
}
}
}
foreach ($aAllPointingTo as $sKeyAttCode => $aPointingTo)
{
foreach($aPointingTo as $iOperatorCode => $aFilter)
{
foreach($aFilter as $oExtFilter)
{
if (!MetaModel::IsValidAttCode($sTableClass, $sKeyAttCode)) continue; // Not defined in the class, skip it
// The aliases should not conflict because normalization occured while building the filter
$oKeyAttDef = MetaModel::GetAttributeDef($sTableClass, $sKeyAttCode);
$sKeyClass = $oExtFilter->GetFirstJoinedClass();
$sKeyClassAlias = $oExtFilter->GetFirstJoinedClassAlias();
// Note: there is no search condition in $oExtFilter, because normalization did merge the condition onto the top of the filter tree
if ($iOperatorCode == TREE_OPERATOR_EQUALS)
{
if (array_key_exists($sTableClass, $aExtKeys) && array_key_exists($sKeyAttCode, $aExtKeys[$sTableClass]))
{
// Specify expected attributes for the target class query
// ... and use the current alias !
$aTranslateNow = array(); // Translation for external fields - must be performed before the join is done (recursion...)
foreach($aExtKeys[$sTableClass][$sKeyAttCode] as $sAttCode => $oAtt)
{
$oExtAttDef = $oAtt->GetExtAttDef();
if ($oExtAttDef->IsBasedOnOQLExpression())
{
$aTranslateNow[$sTargetAlias][$sAttCode] = new FieldExpression($oExtAttDef->GetCode(), $sKeyClassAlias);
}
else
{
$sExtAttCode = $oAtt->GetExtAttCode();
// Translate mainclass.extfield => remoteclassalias.remotefieldcode
$oRemoteAttDef = MetaModel::GetAttributeDef($sKeyClass, $sExtAttCode);
foreach ($oRemoteAttDef->GetSQLExpressions() as $sColId => $sRemoteAttExpr)
{
$aTranslateNow[$sTargetAlias][$sAttCode.$sColId] = new FieldExpression($sExtAttCode, $sKeyClassAlias);
}
}
}
if ($oKeyAttDef instanceof AttributeObjectKey)
{
// Add the condition: `$sTargetAlias`.$sClassAttCode IN (subclasses of $sKeyClass')
$sClassAttCode = $oKeyAttDef->Get('class_attcode');
$oClassAttDef = MetaModel::GetAttributeDef($sTargetClass, $sClassAttCode);
foreach ($oClassAttDef->GetSQLExpressions() as $sColId => $sSQLExpr)
{
$aTranslateNow[$sTargetAlias][$sClassAttCode.$sColId] = new FieldExpressionResolved($sSQLExpr, $sTableAlias);
}
$oClassListExpr = ListExpression::FromScalars(MetaModel::EnumChildClasses($sKeyClass, ENUM_CHILD_CLASSES_ALL));
$oClassExpr = new FieldExpression($sClassAttCode, $sTargetAlias);
$oClassRestriction = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr);
$oBuild->m_oQBExpressions->AddCondition($oClassRestriction);
}
// Translate prior to recursing
//
$oBuild->m_oQBExpressions->Translate($aTranslateNow, false);
self::DbgTrace("External key $sKeyAttCode (class: $sKeyClass), call MakeSQLObjectQuery()");
$oBuild->m_oQBExpressions->PushJoinField(new FieldExpression('id', $sKeyClassAlias));
$oSelectExtKey = $oExtFilter->MakeSQLObjectQuery($oBuild, $aAttToLoad);
$oJoinExpr = $oBuild->m_oQBExpressions->PopJoinField();
$sExternalKeyTable = $oJoinExpr->GetParent();
$sExternalKeyField = $oJoinExpr->GetName();
$aCols = $oKeyAttDef->GetSQLExpressions(); // Workaround a PHP bug: sometimes issuing a Notice if invoking current(somefunc())
$sLocalKeyField = current($aCols); // get the first column for an external key
self::DbgTrace("External key $sKeyAttCode, Join on $sLocalKeyField = $sExternalKeyField");
if ($oKeyAttDef->IsNullAllowed())
{
$oSelectBase->AddLeftJoin($oSelectExtKey, $sLocalKeyField, $sExternalKeyField);
}
else
{
$oSelectBase->AddInnerJoin($oSelectExtKey, $sLocalKeyField, $sExternalKeyField, $sExternalKeyTable);
}
}
}
elseif(MetaModel::GetAttributeOrigin($sKeyClass, $sKeyAttCode) == $sTableClass)
{
$oBuild->m_oQBExpressions->PushJoinField(new FieldExpression($sKeyAttCode, $sKeyClassAlias));
$oSelectExtKey = $oExtFilter->MakeSQLObjectQuery($oBuild, $aAttToLoad);
$oJoinExpr = $oBuild->m_oQBExpressions->PopJoinField();
$sExternalKeyTable = $oJoinExpr->GetParent();
$sExternalKeyField = $oJoinExpr->GetName();
$sLeftIndex = $sExternalKeyField.'_left'; // TODO use GetSQLLeft()
$sRightIndex = $sExternalKeyField.'_right'; // TODO use GetSQLRight()
$LocalKeyLeft = $oKeyAttDef->GetSQLLeft();
$LocalKeyRight = $oKeyAttDef->GetSQLRight();
$oSelectBase->AddInnerJoinTree($oSelectExtKey, $LocalKeyLeft, $LocalKeyRight, $sLeftIndex, $sRightIndex, $sExternalKeyTable, $iOperatorCode);
}
}
}
}
// Translate the selected columns
//
$oBuild->m_oQBExpressions->Translate($aTranslation, false);
// Filter out archived records
//
if (MetaModel::IsArchivable($sTableClass))
{
if (!$oBuild->GetRootFilter()->GetArchiveMode())
{
$bIsOnJoinedClass = array_key_exists($sTargetAlias, $oBuild->GetRootFilter()->GetJoinedClasses());
if ($bIsOnJoinedClass)
{
if (MetaModel::IsParentClass($sTableClass, $sTargetClass))
{
$oNotArchived = new BinaryExpression(new FieldExpressionResolved('archive_flag', $sTableAlias), '=', new ScalarExpression(0));
$oBuild->AddFilteredTable($sTableAlias, $oNotArchived);
}
}
}
}
return $oSelectBase;
}
/**
* Get the expression for the class and its subclasses (if finalclass = 'subclass' ...)
* Simplifies the final expression by grouping classes having the same expression

View File

@@ -17,8 +17,22 @@
// along with iTop. If not, see <http://www.gnu.org/licenses/>
require_once('dbobjectsearch.class.php');
require_once('dbunionsearch.class.php');
$bUseLegacyDBSearch = utils::GetConfig()->Get('use_legacy_dbsearch');
if ($bUseLegacyDBSearch)
{
// excluded from autoload
require_once (APPROOT.'core/legacy/querybuilderexpressionslegacy.class.inc.php');
require_once (APPROOT.'core/legacy/querybuildercontextlegacy.class.inc.php');
require_once(APPROOT.'core/legacy/dbobjectsearchlegacy.class.php');
}
else
{
// excluded from autoload
require_once (APPROOT.'core/querybuilderexpressions.class.inc.php');
require_once (APPROOT.'core/querybuildercontext.class.inc.php');
require_once(APPROOT.'core/dbobjectsearch.class.php');
}
/**
* An object search
@@ -1037,7 +1051,7 @@ abstract class DBSearch
$e->addInfo('OQL', $this->ToOQL());
throw $e;
}
$this->AddQueryTraceSelect($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount, $sRes);
$this->AddQueryTraceSelect($oSQLQuery->GetSourceOQL(), $aOrderBy, $aScalarArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount, $sRes);
return $sRes;
}
@@ -1240,9 +1254,9 @@ abstract class DBSearch
*
* @throws MySQLException
*/
protected function AddQueryTraceSelect($aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount, $sSql)
protected function AddQueryTraceSelect($sOql, $aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart, $bGetCount, $sSql)
{
if (self::$m_bTraceQueries)
if (self::$m_bTraceQueries || (utils::GetConfig()->Get('log_kpi_record_oql') == 1))
{
$aQueryData = array(
'type' => 'select',
@@ -1255,8 +1269,25 @@ abstract class DBSearch
'limit_start' => $iLimitStart,
'is_count' => $bGetCount
);
$sOql = $this->ToOQL(true, $aArgs);
self::AddQueryTrace($aQueryData, $sOql, $sSql);
if (self::$m_bTraceQueries)
{
self::AddQueryTrace($aQueryData, $sOql, $sSql);
}
if (utils::GetConfig()->Get('log_kpi_record_oql') == 1)
{
$aQueryData['oql'] = $sOql;
unset($aQueryData['filter']);
$hLogFile = @fopen(APPROOT.'log/oql_records.txt', 'a');
if ($hLogFile !== false)
{
flock($hLogFile, LOCK_EX);
fwrite($hLogFile, serialize($aQueryData)."\n");
fflush($hLogFile);
flock($hLogFile, LOCK_UN);
fclose($hLogFile);
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,107 @@
<?php
// Copyright (C) 2010-2017 Combodo SARL
//
// This file is part of iTop.
//
// iTop is free software; you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// iTop is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
/**
* Associated with the metamodel -> MakeQuery/MakeQuerySingleTable
*
* @copyright Copyright (C) 2010-2017 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
class QueryBuilderContext
{
protected $m_oRootFilter;
protected $m_aClassAliases;
protected $m_aTableAliases;
protected $m_aModifierProperties;
protected $m_aSelectedClasses;
protected $m_aFilteredTables;
public $m_oQBExpressions;
public function __construct($oFilter, $aModifierProperties, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null)
{
$this->m_oRootFilter = $oFilter;
$this->m_oQBExpressions = new QueryBuilderExpressions($oFilter, $aGroupByExpr, $aSelectExpr);
$this->m_aClassAliases = $oFilter->GetJoinedClasses();
$this->m_aTableAliases = array();
$this->m_aFilteredTables = array();
$this->m_aModifierProperties = $aModifierProperties;
if (is_null($aSelectedClasses))
{
$this->m_aSelectedClasses = $oFilter->GetSelectedClasses();
}
else
{
// For the unions, the selected classes can be upper in the hierarchy (lowest common ancestor)
$this->m_aSelectedClasses = $aSelectedClasses;
}
}
public function GetRootFilter()
{
return $this->m_oRootFilter;
}
public function GenerateTableAlias($sNewName, $sRealName)
{
return MetaModel::GenerateUniqueAlias($this->m_aTableAliases, $sNewName, $sRealName);
}
public function GenerateClassAlias($sNewName, $sRealName)
{
return MetaModel::GenerateUniqueAlias($this->m_aClassAliases, $sNewName, $sRealName);
}
public function GetModifierProperties($sPluginClass)
{
if (array_key_exists($sPluginClass, $this->m_aModifierProperties))
{
return $this->m_aModifierProperties[$sPluginClass];
}
else
{
return array();
}
}
public function GetSelectedClass($sAlias)
{
return $this->m_aSelectedClasses[$sAlias];
}
public function AddFilteredTable($sTableAlias, $oCondition)
{
if (array_key_exists($sTableAlias, $this->m_aFilteredTables))
{
$this->m_aFilteredTables[$sTableAlias][] = $oCondition;
}
else
{
$this->m_aFilteredTables[$sTableAlias] = array($oCondition);
}
}
public function GetFilteredTables()
{
return $this->m_aFilteredTables;
}
}

View File

@@ -0,0 +1,186 @@
<?php
class QueryBuilderExpressions
{
/**
* @var Expression
*/
protected $m_oConditionExpr;
/**
* @var Expression[]
*/
protected $m_aSelectExpr;
/**
* @var Expression[]
*/
protected $m_aGroupByExpr;
/**
* @var Expression[]
*/
protected $m_aJoinFields;
/**
* @var string[]
*/
protected $m_aClassIds;
public function __construct(DBObjectSearch $oSearch, $aGroupByExpr = null, $aSelectExpr = null)
{
$this->m_oConditionExpr = $oSearch->GetCriteria();
if (!$oSearch->GetShowObsoleteData())
{
foreach ($oSearch->GetSelectedClasses() as $sAlias => $sClass)
{
if (MetaModel::IsObsoletable($sClass))
{
$oNotObsolete = new BinaryExpression(new FieldExpression('obsolescence_flag', $sAlias), '=', new ScalarExpression(0));
$this->m_oConditionExpr = $this->m_oConditionExpr->LogAnd($oNotObsolete);
}
}
}
$this->m_aSelectExpr = is_null($aSelectExpr) ? array() : $aSelectExpr;
$this->m_aGroupByExpr = $aGroupByExpr;
$this->m_aJoinFields = array();
$this->m_aClassIds = array();
foreach ($oSearch->GetJoinedClasses() as $sClassAlias => $sClass)
{
$this->m_aClassIds[$sClassAlias] = new FieldExpression('id', $sClassAlias);
}
}
public function GetSelect()
{
return $this->m_aSelectExpr;
}
public function GetGroupBy()
{
return $this->m_aGroupByExpr;
}
public function GetCondition()
{
return $this->m_oConditionExpr;
}
/**
* @return Expression|mixed
*/
public function PopJoinField()
{
return array_pop($this->m_aJoinFields);
}
/**
* @param string $sAttAlias
* @param Expression $oExpression
*/
public function AddSelect($sAttAlias, Expression $oExpression)
{
$this->m_aSelectExpr[$sAttAlias] = $oExpression;
}
/**
* @param Expression $oExpression
*/
public function AddCondition(Expression $oExpression)
{
$this->m_oConditionExpr = $this->m_oConditionExpr->LogAnd($oExpression);
}
/**
* @param Expression $oExpression
*/
public function PushJoinField(Expression $oExpression)
{
array_push($this->m_aJoinFields, $oExpression);
}
/**
* Get tables representing the queried objects
* Could be further optimized: when the first join is an outer join, then the rest can be omitted
*
* @param array $aTables
*
* @return array
*/
public function GetMandatoryTables(&$aTables = null)
{
if (is_null($aTables))
{
$aTables = array();
}
foreach ($this->m_aClassIds as $sClass => $oExpression)
{
$oExpression->CollectUsedParents($aTables);
}
return $aTables;
}
public function GetUnresolvedFields($sAlias, &$aUnresolved)
{
$this->m_oConditionExpr->GetUnresolvedFields($sAlias, $aUnresolved);
foreach ($this->m_aSelectExpr as $sColAlias => $oExpr)
{
$oExpr->GetUnresolvedFields($sAlias, $aUnresolved);
}
if ($this->m_aGroupByExpr)
{
foreach ($this->m_aGroupByExpr as $sColAlias => $oExpr)
{
$oExpr->GetUnresolvedFields($sAlias, $aUnresolved);
}
}
foreach ($this->m_aJoinFields as $oExpression)
{
$oExpression->GetUnresolvedFields($sAlias, $aUnresolved);
}
}
public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true)
{
$this->m_oConditionExpr = $this->m_oConditionExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
foreach ($this->m_aSelectExpr as $sColAlias => $oExpr)
{
$this->m_aSelectExpr[$sColAlias] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
}
if ($this->m_aGroupByExpr)
{
foreach ($this->m_aGroupByExpr as $sColAlias => $oExpr)
{
$this->m_aGroupByExpr[$sColAlias] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
}
}
foreach ($this->m_aJoinFields as $index => $oExpression)
{
$this->m_aJoinFields[$index] = $oExpression->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
}
foreach ($this->m_aClassIds as $sClass => $oExpression)
{
$this->m_aClassIds[$sClass] = $oExpression->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
}
}
public function RenameParam($sOldName, $sNewName)
{
$this->m_oConditionExpr->RenameParam($sOldName, $sNewName);
foreach ($this->m_aSelectExpr as $sColAlias => $oExpr)
{
$this->m_aSelectExpr[$sColAlias] = $oExpr->RenameParam($sOldName, $sNewName);
}
if ($this->m_aGroupByExpr)
{
foreach ($this->m_aGroupByExpr as $sColAlias => $oExpr)
{
$this->m_aGroupByExpr[$sColAlias] = $oExpr->RenameParam($sOldName, $sNewName);
}
}
foreach ($this->m_aJoinFields as $index => $oExpression)
{
$this->m_aJoinFields[$index] = $oExpression->RenameParam($sOldName, $sNewName);
}
}
}

View File

@@ -18,7 +18,6 @@
//
require_once(APPROOT.'core/modulehandler.class.inc.php');
require_once(APPROOT.'core/querybuildercontext.class.inc.php');
require_once(APPROOT.'core/querymodifier.class.inc.php');
require_once(APPROOT.'core/metamodelmodifier.inc.php');
require_once(APPROOT.'core/computing.inc.php');

View File

@@ -2301,183 +2301,3 @@ class CharConcatWSExpression extends CharConcatExpression
}
}
class QueryBuilderExpressions
{
/**
* @var Expression
*/
protected $m_oConditionExpr;
/**
* @var Expression[]
*/
protected $m_aSelectExpr;
/**
* @var Expression[]
*/
protected $m_aGroupByExpr;
/**
* @var Expression[]
*/
protected $m_aJoinFields;
/**
* @var string[]
*/
protected $m_aClassIds;
public function __construct(DBObjectSearch $oSearch, $aGroupByExpr = null, $aSelectExpr = null)
{
$this->m_oConditionExpr = $oSearch->GetCriteria();
if (!$oSearch->GetShowObsoleteData())
{
foreach ($oSearch->GetSelectedClasses() as $sAlias => $sClass)
{
if (MetaModel::IsObsoletable($sClass))
{
$oNotObsolete = new BinaryExpression(new FieldExpression('obsolescence_flag', $sAlias), '=', new ScalarExpression(0));
$this->m_oConditionExpr = $this->m_oConditionExpr->LogAnd($oNotObsolete);
}
}
}
$this->m_aSelectExpr = is_null($aSelectExpr) ? array() : $aSelectExpr;
$this->m_aGroupByExpr = $aGroupByExpr;
$this->m_aJoinFields = array();
$this->m_aClassIds = array();
foreach($oSearch->GetJoinedClasses() as $sClassAlias => $sClass)
{
$this->m_aClassIds[$sClassAlias] = new FieldExpression('id', $sClassAlias);
}
}
public function GetSelect()
{
return $this->m_aSelectExpr;
}
public function GetGroupBy()
{
return $this->m_aGroupByExpr;
}
public function GetCondition()
{
return $this->m_oConditionExpr;
}
/**
* @return Expression|mixed
*/
public function PopJoinField()
{
return array_pop($this->m_aJoinFields);
}
/**
* @param string $sAttAlias
* @param Expression $oExpression
*/
public function AddSelect($sAttAlias, Expression $oExpression)
{
$this->m_aSelectExpr[$sAttAlias] = $oExpression;
}
/**
* @param Expression $oExpression
*/
public function AddCondition(Expression $oExpression)
{
$this->m_oConditionExpr = $this->m_oConditionExpr->LogAnd($oExpression);
}
/**
* @param Expression $oExpression
*/
public function PushJoinField(Expression $oExpression)
{
array_push($this->m_aJoinFields, $oExpression);
}
/**
* Get tables representing the queried objects
* Could be further optimized: when the first join is an outer join, then the rest can be omitted
* @param array $aTables
* @return array
*/
public function GetMandatoryTables(&$aTables = null)
{
if (is_null($aTables)) $aTables = array();
foreach($this->m_aClassIds as $sClass => $oExpression)
{
$oExpression->CollectUsedParents($aTables);
}
return $aTables;
}
public function GetUnresolvedFields($sAlias, &$aUnresolved)
{
$this->m_oConditionExpr->GetUnresolvedFields($sAlias, $aUnresolved);
foreach($this->m_aSelectExpr as $sColAlias => $oExpr)
{
$oExpr->GetUnresolvedFields($sAlias, $aUnresolved);
}
if ($this->m_aGroupByExpr)
{
foreach($this->m_aGroupByExpr as $sColAlias => $oExpr)
{
$oExpr->GetUnresolvedFields($sAlias, $aUnresolved);
}
}
foreach($this->m_aJoinFields as $oExpression)
{
$oExpression->GetUnresolvedFields($sAlias, $aUnresolved);
}
}
public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true)
{
$this->m_oConditionExpr = $this->m_oConditionExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
foreach($this->m_aSelectExpr as $sColAlias => $oExpr)
{
$this->m_aSelectExpr[$sColAlias] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
}
if ($this->m_aGroupByExpr)
{
foreach($this->m_aGroupByExpr as $sColAlias => $oExpr)
{
$this->m_aGroupByExpr[$sColAlias] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
}
}
foreach($this->m_aJoinFields as $index => $oExpression)
{
$this->m_aJoinFields[$index] = $oExpression->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
}
foreach($this->m_aClassIds as $sClass => $oExpression)
{
$this->m_aClassIds[$sClass] = $oExpression->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
}
}
public function RenameParam($sOldName, $sNewName)
{
$this->m_oConditionExpr->RenameParam($sOldName, $sNewName);
foreach($this->m_aSelectExpr as $sColAlias => $oExpr)
{
$this->m_aSelectExpr[$sColAlias] = $oExpr->RenameParam($sOldName, $sNewName);
}
if ($this->m_aGroupByExpr)
{
foreach($this->m_aGroupByExpr as $sColAlias => $oExpr)
{
$this->m_aGroupByExpr[$sColAlias] = $oExpr->RenameParam($sOldName, $sNewName);
}
}
foreach($this->m_aJoinFields as $index => $oExpression)
{
$this->m_aJoinFields[$index] = $oExpression->RenameParam($sOldName, $sNewName);
}
}
}

View File

@@ -0,0 +1,186 @@
<?php
class QueryBuilderExpressions
{
/**
* @var Expression
*/
protected $m_oConditionExpr;
/**
* @var Expression[]
*/
protected $m_aSelectExpr;
/**
* @var Expression[]
*/
protected $m_aGroupByExpr;
/**
* @var Expression[]
*/
protected $m_aJoinFields;
/**
* @var string[]
*/
protected $m_aClassIds;
public function __construct(DBObjectSearch $oSearch, $aGroupByExpr = null, $aSelectExpr = null)
{
$this->m_oConditionExpr = $oSearch->GetCriteria();
if (!$oSearch->GetShowObsoleteData())
{
foreach ($oSearch->GetSelectedClasses() as $sAlias => $sClass)
{
if (MetaModel::IsObsoletable($sClass))
{
$oNotObsolete = new BinaryExpression(new FieldExpression('obsolescence_flag', $sAlias), '=', new ScalarExpression(0));
$this->m_oConditionExpr = $this->m_oConditionExpr->LogAnd($oNotObsolete);
}
}
}
$this->m_aSelectExpr = is_null($aSelectExpr) ? array() : $aSelectExpr;
$this->m_aGroupByExpr = $aGroupByExpr;
$this->m_aJoinFields = array();
$this->m_aClassIds = array();
foreach ($oSearch->GetJoinedClasses() as $sClassAlias => $sClass)
{
$this->m_aClassIds[$sClassAlias] = new FieldExpression('id', $sClassAlias);
}
}
public function GetSelect()
{
return $this->m_aSelectExpr;
}
public function GetGroupBy()
{
return $this->m_aGroupByExpr;
}
public function GetCondition()
{
return $this->m_oConditionExpr;
}
/**
* @return Expression|mixed
*/
public function PopJoinField()
{
return array_pop($this->m_aJoinFields);
}
/**
* @param string $sAttAlias
* @param Expression $oExpression
*/
public function AddSelect($sAttAlias, Expression $oExpression)
{
$this->m_aSelectExpr[$sAttAlias] = $oExpression;
}
/**
* @param Expression $oExpression
*/
public function AddCondition(Expression $oExpression)
{
$this->m_oConditionExpr = $this->m_oConditionExpr->LogAnd($oExpression);
}
/**
* @param Expression $oExpression
*/
public function PushJoinField(Expression $oExpression)
{
array_push($this->m_aJoinFields, $oExpression);
}
/**
* Get tables representing the queried objects
* Could be further optimized: when the first join is an outer join, then the rest can be omitted
*
* @param array $aTables
*
* @return array
*/
public function GetMandatoryTables(&$aTables = null)
{
if (is_null($aTables))
{
$aTables = array();
}
foreach ($this->m_aClassIds as $sClass => $oExpression)
{
$oExpression->CollectUsedParents($aTables);
}
return $aTables;
}
public function GetUnresolvedFields($sAlias, &$aUnresolved)
{
$this->m_oConditionExpr->GetUnresolvedFields($sAlias, $aUnresolved);
foreach ($this->m_aSelectExpr as $sColAlias => $oExpr)
{
$oExpr->GetUnresolvedFields($sAlias, $aUnresolved);
}
if ($this->m_aGroupByExpr)
{
foreach ($this->m_aGroupByExpr as $sColAlias => $oExpr)
{
$oExpr->GetUnresolvedFields($sAlias, $aUnresolved);
}
}
foreach ($this->m_aJoinFields as $oExpression)
{
$oExpression->GetUnresolvedFields($sAlias, $aUnresolved);
}
}
public function Translate($aTranslationData, $bMatchAll = true, $bMarkFieldsAsResolved = true)
{
$this->m_oConditionExpr = $this->m_oConditionExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
foreach ($this->m_aSelectExpr as $sColAlias => $oExpr)
{
$this->m_aSelectExpr[$sColAlias] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
}
if ($this->m_aGroupByExpr)
{
foreach ($this->m_aGroupByExpr as $sColAlias => $oExpr)
{
$this->m_aGroupByExpr[$sColAlias] = $oExpr->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
}
}
foreach ($this->m_aJoinFields as $index => $oExpression)
{
$this->m_aJoinFields[$index] = $oExpression->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
}
foreach ($this->m_aClassIds as $sClass => $oExpression)
{
$this->m_aClassIds[$sClass] = $oExpression->Translate($aTranslationData, $bMatchAll, $bMarkFieldsAsResolved);
}
}
public function RenameParam($sOldName, $sNewName)
{
$this->m_oConditionExpr->RenameParam($sOldName, $sNewName);
foreach ($this->m_aSelectExpr as $sColAlias => $oExpr)
{
$this->m_aSelectExpr[$sColAlias] = $oExpr->RenameParam($sOldName, $sNewName);
}
if ($this->m_aGroupByExpr)
{
foreach ($this->m_aGroupByExpr as $sColAlias => $oExpr)
{
$this->m_aGroupByExpr[$sColAlias] = $oExpr->RenameParam($sOldName, $sNewName);
}
}
foreach ($this->m_aJoinFields as $index => $oExpression)
{
$this->m_aJoinFields[$index] = $oExpression->RenameParam($sOldName, $sNewName);
}
}
}

View File

@@ -0,0 +1,724 @@
<?php
/**
* Class SQLObjectQueryBuilder
*
* @copyright Copyright (C) 2010-2019 Combodo SARL
* @license http://opensource.org/licenses/AGPL-3.0
*/
class SQLObjectQueryBuilder
{
/** @var \DBObjectSearch */
private $oDBObjetSearch;
/** @var bool */
private $bOptimizeQueries;
/** @var bool */
private $bDebugQuery;
/**
* SQLObjectQueryBuilder constructor.
*
* @param \DBObjectSearch $oDBObjetSearch
* @param bool $bOptimizeQueries
* @param bool $bDebugQuery
*/
public function __construct($oDBObjetSearch, $bOptimizeQueries, $bDebugQuery)
{
$this->oDBObjetSearch = $oDBObjetSearch;
$this->bOptimizeQueries = $bOptimizeQueries;
$this->bDebugQuery = $bDebugQuery;
}
/**
* @param array $aAttToLoad
* @param bool $bGetCount
* @param array $aModifierProperties
* @param array $aGroupByExpr
* @param array $aSelectedClasses
* @param array $aSelectExpr
*
* @return null|SQLObjectQuery
* @throws \CoreException
*/
public function BuildSQLQueryStruct($aAttToLoad, $bGetCount, $aModifierProperties, $aGroupByExpr = null, $aSelectedClasses = null, $aSelectExpr = null)
{
$oBuild = new QueryBuilderContext($this->oDBObjetSearch, $aModifierProperties, $aGroupByExpr, $aSelectedClasses, $aSelectExpr);
$oSQLQuery = $this->MakeSQLObjectQuery($oBuild, $aAttToLoad, array());
$oSQLQuery->SetCondition($oBuild->m_oQBExpressions->GetCondition());
if (is_array($aGroupByExpr))
{
$aCols = $oBuild->m_oQBExpressions->GetGroupBy();
$oSQLQuery->SetGroupBy($aCols);
$oSQLQuery->SetSelect($aCols);
}
else
{
$oSQLQuery->SetSelect($oBuild->m_oQBExpressions->GetSelect());
}
if ($aSelectExpr)
{
// Get the fields corresponding to the select expressions
foreach($oBuild->m_oQBExpressions->GetSelect() as $sAlias => $oExpr)
{
if (key_exists($sAlias, $aSelectExpr))
{
$oSQLQuery->AddSelect($sAlias, $oExpr);
}
}
}
$aMandatoryTables = null;
if ($this->bOptimizeQueries)
{
if ($bGetCount)
{
// Simplify the query if just getting the count
$oSQLQuery->SetSelect(array());
}
$oBuild->m_oQBExpressions->GetMandatoryTables($aMandatoryTables);
$oSQLQuery->OptimizeJoins($aMandatoryTables);
}
// Filter tables as late as possible: do not interfere with the optimization process
foreach ($oBuild->GetFilteredTables() as $sTableAlias => $aConditions)
{
if ($aMandatoryTables && array_key_exists($sTableAlias, $aMandatoryTables))
{
foreach ($aConditions as $oCondition)
{
$oSQLQuery->AddCondition($oCondition);
}
}
}
return $oSQLQuery;
}
/**
* @return \SQLObjectQuery|null
* @throws \CoreException
*/
public function MakeSQLObjectDeleteQuery()
{
$aModifierProperties = MetaModel::MakeModifierProperties($this->oDBObjetSearch);
$oBuild = new QueryBuilderContext($this->oDBObjetSearch, $aModifierProperties);
$oSQLQuery = $this->MakeSQLObjectQuery($oBuild, array($this->oDBObjetSearch->GetClassAlias() => array()), array());
$oSQLQuery->SetCondition($oBuild->m_oQBExpressions->GetCondition());
$oSQLQuery->SetSelect($oBuild->m_oQBExpressions->GetSelect());
$oSQLQuery->OptimizeJoins(array());
return $oSQLQuery;
}
/**
* @param array $aValues an array of $sAttCode => $value
*
* @return \SQLObjectQuery|null
* @throws \CoreException
*/
public function MakeSQLObjectUpdateQuery($aValues)
{
$aModifierProperties = MetaModel::MakeModifierProperties($this->oDBObjetSearch);
$oBuild = new QueryBuilderContext($this->oDBObjetSearch, $aModifierProperties);
$aRequested = array(); // Requested attributes are the updated attributes
foreach ($aValues as $sAttCode => $value)
{
$aRequested[$sAttCode] = MetaModel::GetAttributeDef($this->oDBObjetSearch->GetClass(), $sAttCode);
}
$oSQLQuery = $this->MakeSQLObjectQuery($oBuild, array($this->oDBObjetSearch->GetClassAlias() => $aRequested), $aValues);
$oSQLQuery->SetCondition($oBuild->m_oQBExpressions->GetCondition());
$oSQLQuery->SetSelect($oBuild->m_oQBExpressions->GetSelect());
$oSQLQuery->OptimizeJoins(array());
return $oSQLQuery;
}
/**
* @param \QueryBuilderContext $oBuild
* @param null $aAttToLoad
* @param array $aValues
* @return null|SQLObjectQuery
* @throws \CoreException
*/
public function MakeSQLObjectQuery($oBuild, $aAttToLoad = null, $aValues = array())
{
// Note: query class might be different than the class of the filter
// -> this occurs when we are linking our class to an external class (referenced by, or pointing to)
$sClass = $this->oDBObjetSearch->GetFirstJoinedClass();
$sClassAlias = $this->oDBObjetSearch->GetFirstJoinedClassAlias();
$bIsOnQueriedClass = array_key_exists($sClassAlias, $oBuild->GetRootFilter()->GetSelectedClasses());
//self::DbgTrace("Entering: ".$this->oDBObjetSearch->ToOQL().", ".($bIsOnQueriedClass ? "MAIN" : "SECONDARY"));
//$sRootClass = MetaModel::GetRootClass($sClass);
$sKeyField = MetaModel::DBGetKey($sClass);
if ($bIsOnQueriedClass)
{
// default to the whole list of attributes + the very std id/finalclass
$oBuild->m_oQBExpressions->AddSelect($sClassAlias.'id', new FieldExpression('id', $sClassAlias));
if (is_null($aAttToLoad) || !array_key_exists($sClassAlias, $aAttToLoad))
{
$sSelectedClass = $oBuild->GetSelectedClass($sClassAlias);
$aAttList = MetaModel::ListAttributeDefs($sSelectedClass);
}
else
{
$aAttList = $aAttToLoad[$sClassAlias];
}
foreach ($aAttList as $sAttCode => $oAttDef)
{
if (!$oAttDef->IsScalar()) continue;
// keep because it can be used for sorting - if (!$oAttDef->LoadInObject()) continue;
if ($oAttDef->IsBasedOnOQLExpression())
{
$oBuild->m_oQBExpressions->AddSelect($sClassAlias.$sAttCode, new FieldExpression($sAttCode, $sClassAlias));
}
else
{
foreach ($oAttDef->GetSQLExpressions() as $sColId => $sSQLExpr)
{
$oBuild->m_oQBExpressions->AddSelect($sClassAlias.$sAttCode.$sColId, new FieldExpression($sAttCode.$sColId, $sClassAlias));
}
}
}
}
//echo "<p>oQBExpr ".__LINE__.": <pre>\n".print_r($oBuild->m_oQBExpressions, true)."</pre></p>\n";
$aExpectedAtts = array(); // array of (attcode => fieldexpression)
//echo "<p>".__LINE__.": GetUnresolvedFields($sClassAlias, ...)</p>\n";
$oBuild->m_oQBExpressions->GetUnresolvedFields($sClassAlias, $aExpectedAtts);
// Compute a clear view of required joins (from the current class)
// Build the list of external keys:
// -> ext keys required by an explicit join
// -> ext keys mentionned in a 'pointing to' condition
// -> ext keys required for an external field
// -> ext keys required for a friendly name
//
$aExtKeys = array(); // array of sTableClass => array of (sAttCode (keys) => array of (sAttCode (fields)=> oAttDef))
//
// Optimization: could be partially computed once for all (cached) ?
//
if ($bIsOnQueriedClass)
{
// Get all Ext keys for the queried class (??)
foreach(MetaModel::GetKeysList($sClass) as $sKeyAttCode)
{
$sKeyTableClass = MetaModel::GetAttributeOrigin($sClass, $sKeyAttCode);
$aExtKeys[$sKeyTableClass][$sKeyAttCode] = array();
}
}
// Get all Ext keys used by the filter
foreach ($this->oDBObjetSearch->GetCriteria_PointingTo() as $sKeyAttCode => $aPointingTo)
{
if (array_key_exists(TREE_OPERATOR_EQUALS, $aPointingTo))
{
$sKeyTableClass = MetaModel::GetAttributeOrigin($sClass, $sKeyAttCode);
$aExtKeys[$sKeyTableClass][$sKeyAttCode] = array();
}
}
$aFNJoinAlias = array(); // array of (subclass => alias)
foreach ($aExpectedAtts as $sExpectedAttCode => $oExpression)
{
if (!MetaModel::IsValidAttCode($sClass, $sExpectedAttCode)) continue;
$oAttDef = MetaModel::GetAttributeDef($sClass, $sExpectedAttCode);
if ($oAttDef->IsBasedOnOQLExpression())
{
// To optimize: detect a restriction on child classes in the condition expression
// e.g. SELECT FunctionalCI WHERE finalclass IN ('Server', 'VirtualMachine')
$oExpression = DBObjectSearch::GetPolymorphicExpression($sClass, $sExpectedAttCode);
$aRequiredFields = array();
$oExpression->GetUnresolvedFields('', $aRequiredFields);
$aTranslateFields = array();
foreach($aRequiredFields as $sSubClass => $aFields)
{
foreach($aFields as $sAttCode => $oField)
{
$oAttDef = MetaModel::GetAttributeDef($sSubClass, $sAttCode);
if ($oAttDef->IsExternalKey())
{
$sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sAttCode);
$aExtKeys[$sClassOfAttribute][$sAttCode] = array();
}
elseif ($oAttDef->IsExternalField())
{
$sKeyAttCode = $oAttDef->GetKeyAttCode();
$sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sKeyAttCode);
$aExtKeys[$sClassOfAttribute][$sKeyAttCode][$sAttCode] = $oAttDef;
}
else
{
$sClassOfAttribute = MetaModel::GetAttributeOrigin($sSubClass, $sAttCode);
}
if (MetaModel::IsParentClass($sClassOfAttribute, $sClass))
{
// The attribute is part of the standard query
//
$sAliasForAttribute = $sClassAlias;
}
else
{
// The attribute will be available from an additional outer join
// For each subclass (table) one single join is enough
//
if (!array_key_exists($sClassOfAttribute, $aFNJoinAlias))
{
$sAliasForAttribute = $oBuild->GenerateClassAlias($sClassAlias.'_fn_'.$sClassOfAttribute, $sClassOfAttribute);
$aFNJoinAlias[$sClassOfAttribute] = $sAliasForAttribute;
}
else
{
$sAliasForAttribute = $aFNJoinAlias[$sClassOfAttribute];
}
}
$aTranslateFields[$sSubClass][$sAttCode] = new FieldExpression($sAttCode, $sAliasForAttribute);
}
}
$oExpression = $oExpression->Translate($aTranslateFields, false);
$aTranslateNow = array();
$aTranslateNow[$sClassAlias][$sExpectedAttCode] = $oExpression;
$oBuild->m_oQBExpressions->Translate($aTranslateNow, false);
}
}
// Add the ext fields used in the select (eventually adds an external key)
foreach(MetaModel::ListAttributeDefs($sClass) as $sAttCode=>$oAttDef)
{
if ($oAttDef->IsExternalField())
{
if (array_key_exists($sAttCode, $aExpectedAtts))
{
// Add the external attribute
$sKeyAttCode = $oAttDef->GetKeyAttCode();
$sKeyTableClass = MetaModel::GetAttributeOrigin($sClass, $sKeyAttCode);
$aExtKeys[$sKeyTableClass][$sKeyAttCode][$sAttCode] = $oAttDef;
}
}
}
$bRootFirst = MetaModel::GetConfig()->Get('optimize_requests_for_join_count');
if ($bRootFirst)
{
// First query built from the root, adding all tables including the leaf
// Before N.1065 we were joining from the leaf first, but this wasn't a good choice :
// most of the time (obsolescence, friendlyname, ...) we want to get a root attribute !
//
$oSelectBase = null;
$aClassHierarchy = MetaModel::EnumParentClasses($sClass, ENUM_PARENT_CLASSES_ALL, true);
$bIsClassStandaloneClass = (count($aClassHierarchy) == 1);
foreach($aClassHierarchy as $sSomeClass)
{
if (!MetaModel::HasTable($sSomeClass))
{
continue;
}
//self::DbgTrace("Adding join from root to leaf: $sSomeClass... let's call MakeSQLObjectQuerySingleTable()");
$oSelectParentTable = $this->MakeSQLObjectQuerySingleTable($oBuild, $aAttToLoad, $sSomeClass, $aExtKeys, $aValues);
if (is_null($oSelectBase))
{
$oSelectBase = $oSelectParentTable;
if (!$bIsClassStandaloneClass && (MetaModel::IsRootClass($sSomeClass)))
{
// As we're linking to root class first, we're adding a where clause on the finalClass attribute :
// COALESCE($sRootClassFinalClass IN ('$sExpectedClasses'), 1)
// If we don't, the child classes can be removed in the query optimisation phase, including the leaf that was queried
// So we still need to filter records to only those corresponding to the child classes !
// The coalesce is mandatory if we have a polymorphic query (left join)
$oClassListExpr = ListExpression::FromScalars(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL));
$sFinalClassSqlColumnName = MetaModel::DBGetClassField($sSomeClass);
$oClassExpr = new FieldExpression($sFinalClassSqlColumnName, $oSelectBase->GetTableAlias());
$oInExpression = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr);
$oTrueExpression = new TrueExpression();
$aCoalesceAttr = array($oInExpression, $oTrueExpression);
$oFinalClassRestriction = new FunctionExpression("COALESCE", $aCoalesceAttr);
$oBuild->m_oQBExpressions->AddCondition($oFinalClassRestriction);
}
}
else
{
$oSelectBase->AddInnerJoin($oSelectParentTable, $sKeyField, MetaModel::DBGetKey($sSomeClass));
}
}
}
else
{
// First query built upon on the leaf (ie current) class
//
//self::DbgTrace("Main (=leaf) class, call MakeSQLObjectQuerySingleTable()");
if (MetaModel::HasTable($sClass))
{
$oSelectBase = $this->MakeSQLObjectQuerySingleTable($oBuild, $aAttToLoad, $sClass, $aExtKeys, $aValues);
}
else
{
$oSelectBase = null;
// As the join will not filter on the expected classes, we have to specify it explicitely
$sExpectedClasses = implode("', '", MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL));
$oFinalClassRestriction = Expression::FromOQL("`$sClassAlias`.finalclass IN ('$sExpectedClasses')");
$oBuild->m_oQBExpressions->AddCondition($oFinalClassRestriction);
}
// Then we join the queries of the eventual parent classes (compound model)
foreach(MetaModel::EnumParentClasses($sClass) as $sParentClass)
{
if (!MetaModel::HasTable($sParentClass)) continue;
//self::DbgTrace("Parent class: $sParentClass... let's call MakeSQLObjectQuerySingleTable()");
$oSelectParentTable = $this->MakeSQLObjectQuerySingleTable($oBuild, $aAttToLoad, $sParentClass, $aExtKeys, $aValues);
if (is_null($oSelectBase))
{
$oSelectBase = $oSelectParentTable;
}
else
{
$oSelectBase->AddInnerJoin($oSelectParentTable, $sKeyField, MetaModel::DBGetKey($sParentClass));
}
}
}
// Filter on objects referencing me
//
foreach($this->oDBObjetSearch->GetCriteria_ReferencedBy() as $sForeignClass=>$aReferences)
{
foreach($aReferences as $sForeignExtKeyAttCode => $aFiltersByOperator)
{
foreach ($aFiltersByOperator as $iOperatorCode => $aFilters)
{
foreach ($aFilters as $oForeignFilter)
{
$oForeignKeyAttDef = MetaModel::GetAttributeDef($sForeignClass, $sForeignExtKeyAttCode);
//self::DbgTrace("Referenced by foreign key: $sForeignExtKeyAttCode... let's call MakeSQLObjectQuery()");
$sForeignClassAlias = $oForeignFilter->GetFirstJoinedClassAlias();
$oBuild->m_oQBExpressions->PushJoinField(new FieldExpression($sForeignExtKeyAttCode, $sForeignClassAlias));
if ($oForeignKeyAttDef instanceof AttributeObjectKey)
{
$sClassAttCode = $oForeignKeyAttDef->Get('class_attcode');
// Add the condition: `$sForeignClassAlias`.$sClassAttCode IN (subclasses of $sClass')
$oClassListExpr = ListExpression::FromScalars(MetaModel::EnumChildClasses($sClass, ENUM_CHILD_CLASSES_ALL));
$oClassExpr = new FieldExpression($sClassAttCode, $sForeignClassAlias);
$oClassRestriction = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr);
$oBuild->m_oQBExpressions->AddCondition($oClassRestriction);
}
$oSQLObjectQueryBuilder = new SQLObjectQueryBuilder($oForeignFilter, $this->bOptimizeQueries, $this->bDebugQuery);
$oSelectForeign = $oSQLObjectQueryBuilder->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();
$oSelectBase->AddInnerJoinTree($oSelectForeign, $KeyLeft, $KeyRight, $KeyLeft, $KeyRight, $sForeignKeyTable, $iOperatorCode, true);
}
}
}
}
}
// Additional JOINS for Friendly names
//
foreach ($aFNJoinAlias as $sSubClass => $sSubClassAlias)
{
$oSubClassFilter = new DBObjectSearch($sSubClass, $sSubClassAlias);
$oSQLObjectQueryBuilder = new SQLObjectQueryBuilder($oSubClassFilter, $this->bOptimizeQueries, $this->bDebugQuery);
$oSelectFN = $oSQLObjectQueryBuilder->MakeSQLObjectQuerySingleTable($oBuild, $aAttToLoad, $sSubClass, $aExtKeys, array());
$oSelectBase->AddLeftJoin($oSelectFN, $sKeyField, MetaModel::DBGetKey($sSubClass));
}
// That's all... cross fingers and we'll get some working query
//MyHelpers::var_dump_html($oSelectBase, true);
//MyHelpers::var_dump_html($oSelectBase->RenderSelect(), true);
if ($this->bDebugQuery) $oSelectBase->DisplayHtml();
return $oSelectBase;
}
/**
* @param \QueryBuilderContext $oBuild
* @param $aAttToLoad
* @param $sTableClass
* @param $aExtKeys
* @param $aValues
*
* @return \SQLObjectQuery
* @throws \CoreException
*/
private function MakeSQLObjectQuerySingleTable($oBuild, $aAttToLoad, $sTableClass, $aExtKeys, $aValues)
{
// $aExtKeys is an array of sTableClass => array of (sAttCode (keys) => array of sAttCode (fields))
// Prepare the query for a single table (compound objects)
// Ignores the items (attributes/filters) that are not on the target table
// Perform an (inner or left) join for every external key (and specify the expected fields)
//
// Returns an SQLQuery
//
$sTargetClass = $this->oDBObjetSearch->GetFirstJoinedClass();
$sTargetAlias = $this->oDBObjetSearch->GetFirstJoinedClassAlias();
$sTable = MetaModel::DBGetTable($sTableClass);
$sTableAlias = $oBuild->GenerateTableAlias($sTargetAlias.'_'.$sTable, $sTable);
$aTranslation = array();
$aExpectedAtts = array();
$oBuild->m_oQBExpressions->GetUnresolvedFields($sTargetAlias, $aExpectedAtts);
$bIsOnQueriedClass = array_key_exists($sTargetAlias, $oBuild->GetRootFilter()->GetSelectedClasses());
//self::DbgTrace("Entering: tableclass=$sTableClass, filter=".$this->oDBObjetSearch->ToOQL().", ".($bIsOnQueriedClass ? "MAIN" : "SECONDARY"));
// 1 - SELECT and UPDATE
//
// Note: no need for any values nor fields for foreign Classes (ie not the queried Class)
//
$aUpdateValues = array();
// 1/a - Get the key and friendly name
//
// We need one pkey to be the key, let's take the first one available
$oSelectedIdField = null;
$oIdField = new FieldExpressionResolved(MetaModel::DBGetKey($sTableClass), $sTableAlias);
$aTranslation[$sTargetAlias]['id'] = $oIdField;
if ($bIsOnQueriedClass)
{
// Add this field to the list of queried fields (required for the COUNT to work fine)
$oSelectedIdField = $oIdField;
}
// 1/b - Get the other attributes
//
foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef)
{
// Skip this attribute if not defined in this table
if (MetaModel::GetAttributeOrigin($sTargetClass, $sAttCode) != $sTableClass) continue;
// Skip this attribute if not made of SQL columns
if (count($oAttDef->GetSQLExpressions()) == 0) continue;
// Update...
//
if ($bIsOnQueriedClass && array_key_exists($sAttCode, $aValues))
{
assert ($oAttDef->IsBasedOnDBColumns());
foreach ($oAttDef->GetSQLValues($aValues[$sAttCode]) as $sColumn => $sValue)
{
$aUpdateValues[$sColumn] = $sValue;
}
}
}
// 2 - The SQL query, for this table only
//
$oSelectBase = new SQLObjectQuery($sTable, $sTableAlias, array(), $bIsOnQueriedClass, $aUpdateValues, $oSelectedIdField);
// 3 - Resolve expected expressions (translation table: alias.attcode => table.column)
//
foreach(MetaModel::ListAttributeDefs($sTableClass) as $sAttCode=>$oAttDef)
{
// Skip this attribute if not defined in this table
if (MetaModel::GetAttributeOrigin($sTargetClass, $sAttCode) != $sTableClass) continue;
// Select...
//
if ($oAttDef->IsExternalField())
{
// skip, this will be handled in the joined tables (done hereabove)
}
else
{
// standard field, or external key
// add it to the output
foreach ($oAttDef->GetSQLExpressions() as $sColId => $sSQLExpr)
{
if (array_key_exists($sAttCode.$sColId, $aExpectedAtts))
{
$oFieldSQLExp = new FieldExpressionResolved($sSQLExpr, $sTableAlias);
foreach (MetaModel::EnumPlugins('iQueryModifier') as $sPluginClass => $oQueryModifier)
{
$oFieldSQLExp = $oQueryModifier->GetFieldExpression($oBuild, $sTargetClass, $sAttCode, $sColId, $oFieldSQLExp, $oSelectBase);
}
$aTranslation[$sTargetAlias][$sAttCode.$sColId] = $oFieldSQLExp;
}
}
}
}
// 4 - The external keys -> joins...
//
$aAllPointingTo = $this->oDBObjetSearch->GetCriteria_PointingTo();
if (array_key_exists($sTableClass, $aExtKeys))
{
foreach ($aExtKeys[$sTableClass] as $sKeyAttCode => $aExtFields)
{
$oKeyAttDef = MetaModel::GetAttributeDef($sTableClass, $sKeyAttCode);
$aPointingTo = $this->oDBObjetSearch->GetCriteria_PointingTo($sKeyAttCode);
if (!array_key_exists(TREE_OPERATOR_EQUALS, $aPointingTo))
{
// The join was not explicitely defined in the filter,
// we need to do it now
$sKeyClass = $oKeyAttDef->GetTargetClass();
$sKeyClassAlias = $oBuild->GenerateClassAlias($sKeyClass.'_'.$sKeyAttCode, $sKeyClass);
$oExtFilter = new DBObjectSearch($sKeyClass, $sKeyClassAlias);
$aAllPointingTo[$sKeyAttCode][TREE_OPERATOR_EQUALS][$sKeyClassAlias] = $oExtFilter;
}
}
}
foreach ($aAllPointingTo as $sKeyAttCode => $aPointingTo)
{
foreach($aPointingTo as $iOperatorCode => $aFilter)
{
foreach($aFilter as $oExtFilter)
{
if (!MetaModel::IsValidAttCode($sTableClass, $sKeyAttCode)) continue; // Not defined in the class, skip it
// The aliases should not conflict because normalization occured while building the filter
$oKeyAttDef = MetaModel::GetAttributeDef($sTableClass, $sKeyAttCode);
$sKeyClass = $oExtFilter->GetFirstJoinedClass();
$sKeyClassAlias = $oExtFilter->GetFirstJoinedClassAlias();
// Note: there is no search condition in $oExtFilter, because normalization did merge the condition onto the top of the filter tree
if ($iOperatorCode == TREE_OPERATOR_EQUALS)
{
if (array_key_exists($sTableClass, $aExtKeys) && array_key_exists($sKeyAttCode, $aExtKeys[$sTableClass]))
{
// Specify expected attributes for the target class query
// ... and use the current alias !
$aTranslateNow = array(); // Translation for external fields - must be performed before the join is done (recursion...)
foreach($aExtKeys[$sTableClass][$sKeyAttCode] as $sAttCode => $oAtt)
{
$oExtAttDef = $oAtt->GetExtAttDef();
if ($oExtAttDef->IsBasedOnOQLExpression())
{
$aTranslateNow[$sTargetAlias][$sAttCode] = new FieldExpression($oExtAttDef->GetCode(), $sKeyClassAlias);
}
else
{
$sExtAttCode = $oAtt->GetExtAttCode();
// Translate mainclass.extfield => remoteclassalias.remotefieldcode
$oRemoteAttDef = MetaModel::GetAttributeDef($sKeyClass, $sExtAttCode);
foreach ($oRemoteAttDef->GetSQLExpressions() as $sColId => $sRemoteAttExpr)
{
$aTranslateNow[$sTargetAlias][$sAttCode.$sColId] = new FieldExpression($sExtAttCode, $sKeyClassAlias);
}
}
}
if ($oKeyAttDef instanceof AttributeObjectKey)
{
// Add the condition: `$sTargetAlias`.$sClassAttCode IN (subclasses of $sKeyClass')
$sClassAttCode = $oKeyAttDef->Get('class_attcode');
$oClassAttDef = MetaModel::GetAttributeDef($sTargetClass, $sClassAttCode);
foreach ($oClassAttDef->GetSQLExpressions() as $sColId => $sSQLExpr)
{
$aTranslateNow[$sTargetAlias][$sClassAttCode.$sColId] = new FieldExpressionResolved($sSQLExpr, $sTableAlias);
}
$oClassListExpr = ListExpression::FromScalars(MetaModel::EnumChildClasses($sKeyClass, ENUM_CHILD_CLASSES_ALL));
$oClassExpr = new FieldExpression($sClassAttCode, $sTargetAlias);
$oClassRestriction = new BinaryExpression($oClassExpr, 'IN', $oClassListExpr);
$oBuild->m_oQBExpressions->AddCondition($oClassRestriction);
}
// Translate prior to recursing
//
$oBuild->m_oQBExpressions->Translate($aTranslateNow, false);
//self::DbgTrace("External key $sKeyAttCode (class: $sKeyClass), call MakeSQLObjectQuery()");
$oBuild->m_oQBExpressions->PushJoinField(new FieldExpression('id', $sKeyClassAlias));
$oSQLObjectQueryBuilder = new SQLObjectQueryBuilder($oExtFilter, $this->bOptimizeQueries, $this->bDebugQuery);
$oSelectExtKey = $oSQLObjectQueryBuilder->MakeSQLObjectQuery($oBuild, $aAttToLoad);
$oJoinExpr = $oBuild->m_oQBExpressions->PopJoinField();
$sExternalKeyTable = $oJoinExpr->GetParent();
$sExternalKeyField = $oJoinExpr->GetName();
$aCols = $oKeyAttDef->GetSQLExpressions(); // Workaround a PHP bug: sometimes issuing a Notice if invoking current(somefunc())
$sLocalKeyField = current($aCols); // get the first column for an external key
//self::DbgTrace("External key $sKeyAttCode, Join on $sLocalKeyField = $sExternalKeyField");
if ($oKeyAttDef->IsNullAllowed())
{
$oSelectBase->AddLeftJoin($oSelectExtKey, $sLocalKeyField, $sExternalKeyField);
}
else
{
$oSelectBase->AddInnerJoin($oSelectExtKey, $sLocalKeyField, $sExternalKeyField, $sExternalKeyTable);
}
}
}
elseif(MetaModel::GetAttributeOrigin($sKeyClass, $sKeyAttCode) == $sTableClass)
{
$oBuild->m_oQBExpressions->PushJoinField(new FieldExpression($sKeyAttCode, $sKeyClassAlias));
$oSQLObjectQueryBuilder = new SQLObjectQueryBuilder($oExtFilter, $this->bOptimizeQueries, $this->bDebugQuery);
$oSelectExtKey = $oSQLObjectQueryBuilder->MakeSQLObjectQuery($oBuild, $aAttToLoad);
$oJoinExpr = $oBuild->m_oQBExpressions->PopJoinField();
$sExternalKeyTable = $oJoinExpr->GetParent();
$sExternalKeyField = $oJoinExpr->GetName();
$sLeftIndex = $sExternalKeyField.'_left';
$sRightIndex = $sExternalKeyField.'_right';
$LocalKeyLeft = $oKeyAttDef->GetSQLLeft();
$LocalKeyRight = $oKeyAttDef->GetSQLRight();
$oSelectBase->AddInnerJoinTree($oSelectExtKey, $LocalKeyLeft, $LocalKeyRight, $sLeftIndex, $sRightIndex, $sExternalKeyTable, $iOperatorCode);
}
}
}
}
// Translate the selected columns
//
$oBuild->m_oQBExpressions->Translate($aTranslation, false);
// Filter out archived records
//
if (MetaModel::IsArchivable($sTableClass))
{
if (!$oBuild->GetRootFilter()->GetArchiveMode())
{
$bIsOnJoinedClass = array_key_exists($sTargetAlias, $oBuild->GetRootFilter()->GetJoinedClasses());
if ($bIsOnJoinedClass)
{
if (MetaModel::IsParentClass($sTableClass, $sTargetClass))
{
$oNotArchived = new BinaryExpression(new FieldExpressionResolved('archive_flag', $sTableAlias), '=', new ScalarExpression(0));
$oBuild->AddFilteredTable($sTableAlias, $oNotArchived);
}
}
}
}
return $oSelectBase;
}
}

View File

@@ -146,7 +146,6 @@ return array(
'CryptEngine' => $baseDir . '/core/simplecrypt.class.inc.php',
'CustomFieldsHandler' => $baseDir . '/core/customfieldshandler.class.inc.php',
'DBObject' => $baseDir . '/core/dbobject.class.php',
'DBObjectSearch' => $baseDir . '/core/dbobjectsearch.class.php',
'DBObjectSet' => $baseDir . '/core/dbobjectset.class.php',
'DBObjectSetComparator' => $baseDir . '/core/dbobjectset.class.php',
'DBProperty' => $baseDir . '/core/dbproperty.class.inc.php',
@@ -380,8 +379,6 @@ return array(
'Psr\\SimpleCache\\InvalidArgumentException' => $vendorDir . '/psr/simple-cache/src/InvalidArgumentException.php',
'QRcode' => $vendorDir . '/combodo/tcpdf/include/barcodes/qrcode.php',
'Query' => $baseDir . '/application/query.class.inc.php',
'QueryBuilderContext' => $baseDir . '/core/querybuildercontext.class.inc.php',
'QueryBuilderExpressions' => $baseDir . '/core/oql/expression.class.inc.php',
'QueryOQL' => $baseDir . '/application/query.class.inc.php',
'QueryReflection' => $baseDir . '/core/modelreflection.class.inc.php',
'QueryReflectionRuntime' => $baseDir . '/core/modelreflection.class.inc.php',
@@ -405,6 +402,7 @@ return array(
'RuntimeDashboard' => $baseDir . '/application/dashboard.class.inc.php',
'SQLExpression' => $baseDir . '/core/oql/expression.class.inc.php',
'SQLObjectQuery' => $baseDir . '/core/sqlobjectquery.class.inc.php',
'SQLObjectQueryBuilder' => $baseDir . '/core/sqlobjectquerybuilder.class.inc.php',
'SQLQuery' => $baseDir . '/core/sqlquery.class.inc.php',
'SQLUnionQuery' => $baseDir . '/core/sqlunionquery.class.inc.php',
'ScalarExpression' => $baseDir . '/core/oql/expression.class.inc.php',
@@ -1906,6 +1904,7 @@ return array(
'iPageUIExtension' => $baseDir . '/application/applicationextension.inc.php',
'iPopupMenuExtension' => $baseDir . '/application/applicationextension.inc.php',
'iPortalUIExtension' => $baseDir . '/application/applicationextension.inc.php',
'iPreferencesExtension' => $baseDir . '/application/applicationextension.inc.php',
'iProcess' => $baseDir . '/core/backgroundprocess.inc.php',
'iQueryModifier' => $baseDir . '/core/querymodifier.class.inc.php',
'iRestServiceProvider' => $baseDir . '/application/applicationextension.inc.php',

View File

@@ -367,7 +367,6 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b
'CryptEngine' => __DIR__ . '/../..' . '/core/simplecrypt.class.inc.php',
'CustomFieldsHandler' => __DIR__ . '/../..' . '/core/customfieldshandler.class.inc.php',
'DBObject' => __DIR__ . '/../..' . '/core/dbobject.class.php',
'DBObjectSearch' => __DIR__ . '/../..' . '/core/dbobjectsearch.class.php',
'DBObjectSet' => __DIR__ . '/../..' . '/core/dbobjectset.class.php',
'DBObjectSetComparator' => __DIR__ . '/../..' . '/core/dbobjectset.class.php',
'DBProperty' => __DIR__ . '/../..' . '/core/dbproperty.class.inc.php',
@@ -601,8 +600,6 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b
'Psr\\SimpleCache\\InvalidArgumentException' => __DIR__ . '/..' . '/psr/simple-cache/src/InvalidArgumentException.php',
'QRcode' => __DIR__ . '/..' . '/combodo/tcpdf/include/barcodes/qrcode.php',
'Query' => __DIR__ . '/../..' . '/application/query.class.inc.php',
'QueryBuilderContext' => __DIR__ . '/../..' . '/core/querybuildercontext.class.inc.php',
'QueryBuilderExpressions' => __DIR__ . '/../..' . '/core/oql/expression.class.inc.php',
'QueryOQL' => __DIR__ . '/../..' . '/application/query.class.inc.php',
'QueryReflection' => __DIR__ . '/../..' . '/core/modelreflection.class.inc.php',
'QueryReflectionRuntime' => __DIR__ . '/../..' . '/core/modelreflection.class.inc.php',
@@ -626,6 +623,7 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b
'RuntimeDashboard' => __DIR__ . '/../..' . '/application/dashboard.class.inc.php',
'SQLExpression' => __DIR__ . '/../..' . '/core/oql/expression.class.inc.php',
'SQLObjectQuery' => __DIR__ . '/../..' . '/core/sqlobjectquery.class.inc.php',
'SQLObjectQueryBuilder' => __DIR__ . '/../..' . '/core/sqlobjectquerybuilder.class.inc.php',
'SQLQuery' => __DIR__ . '/../..' . '/core/sqlquery.class.inc.php',
'SQLUnionQuery' => __DIR__ . '/../..' . '/core/sqlunionquery.class.inc.php',
'ScalarExpression' => __DIR__ . '/../..' . '/core/oql/expression.class.inc.php',
@@ -2127,6 +2125,7 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b
'iPageUIExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
'iPopupMenuExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
'iPortalUIExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
'iPreferencesExtension' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',
'iProcess' => __DIR__ . '/../..' . '/core/backgroundprocess.inc.php',
'iQueryModifier' => __DIR__ . '/../..' . '/core/querymodifier.class.inc.php',
'iRestServiceProvider' => __DIR__ . '/../..' . '/application/applicationextension.inc.php',

View File

@@ -62,6 +62,7 @@ class ItopDataTestCase extends ItopTestCase
private $aCreatedObjects = array();
const USE_TRANSACTION = true;
const CREATE_TEST_ORG = true;
/**
* @throws Exception
@@ -82,7 +83,10 @@ class ItopDataTestCase extends ItopTestCase
{
CMDBSource::Query('START TRANSACTION');
}
$this->CreateTestOrganization();
if (static::CREATE_TEST_ORG)
{
$this->CreateTestOrganization();
}
}
/**

View File

@@ -61,4 +61,10 @@ class ItopTestCase extends TestCase
}
}
}
public function GetMicroTime()
{
list($usec, $sec) = explode(" ", microtime());
return ((float)$usec + (float)$sec);
}
}

76
test/build_test_oql.php Normal file
View File

@@ -0,0 +1,76 @@
<?php
// Copyright (c) 2010-2017 Combodo SARL
//
// This file is part of iTop.
//
// iTop is free software; you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// iTop is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with iTop. If not, see <http://www.gnu.org/licenses/>
//
/**
* Date: 06/10/2017
*/
require_once('../approot.inc.php');
require_once(APPROOT.'bootstrap.inc.php');
require_once(APPROOT.'application/startup.inc.php');
\LoginWebPage::DoLogin(true);
$sOQLFile = APPROOT.'log/oql_records.txt';
$sTestFile = APPROOT.'test/core/oql_records.php';
$oTestHandle = @fopen($sTestFile, "w");
@fwrite($oTestHandle, "<?php\n\n");
$aFoundOQLs = array();
$iCount = 0;
$iRead = 0;
$oOQLHandle = @fopen($sOQLFile, "r");
if ($oOQLHandle) {
while (($sBuffer = fgets($oOQLHandle)) !== false) {
$iRead++;
$aRecord = unserialize($sBuffer);
$sOQL = $aRecord['oql'];
$sChecksum = md5($sOQL);
if (isset($aFoundOQLs[$sChecksum])) { continue; }
$aFoundOQLs[$sChecksum] = true;
$iCount++;
$sOrderBy = 'unserialize(\''.serialize($aRecord['order_by']).'\')';
$sArgs = 'unserialize(\''.serialize($aRecord['args']).'\')';
$sAttToLoad = 'unserialize(\''.serialize($aRecord['att_to_load']).'\')';
$sExtendedDataSpec = 'unserialize(\''.serialize($aRecord['extended_data_spec']).'\')';
$iLimitCount = $aRecord['limit_count'];
$iLimitStart = $aRecord['limit_start'];
// $sOQL, $aOrderBy, $aArgs, $aAttToLoad, $aExtendedDataSpec, $iLimitCount, $iLimitStart
$sLine = "\$aData[\"SELECT $iCount\"] = array(\"$sOQL\", $sOrderBy, $sArgs, $sAttToLoad, $sExtendedDataSpec, $iLimitCount, $iLimitStart);\n";
@fwrite($oTestHandle, $sLine);
}
if (!feof($oOQLHandle)) {
echo "Erreur: fgets() a échoué\n";
}
@fclose($oOQLHandle);
}
@fwrite($oTestHandle, "\n");
@fclose($oTestHandle);
echo "File '$sTestFile' generated with $iCount entries (from $iRead captured OQL).\n";

488
test/core/OQLToSQLTest.php Normal file

File diff suppressed because one or more lines are too long