Index: modules/field/tests/field.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/tests/field.test,v
retrieving revision 1.31
diff -u -r1.31 field.test
--- modules/field/tests/field.test	23 May 2010 19:10:23 -0000	1.31
+++ modules/field/tests/field.test	24 May 2010 20:08:29 -0000
@@ -623,215 +623,6 @@
     $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 @@
    * 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 @@
     $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 @@
     $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 @@
       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.field.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/tests/field_test.field.inc,v
retrieving revision 1.10
diff -u -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	24 May 2010 20:08:29 -0000
@@ -26,6 +26,14 @@
       '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 @@
  * 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 -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	24 May 2010 20:08:29 -0000
@@ -50,6 +50,37 @@
     ),
     '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/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 -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	24 May 2010 20:08:29 -0000
@@ -27,6 +27,8 @@
       '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 @@
       '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/system/system.api.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.api.php,v
retrieving revision 1.168
diff -u -r1.168 system.api.php
--- modules/system/system.api.php	23 May 2010 19:10:23 -0000	1.168
+++ modules/system/system.api.php	24 May 2010 20:08:29 -0000
@@ -285,6 +285,25 @@
 }
 
 /**
+ * Alter or execute an EntityFieldQuery.
+ *
+ * @param EntityFieldQuery $query
+ *   An EntityFieldQuery, the properties and helper methods are documented
+ *   in the class. 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 result of EntityFieldQuery::execute(). This can be used to
+ *   change the behaviour of EntityFieldQuery entirely. For example, the
+ *   default implementation can only deal with one field storage engine, it's
+ *   possible to write a module that can query across field storage engines.
+ *   Also, the default implementation presumes entities are stored in SQL, the
+ *   execute callback can instead query any other entity storage, local or
+ *   remote.
+ */
+function hook_entity_query_alter($query) {
+}
+
+/**
  * Define administrative paths.
  *
  * Modules may specify whether or not the paths they define in hook_menu() are
Index: modules/file/file.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/file/file.module,v
retrieving revision 1.28
diff -u -r1.28 file.module
--- modules/file/file.module	9 May 2010 19:34:26 -0000	1.28
+++ modules/file/file.module	24 May 2010 20:08:29 -0000
@@ -969,8 +969,11 @@
   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/field/field.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.module,v
retrieving revision 1.73
diff -u -r1.73 field.module
--- modules/field/field.module	23 May 2010 19:10:23 -0000	1.73
+++ modules/field/field.module	24 May 2010 20:08:28 -0000
@@ -102,28 +102,6 @@
 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 @@
  *   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/field.api.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.api.php,v
retrieving revision 1.82
diff -u -r1.82 field.api.php
--- modules/field/field.api.php	23 May 2010 19:10:23 -0000	1.82
+++ modules/field/field.api.php	24 May 2010 20:08:28 -0000
@@ -1440,24 +1440,21 @@
 }
 
 /**
- * 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.
- *
- * @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.
+ * 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 those have been handled.
+ *
+ * @param EntityFieldQuery $query
+ *   An EntityFieldQuery, the properties and helper methods are documented
+ *   in the class.
  *
  * @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
 }
 
@@ -1807,8 +1804,12 @@
     // 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.crud.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.crud.inc,v
retrieving revision 1.60
diff -u -r1.60 field.crud.inc
--- modules/field/field.crud.inc	23 May 2010 19:10:23 -0000	1.60
+++ modules/field/field.crud.inc	24 May 2010 20:08:28 -0000
@@ -1036,25 +1036,23 @@
   $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 @@
 /**
  * @} End of "defgroup field_purge".
  */
