=== modified file 'includes/entity.inc'
--- includes/entity.inc	2010-03-27 05:52:49 +0000
+++ includes/entity.inc	2010-05-07 18:56:15 +0000
@@ -298,3 +298,346 @@ 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 a EntityFieldQueryException when an usupported
+ * condition was specified.
+ */
+class EntityFieldQueryException extends Exception {}
+
+/**
+ * Retrieve entities matching a given set of conditions.
+ *
+ * Storage engines are not required to support all kinds of queries. A
+ * EntityFieldQueryException will be raised if an unsupported condition is specified
+ * or if the query has field conditions / sorts which are stored in different
+ * field storage engines. It entirely depends on the field storage engine what
+ * sort of queries it supports and what kind it does not. For example, if a
+ * field is stored in a separate datastore than the entities they belong to
+ * then it's very likely that passing in both a fieldCondition and an
+ * entityCondition will raise this exception.
+ *
+ * The class methods itself just collection information passed to the methods,
+ * there is no logic before hook_entity_query() is fired in the execute method
+ * so this hook can change EntityFieldQuery totally. If this hook does not
+ * provide a result, then EntityFieldQuery::execute() finds the field storage
+ * engine and hands over the query to hook_field_storage_query. If there are
+ * neither field conditions nor field orders then
+ * EntityFieldQuery::entityQuery() queries the SQL entity tables.
+ */
+class EntityFieldQuery {
+  /**
+   * List of field conditions.
+   *
+   * @see EntityFieldQuery::fieldCondition().
+   */
+  protected $fieldConditions = array();
+
+  /**
+   * List of entity conditions.
+   *
+   * @see EntityFieldQuery::entityCondition().
+   */
+  protected $entityConditions = array();
+
+  /**
+   * List of entity types.
+   *
+   * @see EntityFieldQuery::entityTypes().
+   */
+  protected $entityTypes = array();
+
+  /**
+   * List of orders on fields.
+   *
+   * @see EntityFieldQuery::fieldOrderBy().
+   */
+  protected $fieldOrder = array();
+
+  /**
+   * List of orders on entities.
+   *
+   * @see EntityFieldQuery::entityOrderBy().
+   */
+  protected $entityOrder = array();
+
+  /**
+   * The query range.
+   *
+   * @see EntityFieldQuery::range().
+   */
+  protected $range = array();
+
+  /**
+   * Whether to query the deleted column and it's value.
+   *
+   * @see EntityFieldQuery::deleted().
+   */
+  protected $deleted = FALSE;
+
+  /**
+   * The field names used.
+   */
+  protected $fieldNames = array();
+
+  /**
+   * A condition on field values.
+   *
+   * @param $field_name
+   *   Name of the field.
+   * @param $column
+   *   A column defined in the hook_field_schema() of this field. entity_id and
+   *   bundle can also be used.
+   * @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 the $operator.
+   * @param $operator
+   *   Possible values:
+   *   - '=', '!=', '>', '>=', '<', '<=', 'STARTS_WITH': 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.
+   * @param $delta_group
+   *   An arbitrary identifier, conditions in the same group must have the same
+   *   delta. For example, if a multivalue field has two columns, 'color' and
+   *   'shape' and two values are red square and blue circle and the query
+   *   should find a red circle then the two conditions
+   *   (color = red, shape = circle) will wrongly find this entity. If they
+   *   have the same delta then it will work as expected.
+   * @param $language_group
+   *   An arbitrary identifier, conditions in the same group must have the same
+   *   language.
+   * @return EntityFieldQuery
+   *   The called object.
+   */
+  public function fieldCondition($field_name, $column, $value, $operator = NULL, $delta_group = NULL, $language_group = NULL) {
+    $this->fieldConditions[] = array(
+      'field_name' => $field_name,
+      'column' => $column,
+      'value' => $value,
+      'operator' => $operator,
+      'delta_group' => $delta_group,
+      'language_group' => $language_group,
+    );
+    $this->fieldNames[] = $field_name;
+    return $this;
+  }
+
+  /**
+   * A condition on the entity base table.
+   *
+   * entityTypes() and entityCondition() can not be used on the same query as
+   * entityCondition() also indicates the single entity_type to be used.
+   *
+   * @param $entity_type
+   *   An entity type.
+   * @param $column
+   *   A column defined in the hook_schema() of the base table of this entity
+   *   type.
+   * @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 the $operator.
+   * @param $operator
+   *   Possible values:
+   *   - '=', '!=', '>', '>=', '<', '<=', 'STARTS_WITH': 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.
+   * @return EntityFieldQuery
+   *   The called object.
+   */
+  public function entityCondition($entity_type, $column, $value, $operator = NULL) {
+    $this->entityConditions[] = array(
+      'entity_type' => $entity_type,
+      'column' => $column,
+      'value' => $value,
+      'operator' => $operator,
+    );
+    return $this;
+  }
+
+  /**
+   * A list of entity types to restrict the query to.
+   *
+   * entityTypes() and entityCondition() can not be used on the same query as
+   * entityCondition() also indicates the single entity_type to be used.
+   *
+   * @param $entity_types
+   *   A list of entity types.
+   */
+  public function entityTypes($entity_types) {
+    $this->entityTypes = $entity_types;
+    return $this;
+  }
+
+  /**
+   * Orders the result set by a given entity column.
+   *
+   * 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 entityOrderBy($column, $direction) {
+    $this->entityOrder[] = array(
+      'column' => $column,
+      'direction' => $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_name
+   *   Name of the field.
+   * @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_name, $column, $direction) {
+    $this->fieldOrder[] = array(
+      'field_name' => $field_name,
+      'column' => $column,
+      'direction' => $direction,
+    );
+    $this->fieldNames[] = $field_name;
+    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 $limit
+   *   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;
+  }
+
+  /**
+   * Filter on the data being deleted.
+   *
+   * @param $deleted
+   *   TRUE to only return deleted data, FALSE to return non-deleted data, NULL
+   *   to return everything. Defaults to FALSE.
+   * @return EntityFieldQuery
+   *   The called object.
+   */
+  public function deleted($deleted) {
+    $this->deleted = $deleted;
+    return $this;
+  }
+
+
+  /**
+   * Execute the query.
+   *
+   * @return
+   *   An associative array, the keys are entity_types, the values are entities
+   *   as returned by entity_load() for that entity_type.
+   *   @code
+   *     foreach ($query->execute() as $entity_type => $entities) {
+   *       foreach ($entities as $entity_id => $entity) {
+   *   @endcode
+   */
+  public function execute() {
+    foreach (module_implements('entity_query') as $module) {
+      $function = $module . '_entity_query';
+      $result = $function($this->entityTypes, $this->entityConditions, $this->fieldConditions, $this->fieldOrder, $this->entityOrder, $this->range, $this->deleted);
+      if (isset($result)) {
+        return $result;
+      }
+    }
+    foreach ($this->fieldNames as $field_name) {
+      $field = field_info_field($field_name);
+      if (!isset($storage)) {
+        $storage = $field['storage'];
+      }
+      elseif (array_diff($storage, $field['storage'])) {
+        throw new EntityFieldQueryException(t("Can't handle more than one field storage engine"));
+      }
+    }
+    if ($this->fieldNames) {
+      $function = $storage['module'] . '_field_storage_query';
+      return $function($this->entityTypes, $this->entityConditions, $this->fieldConditions, $this->fieldOrder, $this->entityOrder, $this->range, $this->deleted);
+    }
+    return $this->entityQuery();
+  }
+
+  protected function entityQuery() {
+    $entity_type = '';
+    if ($this->entityConditions) {
+      $entity_type = $this->entityConditions[0]['entity_type'];
+    }
+    else {
+      $n = count($this->entityTypes);
+      if ($n == 1) {
+        $entity_type = $this->entityTypes[0];
+      }
+      elseif ($n > 1) {
+        throw new EntityFieldQueryException(t("Can't query multiple entity tables at once."));
+      }
+      else {
+        throw new EntityFieldQueryException(t('Empty query'));
+      }
+    }
+    $entity_info = entity_get_info($entity_type);
+    $base_table = isset($entity_info['base_table']) ? $entity_info['base_table'] : $entity_type;
+    $query = db_select($base_table);
+    $query->addField($base_table, $entity_info['entity keys']['id'], 'entity_id');
+    $query->addExpression(':entity_type', 'entity_type', array(':entity_type' => $entity_type));
+    foreach ($this->entityConditions as $entity_condition) {
+      if ($entity_type != $entity_condition['entity_type']) {
+        throw new EntityFieldQueryException(t("Can't query multiple entity tables at once."));
+      }
+      $query->condition($entity_condition['column'], $entity_condition['value'], $entity_condition['operator']);
+    }
+    foreach ($this->entityOrder as $order) {
+      $query->orderBy("$base_table." . $order['column'], $order['direction']);
+    }
+    if ($this->range) {
+      $query->range($this->range['start'], $this->range['length']);
+    }
+    $ids = array();
+    foreach ($query->execute() as $entity_id) {
+      $ids[] = $entity_id->entity_id;
+    }
+    return $ids ? array($entity_type => entity_load($entity_type, $ids)) : array();
+  }
+}

