? FIELD-TO-COLUMN-PARTIAL.patch
? Makefile
? constants.pl
? d6-50-nodes.sql.gz
? d7-50-nodes-new.sql.gz
? d7-50-nodes.sql.gz
? head.kpf
? patches
? modules/field/field.cron.inc
? modules/field/modules/combo
? scripts/OLD-generate-autoload.pl
? scripts/generate-autoload.pl
? sites/all/cck
? sites/all/modules/devel
? sites/all/modules/pbs
? sites/all/modules/taint
? sites/default/files
? sites/default/settings.php
Index: modules/field/field.attach.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.attach.inc,v
retrieving revision 1.28
diff -u -F^[fc] -r1.28 field.attach.inc
--- modules/field/field.attach.inc	6 Jul 2009 19:10:01 -0000	1.28
+++ modules/field/field.attach.inc	7 Jul 2009 02:59:52 -0000
@@ -25,7 +25,7 @@ class FieldValidationException extends F
   *   An array of field validation errors, keyed by field name and
   *   delta that contains two keys:
   *   - 'error': A machine-readable error code string, prefixed by
-  *     the field module name.  A field widget may use this code to decide
+  *     the field module name. A field widget may use this code to decide
   *     how to report the error.
   *   - 'message': A human-readable error message such as to be
   *     passed to form_error() for the appropriate form element.
@@ -112,11 +112,11 @@ class FieldQueryException extends FieldE
  *
  * field_attach_load(), field_attach_insert(), and
  * field_attach_update() also define pre-operation hooks,
- * e.g. hook_field_attach_pre_load().  These hooks run before the
+ * e.g. hook_field_attach_pre_load(). These hooks run before the
  * corresponding Field Storage API and Field Type API operations.
  * They allow modules to define additional storage locations
  * (e.g. denormalizing, mirroring) for field data on a per-field
- * basis.  They also allow modules to take over field storage
+ * basis. They also allow modules to take over field storage
  * completely by instructing other implementations of the same hook
  * and the Field Storage API itself not to operate on specified
  * fields.
@@ -167,18 +167,31 @@ class FieldQueryException extends FieldE
  *      will be invoked (field_default_[op])
  *    Internal use only. Do not explicitely set to TRUE, but use
  *    _field_invoke_default() instead.
