### Eclipse Workspace Patch 1.0
#P drupal_test_7
Index: modules/field/field.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.test,v
retrieving revision 1.9
diff -u -r1.9 field.test
--- modules/field/field.test	31 Mar 2009 01:49:51 -0000	1.9
+++ modules/field/field.test	13 Apr 2009 01:22:22 -0000
@@ -216,6 +216,64 @@
     $this->assertEqual($count, 0, 'NULL field leaves no data in table');
   }
 
+  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');
+    $instance = $this->instance;
+    $instance['bundle'] = 'test_bundle_1';
+    field_create_instance($instance);
+
+    // Create two test objects, using two different types and bundles.
+    $entity_types = array('test_entity', 'test_cacheable_entity');
+    $entities = array(field_test_create_stub_entity(0, 0, 'test_bundle'), field_test_create_stub_entity(1, 1, 'test_bundle_1'));
+
+    // Create first test object with random data.
+    $values = array();
+    for ($delta = 0; $delta < $cardinality; $delta++) {
+      $values[$delta] = mt_rand(1, 127);
+      $entities[0]->{$this->field_name}[$delta] = array('value' => $values[$delta]);
+    }
+    field_attach_insert($entity_types[0], $entities[0]);
+
+    // Create second test object, sharing a value with the first one.
+    $common_value = $values[$cardinality - 1];
+    $entities[1]->{$this->field_name} = array(array('value' => $common_value));
+    field_attach_insert($entity_types[1], $entities[1]);
+
+    // Query on the object's values.
+    for ($delta = 0; $delta < $cardinality; $delta++) {
+      $conditions = array('value' => $values[$delta]);
+      $result = field_attach_query($this->field_name, $conditions);
+      $this->assertTrue(isset($result[$entity_types[0]][0]), t('Query on value %delta returns the object', array('%delta' => $delta)));
+    }
+
+    // Query on a value not in the object.
+    do {
+      $value = mt_rand(1, 127);
+    } while (in_array($value, $values));
+    $conditions = array('value' => $value);
+    $result = field_attach_query($this->field_name, $conditions);
+    $this->assertFalse(isset($result[$entity_types[0]][0]), t("Query on a value that is not in the object doesn't return the object"));
+
+    // Query on the value shared by both objects.
+    $conditions = array('value' => $common_value);
+    $result = field_attach_query($this->field_name, $conditions);
+    $this->assertTrue(isset($result[$entity_types[0]][0]) && isset($result[$entity_types[1]][1]), t('Query on a value common to both objects returns both objects'));
+    $conditions = array('type' => $entity_types[0], 'value' => $common_value);
+    $result = field_attach_query($this->field_name, $conditions);
+    $this->assertTrue(isset($result[$entity_types[0]][0]) && !isset($result[$entity_types[1]][1]), t("Query on a value common to both objects and a 'type' condition only returns the relevant object"));
+    $conditions = array('bundle' => $entities[0]->fttype, 'value' => $common_value);
+    $result = field_attach_query($this->field_name, $conditions);
+    $this->assertTrue(isset($result[$entity_types[0]][0]) && !isset($result[$entity_types[1]][1]), t("Query on a value common to both objects and a 'bundle' condition only returns the relevant object"));
+    $conditions = array('entity_id' => $entities[0]->ftid, 'value' => $common_value);
+    $result = field_attach_query($this->field_name, $conditions);
+    $this->assertTrue(isset($result[$entity_types[0]][0]) && !isset($result[$entity_types[1]][1]), t("Query on a value common to both objects and an 'entity_id' condition only returns the relevant object"));
+
+    // TODO : test field_attach_query_revisions
+  }
+
   function testFieldAttachViewAndPreprocess() {
     $entity_type = 'test_entity';
     $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
Index: modules/field/field.attach.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.attach.inc,v
retrieving revision 1.9
diff -u -r1.9 field.attach.inc
--- modules/field/field.attach.inc	31 Mar 2009 01:59:34 -0000	1.9
+++ modules/field/field.attach.inc	13 Apr 2009 01:22:20 -0000
@@ -163,32 +163,39 @@
  *  - TRUE: render the default field implementation of the field hook.
  *  - FALSE: render the field module's implementation of the field hook.
  */
-function _field_invoke($op, $obj_type, &$object, &$a = NULL, &$b = NULL, $default = FALSE) {
+function _field_invoke($op, $obj_type, &$object, &$a = NULL, &$b = NULL, $options = array()) {
+  $default_options = array(
+    'default' => FALSE,
+  );
+  $options += $default_options;
+
   list(, , $bundle) = field_attach_extract_ids($obj_type, $object);
   $instances = field_info_instances($bundle);
 
   $return = array();
   foreach ($instances 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);
+    if (empty($options['field_name']) || $options['field_name'] == $field_name) {
+      $field = field_info_field($field_name);
+      $items = isset($object->$field_name) ? $object->$field_name : array();
+
+      $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 (is_array($result)) {
+          $return = array_merge($return, $result);
+        }
+        else if (isset($result)) {
+          $return[] = $result;
+        }
       }
-      else if (isset($result)) {
-        $return[] = $result;
+      // Put back the altered items in the object, if the field was present to
+      // begin with (avoid replacing missing field with empty array(), those are
+      // not semantically equivalent on update).
+      if (isset($object->$field_name)) {
+        $object->$field_name = $items;
       }
     }
-    // Put back the altered items in the object, if the field was present to
-    // begin with (avoid replacing missing field with empty array(), those are
-    // not semantically equivalent on update).
-    if (isset($object->$field_name)) {
-      $object->$field_name = $items;
-    }
   }
 
   return $return;
@@ -197,8 +204,9 @@
 /**
  * Invoke field.module's version of a field hook.
  */
-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);
 }
 
 /**
@@ -609,6 +617,103 @@
 }
 
 /**
+ * Return all object types and ids containing field data items matching a query.
+ *
+ * Note that field values that come out of a regular field_attach_load() call
+ * go through hook_field_load() and hook_field_attach_load() invocations, which
+ * might add to or affect the raw stored values. The conditions specified in
+ * field_attach_query() and field_attach_query_revisions() only apply to the
+ * stored values.
+ *
+ * @param $field_name
+ *   The name of the field to query.
+ * @param $conditions
+ *   An array of query conditions on the field columns.
+ *   Array keys can be:
+ *   - 'type': specify a condition on object type (e.g. 'node', 'user'...)
+ *   - 'bundle': specify a condition on object bundle (e.g. node type)
+ *   - 'entity_id': specify a condition on object id (e.g node nid, user uid...)
+ *     Note that the field_attach_query_revisions() function additionally
+ *     accepts conditions on object revision id.
+ *   - any of the columns for $field_name's field type.
+ *   Array values can be:
+ *   - A value of the same data type as the column.
+ *     The condition will be interpreted as 'column = value'
+ *   - An array of values of the same data type as the column.
+ *     The condition will be interpreted as 'column IN (value)'.
+ *   Not all storage engines are required to support queries on all column types.
+ *   TODO: how can a module providing 'X as field' safely build on this, then ?
+ *   This issue will also arise if we want to support operators : '<', '>' are OK, but what about 'LIKE' ?
+ * @param $result_format
+ *   - FIELD_QUERY_RETURN_VALUES (default): return the values for the field.
+ *   - FIELD_QUERY_RETURN_IDS: return the ids of the objects matching the
+ *     conditions.
+ * @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
+ *   - FIELD_LOAD_REVISION: query all revisions.
+ * @return
+ *   An array keyed by object type (e.g. 'node', 'user'...), then by object id,
+ *   and whose values depend on the $result_format parameter:
+ *   - 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, or in the actual delta
+ *     sequence.
+ *     The pseudo-objects only include properties that the Field API knows
+ *     about: bundle, id, revision id, and field values (no node title, user
+ *     name...).
+ *   - FIELD_QUERY_RETURN_IDS: the object id.
+ */
+function _field_attach_query($field_name, $conditions, $result_format = FIELD_QUERY_RETURN_VALUES, $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, $skip_field);
+  }
+  // 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);
+  }
+
+  // TODO: what about field types that handle their own storage,
+  // or add to the field values fetching from another table (filefield) ?
+  // should we provide a hook_field_query() to let them handle that ?
+
+  if ($result_format == FIELD_QUERY_RETURN_VALUES) {
+    foreach ($results as $obj_type => $type_results) {
+      foreach ($type_results as $id => $pseudo_object) {
+        // Invoke hook_field_load().
+        // TODO: revisit when hook_field_load is made multiple.
+        $a = $b = NULL;
+        $custom_additions = _field_invoke('load', $obj_type, $pseudo_object, $a, $b, array('field_name' => $field_name));
+        $pseudo_object->$field_name = $custom_additions;
+
+        // Invoke hook_field_attach_load().
+        foreach (module_implements('field_attach_load') as $module) {
+          $function = $module . '_field_attach_load';
+          $function($obj_type, $pseudo_object);
+        }
+
+        $results[$obj_type][$id] = $pseudo_object;
+      }
+    }
+  }
+
+  return $results;
+}
+
+/**
+ * TODO
+ */
+function _field_attach_query_revisions($field_name, $conditions, $result_format = FIELD_QUERY_RETURN_VALUES) {
+  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
  * each field's rendered content depends on the display formatter and
@@ -772,5 +877,34 @@
 }
 
 /**
+ * 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;
+}
+
+/**
  * @autoload} End of "@autoload field_attach"
  */