=== modified file 'modules/field/field.api.php'
--- modules/field/field.api.php	2010-05-06 05:59:30 +0000
+++ modules/field/field.api.php	2010-05-07 16:42:08 +0000
@@ -1432,24 +1432,29 @@ function hook_field_storage_delete_revis
 }
 
 /**
- * Handle a field query.
+ * Handle a entity + field query.
  *
- * This hook is invoked from field_attach_query() to ask the field storage
- * module to handle a field query.
+ * This hook is fired from EntityFieldQuery::execute() if no
+ * hook_entity_query() implementation returned any result.
  *
- * @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.
- *
- * @return
- *   See field_attach_query().
+ * @param $entity_types
+ *   A list of entity types as passed to EntityFieldQuery::entityTypes().
+ * @param $entity_conditions
+ *   An list of entity conditions. Each entry maps to one call of
+ *   EntityFieldQuery::entityConditions().
+ * @param $field_conditions
+ *   An list of field conditions. Each entry maps to one call of
+ *   EntityFieldQuery::fieldConditions().
+ * @param $field_order
+ *   An list of field orders. Each entry maps to one call of
+ *   EntityFieldQuery::fieldOrderBy().
+ * @param $range
+ *   The query range as passed to EntityFieldQuery::range().
+ * @param $deleted
+ *   Whether to delete with the deleted values as passed to
+ *   EntityFieldQuery::deleted()
  */