-
Index: modules/field/field.attach.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.attach.inc,v
retrieving revision 1.88
diff -u -r1.88 field.attach.inc
--- modules/field/field.attach.inc	23 May 2010 19:10:23 -0000	1.88
+++ modules/field/field.attach.inc	24 May 2010 20:08:28 -0000
@@ -31,15 +31,6 @@
 }
 
 /**
- * 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.
@@ -604,6 +595,7 @@
   );
   $options += $default_options;
 
+if (!$entity_type) file_put_contents('/tmp/log', var_export(debug_backtrace(), TRUE));
   $info = entity_get_info($entity_type);
   // Only the most current revision of non-deleted fields for cacheable entity
   // types can be cached.
@@ -1046,132 +1038,6 @@
 }
 
 /**
- * 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: includes/entity.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/entity.inc,v
retrieving revision 1.8
diff -u -r1.8 entity.inc
--- includes/entity.inc	9 May 2010 13:27:31 -0000	1.8
+++ includes/entity.inc	24 May 2010 20:08:27 -0000
@@ -290,3 +290,616 @@
     $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 usupported
+ * condition was specified.
+ */
+class EntityFieldQueryException extends Exception {}
+
+/**
+ * Retrieve entities matching a given set of conditions.
+ *
+ * This class allows finding entities based on entity properties (for eg.
+ * node.changed), field values and generic entity meta data (bundle,
+ * entity type, entity id, 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 users.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' 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 to the query.
+ *
+ * Storage engines are not required to support all kinds of queries. By default
+ * an 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. However, this logic can be overridden in
+ * hook_entity_query().
+ */
+class EntityFieldQuery {
+  /**
+   * 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 orders on entity-generic metadata.
+   *
+   * @var array
+   * @see EntityFieldQuery::entityOrderBy()
+   */
+  public $entityOrder = array();
+
+  /**
+   * List of orders on fields.
+   *
+   * @var array
+   * @see EntityFieldQuery::fieldOrderBy()
+   */
+  public $fieldOrder = array();
+
+  /**
+   * List of orders on entities.
+   *
+   * @var array
+   * @see EntityFieldQuery::entityOrderBy()
+   */
+  public $propertyOrder = array();
+
+  /**
+   * The query range.
+   *
+   * @var array
+   * @see EntityFieldQuery::range()
+   */
+  public $range = array();
+
+  /**
+   * Whether to query the deleted column and it's value.
+   *
+   * @see EntityFieldQuery::deleted()
+   */
+  public $deleted = FALSE;
+
+  /**
+   * The field ids used.
+   *
+   * @var array
+   */
+  public $fields = array();
+
+  /*
+   * Is this a count query?
+   *
+   * @var boolean
+   */
+  public $count = FALSE;
+
+
+  /**
+   * Current or revision?
+   *
+   * @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.
+   *
+   * @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 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, use $name entity_type, a string
+   * $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 to test $name 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', '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.
+   *
+   * @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
+   *   ommitted then the field_id is used only to find the storage engine to
+   *   talk to and that will run a query based on entity and property
+   *   conditions.
+   * @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', '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.
+   * @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, $column = NULL, $value = NULL, $operator = NULL, $delta_group = NULL, $language_group = NULL) {
+    if (is_scalar($field)) {
+      $field = field_info_field($field);
+    }
+    if (isset($column)) {
+      $this->fieldConditions[] = array(
+        'field' => $field,
+        'column' => $column,
+        'value' => $value,
+        'operator' => $operator,
+        'delta_group' => $delta_group,
+        'language_group' => $language_group,
+      );
+    }
+    $this->fields[] = $field;
+    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, 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 the $operator.
+   * @param $operator
+   *   Possible values:
+   *   - '=', '!=', '>', '>=', '<', '<=', 'STARTS_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.
+   *
+   * @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);
+    }
+    $this->fieldOrder[] = array(
+      'field' => $field,
+      'column' => $column,
+      'direction' => $direction,
+    );
+    $this->fields[] = $field;
+    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 $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;
+  }
+
+  /**
+   * Filters 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 = TRUE) {
+    $this->deleted = $deleted;
+    return $this;
+  }
+
+  /**
+   * Queries the current or every revision.
+   *
+   * Note that this only affects field condiitions, properties always use the
+   * base table.
+   *
+   * @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.
+   *
+   * @return
+   *   Either a number if count() was called or a two dimension associative
+   *   array, The first keys are entity_types, the second key is the relevant
+   *   id. In most this case this will be entity id the only exception is when
+   *   age(FIELD_LOAD_REVISION) is called and field conditions / sorts are
+   *   present -- in this case the key will be revision id. The values are
+   *   always stub entities as returned by entity_create_stub_entity().
+   *   @code
+   *     foreach ($query->execute() as $entity_type => $entities) {
+   *       foreach ($entities as $entity_id => $entity) {
+   *   @endcode
+   *   Note if the entity_type is known (and it often is and passed in
+   *   explicitly) then the following snippet will load the entities found:
+   *   @code
+   *     $result = $query->execute;
+   *     $entities = entity_load($entity_type, array_keys($result[$entity_type]));
+   *   @endcode
+   *   Also, the orderedResults property of this object 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.
+   */
+  public function execute() {
+    drupal_alter('entity_query', $this);
+    if (function_exists($this->executeCallback)) {
+      return $this->executeCallback($this);
+    }
+    // If no override, find the storage engine to be used.
+    foreach ($this->fields as $field) {
+      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->fields) {
+      if (empty($storage['module'])) {
+        throw new EntityFieldQueryException(t("Field storage engine not found."));
+      }
+      $function = $storage['module'] . '_field_storage_query';
+      $return = $function($this);
+      if (!empty($this->propertyConditions) || !empty($this->propertyOrderBy)) {
+        throw new EntityFieldQueryException(t('Property conditions / orders were not handled.'));
+      }
+      return $return;
+    }
+    return $this->propertyQuery();
+  }
+
+  /**
+   * Queries entity tables in SQL for property conditions and sorts.
+   *
+   * This method is only used if there are no field conditions and sorts.
+   */
+  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, $condition);
+    }
+
+    // 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 range and returns the requested list.
+   *
+   * @param $select_query
+   *   A SelectQuery which has entity_type, entity_id, revision_id and bundle
+   *   fields added.
+   *
+   * @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);
+  }
+
+  /**
+   * Internal. Adds a condition to an already built SelectQuery.
+   *
+   * 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 $having
+   *   HAVING or WHERE. This is necessary because SQL can't handle WHERE
+   *   conditions on aliased columns.
+   * @param $condition
+   *   A condition as described in EntityFieldQuery::fieldCondition() and
+   *   EntityFieldQuery::entityCondition().
+   */
+  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/simpletest/simpletest.info
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/simpletest.info,v
retrieving revision 1.16
diff -u -r1.16 simpletest.info
--- modules/simpletest/simpletest.info	3 Feb 2010 18:16:23 -0000	1.16
+++ modules/simpletest/simpletest.info	24 May 2010 20:08:29 -0000
@@ -19,6 +19,7 @@
 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/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 -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	24 May 2010 20:08:28 -0000