Index: modules/field/field.api.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.api.php,v
retrieving revision 1.6
diff -u -r1.6 field.api.php
--- modules/field/field.api.php	30 Mar 2009 03:44:55 -0000	1.6
+++ modules/field/field.api.php	13 Apr 2009 01:22:20 -0000
@@ -192,6 +192,10 @@
  *   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
@@ -477,6 +481,12 @@
  * This hook is invoked after the field module has performed the operation.
  *
  * See field_attach_load() for details and arguments.
+ *
+ * Note that $object 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...)
+ *
  * TODO: Currently, this hook only accepts a single object a time.
  */
 function hook_field_attach_load($obj_type, $object) {
@@ -553,6 +563,12 @@
 }
 
 /**
+ * TODO
+ */
+function hook_field_attach_pre_query($field_name, $conditions, $result_format, $skip_field) {
+}
+
+/**
  * Act on field_attach_delete.
  *
  * This hook is invoked after the field module has performed the operation.
Index: modules/field/field.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.module,v
retrieving revision 1.7
diff -u -r1.7 field.module
--- modules/field/field.module	26 Mar 2009 13:31:24 -0000	1.7
+++ modules/field/field.module	13 Apr 2009 01:22:21 -0000
@@ -78,6 +78,14 @@
  */
 define('FIELD_LOAD_REVISION', 'FIELD_LOAD_REVISION');
 