-function hook_field_storage_query($field_name, $conditions, $options) {
+function hook_field_storage_query($entity_types, $entity_conditions, $field_conditions, $field_order, $entity_order, $range, $deleted) {
   // @todo Needs function body
 }
 
@@ -1620,6 +1625,8 @@ function hook_field_storage_pre_update($
 }
 
 /**
+<<<<<<< TREE
+=======
  * Act before the storage backend runs the query.
  *
  * This hook should be implemented by modules that use
@@ -1648,6 +1655,7 @@ function hook_field_storage_pre_query($f
 }
 
 /**
+>>>>>>> MERGE-SOURCE
  * @} End of "ingroup field_storage"
  */
 
@@ -1714,8 +1722,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-04-04 12:48:18 +0000
+++ modules/field/field.attach.inc	2010-05-03 05:45:19 +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.
@@ -1042,132 +1033,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-04-24 07:19:09 +0000
+++ modules/field/field.crud.inc	2010-05-03 08:10:30 +0000
@@ -993,21 +993,17 @@ function field_purge_batch($batch_size) 
   foreach ($instances as $instance) {
     $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));
+    // Retrieve some entities.
+    $query = new EntityFieldQuery;
+    $results = $query
+      ->fieldCondition($field['field_name'], 'bundle', $instance['bundle'])
+      ->deleted(TRUE)
+      ->range(0, $batch_size)
+      ->execute();
 
+    if ($results) {
+      foreach ($results as $entity_type => $entities) {
         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;
-          }
-
           // Purge the data for the entity.
           field_purge_data($entity_type, $entity, $field, $instance);
         }
