? MISC-FIELD-CHANGES-REAPPLY.patch ? Makefile ? d6-50-nodes.sql.gz ? d7-50-nodes-new.sql.gz ? d7-50-nodes.sql.gz ? head.kpf ? patches ? modules/field/field.bulk.update.inc ? modules/simpletest/tests/entity_query.test ? scripts/OLD-generate-autoload.pl ? scripts/generate-autoload.pl ? sites/all/cck ? sites/all/modules/color ? sites/all/modules/combofield ? sites/all/modules/devel ? sites/all/modules/pbs ? sites/all/modules/taint ? sites/default/files ? sites/default/private ? sites/default/settings.php Index: includes/entity.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/entity.inc,v retrieving revision 1.8 diff -u -F '^[fc]' -r1.8 entity.inc --- includes/entity.inc 9 May 2010 13:27:31 -0000 1.8 +++ includes/entity.inc 8 Jun 2010 13:42:05 -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, + * EntityFieldQuery::RETURN_ALL 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']); + } + } + +} Index: modules/field/field.api.php =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.api.php,v retrieving revision 1.83 diff -u -F '^[fc]' -r1.83 field.api.php --- modules/field/field.api.php 6 Jun 2010 00:24:16 -0000 1.83 +++ modules/field/field.api.php 8 Jun 2010 13:42:06 -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"); } } Index: modules/field/field.attach.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.attach.inc,v retrieving revision 1.89 diff -u -F '^[fc]' -r1.89 field.attach.inc --- modules/field/field.attach.inc 6 Jun 2010 00:24:16 -0000 1.89 +++ modules/field/field.attach.inc 8 Jun 2010 13:42:06 -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 Index: modules/field/field.crud.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.crud.inc,v retrieving revision 1.63 diff -u -F '^[fc]' -r1.63 field.crud.inc --- modules/field/field.crud.inc 5 Jun 2010 12:05:25 -0000 1.63 +++ modules/field/field.crud.inc 8 Jun 2010 13:42:06 -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". */ - Index: modules/field/field.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.module,v retrieving revision 1.74 diff -u -F '^[fc]' -r1.74 field.module --- modules/field/field.module 26 May 2010 11:54:19 -0000 1.74 +++ modules/field/field.module 8 Jun 2010 13:42:07 -0000 @@ -102,28 +102,6 @@ class FieldException extends Exception { 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(); } /** Index: modules/field/modules/field_sql_storage/field_sql_storage.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/field_sql_storage/field_sql_storage.module,v retrieving revision 1.46 diff -u -F '^[fc]' -r1.46 field_sql_storage.module --- modules/field/modules/field_sql_storage/field_sql_storage.module 6 May 2010 15:29:51 -0000 1.46 +++ modules/field/modules/field_sql_storage/field_sql_storage.module 8 Jun 2010 13:42:07 -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(); - - $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++; - } - } - } 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; + // 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']); + } + + 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.'); + } + $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; } /** Index: modules/field/modules/list/list.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/list/list.module,v retrieving revision 1.32 diff -u -F '^[fc]' -r1.32 list.module --- modules/field/modules/list/list.module 12 May 2010 08:55:47 -0000 1.32 +++ modules/field/modules/list/list.module 8 Jun 2010 13:42:07 -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. */ Index: modules/field/tests/field.test =================================================================== RCS file: /cvs/drupal/drupal/modules/field/tests/field.test,v retrieving revision 1.31 diff -u -F '^[fc]' -r1.31 field.test --- modules/field/tests/field.test 23 May 2010 19:10:23 -0000 1.31 +++ modules/field/tests/field.test 8 Jun 2010 13:42:08 -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'); } Index: modules/field/tests/field_test.entity.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/tests/field_test.entity.inc,v retrieving revision 1.10 diff -u -F '^[fc]' -r1.10 field_test.entity.inc --- modules/field/tests/field_test.entity.inc 23 May 2010 19:10:23 -0000 1.10 +++ modules/field/tests/field_test.entity.inc 8 Jun 2010 13:42:08 -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, + ), ); } Index: modules/field/tests/field_test.field.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/tests/field_test.field.inc,v retrieving revision 1.10 diff -u -F '^[fc]' -r1.10 field_test.field.inc --- modules/field/tests/field_test.field.inc 18 May 2010 18:30:49 -0000 1.10 +++ modules/field/tests/field_test.field.inc 8 Jun 2010 13:42:08 -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, + ), + ), + ); + } } /** Index: modules/field/tests/field_test.install =================================================================== RCS file: /cvs/drupal/drupal/modules/field/tests/field_test.install,v retrieving revision 1.2 diff -u -F '^[fc]' -r1.2 field_test.install --- modules/field/tests/field_test.install 4 Dec 2009 16:49:46 -0000 1.2 +++ modules/field/tests/field_test.install 8 Jun 2010 13:42:08 -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( Index: modules/file/file.module =================================================================== RCS file: /cvs/drupal/drupal/modules/file/file.module,v retrieving revision 1.28 diff -u -F '^[fc]' -r1.28 file.module --- modules/file/file.module 9 May 2010 19:34:26 -0000 1.28 +++ modules/file/file.module 8 Jun 2010 13:42:08 -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(); } } Index: modules/simpletest/simpletest.info =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/simpletest.info,v retrieving revision 1.16 diff -u -F '^[fc]' -r1.16 simpletest.info --- modules/simpletest/simpletest.info 3 Feb 2010 18:16:23 -0000 1.16 +++ modules/simpletest/simpletest.info 8 Jun 2010 13:42:08 -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 Index: modules/system/system.api.php =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.api.php,v retrieving revision 1.168 diff -u -F '^[fc]' -r1.168 system.api.php --- modules/system/system.api.php 23 May 2010 19:10:23 -0000 1.168 +++ modules/system/system.api.php 8 Jun 2010 13:42:09 -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