+/**
+ * TODO
+ */
+define('FIELD_QUERY_RETURN_VALUES', 'FIELD_QUERY_RETURN_VALUES');
+/**
+ * TODO
+ */
+define('FIELD_QUERY_RETURN_IDS', 'FIELD_QUERY_RETURN_IDS');
 
 /**
  * Base class for all exceptions thrown by Field API functions.
Index: modules/field/field.autoload.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.autoload.inc,v
retrieving revision 1.6
diff -u -r1.6 field.autoload.inc
--- modules/field/field.autoload.inc	26 Mar 2009 13:31:24 -0000	1.6
+++ modules/field/field.autoload.inc	13 Apr 2009 01:22:21 -0000
@@ -248,6 +248,26 @@
 }
 
 /**
+ * TODO
+ *
+ * This function is an autoloader for _field_attach_query() in modules/field/field.attach.inc.
+ */
+function field_attach_query($field_name, $conditions, $result_format = FIELD_QUERY_RETURN_VALUES, $age = FIELD_LOAD_CURRENT) {
+  require_once DRUPAL_ROOT . '/modules/field/field.attach.inc';
+  return _field_attach_query($field_name, $conditions, $result_format, $age);
+}
+
+/**
+ * TODO
+ *
+ * This function is an autoloader for _field_attach_query_revisions() in modules/field/field.attach.inc.
+ */
+function field_attach_query_revisions($field_name, $conditions, $result_format = FIELD_QUERY_RETURN_VALUES) {
+  require_once DRUPAL_ROOT . '/modules/field/field.attach.inc';
+  return _field_attach_query_revisions($field_name, $conditions, $result_format);
+}
+
+/**
  * Generate and return a structured content array tree suitable for
  * drupal_render() for all of the fields on an object. The format of
  * each field's rendered content depends on the display formatter and
@@ -370,6 +390,29 @@
 }
 
 /**
+ * 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.
+ *
+ * This function is an autoloader for _field_attach_create_stub_object() in modules/field/field.attach.inc.
+ */
+function field_attach_create_stub_object($object_type, $ids) {
+  require_once DRUPAL_ROOT . '/modules/field/field.attach.inc';
+  return _field_attach_create_stub_object($object_type, $ids);
+}
+
+/**
  * @} End of "field_attach"
  */
 
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.6
diff -u -r1.6 field_sql_storage.module
--- modules/field/modules/field_sql_storage/field_sql_storage.module	30 Mar 2009 03:44:55 -0000	1.6
+++ modules/field/modules/field_sql_storage/field_sql_storage.module	13 Apr 2009 01:22:22 -0000
@@ -214,7 +214,8 @@
         // 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.
@@ -349,6 +350,97 @@
   }
 }
 
+
+/**
+ * Implementation of hook_field_storage_query().
+ *
+ * TODO : Doc should go in field.api.php doc.
+ *
+ * @param $field_name
+ *   The name of the field to query.
+ * @param $conditions
+ *   An array of query conditions on the field columns. Supported columns:
+ *   'type', 'bundle', 'entity_id', 'revision_id', plus field columns.
+ * @param $result_format
+ *   TODO
+ * @param $age
+ *   - FIELD_LOAD_CURRENT: query the most recent revisions for all objects
+ *   - FIELD_LOAD_REVISION: query all revisions.
+ * @return
+ *   TODO
+ */
+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;
+  $table = $load_current ? _field_sql_storage_tablename($field_name) : _field_sql_storage_revision_tablename($field_name);
+
+  $field = field_info_field($field_name);
+  $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 $column => $value) {
+    // 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, is_array($value) ? 'IN' : '=');
+  }
+
+  $results = $query->execute();
+
+  $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] = $id;
+    }
+  }
+
+  return $return;
+}
+
 /**
  * Implementation of hook_field_storage_delete_instance().
  *