+ *  - 'deleted'
+ *    If 1, the function will operate on deleted fields as well as
+ *    non-deleted fields.  If unset or 0, only non-deleted fields are
+ *    operated on.
  */
 function _field_invoke($op, $obj_type, $object, &$a = NULL, &$b = NULL, $options = array()) {
   // Merge default options.
   $default_options = array(
     'default' => FALSE,
+    'deleted' => 0,
   );
   $options += $default_options;
 
   // Iterate through the object's field instances.
   $return = array();
   list(, , $bundle) = field_attach_extract_ids($obj_type, $object);
-  foreach (field_info_instances($bundle) as $instance) {
+
+  if ($options['deleted']) {
+    $instances = field_read_field(array('bundle' => $bundle, array('include_deleted' => $options['deleted'])));
+  }
+  else {
+    $instances = field_info_instances($bundle);
+  }
+
+  foreach ($instances as $instance) {
     $field_name = $instance['field_name'];
 
     // When in 'single field' mode, only act on the specified field.
@@ -235,8 +248,8 @@ function _field_invoke($op, $obj_type, $
  *   Currently always NULL.
  * @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
+ *  - 'field_id'
+ *    The field id 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
@@ -247,6 +260,10 @@ function _field_invoke($op, $obj_type, $
  *      will be invoked (field_default_[op])
  *    Internal use only. Do not explicitely set to TRUE, but use
  *    _field_invoke_multiple_default() instead.
+ *  - 'deleted'
+ *    If 1, the function will operate on deleted fields as well as
+ *    non-deleted fields.  If unset or 0, only non-deleted fields are
+ *    operated on.
  * @return
  *   An array of returned values keyed by object id.
  */
@@ -254,6 +271,7 @@ function _field_invoke_multiple($op, $ob
   // Merge default options.
   $default_options = array(
     'default' => FALSE,
+    'deleted' => 0,
   );
   $options += $default_options;
 
@@ -267,13 +285,21 @@ function _field_invoke_multiple($op, $ob
   // invoked.
   foreach ($objects as $object) {
     list($id, $vid, $bundle) = field_attach_extract_ids($obj_type, $object);
-    foreach (field_info_instances($bundle) as $instance) {
+    
+    if ($options['deleted']) {
+      $instances = field_read_field(array('bundle' => $bundle, array('include_deleted' => $options['deleted'])));
+    }
+    else {
+      $instances = field_info_instances($bundle);
+    }
+
+    foreach ($instances as $instance) {
       $field_name = $instance['field_name'];
       // When in 'single field' mode, only act on the specified field.
-      if (empty($options['field_name']) || $options['field_name'] == $field_name) {
+      if (empty($options['field_id']) || $options['field_id'] == $instance['field_id']) {
         // 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);
+          $fields[$field_name] = field_info_field_by_id($instance['field_id']);
         }
         // Group the corresponding instances and objects.
         $grouped_instances[$field_name][$id] = $instance;
@@ -291,7 +317,7 @@ function _field_invoke_multiple($op, $ob
   foreach ($fields as $field_name => $field) {
     $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);
+      $results = $function($obj_type, $grouped_objects[$field_name], $field, $grouped_instances[$field_name], $grouped_items[$field_name], $options, $a, $b);
       if (isset($results)) {
         // Collect results by object.
         // For hooks with array results, we merge results together.
@@ -389,15 +415,34 @@ function field_attach_form($obj_type, $o
  *   fields, or FIELD_LOAD_REVISION to load the version indicated by
  *   each object. Defaults to FIELD_LOAD_CURRENT; use
  *   field_attach_load_revision() instead of passing FIELD_LOAD_REVISION.
+ * @param $options
+ *   An associative array of additional options, with the following keys:
+ *  - 'field_id'
+ *    The field id that should be loaded, instead of
+ *    loading all fields, for each object. Note that returned objects
+ *    may contain data for other fields, for example if they are read
+ *    from a cache.
+ *  - 'deleted'
+ *    If 1, the function will operate on deleted fields as well as
+ *    non-deleted fields.  If unset or 0, only non-deleted fields are
+ *    operated on.
  * @returns
  *   Loaded field values are added to $objects. Fields with no values should be
  *   set as an empty array.
  */
-function field_attach_load($obj_type, $objects, $age = FIELD_LOAD_CURRENT) {
+function field_attach_load($obj_type, $objects, $age = FIELD_LOAD_CURRENT, $options = array()) {
   $load_current = $age == FIELD_LOAD_CURRENT;
 
+  // Merge default options.
+  $default_options = array(
+    'deleted' => 0,
+  );
+  $options += $default_options;
+
+  // Only the most current revision of non-deleted fields for
+  // cacheable fieldable types can be cached.
   $info = field_info_fieldable_types($obj_type);
-  $cacheable = $load_current && $info['cacheable'];
+  $cacheable = $load_current && $info['cacheable'] && empty($options['deleted']);
 
   if (empty($objects)) {
     return;
@@ -441,21 +486,21 @@ function field_attach_load($obj_type, $o
     $skip_fields = array();
     foreach (module_implements('field_attach_pre_load') as $module) {
       $function = $module . '_field_attach_pre_load';
-      $function($obj_type, $queried_objects, $age, $skip_fields);
+      $function($obj_type, $queried_objects, $age, $skip_fields, $options);
     }
 
     // Invoke the storage engine's hook_field_storage_load(): the field storage
     // engine loads the rest.
-    module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_load', $obj_type, $queried_objects, $age, $skip_fields);
+    module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_load', $obj_type, $queried_objects, $age, $skip_fields, $options);
 
     // Invoke field-type module's hook_field_load().
-    _field_invoke_multiple('load', $obj_type, $queried_objects, $age);
+    _field_invoke_multiple('load', $obj_type, $queried_objects, $age, $options);
 
     // Invoke hook_field_attach_load(): let other modules act on loading the
     // object.
     foreach (module_implements('field_attach_load') as $module) {
       $function = $module . '_field_attach_load';
-      $function($obj_type, $queried_objects, $age);
+      $function($obj_type, $queried_objects, $age, $options);
     }
 
     // Build cache data.
@@ -742,8 +787,8 @@ function field_attach_delete_revision($o
  * 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 $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).
@@ -756,6 +801,8 @@ function field_attach_delete_revision($o
  *     - '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...),
+ *     - 'deleted': condition on whether the field's data is
+ *        marked deleted for the object (defaults to FALSE if not specified)
  *     The field_attach_query_revisions() function additionally supports:
  *     - 'revision_id': condition on object revision id (e.g node vid).
  *   Supported operators:
@@ -778,10 +825,18 @@ function field_attach_delete_revision($o
  *     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 $count
+ *   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 objects. This parameter has no default value so
+ *   callers must make an explicit choice to potentially retrieve an
+ *   enormous result set.
+ * @param &$cursor
+ *   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 $cursor on return. If NULL,
+ *   the first result set is returned and no next cursor is returned.
  * @param $age
  *   Internal use only. Use field_attach_query_revisions() instead of passing
  *   FIELD_LOAD_REVISION.
@@ -791,26 +846,24 @@ function field_attach_delete_revision($o
  *     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...).
+ *   or revision id (depending of the value of the $age parameter).
+ *   The values are pseudo-objects 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_name, $conditions, $result_format = FIELD_QUERY_RETURN_IDS, $age = FIELD_LOAD_CURRENT) {
+function field_attach_query($field_id, $conditions, $count, &$cursor = NULL, $age = FIELD_LOAD_CURRENT) {
+  if (!isset($cursor)) {
+    $cursor = 0;
+  }
+
   // 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);
+    $results = $function($field_id, $conditions, $count, $cursor, $age, $skip_field);
     // Stop as soon as a module claims it handled the query.
     if ($skip_field) {
       break;
@@ -818,43 +871,7 @@ function field_attach_query($field_name,
   }
   // 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);
-          }
-        }
-      }
-    }
+    $results = module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_query', $field_id, $conditions, $count, $cursor, $age);
   }
 
   return $results;
@@ -865,19 +882,26 @@ function field_attach_query($field_name,
  *
  * See field_attach_query() for more informations.
  *
- * @param $field_name
- *   The name of the field to query.
+ * @param $field_id
+ *   The id 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().
+ * @param $count
+ *   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 objects. This parameter has no default value so
+ *   callers must make an explicit choice to potentially retrieve an
+ *   enormous result set.
+ * @param &$cursor
+ *   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 $cursor on return.
  * @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);
+function field_attach_query_revisions($field_id, $conditions, $count, &$cursor = NULL) {
+  return field_attach_query($field_id, $conditions, $count, $cursor, FIELD_LOAD_REVISION);
 }
 
 /**
Index: modules/field/field.crud.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.crud.inc,v
retrieving revision 1.19
diff -u -F^[fc] -r1.19 field.crud.inc
--- modules/field/field.crud.inc	4 Jul 2009 06:19:46 -0000	1.19
+++ modules/field/field.crud.inc	7 Jul 2009 02:59:52 -0000
@@ -267,6 +267,9 @@ function field_create_field($field) {
   // Store the field and create the id.
   drupal_write_record('field_config', $field);
 
+  // The 'data' property is not part of the public field record.
+  unset($field['data']);
+
   // Invoke hook_field_storage_create_field after the field is
   // complete (e.g. it has its id).
   module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_create_field', $field);
@@ -439,7 +442,7 @@ function field_create_instance($instance
   // TODO : do we want specific messages when clashing with a disabled or inactive instance ?
   $prior_instance = field_read_instance($instance['field_name'], $instance['bundle'], array('include_inactive' => TRUE));
   if (!empty($prior_instance)) {
-    throw new FieldException('Attempt to create a field instance which already exists.');
+    throw new FieldException(t('Attempt to create a field instance %field_name,%bundle which already exists.', array('%field_name' => $instance['field_name'], '%bundle' => $instance['bundle'])));
   }
 
   _field_write_instance($instance);
@@ -688,3 +691,234 @@ function field_delete_instance($field_na
 /**
  * @} End of "defgroup field_crud".
  */
+
+/*
+ * @defgroup field_purge Field API bulk data deletion
+ * @{
+ * Clean up after Field API bulk deletion operations.
+ *
+ * Field API provides functions for deleting data attached to individual
+ * objects as well as deleting entire fields or field instances in a single
+ * operation.
+ *
+ * Deleting field data items for an object with field_attach_delete() involves
+ * three separate operations:
+ *
+ *   * Invoking the Field Type API hook_field_delete() for each field on the
+ * object. The hook for each field type receives the object and the specific
+ * field being deleted. A file field module might use this hook to delete
+ * uploaded files from the filesystem.
+ *   * Invoking the Field Storage API hook_field_storage_delete() to remove
+ * data from the primary field storage. The hook implementation receives the
+ * object being deleted and deletes data for all of the object's bundle's
+ * fields.
+ *   * Invoking the global Field Attach API hook_field_attach_delete() for all
+ * modules that implement it. Each hook implementation receives the object
+ * being deleted and can operate on whichever subset of the object's bundle's
+ * fields it chooses to.
+ *
+ * These hooks are invoked immediately when field_attach_delete() is
+ * called. Similar operations are performed for field_attach_delete_revision().
+ *
+ * When a field, bundle, or field instance is deleted, it is not practical to
+ * invoke these hooks immediately on every affected object in a single page
+ * request; there could be thousands or millions of them. Instead, the
+ * appropriate field data items, instances, and/or fields are marked as deleted
+ * so that subsequent load or query operations will not return them. Later, a
+ * separate process cleans up, or "purges", the marked-as-deleted data by going
+ * through the three-step process described above and, finally, removing
+ * deleted field and instance records.
+ *
+ * Purging field data is made somewhat tricky by the fact that, while
+ * field_attach_delete() has a complete object to pass to the various deletion
+ * hooks, the Field API purge process only has the field data it has previously
+ * stored. It cannot reconstruct complete original objects to pass to the
+ * deletion hooks. It is even possible that the original object to which some
+ * Field API data was attached has been itself deleted before the field purge
+ * operation takes place.
+ *
+ * Field API resolves this problem by using "pseudo-objects" during purge
+ * operations. A pseudo-object contains only the information from the original
+ * object that Field API knows about: entity type, id, revision id, and
+ * bundle. It also contains the field data for whichever field instance is
+ * currently being purged. For example, suppose that the node type 'story' used
+ * to contain a field called 'subtitle' but the field was deleted. If node 37
+ * was a story with a subtitle, the pseudo-object passed to the purge hooks
+ * would look something like this:
+ *
+ * @code
+ *   $obj = stdClass Object(
+ *     [nid] => 37,
+ *     [vid] => 37,
+ *     [type] => 'story',
+ *     [subtitle] => array(
+ *       [0] => array(
+ *         'value' => 'subtitle text',
+ *       ),
+ *     ),
+ *   );
+ * @endcode
+ */
+
+/**
+ * Purge some deleted Field API data, instances, or field.
+ *
+ * This function will purge up to a specified maximum number of field
+ * data records and then return. If a deleted field instance with no
+ * remaining data records is found, the instance itself will be
+ * purged. If a deleted field with no remaining field instances is
+ * found, the field itself will be purged.
+ *
+ * @param $batch_size
+ *   The maximum number of field data records to purge before returning.
+ */
+function field_purge($batch_size) {
+  // Retrieve all deleted field instances. We cannot use
+  // field_info_instances() because that function does not return
+  // deleted instances.
+  //
+  // TODO: Perhaps the include_deleted argument to this
+  // function should be replaced by inspecting the query parameters.
+  $instances = field_read_instances(array('deleted' => 1), array('include_deleted' => 1));
+
+  foreach ($instances as $instance) {
+    // Retrieve the field. We cannot use field_info_field() because
+    // the field might be deleted.
+    //
+    // TODO: See above comment.
+    $fields = field_read_fields(array('id' => $instance['field_id']), array('include_deleted' => 1));
+    $field = $fields[$instance['field_id']];
+
+    // Retrieve some pseudo-objects.
+    $obj_types = field_attach_query($instance['field_id'], array(array('bundle', $instance['bundle']), array('deleted', 1)), $batch_size);
+
+    if (count($obj_types) > 0) {
+      // Field data for the instance still exists.
+      foreach ($obj_types as $obj_type => $objects) {
+
+        field_attach_load($obj_type, $objects, FIELD_LOAD_CURRENT, array('field_id' => $field['id'], 'deleted' => 1));
+        
+        foreach ($objects as $id => $object) {
+
+          // field_attach_query() may return more results than we
+          // asked for. If we've done our batch size, stop.
+          if ($batch_size-- <= 0) {
+            return;
+          }
+
+          // Purge the data for the object.
+          field_purge_data($obj_type, $object, $field, $instance);
+        }
+      }
+    }
+    else {
+      // No field data remains for the instance, so we can remove it.
+      field_purge_instance($instance);
+    }
+  }
+
+  // Retrieve all deleted fields. Any that have no bundles can be purged.
+  //
+  // TODO: See above comment.
+  $fields = field_read_fields(array('deleted' => 1), array('include_deleted' => 1));
+  foreach ($fields as $field) {
+    // field_read_fields() does not return $field['bundles'] which we need.
+    $field = field_info_field_by_id($field['id']);
+    if (!isset($field['bundles']) || count($field['bundles']) == 0) {
+      field_purge_field($field);
+    }
+  }
+}
+
+/**
+ * Purge the field data for a single field on a single pseudo-object.
+ *
+ * This is basically the same as field_attach_delete() except it only
+ * applies to a single field. The object itself is not being deleted, and
+ * it is quite possible that other field data will remain attached to
+ * it.
+ *
+ * TODO: refactor with field_attach_delete()?
+ *
+ * @param $obj_type
+ *   The type of $object; e.g. 'node' or 'user'.
+ * @param $object
+ *   The pseudo-object whose field data to delete.
+ * @param $field
+ *   The (possibly deleted) field whose data is being purged.
+ * @param $instance
+ *   The deleted field instance whose data is being purged.
+ */
+function field_purge_data($obj_type, $object, $field, $instance) {
+  // Each field type's hook_field_delete() only expects to operate on
+  // a single field at a time, so we can use it as-is for purging.
+  $options = array('field_id' => $instance['field_id']);
+  _field_invoke('delete', $obj_type, $object, $dummy, $dummy, $options);
+
+  // Tell the field storage system to purge the data.
+  module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_purge', $obj_type, $object, $field, $instance);
+
+  // Let other modules act on purging the data.
+  foreach (module_implements('field_attach_purge') as $module) {
+    $function = $module . '_field_attach_purge';
+    $function($obj_type, $object, $field, $instance);
+  }
+}
+
+/**
+ * Purge a field instance record from the database.
+ *
+ * This function assumes all data for the instance has already been
+ * purged, and should only be called by field_purge().
+ *
+ * @param $instance
+ *   The instance record to purge.
+ */
+function field_purge_instance($instance) {
+  db_delete('field_config_instance')
+    ->condition('id', $instance['id'])
+    ->execute();
+
+  // Notify the storage engine.
+  module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_purge_instance', $instance);
+
+  // Clear the cache.
+  field_cache_clear();
+
+  // Invoke external hooks after the cache is cleared for API consistency.
+  module_invoke_all('field_purge_instance', $instance);
+}
+
+/**
+ * Purge a field record from the database.
+ *
+ * This function assumes all instances for the field has already been
+ * purged, and should only be called by field_purge().
+ *
+ * @param $field
+ *   The field record to purge.
+ */
+function field_purge_field($field) {
+  $instances = field_read_instances(array('field_id' => $field['id']), array('include_deleted' => 1));
+  if (count($instances) > 0) {
+    throw new FieldException("Attempt to purge a field that still has instances.");
+  }
+
+  db_delete('field_config')
+    ->condition('id', $field['id'])
+    ->execute();
+
+  // Notify the storage engine.
+  module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_purge_field', $field);
+
+  // Clear the cache.
+  field_cache_clear();
+
+  // Invoke external hooks after the cache is cleared for API consistency.
+  module_invoke_all('field_purge_field', $field);
+}
+
+/**
+ * @} End of "defgroup field_purge".
+ */
+
Index: modules/field/field.info.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.info.inc,v
retrieving revision 1.7
diff -u -F^[fc] -r1.7 field.info.inc
--- modules/field/field.info.inc	30 Jun 2009 03:12:03 -0000	1.7
+++ modules/field/field.info.inc	7 Jul 2009 02:59:52 -0000
@@ -40,6 +40,18 @@ function _field_get_formatter($display, 
 }
 
 /**
+ * Clear the field info cache without clearing the field data cache.
+ *
+ * This is useful when deleted fields or instances are purged.  We
+ * need to remove the purged records, but no actual field data items
+ * are affected.
+ */
+function _field_info_clear_cache() {
+  _field_info_collate_types(TRUE);
+  _field_info_collate_fields(TRUE);
+}
+
+/**
  * Collate all information on field types, widget types and related structures.
  *
  * @param $reset
@@ -183,6 +195,7 @@ function _field_info_collate_fields($res
     else {
       $info = array(
         'fields' => field_read_fields(),
+        'field_ids' => field_read_fields(array(), array('include_deleted' => 1)),
         'instances' => array_fill_keys(array_keys(field_info_bundles()), array()),
       );
 
@@ -191,6 +204,7 @@ function _field_info_collate_fields($res
       foreach ($instances as $instance) {
         $info['instances'][$instance['bundle']][$instance['field_name']] = $instance;
         $info['fields'][$instance['field_name']]['bundles'][] = $instance['bundle'];
+        $info['field_ids'][$instance['field_id']]['bundles'][] = $instance['bundle'];
       }
 
       cache_set('field_info_fields', $info, 'cache_field');
@@ -398,7 +412,8 @@ function field_info_fields() {
  * Return data about an individual field.
  *
  * @param $field_name
- *   The name of the field to retrieve.
+ *   The name of the field to retrieve. $field_name can only refer to a
+ *   non-deleted field.
  * @return
  *   The named field object, or NULL. The Field object has an additional
  *   property, bundles, which is an array of all the bundles to which
@@ -412,6 +427,24 @@ function field_info_field($field_name) {
 }
 
 /**
+ * Return data about an individual field by its id.
+ *
+ * @param $field_id
+ *   The id of the field to retrieve. $field_id can refer to a
+ *   deleted field.
+ * @return
+ *   The named field object, or NULL. The Field object has an additional
+ *   property, bundles, which is an array of all the bundles to which
+ *   this field belongs.
+ */
+function field_info_field_by_id($field_id) {
+  $info = _field_info_collate_fields();
+  if (isset($info['field_ids'][$field_id])) {
+    return $info['field_ids'][$field_id];
+  }
+}
+
+/**
  * Return an array of instance data for a given bundle,
  * or for all known bundles, keyed by bundle name and field name.
  *
Index: modules/field/field.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.module,v
retrieving revision 1.16
diff -u -F^[fc] -r1.16 field.module
--- modules/field/field.module	2 Jul 2009 20:37:03 -0000	1.16
+++ modules/field/field.module	7 Jul 2009 02:59:52 -0000
@@ -55,6 +55,10 @@
  *   pluggable back-end storage system for actual field data. The
  *   default implementation, field_sql_storage.module, stores field data
  *   in the local SQL database.
+
+ * - @link field_purge Field API bulk data deletion @endlink. Cleans
+ *   up after bulk deletion operations such as field_delete_field()
+ *   and field_delete_instance().
  */
 
 /**
@@ -90,8 +94,14 @@
 /**
  * @name Field query flags
  * @{
- * Flags for use in the $result_format parameter in field_attach_query().
+ * Flags for field_attach_query().
+ */
+
+/**
+ * Limit argument for field_attach_query() to request all available
+ * objects instead of a limited number.
  */
+define('FIELD_QUERY_NO_LIMIT', 'FIELD_QUERY_NO_LIMIT');
 
 /**
  * Result format argument for field_attach_query().
@@ -341,8 +351,7 @@ function field_cache_clear($rebuild_sche
   cache_clear_all('*', 'cache_field', TRUE);
 
   module_load_include('inc', 'field', 'field.info');
-  _field_info_collate_types(TRUE);
-  _field_info_collate_fields(TRUE);
+  _field_info_clear_cache();
 
   // Refresh the schema to pick up new information.
   // TODO : if db storage gets abstracted out, we'll need to revisit how and when
Index: modules/field/field.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.test,v
retrieving revision 1.29
diff -u -F^[fc] -r1.29 field.test
--- modules/field/field.test	2 Jul 2009 20:19:48 -0000	1.29
+++ modules/field/field.test	7 Jul 2009 02:59:53 -0000
@@ -26,7 +26,8 @@ class FieldAttachTestCase extends Drupal
 
     $this->field_name = drupal_strtolower($this->randomName() . '_field_name');
     $this->field = array('field_name' => $this->field_name, 'type' => 'test_field', 'cardinality' => 4);
-    field_create_field($this->field);
+    $this->field = field_create_field($this->field);
+    $this->field_id = $this->field['id'];
     $this->instance = array(
       'field_name' => $this->field_name,
       'bundle' => 'test_bundle',
@@ -296,7 +297,7 @@ class FieldAttachTestCase extends Drupal
     // 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);
+      $result = field_attach_query($this->field_id, $conditions, FIELD_QUERY_NO_LIMIT);
       $this->assertTrue(isset($result[$entity_types[1]][1]), t('Query on value %delta returns the object', array('%delta' => $delta)));
     }
 
@@ -305,74 +306,37 @@ class FieldAttachTestCase extends Drupal
       $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);
+    $result = field_attach_query($this->field_id, $conditions, FIELD_QUERY_NO_LIMIT);
     $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);
+    $result = field_attach_query($this->field_id, $conditions, 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 objects returns both objects'));
 
     $conditions = array(array('type', $entity_types[1]), array('value', $common_value));
-    $result = field_attach_query($this->field_name, $conditions);
+    $result = field_attach_query($this->field_id, $conditions, 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 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);
+    $result = field_attach_query($this->field_id, $conditions, 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 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);
+    $result = field_attach_query($this->field_id, $conditions, 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 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);
+    $result = field_attach_query($this->field_id, $conditions, FIELD_QUERY_NO_LIMIT);
     $expected = array(
       $entity_types[1] => array(
-        $entities[1]->ftid => $entities[1]->ftid,
+        $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'));
-
-    // 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'));
   }
 
   /**
@@ -402,7 +366,7 @@ class FieldAttachTestCase extends Drupal
     // 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);
+      $result = field_attach_query_revisions($this->field_id, $conditions, FIELD_QUERY_NO_LIMIT);
       $this->assertTrue(isset($result[$entity_type][1]), t('Query on value %delta returns the object', array('%delta' => $delta)));
     }
 
@@ -411,62 +375,29 @@ class FieldAttachTestCase extends Drupal
       $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);
+    $result = field_attach_query_revisions($this->field_id, $conditions, FIELD_QUERY_NO_LIMIT);
     $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);
+    $result = field_attach_query_revisions($this->field_id, $conditions, FIELD_QUERY_NO_LIMIT);
     $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);
+    $result = field_attach_query_revisions($this->field_id, $conditions, FIELD_QUERY_NO_LIMIT);
     $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);
+    $result = field_attach_query_revisions($this->field_id, $conditions, FIELD_QUERY_NO_LIMIT);
     $expected = array(
       $entity_type => array(
-        $entities[1]->ftvid => $entities[1]->ftid,
+        $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'));
-
-    // 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'));
   }
 
   function testFieldAttachViewAndPreprocess() {
@@ -1654,3 +1585,183 @@ class FieldInstanceTestCase extends Drup
     $this->assertTrue(!empty($another_instance) && empty($another_instance['deleted']), t('A non-deleted field instance is not marked for deletion.'));
   }
 }
+
+class FieldBulkDeleteTestCase extends DrupalWebTestCase {
+  protected $field;
+
+  public static function getInfo() {
+    return array(
+      'name'  => t('Field bulk delete tests'),
+      'description'  => t("Bulk delete fields and instances, and clean up afterwards."),
+      'group' => t('Field')
+    );
+  }
+
+  /**
+   * Generate random values for a field_test field.
+   *
+   * @param $cardinality
+   *   Number of values to generate.
+   * @return
+   *  An array of random values, in the format expected for field values.
+   */
+  function _generateTestFieldValues($cardinality) {
+    $values = array();
+    for ($i = 0; $i < $cardinality; $i++) {
+      // field_test fields treat 0 as 'empty value'.
+      $values[$i]['value'] = mt_rand(1, 127);
+    }
+    return $values;
+  }
+
+  function _generateStubObjects($obj_type, $objects, $field_name = NULL) {
+    $stubs = array();
+    foreach ($objects as $obj) {
+      $stub = field_attach_create_stub_object($obj_type, field_attach_extract_ids($obj_type, $obj));
+      if (isset($field_name)) {
+        $stub->{$field_name} = $obj->{$field_name};
+      }
+      $stubs[] = $stub;
+    }
+    return $stubs;
+  }
+
+  function setUp() {
+    parent::setUp('field_test');
+
+    // TODO: Create a second field that we do not delete so we can
+    // test that its data stays around.
+
+    // Create two bundles
+    $this->bundles = array('bb_1' => 'bb_1', 'bb_2' => 'bb_2');
+    foreach ($this->bundles as $name => $desc) {
+      field_test_create_bundle($name, $desc);
+    }
+
+    // Create two fields
+    $field = array('field_name' => 'bf_1', 'type' => 'test_field', 'cardinality' => 1);
+    $this->fields[] = field_create_field($field);
+    $field = array('field_name' => 'bf_2', 'type' => 'test_field', 'cardinality' => 4);
+    $this->fields[] = field_create_field($field);
+
+    // For each bundle, create an instance of each field, and 10
+    // objects with values for each field.
+    $id = 0;
+    $this->entity_type = 'test_entity';
+    foreach ($this->bundles as $bundle) {
+      foreach ($this->fields as $field) {
+        $instance = array(
+          'field_name' => $field['field_name'],
+          'bundle' => $bundle,
+          'widget' => array(
+            'type' => 'test_field_widget',
+          )
+        );
+        $this->instances[] = field_create_instance($instance);
+      }
+      
+      for ($i = 0; $i < 10; $i++) {
+        $entity = field_test_create_stub_entity($id, $id, $bundle);
+        foreach ($this->fields as $field) {
+          $entity->{$field['field_name']} = $this->_generateTestFieldValues($field['cardinality']);
+        }
+        $this->entities[$id] = $entity;
+        field_attach_insert($this->entity_type, $entity);
+        $id++;
+      }
+    }
+  }
+
+  function tearDown() {
+    parent::tearDown();
+    $this->fields = array();
+    $this->instances = array();
+    field_test_delete_bundle('bb_1');
+    field_test_delete_bundle('bb_2');
+  }
+
+  // TODO: This tests how field_attach_query() interacts with
+  // field_delete_instance().  Move to FieldCrudTestCase?
+  function testDeleteFieldInstance() {
+    $bundle = reset($this->bundles);
+    $field = reset($this->fields);
+
+    // There are 10 objects of this bundle
+    $found = field_attach_query($field['id'], array(array('bundle', $bundle)), FIELD_QUERY_NO_LIMIT);
+    $this->assertEqual(count($found['test_entity']), 10, 'Correct number of objects found before deleting');
+    
+    // Delete the instance
+    field_delete_instance($field['field_name'], $bundle);
+
+    // The instance still exists, deleted.
+    $instances = field_read_instances(array('field_id' => $field['id'], 'deleted' => 1), array('include_deleted' => 1, 'include_inactive' => 1));
+    $this->assertEqual(count($instances), 1, 'There is one deleted instance');
+    $this->assertEqual($instances[0]['bundle'], $bundle, 'The deleted instance is for the correct bundle');
+
+    // There are 0 objects of this bundle
+    $found = field_attach_query($field['id'], array(array('bundle', $bundle)), FIELD_QUERY_NO_LIMIT);
+    $this->assertTrue(!isset($found['test_entity']), 'No objects found after deleting');
+
+    // There are 10 deleted objects of this bundle, and their values
+    // are correct.
+    $found = field_attach_query($field['id'], array(array('bundle', $bundle), array('deleted', 1)), FIELD_QUERY_NO_LIMIT);
+    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 objects found after deleting');
+    foreach ($found['test_entity'] as $id => $obj) {
+      $this->assertEqual($this->entities[$id]->{$field['field_name']}, $obj->{$field['field_name']}, "Deleted object $id loaded correctly");
+    }
+  }
+
+  function testPurgeInstance() {
+    field_test_memorize();
+
+    $bundle = reset($this->bundles);
+    $field = reset($this->fields);
+
+    // Delete the instance
+    field_delete_instance($field['field_name'], $bundle);
+
+    // No field hooks were called.
+    $mem = field_test_memorize();
+    $this->assertEqual(count($mem), 0, 'No field hooks were called');
+
+    $batch_size = 2;
+    for ($count = 8; $count >= 0; $count -= 2) {
+      // Purge two objects
+      field_purge($batch_size);
+
+      // There are $count deleted objects left.
+      $found = field_attach_query($field['id'], array(array('bundle', $bundle), array('deleted', 1)), FIELD_QUERY_NO_LIMIT);
+      $this->assertEqual($count ? count($found['test_entity']) : count($found), $count, 'Correct number of objects found after purging 2');
+    }
+
+    // hook_field_delete() was called on a pseudo-object for each
+    // object.  Each pseudo object has a $field property that matches
+    // the original object, but no others.
+    $mem = field_test_memorize();
+    $this->assertEqual(count($mem['field_test_field_delete']), 10, 'hook_field_delete was called for the right number of objects');
+    $stubs = $this->_generateStubObjects($this->entity_type, $this->entities, $field['field_name']);
+    $count = count($stubs);
+    foreach ($mem['field_test_field_delete'] as $args) {
+      $obj = $args[1];
+      $this->assertEqual($stubs[$obj->ftid], $obj, "hook_field_delete() called with the correct stub {$obj->ftid}");
+      unset($stubs[$obj->ftid]);
+    }
+    $this->assertEqual(count($stubs), $count-10, 'hook_field_delete was called with each object once');
+
+    // The instance still exists, deleted.
+    $instances = field_read_instances(array('field_id' => $field['id'], 'deleted' => 1), array('include_deleted' => 1, 'include_inactive' => 1));
+    $this->assertEqual(count($instances), 1, 'There is one deleted instance');
+
+    // Purge the instance
+    field_purge($batch_size);
+
+    // The instance is gone.
+    $instances = field_read_instances(array('field_id' => $field['id'], 'deleted' => 1), array('include_deleted' => 1, 'include_inactive' => 1));
+    $this->assertEqual(count($instances), 0, 'The instance is gone');
+
+    // The field still exists, not deleted.
+    $fields = field_read_fields(array('id' => $field['id']), array('include_deleted' => 1, 'include_inactive' => 1));
+    $this->assertEqual($field, $fields[$field['id']], 'The field exists and is not deleted');
+  }
+}
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.14
diff -u -F^[fc] -r1.14 field_sql_storage.module
--- modules/field/modules/field_sql_storage/field_sql_storage.module	15 Jun 2009 10:10:46 -0000	1.14
+++ modules/field/modules/field_sql_storage/field_sql_storage.module	7 Jul 2009 02:59:53 -0000
@@ -202,7 +202,7 @@ function field_sql_storage_field_storage
 /**
  * Implement hook_field_storage_load().
  */
-function field_sql_storage_field_storage_load($obj_type, $objects, $age, $skip_fields = array()) {
+function field_sql_storage_field_storage_load($obj_type, $objects, $age, $skip_fields, $options) {
   $etid = _field_sql_storage_etid($obj_type);
   $load_current = $age == FIELD_LOAD_CURRENT;
 
@@ -211,26 +211,40 @@ function field_sql_storage_field_storage
   $delta_count = array();
   foreach ($objects as $obj) {
     list($id, $vid, $bundle) = field_attach_extract_ids($obj_type, $obj);
-    foreach (field_info_instances($bundle) as $field_name => $instance) {
-      if (!isset($skip_fields[$field_name])) {
+
+    if ($options['deleted']) {
+      $instances = field_read_instances(array('bundle' => $bundle), array('include_deleted' => $options['deleted']));
+    }
+    else {
+      $instances = field_info_instances($bundle);
+    }
+
+    foreach ($instances as $field_name => $instance) {
+      $field_name = $instance['field_name'];
+      if (!isset($skip_fields[$field_name]) && (!isset($options['field_id']) || $options['field_id'] == $instance['field_id'])) {
         $objects[$id]->{$field_name} = array();
-        $field_ids[$field_name][] = $load_current ? $id : $vid;
+        $field_ids[$instance['field_id']][] = $load_current ? $id : $vid;
         $delta_count[$id][$field_name] = 0;
       }
     }
   }
 
-  foreach ($field_ids as $field_name => $ids) {
-    $field = field_info_field($field_name);
+  foreach ($field_ids as $field_id => $ids) {
+    $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);
 
-    $results = db_select($table, 't')
+    $query = db_select($table, 't')
       ->fields('t')
       ->condition('etid', $etid)
       ->condition($load_current ? 'entity_id' : 'revision_id', $ids, 'IN')
-      ->condition('deleted', 0)
-      ->orderBy('delta')
-      ->execute();
+      ->orderBy('delta');
+
+    if (empty($options['deleted'])) {
+      $query->condition('deleted', 0);
+    }
+
+    $results = $query->execute();
 
     foreach ($results as $row) {
       if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta_count[$row->entity_id][$field_name] < $field['cardinality']) {
@@ -328,7 +342,7 @@ function field_sql_storage_field_storage
 /**
  * Implement hook_field_storage_delete().
  *
- * This function actually deletes the data from the database.
+ * This function deletes data for all fields for an object from the database.
  */
 function field_sql_storage_field_storage_delete($obj_type, $object) {
   list($id, $vid, $bundle) = field_attach_extract_ids($obj_type, $object);
@@ -336,47 +350,56 @@ function field_sql_storage_field_storage
 
   $instances = field_info_instances($bundle);
   foreach ($instances as $instance) {
-    $field_name = $instance['field_name'];
-    $field = field_read_field($field_name);
-    $table_name = _field_sql_storage_tablename($field);
-    $revision_name = _field_sql_storage_revision_tablename($field);
-    db_delete($table_name)
-      ->condition('etid', $etid)
-      ->condition('entity_id', $id)
-      ->execute();
-    db_delete($revision_name)
-      ->condition('etid', $etid)
-      ->condition('entity_id', $id)
-      ->execute();
+    $field = field_info_field($instance['field_name']);
+    field_sql_storage_field_storage_purge($obj_type, $object, $field, $instance);
   }
 }
 
+/**
+ * Implement hook_field_storage_purge().
+ *
+ * This function deletes data from the database for a single field on
+ * an object.
+ */
+function field_sql_storage_field_storage_purge($obj_type, $object, $field, $instance) {
+  list($id, $vid, $bundle) = field_attach_extract_ids($obj_type, $object);
+  $etid = _field_sql_storage_etid($obj_type);
+
+  $field = field_info_field_by_id($field['id']);
+  $table_name = _field_sql_storage_tablename($field);
+  $revision_name = _field_sql_storage_revision_tablename($field);
+  db_delete($table_name)
+    ->condition('etid', $etid)
+    ->condition('entity_id', $id)
+    ->execute();
+  db_delete($revision_name)
+    ->condition('etid', $etid)
+    ->condition('entity_id', $id)
+    ->execute();
+}
 
 /**
  * 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;
+function field_sql_storage_field_storage_query($field_id, $conditions, $count, &$cursor, $age) {
   $load_current = $age == FIELD_LOAD_CURRENT;
-
-  $field = field_info_field($field_name);
+  
+  $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');
+  $query->fields('t', array('bundle', 'entity_id', 'revision_id'));
   $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'));
-  }
+    // TODO: This probably does not match an index.
+    // TODO: I do not think we need to sort at all any more.
+    //->orderBy(array('etid', 'entity_id', 'delta'))
+    ;
+
   // Add conditions.
   foreach ($conditions as $condition) {
     // A condition is either a (column, value, operator) triple, or a
@@ -404,47 +427,49 @@ function field_sql_storage_field_storage
       $column = _field_sql_storage_columnname($field_name, $column);
     }
     $query->condition($column, $value, $operator);
-  }
 
-  $results = $query->execute();
+    if ($column == 'deleted') {
+      $deleted = $value;
+    }
+  }
 
-  // Build results.
+  // Exclude deleted data unless we have a condition on it.
+  if (!isset($deleted)) {
+    $query->condition('deleted', 0);
+  }
+  
+  // Initialize results array
   $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]++;
-      }
+  // Query for batches of rows until we've read $count objects or
+  // until we get no new rows.
+  $limit = $count;
+  do {
+    if ($limit != FIELD_QUERY_NO_LIMIT) {
+      $query->range($cursor, $limit);
     }
-    else {
-      // Simply return the list of selected ids.
-      $return[$row->type][$id] = $row->entity_id;
+    $results = $query->execute();
+    
+    $found = FALSE;
+    foreach ($results as $row) {
+      $found = TRUE;
+      ++$cursor;
+      
+      // 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;
+
+      // We get multiple rows if the field has multiple deltas.  Only
+      // coun the first one.
+      if (isset($return[$row->type][$id])) {
+        continue;
+      }
+      
+      $return[$row->type][$id] = field_attach_create_stub_object($row->type, array($row->entity_id, $row->revision_id, $row->bundle));
+      --$count;
     }
-  }
+  } while ($found && ($limit != FIELD_QUERY_NO_LIMIT || $count > 0));
 
   return $return;
 }
Index: modules/simpletest/tests/field_test.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/field_test.module,v
retrieving revision 1.11
diff -u -F^[fc] -r1.11 field_test.module
--- modules/simpletest/tests/field_test.module	5 Jul 2009 18:00:10 -0000	1.11
+++ modules/simpletest/tests/field_test.module	7 Jul 2009 02:59:53 -0000
@@ -428,6 +428,30 @@ function field_test_field_create_field($
 }
 
 /**
+ * Memorize calls to hook_field_insert().
+ */
+function field_test_field_insert($obj_type, $object, $field, $instance, $items) {
+  $args = func_get_args();
+  field_test_memorize(__FUNCTION__, $args);
+}
+
+/**
+ * Memorize calls to hook_field_update().
+ */
+function field_test_field_update($obj_type, $object, $field, $instance, $items) {
+  $args = func_get_args();
+  field_test_memorize(__FUNCTION__, $args);
+}
+
+/**
+ * Memorize calls to hook_field_delete().
+ */
+function field_test_field_delete($obj_type, $object, $field, $instance, $items) {
+  $args = func_get_args();
+  field_test_memorize(__FUNCTION__, $args);
+}
+
+/**
  * Implement hook_field_validate().
  *
  * Possible error codes:
