diff --git a/application/ui.extkeywidget.class.inc.php b/application/ui.extkeywidget.class.inc.php index 8d412b04d..cd4e007e3 100644 --- a/application/ui.extkeywidget.class.inc.php +++ b/application/ui.extkeywidget.class.inc.php @@ -142,7 +142,12 @@ class UIExtKeyWidget throw new Exception('Implementation: null value for allowed values definition'); } $oAllowedValues->SetShowObsoleteData(utils::ShowObsoleteData()); - if ($oAllowedValues->Count() < $iMaxComboLength) + // Don't automatically launch the search if the table is huge + $bDoSearch = !utils::IsHighCardinality($this->sTargetClass); + $sJSDoSearch = $bDoSearch ? 'true' : 'false'; + + // We just need to compare the number of entries with MaxComboLength, so no need to get the real count. + if ($oAllowedValues->Count($iMaxComboLength * 2) < $iMaxComboLength) { // Discrete list of values, use a SELECT or RADIO buttons depending on the config switch($sDisplayStyle) @@ -226,7 +231,7 @@ class UIExtKeyWidget } $oPage->add_ready_script( <<iId} = new ExtKeyWidget('{$this->iId}', '{$this->sTargetClass}', '$sFilter', '$sTitle', true, $sWizHelper, '{$this->sAttCode}', $sJSSearchMode); + oACWidget_{$this->iId} = new ExtKeyWidget('{$this->iId}', '{$this->sTargetClass}', '$sFilter', '$sTitle', true, $sWizHelper, '{$this->sAttCode}', $sJSSearchMode, $sJSDoSearch); oACWidget_{$this->iId}.emptyHtml = "

$sMessage

"; $('#$this->iId').bind('update', function() { oACWidget_{$this->iId}.Update(); } ); $('#$this->iId').bind('change', function() { $(this).trigger('extkeychange') } ); @@ -261,7 +266,7 @@ EOF $iFieldSize = isset($aArgs['iFieldSize']) ? $aArgs['iFieldSize'] : 20; //@@@ $this->oAttDef->GetMaxSize(); // the input for the auto-complete - $sHTMLValue .= "Count()."\" type=\"text\" id=\"label_$this->iId\" value=\"$sDisplayValue\"/>"; + $sHTMLValue .= "iId\" value=\"$sDisplayValue\"/>"; $sHTMLValue .= "iId}\" style=\"border:0;vertical-align:middle;cursor:pointer;\" src=\"../images/mini_search.gif?itopversion=".ITOP_VERSION."\" onClick=\"oACWidget_{$this->iId}.Search();\"/>"; // another hidden input to store & pass the object's Id @@ -271,7 +276,7 @@ EOF // Scripts to start the autocomplete and bind some events to it $oPage->add_ready_script( <<iId} = new ExtKeyWidget('{$this->iId}', '{$this->sTargetClass}', '$sFilter', '$sTitle', false, $sWizHelper, '{$this->sAttCode}', $sJSSearchMode); + oACWidget_{$this->iId} = new ExtKeyWidget('{$this->iId}', '{$this->sTargetClass}', '$sFilter', '$sTitle', false, $sWizHelper, '{$this->sAttCode}', $sJSSearchMode, $sJSDoSearch); oACWidget_{$this->iId}.emptyHtml = "

$sMessage

