diff --git a/modules/field/field.api.php b/modules/field/field.api.php
index b83dc8db26aca138e33d0faebfad2bd361256613..24f5e583947c2766cb49ad8a64ff3108e0787eea 100644
--- a/modules/field/field.api.php
+++ b/modules/field/field.api.php
@@ -238,6 +238,10 @@ function hook_field_load($obj_type, $objects, $field, $instances, &$items, $age)
  *   The type of $object.
  * @param $object
  *   The object for the operation.
+ *   Note that this might not be a full-fledged 'object'. When invoked through
+ *   field_attach_query(), the $object will only include properties that the
+ *   Field API knows about: bundle, id, revision id, and field values (no node
+ *   title, user name...).
  * @param $field
  *   The field structure for the operation.
  * @param $instance
@@ -503,12 +507,17 @@ function hook_field_attach_pre_load($obj_type, $objects, $age, &$skip_fields) {
  * This hook is invoked after the field module has performed the operation.
  *
  * Unlike other field_attach hooks, this hook accounts for 'multiple loads'.
- * It takes an array of objects indexed by object id as its first parameter.
- * For performance reasons, information for all available objects should be
- * loaded in a single query where possible.
+ * Instead of the usual $object parameter, it accepts an array of objects,
+ * indexed by object id. For performance reasons, information for all available
+ * objects should be loaded in a single query where possible.
  *
- * Note that the changes made to the objects' field values get cached by the
- * field cache for subsequent loads.
+ * Note that $objects might not be full-fledged 'objects'. When invoked through
+ * field_attach_query(), each object only includes properties that the Field
+ * API knows about: bundle, id, revision id, and field values (no node title,
+ * user name...)
+
+ * The changes made to the objects' field values get cached by the field cache
+ * for subsequent loads.
  *
  * See field_attach_load() for details and arguments.
  */
@@ -585,6 +594,38 @@ function hook_field_attach_pre_insert($obj_type, $object, &$skip_fields) {
 function hook_field_attach_pre_update($obj_type, $object, &$skip_fields) {
 }
 
+/**
+ * Act on field_attach_pre_query.
+ *
+ * This hook should be implemented by modules that use
+ * hook_field_attach_pre_load(), hook_field_attach_pre_insert() and
+ * hook_field_attach_pre_update() to bypass the regular storage engine, to
+ * handle field queries.
+ *
+ * @param $field_name
+ *   The name of the field to query.
+ * @param $conditions
+ *   See field_attach_query().
+ *   A storage module that doesn't support querying a given column should raise
+ *   a FieldQueryException. Incompatibilities should be mentioned on the module
+ *   project page.
+ * @param $result_format
+ *   See field_attach_query().
+ * @param $age
+ *   - FIELD_LOAD_CURRENT: query the most recent revisions for all
+ *     objects. The results will be keyed by object type and object id.
+ *   - FIELD_LOAD_REVISION: query all revisions. The results will be keyed by
+ *     object type and object revision id.
+ * @param $skip_field
+ *   Boolean, always coming as FALSE.
+ * @return
+ *   See field_attach_query().
+ *   The $skip_field parameter should be set to TRUE if the query has been
+ *   handled.
+ */
+function hook_field_attach_pre_query($field_name, $conditions, $result_format, $age, &$skip_field) {
+}
+
 /**
  * Act on field_attach_delete.
  *
@@ -736,6 +777,26 @@ function hook_field_storage_delete($obj_type, $object) {
 function hook_field_storage_delete_revision($obj_type, $object) {
 }
 
+/**
+ * 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 $result_format
+ *   See field_attach_query().
+ * @param $age
+ *   See field_attach_query().
+ * @return
+ *   See field_attach_query().
+ */
+function hook_field_storage_query($field_name, $conditions, $result_format, $age) {
+}
+
 /**
  * Act on creation of a new bundle.
  *
diff --git a/modules/field/field.attach.inc b/modules/field/field.attach.inc
index 4a24c171361519c1e0eda9fe34521fa14b3d3bc2..9b9e92a3fe99d66d1440f4fe61a11102e4052896 100644
--- a/modules/field/field.attach.inc
+++ b/modules/field/field.attach.inc
@@ -13,8 +13,7 @@
 // Should all iteration through available fields be done here instead of in Field?
 
 /**
- * Exception class thrown by field_attach_validate() when field
- * validation errors occur.
+ * Exception thrown by field_attach_validate() on field validation errors.
  */
 class FieldValidationException extends FieldException {
   var $errors;
@@ -37,6 +36,15 @@ function __construct($errors) {
   }
 }
 
+/**
+ * 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
  * @{
@@ -146,47 +154,77 @@ function __construct($errors) {
  * @param $b
  *   - The $form_state in the 'submit' operation.
  *   - Otherwise NULL.
- *
- * @param $default
- *   - TRUE: use the default field implementation of the field hook.
- *   - FALSE: use the field module's implementation of the field hook.
+ * @param $options
+ *   An associative array of additional options, with the following keys:
+ *  - 'field_name'
+ *    The name of the field whose operation should be invoked. By default, the
+ *    operation is invoked on all the fields in the object's bundle.
+ *  - 'default'
+ *    A boolean value, specifying which implementation of the operation should
+ *    be invoked.
+ *    - if FALSE (default), the field types implementation of the operation
+ *      will be invoked (hook_field_[op])
+ *    - If TRUE, the default field implementation of the field operation
+ *      will be invoked (field_default_[op])
+ *    Internal use only. Do not explicitely set to TRUE, but use
+ *    _field_invoke_default() instead.
  */
-function _field_invoke($op, $obj_type, $object, &$a = NULL, &$b = NULL, $default = FALSE) {
-  list(, , $bundle) = field_attach_extract_ids($obj_type, $object);
-  $instances = field_info_instances($bundle);
-
+function _field_invoke($op, $obj_type, $object, &$a = NULL, &$b = NULL, $options = array()) {
+  // Merge default options.
+  $default_options = array(
+    'default' => FALSE,
+  );
+  $options += $default_options;
+
+  // Iterate through the object's field instances.
   $return = array();
-  foreach ($instances as $instance) {
+  list(, , $bundle) = field_attach_extract_ids($obj_type, $object);
+  foreach (field_info_instances($bundle) as $instance) {
     $field_name = $instance['field_name'];
-    $field = field_info_field($field_name);
-    $items = isset($object->$field_name) ? $object->$field_name : array();
 
-    $function = $default ? 'field_default_' . $op : $field['module'] . '_field_' . $op;
-    if (drupal_function_exists($function)) {
-      $result = $function($obj_type, $object, $field, $instance, $items, $a, $b);
-      if (is_array($result)) {
-        $return = array_merge($return, $result);
+    // When in 'single field' mode, only act on the specified field.
+    if (empty($options['field_name']) || $options['field_name'] == $field_name) {
+      $field = field_info_field($field_name);
+
+      // Extract the field values into a separate variable, easily accessed by
+      // hook implementations.
+      $items = isset($object->$field_name) ? $object->$field_name : array();
+
+      // Invoke the field hook and collect results.
+      $function = $options['default'] ? 'field_default_' . $op : $field['module'] . '_field_' . $op;
+      if (drupal_function_exists($function)) {
+        $result = $function($obj_type, $object, $field, $instance, $items, $a, $b);
+        if (isset($result)) {
+          // For hooks with array results, we merge results together.
+          // For hooks with scalar results, we collect results in an array.
+          if (is_array($result)) {
+            $return = array_merge($return, $result);
+          }
+          else {
+            $return[] = $result;
+          }
+        }
       }
-      elseif (isset($result)) {
-        $return[] = $result;
+
+      // Populate field values back in the object, but avoid replacing missing
+      // fields with an empty array (those are not equivalent on update).
+      if ($items !== array() || property_exists($object, $field_name)) {
+        $object->$field_name = $items;
       }
     }
-    // Populate field values back in the object, but avoid replacing missing
-    // fields with an empty array (those are not equivalent on update).
-    if ($items !== array() || property_exists($object, $field_name)) {
-      $object->$field_name = $items;
-    }
   }
 
   return $return;
 }
 
 /**
- * Invoke a field operation across fields on multiple objects.
+ * Invoke a field hook across fields on multiple objects.
  *
  * @param $op
  *   Possible operations include:
  *   - load
+ *   For all other operations, use _field_invoke() / field_invoke_default()
+ *   instead.
  * @param $obj_type
  *   The type of $object; e.g. 'node' or 'user'.
  * @param $objects
@@ -196,53 +234,75 @@ function _field_invoke($op, $obj_type, $object, &$a = NULL, &$b = NULL, $default
  *   - Otherwise NULL.
  * @param $b
  *   Currently always NULL.
- * @param $default
- *   - TRUE: use the default field implementation of the field hook.
- *   - FALSE: use the field module's implementation of the field hook.
+ * @param $options
+ *   An associative array of additional options, with the following keys:
+ *  - 'field_name'
+ *    The name of the field whose operation should be invoked. By default, the
+ *    operation is invoked on all the fields in the objects' bundles.
+ *  - 'default'
+ *    A boolean value, specifying which implementation of the operation should
+ *    be invoked.
+ *    - if FALSE (default), the field types implementation of the operation
+ *      will be invoked (hook_field_[op])
+ *    - If TRUE, the default field implementation of the field operation
+ *      will be invoked (field_default_[op])
+ *    Internal use only. Do not explicitely set to TRUE, but use
+ *    _field_invoke_multiple_default() instead.
  * @return
  *   An array of returned values keyed by object id.
  */
-function _field_invoke_multiple($op, $obj_type, $objects, &$a = NULL, &$b = NULL, $default = FALSE) {
+function _field_invoke_multiple($op, $obj_type, $objects, &$a = NULL, &$b = NULL, $options = array()) {
+  // Merge default options.
+  $default_options = array(
+    'default' => FALSE,
+  );
+  $options += $default_options;
+
   $fields = array();
   $grouped_instances = array();
   $grouped_objects = array();
   $grouped_items = array();
   $return = array();
 
-  // Preparation:
-  // - Get the list of fields contained in the various bundles.
-  // - For each field, group the corresponding instances, objects and field
-  //   values.
-  // - Initialize the return value for each object.
+  // Go through the objects and collect the fields on which the hook should be
+  // invoked.
   foreach ($objects as $object) {
     list($id, $vid, $bundle) = field_attach_extract_ids($obj_type, $object);
     foreach (field_info_instances($bundle) as $instance) {
       $field_name = $instance['field_name'];
-      if (!isset($grouped_fields[$field_name])) {
-        $fields[$field_name] = field_info_field($field_name);
+      // When in 'single field' mode, only act on the specified field.
+      if (empty($options['field_name']) || $options['field_name'] == $field_name) {
+        // Add the field to the list of fields to invoke the hook on.
+        if (!isset($fields[$field_name])) {
+          $fields[$field_name] = field_info_field($field_name);
+        }
+        // Group the corresponding instances and objects.
+        $grouped_instances[$field_name][$id] = $instance;
+        $grouped_objects[$field_name][$id] = $objects[$id];
+        // Extract the field values into a separate variable, easily accessed
+        // by hook implementations.
+        $grouped_items[$field_name][$id] = isset($object->$field_name) ? $object->$field_name : array();
       }
-      $grouped_instances[$field_name][$id] = $instance;
-      $grouped_objects[$field_name][$id] = $objects[$id];
-      $grouped_items[$field_name][$id] = isset($object->$field_name) ? $object->$field_name : array();
     }
+    // Initialize the return value for each object.
     $return[$id] = array();
   }
 
-  // Call each field's operation.
+  // For each field, invoke the field hook and collect results.
   foreach ($fields as $field_name => $field) {
-    if (!empty($field)) {
-      $function = $default ? 'field_default_' . $op : $field['module'] . '_field_' . $op;
-      if (drupal_function_exists($function)) {
-        $results = $function($obj_type, $grouped_objects[$field_name], $field, $grouped_instances[$field_name], $grouped_items[$field_name], $a, $b);
-        // Merge results by object.
-        if (isset($results)) {
-          foreach ($results as $id => $result) {
-            if (is_array($result)) {
-              $return[$id] = array_merge($return[$id], $result);
-            }
-            else {
-              $return[$id][] = $result;
-            }
+    $function = $options['default'] ? 'field_default_' . $op : $field['module'] . '_field_' . $op;
+    if (drupal_function_exists($function)) {
+      $results = $function($obj_type, $grouped_objects[$field_name], $field, $grouped_instances[$field_name], $grouped_items[$field_name], $a, $b);
+      if (isset($results)) {
+        // Collect results by object.
+        // For hooks with array results, we merge results together.
+        // For hooks with scalar results, we collect results in an array.
+        foreach ($results as $id => $result) {
+          if (is_array($result)) {
+            $return[$id] = array_merge($return[$id], $result);
+          }
+          else {
+            $return[$id][] = $result;
           }
         }
       }
@@ -262,16 +322,30 @@ function _field_invoke_multiple($op, $obj_type, $objects, &$a = NULL, &$b = NULL
 
 /**
  * Invoke field.module's version of a field hook.
+ *
+ * This function invokes the field_default_[op]() function.
+ * Use _field_invoke() to invoke the field type implementation,
+ * hook_field_[op]().
+ *
+ * @see _field_invoke().
  */
-function _field_invoke_default($op, $obj_type, $object, &$a = NULL, &$b = NULL) {
-  return _field_invoke($op, $obj_type, $object, $a, $b, TRUE);
+function _field_invoke_default($op, $obj_type, $object, &$a = NULL, &$b = NULL, $options = array()) {
+  $options['default'] = TRUE;
+  return _field_invoke($op, $obj_type, $object, $a, $b, $options);
 }
 
 /**
  * Invoke field.module's version of a field hook on multiple objects.
+ *
+ * This function invokes the field_default_[op]() function.
+ * Use _field_invoke_multiple() to invoke the field type implementation,
+ * hook_field_[op]().
+ *
+ * @see _field_invoke_multiple().
  */
-function _field_invoke_multiple_default($op, $obj_type, $objects, &$a = NULL, &$b = NULL) {
-  return _field_invoke_multiple($op, $obj_type, $objects, $a, $b, TRUE);
+function _field_invoke_multiple_default($op, $obj_type, $objects, &$a = NULL, &$b = NULL, $options = array()) {
+  $options['default'] = TRUE;
+  return _field_invoke_multiple($op, $obj_type, $objects, $a, $b, $options);
 }
 
 /**
@@ -651,6 +725,154 @@ function field_attach_delete_revision($obj_type, $object) {
   }
 }
 
+/**
+ * Retrieve objects 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 object.
+ *
+ * @param $field_name
+ *   The name 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 for $field_name's field type: condition on field
+ *       value,
+ *     - 'type': condition on object type (e.g. 'node', 'user'...),
+ *     - 'bundle': condition on object bundle (e.g. node type),
+ *     - 'entity_id': condition on object id (e.g node nid, user uid...),
+ *     The field_attach_query_revisions() function additionally supports:
+ *     - 'revision_id': condition on object 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': 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 $result_format
+ *   - FIELD_QUERY_RETURN_IDS (default): return the ids of the objects matching the
+ *     conditions.
+ *   - FIELD_QUERY_RETURN_VALUES: return the values for the field.
+ * @param $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
+ *     objects. The results will be keyed by object type and object id.
+ *   - FIELD_LOAD_REVISION: query all revisions. The results will be keyed by
+ *     object type and object revision id.
+ * @return
+ *   An array keyed by object type (e.g. 'node', 'user'...), then by object id
+ *   or revision id (depending of the value of the $age parameter), and whose
+ *   values depend on the $result_format parameter:
+ *   - FIELD_QUERY_RETURN_IDS: the object id.
+ *   - FIELD_QUERY_RETURN_VALUES: a pseudo-object with values for the
+ *     $field_name field. This only includes values matching the conditions,
+ *     and thus might not contain all actual values and actual delta sequence
+ *     (although values oprder is preserved).
+ *     The pseudo-objects only include properties that the Field API knows
+ *     about: bundle, id, revision id, and field values (no node title, user
+ *     name...).
+ *   Throws a FieldQueryException if the field's storage doesn't support the
+ *   specified conditions.
+ */
+function field_attach_query($field_name, $conditions, $result_format = FIELD_QUERY_RETURN_IDS, $age = FIELD_LOAD_CURRENT) {
+  // Give a chance to 3rd party modules that bypass the storage engine to
+  // handle the query.
+  $skip_field = FALSE;
+  foreach (module_implements('field_attach_pre_query') as $module) {
+    $function = $module . '_field_attach_pre_query';
+    $results = $function($field_name, $conditions, $result_format, $age, $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) {
+    $results = module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_query', $field_name, $conditions, $result_format, $age);
+  }
+
+  if ($result_format == FIELD_QUERY_RETURN_VALUES) {
+    foreach ($results as $obj_type => $pseudo_objects) {
+      if ($age == FIELD_LOAD_CURRENT) {
+        // Invoke hook_field_load().
+        $b = NULL;
+        _field_invoke_multiple('load', $obj_type, $pseudo_objects, $age, $b, array('field_name' => $field_name));
+
+        // Invoke hook_field_attach_load().
+        foreach (module_implements('field_attach_load') as $module) {
+          $function = $module . '_field_attach_load';
+          $function($obj_type, $pseudo_objects, $age);
+        }
+      }
+      else {
+        // The 'multiple' hooks expect an array of objects keyed by object id,
+        // and thus cannot be used directly when querying revisions. The hooks
+        // are therefore called on each object separately, which might cause
+        // performance issues when large numbers of revisions are retrieved.
+        foreach ($pseudo_objects as $vid => $pseudo_object) {
+          list($id) = _field_attach_extract_ids($obj_type, $pseudo_object);
+          $objects = array($id => $pseudo_object);
+
+          // Invoke hook_field_load().
+          $b = NULL;
+          _field_invoke_multiple('load', $obj_type, $objects, $age, $b, array('field_name' => $field_name));
+
+          // Invoke hook_field_attach_load().
+          foreach (module_implements('field_attach_load') as $module) {
+            $function = $module . '_field_attach_load';
+            $function($obj_type, $objects, $age);
+          }
+        }
+      }
+    }
+  }
+
+  return $results;
+}
+
+/**
+ * Retrieve object revisions matching a given set of conditions.
+ *
+ * See field_attach_query() for more informations.
+ *
+ * @param $field_name
+ *   The name of the field to query.
+ * @param $conditions
+ *   See field_attach_query().
+ * @param $result_format
+ *   See field_attach_query().
+ *   Note that the FIELD_QUERY_RETURN_VALUES option might cause performance
+ *   issues with field_attach_query_revisions().
+ * @return
+ *   See field_attach_query().
+ */
+function field_attach_query_revisions($field_name, $conditions, $result_format = FIELD_QUERY_RETURN_IDS) {
+  return field_attach_query($field_name, $conditions, $result_format, FIELD_LOAD_REVISION);
+}
+
 /**
  * Generate and return a structured content array tree suitable for
  * drupal_render() for all of the fields on an object. The format of
@@ -811,6 +1033,35 @@ function field_attach_extract_ids($object_type, $object) {
   return array($id, $vid, $bundle, $cacheable);
 }
 
+/**
+ * Helper function to assemble an object structure with initial ids.
+ *
+ * This function can be seen as reciprocal to field_attach_extract_ids().
+ *
+ * @param $obj_type
+ *   The type of $object; e.g. 'node' or 'user'.
+ * @param $ids
+ *   A numerically indexed array, as returned by field_attach_extract_ids(),
+ *   containing these elements:
+ *   0: primary id of the object
+ *   1: revision id of the object, or NULL if $obj_type is not versioned
+ *   2: bundle name of the object
+ * @return
+ *   An $object structure, initialized with the ids provided.
+ */
+function field_attach_create_stub_object($obj_type, $ids) {
+  $object = new stdClass();
+  $info = field_info_fieldable_types($obj_type);
+  $object->{$info['id key']} = $ids[0];
+  if (isset($info['revision key']) && !is_null($ids[1])) {
+    $object->{$info['revision key']} = $ids[1];
+  }
+  if ($info['bundle key']) {
+    $object->{$info['bundle key']} = $ids[2];
+  }
+  return $object;
+}
+
 /**
  * @} End of "defgroup field_attach"
  */
diff --git a/modules/field/field.module b/modules/field/field.module
index 29bf758faec47d35509c340081569406e173f613..f518169f975d9123600ff32c45e8936f367fc78a 100644
--- a/modules/field/field.module
+++ b/modules/field/field.module
@@ -87,6 +87,25 @@
  */
 define('FIELD_LOAD_REVISION', 'FIELD_LOAD_REVISION');
 
+/**
+ * @name Field query flags
+ * @{
+ * Flags for use in the $result_format parameter in field_attach_query().
+ */
+
+/**
+ * Result format argument for field_attach_query().
+ */
+define('FIELD_QUERY_RETURN_VALUES', 'FIELD_QUERY_RETURN_VALUES');
+
+/**
+ * Result format argument for field_attach_query().
+ */
+define('FIELD_QUERY_RETURN_IDS', 'FIELD_QUERY_RETURN_IDS');
+
+/**
+ * @} End of "Field query flags".
+ */
 
 /**
  * Base class for all exceptions thrown by Field API functions.
diff --git a/modules/field/field.test b/modules/field/field.test
index c67eb72ab29b4b84c7cc053942940b33d77823f1..826410bd32575ed8d60fff5b48f3f926f58f5d74 100644
--- a/modules/field/field.test
+++ b/modules/field/field.test
@@ -261,6 +261,216 @@ class FieldAttachTestCase extends DrupalWebTestCase {
     $this->assertEqual($entity->{$this->field_name}, $values, t('Insert: missing field results in default value saved'));
   }
 
+  /**
+   * Test field_attach_query().
+   */
+  function testFieldAttachQuery() {
+    $cardinality = $this->field['cardinality'];
+
+    // 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 two test objects, 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 object 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}[$delta] = array('value' => $values[$delta]);
+    }
+    field_attach_insert($entity_types[1], $entities[1]);
+
+    // Create second test object, sharing a value with the first one.
+    $common_value = $values[$cardinality - 1];
+    $entities[2]->{$this->field_name} = array(array('value' => $common_value));
+    field_attach_insert($entity_types[2], $entities[2]);
+
+    // Query on the object's values.
+    for ($delta = 0; $delta < $cardinality; $delta++) {
+      $conditions = array(array('value', $values[$delta]));
+      $result = field_attach_query($this->field_name, $conditions);
+      $this->assertTrue(isset($result[$entity_types[1]][1]), t('Query on value %delta returns the object', array('%delta' => $delta)));
+    }
+
+    // Query on a value that is not in the object.
+    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_name, $conditions);
+    $this->assertFalse(isset($result[$entity_types[1]][1]), t("Query on a value that is not in the object doesn't return the object"));
+
+    // Query on the value shared by both objects, and discriminate using
+    // additional conditions.
+
+    $conditions = array(array('value', $common_value));
+    $result = field_attach_query($this->field_name, $conditions);
+    $this->assertTrue(isset($result[$entity_types[1]][1]) && isset($result[$entity_types[2]][2]), t('Query on a value common to both objects returns both objects'));
+
+    $conditions = array(array('type', $entity_types[1]), array('value', $common_value));
+    $result = field_attach_query($this->field_name, $conditions);
+    $this->assertTrue(isset($result[$entity_types[1]][1]) && !isset($result[$entity_types[2]][2]), t("Query on a value common to both objects and a 'type' condition only returns the relevant object"));
+
+    $conditions = array(array('bundle', $entities[1]->fttype), array('value', $common_value));
+    $result = field_attach_query($this->field_name, $conditions);
+    $this->assertTrue(isset($result[$entity_types[1]][1]) && !isset($result[$entity_types[2]][2]), t("Query on a value common to both objects and a 'bundle' condition only returns the relevant object"));
+
+    $conditions = array(array('entity_id', $entities[1]->ftid), array('value', $common_value));
+    $result = field_attach_query($this->field_name, $conditions);
+    $this->assertTrue(isset($result[$entity_types[1]][1]) && !isset($result[$entity_types[2]][2]), t("Query on a value common to both objects and an 'entity_id' condition only returns the relevant object"));
+
+    // Test FIELD_QUERY_RETURN_IDS result format.
+    $conditions = array(array('value', $values[0]));
+    $result = field_attach_query($this->field_name, $conditions);
+    $expected = array(
+      $entity_types[1] => array(
+        $entities[1]->ftid => $entities[1]->ftid,
+      )
+    );
+    $this->assertEqual($result, $expected, t('FIELD_QUERY_RETURN_IDS result format returns the expect result'));
+
+    // Test FIELD_QUERY_RETURN_VALUES result format.
+    // Configure the instances so that we test hook_field_load() (see
+    // field_test_field_load() in field_test.module).
+    $this->instance['settings']['test_hook_field_load'] = TRUE;
+    field_update_instance($this->instance);
+    $this->instance2['settings']['test_hook_field_load'] = TRUE;
+    field_update_instance($this->instance2);
+
+    // Query for one of the values in the 1st object and the value shared by
+    // both objects.
+    $conditions = array(array('value', array($values[0], $common_value)));
+    $result = field_attach_query($this->field_name, $conditions, FIELD_QUERY_RETURN_VALUES);
+    $expected = array(
+      $entity_types[1] => array(
+        $entities[1]->ftid => (object) array(
+          'ftid' => $entities[1]->ftid,
+          'ftvid' => $entities[1]->ftvid,
+          'fttype' => $entities[1]->fttype,
+          $this->field_name => array(
+            array('value' => $values[0], 'additional_key' => 'additional_value'),
+            array('value' => $common_value, 'additional_key' => 'additional_value'),
+          ),
+        ),
+      ),
+      $entity_types[2] => array(
+        $entities[2]->ftid => (object) array(
+          'ftid' => $entities[2]->ftid,
+          'ftvid' => $entities[2]->ftvid,
+          'fttype' => $entities[2]->fttype,
+          $this->field_name => array(
+            array('value' => $common_value, 'additional_key' => 'additional_value'),
+          ),
+        ),
+      ),
+    );
+    $this->assertEqual($result, $expected, t('FIELD_QUERY_RETURN_VALUES result format returns the expect result'));
+  }
+
+  /**
+   * Test field_attach_query_revisions().
+   */
+  function testFieldAttachQueryRevisions() {
+    $cardinality = $this->field['cardinality'];
+
+    // Create first object 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));
+    $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}[$delta] = array('value' => $values[$delta]);
+    }
+    field_attach_insert($entity_type, $entities[1]);
+
+    // Create second object revision, sharing a value with the first one.
+    $common_value = $values[$cardinality - 1];
+    $entities[2]->{$this->field_name}[0] = array('value' => $common_value);
+    field_attach_update($entity_type, $entities[2]);
+
+    // Query on the object's values.
+    for ($delta = 0; $delta < $cardinality; $delta++) {
+      $conditions = array(array('value', $values[$delta]));
+      $result = field_attach_query_revisions($this->field_name, $conditions);
+      $this->assertTrue(isset($result[$entity_type][1]), t('Query on value %delta returns the object', array('%delta' => $delta)));
+    }
+
+    // Query on a value that is not in the object.
+    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_name, $conditions);
+    $this->assertFalse(isset($result[$entity_type][1]), t("Query on a value that is not in the object doesn't return the object"));
+
+    // Query on the value shared by both objects, and discriminate using
+    // additional conditions.
+
+    $conditions = array(array('value', $common_value));
+    $result = field_attach_query_revisions($this->field_name, $conditions);
+    $this->assertTrue(isset($result[$entity_type][1]) && isset($result[$entity_type][2]), t('Query on a value common to both objects returns both objects'));
+
+    $conditions = array(array('revision_id', $entities[1]->ftvid), array('value', $common_value));
+    $result = field_attach_query_revisions($this->field_name, $conditions);
+    $this->assertTrue(isset($result[$entity_type][1]) && !isset($result[$entity_type][2]), t("Query on a value common to both objects and a 'revision_id' condition only returns the relevant object"));
+
+    // Test FIELD_QUERY_RETURN_IDS result format.
+    $conditions = array(array('value', $values[0]));
+    $result = field_attach_query_revisions($this->field_name, $conditions);
+    $expected = array(
+      $entity_type => array(
+        $entities[1]->ftvid => $entities[1]->ftid,
+      )
+    );
+    $this->assertEqual($result, $expected, t('FIELD_QUERY_RETURN_IDS result format returns the expect result'));
+
+    // Test FIELD_QUERY_RETURN_VALUES result format.
+    // Configure the instance so that we test hook_field_load() (see
+    // field_test_field_load() in field_test.module).
+    $this->instance['settings']['test_hook_field_load'] = TRUE;
+    field_update_instance($this->instance);
+
+    // Query for one of the values in the 1st object and the value shared by
+    // both objects.
+    $conditions = array(array('value', array($values[0], $common_value)));
+    $result = field_attach_query_revisions($this->field_name, $conditions, FIELD_QUERY_RETURN_VALUES);
+    $expected = array(
+      $entity_type => array(
+        $entities[1]->ftvid => (object) array(
+          'ftid' => $entities[1]->ftid,
+          'ftvid' => $entities[1]->ftvid,
+          'fttype' => $entities[1]->fttype,
+          $this->field_name => array(
+            array('value' => $values[0], 'additional_key' => 'additional_value'),
+            array('value' => $common_value, 'additional_key' => 'additional_value'),
+          ),
+        ),
+        $entities[2]->ftvid => (object) array(
+          'ftid' => $entities[2]->ftid,
+          'ftvid' => $entities[2]->ftvid,
+          'fttype' => $entities[2]->fttype,
+          $this->field_name => array(
+            array('value' => $common_value, 'additional_key' => 'additional_value'),
+          ),
+        ),
+      ),
+    );
+    $this->assertEqual($result, $expected, t('FIELD_QUERY_RETURN_VALUES result format returns the expect result'));
+
+    // TODO : test
+  }
+
   function testFieldAttachViewAndPreprocess() {
     $entity_type = 'test_entity';
     $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
diff --git a/modules/field/modules/field_sql_storage/field_sql_storage.module b/modules/field/modules/field_sql_storage/field_sql_storage.module
index 18f89adb83b482d8511062811b35b1cd255ff169..5a3d659b9779193b2f3d47457a3e5eeba7000b51 100644
--- a/modules/field/modules/field_sql_storage/field_sql_storage.module
+++ b/modules/field/modules/field_sql_storage/field_sql_storage.module
@@ -238,7 +238,8 @@ function field_sql_storage_field_storage_load($obj_type, &$objects, $age, $skip_
         // For each column declared by the field, populate the item
         // from the prefixed database column.
         foreach ($field['columns'] as $column => $attributes) {
-          $item[$column] = $row->{_field_sql_storage_columnname($field_name, $column)};
+          $column_name = _field_sql_storage_columnname($field_name, $column);
+          $item[$column] = $row->$column_name;
         }
 
         // Add the item to the field values for the entity.
@@ -350,6 +351,108 @@ function field_sql_storage_field_storage_delete($obj_type, $object) {
   }
 }
 
+
+/**
+ * Implement hook_field_storage_query().
+ */
+function field_sql_storage_field_storage_query($field_name, $conditions, $result_format, $age) {
+  $load_values = $result_format == FIELD_QUERY_RETURN_VALUES;
+  $load_current = $age == FIELD_LOAD_CURRENT;
+
+  $field = field_info_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');
+  $query
+    ->fields('e', array('type'))
+    ->condition('deleted', 0)
+    ->orderBy('delta');
+  // Add fields, depending on the return format.
+  if ($load_values) {
+    $query->fields('t');
+  }
+  else {
+    $query->fields('t', array('entity_id', 'revision_id'));
+  }
+  // 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 NULL:
+        $operator = is_array($value) ? 'IN' : '=';
+        break;
+
+      case 'STARTS_WITH':
+        $operator = 'LIKE';
+        $value .= '%';
+        break;
+
+      case 'ENDS_WITH':
+        $operator = 'LIKE';
+        $value = "$value%";
+        break;
+
+      case 'CONTAINS':
+        $operator = 'LIKE';
+        $value = "%$value%";
+        break;
+    }
+    // Translate field columns into prefixed db columns.
+    if (in_array($column, $field_columns)) {
+      $column = _field_sql_storage_columnname($field_name, $column);
+    }
+    $query->condition($column, $value, $operator);
+  }
+
+  $results = $query->execute();
+
+  // Build results.
+  $return = array();
+  $delta_count = array();
+  foreach ($results as $row) {
+    // If querying all revisions and the entity type has revisions, we need to
+    // key the results by revision_ids.
+    $entity_type = field_info_fieldable_types($row->type);
+    $id = ($load_current || empty($entity_type['revision key'])) ? $row->entity_id : $row->revision_id;
+
+    if ($load_values) {
+      // Populate actual field values.
+      if (!isset($delta_count[$row->type][$id])) {
+        $delta_count[$row->type][$id] = 0;
+      }
+      if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta_count[$row->type][$id] < $field['cardinality']) {
+        $item = array();
+        // For each column declared by the field, populate the item
+        // from the prefixed database column.
+        foreach ($field['columns'] as $column => $attributes) {
+          $column_name = _field_sql_storage_columnname($field_name, $column);
+          $item[$column] = $row->$column_name;
+        }
+
+        // Initialize the 'pseudo object' if needed.
+        if (!isset($return[$row->type][$id])) {
+          $return[$row->type][$id] = field_attach_create_stub_object($row->type, array($row->entity_id, $row->revision_id, $row->bundle));
+        }
+        // Add the item to the field values for the entity.
+        $return[$row->type][$id]->{$field_name}[] = $item;
+        $delta_count[$row->type][$id]++;
+      }
+    }
+    else {
+      // Simply return the list of selected ids.
+      $return[$row->type][$id] = $row->entity_id;
+    }
+  }
+
+  return $return;
+}
+
 /**
  * Implement hook_field_storage_delete_revision().
  *