@@ -1119,4 +1115,3 @@ function field_purge_field($field) {
 /**
  * @} End of "defgroup field_purge".
  */
-

=== modified file 'modules/field/field.module'
--- modules/field/field.module	2010-04-13 15:23:02 +0000
+++ modules/field/field.module	2010-05-03 07:42:36 +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 {}
@@ -653,8 +631,10 @@ 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();
+  $query->fieldCondition($field['field_name'], 'entity_id', 0, '>');
+  $query->range(0, 1);
+  return (bool) $query->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-05-07 18:21:47 +0000
@@ -77,7 +77,7 @@ function _field_sql_storage_revision_tab
  *   table that is unique among all other fields.
  */
 function _field_sql_storage_columnname($name, $column) {
-  return $name . '_' . $column;
+  return in_array($column, array('bundle', 'entity_id')) ? $column : $name . '_' . $column;
 }
 
 /**
@@ -482,129 +482,121 @@ 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($entity_types, $entity_conditions, $field_conditions, $field_order, $entity_order, $range, $deleted) {
+  $groups = array();
+  foreach ($field_conditions as $key => $condition) {
+    $field = field_info_field($condition['field_name']);
+    $tablename = _field_sql_storage_tablename($field);
+    if (isset($query)) {
+      // Every condition needs a new table.
+      $table_alias = $query->join($tablename, $tablename, "$tablename.etid = $field_base_table.etid AND $tablename.entity_id = $field_base_table.entity_id");
+    }
+    else {
+      $query = db_select($tablename);
+      $query->addField($tablename, 'entity_id', 'entity_id');
+      // As only a numeric ID is stored instead of the entity type so add the
+      // field_config_entity_type table to resolve the etid to a more readable
+      // name.
+      $query->join('field_config_entity_type', 'fcet', "fcet.etid = $tablename.etid");
+      $query->addField('fcet', 'type', 'entity_type');
+      $table_alias = $field_base_table = $tablename;
+    }
+    $columnname = _field_sql_storage_columnname($condition['field_name'], $condition['column']);
+    // Add the condition itself.
+    $query->condition("$table_alias.$columnname", $condition['value'], $condition['operator']);
+    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 {
+          $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++;
+  }
+  if ($entity_conditions) {
+    $entity_type = '';
+    foreach ($entity_conditions as $entity_condition) {
+      if (!$entity_type) {
+        $entity_type = $entity_condition['entity_type'];
+        $entity_base_table = _field_sql_storage_query_join_entity($query, $entity_type, $field_base_table);
+      }
+      elseif ($entity_type != $entity_condition['entity_type']) {
+        throw new EntityFieldQueryException(t("Can't have conditions on two different entity types"));
       }
+      $query->condition("$entity_base_table." . $entity_condition['column'], $entity_condition['value'], isset($entity_condition['operator']) ? $entity_condition['operator'] : NULL);
     }
-  } 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_types = $entity_type;
+  }
+  // $entity_types might be an array passing to EntityFieldQuery::entityTypes()
+  // or it might be the single entity type from the entity conditions. DBTNG
+  // does the right thing for an array (IN) and a string (=) both if the
+  // operator is not specified, which is exactly what we need. The
+  // necessary table was added when the $query was created.
+  if ($entity_types) {
+    $query->condition('fcet.type', $entity_types);
+  }
+  foreach ($entity_order as $order) {
+    if (!isset($entity_base_table)) {
+      $entity_base_table = _field_sql_storage_query_join_entity($query, $entity_types[0], $field_base_table);
+    }
+    $query->orderBy("$entity_base_table." . $order['column'], $order['direction']);
   }
+  foreach ($field_order as $order) {
+    $columnname = _field_sql_storage_columnname($order['field_name'], $order['column']);
+    $field = field_info_field($order['field_name']);
+    $tablename = _field_sql_storage_tablename($field);
+    $query->orderBy("$tablename.$columnname", $order['direction']);
+  }
+  if ($range) {
+    $query->range($range['start'], $range['length']);
+  }
+  if (isset($deleted)) {
+    $query->condition("$table_alias.deleted", (int) $deleted);
+  }
+  $entities = array();
+  $all_ids = array();
+  foreach ($query->execute() as $partial_entity) {
+    $all_ids[$partial_entity->entity_type][] = $partial_entity->entity_id;
+  }
+  foreach ($all_ids as $entity_type => $ids) {
+    $entities[$entity_type] = entity_load($entity_type, $ids);
+  }
+  return $entities;
+}
 
-  return $return;
+/**
+ * Add the base entity table to a field query object.
+ *
+ * @param $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 $query. As only INNER JOINs are used, it does not
+ *   matter which.
+ */
+function _field_sql_storage_query_join_entity(SelectQuery $query, $entity_type, $field_base_table) {;
+  $entity_info = entity_get_info($entity_type);
+  $entity_base_table = isset($entity_info['base_table']) ? $entity_info['base_table'] : $entity_type;
+  // Figure out whether the entity is revisioned or not and JOIN based on this
+  // information.
+  if (empty($entity_info['entity keys']['revision'])) {
+    $entity_field = $entity_info['entity keys']['id'];
+    $field = 'entity_id';
+  }
+  else {
+    $entity_field = $entity_info['entity keys']['revision'];
+    $field = 'revision_id';
+  }
+  $query->join($entity_base_table, $entity_base_table, "$entity_base_table.$entity_field = $field_base_table.$field");
+  return $entity_base_table;
 }
 
+
 /**
  * Implements hook_field_storage_delete_revision().
  *

=== modified file 'modules/field/modules/list/list.module'
--- modules/field/modules/list/list.module	2010-05-06 05:59:30 +0000
+++ modules/field/modules/list/list.module	2010-05-07 16:42:08 +0000
@@ -101,7 +101,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-06 05:59:30 +0000
+++ modules/field/tests/field.test	2010-05-07 18:52:20 +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'));
-  }
 }
 
 /**
@@ -2945,7 +2736,7 @@ class FieldBulkDeleteTestCase extends Fi
 
     // For each bundle, create an instance of each field, and 10
     // entities with values for each field.
-    $id = 0;
+    $id = 1;
     $this->entity_type = 'test_entity';
     foreach ($this->bundles as $bundle) {
       foreach ($this->fields as $field) {
@@ -2966,6 +2757,8 @@ class FieldBulkDeleteTestCase extends Fi
           $entity->{$field['field_name']}[LANGUAGE_NONE] = $this->_generateTestFieldValues($field['cardinality']);
         }
         $this->entities[$id] = $entity;
+        drupal_write_record('test_entity', $entity);
+        drupal_write_record('test_entity_revision', $entity);
         field_attach_insert($this->entity_type, $entity);
         $id++;
       }
@@ -2977,7 +2770,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().
    */
@@ -2986,8 +2779,12 @@ 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['field_name'], 'bundle', $bundle)
+      ->execute();
     $this->assertEqual(count($found['test_entity']), 10, 'Correct number of entities found before deleting');