"; $('#label_$this->iId').autocomplete(GetAbsoluteUrlAppRoot()+'pages/ajax.render.php', { scroll:true, minChars:{$iMinChars}, autoFill:false, matchContains:true, mustMatch: true, keyHolder:'#{$this->iId}', extraParams:{operation:'ac_extkey', sTargetClass:'{$this->sTargetClass}',sFilter:'$sFilter',bSearchMode:$JSSearchMode, json: function() { return $sWizHelperJSON; } }}); $('#label_$this->iId').keyup(function() { if ($(this).val() == '') { $('#$this->iId').val(''); } } ); // Useful for search forms: empty value in the "label", means no value, immediatly ! @@ -338,7 +343,7 @@ EOF $aParams = array(); $oFilter = new DBObjectSearch($this->sTargetClass); } - $bOpen = MetaModel::GetConfig()->Get('legacy_search_drawer_open'); + $bOpen = MetaModel::GetConfig()->Get('legacy_search_drawer_open') || utils::IsHighCardinality($this->sTargetClass); $oFilter->SetModifierProperty('UserRightsGetSelectFilter', 'bSearchMode', $this->bSearchMode); $oBlock = new DisplayBlock($oFilter, 'search', false, $aParams); $sHTML .= $oBlock->GetDisplay($oPage, $this->iId, array('open' => $bOpen, 'currentId' => $this->iId)); @@ -408,14 +413,35 @@ EOF $iCurrentExtKeyId = (is_null($oObj) || $this->sAttCode === '') ? 0 : $oObj->Get($this->sAttCode); $oValuesSet = new ValueSetObjects($sFilter, 'friendlyname'); // Bypass GetName() to avoid the encoding by htmlentities + $iMax = 150; + $oValuesSet->SetLimit($iMax); + $oValuesSet->SetSort(false); $oValuesSet->SetModifierProperty('UserRightsGetSelectFilter', 'bSearchMode', $this->bSearchMode); - $aValues = $oValuesSet->GetValues(array('this' => $oObj, 'current_extkey_id' => $iCurrentExtKeyId), $sContains); - foreach($aValues as $sKey => $sFriendlyName) + + $aValuesEquals = $oValuesSet->GetValues(array('this' => $oObj, 'current_extkey_id' => $iCurrentExtKeyId), $sContains, 'equals_start_with'); + asort($aValuesEquals); + foreach($aValuesEquals as $sKey => $sFriendlyName) { $oP->add(trim($sFriendlyName)."\t".$sKey."\n"); + $iMax--; + } + if ($iMax <= 0) + { + return; + } + + $oValuesSet->SetLimit($iMax); + $aValuesContains = $oValuesSet->GetValues(array('this' => $oObj, 'current_extkey_id' => $iCurrentExtKeyId), $sContains, 'contains'); + asort($aValuesContains); + foreach($aValuesContains as $sKey => $sFriendlyName) + { + if (!isset($aValuesEquals[$sKey])) + { + $oP->add(trim($sFriendlyName)."\t".$sKey."\n"); + } } } - + /** * Get the display name of the selected object, to fill back the autocomplete */ diff --git a/application/utils.inc.php b/application/utils.inc.php index 229d41d4c..57c84a892 100644 --- a/application/utils.inc.php +++ b/application/utils.inc.php @@ -1840,4 +1840,17 @@ class utils } return $aCleanHeaders; } + + /** + * Check if the given class if configured as a high cardinality class. + * + * @param $sClass + * + * @return bool + */ + public static function IsHighCardinality($sClass) + { + $aHugeClasses = MetaModel::GetConfig()->Get('high_cardinality_classes'); + return in_array($sClass, $aHugeClasses); + } } diff --git a/core/config.class.inc.php b/core/config.class.inc.php index b01460142..e3516f5c2 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -970,6 +970,22 @@ class Config 'source_of_value' => '', 'show_in_conf_sample' => true, ), + 'optimize_requests_for_join_count' => array( + 'type' => 'bool', + 'description' => 'Optimize request joins to minimize the count (default is true, try to set it to false in case of performance issues)', + 'default' => true, + 'value' => true, + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), + 'high_cardinality_classes' => array( + 'type' => 'array', + 'description' => 'List of classes with high cardinality (force auto-complete mode)', + 'default' => array(), + 'value' => array(), + 'source_of_value' => '', + 'show_in_conf_sample' => true, + ), ); public function IsProperty($sPropCode) diff --git a/core/dbobjectsearch.class.php b/core/dbobjectsearch.class.php index 2122db3a8..71a635c54 100644 --- a/core/dbobjectsearch.class.php +++ b/core/dbobjectsearch.class.php @@ -1871,46 +1871,86 @@ class DBObjectSearch extends DBSearch } } - // 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) + $bRootFirst = MetaModel::GetConfig()->Get('optimize_requests_for_join_count'); + if ($bRootFirst) { - if (!MetaModel::HasTable($sSomeClass)) + // 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) { - 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))) + if (!MetaModel::HasTable($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); + 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->AddInnerJoin($oSelectParentTable, $sKeyField, MetaModel::DBGetKey($sSomeClass)); + $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)); + } } } diff --git a/core/dbobjectset.class.php b/core/dbobjectset.class.php index a262611b3..c36772a46 100644 --- a/core/dbobjectset.class.php +++ b/core/dbobjectset.class.php @@ -77,18 +77,20 @@ class DBObjectSet implements iDBObjectSetIterator * @var mysqli_result */ protected $m_oSQLResult; + protected $m_bSort; /** * Create a new set based on a Search definition. - * + * * @param DBSearch $oFilter The search filter defining the objects which are part of the set (multiple columns/objects per row are supported) - * @param hash $aOrderBy Array of '[.]attcode' => bAscending - * @param hash $aArgs Values to substitute for the search/query parameters (if any). Format: param_name => value + * @param array $aOrderBy Array of '[.]attcode' => bAscending + * @param array $aArgs Values to substitute for the search/query parameters (if any). Format: param_name => value * @param hash $aExtendedDataSpec * @param int $iLimitCount Maximum number of rows to load (i.e. equivalent to MySQL's LIMIT start, count) * @param int $iLimitStart Index of the first row to load (i.e. equivalent to MySQL's LIMIT start, count) + * @param bool $bSort if false no order by is done */ - public function __construct(DBSearch $oFilter, $aOrderBy = array(), $aArgs = array(), $aExtendedDataSpec = null, $iLimitCount = 0, $iLimitStart = 0) + public function __construct(DBSearch $oFilter, $aOrderBy = array(), $aArgs = array(), $aExtendedDataSpec = null, $iLimitCount = 0, $iLimitStart = 0, $bSort = true) { $this->m_oFilter = $oFilter->DeepClone(); $this->m_aAddedIds = array(); @@ -98,6 +100,7 @@ class DBObjectSet implements iDBObjectSetIterator $this->m_aExtendedDataSpec = $aExtendedDataSpec; $this->m_iLimitCount = $iLimitCount; $this->m_iLimitStart = $iLimitStart; + $this->m_bSort = $bSort; $this->m_iNumTotalDBRows = null; $this->m_iNumLoadedDBRows = 0; @@ -601,10 +604,15 @@ class DBObjectSet implements iDBObjectSetIterator * * Limitation: the sort order has no effect on objects added in-memory * - * @return hash Format: field_code => boolean (true = ascending, false = descending) + * @return array Format: field_code => boolean (true = ascending, false = descending) */ public function GetRealSortOrder() { + if (!$this->m_bSort) + { + // No order by + return array(); + } // Get the class default sort order if not specified with the API // if (empty($this->m_aOrderBy)) @@ -702,13 +710,19 @@ class DBObjectSet implements iDBObjectSetIterator * May actually perform the SQL query SELECT COUNT... if the set was not previously loaded, or loaded with a * SetLimit * + * @param int $iLimit used for autocomplete: the count is only used to know if the number of entries exceed + * a certain amount or not + * * @return int The total number of rows for this set. + * @throws \CoreException + * @throws \MissingQueryArgument + * @throws \MySQLException */ - public function Count() + public function Count($iLimit = 0) { if (is_null($this->m_iNumTotalDBRows)) { - $sSQL = $this->m_oFilter->MakeSelectQuery(array(), $this->m_aArgs, null, null, 0, 0, true); + $sSQL = $this->m_oFilter->MakeSelectQuery(array(), $this->m_aArgs, null, null, $iLimit, 0, true); $resQuery = CMDBSource::Query($sSQL); if (!$resQuery) return 0; diff --git a/core/metamodel.class.php b/core/metamodel.class.php index a5b145878..c33f7db4c 100644 --- a/core/metamodel.class.php +++ b/core/metamodel.class.php @@ -422,13 +422,13 @@ abstract class MetaModel } /** - * Returns the friendly name IIF it is equivalent to a single attribute - */ + * Returns the friendly name IIF it is equivalent to a single attribute + */ final static public function GetFriendlyNameAttributeCode($sClass) { $aNameSpec = self::GetNameSpec($sClass); $sFormat = trim($aNameSpec[0]); - $aAttributes = $aNameSpec[1]; + $aAttributes = $aNameSpec[1]; if (($sFormat != '') && ($sFormat != '%1$s')) { return null; @@ -440,6 +440,20 @@ abstract class MetaModel return reset($aAttributes); } + /** + * Returns the list of attributes composing the friendlyname + * + * @param $sClass + * + * @return array + */ + final static public function GetFriendlyNameAttributeCodeList($sClass) + { + $aNameSpec = self::GetNameSpec($sClass); + $aAttributes = $aNameSpec[1]; + return $aAttributes; + } + final static public function GetStateAttributeCode($sClass) { self::_check_subclass($sClass); @@ -3806,6 +3820,7 @@ abstract class MetaModel // $aTableInfo = CMDBSource::GetTableInfo($sTable); $aTableInfo['Fields'][$sKeyField]['used'] = true; + $aFriendlynameAttcodes = self::GetFriendlyNameAttributeCodeList($sClass); foreach(self::ListAttributeDefs($sClass) as $sAttCode=>$oAttDef) { if (!$oAttDef->CopyOnAllTables()) @@ -3819,6 +3834,14 @@ abstract class MetaModel $aTableInfo['Fields'][$sField]['used'] = true; $bIndexNeeded = $oAttDef->RequiresIndex(); + if (!$bIndexNeeded) + { + // Add an index on the columns of the friendlyname + if (in_array($sField, $aFriendlynameAttcodes)) + { + $bIndexNeeded = true; + } + } $sFieldDefinition = "`$sField` $sDBFieldSpec"; if (!CMDBSource::IsField($sTable, $sField)) diff --git a/core/sqlobjectquery.class.inc.php b/core/sqlobjectquery.class.inc.php index 72290f0c0..a39a8045a 100644 --- a/core/sqlobjectquery.class.inc.php +++ b/core/sqlobjectquery.class.inc.php @@ -47,7 +47,7 @@ class SQLObjectQuery extends SQLQuery private $m_aValues = array(); // Values to set in case of an update query private $m_oSelectedIdField = null; private $m_aJoinSelects = array(); - private $m_bBeautifulQuery = false; + protected $m_bBeautifulQuery = false; // Data set by PrepareRendering() private $__aFrom; @@ -312,6 +312,14 @@ class SQLObjectQuery extends SQLQuery $this->PrepareRendering(); $sFrom = self::ClauseFrom($this->__aFrom, $sIndent); $sWhere = self::ClauseWhere($this->m_oConditionExpr, $aArgs); + if ($iLimitCount > 0) + { + $sLimit = 'LIMIT '.$iLimitStart.', '.$iLimitCount; + } + else + { + $sLimit = ''; + } if ($bGetCount) { if (count($this->__aSelectedIdFields) > 0) @@ -322,11 +330,13 @@ class SQLObjectQuery extends SQLQuery $aCountFields[] = "COALESCE($sFieldExpr, 0)"; // Null values are excluded from the count } $sCountFields = implode(', ', $aCountFields); - $sSQL = "SELECT$sLineSep COUNT(DISTINCT $sCountFields) AS COUNT$sLineSep FROM $sFrom$sLineSep WHERE $sWhere"; + // Count can be limited for performance reason, in this case the total amount is not important, + // we only need to know if the number of entries is greater than a certain amount. + $sSQL = "SELECT COUNT(*) AS COUNT FROM (SELECT$sLineSep DISTINCT $sCountFields $sLineSep FROM $sFrom$sLineSep WHERE $sWhere $sLimit) AS _tatooine_"; } else { - $sSQL = "SELECT$sLineSep COUNT(*) AS COUNT$sLineSep FROM $sFrom$sLineSep WHERE $sWhere"; + $sSQL = "SELECT COUNT(*) AS COUNT FROM (SELECT$sLineSep 1 $sLineSep FROM $sFrom$sLineSep WHERE $sWhere $sLimit) AS _tatooine_"; } } else @@ -337,14 +347,7 @@ class SQLObjectQuery extends SQLQuery { $sOrderBy = "ORDER BY $sOrderBy$sLineSep"; } - if ($iLimitCount > 0) - { - $sLimit = 'LIMIT '.$iLimitStart.', '.$iLimitCount; - } - else - { - $sLimit = ''; - } + $sSQL = "SELECT$sLineSep DISTINCT $sSelect$sLineSep FROM $sFrom$sLineSep WHERE $sWhere$sLineSep $sOrderBy $sLimit"; } return $sSQL; diff --git a/core/sqlquery.class.inc.php b/core/sqlquery.class.inc.php index 57b15e5bd..0fdd11ee3 100644 --- a/core/sqlquery.class.inc.php +++ b/core/sqlquery.class.inc.php @@ -39,7 +39,7 @@ require_once('cmdbsource.class.inc.php'); abstract class SQLQuery { private $m_SourceOQL = ''; - private $m_bBeautifulQuery = false; + protected $m_bBeautifulQuery = false; public function __construct() { diff --git a/core/sqlunionquery.class.inc.php b/core/sqlunionquery.class.inc.php index 238a823e9..8f42315e0 100644 --- a/core/sqlunionquery.class.inc.php +++ b/core/sqlunionquery.class.inc.php @@ -58,7 +58,7 @@ class SQLUnionQuery extends SQLQuery { $aQueriesHtml[] = '