@@ -482,127 +482,100 @@
 /**
  * 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;
+  foreach ($query->fieldConditions as $key => $condition) {
+    $field = $condition['field'];
+    if ($field['cardinality'] != 1) {
+      $select_query->distinct();
+    }
+    $tablename = $tablename_function($field);
+    // Every condition 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");
+    // Add the condition itself.
+    $sql_field = "$table_alias." . _field_sql_storage_columnname($field['field_name'], $condition['column']);
+    $query->addCondition($select_query, $sql_field, $condition);
+    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;
+  foreach ($query->fieldOrder as $order) {
+    $field = $order['field'];
+    $tablename = $tablename_function($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 $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 -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	24 May 2010 20:08:28 -0000
@@ -99,7 +99,7 @@
  *
  * @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/simpletest/tests/entity_query.test
===================================================================
RCS file: modules/simpletest/tests/entity_query.test
diff -N modules/simpletest/tests/entity_query.test
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/simpletest/tests/entity_query.test	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,327 @@
+<?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_bundle_key');
+    field_attach_create_bundle('bundle2', 'test_entity_bundle_key');
+    field_attach_create_bundle('test_entity_bundle', 'test_entity_bundle');
+
+    $instances = array();
+    $this->fields = array();
+    $this->field_names[0] = $field_name = drupal_strtolower($this->randomName() . '_field_name');
+    $field = array('field_name' => $field_name, 'type' => 'test_field', 'cardinality' => 4);
+    $field = field_create_field($field);
+    $this->fields[0] = $field;
+    $instance = array(
+      'field_name' => $field_name,
+      'entity_type' => '',
+      'bundle' => '',
+      'label' => $this->randomName() . '_label',
+      'description' => $this->randomName() . '_description',
+      'weight' => mt_rand(0, 127),
+      'settings' => array(
+        'test_instance_setting' => $this->randomName(),
+      ),
+      'widget' => array(
+        'type' => 'test_field_widget',
+        'label' => 'Test Field',
+        'settings' => array(
+          'test_widget_setting' => $this->randomName(),
+        )
+      )
+    );
+    $instances[0] = $instance;
+
+
+    // Add an instance to that bundle.
+    $instances[0]['bundle'] = 'bundle1';
+    $instances[0]['entity_type'] = 'test_entity_bundle_key';
+    field_create_instance($instances[0]);
+    $instances[0]['bundle'] = $instances[0]['entity_type'] = 'test_entity_bundle';
+    field_create_instance($instances[0]);
+
+    $this->field_names[1] = $field_name = drupal_strtolower($this->randomName() . '_field_name');
+    $field = array('field_name' => $field_name, 'type' => 'shape', 'cardinality' => 4);
+    $field = field_create_field($field);
+    $this->fields[1] = $field;
+    $instance = array(
+      'field_name' => $field_name,
+      'entity_type' => '',
+      'bundle' => '',
+      'label' => $this->randomName() . '_label',
+      'description' => $this->randomName() . '_description',
+      'weight' => mt_rand(0, 127),
+      'settings' => array(
+        'test_instance_setting' => $this->randomName(),
+      ),
+      'widget' => array(
+        'type' => 'test_field_widget',
+        'label' => 'Test Field',
+        'settings' => array(
+          'test_widget_setting' => $this->randomName(),
+        )
+      )
+    );
+    $instances[1] = $instance;
+
+    // Add an instance to that bundle.
+    $instances[1]['bundle'] = 'bundle1';
+    $instances[1]['entity_type'] = 'test_entity_bundle_key';
+    field_create_instance($instances[1]);
+    $instances[1]['bundle'] = $instances[1]['entity_type'] = 'test_entity_bundle';
+    field_create_instance($instances[1]);
+
+    // Write entity base table if there is one.
+    $entities = array();
+
+    // First, of type test_entity_bundle_key
+    for ($i = 1; $i < 5; $i++) {
+      $entity = new stdClass;
+      $entity->ftid = $i;
+      $entity->fttype = 'bundle1';
+      $entity->{$this->field_names[0]}[LANGUAGE_NONE][0]['value'] = $i;
+      drupal_write_record('test_entity_bundle_key', $entity);
+      field_attach_insert('test_entity_bundle_key', $entity);
+    }
+    $entity = new stdClass;
+    $entity->ftid = 5;
+    $entity->fttype = 'bundle1';
+    $entity->{$this->field_names[1]}[LANGUAGE_NONE][0]['shape'] = 'square';
+    $entity->{$this->field_names[1]}[LANGUAGE_NONE][0]['color'] = 'red';
+    $entity->{$this->field_names[1]}[LANGUAGE_NONE][1]['shape'] = 'circle';
+    $entity->{$this->field_names[1]}[LANGUAGE_NONE][1]['color'] = 'blue';
+    drupal_write_record('test_entity_bundle', $entity);
+    field_attach_insert('test_entity_bundle', $entity);
+  }
+
+  /**
+   * Test field_attach_query().
+   */
+  function testEntityFieldQuery() {
+    // First, test without options.
+    $query = new EntityFieldQuery();
+    $query->fieldCondition($this->fields[0], 'value', 3, '=');
+    $this->assertEntityFieldQuery($query, array(
+      array('test_entity_bundle_key', 3),
+    ), t('Test the "equal to" operation.'));
+    $query = new EntityFieldQuery();
+    $query->fieldCondition($this->fields[0], 'value', 2, '>');
+    $this->assertEntityFieldQuery($query, array(
+      array('test_entity_bundle_key', 3),
+      array('test_entity_bundle_key', 4),
+    ), t('Test the "greater than" operation.'));
+
+    $query = new EntityFieldQuery();
+    $query->fieldCondition($this->fields[0], 'value', array(3, 4), 'IN');
+    $this->assertEntityFieldQuery($query, array(
+      array('test_entity_bundle_key', 3),
+      array('test_entity_bundle_key', 4),
+    ), t('Test the "in" operation.'));
+
+    $query = new EntityFieldQuery();
+    $query->fieldCondition($this->fields[0], 'value', array(1, 3), 'BETWEEN');
+    $this->assertEntityFieldQuery($query, array(
+      array('test_entity_bundle_key', 1),
+      array('test_entity_bundle_key', 2),
+      array('test_entity_bundle_key', 3),
+    ), t('Test the "between" operation.'));
+
+    $query = new EntityFieldQuery();
+    $query->fieldCondition($this->fields[0], 'value', 3);
+    $this->assertEntityFieldQuery($query, array(
+      array('test_entity_bundle_key', 3),
+    ), t('Test omission of an operator with a single item.'));
+
+    $query = new EntityFieldQuery();
+    $query->fieldCondition($this->fields[0], 'value', array(3, 4));
+    $this->assertEntityFieldQuery($query, array(
+      array('test_entity_bundle_key', 3),
+      array('test_entity_bundle_key', 4),
+    ), t('Test omission of an operator with multiple items.'));
+
+    $query = new EntityFieldQuery();
+    $query
+      ->entityCondition('entity_type', 'test_entity_bundle_key')
+      ->propertyCondition('ftid', 1);
+    $this->assertEntityFieldQuery($query, array(
+      array('test_entity_bundle_key', 1),
+    ), t('Test property conditions.'));
+
+    $query = new EntityFieldQuery();
+    $query
+      ->entityCondition('entity_type', 'test_entity_bundle_key')
+      ->entityCondition('bundle', 'bundle', 'STARTS_WITH')
+      ->propertyCondition('ftid', 1, '>')
+      ->fieldCondition($this->fields[0], 'value', 4, '<');
+    $this->assertEntityFieldQuery($query, array(
+      array('test_entity_bundle_key', 2),
+      array('test_entity_bundle_key', 3),
+    ), t('Test entity and property and field conditions.'));
+
+    $query = new EntityFieldQuery();
+    $query
+      ->entityCondition('entity_type', 'test_entity_bundle_key')
+      ->entityCondition('bundle', 'bundle2')
+      ->propertyCondition('ftid', 1, '>')
+      ->fieldCondition($this->fields[0], 'value', 4, '<');
+    $this->assertEntityFieldQuery($query, array(
+    ), t('Test entity and property and field conditions.'));
+
+    $query = new EntityFieldQuery();
+    $query
+      ->entityCondition('entity_type', 'test_entity_bundle_key')
+      ->propertyCondition('ftid', 1, '>')
+      ->fieldCondition($this->fields[0], 'value', 4, '<');
+    $this->assertEntityFieldQuery($query, array(
+      array('test_entity_bundle_key', 2),
+      array('test_entity_bundle_key', 3),
+    ), t('Test property and field conditions.'));
+
+    $query = new EntityFieldQuery();
+    $query->fieldCondition($this->fields[0], 'value', 0, '>');
+    $query->fieldOrderBy($this->fields[0], 'value', 'asc');
+    $this->assertEntityFieldQuery($query, array(
+      array('test_entity_bundle_key', 1),
+      array('test_entity_bundle_key', 2),
+      array('test_entity_bundle_key', 3),
+      array('test_entity_bundle_key', 4),
+    ), t('Field sort: ascending.'), TRUE);
+
+    $query = new EntityFieldQuery();
+    $query->fieldCondition($this->fields[0], 'value', 0, '>');
+    $query->fieldOrderBy($this->fields[0], 'value', 'desc');
+    $this->assertEntityFieldQuery($query, array(
+      array('test_entity_bundle_key', 4),
+      array('test_entity_bundle_key', 3),
+      array('test_entity_bundle_key', 2),
+      array('test_entity_bundle_key', 1),
+    ), t('Field sort: descending.'), TRUE);
+
+    $query = new EntityFieldQuery();
+    $query->entityCondition('entity_type', 'test_entity_bundle_key');
+    $query->propertyOrderBy('ftid', 'asc');
+    $this->assertEntityFieldQuery($query, array(
+      array('test_entity_bundle_key', 1),
+      array('test_entity_bundle_key', 2),
+      array('test_entity_bundle_key', 3),
+      array('test_entity_bundle_key', 4),
+    ), t('Entity sort: ascending.'), TRUE);
+
+    $query = new EntityFieldQuery();
+    $query->entityCondition('entity_type', 'test_entity_bundle_key');
+    $query->propertyOrderBy('ftid', 'desc');
+    $this->assertEntityFieldQuery($query, array(
+      array('test_entity_bundle_key', 4),
+      array('test_entity_bundle_key', 3),
+      array('test_entity_bundle_key', 2),
+      array('test_entity_bundle_key', 1),
+    ), t('Entity sort: descending.'), TRUE);
+
+    $query = new EntityFieldQuery();
+    $query->entityCondition('entity_type', 'test_entity_bundle_key');
+    $query->propertyOrderBy('ftid', 'asc');
+    $query->range(0, 2);
+    $this->assertEntityFieldQuery($query, array(
+      array('test_entity_bundle_key', 1),
+      array('test_entity_bundle_key', 2),
+    ), t('Test limit.'), TRUE);
+
+    $query = new EntityFieldQuery();
+    $query->entityCondition('entity_type', 'test_entity_bundle_key');
+    $query->propertyOrderBy('ftid', 'asc');
+    $query->range(2, 4);
+    $this->assertEntityFieldQuery($query, array(
+      array('test_entity_bundle_key', 3),
+      array('test_entity_bundle_key', 4),
+    ), t('Test offset.'), TRUE);
+
+    for ($i = 6; $i < 10; $i++) {
+      $entity = new stdClass;
+      $entity->ftid = $i;
+      $entity->{$this->field_names[0]}[LANGUAGE_NONE][0]['value'] = $i - 5;
+      drupal_write_record('test_entity_bundle', $entity);
+      field_attach_insert('test_entity_bundle', $entity);
+    }
+
+    $query = new EntityFieldQuery();
+    $query->fieldCondition($this->fields[0], 'value', 2, '>');
+    $this->assertEntityFieldQuery($query, array(
+      array('test_entity_bundle_key', 3),
+      array('test_entity_bundle_key', 4),
+      array('test_entity_bundle', 8),
+      array('test_entity_bundle', 9),
+    ), t('Select a field across multiple entities.'));
+
+    $query = new EntityFieldQuery();
+    $query->fieldCondition($this->fields[1], 'shape', 'square');
+    $query->fieldCondition($this->fields[1], 'color', 'blue');
+    $this->assertEntityFieldQuery($query, array(
+      array('test_entity_bundle', 5),
+    ), t('Test without a delta group.'));
+
+    $query = new EntityFieldQuery();
+    $query->fieldCondition($this->fields[1], 'shape', 'square', '=', 'group');
+    $query->fieldCondition($this->fields[1], 'color', 'blue', '=', 'group');
+    $this->assertEntityFieldQuery($query, array(), t('Test with a delta group.'));
+
+    $pass = FALSE;
+    $query = new EntityFieldQuery();
+    try {
+      $query->execute();
+    }
+    catch (EntityFieldQueryException $exception) {
+      $pass = ($exception->getMessage() == t('For this query an entity type must be specified.'));
+    }
+    $this->assertTrue($pass, t("Can't query the universe."));
+  }
+
+  /**
+   * 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 assertEntityFieldQuery($query, $intended_results, $message, $ordered = FALSE) {
+    $results = array();
+    foreach ($query->execute() as $entity_type => $entity_ids) {
+      foreach ($entity_ids as $entity_id => $stub_entity) {
+        $results[] = array($entity_type, $entity_id);
+      }
+    }
+    if (!isset($ordered) || !$ordered) {
+      sort($results);
+      sort($intended_results);
+    }
+    $this->assertEqual($results, $intended_results, $message);
+  }
+}