diff --git a/core/lib/Drupal/Core/Entity/Query/ConditionAggregateBase.php b/core/lib/Drupal/Core/Entity/Query/ConditionAggregateBase.php new file mode 100644 index 0000000..ce9f1c7 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Query/ConditionAggregateBase.php @@ -0,0 +1,27 @@ +conditions[] = array( + 'field' => $field, + 'function' => $function, + 'value' => $value, + 'operator' => $operator, + 'langcode' => $langcode, + ); + + return $this; + } + +} diff --git a/core/lib/Drupal/Core/Entity/Query/ConditionInterface.php b/core/lib/Drupal/Core/Entity/Query/ConditionAggregateInterface.php similarity index 70% copy from core/lib/Drupal/Core/Entity/Query/ConditionInterface.php copy to core/lib/Drupal/Core/Entity/Query/ConditionAggregateInterface.php index d55cced..b3eb64b 100644 --- a/core/lib/Drupal/Core/Entity/Query/ConditionInterface.php +++ b/core/lib/Drupal/Core/Entity/Query/ConditionAggregateInterface.php @@ -2,15 +2,15 @@ /** * @file - * Contains \Drupal\Core\Entity\ConditionInterface. + * Contains \Drupal\Core\Entity\Query\ConditionAggregateInterface. */ namespace Drupal\Core\Entity\Query; /** - * Defines the entity query condition interface. + * Defines aggregated entity query conditions. */ -interface ConditionInterface { +interface ConditionAggregateInterface { /** * Gets the current conjunction. @@ -32,14 +32,17 @@ public function count(); /** * Adds a condition. * - * @param string $field + * @param string|ConditionAggregateInterface $field + * @param string $function * @param mixed $value * @param string $operator * @param string $langcode - * @return ConditionInterface + * + * @return \Drupal\Core\Entity\Query\ConditionAggregateInterface + * The called object. * @see \Drupal\Core\Entity\Query\QueryInterface::condition() */ - public function condition($field, $value = NULL, $operator = NULL, $langcode = NULL); + public function condition($field, $function = NULL, $value = NULL, $operator = NULL, $langcode = NULL); /** * Queries for the existence of a field. @@ -49,16 +52,16 @@ public function condition($field, $value = NULL, $operator = NULL, $langcode = N * @return ConditionInterface * @see \Drupal\Core\Entity\Query\QueryInterface::exists() */ - public function exists($field, $langcode = NULL); + public function exists($field, $function, $langcode = NULL); /** - * Queries for the existence of a field. + * Queries for the nonexistence of a field. * * @param string $field * @return ConditionInterface; * @see \Drupal\Core\Entity\Query\QueryInterface::notexists() */ - public function notExists($field, $langcode = NULL); + public function notExists($field, $function, $langcode = NULL); /** * Gets a complete list of all conditions in this conditional clause. @@ -77,4 +80,5 @@ public function &conditions(); * The query object this conditional clause belongs to. */ public function compile($query); + } diff --git a/core/lib/Drupal/Core/Entity/Query/ConditionBase.php b/core/lib/Drupal/Core/Entity/Query/ConditionBase.php index 2c428a4..4ef756b 100644 --- a/core/lib/Drupal/Core/Entity/Query/ConditionBase.php +++ b/core/lib/Drupal/Core/Entity/Query/ConditionBase.php @@ -7,41 +7,8 @@ namespace Drupal\Core\Entity\Query; -/** - * Common code for all implementations of the entity query condition interface. - */ -abstract class ConditionBase implements ConditionInterface { - /** - * Array of conditions. - * - * @var array - */ - protected $conditions = array(); - - /** - * Constructs a Condition object. - * - * @param string $conjunction - * The operator to use to combine conditions: 'AND' or 'OR'. - */ - public function __construct($conjunction = 'AND') { - $this->conjunction = $conjunction; - } - - /** - * Implements \Drupal\Core\Entity\Query\ConditionInterface::getConjunction(). - */ - public function getConjunction() { - return $this->conjunction; - } - - /** - * Implements \Countable::count(). - */ - public function count() { - return count($this->conditions) - 1; - } +abstract class ConditionBase extends ConditionFundamentals implements ConditionInterface { /** * Implements \Drupal\Core\Entity\Query\ConditionInterface::compile(). @@ -56,23 +23,4 @@ public function condition($field, $value = NULL, $operator = NULL, $langcode = N return $this; } - - /** - * Implements \Drupal\Core\Entity\Query\ConditionInterface::conditions(). - */ - public function &conditions() { - return $this->conditions; - } - - /** - * Makes sure condition groups are cloned as well. - */ - function __clone() { - foreach ($this->conditions as $key => $condition) { - if ($condition['field'] instanceOf ConditionInterface) { - $this->conditions[$key]['field'] = clone($condition['field']); - } - } - } - -} +} \ No newline at end of file diff --git a/core/lib/Drupal/Core/Entity/Query/ConditionBase.php b/core/lib/Drupal/Core/Entity/Query/ConditionFundamentals.php similarity index 70% copy from core/lib/Drupal/Core/Entity/Query/ConditionBase.php copy to core/lib/Drupal/Core/Entity/Query/ConditionFundamentals.php index 2c428a4..90cb2a0 100644 --- a/core/lib/Drupal/Core/Entity/Query/ConditionBase.php +++ b/core/lib/Drupal/Core/Entity/Query/ConditionFundamentals.php @@ -2,15 +2,15 @@ /** * @file - * Contains \Drupal\Core\Entity\Query\ConditionBase. + * Contains \Drupal\Core\Entity\Query\ConditionFundamentals. */ namespace Drupal\Core\Entity\Query; /** - * Common code for all implementations of the entity query condition interface. + * Common code for all implementations of the entity query condition interfaces. */ -abstract class ConditionBase implements ConditionInterface { +abstract class ConditionFundamentals { /** * Array of conditions. @@ -44,20 +44,6 @@ public function count() { } /** - * Implements \Drupal\Core\Entity\Query\ConditionInterface::compile(). - */ - public function condition($field, $value = NULL, $operator = NULL, $langcode = NULL) { - $this->conditions[] = array( - 'field' => $field, - 'value' => $value, - 'operator' => $operator, - 'langcode' => $langcode, - ); - - return $this; - } - - /** * Implements \Drupal\Core\Entity\Query\ConditionInterface::conditions(). */ public function &conditions() { @@ -65,9 +51,11 @@ public function &conditions() { } /** + * Implements the magic __clone function. + * * Makes sure condition groups are cloned as well. */ - function __clone() { + public function __clone() { foreach ($this->conditions as $key => $condition) { if ($condition['field'] instanceOf ConditionInterface) { $this->conditions[$key]['field'] = clone($condition['field']); diff --git a/core/lib/Drupal/Core/Entity/Query/ConditionInterface.php b/core/lib/Drupal/Core/Entity/Query/ConditionInterface.php index d55cced..ab5bf82 100644 --- a/core/lib/Drupal/Core/Entity/Query/ConditionInterface.php +++ b/core/lib/Drupal/Core/Entity/Query/ConditionInterface.php @@ -32,7 +32,7 @@ public function count(); /** * Adds a condition. * - * @param string $field + * @param string|ConditionInterface $field * @param mixed $value * @param string $operator * @param string $langcode diff --git a/core/lib/Drupal/Core/Entity/Query/QueryAggregateInterface.php b/core/lib/Drupal/Core/Entity/Query/QueryAggregateInterface.php new file mode 100644 index 0000000..b23f0f6 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Query/QueryAggregateInterface.php @@ -0,0 +1,161 @@ +', '>', '>=', '<', '<=', 'STARTS_WITH', 'CONTAINS', + * 'ENDS_WITH': These operators expect $value to be a literal of the + * same type as the column. + * - 'IN', 'NOT IN': These operators expect $value to be an array of + * literals of the same type as the column. + * - 'BETWEEN': This operator expects $value to be an array of two literals + * of the same type as the column. + * + * @param string $langcode + * (optional) The language code. + * + * @return \Drupal\Core\Entity\Query\QueryAggregateInterface + * The called object. + * + * @see \Drupal\Core\Entity\Query\QueryInterface::condition(). + */ + public function conditionAggregate($field, $function = NULL, $value = NULL, $operator = '=', $langcode = NULL); + + /** + * Queries for the existence of a field. + * + * @param string $field + * The name of the field. + * @param string $function + * The aggregate function. This is only marked optional for interface + * compatibility, it is illegal to leave it out. + * @param $langcode + * (optional) The language code. + * + * @return \Drupal\Core\Entity\Query\QueryAggregateInterface + * The called object. + */ + public function exists($field, $function = NULL, $langcode = NULL); + + /** + * Queries for the nonexistence of a field. + * + * @param string $field. + * The name of a field. + * @param string $function + * The aggregate function. This is only marked optional for interface + * compatibility, it is illegal to leave it out. + * @param string $langcode + * (optional) The language code. + * + * @return \Drupal\Core\Entity\Query\QueryAggregateInterface + * The called object. + */ + public function notExists($field, $function = NULL, $langcode = NULL); + + /** + * Creates an object holding a group of conditions. + * + * See andConditionAggregateGroup() and orConditionAggregateGroup() for more. + * + * @param $conjunction + * - AND (default): this is the equivalent of andConditionAggregateGroup(). + * - OR: this is the equivalent of andConditionAggregateGroup(). + * + * return ConditionInterface + * An object holding a group of conditions. + */ + public function conditionAggregateGroupFactory($conjunction = 'AND'); + + /** + * Sorts by an aggregated value. + * + * @param $field + * The name of a field. + * @param string $function + * The aggregate function. This is only marked optional for interface + * compatibility, it is illegal to leave it out. + * @param string $direction + * The order of sorting, either DESC for descending of ASC for ascending. + * @param $langcode + * (optional) The language code. + * + * @return \Drupal\Core\Entity\Query\QueryAggregateInterface + * The called object. + */ + public function sortAggregate($field, $function, $direction = 'ASC', $langcode = NULL); + + /** + * Executes the aggregate query. + * + * @return array + * A list of result row arrays. Each result row contains the aggregate + * results as keys and also the groupBy columns as keys: + * @code + * $result = $query + * ->aggregate('nid', 'count') + * ->condition('status', 1) + * ->groupby('type') + * ->executeAggregate(); + * @endcode + * Will return: + * @code + * $result[0] = array('count_nid' => 3, 'type' => 'page'); + * $result[1] = array('count_nid' => 1, 'type' => 'poll'); + * $result[2] = array('count_nid' => 4, 'type' => 'story'); + * @endcode + */ + public function execute(); +} diff --git a/core/lib/Drupal/Core/Entity/Query/QueryBase.php b/core/lib/Drupal/Core/Entity/Query/QueryBase.php index 77dc07d..1197881 100644 --- a/core/lib/Drupal/Core/Entity/Query/QueryBase.php +++ b/core/lib/Drupal/Core/Entity/Query/QueryBase.php @@ -12,7 +12,7 @@ /** * The base entity query class. */ -abstract class QueryBase implements QueryInterface { +abstract class QueryBase { /** * The entity type this query runs against. @@ -22,7 +22,7 @@ protected $entityType; /** - * The sort data. + * The list of sorts. * * @var array */ @@ -43,6 +43,34 @@ protected $condition; /** + * The list of aggregate expressions. + * + * @var array + */ + protected $aggregate = array(); + + /** + * The list of columns to group on. + * + * @var array + */ + protected $groupBy = array(); + + /** + * Aggregate Conditions + * + * @var ConditionAggregateInterface + */ + protected $conditionAggregate; + + /** + * The list of sorts over the aggregate results. + * + * @var array + */ + protected $sortAggregate = array(); + + /** * The query range. * * @var array @@ -50,6 +78,20 @@ protected $range = array(); /** + * The query metadata for alter purposes. + * + * @var array + */ + protected $alterMetaData; + + /** + * The query tags. + * + * @var array + */ + protected $alterTags; + + /** * Whether access check is requested or not. Defaults to TRUE. * * @var bool @@ -81,6 +123,9 @@ public function __construct($entity_type, $conjunction) { $this->entityType = $entity_type; $this->conjunction = $conjunction; $this->condition = $this->conditionGroupFactory($conjunction); + if (method_exists($this, 'conditionAggregateGroupFactory')) { + $this->conditionAggregate = $this->conditionAggregateGroupFactory($conjunction); + } } /** @@ -142,8 +187,9 @@ public function orConditionGroup() { /** * Implements \Drupal\Core\Entity\Query\QueryInterface::sort(). */ - public function sort($property, $direction = 'ASC', $langcode = NULL) { - $this->sort[$property] = array( + public function sort($field, $direction = 'ASC', $langcode = NULL) { + $this->sort[] = array( + 'field' => $field, 'direction' => $direction, 'langcode' => $langcode, ); @@ -283,4 +329,80 @@ public function addMetaData($key, $object) { public function getMetaData($key) { return isset($this->alterMetaData[$key]) ? $this->alterMetaData[$key] : NULL; } + + /** + * Implements \Drupal\Core\Entity\Query\QueryAggregateInterface::aggregate() + */ + public function aggregate($field, $function, $langcode = NULL, &$alias = NULL) { + if (!isset($alias)) { + $alias = $this->getAlias($field, $function); + } + + $this->aggregate[$alias] = array( + 'field' => $field, + 'function' => $function, + 'alias' => $alias, + 'langcode' => $langcode, + ); + + return $this; + } + + /** + * Implements \Drupal\Core\Entity\Query\QueryAggregateInterface::conditionAggregate(). + */ + public function conditionAggregate($field, $function = NULL, $value = NULL, $operator = '=', $langcode = NULL) { + $this->aggregate($field, $function, $langcode); + $this->conditionAggregate->condition($field, $function, $value, $operator, $langcode); + + return $this; + } + + /** + * Implements \Drupal\Core\Entity\Query\QueryAggregateInterface::sortAggregate(). + */ + public function sortAggregate($field, $function, $direction = 'ASC', $langcode = NULL) { + $alias = $this->getAlias($field, $function); + + $this->sortAggregate[$alias] = array( + 'field' => $field, + 'function' => $function, + 'direction' => $direction, + 'langcode' => $langcode, + ); + $this->aggregate($field, $function, $langcode, $alias); + + return $this; + } + + /** + * Implements \Drupal\Core\Entity\Query\QueryAggregateInterface::execute(). + */ + public function groupBy($field, $langcode = NULL) { + $this->groupBy[] = array( + 'field' => $field, + 'langcode' => $langcode, + ); + + return $this; + } + + /** + * Implements \Drupal\Core\Entity\Query\QueryAggregateInterface::conditioNGroupFactory() + */ + abstract function conditionGroupFactory($conjunction = 'AND'); + + /** + * Helper generating an alias. + * + * @param string $field + * The + * + * @return string + * The alias for the field. + */ + protected function getAlias($field, $function) { + return strtolower($field . '_'. $function); + } + } diff --git a/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Condition.php b/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Condition.php index a0d6d6a..30ac520 100644 --- a/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Condition.php +++ b/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Condition.php @@ -56,6 +56,11 @@ public function notExists($field, $langcode = NULL) { return $this->condition($field, NULL, 'IS NULL', $langcode); } + /** + * Translates the string operators to SQL equivalents. + * + * @param array $condition + */ protected function translateCondition(&$condition) { switch ($condition['operator']) { case 'STARTS_WITH': diff --git a/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Condition.php b/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/ConditionAggregate.php similarity index 64% copy from core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Condition.php copy to core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/ConditionAggregate.php index a0d6d6a..fb8881a 100644 --- a/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Condition.php +++ b/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/ConditionAggregate.php @@ -2,17 +2,23 @@ /** * @file - * Definition of Drupal\field_sql_storage\Query\ConditionSql. + * Contains \Drupal\field_sql_storage\Query\ConditionAggregate. */ namespace Drupal\field_sql_storage\Entity; -use Drupal\Core\Entity\Query\ConditionBase; -use Drupal\Core\Entity\Query\ConditionInterface; use Drupal\Core\Database\Query\SelectInterface; -use Drupal\Core\Database\Query\Condition as SqlCondition; +use Drupal\Core\Entity\Query\ConditionAggregateBase; +use Drupal\Core\Entity\Query\ConditionAggregateInterface; -class Condition extends ConditionBase { +class ConditionAggregate extends ConditionAggregateBase { + + /** + * Conjuction. + * + * @var string + */ + protected $conjuction = ''; /** * Implements Drupal\Core\Entity\Query\ConditionInterface::compile(). @@ -26,8 +32,8 @@ public function compile($conditionContainer) { $sqlQuery = $conditionContainer instanceof SelectInterface ? $conditionContainer : $conditionContainer->sqlQuery; $tables = new Tables($sqlQuery); foreach ($this->conditions as $condition) { - if ($condition['field'] instanceOf ConditionInterface) { - $sqlCondition = new SqlCondition($condition['field']->getConjunction()); + if ($condition['field'] instanceOf ConditionAggregateInterface) { + $sqlCondition = new Condition($condition['field']->getConjunction()); // Add the SQL query to the object before calling this method again. $sqlCondition->sqlQuery = $sqlQuery; $condition['field']->compile($sqlCondition); @@ -37,7 +43,9 @@ public function compile($conditionContainer) { $type = strtoupper($this->conjunction) == 'OR' || $condition['operator'] == 'IS NULL' ? 'LEFT' : 'INNER'; $this->translateCondition($condition); $field = $tables->addField($condition['field'], $type, $condition['langcode']); - $conditionContainer->condition($field, $condition['value'], $condition['operator']); + $function = $condition['function']; + $placeholder = ':db_placeholder_' . $conditionContainer->nextPlaceholder(); + $conditionContainer->having("$function($field) {$condition['operator']} $placeholder", array($placeholder => $condition['value'])); } } } @@ -45,17 +53,22 @@ public function compile($conditionContainer) { /** * Implements Drupal\Core\Entity\Query\ConditionInterface::exists(). */ - public function exists($field, $langcode = NULL) { - return $this->condition($field, NULL, 'IS NOT NULL', $langcode); + public function exists($field, $function, $langcode = NULL) { + return $this->condition($field, $function, NULL, 'IS NOT NULL', $langcode); } /** * Implements Drupal\Core\Entity\Query\ConditionInterface::notExists(). */ - public function notExists($field, $langcode = NULL) { - return $this->condition($field, NULL, 'IS NULL', $langcode); + public function notExists($field, $function, $langcode = NULL) { + return $this->condition($field, $function, NULL, 'IS NULL', $langcode); } + /** + * Translates the string operators to SQL equivalents. + * + * @param array $condition + */ protected function translateCondition(&$condition) { switch ($condition['operator']) { case 'STARTS_WITH': diff --git a/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Query.php b/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Query.php index 41091a6..bbfe5d0 100644 --- a/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Query.php +++ b/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/Query.php @@ -9,11 +9,46 @@ use Drupal\Core\Entity\Query\QueryBase; use Drupal\Core\Entity\Query\QueryException; +use Drupal\Core\Entity\Query\QueryInterface; /** * The SQL storage entity query class. */ -class Query extends QueryBase { +class Query extends QueryBase implements QueryInterface { + + /** + * Contains the entity info for the entity type of that query. + * + * @var array + * + * @see \Drupal\Core\Entity\EntityManager + */ + protected $entityInfo; + + /** + * The build sql select query. + * + * @var \Drupal\Core\Database\Query\SelectInterface + */ + protected $sqlQuery; + + /** + * An array of fields keyed by the field alias. Each entry correlates to the + * arguments of \Drupal\Core\Database\Query\SelectInterface::addField(), so + * the first one is the table alias, the second one the field and the last + * one optional the field alias. + * + * @var array + */ + protected $sqlFields = array(); + + /** + * An array of strings added as to the group by, keyed by the string to avoid + * duplicates. + * + * @var array + */ + protected $sqlGroupBy = array(); /** * @var \Drupal\Core\Database\Connection @@ -45,94 +80,125 @@ public function conditionGroupFactory($conjunction = 'AND') { * Implements Drupal\Core\Entity\Query\QueryInterface::execute(). */ public function execute() { + return $this + ->prepare() + ->compile() + ->addSort() + ->finish() + ->result(); + } + + /** + * @return \Drupal\field_sql_storage\Entity\Query + * @throws \Drupal\Core\Entity\Query\QueryException + */ + protected function prepare() { $entity_type = $this->entityType; + // @todo change to a method call once http://drupal.org/node/1892462 is in. - $entity_info = entity_get_info($entity_type); - if (!isset($entity_info['base_table'])) { + $this->entityInfo = entity_get_info($entity_type); + if (!isset($this->entityInfo['base_table'])) { throw new QueryException("No base table, invalid query."); } - $base_table = $entity_info['base_table']; + $base_table = $this->entityInfo['base_table']; $simple_query = TRUE; - if (isset($entity_info['data_table'])) { + if (isset($this->entityInfo['data_table'])) { $simple_query = FALSE; } - $sqlQuery = $this->connection->select($base_table, 'base_table', array('conjunction' => $this->conjunction)); - $sqlQuery->addMetaData('entity_type', $entity_type); - $id_field = $entity_info['entity_keys']['id']; - $fields[$id_field] = TRUE; - if (empty($entity_info['entity_keys']['revision'])) { - // Add the key field for fetchAllKeyed(). When there is no revision - // support, this is the entity key. - $sqlQuery->addField('base_table', $entity_info['entity_keys']['id']); + $this->sqlQuery = $this->connection->select($base_table, 'base_table', array('conjunction' => $this->conjunction)); + $this->sqlQuery->addMetaData('entity_type', $entity_type); + $id_field = $this->entityInfo['entity_keys']['id']; + // Add the key field for fetchAllKeyed(). + if (empty($this->entityInfo['entity_keys']['revision'])) { + // When there is no revision support, the key field is the entity key. + $this->sqlFields["base_table.$id_field"] = array('base_table', $id_field); + // Now add the value column for fetchAllKeyed(). This is always the + // entity id. + $this->sqlFields["base_table.$id_field" . '_1'] = array('base_table', $id_field); } else { - // Add the key field for fetchAllKeyed(). When there is revision - // support, this is the revision key. - $revision_field = $entity_info['entity_keys']['revision']; - $fields[$revision_field] = TRUE; - $sqlQuery->addField('base_table', $revision_field); - } - // Now add the value column for fetchAllKeyed(). This is always the - // entity id. - $sqlQuery->addField('base_table', $id_field); + // When there is revision support, the key field is the revision key. + $revision_field = $this->entityInfo['entity_keys']['revision']; + $this->sqlFields["base_table.$revision_field"] = array('base_table', $revision_field); + // Now add the value column for fetchAllKeyed(). This is always the + // entity id. + $this->sqlFields["base_table.$id_field"] = array('base_table', $id_field); + } if ($this->accessCheck) { - $sqlQuery->addTag($entity_type . '_access'); + $this->sqlQuery->addTag($entity_type . '_access'); } - $sqlQuery->addTag('entity_query'); - $sqlQuery->addTag('entity_query_' . $this->entityType); + $this->sqlQuery->addTag('entity_query'); + $this->sqlQuery->addTag('entity_query_' . $this->entityType); // Add further tags added. if (isset($this->alterTags)) { foreach ($this->alterTags as $tag => $value) { - $sqlQuery->addTag($tag); + $this->sqlQuery->addTag($tag); } } // Add further metadata added. if (isset($this->alterMetaData)) { foreach ($this->alterMetaData as $key => $value) { - $sqlQuery->addMetaData($key, $value); + $this->sqlQuery->addMetaData($key, $value); } } // This now contains first the table containing entity properties and // last the entity base table. They might be the same. - $sqlQuery->addMetaData('age', $this->age); - $sqlQuery->addMetaData('simple_query', $simple_query); - $this->condition->compile($sqlQuery); + $this->sqlQuery->addMetaData('age', $this->age); + $this->sqlQuery->addMetaData('simple_query', $simple_query); + return $this; + } + + /** + * @return \Drupal\field_sql_storage\Entity\Query + */ + protected function compile() { + $this->condition->compile($this->sqlQuery); + return $this; + } + + /** + * @return \Drupal\field_sql_storage\Entity\Query + */ + protected function addSort() { if ($this->count) { - $this->sort = FALSE; + $this->sort = array(); } // Gather the SQL field aliases first to make sure every field table // necessary is added. This might change whether the query is simple or // not. See below for more on simple queries. $sort = array(); if ($this->sort) { - $tables = new Tables($sqlQuery); - foreach ($this->sort as $property => $data) { - $sort[$property] = isset($fields[$property]) ? $property : $tables->addField($property, 'LEFT', $data['langcode']); + foreach ($this->sort as $key => $data) { + $sort[$key] = $this->getSqlField($data['field'], $data['langcode']); } } + $simple_query = $this->isSimpleQuery(); // If the query is set up for paging either via pager or by range or a // count is requested, then the correct amount of rows returned is // important. If the entity has a data table or multiple value fields are // involved then each revision might appear in several rows and this needs // a significantly more complex query. - $simple_query = (!$this->pager && !$this->range && !$this->count) || $sqlQuery->getMetaData('simple_query'); if (!$simple_query) { // First, GROUP BY revision id (if it has been added) and entity id. // Now each group contains a single revision of an entity. - foreach (array_keys($fields) as $field) { - $sqlQuery->groupBy($field); + foreach ($this->sqlFields as $field) { + $group_by = "$field[0].$field[1]"; + $this->sqlGroupBy[$group_by] = $group_by; } } // Now we know whether this is a simple query or not, actually do the // sorting. - foreach ($sort as $property => $sql_alias) { - $direction = $this->sort[$property]['direction']; - if ($simple_query || isset($fields[$property])) { + foreach ($sort as $key => $sql_alias) { + $direction = $this->sort[$key]['direction']; + if ($simple_query || isset($this->sqlGroupBy[$sql_alias])) { // Simple queries, and the grouped columns of complicated queries // can be ordered normally, without the aggregation function. - $sqlQuery->orderBy($sql_alias, $direction); + $this->sqlQuery->orderBy($sql_alias, $direction); + if (!isset($this->sqlFields[$sql_alias])) { + $this->sqlFields[$sql_alias] = explode('.', $sql_alias); + } } else { // Order based on the smallest element of each group if the @@ -140,21 +206,76 @@ public function execute() { // if the direction is descending. $function = $direction == 'ASC' ? 'min' : 'max'; $expression = "$function($sql_alias)"; - $sqlQuery->addExpression($expression, "order_by_{$property}_$direction"); - $sqlQuery->orderBy($expression, $direction); + $this->sqlQuery->addExpression($expression); + $this->sqlQuery->orderBy($expression, $direction); } } + return $this; + } + + /** + * Finish the query by adding fields, GROUP BY and range. + * + * @return \Drupal\field_sql_storage\Entity\Query + */ + protected function finish() { $this->initializePager(); if ($this->range) { - $sqlQuery->range($this->range['start'], $this->range['length']); + $this->sqlQuery->range($this->range['start'], $this->range['length']); + } + foreach ($this->sqlGroupBy as $field) { + $this->sqlQuery->groupBy($field); + } + foreach ($this->sqlFields as $field) { + $this->sqlQuery->addField($field[0], $field[1], isset($field[2]) ? $field[2] : NULL); } + return $this; + } + + /** + * Actually create the result. + * + * @return int|array + */ + protected function result() { if ($this->count) { - return $sqlQuery->countQuery()->execute()->fetchField(); + return $this->sqlQuery->countQuery()->execute()->fetchField(); } // Return a keyed array of results. The key is either the revision_id or // the entity_id depending on whether the entity type supports revisions. // The value is always the entity id. - return $sqlQuery->execute()->fetchAllKeyed(); + return $this->sqlQuery->execute()->fetchAllKeyed(); + } + + protected function getSqlField($field, $langcode) { + if (!isset($this->tables)) { + $this->tables = new Tables($this->sqlQuery); + } + $base_property = "base_table.$field"; + if (isset($this->sqlFields[$base_property])) { + return $base_property; + } + else { + return $this->tables->addField($field, 'LEFT', $langcode); + } + } + + /** + * Whether the query requires GROUP BY and ORDER BY MIN/MAX. + * + * @return bool + */ + protected function isSimpleQuery() { + return (!$this->pager && !$this->range && !$this->count) || $this->sqlQuery->getMetaData('simple_query'); + } + + /** + * Zero out fields and GROUP BY when cloning. + */ + public function __clone() { + parent::__clone(); + $this->sqlFields = array(); + $this->sqlGroupBy = array(); } } diff --git a/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/QueryAggregate.php b/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/QueryAggregate.php new file mode 100644 index 0000000..662d193 --- /dev/null +++ b/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/QueryAggregate.php @@ -0,0 +1,192 @@ +entityManager = $entity_manager; + $this->connection = $connection; + } + + /** + * Implements \Drupal\Core\Entity\Query\QueryAggregateInterface::execute() + */ + public function execute() { + return $this + ->prepare() + ->addAggregate() + ->compile() + ->compileAggregate() + ->addGroupBy() + ->addSort() + ->addSortAggregate() + ->finish() + ->result(); + } + + public function prepare() { + parent::prepare(); + // Throw away the id fields. + $this->sqlFields = array(); + return $this; + } + + /** + * Implements \Drupal\Core\Entity\Query\QueryAggregateInterface::conditionAggregateGroupFactory() + */ + public function conditionAggregateGroupFactory($conjunction = 'AND') { + return new ConditionAggregate($conjunction); + } + + /** + * Implements \Drupal\Core\Entity\Query\QueryAggregateInterface::exists() + */ + public function exists($field, $function = NULL, $langcode = NULL) { + // @todo Implement that function. + return parent::exists($field, $langcode); + } + + /** + * Implements \Drupal\Core\Entity\Query\QueryAggregateInterface::exists() + */ + public function notExists($field, $function = NULL, $langcode = NULL) { + // @todo Implement that function. + return parent::notExists($field, $langcode); + } + + + /** + * @return \Drupal\field_sql_storage\Entity\Queryaggregate + */ + protected function addAggregate() { + if ($this->aggregate) { + foreach ($this->aggregate as $aggregate) { + $sql_field = $this->getSqlField($aggregate['field'], $aggregate['langcode']); + $this->sqlExpressions[$aggregate['alias']] = $aggregate['function'] . "($sql_field)"; + } + } + return $this; + } + + /** + * Build the aggregation conditions part of the query. + * + * @return \Drupal\field_sql_storage\Entity\QueryAggregate + * The called object. + */ + protected function compileAggregate() { + $this->conditionAggregate->compile($this->sqlQuery); + return $this; + } + + /** + * Add the groupby values to the actual query. + * + * @return \Drupal\field_sql_storage\Entity\QueryAggregate + * The called object. + */ + protected function addGroupBy() { + foreach ($this->groupBy as $group_by) { + $field = $group_by['field']; + $sql_field = $this->getSqlField($field, $group_by['langcode']); + $this->sqlGroupBy[$sql_field] = $sql_field; + list($table, $real_sql_field) = explode('.', $sql_field); + $this->sqlFields[$sql_field] = array($table, $real_sql_field, $this->createSqlAlias($field, $real_sql_field)); + } + + return $this; + } + + /** + * Build the aggregation sort part of the query. + * + * @return \Drupal\field_sql_storage\Entity\QueryAggregate + * The called object. + */ + protected function addSortAggregate() { + if(!$this->count) { + foreach ($this->sortAggregate as $alias => $sort) { + $this->sqlQuery->orderBy($this->sqlExpressions[$alias], $sort['direction']); + } + } + return $this; + } + + + /** + * + * @return \Drupal\field_sql_storage\Entity\QueryAggregate + * The called object. + */ + protected function finish() { + foreach ($this->sqlExpressions as $alias => $expression) { + $this->sqlQuery->addExpression($expression, $alias); + } + return parent::finish(); + } + + /** + * @param string $field + * The field as passed in by the caller. + * @param string $sql_field + * The sql field as returned by getSqlField. + * @return string + * The SQL alias expected in the return value. The dots in $sql_field are + * replaced with underscores and if a default fallback to .value happened, + * the _value is stripped. + */ + function createSqlAlias($field, $sql_field) { + $alias = str_replace('.', '_', $sql_field); + if (substr($alias, 0, 6) === 'field_' && substr($field, -6) !== '_value' && substr($alias, -6) === '_value') { + $alias = substr($alias, 0, -6); + } + return $alias; + } + + /** + * @return array|int + * Returns the aggregated result, or a number if it's a count query. + */ + protected function result() { + if ($this->count) { + return parent::result(); + } + $return = array(); + foreach ($this->sqlQuery->execute() as $row) { + $return[] = (array)$row; + } + return $return; + } + +} diff --git a/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/QueryFactory.php b/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/QueryFactory.php index d8b36ba..5ee132c 100644 --- a/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/QueryFactory.php +++ b/core/modules/field_sql_storage/lib/Drupal/field_sql_storage/Entity/QueryFactory.php @@ -2,23 +2,63 @@ /** * @file - * Definition of Drupal\field_sql_storage\Entity\QueryFactory. + * Contains \Drupal\field_sql_storage\Entity\QueryFactory. */ namespace Drupal\field_sql_storage\Entity; use Drupal\Core\Database\Connection; +use Drupal\Core\Entity\EntityManager; /** * Factory class creating entity query objects for the SQL backend. + * + * @see \Drupal\field_sql_storage\Entity\Query + * @see \Drupal\field_sql_storage\Entity\QueryAggregate */ class QueryFactory { + /** + * Constructs a QueryFactory object. + * + * @param \Drupal\Core\Database\Connection $connection + * The database connection used by the entity query. + */ function __construct(Connection $connection) { $this->connection = $connection; } + /** + * Constructs a entity query for a certain entity type. + * + * @param string $entity_type + * The entity type. + * @param string $conjunction + * - AND: all of the conditions on the query need to match. + * - OR: at least one of the conditions on the query need to match. + * + * @return \Drupal\field_sql_storage\Entity\Query + * The factored query. + */ function get($entity_type, $conjunction = 'AND') { return new Query($entity_type, $conjunction, $this->connection); } + + /** + * Constructs a entity aggregation query for a certain entity type. + * + * @param string $entity_type + * The entity type. + * @param string $conjunction + * - AND: all of the conditions on the query need to match. + * - OR: at least one of the conditions on the query need to match. + * + * @return \Drupal\field_sql_storage\Entity\QueryAggregate + * The factored aggregation query. + */ + function getAggregate($entity_type, $conjunction = 'AND') { + // @todo Do the same as in the config query patch. + return new QueryAggregate($entity_type, drupal_container()->get('plugin.manager.entity'), $conjunction, $this->connection); + } + } diff --git a/core/modules/system/lib/Drupal/system/Tests/Entity/EntityQueryAggregationTest.php b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityQueryAggregationTest.php new file mode 100644 index 0000000..47c1211 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Entity/EntityQueryAggregationTest.php @@ -0,0 +1,591 @@ + 'Entity Query aggregation', + 'description' => 'Tests the Entity Query Aggregation API', + 'group' => 'Entity API', + ); + } + + protected function setUp() { + parent::setUp(); + $this->installSchema('user', 'users'); + $this->installSchema('system', 'sequences'); + $this->installSchema('field', 'field_config'); + $this->installSchema('field', 'field_config_instance'); + $this->installSchema('entity_test', 'entity_test'); + + $this->entityStorageController = $this->container->get('plugin.manager.entity')->getStorageController('entity_test'); + $this->factory = drupal_container()->get('entity.query.field_sql_storage'); + + // Add some fieldapi fields to be used in the test. + + // @todo Maybe start from 0. + for ($i = 1; $i <= 2; $i++) { + $field = array( + 'field_name' => 'field_test_' . $i, + 'type' => 'number_integer', + 'cardinality' => 2, + ); + field_create_field($field); + $instance = array( + 'field_name' => $field['field_name'], + 'entity_type' => 'entity_test', + 'bundle' => 'entity_test', + ); + field_create_instance($instance); + } + + $entity = $this->entityStorageController->create(array( + 'id' => 1, + 'user_id' => 1, + 'field_test_1' => array(LANGUAGE_NOT_SPECIFIED => array(array('value' => 1))), + 'field_test_2' => array(LANGUAGE_NOT_SPECIFIED => array(array('value' => 2))), + )); + $entity->enforceIsNew(); + $entity->save(); + + $entity = $this->entityStorageController->create(array( + 'id' => 2, + 'user_id' => 2, + 'field_test_1' => array(LANGUAGE_NOT_SPECIFIED => array(array('value' => 1))), + 'field_test_2' => array(LANGUAGE_NOT_SPECIFIED => array(array('value' => 7))), + )); + $entity->enforceIsNew(); + $entity->save(); + $entity = $this->entityStorageController->create(array( + 'id' => 3, + 'user_id' => 2, + 'field_test_1' => array(LANGUAGE_NOT_SPECIFIED => array(array('value' => 2))), + 'field_test_2' => array(LANGUAGE_NOT_SPECIFIED => array(array('value' => 1))), + )); + $entity->enforceIsNew(); + $entity->save(); + $entity = $this->entityStorageController->create(array( + 'id' => 4, + 'user_id' => 2, + 'field_test_1' => array(LANGUAGE_NOT_SPECIFIED => array(array('value' => 2))), + 'field_test_2' => array(LANGUAGE_NOT_SPECIFIED => array(array('value' => 8))), + )); + $entity->enforceIsNew(); + $entity->save(); + $entity = $this->entityStorageController->create(array( + 'id' => 5, + 'user_id' => 3, + 'field_test_1' => array(LANGUAGE_NOT_SPECIFIED => array(array('value' => 2))), + 'field_test_2' => array(LANGUAGE_NOT_SPECIFIED => array(array('value' => 2))), + )); + $entity->enforceIsNew(); + $entity->save(); + $entity = $this->entityStorageController->create(array( + 'id' => 6, + 'user_id' => 3, + 'field_test_1' => array(LANGUAGE_NOT_SPECIFIED => array(array('value' => 3))), + 'field_test_2' => array(LANGUAGE_NOT_SPECIFIED => array(array('value' => 8))), + )); + $entity->enforceIsNew(); + $entity->save(); + + } + + /** + * Test aggregation support. + */ + public function testAggregation() { + // Apply just a simple groupby. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->groupBy('user_id') + ->execute(); + + $this->assertResults(array( + array('user_id' => 1), + array('user_id' => 2), + array('user_id' => 3), + )); + + $function_expected = array(); + $function_expected['count'] = array(array('id_count' => 6)); + $function_expected['min'] = array(array('id_min' => 1)); + $function_expected['max'] = array(array('id_max' => 6)); + $function_expected['sum'] = array(array('id_sum' => 21)); + $function_expected['avg'] = array(array('id_avg' => (21.0/6.0))); + + // Apply just a simple aggregation for different aggregation functions. + foreach ($function_expected as $aggregation_function => $expected) { + $this->queryResult = $this->factory->getAggregate('entity_test') + ->aggregate('id', $aggregation_function) + ->execute(); + $this->assertEqual($this->queryResult, $expected); + } + + // Apply aggregation and groupby on the same query. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->aggregate('id', 'COUNT') + ->groupBy('user_id') + ->execute(); + $this->assertResults(array( + array('user_id' => 1, 'id_count' => 1), + array('user_id' => 2, 'id_count' => 3), + array('user_id' => 3, 'id_count' => 2), + )); + + // Apply aggregation and a condition which matches. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->aggregate('id', 'COUNT') + ->conditionAggregate('id', 'COUNT', 8) + ->execute(); + $this->assertResults(array()); + + // Don't call aggregate to test the implicit aggregate call. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->conditionAggregate('id', 'COUNT', 8) + ->execute(); + $this->assertResults(array()); + + // Apply aggregation and a condition which matches. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->aggregate('id', 'count') + ->conditionAggregate('id', 'COUNT', 6) + ->execute(); + $this->assertResults(array(array('id_count' => 6))); + + // Apply aggregation, a groupby and a condition which matches partially via + // the operator '='. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->aggregate('id', 'count') + ->conditionAggregate('id', 'count', 2) + ->groupBy('user_id') + ->execute(); + $this->assertResults(array(array('id_count' => 2, 'user_id' => 3))); + + // Apply aggregation, a groupby and a condition which matches partially via + // the operator '>'. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->aggregate('id', 'count') + ->conditionAggregate('id', 'COUNT', 1, '>') + ->groupBy('user_id') + ->execute(); + $this->assertResults(array( + array('id_count' => 2, 'user_id' => 3), + array('id_count' => 3, 'user_id' => 2), + )); + + // Apply aggregation and a sort. This might not be useful, but have a proper + // test coverage. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->aggregate('id', 'COUNT') + ->sortAggregate('id', 'COUNT') + ->execute(); + $this->assertResults(array(array('id_count' => 6))); + + // Don't call aggregate to test the implicit aggregate call. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->sortAggregate('id', 'COUNT') + ->execute(); + $this->assertResults(array(array('id_count' => 6))); + + // Apply aggregation, groupby and a sort descending. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->aggregate('id', 'COUNT') + ->groupBy('user_id') + ->sortAggregate('id', 'COUNT', 'DESC') + ->execute(); + $this->assertResults(array( + array('user_id' => 2, 'id_count' => 3), + array('user_id' => 3, 'id_count' => 2), + array('user_id' => 1, 'id_count' => 1), + )); + + // Apply aggregation, groupby and a sort ascending. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->aggregate('id', 'COUNT') + ->groupBy('user_id') + ->sortAggregate('id', 'COUNT', 'ASC') + ->execute(); + $this->assertResults(array( + array('user_id' => 1, 'id_count' => 1), + array('user_id' => 3, 'id_count' => 2), + array('user_id' => 2, 'id_count' => 3), + )); + + // Apply aggregation, groupby, a aggregation condition and a sort with the + // operator '='. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->aggregate('id', 'COUNT') + ->groupBy('user_id') + ->sortAggregate('id', 'COUNT') + ->conditionAggregate('id', 'COUNT', 2) + ->execute(); + $this->assertResults(array(array('id_count' => 2, 'user_id' => 3))); + + // Apply aggregation, groupby, a aggregation condition and a sort with the + // operator '<' and order ASC. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->aggregate('id', 'COUNT') + ->groupBy('user_id') + ->sortAggregate('id', 'COUNT', 'ASC') + ->conditionAggregate('id', 'COUNT', 3, '<') + ->execute(); + $this->assertResults(array( + array('id_count' => 1, 'user_id' => 1), + array('id_count' => 2, 'user_id' => 3), + )); + + // Apply aggregation, groupby, a aggregation condition and a sort with the + // operator '<' and order DESC. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->aggregate('id', 'COUNT') + ->groupBy('user_id') + ->sortAggregate('id', 'COUNT', 'DESC') + ->conditionAggregate('id', 'COUNT', 3, '<') + ->execute(); + $this->assertResults(array( + array('id_count' => 2, 'user_id' => 3), + array('id_count' => 1, 'user_id' => 1), + )); + + // Test aggregation/groupby support for fieldapi fields. + + // Just group by a fieldapi field. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->groupBy('field_test_1') + ->execute(); + $this->assertResults(array( + array('field_test_1' => 1), + array('field_test_1' => 2), + array('field_test_1' => 3), + )); + + // Group by a fieldapi field and aggregate a normal property. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->aggregate('user_id', 'COUNT') + ->groupBy('field_test_1') + ->execute(); + + $this->assertResults(array( + array('field_test_1' => 1, 'user_id_count' => 2), + array('field_test_1' => 2, 'user_id_count' => 3), + array('field_test_1' => 3, 'user_id_count' => 1), + )); + + // Group by a normal property and aggregate a fieldapi field. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->aggregate('field_test_1', 'COUNT') + ->groupBy('user_id') + ->execute(); + + $this->assertResults(array( + array('user_id' => 1, 'field_test_1_count' => 1), + array('user_id' => 2, 'field_test_1_count' => 3), + array('user_id' => 3, 'field_test_1_count' => 2), + )); + + $this->queryResult = $this->factory->getAggregate('entity_test') + ->aggregate('field_test_1', 'SUM') + ->groupBy('user_id') + ->execute(); + $this->assertResults(array( + array('user_id' => 1, 'field_test_1_sum' => 1), + array('user_id' => 2, 'field_test_1_sum' => 5), + array('user_id' => 3, 'field_test_1_sum' => 5), + )); + + // Aggregate by two different fieldapi fields. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->aggregate('field_test_1', 'SUM') + ->aggregate('field_test_2', 'SUM') + ->groupBy('user_id') + ->execute(); + $this->assertResults(array( + array('user_id' => 1, 'field_test_1_sum' => 1, 'field_test_2_sum' => 2), + array('user_id' => 2, 'field_test_1_sum' => 5, 'field_test_2_sum' => 16), + array('user_id' => 3, 'field_test_1_sum' => 5, 'field_test_2_sum' => 10), + )); + + // This time aggregate the same field twice. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->aggregate('field_test_1', 'SUM') + ->aggregate('field_test_1', 'COUNT') + ->groupBy('user_id') + ->execute(); + $this->assertResults(array( + array('user_id' => 1, 'field_test_1_sum' => 1, 'field_test_1_count' => 1), + array('user_id' => 2, 'field_test_1_sum' => 5, 'field_test_1_count' => 3), + array('user_id' => 3, 'field_test_1_sum' => 5, 'field_test_1_count' => 2), + )); + + // Group by and aggregate by a fieldapi field. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->groupBy('field_test_1') + ->aggregate('field_test_2', 'COUNT') + ->execute(); + $this->assertResults(array( + array('field_test_1' => 1, 'field_test_2_count' => 2), + array('field_test_1' => 2, 'field_test_2_count' => 3), + array('field_test_1' => 3, 'field_test_2_count' => 1), + )); + + // Group by and aggregate by a fieldapi field and use multiple aggregate + // functions. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->groupBy('field_test_1') + ->aggregate('field_test_2', 'COUNT') + ->aggregate('field_test_2', 'SUM') + ->execute(); + $this->assertResults(array( + array('field_test_1' => 1, 'field_test_2_count' => 2, 'field_test_2_sum' => 9), + array('field_test_1' => 2, 'field_test_2_count' => 3, 'field_test_2_sum' => 11), + array('field_test_1' => 3, 'field_test_2_count' => 1, 'field_test_2_sum' => 8), + )); + + // Apply a aggregate condition for a fieldapi field and group by a simple + // property. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->conditionAggregate('field_test_1', 'COUNT', 3) + ->groupBy('user_id') + ->execute(); + $this->assertResults(array( + array('user_id' => 2, 'field_test_1_count' => 3), + array('user_id' => 3, 'field_test_1_count' => 2), + )); + + $this->queryResult = $this->factory->getAggregate('entity_test') + ->aggregate('field_test_1', 'SUM') + ->conditionAggregate('field_test_1', 'COUNT', 2, '>') + ->groupBy('user_id') + ->execute(); + $this->assertResults(array( + array('user_id' => 2, 'field_test_1_sum' => 5, 'field_test_1_count' => 3), + array('user_id' => 3, 'field_test_1_sum' => 5, 'field_test_1_count' => 2), + )); + + // Apply a aggregate condition for a simple property and a group by a + // fieldapi field. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->conditionAggregate('user_id', 'COUNT', 2) + ->groupBy('field_test_1') + ->execute(); + $this->assertResults(array( + array('field_test_1' => 1, 'user_id_count' => 2), + )); + + $this->queryResult = $this->factory->getAggregate('entity_test') + ->conditionAggregate('user_id', 'COUNT', 2, '>') + ->groupBy('field_test_1') + ->execute(); + $this->assertResults(array( + array('field_test_1' => 1, 'user_id_count' => 2), + array('field_test_1' => 2, 'user_id_count' => 3), + )); + + // Apply a aggregate condition and a group by fieldapi fields. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->groupBy('field_test_1') + ->conditionAggregate('field_test_2', 'COUNT', 2) + ->execute(); + $this->assertResults(array( + array('field_test_1' => 1, 'field_test_2_count' => 2), + )); + $this->queryResult = $this->factory->getAggregate('entity_test') + ->groupBy('field_test_1') + ->conditionAggregate('field_test_2', 'COUNT', 2, '>') + ->execute(); + $this->assertResults(array( + array('field_test_1' => 1, 'field_test_2_count' => 2), + array('field_test_1' => 2, 'field_test_2_count' => 3), + )); + + // Apply a aggregate condition and a group by fieldapi fields with multiple + // conditions via AND. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->groupBy('field_test_1') + ->conditionAggregate('field_test_2', 'COUNT', 2) + ->conditionAggregate('field_test_2', 'SUM', 8) + ->execute(); + $this->assertResults(array()); + + // Apply a aggregate condition and a group by fieldapi fields with multiple + // conditions via OR. + $this->queryResult = $this->factory->getAggregate('entity_test', 'OR') + ->groupBy('field_test_1') + ->conditionAggregate('field_test_2', 'COUNT', 2) + ->conditionAggregate('field_test_2', 'SUM', 8) + ->execute(); + $this->assertResults(array( + array('field_test_1' => 1, 'field_test_2_count' => 2, 'field_test_2_sum' => 9), + array('field_test_1' => 3, 'field_test_2_count' => 1, 'field_test_2_sum' => 8), + )); + + // Group by a normal property and aggregate a fieldapi field and sort by + // the groupby field. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->aggregate('field_test_1', 'COUNT') + ->groupBy('user_id') + ->sort('user_id', 'DESC') + ->execute(); + + $this->assertResults(array( + array('user_id' => 3, 'field_test_1_count' => 2), + array('user_id' => 2, 'field_test_1_count' => 3), + array('user_id' => 1, 'field_test_1_count' => 1), + )); + + $this->queryResult = $this->factory->getAggregate('entity_test') + ->aggregate('field_test_1', 'COUNT') + ->groupBy('user_id') + ->sort('user_id', 'ASC') + ->execute(); + + $this->assertResults(array( + array('user_id' => 1, 'field_test_1_count' => 1), + array('user_id' => 2, 'field_test_1_count' => 3), + array('user_id' => 3, 'field_test_1_count' => 2), + )); + + $this->queryResult = $this->factory->getAggregate('entity_test') + ->conditionAggregate('field_test_1', 'COUNT', 2, '>') + ->groupBy('user_id') + ->sort('user_id', 'ASC') + ->execute(); + + $this->assertResults(array( + array('user_id' => 2, 'field_test_1_count' => 3), + array('user_id' => 3, 'field_test_1_count' => 2), + )); + + // Group by a normal property and aggregate a fieldapi field and sort by + // the aggregated field. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->sortAggregate('field_test_1', 'COUNT', 'DESC') + ->groupBy('user_id') + ->execute(); + $this->assertResults(array( + array('user_id' => 2, 'field_test_1_count' => 3), + array('user_id' => 3, 'field_test_1_count' => 2), + array('user_id' => 1, 'field_test_1_count' => 1), + )); + + $this->queryResult = $this->factory->getAggregate('entity_test') + ->sortAggregate('field_test_1', 'COUNT', 'ASC') + ->groupBy('user_id') + ->execute(); + + $this->assertResults(array( + array('user_id' => 1, 'field_test_1_count' => 1), + array('user_id' => 3, 'field_test_1_count' => 2), + array('user_id' => 2, 'field_test_1_count' => 3), + )); + + // Group by and aggregate by fieldapi fields and sort by the groupby field. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->groupBy('field_test_1') + ->aggregate('field_test_2', 'COUNT') + ->sort('field_test_1', 'ASC') + ->execute(); + $this->assertResults(array( + array('field_test_1' => 1, 'field_test_2_count' => 2), + array('field_test_1' => 2, 'field_test_2_count' => 3), + array('field_test_1' => 3, 'field_test_2_count' => 1), + )); + + $this->queryResult = $this->factory->getAggregate('entity_test') + ->groupBy('field_test_1') + ->aggregate('field_test_2', 'COUNT') + ->sort('field_test_1', 'DESC') + ->execute(); + $this->assertResults(array( + array('field_test_1' => 3, 'field_test_2_count' => 1), + array('field_test_1' => 2, 'field_test_2_count' => 3), + array('field_test_1' => 1, 'field_test_2_count' => 2), + )); + + // Groupy and aggregate by fieldapi fields and sort by the aggregated field. + $this->queryResult = $this->factory->getAggregate('entity_test') + ->groupBy('field_test_1') + ->sortAggregate('field_test_2', 'COUNT', 'DESC') + ->execute(); + $this->assertResults(array( + array('field_test_1' => 2, 'field_test_2_count' => 3), + array('field_test_1' => 1, 'field_test_2_count' => 2), + array('field_test_1' => 3, 'field_test_2_count' => 1), + )); + + $this->queryResult = $this->factory->getAggregate('entity_test') + ->groupBy('field_test_1') + ->sortAggregate('field_test_2', 'COUNT', 'DESC') + ->execute(); + $this->assertResults(array( + array('field_test_1' => 3, 'field_test_2_count' => 1), + array('field_test_1' => 1, 'field_test_2_count' => 2), + array('field_test_1' => 2, 'field_test_2_count' => 3), + )); + + } + + public function assertResults($expected) { + if (empty($expected)) { + $this->assertTrue(empty($this->queryResult)); + return; + } + $found = TRUE; + foreach ($this->queryResult as $row) { + foreach ($expected as $expected_row) { + if (!array_diff_assoc($expected_row, $row) && !array_diff_assoc($row, $expected_row)) { + continue 2; + } + } + $found = FALSE; + break; + } + $this->assertTrue($found, strtr('!expected expected, !found found', array('!expected' => print_r($expected, TRUE), '!found' => print_r($this->queryResult, TRUE)))); + } +} +