'.$oSQLQuery->DisplayHtml().'

'; } - echo implode('UNION', $aQueries); + echo implode('UNION', $aQueriesHtml); } public function AddInnerJoin($oSQLQuery, $sLeftField, $sRightField, $sRightTable = '') @@ -85,7 +85,6 @@ class SQLUnionQuery extends SQLQuery { $this->m_bBeautifulQuery = $bBeautifulQuery; $sLineSep = $this->m_bBeautifulQuery ? "\n" : ''; - $sIndent = $this->m_bBeautifulQuery ? " " : null; $aSelects = array(); foreach ($this->aQueries as $oSQLQuery) @@ -93,36 +92,33 @@ class SQLUnionQuery extends SQLQuery // Render SELECTS without orderby/limit/count $aSelects[] = $oSQLQuery->RenderSelect(array(), $aArgs, 0, 0, false, $bBeautifulQuery); } - $sSelects = '('.implode(")$sLineSep UNION$sLineSep(", $aSelects).')'; - - if ($bGetCount) + if ($iLimitCount > 0) { - $sFrom = "($sLineSep$sSelects$sLineSep) as __selects__"; - $sSQL = "SELECT$sLineSep COUNT(*) AS COUNT$sLineSep FROM $sFrom$sLineSep"; + $sLimit = 'LIMIT '.$iLimitStart.', '.$iLimitCount; + } + else + { + $sLimit = ''; + } + + if ($bGetCount) + { + $sSelects = '('.implode(" $sLimit)$sLineSep UNION$sLineSep(", $aSelects)." $sLimit)"; + $sFrom = "($sLineSep$sSelects$sLineSep) as __selects__"; + $sSQL = "SELECT COUNT(*) AS COUNT FROM (SELECT$sLineSep 1 $sLineSep FROM $sFrom$sLineSep) AS _union_tatooine_"; } else { - $aSelects = array(); - foreach ($this->aQueries as $oSQLQuery) - { - // Render SELECT without orderby/limit/count - $aSelects[] = $oSQLQuery->RenderSelect(array(), $aArgs, 0, 0, false, $bBeautifulQuery); - } - $sSelect = $this->aQueries[0]->RenderSelectClause(); $sOrderBy = $this->aQueries[0]->RenderOrderByClause($aOrderBy); if (!empty($sOrderBy)) { - $sOrderBy = "ORDER BY $sOrderBy$sLineSep"; - } - if ($iLimitCount > 0) - { - $sLimit = 'LIMIT '.$iLimitStart.', '.$iLimitCount; + $sOrderBy = "ORDER BY $sOrderBy$sLineSep $sLimit"; + $sSQL = '('.implode(")$sLineSep UNION$sLineSep (", $aSelects).')'.$sLineSep.$sOrderBy; } else { - $sLimit = ''; + $sSQL = '('.implode(" $sLimit)$sLineSep UNION$sLineSep (", $aSelects)." $sLimit)"; } - $sSQL = $sSelects.$sLineSep.$sOrderBy.' '.$sLimit; } return $sSQL; } diff --git a/core/valuesetdef.class.inc.php b/core/valuesetdef.class.inc.php index e9846f77d..33822429f 100644 --- a/core/valuesetdef.class.inc.php +++ b/core/valuesetdef.class.inc.php @@ -52,7 +52,7 @@ abstract class ValueSetDefinition } - public function GetValues($aArgs, $sContains = '') + public function GetValues($aArgs, $sContains = '', $sOperation = 'contains') { if (!$this->m_bIsLoaded) { @@ -93,12 +93,16 @@ abstract class ValueSetDefinition class ValueSetObjects extends ValueSetDefinition { protected $m_sContains; + protected $m_sOperation; protected $m_sFilterExpr; // in OQL protected $m_sValueAttCode; protected $m_aOrderBy; protected $m_aExtraConditions; private $m_bAllowAllData; private $m_aModifierProperties; + private $m_bSort; + private $m_iLimit; + /** * @param hash $aOrderBy Array of '[.]attcode' => bAscending @@ -106,12 +110,15 @@ class ValueSetObjects extends ValueSetDefinition public function __construct($sFilterExp, $sValueAttCode = '', $aOrderBy = array(), $bAllowAllData = false, $aModifierProperties = array()) { $this->m_sContains = ''; + $this->m_sOperation = ''; $this->m_sFilterExpr = $sFilterExp; $this->m_sValueAttCode = $sValueAttCode; $this->m_aOrderBy = $aOrderBy; $this->m_bAllowAllData = $bAllowAllData; $this->m_aModifierProperties = $aModifierProperties; $this->m_aExtraConditions = array(); + $this->m_bSort = true; + $this->m_iLimit = 0; } public function SetModifierProperty($sPluginClass, $sProperty, $value) @@ -163,11 +170,11 @@ class ValueSetObjects extends ValueSetDefinition return new DBObjectSet($oFilter, $this->m_aOrderBy, $aArgs); } - public function GetValues($aArgs, $sContains = '') + public function GetValues($aArgs, $sContains = '', $sOperation = 'contains') { - if (!$this->m_bIsLoaded || ($sContains != $this->m_sContains)) + if (!$this->m_bIsLoaded || ($sContains != $this->m_sContains) || ($sOperation != $this->m_sOperation)) { - $this->LoadValues($aArgs, $sContains); + $this->LoadValues($aArgs, $sContains, $sOperation); $this->m_bIsLoaded = true; } // The results are already filtered and sorted (on friendly name) @@ -175,9 +182,10 @@ class ValueSetObjects extends ValueSetDefinition return $aRet; } - protected function LoadValues($aArgs, $sContains = '') + protected function LoadValues($aArgs, $sContains = '', $sOperation = 'contains') { $this->m_sContains = $sContains; + $this->m_sOperation = $sOperation; $this->m_aValues = array(); @@ -202,12 +210,54 @@ class ValueSetObjects extends ValueSetDefinition } } - $oValueExpr = new ScalarExpression('%'.$sContains.'%'); - $oNameExpr = new FieldExpression('friendlyname', $oFilter->GetClassAlias()); - $oNewCondition = new BinaryExpression($oNameExpr, 'LIKE', $oValueExpr); - $oFilter->AddConditionExpression($oNewCondition); + switch ($sOperation) + { + case 'equals_start_with': + $aAttributes = MetaModel::GetFriendlyNameAttributeCodeList($oFilter->GetClass()); + $sClassAlias = $oFilter->GetClassAlias(); + $aFilters = array(); + // Equals first + $oValueExpr = new ScalarExpression($sContains); + foreach($aAttributes as $sAttribute) + { + $oNewFilter = $oFilter->DeepClone(); + $oNameExpr = new FieldExpression($sAttribute, $sClassAlias); + $oCondition = new BinaryExpression($oNameExpr, 'LIKE', $oValueExpr); + $oNewFilter->AddConditionExpression($oCondition); + $aFilters[] = $oNewFilter; + } + // start with next + $oValueExpr = new ScalarExpression($sContains.'%'); + foreach($aAttributes as $sAttribute) + { + $oNewFilter = $oFilter->DeepClone(); + $oNameExpr = new FieldExpression($sAttribute, $sClassAlias); + $oCondition = new BinaryExpression($oNameExpr, 'LIKE', $oValueExpr); + $oNewFilter->AddConditionExpression($oCondition); + $aFilters[] = $oNewFilter; + } + // Unions are much faster than OR conditions + $oFilter = new DBUnionSearch($aFilters); + break; - $oObjects = new DBObjectSet($oFilter, $this->m_aOrderBy, $aArgs); + default: + $oValueExpr = new ScalarExpression('%'.$sContains.'%'); + $oNameExpr = new FieldExpression('friendlyname', $oFilter->GetClassAlias()); + $oNewCondition = new BinaryExpression($oNameExpr, 'LIKE', $oValueExpr); + $oFilter->AddConditionExpression($oNewCondition); + break; + } + + $oObjects = new DBObjectSet($oFilter, $this->m_aOrderBy, $aArgs, null, $this->m_iLimit, 0, $this->m_bSort); + if (empty($this->m_sValueAttCode)) + { + $aAttToLoad = array($oFilter->GetClassAlias() => array('friendlyname')); + } + else + { + $aAttToLoad = array($oFilter->GetClassAlias() => array($this->m_sValueAttCode)); + } + $oObjects->OptimizeColumnLoad($aAttToLoad); while ($oObject = $oObjects->Fetch()) { if (empty($this->m_sValueAttCode)) @@ -231,6 +281,22 @@ class ValueSetObjects extends ValueSetDefinition { return $this->m_sFilterExpr; } + + /** + * @param $iLimit + */ + public function SetLimit($iLimit) + { + $this->m_iLimit = $iLimit; + } + + /** + * @param $bSort + */ + public function SetSort($bSort) + { + $this->m_bSort = $bSort; + } } diff --git a/js/extkeywidget.js b/js/extkeywidget.js index c0c9bf9ac..51066363f 100644 --- a/js/extkeywidget.js +++ b/js/extkeywidget.js @@ -15,7 +15,7 @@ // You should have received a copy of the GNU Affero General Public License // along with iTop. If not, see -function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper, sAttCode, bSearchMode) +function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper, sAttCode, bSearchMode, bDoSearch) { this.id = id; this.sOriginalTargetClass = sTargetClass; @@ -29,6 +29,7 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper this.ajax_request = null; this.bSelectMode = bSelectMode; // true if the edited field is a SELECT, false if it's an autocomplete this.bSearchMode = bSearchMode; // true if selecting a value in the context of a search form + this.bDoSearch = bDoSearch; // false if the search is not launched var me = this; this.Init = function() @@ -95,7 +96,10 @@ function ExtKeyWidget(id, sTargetClass, sFilter, sTitle, bSelectMode, oWizHelper me.UpdateButtons(); me.ajax_request = null; FixSearchFormsDisposition(); - me.DoSearchObjects(); + if (me.bDoSearch) + { + me.DoSearchObjects(); + } }, 'html' );