+    return;
 
     // Delete the instance.
     $instance = field_info_instance($this->entity_type, $field['field_name'], $bundle);
@@ -2999,13 +2796,17 @@ 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));
+    $found = $query
+      ->fieldCondition($field['field_name'], '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));
-    field_attach_load($this->entity_type, $found[$this->entity_type], FIELD_LOAD_CURRENT, array('field_id' => $field['id'], 'deleted' => 1));
+    $found = $query
+      ->fieldCondition($field['field_name'], 'bundle', $bundle)
+      ->deleted(TRUE)
+      ->execute();
     $this->assertEqual(count($found['test_entity']), 10, 'Correct number of entities found after deleting');
     foreach ($found['test_entity'] as $id => $entity) {
       $this->assertEqual($this->entities[$id]->{$field['field_name']}, $entity->{$field['field_name']}, "Entity $id with deleted data loaded correctly");
@@ -3036,7 +2837,11 @@ 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['field_name'], '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-06 05:59:30 +0000
+++ modules/field/tests/field_test.entity.inc	2010-05-07 17:16:53 +0000
@@ -25,6 +25,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',
@@ -46,6 +48,29 @@ function field_test_entity_info() {
       'bundles' => $bundles,
       'view modes' => $test_entity_modes,
     ),
+    'test_entity_1' => array(
+      'name' => t('Test Entity 1'),
+      'base table' => 'test_entity_1',
+      '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_2' => array(
+      'name' => t('Test Entity 2'),
+      'base table' => 'test_entity_2',
+      '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-03-12 19:51:40 +0000
+++ modules/field/tests/field_test.field.inc	2010-05-03 09:24:02 +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.'),
+      'settings' => array(),
+      'instance_settings' => array(),
+      'default_widget' => 'test_field_widget',
+      'default_formatter' => 'field_test_default',
+    ),
   );
 }
 
@@ -33,18 +41,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,
+        ),
+      ),
+    );
+  }
 }
 
 /**
@@ -63,7 +89,7 @@ function field_test_field_load($entity_t
   foreach ($items as $id => $item) {
     // To keep the test non-intrusive, only act for instances with the
     // test_hook_field_load setting explicitly set to TRUE.
-    if ($instances[$id]['settings']['test_hook_field_load']) {
+    if (isset($instances[$id]['settings']['test_hook_field_load'])) {
       foreach ($item as $delta => $value) {
         // Don't add anything on empty values.
         if ($value) {

=== 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-05-07 17:25:36 +0000
@@ -41,7 +41,7 @@ function field_test_schema() {
         'description' => 'The type of this test_entity.',
         'type' => 'varchar',
         'length' => 32,
-        'not null' => TRUE,
+        'not null' => FALSE,
         'default' => '',
       ),
     ),
@@ -50,6 +50,37 @@ function field_test_schema() {
     ),
     'primary key' => array('ftid'),
   );
+  $schema['test_entity_1'] = array(
+    'description' => 'The base table for test_entities.',
+    'fields' => array(
+      'ftid' => array(
+        'description' => 'The {test_entity} this version belongs to.',
+        '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_2'] = array(
+    'description' => 'Stores information about each saved version of a {test_entity}.',
+    'fields' => array(
+      'ftid' => array(
+        'description' => 'The {test_entity} this version belongs to.',
+        '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(
@@ -66,6 +97,20 @@ function field_test_schema() {
         'unsigned' => TRUE,
         'not null' => TRUE,
       ),
+      // Until the TODO in DrupalDefaultEntityController::buildQuery is
+      // resolved, a timestamp and an uid is mandatory.
+      'timestamp' => array(
+        'description' => 'A Unix timestamp indicating when this version was created.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'uid' => array(
+        'description' => 'The {users}.uid that created this version.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
     ),
     'indexes' => array(
       'nid' => array('ftid'),

=== modified file 'modules/simpletest/drupal_web_test_case.php'
--- modules/simpletest/drupal_web_test_case.php	2010-05-06 05:59:30 +0000
+++ modules/simpletest/drupal_web_test_case.php	2010-05-07 18:13:04 +0000
@@ -1280,7 +1280,7 @@ class DrupalWebTestCase extends DrupalTe
       $schema = drupal_get_schema(NULL, TRUE);
       $ret = array();
       foreach ($schema as $name => $table) {
-        db_drop_table($name);
+#        db_drop_table($name);
       }
 
       // Return the database prefix to the original.

=== modified file 'modules/simpletest/simpletest.info'
--- modules/simpletest/simpletest.info	2010-02-03 18:16:22 +0000
+++ modules/simpletest/simpletest.info	2010-05-03 09:04:29 +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-05-03 09:54:39 +0000
@@ -0,0 +1,304 @@
+<?php
+
+// $Id$
+
+/**
+ * @file
+ * Unit test file for the entity API.
+ */
+
+/**
+ * Tests EntityFieldQuery.
+ */
+class EntityFieldQueryTestCase extends DrupalWebTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Entity query',
+      'description' => 'Test the EntityFieldQuery class.',
+      'group' => 'Entity API',
+    );
+  }
+
+  function setUp() {
+    parent::setUp(array('field_test'));
+
+    field_attach_create_bundle('bundle1', 'test_entity_1');
+    field_attach_create_bundle('bundle2', 'test_entity_1');
+    field_attach_create_bundle('test_entity_2', 'test_entity_2');
+
+    $instances = array();
+    $this->fields = array();
+    $this->fields[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);
+    $field_id = $field['id'];
+    $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_1';
+    field_create_instance($instances[0]);
+    $instances[0]['bundle'] = $instances[0]['entity_type'] = 'test_entity_2';
+    field_create_instance($instances[0]);
+
+    $this->fields[1] = $field_name = drupal_strtolower($this->randomName() . '_field_name');
+    $field = array('field_name' => $field_name, 'type' => 'shape', 'cardinality' => 4);
+    $field = field_create_field($field);
+    $field_id = $field['id'];
+    $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_1';
+    field_create_instance($instances[1]);
+    $instances[1]['bundle'] = $instances[1]['entity_type'] = 'test_entity_2';
+    field_create_instance($instances[1]);
+
+    // Write entity base table if there is one.
+    $entities = array();
+
+    // First, of type test_entity_1
+    for ($i = 1; $i < 5; $i++) {
+      $entity = new stdClass;
+      $entity->ftid = $i;
+      $entity->fttype = 'bundle1';
+      $entity->{$this->fields[0]}[LANGUAGE_NONE][0]['value'] = $i;
+      drupal_write_record('test_entity_1', $entity);
+      field_attach_insert('test_entity_1', $entity);
+    }
+    $entity = new stdClass;
+    $entity->ftid = 5;
+    $entity->fttype = 'bundle1';
+    $entity->{$this->fields[1]}[LANGUAGE_NONE][0]['shape'] = 'square';
+    $entity->{$this->fields[1]}[LANGUAGE_NONE][0]['color'] = 'red';
+    $entity->{$this->fields[1]}[LANGUAGE_NONE][1]['shape'] = 'circle';
+    $entity->{$this->fields[1]}[LANGUAGE_NONE][1]['color'] = 'blue';
+    drupal_write_record('test_entity_2', $entity);
+    field_attach_insert('test_entity_2', $entity);
+  }
+
+  /**
+   * Test field_attach_query().
+   */
+  function testEntityFieldQuery() {
+    // First, test without options.
+    $query = new EntityFieldQuery();
+    $query->fieldCondition($this->fields[0], 'value', 3, '=');
+    $this->EntityFieldQueryHelper($query, array(
+      array('test_entity_1', 3),
+    ), t('Test the "equal to" operation.'));
+    $query = new EntityFieldQuery();
+    $query->fieldCondition($this->fields[0], 'value', 2, '>');
+    $this->EntityFieldQueryHelper($query, array(
+      array('test_entity_1', 3),
+      array('test_entity_1', 4),
+    ), t('Test the "greater than" operation.'));
+
+    $query = new EntityFieldQuery();
+    $query->fieldCondition($this->fields[0], 'value', array(3, 4), 'IN');
+    $this->EntityFieldQueryHelper($query, array(
+      array('test_entity_1', 3),
+      array('test_entity_1', 4),
+    ), t('Test the "in" operation.'));
+
+    $query = new EntityFieldQuery();
+    $query->fieldCondition($this->fields[0], 'value', array(1, 3), 'BETWEEN');
+    $this->EntityFieldQueryHelper($query, array(
+      array('test_entity_1', 1),
+      array('test_entity_1', 2),
+      array('test_entity_1', 3),
+    ), t('Test the "between" operation.'));
+
+    $query = new EntityFieldQuery();
+    $query->fieldCondition($this->fields[0], 'value', 3);
+    $this->EntityFieldQueryHelper($query, array(
+      array('test_entity_1', 3),
+    ), t('Test omission of an operator with a single item.'));
+
+    $query = new EntityFieldQuery();
+    $query->fieldCondition($this->fields[0], 'value', array(3, 4));
+    $this->EntityFieldQueryHelper($query, array(
+      array('test_entity_1', 3),
+      array('test_entity_1', 4),
+    ), t('Test omission of an operator with multiple items.'));
+
+    $query = new EntityFieldQuery();
+    $query->entityCondition('test_entity_1', 'ftid', 1);
+    $this->EntityFieldQueryHelper($query, array(
+      array('test_entity_1', 1),
+    ), t('Test entity conditions.'));
+
+    $query = new EntityFieldQuery();
+    $query->entityCondition('test_entity_1', 'ftid', 1, '>');
+    $query->fieldCondition($this->fields[0], 'value', 4, '<');
+    $this->EntityFieldQueryHelper($query, array(
+      array('test_entity_1', 2),
+      array('test_entity_1', 3),
+    ), t('Test entity and field conditions.'));
+
+    $query = new EntityFieldQuery();
+    $query->fieldCondition($this->fields[0], 'value', 0, '>');
+    $query->fieldOrderBy($this->fields[0], 'value', 'asc');
+    $this->EntityFieldQueryHelper($query, array(
+      array('test_entity_1', 1),
+      array('test_entity_1', 2),
+      array('test_entity_1', 3),
+      array('test_entity_1', 4),
+    ), t('Field sort: ascending.'), TRUE);
+
+    $query = new EntityFieldQuery();
+    $query->fieldCondition($this->fields[0], 'value', 0, '>');
+    $query->fieldOrderBy($this->fields[0], 'value', 'desc');
+    $this->EntityFieldQueryHelper($query, array(
+      array('test_entity_1', 4),
+      array('test_entity_1', 3),
+      array('test_entity_1', 2),
+      array('test_entity_1', 1),
+    ), t('Field sort: descending.'), TRUE);
+
+    $query = new EntityFieldQuery();
+    $query->entityTypes(array('test_entity_1'));
+    $query->entityOrderBy('ftid', 'asc');
+    $this->EntityFieldQueryHelper($query, array(
+      array('test_entity_1', 1),
+      array('test_entity_1', 2),
+      array('test_entity_1', 3),
+      array('test_entity_1', 4),
+    ), t('Entity sort: ascending.'), TRUE);
+
+    $query = new EntityFieldQuery();
+    $query->entityTypes(array('test_entity_1'));
+    $query->entityOrderBy('ftid', 'desc');
+    $this->EntityFieldQueryHelper($query, array(
+      array('test_entity_1', 4),
+      array('test_entity_1', 3),
+      array('test_entity_1', 2),
+      array('test_entity_1', 1),
+    ), t('Entity sort: descending.'), TRUE);
+
+    $query = new EntityFieldQuery();
+    $query->entityTypes(array('test_entity_1'));
+    $query->entityOrderBy('ftid', 'asc');
+    $query->range(0, 2);
+    $this->EntityFieldQueryHelper($query, array(
+      array('test_entity_1', 1),
+      array('test_entity_1', 2),
+    ), t('Test limit.'), TRUE);
+
+    $query = new EntityFieldQuery();
+    $query->entityTypes(array('test_entity_1'));
+    $query->entityOrderBy('ftid', 'asc');
+    $query->range(2, 4);
+    $this->EntityFieldQueryHelper($query, array(
+      array('test_entity_1', 3),
+      array('test_entity_1', 4),
+    ), t('Test offset.'), TRUE);
+
+    for ($i = 6; $i < 10; $i++) {
+      $entity = new stdClass;
+      $entity->ftid = $i;
+      $entity->{$this->fields[0]}[LANGUAGE_NONE][0]['value'] = $i - 5;
+      drupal_write_record('test_entity_2', $entity);
+      field_attach_insert('test_entity_2', $entity);
+    }
+
+    $query = new EntityFieldQuery();
+    $query->fieldCondition($this->fields[0], 'value', 2, '>');
+    $this->EntityFieldQueryHelper($query, array(
+      array('test_entity_1', 3),
+      array('test_entity_1', 4),
+      array('test_entity_2', 8),
+      array('test_entity_2', 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->EntityFieldQueryHelper($query, array(
+      array('test_entity_2', 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->EntityFieldQueryHelper($query, array(), t('Test with a delta group.'));
+
+    $pass = FALSE;
+    $query = new EntityFieldQuery();
+    try {
+      $query->execute();
+    }
+    catch (EntityFieldQueryException $exception) {
+      $pass = ($exception->getMessage() == t('Empty query'));
+    }
+    $this->assertTrue($pass, t("Can't query the universe."));
+  }
+
+  /**
+   * Fetch the results of an EntityFieldQuery and compare.
+   *
+   * @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 EntityFieldQueryHelper($query, $intended_results, $message, $ordered = FALSE) {
+    $results = array();
+    foreach ($query->execute() as $entity_type => $entities) {
+      foreach ($entities as $entity_id => $entity) {
+        // These should be the test entities where that field is 2 or more.
+        $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-07 12:59:07 +0000
+++ modules/system/system.api.php	2010-05-07 16:42:08 +0000
@@ -268,6 +268,36 @@ function hook_entity_update($entity, $ty
 }
 
 /**
+ * Handle a entity + field query.
+ *
+ * This hook can change the behaviour EntityFieldQuery as necessary, see the
+ * return value of the function why.
+ *
+ * @param $entity_types
+ *   A list of entity types as passed to EntityFieldQuery::entityTypes().
+ * @param $entity_conditions
+ *   An list of entity conditions. Each entry maps to one call of
+ *   EntityFieldQuery::entityConditions().
+ * @param $field_conditions
+ *   An list of field conditions. Each entry maps to one call of
+ *   EntityFieldQuery::fieldConditions().
+ * @param $field_order
+ *   An list of field orders. Each entry maps to one call of
+ *   EntityFieldQuery::fieldOrderBy().
+ * @param $range
+ *   The query range as passed to EntityFieldQuery::range().
+ * @param $deleted
+ *   Whether to delete with the deleted values as passed to
+ *   EntityFieldQuery::deleted().
+ * @return
+ *   If anything but NULL is returned that's going to be the return value of
+ *   EntityFieldQuery::execute() immediately, so hook_field_storage_query won't
+ *   be fired automatically.
+ */
+function hook_entity_query($entity_types, $entity_conditions, $field_conditions, $field_order, $entity_order, $range, $deleted) {
+}
+
+/**
  * Define administrative paths.
  *
  * Modules may specify whether or not the paths they define in hook_menu() are

