=== modified file 'includes/entity.inc' --- includes/entity.inc 2010-05-09 13:27:31 +0000 +++ includes/entity.inc 2010-06-08 06:51:42 +0000 @@ -290,3 +290,662 @@ class DrupalDefaultEntityController impl $this->entityCache += $entities; } } + +/** + * Exception thrown by EntityFieldQuery() on unsupported query syntax. + * + * Some storage modules might not support the full range of the syntax for + * conditions, and will raise an EntityFieldQueryException when an unsupported + * condition was specified. + */ +class EntityFieldQueryException extends Exception {} + +/** + * Retrieves entities matching a given set of conditions. + * + * This class allows finding entities based on entity properties (for example, + * node->changed), field values, and generic entity meta data (bundle, + * entity type, entity id, and revision ID). It is not possible to query across + * multiple entity types. For example, there is no facility to find published + * nodes written by users created in the last hour, as this would require + * querying both node->status and user->created. + * + * Normally we would not want to have public properties on the object, as that + * allows the object's state to become inconsistent too easily. However, this + * class's standard use case involves primarily code that does need to have + * direct access to the collected properties in order to handle alternate + * execution routines. We therefore use public properties for simplicity. Note + * that code that is simply creating and running a field query should still use + * the appropriate methods add conditions on the query. + * + * Storage engines are not required to support every type of query. By default, + * an EntityFieldQueryException will be raised if an unsupported condition is + * specified or if the query has field conditions or sorts that are stored in + * different field storage engines. However, this logic can be overridden in + * hook_entity_query(). + */ +class EntityFieldQuery { + /** + * Return everything regardless they are deleted or not. + * + * @see EntityFieldQuery::deleted() + */ + const RETURN_ALL = NULL; + + /** + * Associative array of entity-generic metadata conditions. + * + * @var array + * + * @see EntityFieldQuery::entityCondition() + */ + public $entityConditions = array(); + + /** + * List of field conditions. + * + * @var array + * + * @see EntityFieldQuery::fieldCondition() + */ + public $fieldConditions = array(); + + /** + * List of property conditions. + * + * @var array + * + * @see EntityFieldQuery::propertyCondition() + */ + public $propertyConditions = array(); + + /** + * List of order clauses for entity-generic metadata. + * + * @var array + * + * @see EntityFieldQuery::entityOrderBy() + */ + public $entityOrder = array(); + + /** + * List of order clauses for fields. + * + * @var array + * + * @see EntityFieldQuery::fieldOrderBy() + */ + public $fieldOrder = array(); + + /** + * List of order clauses for entities. + * + * @var array + * + * @see EntityFieldQuery::entityOrderBy() + */ + public $propertyOrder = array(); + + /** + * The query range. + * + * @var array + * + * @see EntityFieldQuery::range() + */ + public $range = array(); + + /** + * Query behavior for deleted data. + * + * TRUE to return only deleted data, FALSE to return only non-deleted data, + * NULL to return everything. + * + * @see EntityFieldQuery::deleted() + */ + public $deleted = FALSE; + + /** + * A list of field arrays used. + * + * Field names passed to EntityFieldQuery::fieldCondition() and + * EntityFieldQuery::fieldOrderBy() are run through field_info_field() before + * stored in this array. This way, the elements of this array are field + * arrays. + * + * @var array + */ + public $fields = array(); + + /** + * TRUE if this is a count query, FALSE if it isn't. + * + * @var boolean + */ + public $count = FALSE; + + /** + * Flag indicating whether this is querying current or all revisions. + * + * @var int + * + * @see EntityFieldQuery::age() + */ + public $age = FIELD_LOAD_CURRENT; + + /** + * The ordered results. + * + * @var array + * + * @see EntityFieldQuery::execute(). + */ + public $orderedResults = array(); + + /** + * The method executing the query, if it is overriding the default. + * + * @var string + * + * @see EntityFieldQuery::execute(). + */ + public $executeCallback = ''; + + /** + * Adds a condition on entity-generic metadata. + * + * If the overall query contains only entity conditions or ordering, or if + * there are property conditions, then specifying the entity type is + * mandatory. If there are field conditions or ordering but no property + * conditions or ordering, then specifying an entity type is optional. While + * the field storage engine might support field conditions on more than one + * entity type, there is no way to query across multiple entity base tables by + * default. To specify the entity type, pass in 'entity_type' for $name, + * the type as a string for $value, and no $operator (it's disregarded). + * + * 'bundle', 'revision_id' and 'entity_id' have no such restrictions. + * + * @param $name + * 'entity_type', 'bundle', 'revision_id' or 'entity_id'. + * @param $value + * The value for $name. In most cases, this is a scalar. For more complex + * options, it is an array. The meaning of each element in the array is + * dependent on $operator. + * @param $operator + * Possible values: + * - '=', '!=', '>', '>=', '<', '<=', 'STARTS_WITH', 'CONTAINS': 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. + * + * @return EntityFieldQuery + * The called object. + */ + public function entityCondition($name, $value, $operator = NULL) { + $this->entityConditions[$name] = array( + 'value' => $value, + 'operator' => $operator, + ); + return $this; + } + + /** + * Adds a condition on field values. + * + * @param $field + * Either a field name or a field array. + * @param $column + * A column defined in the hook_field_schema() of this field. If this is + * omitted then the query will find only entities that have data in this + * field, using the entity and property conditions if there are any. + * @param $value + * The value to test the column value against. In most cases, this is a + * scalar. For more complex options, it is an array. The meaning of each + * element in the array is dependent on $operator. + * @param $operator + * Possible values: + * - '=', '!=', '>', '>=', '<', '<=', 'STARTS_WITH', 'CONTAINS': 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 $delta_group + * An arbitrary identifier: conditions in the same group must have the same + * $delta_group. For example, let's presume a multivalue field which has + * two columns, 'color' and 'shape', and for entity id 1, there are two + * values: red/square and blue/circle. Entity ID 1 does not have values + * corresponding to 'red circle', however if you pass 'red' and 'circle' as + * conditions, it will appear in the results - by default queries will run + * against any combination of deltas. By passing the conditions with the + * same $delta_group it will ensure that only values attached to the same + * delta are matched, and entity 1 would then be excluded from the results. + * @param $language_group + * An arbitrary identifier: conditions in the same group must have the same + * $language_group. + * + * @return EntityFieldQuery + * The called object. + */ + public function fieldCondition($field, $column = NULL, $value = NULL, $operator = NULL, $delta_group = NULL, $language_group = NULL) { + if (is_scalar($field)) { + $field = field_info_field($field); + } + // Ensure the same index is used for fieldConditions as for fields. + $index = count($this->fields); + $this->fields[$index] = $field; + if (isset($column)) { + $this->fieldConditions[$index] = array( + 'field' => $field, + 'column' => $column, + 'value' => $value, + 'operator' => $operator, + 'delta_group' => $delta_group, + 'language_group' => $language_group, + ); + } + return $this; + } + + /** + * Adds a condition on an entity-specific property. + * + * An $entity_type must be specified by calling + * EntityFieldCondition::entityCondition('entity_type', $entity_type) before + * executing the query. Also, by default only entities stored in SQL are + * supported; however, EntityFieldQuery::executeCallback can be set to handle + * different entity storage. + * + * @param $column + * A column defined in the hook_schema() of the base table of the entity. + * @param $value + * The value to test the field against. In most cases, this is a scalar. For + * more complex options, it is an array. The meaning of each element in the + * array is dependent on $operator. + * @param $operator + * Possible values: + * - '=', '!=', '>', '>=', '<', '<=', 'STARTS_WITH', 'CONTAINS': 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. + * The operator can be omitted, and will default to 'IN' if the value is an + * array, or to '=' otherwise. + * + * @return EntityFieldQuery + * The called object. + */ + public function propertyCondition($column, $value, $operator = NULL) { + $this->propertyConditions[] = array( + 'column' => $column, + 'value' => $value, + 'operator' => $operator, + ); + return $this; + } + + /** + * Orders the result set by entity-generic metadata. + * + * If called multiple times, the query will order by each specified column in + * the order this method is called. + * + * @param $name + * 'entity_type', 'bundle', 'revision_id' or 'entity_id'. + * @param $direction + * The direction to sort. Legal values are "ASC" and "DESC". + * + * @return EntityFieldQuery + * The called object. + */ + public function entityOrderBy($name, $direction) { + $this->entityOrder[$name] = $direction; + return $this; + } + + /** + * Orders the result set by a given field column. + * + * If called multiple times, the query will order by each specified column in + * the order this method is called. + * + * @param $field + * Either a field name or a field array. + * @param $column + * A column defined in the hook_field_schema() of this field. entity_id and + * bundle can also be used. + * @param $direction + * The direction to sort. Legal values are "ASC" and "DESC". + * + * @return EntityFieldQuery + * The called object. + */ + public function fieldOrderBy($field, $column, $direction) { + if (is_scalar($field)) { + $field = field_info_field($field); + } + // Ensure the same index is used for fieldOrder as for fields. + $index = count($this->fields); + $this->fields[$index] = $field; + $this->fieldOrder[$index] = array( + 'field' => $field, + 'column' => $column, + 'direction' => $direction, + ); + return $this; + } + + /** + * Orders the result set by an entity-specific property. + * + * An $entity_type must be specified by calling + * EntityFieldCondition::entityCondition('entity_type', $entity_type) before + * executing the query. + * + * If called multiple times, the query will order by each specified column in + * the order this method is called. + * + * @param $column + * The column on which to order. + * @param $direction + * The direction to sort. Legal values are "ASC" and "DESC". + * + * @return EntityFieldQuery + * The called object. + */ + public function propertyOrderBy($column, $direction) { + $this->propertyOrder[] = array( + 'column' => $column, + 'direction' => $direction, + ); + return $this; + } + + /** + * Sets the query to be a count query only. + * + * @return EntityFieldQuery + * The called object. + */ + public function count() { + $this->count = TRUE; + return $this; + } + + /** + * Restricts a query to a given range in the result set. + * + * @param $start + * The first entity from the result set to return. If NULL, removes any + * range directives that are set. + * @param $length + * The number of entities to return from the result set. + * + * @return EntityFieldQuery + * The called object. + */ + public function range($start = NULL, $length = NULL) { + $this->range = array( + 'start' => $start, + 'length' => $length, + ); + return $this; + } + + /** + * Filters on the data being deleted. + * + * @param $deleted + * TRUE to only return deleted data, FALSE to return non-deleted data, + * EntityFieldQuery::RETURN_ALL to return everything. Defaults to FALSE. + * + * @return EntityFieldQuery + * The called object. + */ + public function deleted($deleted = TRUE) { + $this->deleted = $deleted; + return $this; + } + + /** + * Queries the current or every revision. + * + * Note that this only affects field conditions. Property conditions always + * apply to the current revision. + * @TODO: Once revision tables have been cleaned up, revisit this. + * + * @param $age + * - FIELD_LOAD_CURRENT (default): Query the most recent revisions for all + * entities. The results will be keyed by entity type and entity ID. + * - FIELD_LOAD_REVISION: Query all revisions. The results will be keyed by + * entity type and entity revision ID. + * + * @return EntityFieldQuery + * The called object. + */ + public function age($age) { + $this->age = $age; + return $this; + } + + /** + * Executes the query. + * + * After executing the query, $this->orderedResults will contain a list of + * the same stub entities in the order returned by the query. This is only + * relevant if there are multiple entity types in the returned value and + * a field ordering was requested. In every other case, the returned value + * contains everything necessary for processing. + * + * @return + * Either a number if count() was called or an array of associative + * arrays of stub entities. The outer array keys are entity types, and the + * inner array keys are the relevant ID. (In most this cases this will be + * the entity ID. The only exception is when age=FIELD_LOAD_REVISION is used + * and field conditions or sorts are present -- in this case, the key will + * be the revision ID.) The inner array values are always stub entities, as + * returned by entity_create_stub_entity(). To traverse the returned array: + * @code + * foreach ($query->execute() as $entity_type => $entities) { + * foreach ($entities as $entity_id => $entity) { + * @endcode + * Note if the entity type is known, then the following snippet will load + * the entities found: + * @code + * $result = $query->execute; + * $entities = entity_load($my_type, array_keys($result[$my_type])); + * @endcode + */ + public function execute() { + drupal_alter('entity_query', $this); + if (function_exists($this->executeCallback)) { + return $this->executeCallback($this); + } + // If there are no field conditions and sorts, and no execute callback + // then we default to querying entity tables in SQL. + if (empty($this->fields)) { + return $this->propertyQuery(); + } + // If no override, find the storage engine to be used. + foreach ($this->fields as $field) { + if (!isset($storage)) { + $storage = $field['storage']['module']; + } + elseif ($storage != $field['storage']['module']) { + throw new EntityFieldQueryException(t("Can't handle more than one field storage engine")); + } + } + if (empty($storage)) { + throw new EntityFieldQueryException(t("Field storage engine not found.")); + } + $function = $storage . '_field_storage_query'; + $result = $function($this); + if (!empty($this->propertyConditions)) { + throw new EntityFieldQueryException(t('Property query conditions were not handled in !function.', array('!function' => $function))); + } + if (!empty($this->propertyOrderBy)) { + throw new EntityFieldQueryException(t('Property query order by was not handled in !function.', array('!function' => $function))); + } + return $result; + } + + /** + * Queries entity tables in SQL for property conditions and sorts. + * + * This method is only used if there are no field conditions and sorts. + * + * @return + * See EntityFieldQuery::execute(). + */ + protected function propertyQuery() { + if (empty($this->entityConditions['entity_type'])) { + throw new EntityFieldQueryException(t('For this query an entity type must be specified.')); + } + $entity_type = $this->entityConditions['entity_type']['value']; + unset($this->entityConditions['entity_type']); + $entity_info = entity_get_info($entity_type); + if (empty($entity_info['base table'])) { + throw new EntityFieldQueryException(t('Entity %entity has no base table.', array('%entity' => $entity_type))); + } + $base_table = $entity_info['base table']; + $select_query = db_select($base_table); + $select_query->addExpression(':entity_type', 'entity_type', array(':entity_type' => $entity_type)); + // Process the four possible entity condition. + // The id field is always present in entity keys. + $sql_field = $entity_info['entity keys']['id']; + $id_map['entity_id'] = $sql_field; + $select_query->addField($base_table, $sql_field, 'entity_id'); + if (isset($this->entityConditions['entity_id'])) { + $this->addCondition($select_query, $sql_field, $this->entityConditions['entity_id']); + } + + // If there is a revision key defined, use it. + if (!empty($entity_info['entity keys']['revision'])) { + $sql_field = $entity_info['entity keys']['revision']; + $select_query->addField($base_table, $sql_field, 'revision_id'); + if (isset($this->entityConditions['revision_id'])) { + $this->addCondition($select_query, $sql_field, $this->entityConditions['revision_id']); + } + } + else { + $sql_field = 'revision_id'; + $select_query->addExpression('NULL', 'revision_id'); + } + $id_map['revision_id'] = $sql_field; + + // Handle bundles. + if (!empty($entity_info['entity keys']['bundle'])) { + $sql_field = $entity_info['entity keys']['bundle']; + $select_query->addField($base_table, $sql_field, 'bundle'); + $having = FALSE; + } + else { + $sql_field = 'bundle'; + $select_query->addExpression(':bundle', 'bundle', array(':bundle' => $entity_type)); + $having = TRUE; + } + $id_map['bundle'] = $sql_field; + if (isset($this->entityConditions['bundle'])) { + $this->addCondition($select_query, $sql_field, $this->entityConditions['bundle'], $having); + } + + foreach ($this->entityOrder as $key => $direction) { + if (isset($id_map[$key])) { + $select_query->orderBy($id_map[$key], $direction); + } + else { + throw new EntityFieldQueryException(t('Do not know how to order on @key for @entity_type', array('@key' => $key, '@entity_type' => $entity_type))); + } + } + $this->processProperty($select_query, $base_table); + return $this->finishQuery($select_query); + } + + /** + * Finishes the query. + * + * Adds the range and returns the requested list. + * + * @param SelectQuery $select_query + * A SelectQuery which has entity_type, entity_id, revision_id and bundle + * fields added. + * @param $id_key + * Which field's values to use as the returned array keys. + * + * @return + * See EntityFieldQuery::execute(). + */ + function finishQuery($select_query, $id_key = 'entity_id') { + if ($this->range) { + $select_query->range($this->range['start'], $this->range['length']); + } + if ($this->count) { + return $select_query->countQuery()->execute()->fetchField(); + } + $return = array(); + foreach ($select_query->execute() as $partial_entity) { + $entity = entity_create_stub_entity($partial_entity->entity_type, array($partial_entity->entity_id, $partial_entity->revision_id, $partial_entity->bundle)); + $return[$partial_entity->entity_type][$partial_entity->$id_key] = $entity; + $this->ordered_results[] = $partial_entity; + } + return $return; + } + + /** + * Processes the property condition and orders. + * + * This is a helper for hook_entity_query() and hook_field_storage_query(). + * + * @param SelectQuery $select_query + * A SelectQuery object. + * @param $entity_base_table + * The name of the entity base table. This already should be in + * $select_query. + */ + public function processProperty(SelectQuery $select_query, $entity_base_table) { + foreach ($this->propertyConditions as $entity_condition) { + $this->addCondition($select_query, "$entity_base_table." . $entity_condition['column'], $entity_condition); + } + foreach ($this->propertyOrder as $order) { + $select_query->orderBy("$entity_base_table." . $order['column'], $order['direction']); + } + unset($this->propertyConditions, $this->propertyOrder); + } + + /** + * Adds a condition to an already built SelectQuery (internal function). + * + * This is a helper for hook_entity_query() and hook_field_storage_query(). + * + * @param SelectQuery $select_query + * A SelectQuery object. + * @param $sql_field + * The name of the field. + * @param $condition + * A condition as described in EntityFieldQuery::fieldCondition() and + * EntityFieldQuery::entityCondition(). + * @param $having + * HAVING or WHERE. This is necessary because SQL can't handle WHERE + * conditions on aliased columns. + */ + public function addCondition(SelectQuery $select_query, $sql_field, $condition, $having = FALSE) { + $method = $having ? 'havingCondition' : 'condition'; + $like_prefix = ''; + switch ($condition['operator']) { + case 'CONTAINS': + $like_prefix = '%'; + case 'STARTS_WITH': + $select_query->$method($sql_field, $like_prefix . db_like($condition['value']) . '%', 'LIKE'); + break; + default: + $select_query->$method($sql_field, $condition['value'], $condition['operator']); + } + } + +} === modified file 'modules/field/field.api.php' --- modules/field/field.api.php 2010-06-06 00:24:16 +0000 +++ modules/field/field.api.php 2010-06-08 06:43:52 +0000 @@ -1470,24 +1470,20 @@ function hook_field_storage_delete_revis } /** - * Handle a field query. + * Execute an EntityFieldQuery. * - * This hook is invoked from field_attach_query() to ask the field storage - * module to handle a field query. + * This hook is called to find the entities having certain entity and field + * conditions and sort them in the given field order. If the field storage + * engine also handles property sorts and orders, it should unset those + * properties in the called object to signal that those have been handled. * - * @param $field_name - * The name of the field to query. - * @param $conditions - * See field_attach_query(). A storage module that doesn't support querying a - * given column should raise a FieldQueryException. Incompatibilities should - * be mentioned on the module project page. - * @param $options - * See field_attach_query(). All option keys are guaranteed to be specified. + * @param EntityFieldQuery $query + * An EntityFieldQuery. * * @return - * See field_attach_query(). + * See EntityFieldQuery::execute() for the return values. */ -function hook_field_storage_query($field_name, $conditions, $options) { +function hook_field_storage_query($query) { // @todo Needs function body } @@ -1658,34 +1654,6 @@ function hook_field_storage_pre_update($ } /** - * Act before the storage backend runs the query. - * - * This hook should be implemented by modules that use - * hook_field_storage_pre_load(), hook_field_storage_pre_insert() and - * hook_field_storage_pre_update() to bypass the regular storage engine, to - * handle field queries. - * - * @param $field_name - * The name of the field to query. - * @param $conditions - * See field_attach_query(). - * A storage module that doesn't support querying a given column should raise - * a FieldQueryException. Incompatibilities should be mentioned on the module - * project page. - * @param $options - * See field_attach_query(). All option keys are guaranteed to be specified. - * @param $skip_field - * Boolean, always coming as FALSE. - * @return - * See field_attach_query(). - * The $skip_field parameter should be set to TRUE if the query has been - * handled. - */ -function hook_field_storage_pre_query($field_name, $conditions, $options, &$skip_field) { - // @todo Needs function body. -} - -/** * Alters the display settings of a field before it gets displayed. * * Note that instead of hook_field_display_alter(), which is called for all @@ -1837,8 +1805,12 @@ function hook_field_update_forbid($field // Identify the keys that will be lost. $lost_keys = array_diff(array_keys($field['settings']['allowed_values']), array_keys($prior_field['settings']['allowed_values'])); // If any data exist for those keys, forbid the update. - $count = field_attach_query($prior_field['id'], array('value', $lost_keys, 'IN'), 1); - if ($count > 0) { + $query = new EntityFieldQuery; + $found = $query + ->fieldCondition($prior_field['field_name'], 'value', $lost_keys) + ->range(0, 1) + ->execute(); + if ($found) { throw new FieldUpdateForbiddenException("Cannot update a list field not to include keys with existing data"); } } === modified file 'modules/field/field.attach.inc' --- modules/field/field.attach.inc 2010-06-06 00:24:16 +0000 +++ modules/field/field.attach.inc 2010-06-08 06:19:40 +0000 @@ -31,15 +31,6 @@ class FieldValidationException extends F } /** - * Exception thrown by field_attach_query() on unsupported query syntax. - * - * Some storage modules might not support the full range of the syntax for - * conditions, and will raise a FieldQueryException when an usupported - * condition was specified. - */ -class FieldQueryException extends FieldException {} - -/** * @defgroup field_storage Field Storage API * @{ * Implement a storage engine for Field API data. @@ -1046,132 +1037,6 @@ function field_attach_delete_revision($e } /** - * Retrieve entities matching a given set of conditions. - * - * Note that the query 'conditions' only apply to the stored values. - * In a regular field_attach_load() call, field values additionally go through - * hook_field_load() and hook_field_attach_load() invocations, which can add - * to or affect the raw stored values. The results of field_attach_query() - * might therefore differ from what could be expected by looking at a regular, - * fully loaded entity. - * - * @param $field_id - * The id of the field to query. - * @param $conditions - * An array of query conditions. Each condition is a numerically indexed - * array, in the form: array(column, value, operator). - * Not all storage engines are required to support queries on all column, or - * with all operators below. A FieldQueryException will be raised if an - * unsupported condition is specified. - * Supported columns: - * - any of the columns defined in hook_field_schema() for $field_name's - * field type: condition on field value, - * - 'type': condition on entity type (e.g. 'node', 'user'...), - * - 'bundle': condition on entity bundle (e.g. node type), - * - 'entity_id': condition on entity id (e.g node nid, user uid...), - * - 'deleted': condition on whether the field's data is - * marked deleted for the entity (defaults to FALSE if not specified) - * The field_attach_query_revisions() function additionally supports: - * - 'revision_id': condition on entity revision id (e.g node vid). - * Supported operators: - * - '=', '!=', '>', '>=', '<', '<=', 'STARTS_WITH', 'ENDS_WITH', - * 'CONTAINS': these operators expect the value as a literal of the same - * type as the column, - * - 'IN', 'NOT IN': this operator expects the value as an array of - * literals of the same type as the column. - * - 'BETWEEN': this operator expects the value as an array of two literals - * of the same type as the column. - * The operator can be ommitted, and will default to 'IN' if the value is - * an array, or to '=' otherwise. - * Example values for $conditions: - * @code - * array( - * array('type', 'node'), - * ); - * array( - * array('bundle', array('article', 'page')), - * array('value', 12, '>'), - * ); - * @endcode - * @param $options - * An associative array of additional options: - * - limit: The number of results that is requested. This is only a hint to - * the storage engine(s); callers should be prepared to handle fewer or - * more results. Specify FIELD_QUERY_NO_LIMIT to retrieve all available - * entities. This option has a default value of 0 so callers must make an - * explicit choice to potentially retrieve an enormous result set. - * - cursor: A reference to an opaque cursor that allows a caller to iterate - * through multiple result sets. On the first call, pass 0; the correct - * value to pass on the next call will be written into the value on return. - * When there is no more query data available, the value will be filled in - * with FIELD_QUERY_COMPLETE. If cursor is passed as NULL, the first result - * set is returned and no next cursor is returned. - * - count: If TRUE, return a single count of all matching entities; limit and - * cursor are ignored. - * - age: Internal use only. Use field_attach_query_revisions() instead of - * passing FIELD_LOAD_REVISION. - * - FIELD_LOAD_CURRENT (default): query the most recent revisions for all - * entities. The results will be keyed by entity type and entity id. - * - FIELD_LOAD_REVISION: query all revisions. The results will be keyed by - * entity type and entity revision id. - * @return - * An array keyed by entity type (e.g. 'node', 'user'...), then by entity id - * or revision id (depending of the value of the $age parameter). The values - * are pseudo-entities with the bundle, id, and revision id fields filled in. - * Throws a FieldQueryException if the field's storage doesn't support the - * specified conditions. - */ -function field_attach_query($field_id, $conditions, $options = array()) { - // Merge in default options. - $default_options = array( - 'limit' => 0, - 'cursor' => 0, - 'count' => FALSE, - 'age' => FIELD_LOAD_CURRENT, - ); - $options += $default_options; - - // Give a chance to 3rd party modules that bypass the storage engine to - // handle the query. - $skip_field = FALSE; - foreach (module_implements('field_storage_pre_query') as $module) { - $function = $module . '_field_storage_pre_query'; - $results = $function($field_id, $conditions, $options, $skip_field); - // Stop as soon as a module claims it handled the query. - if ($skip_field) { - break; - } - } - // If the request hasn't been handled, let the storage engine handle it. - if (!$skip_field) { - $field = field_info_field_by_id($field_id); - $function = $field['storage']['module'] . '_field_storage_query'; - $results = $function($field_id, $conditions, $options); - } - - return $results; -} - -/** - * Retrieve entity revisions matching a given set of conditions. - * - * See field_attach_query() for more informations. - * - * @param $field_id - * The id of the field to query. - * @param $conditions - * See field_attach_query(). - * @param $options - * An associative array of additional options. See field_attach_query(). - * @return - * See field_attach_query(). - */ -function field_attach_query_revisions($field_id, $conditions, $options = array()) { - $options['age'] = FIELD_LOAD_REVISION; - return field_attach_query($field_id, $conditions, $options); -} - -/** * Prepare field data prior to display. * * This function must be called before field_attach_view(). It lets field === modified file 'modules/field/field.crud.inc' --- modules/field/field.crud.inc 2010-06-05 12:05:25 +0000 +++ modules/field/field.crud.inc 2010-06-08 06:19:40 +0000 @@ -1036,25 +1036,23 @@ function field_purge_batch($batch_size) $instances = field_read_instances(array('deleted' => 1), array('include_deleted' => 1)); foreach ($instances as $instance) { + // field_purge_data() will need the field array. $field = field_info_field_by_id($instance['field_id']); - - // Retrieve some pseudo-entities. - $entity_types = field_attach_query($instance['field_id'], array(array('bundle', $instance['bundle']), array('deleted', 1)), array('limit' => $batch_size)); - - if (count($entity_types) > 0) { - // Field data for the instance still exists. - foreach ($entity_types as $entity_type => $entities) { - field_attach_load($entity_type, $entities, FIELD_LOAD_CURRENT, array('field_id' => $field['id'], 'deleted' => 1)); - - foreach ($entities as $id => $entity) { - // field_attach_query() may return more results than we asked for. - // Stop when he have done our batch size. - if ($batch_size-- <= 0) { - return; - } - + // Retrieve some entities. + $query = new EntityFieldQuery; + $results = $query + ->fieldCondition($field) + ->entityCondition('bundle', $instance['bundle']) + ->deleted(TRUE) + ->range(0, $batch_size) + ->execute(); + + if ($results) { + foreach ($results as $entity_type => $stub_entities) { + field_attach_load($entity_type, $stub_entities, FIELD_LOAD_CURRENT, array('field_id' => $field['id'], 'deleted' => 1)); + foreach ($stub_entities as $stub_entity) { // Purge the data for the entity. - field_purge_data($entity_type, $entity, $field, $instance); + field_purge_data($entity_type, $stub_entity, $field, $instance); } } } @@ -1164,4 +1162,3 @@ function field_purge_field($field) { /** * @} End of "defgroup field_purge". */ - === modified file 'modules/field/field.module' --- modules/field/field.module 2010-05-26 11:54:19 +0000 +++ modules/field/field.module 2010-06-08 06:19:40 +0000 @@ -102,28 +102,6 @@ define('FIELD_LOAD_CURRENT', 'FIELD_LOAD define('FIELD_LOAD_REVISION', 'FIELD_LOAD_REVISION'); /** - * @name Field query flags - * @{ - * Flags for field_attach_query(). - */ - -/** - * Limit argument for field_attach_query() to request all available - * entities instead of a limited number. - */ -define('FIELD_QUERY_NO_LIMIT', 'FIELD_QUERY_NO_LIMIT'); - -/** - * Cursor return value for field_attach_query() to indicate that no - * more data is available. - */ -define('FIELD_QUERY_COMPLETE', 'FIELD_QUERY_COMPLETE'); - -/** - * @} End of "Field query flags". - */ - -/** * Exception class thrown by hook_field_update_forbid(). */ class FieldUpdateForbiddenException extends FieldException {} @@ -877,8 +855,12 @@ function field_get_items($entity_type, $ * TRUE if the field has data for any entity; FALSE otherwise. */ function field_has_data($field) { - $results = field_attach_query($field['id'], array(), array('limit' => 1)); - return !empty($results); + $query = new EntityFieldQuery(); + return (bool) $query + ->fieldCondition($field) + ->range(0, 1) + ->count() + ->execute(); } /** === modified file 'modules/field/modules/field_sql_storage/field_sql_storage.module' --- modules/field/modules/field_sql_storage/field_sql_storage.module 2010-05-06 15:29:51 +0000 +++ modules/field/modules/field_sql_storage/field_sql_storage.module 2010-06-08 06:22:55 +0000 @@ -482,127 +482,114 @@ function field_sql_storage_field_storage /** * Implements hook_field_storage_query(). */ -function field_sql_storage_field_storage_query($field_id, $conditions, $options) { - $load_current = $options['age'] == FIELD_LOAD_CURRENT; - - $field = field_info_field_by_id($field_id); - $field_name = $field['field_name']; - $table = $load_current ? _field_sql_storage_tablename($field) : _field_sql_storage_revision_tablename($field); - $field_columns = array_keys($field['columns']); - - // Build the query. - $query = db_select($table, 't'); - $query->join('field_config_entity_type', 'e', 't.etid = e.etid'); - - // Add conditions. - foreach ($conditions as $condition) { - // A condition is either a (column, value, operator) triple, or a - // (column, value) pair with implied operator. - @list($column, $value, $operator) = $condition; - // Translate operator and value if needed. - switch ($operator) { - case 'STARTS_WITH': - $operator = 'LIKE'; - $value = db_like($value) . '%'; - break; - - case 'ENDS_WITH': - $operator = 'LIKE'; - $value = '%' . db_like($value); - break; - - case 'CONTAINS': - $operator = 'LIKE'; - $value = '%' . db_like($value) . '%'; - break; - } - // Translate field columns into prefixed db columns. - if (in_array($column, $field_columns)) { - $column = _field_sql_storage_columnname($field_name, $column); - } - // Translate entity types into numeric ids. Expressing the condition on the - // local 'etid' column rather than the JOINed 'type' column avoids a - // filesort. - if ($column == 'type') { - $column = 't.etid'; - if (is_array($value)) { - foreach (array_keys($value) as $key) { - $value[$key] = _field_sql_storage_etid($value[$key]); +function field_sql_storage_field_storage_query(EntityFieldQuery $query) { + $groups = array(); + if ($query->age == FIELD_LOAD_CURRENT) { + $tablename_function = '_field_sql_storage_tablename'; + $id_key = 'entity_id'; + } + else { + $tablename_function = '_field_sql_storage_revision_tablename'; + $id_key = 'revision_id'; + } + $field = $query->fields[0]; + $tablename = $tablename_function($field); + $select_query = db_select($tablename); + $select_query->fields($tablename, array('entity_id', 'revision_id', 'bundle')); + // As only a numeric ID is stored instead of the entity type add the + // field_config_entity_type table to resolve the etid to a more readable + // name. + $select_query->join('field_config_entity_type', 'fcet', "fcet.etid = $tablename.etid"); + $select_query->addField('fcet', 'type', 'entity_type'); + $field_base_table = $tablename; + $table_aliases = array(); + // Do not add fields[0] twice, we already have the first alias. + unset($query->fields[0]); + $table_aliases[0] = $field_base_table; + // Add tables for the fields used. + foreach ($query->fields as $key => $field) { + if ($field['cardinality'] != 1) { + $select_query->distinct(); + } + $tablename = $tablename_function($field); + // Every field needs a new table. + $table_alias = $tablename . $key; + $select_query->join($tablename, $table_alias, "$table_alias.etid = $field_base_table.etid AND $table_alias.$id_key = $field_base_table.$id_key"); + $table_aliases[$key] = $table_alias; + } + // Add field conditions. + foreach ($query->fieldConditions as $key => $condition) { + $table_alias = $table_aliases[$key]; + $field = $condition['field']; + // Add the specified condition. + $sql_field = "$table_alias." . _field_sql_storage_columnname($field['field_name'], $condition['column']); + $query->addCondition($select_query, $sql_field, $condition); + // Add delta / language group conditions. + foreach (array('delta', 'language') as $column) { + if (isset($condition[$column .'_group'])) { + $group_name = $condition[$column .'_group']; + if (!isset($groups[$column][$group_name])) { + $groups[$column][$group_name] = $table_alias; + } + else { + $select_query->where("$table_alias.$column = " . $groups[$column][$group_name] . ".$column"); } - } - else { - $value = _field_sql_storage_etid($value); } } - // Track condition on 'deleted'. - if ($column == 'deleted') { - $condition_deleted = TRUE; - } - - $query->condition($column, $value, $operator); } - // Exclude deleted data unless we have a condition on it. - if (!isset($condition_deleted)) { - $query->condition('deleted', 0); - } - - // For a count query, return the count now. - if ($options['count']) { - return $query - ->fields('t', array('etid', 'entity_id', 'revision_id')) - ->distinct() - ->countQuery() - ->execute() - ->fetchField(); - } - - // For a data query, add fields. - $query - ->fields('t', array('bundle', 'entity_id', 'revision_id')) - ->fields('e', array('type')) - // We need to ensure entities arrive in a consistent order for the - // range() operation to work. - ->orderBy('t.etid') - ->orderBy('t.entity_id'); - - // Initialize results array - $return = array(); - - // Getting $count entities possibly requires reading more than $count rows - // since fields with multiple values span over several rows. We query for - // batches of $count rows until we've either read $count entities or received - // less rows than asked for. - $entity_count = 0; - do { - if ($options['limit'] != FIELD_QUERY_NO_LIMIT) { - $query->range($options['cursor'], $options['limit']); - } - $results = $query->execute(); + // Add field orders. + foreach ($query->fieldOrder as $key => $order) { + $table_alias = $table_aliases[$key]; + $field = $order['field']; + $sql_field = "$table_alias." . _field_sql_storage_columnname($field['field_name'], $order['column']); + $select_query->orderBy($sql_field, $order['direction']); + } - $row_count = 0; - foreach ($results as $row) { - $row_count++; - $options['cursor']++; - // If querying all revisions and the entity type has revisions, we need - // to key the results by revision_ids. - $entity_type = entity_get_info($row->type); - $id = ($load_current || empty($entity_type['entity keys']['revision'])) ? $row->entity_id : $row->revision_id; - - if (!isset($return[$row->type][$id])) { - $return[$row->type][$id] = entity_create_stub_entity($row->type, array($row->entity_id, $row->revision_id, $row->bundle)); - $entity_count++; - } + if (isset($query->deleted)) { + $select_query->condition("$field_base_table.deleted", (int) $query->deleted); + } + if ($query->propertyConditions || $query->propertyOrder) { + if (empty($query->entityConditions['entity_type']['value'])) { + throw new EntityFieldQueryException('Property conditions and orders must have an entity type defined.'); } - } while ($options['limit'] != FIELD_QUERY_NO_LIMIT && $row_count == $options['limit'] && $entity_count < $options['limit']); - - // The query is complete when the last batch returns less rows than asked - // for. - if ($row_count < $options['limit']) { - $options['cursor'] = FIELD_QUERY_COMPLETE; + $entity_type = $query->entityConditions['entity_type']['value']; + $entity_base_table = _field_sql_storage_query_join_entity($select_query, $entity_type, $field_base_table); + $query->entityConditions['entity_type']['operator'] = '='; + $query->processProperty($select_query, $entity_base_table); + } + foreach ($query->entityConditions as $key => $condition) { + $sql_field = $key == 'entity_type' ? 'fcet.type' : "$field_base_table.$key"; + $query->addCondition($select_query, $sql_field, $condition); } + foreach ($query->entityOrder as $key => $direction) { + $sql_field = $key == 'entity_type' ? 'fcet.type' : "$field_base_table.$key"; + $query->orderBy($sql_field, $direction); + } + return $query->finishQuery($select_query, $id_key); +} - return $return; +/** + * Adds the base entity table to a field query object. + * + * @param SelectQuery $select_query + * A SelectQuery containing at least one table as specified by + * _field_sql_storage_tablename(). + * @param $entity_type + * The entity type for which the base table should be joined. + * @param $field_base_table + * Name of a table in $select_query. As only INNER JOINs are used, it does + * not matter which. + * + * @return + * The name of the entity base table joined in. + */ +function _field_sql_storage_query_join_entity(SelectQuery $select_query, $entity_type, $field_base_table) { + $entity_info = entity_get_info($entity_type); + $entity_base_table = $entity_info['base table']; + $entity_field = $entity_info['entity keys']['id']; + $select_query->join($entity_base_table, $entity_base_table, "$entity_base_table.$entity_field = $field_base_table.entity_id"); + return $entity_base_table; } /** === modified file 'modules/field/modules/list/list.module' --- modules/field/modules/list/list.module 2010-05-12 08:55:47 +0000 +++ modules/field/modules/list/list.module 2010-06-08 06:41:12 +0000 @@ -99,7 +99,7 @@ function list_field_schema($field) { * * @todo: If $has_data, add a form validate function to verify that the * new allowed values do not exclude any keys for which data already - * exists in the databae (use field_attach_query()) to find out. + * exists in the field storage (use EntityFieldQuery to find out). * Implement the validate function via hook_field_update_forbid() so * list.module does not depend on form submission. */ === modified file 'modules/field/tests/field.test' --- modules/field/tests/field.test 2010-05-23 19:10:22 +0000 +++ modules/field/tests/field.test 2010-06-08 06:19:40 +0000 @@ -623,215 +623,6 @@ class FieldAttachStorageTestCase extends $this->assertFalse(field_read_instance('test_entity', $this->field_name, $this->instance['bundle']), "First field is deleted"); $this->assertFalse(field_read_instance('test_entity', $field_name, $instance['bundle']), "Second field is deleted"); } - - /** - * Test field_attach_query(). - */ - function testFieldAttachQuery() { - $cardinality = $this->field['cardinality']; - $langcode = LANGUAGE_NONE; - - // Create an additional bundle with an instance of the field. - field_test_create_bundle('test_bundle_1', 'Test Bundle 1'); - $this->instance2 = $this->instance; - $this->instance2['bundle'] = 'test_bundle_1'; - field_create_instance($this->instance2); - - // Create instances of both fields on the second entity type. - $instance = $this->instance; - $instance['entity_type'] = 'test_cacheable_entity'; - field_create_instance($instance); - $instance2 = $this->instance2; - $instance2['entity_type'] = 'test_cacheable_entity'; - field_create_instance($instance2); - - // Unconditional count query returns 0. - $count = field_attach_query($this->field_id, array(), array('count' => TRUE)); - $this->assertEqual($count, 0, t('With no entities, count query returns 0.')); - - // Create two test entities, using two different types and bundles. - $entity_types = array(1 => 'test_entity', 2 => 'test_cacheable_entity'); - $entities = array(1 => field_test_create_stub_entity(1, 1, 'test_bundle'), 2 => field_test_create_stub_entity(2, 2, 'test_bundle_1')); - - // Create first test entity with random (distinct) values. - $values = array(); - for ($delta = 0; $delta < $cardinality; $delta++) { - do { - $value = mt_rand(1, 127); - } while (in_array($value, $values)); - $values[$delta] = $value; - $entities[1]->{$this->field_name}[$langcode][$delta] = array('value' => $values[$delta]); - } - field_attach_insert($entity_types[1], $entities[1]); - - // Unconditional count query returns 1. - $count = field_attach_query($this->field_id, array(), array('count' => TRUE)); - $this->assertEqual($count, 1, t('With one entity, count query returns @count.', array('@count' => $count))); - - // Create second test entity, sharing a value with the first one. - $common_value = $values[$cardinality - 1]; - $entities[2]->{$this->field_name} = array($langcode => array(array('value' => $common_value))); - field_attach_insert($entity_types[2], $entities[2]); - - // Query on the entity's values. - for ($delta = 0; $delta < $cardinality; $delta++) { - $conditions = array(array('value', $values[$delta])); - $result = field_attach_query($this->field_id, $conditions, array('limit' => FIELD_QUERY_NO_LIMIT)); - $this->assertTrue(isset($result[$entity_types[1]][1]), t('Query on value %delta returns the entity', array('%delta' => $delta))); - - $count = field_attach_query($this->field_id, $conditions, array('count' => TRUE)); - $this->assertEqual($count, ($values[$delta] == $common_value) ? 2 : 1, t('Count query on value %delta counts %count entities', array('%delta' => $delta, '%count' => $count))); - } - - // Query on a value that is not in the entity. - do { - $different_value = mt_rand(1, 127); - } while (in_array($different_value, $values)); - $conditions = array(array('value', $different_value)); - $result = field_attach_query($this->field_id, $conditions, array('limit' => FIELD_QUERY_NO_LIMIT)); - $this->assertFalse(isset($result[$entity_types[1]][1]), t("Query on a value that is not in the entity doesn't return the entity")); - $count = field_attach_query($this->field_id, $conditions, array('count' => TRUE)); - $this->assertEqual($count, 0, t("Count query on a value that is not in the entity doesn't count the entity")); - - // Query on the value shared by both entities, and discriminate using - // additional conditions. - - $conditions = array(array('value', $common_value)); - $result = field_attach_query($this->field_id, $conditions, array('limit' => FIELD_QUERY_NO_LIMIT)); - $this->assertTrue(isset($result[$entity_types[1]][1]) && isset($result[$entity_types[2]][2]), t('Query on a value common to both entities returns both entities')); - $count = field_attach_query($this->field_id, $conditions, array('count' => TRUE)); - $this->assertEqual($count, 2, t('Count query on a value common to both entities counts both entities')); - - $conditions = array(array('type', $entity_types[1]), array('value', $common_value)); - $result = field_attach_query($this->field_id, $conditions, array('limit' => FIELD_QUERY_NO_LIMIT)); - $this->assertTrue(isset($result[$entity_types[1]][1]) && !isset($result[$entity_types[2]][2]), t("Query on a value common to both entities and a 'type' condition only returns the relevant entity")); - $count = field_attach_query($this->field_id, $conditions, array('count' => TRUE)); - $this->assertEqual($count, 1, t("Count query on a value common to both entities and a 'type' condition only returns the relevant entity")); - - $conditions = array(array('bundle', $entities[1]->fttype), array('value', $common_value)); - $result = field_attach_query($this->field_id, $conditions, array('limit' => FIELD_QUERY_NO_LIMIT)); - $this->assertTrue(isset($result[$entity_types[1]][1]) && !isset($result[$entity_types[2]][2]), t("Query on a value common to both entities and a 'bundle' condition only returns the relevant entity")); - $count = field_attach_query($this->field_id, $conditions, array('count' => TRUE)); - $this->assertEqual($count, 1, t("Count query on a value common to both entities and a 'bundle' condition only counts the relevant entity")); - - $conditions = array(array('entity_id', $entities[1]->ftid), array('value', $common_value)); - $result = field_attach_query($this->field_id, $conditions, array('limit' => FIELD_QUERY_NO_LIMIT)); - $this->assertTrue(isset($result[$entity_types[1]][1]) && !isset($result[$entity_types[2]][2]), t("Query on a value common to both entities and an 'entity_id' condition only returns the relevant entity")); - $count = field_attach_query($this->field_id, $conditions, array('count' => TRUE)); - $this->assertEqual($count, 1, t("Count query on a value common to both entities and an 'entity_id' condition only counts the relevant entity")); - - // Test result format. - $conditions = array(array('value', $values[0])); - $result = field_attach_query($this->field_id, $conditions, array('limit' => FIELD_QUERY_NO_LIMIT)); - $expected = array( - $entity_types[1] => array( - $entities[1]->ftid => field_test_create_stub_entity($entities[1]->ftid, $entities[1]->ftvid), - ) - ); - $this->assertEqual($result, $expected, t('Result format is correct.')); - - // Now test the count/offset paging capability. - - // Create a new bundle with an instance of the field. - field_test_create_bundle('offset_bundle', 'Offset Test Bundle'); - $this->instance2 = $this->instance; - $this->instance2['bundle'] = 'offset_bundle'; - field_create_instance($this->instance2); - - // Create 20 test entities, using the new bundle, but with - // non-sequential ids so we can tell we are getting the right ones - // back. We do not need unique values since field_attach_query() - // won't return them anyway. - $offset_entities = array(); - $offset_id = mt_rand(1, 3); - for ($i = 0; $i < 20; ++$i) { - $offset_id += mt_rand(2, 5); - $offset_entities[$offset_id] = field_test_create_stub_entity($offset_id, $offset_id, 'offset_bundle'); - $offset_entities[$offset_id]->{$this->field_name}[$langcode][0] = array('value' => $offset_id); - field_attach_insert('test_entity', $offset_entities[$offset_id]); - } - - // Query for the offset entities in batches, making sure we get - // back the right ones. - $cursor = 0; - foreach (array(1 => 1, 3 => 3, 5 => 5, 8 => 8, 13 => 3) as $count => $expect) { - $found = field_attach_query($this->field_id, array(array('bundle', 'offset_bundle')), array('limit' => $count, 'cursor' => &$cursor)); - if (isset($found['test_entity'])) { - $this->assertEqual(count($found['test_entity']), $expect, t('Requested @count, expected @expect, got @found, cursor @cursor', array('@count' => $count, '@expect' => $expect, '@found' => count($found['test_entity']), '@cursor' => $cursor))); - foreach ($found['test_entity'] as $id => $entity) { - $this->assert(isset($offset_entities[$id]), "Entity $id found"); - unset($offset_entities[$id]); - } - } - else { - $this->assertEqual(0, $expect, t('Requested @count, expected @expect, got @found, cursor @cursor', array('@count' => $count, '@expect' => $expect, '@found' => 0, '@cursor' => $cursor))); - } - } - $this->assertEqual(count($offset_entities), 0, "All entities found"); - $this->assertEqual($cursor, FIELD_QUERY_COMPLETE, "Cursor is FIELD_QUERY_COMPLETE"); - } - - /** - * Test field_attach_query_revisions(). - */ - function testFieldAttachQueryRevisions() { - $cardinality = $this->field['cardinality']; - - // Create first entity revision with random (distinct) values. - $entity_type = 'test_entity'; - $entities = array(1 => field_test_create_stub_entity(1, 1), 2 => field_test_create_stub_entity(1, 2)); - $langcode = LANGUAGE_NONE; - $values = array(); - for ($delta = 0; $delta < $cardinality; $delta++) { - do { - $value = mt_rand(1, 127); - } while (in_array($value, $values)); - $values[$delta] = $value; - $entities[1]->{$this->field_name}[$langcode][$delta] = array('value' => $values[$delta]); - } - field_attach_insert($entity_type, $entities[1]); - - // Create second entity revision, sharing a value with the first one. - $common_value = $values[$cardinality - 1]; - $entities[2]->{$this->field_name}[$langcode][0] = array('value' => $common_value); - field_attach_update($entity_type, $entities[2]); - - // Query on the entity values. - for ($delta = 0; $delta < $cardinality; $delta++) { - $conditions = array(array('value', $values[$delta])); - $result = field_attach_query_revisions($this->field_id, $conditions, array('limit' => FIELD_QUERY_NO_LIMIT)); - $this->assertTrue(isset($result[$entity_type][1]), t('Query on value %delta returns the entity', array('%delta' => $delta))); - } - - // Query on a value that is not in the entity. - do { - $different_value = mt_rand(1, 127); - } while (in_array($different_value, $values)); - $conditions = array(array('value', $different_value)); - $result = field_attach_query_revisions($this->field_id, $conditions, array('limit' => FIELD_QUERY_NO_LIMIT)); - $this->assertFalse(isset($result[$entity_type][1]), t("Query on a value that is not in the entity doesn't return the entity")); - - // Query on the value shared by both entities, and discriminate using - // additional conditions. - - $conditions = array(array('value', $common_value)); - $result = field_attach_query_revisions($this->field_id, $conditions, array('limit' => FIELD_QUERY_NO_LIMIT)); - $this->assertTrue(isset($result[$entity_type][1]) && isset($result[$entity_type][2]), t('Query on a value common to both entities returns both entities')); - - $conditions = array(array('revision_id', $entities[1]->ftvid), array('value', $common_value)); - $result = field_attach_query_revisions($this->field_id, $conditions, array('limit' => FIELD_QUERY_NO_LIMIT)); - $this->assertTrue(isset($result[$entity_type][1]) && !isset($result[$entity_type][2]), t("Query on a value common to both entities and a 'revision_id' condition only returns the relevant entity")); - - // Test FIELD_QUERY_RETURN_IDS result format. - $conditions = array(array('value', $values[0])); - $result = field_attach_query_revisions($this->field_id, $conditions, array('limit' => FIELD_QUERY_NO_LIMIT)); - $expected = array( - $entity_type => array( - $entities[1]->ftid => field_test_create_stub_entity($entities[1]->ftid, $entities[1]->ftvid), - ) - ); - $this->assertEqual($result, $expected, t('FIELD_QUERY_RETURN_IDS result format returns the expect result')); - } } /** @@ -3026,7 +2817,7 @@ class FieldBulkDeleteTestCase extends Fi * the database and that the appropriate Field API functions can * operate on the deleted data and instance. * - * This tests how field_attach_query() interacts with + * This tests how EntityFieldQuery interacts with * field_delete_instance() and could be moved to FieldCrudTestCase, * but depends on this class's setUp(). */ @@ -3035,7 +2826,11 @@ class FieldBulkDeleteTestCase extends Fi $field = reset($this->fields); // There are 10 entities of this bundle. - $found = field_attach_query($field['id'], array(array('bundle', $bundle)), array('limit' => FIELD_QUERY_NO_LIMIT)); + $query = new EntityFieldQuery; + $found = $query + ->fieldCondition($field) + ->entityCondition('bundle', $bundle) + ->execute(); $this->assertEqual(count($found['test_entity']), 10, 'Correct number of entities found before deleting'); // Delete the instance. @@ -3048,12 +2843,21 @@ class FieldBulkDeleteTestCase extends Fi $this->assertEqual($instances[0]['bundle'], $bundle, 'The deleted instance is for the correct bundle'); // There are 0 entities of this bundle with non-deleted data. - $found = field_attach_query($field['id'], array(array('bundle', $bundle)), array('limit' => FIELD_QUERY_NO_LIMIT)); + $query = new EntityFieldQuery; + $found = $query + ->fieldCondition($field) + ->entityCondition('bundle', $bundle) + ->execute(); $this->assertTrue(!isset($found['test_entity']), 'No entities found after deleting'); // There are 10 entities of this bundle when deleted fields are allowed, and // their values are correct. - $found = field_attach_query($field['id'], array(array('bundle', $bundle), array('deleted', 1)), array('limit' => FIELD_QUERY_NO_LIMIT)); + $query = new EntityFieldQuery; + $found = $query + ->fieldCondition($field) + ->entityCondition('bundle', $bundle) + ->deleted(TRUE) + ->execute(); field_attach_load($this->entity_type, $found[$this->entity_type], FIELD_LOAD_CURRENT, array('field_id' => $field['id'], 'deleted' => 1)); $this->assertEqual(count($found['test_entity']), 10, 'Correct number of entities found after deleting'); foreach ($found['test_entity'] as $id => $entity) { @@ -3085,7 +2889,12 @@ class FieldBulkDeleteTestCase extends Fi field_purge_batch($batch_size); // There are $count deleted entities left. - $found = field_attach_query($field['id'], array(array('bundle', $bundle), array('deleted', 1)), array('limit' => FIELD_QUERY_NO_LIMIT)); + $query = new EntityFieldQuery; + $found = $query + ->fieldCondition($field) + ->entityCondition('bundle', $bundle) + ->deleted(TRUE) + ->execute(); $this->assertEqual($count ? count($found['test_entity']) : count($found), $count, 'Correct number of entities found after purging 2'); } === modified file 'modules/field/tests/field_test.entity.inc' --- modules/field/tests/field_test.entity.inc 2010-05-23 19:10:22 +0000 +++ modules/field/tests/field_test.entity.inc 2010-06-08 06:19:40 +0000 @@ -27,6 +27,8 @@ function field_test_entity_info() { 'name' => t('Test Entity'), 'fieldable' => TRUE, 'field cache' => FALSE, + 'base table' => 'test_entity', + 'revision table' => 'test_entity_revision', 'entity keys' => array( 'id' => 'ftid', 'revision' => 'ftvid', @@ -48,6 +50,29 @@ function field_test_entity_info() { 'bundles' => $bundles, 'view modes' => $test_entity_modes, ), + 'test_entity_bundle_key' => array( + 'name' => t('Test Entity with a bundle key.'), + 'base table' => 'test_entity_bundle_key', + 'fieldable' => TRUE, + 'field cache' => FALSE, + 'entity keys' => array( + 'id' => 'ftid', + 'bundle' => 'fttype', + ), + 'bundles' => array('bundle1' => array('label' => 'Bundle1'), 'bundle2' => array('label' => 'Bundle2')), + 'view modes' => $test_entity_modes, + ), + 'test_entity_bundle' => array( + 'name' => t('Test Entity with a specified bundle.'), + 'base table' => 'test_entity_bundle', + 'fieldable' => TRUE, + 'field cache' => FALSE, + 'entity keys' => array( + 'id' => 'ftid', + ), + 'bundles' => array('test_entity_2' => array('label' => 'Test entity 2')), + 'view modes' => $test_entity_modes, + ), ); } === modified file 'modules/field/tests/field_test.field.inc' --- modules/field/tests/field_test.field.inc 2010-05-18 18:30:49 +0000 +++ modules/field/tests/field_test.field.inc 2010-06-08 06:19:40 +0000 @@ -26,6 +26,14 @@ function field_test_field_info() { 'default_widget' => 'test_field_widget', 'default_formatter' => 'field_test_default', ), + 'shape' => array( + 'label' => t('Shape'), + 'description' => t('Another dummy field type.'), + 'settings' => array(), + 'instance_settings' => array(), + 'default_widget' => 'test_field_widget', + 'default_formatter' => 'field_test_default', + ), 'hidden_test_field' => array( 'no_ui' => TRUE, 'label' => t('Hidden from UI test field'), @@ -42,18 +50,36 @@ function field_test_field_info() { * Implements hook_field_schema(). */ function field_test_field_schema($field) { - return array( - 'columns' => array( - 'value' => array( - 'type' => 'int', - 'size' => 'tiny', - 'not null' => FALSE, + if ($field['type'] == 'test_field') { + return array( + 'columns' => array( + 'value' => array( + 'type' => 'int', + 'size' => 'medium', + 'not null' => FALSE, + ), ), - ), - 'indexes' => array( - 'value' => array('value'), - ), - ); + 'indexes' => array( + 'value' => array('value'), + ), + ); + } + else { + return array( + 'columns' => array( + 'shape' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => FALSE, + ), + 'color' => array( + 'type' => 'varchar', + 'length' => 32, + 'not null' => FALSE, + ), + ), + ); + } } /** === modified file 'modules/field/tests/field_test.install' --- modules/field/tests/field_test.install 2009-12-04 16:49:45 +0000 +++ modules/field/tests/field_test.install 2010-06-08 06:19:40 +0000 @@ -50,6 +50,37 @@ function field_test_schema() { ), 'primary key' => array('ftid'), ); + $schema['test_entity_bundle_key'] = array( + 'description' => 'The base table for test entities with a bundle key.', + 'fields' => array( + 'ftid' => array( + 'description' => 'The primary indentifier for a test_entity_bundle_key.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'fttype' => array( + 'description' => 'The type of this test_entity.', + 'type' => 'varchar', + 'length' => 32, + 'not null' => FALSE, + 'default' => '', + ), + ), + ); + $schema['test_entity_bundle'] = array( + 'description' => 'The base table for test entities with a bundle.', + 'fields' => array( + 'ftid' => array( + 'description' => 'The primary indentifier for a test_entity_bundle.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + ), + ); $schema['test_entity_revision'] = array( 'description' => 'Stores information about each saved version of a {test_entity}.', 'fields' => array( === modified file 'modules/file/file.module' --- modules/file/file.module 2010-05-09 19:34:26 +0000 +++ modules/file/file.module 2010-06-08 06:19:40 +0000 @@ -969,8 +969,11 @@ function file_get_file_references($file, foreach ($fields as $field_name => $file_field) { if ((empty($field_type) || $field['type'] == $field_type) && !isset($references[$field_name])) { // Get each time this file is used within a field. - $cursor = 0; - $references[$field_name] = field_attach_query($file_field['id'], array(array('fid', $file->fid)), array('limit' => FIELD_QUERY_NO_LIMIT, 'cursor' => &$cursor, 'age'=> $age)); + $query = new EntityFieldQuery; + $query + ->fieldCondition($file_field, 'fid', $file->fid) + ->age($age); + $references[$field_name] = $query->execute(); } } === modified file 'modules/simpletest/simpletest.info' --- modules/simpletest/simpletest.info 2010-02-03 18:16:22 +0000 +++ modules/simpletest/simpletest.info 2010-06-08 06:19:40 +0000 @@ -19,6 +19,7 @@ files[] = tests/bootstrap.test files[] = tests/cache.test files[] = tests/common.test files[] = tests/database_test.test +files[] = tests/entity_query.test files[] = tests/error.test files[] = tests/file.test files[] = tests/filetransfer.test === added file 'modules/simpletest/tests/entity_query.test' --- modules/simpletest/tests/entity_query.test 1970-01-01 00:00:00 +0000 +++ modules/simpletest/tests/entity_query.test 2010-06-08 06:40:44 +0000 @@ -0,0 +1,849 @@ + 'Entity query', + 'description' => 'Test the EntityFieldQuery class.', + 'group' => 'Entity API', + ); + } + + function setUp() { + parent::setUp(array('field_test')); + + field_attach_create_bundle('bundle1', 'test_entity_bundle_key'); + field_attach_create_bundle('bundle2', 'test_entity_bundle_key'); + field_attach_create_bundle('test_bundle', 'test_entity'); + field_attach_create_bundle('test_entity_bundle', 'test_entity_bundle'); + + $instances = array(); + $this->fields = array(); + $this->field_names[0] = $field_name = drupal_strtolower($this->randomName() . '_field_name'); + $field = array('field_name' => $field_name, 'type' => 'test_field', 'cardinality' => 4); + $field = field_create_field($field); + $this->fields[0] = $field; + $instance = array( + 'field_name' => $field_name, + 'entity_type' => '', + 'bundle' => '', + 'label' => $this->randomName() . '_label', + 'description' => $this->randomName() . '_description', + 'weight' => mt_rand(0, 127), + 'settings' => array( + 'test_instance_setting' => $this->randomName(), + ), + 'widget' => array( + 'type' => 'test_field_widget', + 'label' => 'Test Field', + 'settings' => array( + 'test_widget_setting' => $this->randomName(), + ) + ) + ); + + $instances[0] = $instance; + + // Add an instance to that bundle. + $instances[0]['bundle'] = 'bundle1'; + $instances[0]['entity_type'] = 'test_entity_bundle_key'; + field_create_instance($instances[0]); + $instances[0]['bundle'] = $instances[0]['entity_type'] = 'test_entity_bundle'; + field_create_instance($instances[0]); + + $this->field_names[1] = $field_name = drupal_strtolower($this->randomName() . '_field_name'); + $field = array('field_name' => $field_name, 'type' => 'shape', 'cardinality' => 4); + $field = field_create_field($field); + $this->fields[1] = $field; + $instance = array( + 'field_name' => $field_name, + 'entity_type' => '', + 'bundle' => '', + 'label' => $this->randomName() . '_label', + 'description' => $this->randomName() . '_description', + 'weight' => mt_rand(0, 127), + 'settings' => array( + 'test_instance_setting' => $this->randomName(), + ), + 'widget' => array( + 'type' => 'test_field_widget', + 'label' => 'Test Field', + 'settings' => array( + 'test_widget_setting' => $this->randomName(), + ) + ) + ); + + $instances[1] = $instance; + + // Add an instance to that bundle. + $instances[1]['bundle'] = 'bundle1'; + $instances[1]['entity_type'] = 'test_entity_bundle_key'; + field_create_instance($instances[1]); + $instances[1]['bundle'] = $instances[1]['entity_type'] = 'test_entity_bundle'; + field_create_instance($instances[1]); + + $this->instances = $instances; + // Write entity base table if there is one. + $entities = array(); + + // Create entities which have a 'bundle key' defined. + for ($i = 1; $i < 7; $i++) { + $entity = new stdClass; + $entity->ftid = $i; + $entity->fttype = ($i < 5) ? 'bundle1' : 'bundle2'; + + $entity->{$this->field_names[0]}[LANGUAGE_NONE][0]['value'] = $i; + drupal_write_record('test_entity_bundle_key', $entity); + field_attach_insert('test_entity_bundle_key', $entity); + } + + $entity = new stdClass; + $entity->ftid = 5; + $entity->fttype = 'bundle2'; + $entity->{$this->field_names[1]}[LANGUAGE_NONE][0]['shape'] = 'square'; + $entity->{$this->field_names[1]}[LANGUAGE_NONE][0]['color'] = 'red'; + $entity->{$this->field_names[1]}[LANGUAGE_NONE][1]['shape'] = 'circle'; + $entity->{$this->field_names[1]}[LANGUAGE_NONE][1]['color'] = 'blue'; + drupal_write_record('test_entity_bundle', $entity); + field_attach_insert('test_entity_bundle', $entity); + + $instances[2] = $instance; + $instances[2]['bundle'] = 'test_bundle'; + $instances[2]['field_name'] = $this->field_names[0]; + $instances[2]['entity_type'] = 'test_entity'; + field_create_instance($instances[2]); + + // Create entities with support for revisions. + for ($i = 1; $i < 5; $i++) { + $entity = new stdClass; + $entity->ftid = $i; + $entity->ftvid = $i; + $entity->fttype = 'test_bundle'; + $entity->{$this->field_names[0]}[LANGUAGE_NONE][0]['value'] = $i; + + drupal_write_record('test_entity', $entity); + field_attach_insert('test_entity', $entity); + drupal_write_record('test_entity_revision', $entity); + } + + // Add two revisions to an entity. + for($i = 100; $i < 102; $i++) { + $entity = new stdClass; + $entity->ftid = 4; + $entity->ftvid = $i; + $entity->fttype = 'test_bundle'; + $entity->{$this->field_names[0]}[LANGUAGE_NONE][0]['value'] = $i; + + drupal_write_record('test_entity', $entity, 'ftid'); + drupal_write_record('test_entity_revision', $entity); + + db_update('test_entity') + ->fields(array('ftvid' => $entity->ftvid)) + ->condition('ftid', $entity->ftid) + ->execute(); + + field_attach_update('test_entity', $entity); + } + } + + /** + * Tests EntityFieldQuery. + */ + function testEntityFieldQuery() { + // Test entity_type condition. + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'test_entity_bundle_key'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + array('test_entity_bundle_key', 2), + array('test_entity_bundle_key', 3), + array('test_entity_bundle_key', 4), + array('test_entity_bundle_key', 5), + array('test_entity_bundle_key', 6), + ), t('Test entity entity_type condition.')); + + // Test entity_id condition. + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->entityCondition('entity_id', '3'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 3), + ), t('Test entity entity_id condition.')); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->propertyCondition('ftid', '3'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 3), + ), t('Test entity entity_id condition and entity_id property condition.')); + + // Test bundle condition. + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->entityCondition('bundle', 'bundle1'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + array('test_entity_bundle_key', 2), + array('test_entity_bundle_key', 3), + array('test_entity_bundle_key', 4), + ), t('Test entity bundle condition: bundle1.')); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->entityCondition('bundle', 'bundle2'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 5), + array('test_entity_bundle_key', 6), + ), t('Test entity bundle condition: bundle2.')); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->propertyCondition('fttype', 'bundle2'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 5), + array('test_entity_bundle_key', 6), + ), t('Test entity bundle condition and bundle property condition.')); + + // Test revision_id condition. + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity') + ->entityCondition('revision_id', '3'); + $this->assertEntityFieldQuery($query, array( + array('test_entity', 3), + ), t('Test entity revision_id condition.')); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity') + ->propertyCondition('ftvid', '3'); + $this->assertEntityFieldQuery($query, array( + array('test_entity', 3), + ), t('Test entity revision_id condition and revision_id property condition.')); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity') + ->fieldCondition($this->fields[0], 'value', 100, '>=') + ->age(FIELD_LOAD_REVISION); + $this->assertEntityFieldQuery($query, array( + array('test_entity', 100), + array('test_entity', 101), + ), t('Test revision age.')); + + // Test entity sort by entity_id. + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->entityOrderBy('entity_id', 'ASC'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + array('test_entity_bundle_key', 2), + array('test_entity_bundle_key', 3), + array('test_entity_bundle_key', 4), + array('test_entity_bundle_key', 5), + array('test_entity_bundle_key', 6), + ), t('Test sort entity entity_id in ascending order.'), TRUE); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->entityOrderBy('entity_id', 'DESC'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 6), + array('test_entity_bundle_key', 5), + array('test_entity_bundle_key', 4), + array('test_entity_bundle_key', 3), + array('test_entity_bundle_key', 2), + array('test_entity_bundle_key', 1), + ), t('Test sort entity entity_id in descending order.'), TRUE); + + // Test property sort by entity id. + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->propertyOrderBy('ftid', 'ASC'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + array('test_entity_bundle_key', 2), + array('test_entity_bundle_key', 3), + array('test_entity_bundle_key', 4), + array('test_entity_bundle_key', 5), + array('test_entity_bundle_key', 6), + ), t('Test sort entity entity_id property in ascending order.'), TRUE); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->propertyOrderBy('ftid', 'DESC'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 6), + array('test_entity_bundle_key', 5), + array('test_entity_bundle_key', 4), + array('test_entity_bundle_key', 3), + array('test_entity_bundle_key', 2), + array('test_entity_bundle_key', 1), + ), t('Test sort entity entity_id property in descending order.'), TRUE); + + // Test entity sort by bundle. + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->entityOrderBy('bundle', 'ASC') + ->propertyOrderBy('ftid', 'ASC'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + array('test_entity_bundle_key', 2), + array('test_entity_bundle_key', 3), + array('test_entity_bundle_key', 4), + array('test_entity_bundle_key', 5), + array('test_entity_bundle_key', 6), + ), t('Test sort entity bundle in ascending order.'), TRUE); + + $query = new EntityFieldQuery(); + $foo = $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->entityOrderBy('bundle', 'DESC') + ->propertyOrderBy('ftid', 'DESC'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 6), + array('test_entity_bundle_key', 5), + array('test_entity_bundle_key', 4), + array('test_entity_bundle_key', 3), + array('test_entity_bundle_key', 2), + array('test_entity_bundle_key', 1), + ), t('Test sort entity bundle in descending order.'), TRUE); + + // Test entity sort by revision_id. + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity') + ->entityOrderBy('revision_id', 'ASC'); + $this->assertEntityFieldQuery($query, array( + array('test_entity', 1), + array('test_entity', 2), + array('test_entity', 3), + array('test_entity', 4), + ), t('Test sort entity revision_id in ascending order.'), TRUE); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity') + ->entityOrderBy('revision_id', 'DESC'); + $this->assertEntityFieldQuery($query, array( + array('test_entity', 4), + array('test_entity', 3), + array('test_entity', 2), + array('test_entity', 1), + ), t('Test sort entity revision_id in descending order.'), TRUE); + + // Test property sort by revision_id. + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity') + ->propertyOrderBy('ftvid', 'ASC'); + $this->assertEntityFieldQuery($query, array( + array('test_entity', 1), + array('test_entity', 2), + array('test_entity', 3), + array('test_entity', 4), + ), t('Test sort entity revision_id property in ascending order.'), TRUE); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity') + ->propertyOrderBy('ftvid', 'DESC'); + $this->assertEntityFieldQuery($query, array( + array('test_entity', 4), + array('test_entity', 3), + array('test_entity', 2), + array('test_entity', 1), + ), t('Test sort entity revision_id property in descending order.'), TRUE); + + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'test_entity_bundle_key'); + $query->fieldOrderBy($this->fields[0], 'value', 'ASC'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + array('test_entity_bundle_key', 2), + array('test_entity_bundle_key', 3), + array('test_entity_bundle_key', 4), + ), t('Test sort field in ascending order without field condition.'), TRUE); + + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'test_entity_bundle_key'); + $query->fieldOrderBy($this->fields[0], 'value', 'DESC'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 4), + array('test_entity_bundle_key', 3), + array('test_entity_bundle_key', 2), + array('test_entity_bundle_key', 1), + ), t('Test sort field in descending order without field condition.'), TRUE); + + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'test_entity_bundle_key'); + $query->fieldCondition($this->fields[0], 'value', 0, '>'); + $query->fieldOrderBy($this->fields[0], 'value', 'asc'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + array('test_entity_bundle_key', 2), + array('test_entity_bundle_key', 3), + array('test_entity_bundle_key', 4), + ), t('Test sort field in ascending order.'), TRUE); + + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'test_entity_bundle_key'); + $query->fieldCondition($this->fields[0], 'value', 0, '>'); + $query->fieldOrderBy($this->fields[0], 'value', 'desc'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 4), + array('test_entity_bundle_key', 3), + array('test_entity_bundle_key', 2), + array('test_entity_bundle_key', 1), + ), t('Test sort field in descending order.'), TRUE); + + // Test "in" operation with entity entity_type condition and entity_id + // property condition. + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->propertyCondition('ftid', array(1, 3, 4), 'IN'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + array('test_entity_bundle_key', 3), + array('test_entity_bundle_key', 4), + ), t('Test "in" operation with entity entity_type condition and entity_id property condition.')); + + // Test "in" operation with entity entity_type condition and entity_id + // property condition. Sort in descending order by entity_id. + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->propertyCondition('ftid', array(1, 3, 4), 'IN') + ->propertyOrderBy('ftid', 'DESC'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 4), + array('test_entity_bundle_key', 3), + array('test_entity_bundle_key', 1), + ), t('Test "in" operation with entity entity_type condition and entity_id property condition. Sort entity_id in descending order.'), TRUE); + + // Test query count + $query = new EntityFieldQuery(); + $query_count = $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->count() + ->execute(); + $this->assertEqual($query_count, 6, t('Test query count on entity condition.')); + + $query = new EntityFieldQuery(); + $query_count = $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->propertyCondition('ftid', '1') + ->count() + ->execute(); + $this->assertEqual($query_count, 1, t('Test query count on entity and property condition.')); + + $query = new EntityFieldQuery(); + $query_count = $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->propertyCondition('ftid', '4', '>') + ->count() + ->execute(); + $this->assertEqual($query_count, 2, t('Test query count on entity and property condition with operator.')); + + $query = new EntityFieldQuery(); + $query_count = $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->fieldCondition($this->fields[0], 'value', 3, '=') + ->count() + ->execute(); + $this->assertEqual($query_count, 1, t('Test query count on field condition.')); + + // First, test without options. + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->propertyCondition('ftid', 1, 'CONTAINS'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + ), t('Test the "contains" operation on a property.')); + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[0], 'value', 3, 'CONTAINS'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 3), + array('test_entity', 3), + ), t('Test the "contains" operation on a field.')); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->propertyCondition('ftid', 1, '='); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + ), t('Test the "equal to" operation on a property.')); + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[0], 'value', 3, '='); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 3), + array('test_entity', 3), + ), t('Test the "equal to" operation on a field.')); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->propertyCondition('ftid', 3, '!='); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + array('test_entity_bundle_key', 2), + array('test_entity_bundle_key', 4), + array('test_entity_bundle_key', 5), + array('test_entity_bundle_key', 6), + ), t('Test the "not equal to" operation on a property.')); + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[0], 'value', 3, '!='); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + array('test_entity_bundle_key', 2), + array('test_entity_bundle_key', 4), + array('test_entity', 1), + array('test_entity', 2), + array('test_entity', 4), + ), t('Test the "not equal to" operation on a field.')); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->propertyCondition('ftid', 2, '<'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + ), t('Test the "less than" operation on a property.')); + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[0], 'value', 2, '<'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + array('test_entity', 1), + ), t('Test the "less than" operation on a field.')); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->propertyCondition('ftid', 2, '<='); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + array('test_entity_bundle_key', 2), + ), t('Test the "less than or equal to" operation on a property.')); + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[0], 'value', 2, '<='); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + array('test_entity_bundle_key', 2), + array('test_entity', 1), + array('test_entity', 2), + ), t('Test the "less than or equal to" operation on a field.')); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->propertyCondition('ftid', 4, '>'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 5), + array('test_entity_bundle_key', 6), + ), t('Test the "greater than" operation on a property.')); + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[0], 'value', 2, '>'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 3), + array('test_entity_bundle_key', 4), + array('test_entity', 3), + array('test_entity', 4), + ), t('Test the "greater than" operation on a field.')); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->propertyCondition('ftid', 4, '>='); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 4), + array('test_entity_bundle_key', 5), + array('test_entity_bundle_key', 6), + ), t('Test the "greater than or equal to" operation on a property.')); + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[0], 'value', 3, '>='); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 3), + array('test_entity_bundle_key', 4), + array('test_entity', 3), + array('test_entity', 4), + ), t('Test the "greater than or equal to" operation on a field.')); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->propertyCondition('ftid', array(3, 4), 'NOT IN'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + array('test_entity_bundle_key', 2), + array('test_entity_bundle_key', 5), + array('test_entity_bundle_key', 6), + ), t('Test the "not in" operation on a property.')); + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[0], 'value', array(3, 4, 100, 101), 'NOT IN'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + array('test_entity_bundle_key', 2), + array('test_entity', 1), + array('test_entity', 2), + ), t('Test the "not in" operation on a field.')); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->propertyCondition('ftid', array(3, 4), 'IN'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 3), + array('test_entity_bundle_key', 4), + ), t('Test the "in" operation on a property.')); + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[0], 'value', array(2, 3), 'IN'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 2), + array('test_entity_bundle_key', 3), + array('test_entity', 2), + array('test_entity', 3), + ), t('Test the "in" operation on a field.')); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->propertyCondition('ftid', array(1, 3), 'BETWEEN'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + array('test_entity_bundle_key', 2), + array('test_entity_bundle_key', 3), + ), t('Test the "between" operation on a property.')); + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[0], 'value', array(1, 3), 'BETWEEN'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + array('test_entity_bundle_key', 2), + array('test_entity_bundle_key', 3), + array('test_entity', 1), + array('test_entity', 2), + array('test_entity', 3), + ), t('Test the "between" operation on a field.')); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->propertyCondition('fttype', 'bun', 'STARTS_WITH'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + array('test_entity_bundle_key', 2), + array('test_entity_bundle_key', 3), + array('test_entity_bundle_key', 4), + array('test_entity_bundle_key', 5), + array('test_entity_bundle_key', 6), + ), t('Test the "starts_with" operation on a property.')); + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[1], 'shape', 'squ', 'STARTS_WITH'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle', 5), + ), t('Test the "starts_with" operation on a field.')); + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[0], 'value', 3); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 3), + array('test_entity', 3), + ), t('Test omission of an operator with a single item.')); + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[0], 'value', array(2, 3)); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 2), + array('test_entity_bundle_key', 3), + array('test_entity', 2), + array('test_entity', 3), + ), t('Test omission of an operator with multiple items.')); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->propertyCondition('ftid', 1, '>') + ->fieldCondition($this->fields[0], 'value', 4, '<'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 2), + array('test_entity_bundle_key', 3), + ), t('Test entity, property and field conditions.')); + + $query = new EntityFieldQuery(); + $query + ->entityCondition('entity_type', 'test_entity_bundle_key') + ->entityCondition('bundle', 'bundle', 'STARTS_WITH') + ->propertyCondition('ftid', 4) + ->fieldCondition($this->fields[0], 'value', 4); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 4), + ), t('Test entity condition with "starts_with" operation, and property and field conditions.')); + + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'test_entity_bundle_key'); + $query->propertyOrderBy('ftid', 'asc'); + $query->range(0, 2); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + array('test_entity_bundle_key', 2), + ), t('Test limit on a property.'), TRUE); + + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'test_entity_bundle_key'); + $query->fieldCondition($this->fields[0], 'value', 0, '>='); + $query->fieldOrderBy($this->fields[0], 'value', 'asc'); + $query->range(0, 2); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 1), + array('test_entity_bundle_key', 2), + ), t('Test limit on a field.'), TRUE); + + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'test_entity_bundle_key'); + $query->propertyOrderBy('ftid', 'asc'); + $query->range(4, 6); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 5), + array('test_entity_bundle_key', 6), + ), t('Test offset on a property.'), TRUE); + + $query = new EntityFieldQuery(); + $query->entityCondition('entity_type', 'test_entity_bundle_key'); + $query->fieldCondition($this->fields[0], 'value', 0, '>'); + $query->fieldOrderBy($this->fields[0], 'value', 'asc'); + $query->range(2, 4); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 3), + array('test_entity_bundle_key', 4), + ), t('Test offset on a field.'), TRUE); + + for ($i = 6; $i < 10; $i++) { + $entity = new stdClass; + $entity->ftid = $i; + $entity->{$this->field_names[0]}[LANGUAGE_NONE][0]['value'] = $i - 5; + drupal_write_record('test_entity_bundle', $entity); + field_attach_insert('test_entity_bundle', $entity); + } + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[0], 'value', 2, '>'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 3), + array('test_entity_bundle_key', 4), + array('test_entity', 3), + array('test_entity', 4), + array('test_entity_bundle', 8), + array('test_entity_bundle', 9), + ), t('Select a field across multiple entities.')); + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[1], 'shape', 'square'); + $query->fieldCondition($this->fields[1], 'color', 'blue'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle', 5), + ), t('Test without a delta group.')); + + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[1], 'shape', 'square', '=', 'group'); + $query->fieldCondition($this->fields[1], 'color', 'blue', '=', 'group'); + $this->assertEntityFieldQuery($query, array(), t('Test with a delta group.')); + + // Test query on a deleted field. + field_attach_delete_bundle('test_entity_bundle_key', 'bundle1'); + field_attach_delete_bundle('test_entity', 'test_bundle'); + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[0], 'value', '3'); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle', 8), + ), t('Test query on a field after deleting field from some entities.')); + + field_attach_delete_bundle('test_entity_bundle', 'test_entity_bundle'); + $query = new EntityFieldQuery(); + $query->fieldCondition($this->fields[0], 'value', '3'); + $this->assertEntityFieldQuery($query, array(), t('Test query on a field after deleting field from all entities.')); + + $query = new EntityFieldQuery(); + $query + ->fieldCondition($this->fields[0], 'value', '3') + ->deleted(TRUE); + $this->assertEntityFieldQuery($query, array( + array('test_entity_bundle_key', 3), + array('test_entity_bundle', 8), + array('test_entity', 3), + ), t('Test query on a deleted field with deleted option set to TRUE.')); + + $pass = FALSE; + $query = new EntityFieldQuery(); + try { + $query->execute(); + } + catch (EntityFieldQueryException $exception) { + $pass = ($exception->getMessage() == t('For this query an entity type must be specified.')); + } + $this->assertTrue($pass, t("Can't query the universe.")); + } + + /** + * Fetches the results of an EntityFieldQuery and compares. + * + * @param $query + * An EntityFieldQuery to run. + * @param $intended_results + * A list of results, every entry is again a list, first being the entity + * type, the second being the entity_id. + * @param $message + * The message to be displayed as the result of this test. + * @param $ordered + * If FALSE then the result of EntityFieldQuery will match + * $intended_results even if the order is not the same. If TRUE then order + * should match too. + */ + function assertEntityFieldQuery($query, $intended_results, $message, $ordered = FALSE) { + $results = array(); + foreach ($query->execute() as $entity_type => $entity_ids) { + foreach ($entity_ids as $entity_id => $stub_entity) { + $results[] = array($entity_type, $entity_id); + } + } + if (!isset($ordered) || !$ordered) { + sort($results); + sort($intended_results); + } + $this->assertEqual($results, $intended_results, $message); + } +} === modified file 'modules/system/system.api.php' --- modules/system/system.api.php 2010-05-23 19:10:22 +0000 +++ modules/system/system.api.php 2010-06-08 06:19:40 +0000 @@ -285,6 +285,25 @@ function hook_entity_update($entity, $ty } /** + * Alter or execute an EntityFieldQuery. + * + * @param EntityFieldQuery $query + * An EntityFieldQuery. One of the most important properties to be changed is + * EntityFieldQuery::executeCallback. If this is set to an existing function, + * this function will get the query as its single argument and its result + * will be the returned as the result of EntityFieldQuery::execute(). This can + * be used to change the behavior of EntityFieldQuery entirely. For example, + * the default implementation can only deal with one field storage engine, but + * it is possible to write a module that can query across field storage + * engines. Also, the default implementation presumes entities are stored in + * SQL, but the execute callback could instead query any other entity storage, + * local or remote. + */ +function hook_entity_query_alter($query) { + $query->executeCallback = 'my_module_query_callback'; +} + +/** * Define administrative paths. * * Modules may specify whether or not the paths they define in hook_menu() are