? 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.33
diff -u -F^[fc] -r1.33 field.attach.inc
--- modules/field/field.attach.inc	16 Jul 2009 10:30:12 -0000	1.33
+++ modules/field/field.attach.inc	22 Jul 2009 04:17:40 -0000
@@ -152,6 +152,10 @@ class FieldQueryException extends FieldE
  *  - '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.
+ *    NOTE: Do not specify this option and deleted as TRUE. 
+ *  - '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
  *    be invoked.
@@ -161,22 +165,35 @@ 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 TRUE, the function will operate on deleted fields as well as
+ *    non-deleted fields.  If unset or FALSE, 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' => FALSE,
   );
   $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_instances(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']) && (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
@@ -230,7 +247,11 @@ function _field_invoke($op, $obj_type, $
  * @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
+ *    The field name whose operation should be invoked. By default, the
+ *    operation is invoked on all the fields in the objects'
+ *    bundles. NOTE: Do not specify this option and deleted as TRUE.
+ *  - '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
@@ -241,6 +262,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 TRUE, the function will operate on deleted fields as well as
+ *    non-deleted fields.  If unset or FALSE, only non-deleted fields are
+ *    operated on.
  * @return
  *   An array of returned values keyed by object id.
  */
@@ -248,6 +273,7 @@ function _field_invoke_multiple($op, $ob
   // Merge default options.
   $default_options = array(
     'default' => FALSE,
+    'deleted' => FALSE,
   );
   $options += $default_options;
 
@@ -261,20 +287,29 @@ 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_id = $instance['field_id'];
       $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'] == $field_id) && (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);
+        if (!isset($fields[$field_id])) {
+          $fields[$field_id] = field_info_field_by_id($field_id);
         }
         // Group the corresponding instances and objects.
-        $grouped_instances[$field_name][$id] = $instance;
-        $grouped_objects[$field_name][$id] = $objects[$id];
+        $grouped_instances[$field_id][$id] = $instance;
+        $grouped_objects[$field_id][$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_items[$field_id][$id] = isset($object->$field_name) ? $object->$field_name : array();
       }
     }
     // Initialize the return value for each object.
@@ -282,10 +317,11 @@ function _field_invoke_multiple($op, $ob
   }
 
   // For each field, invoke the field hook and collect results.
-  foreach ($fields as $field_name => $field) {
+  foreach ($fields as $field_id => $field) {
+    $field_name = $field['field_name'];
     $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_id], $field, $grouped_instances[$field_id], $grouped_items[$field_id], $options, $a, $b);
       if (isset($results)) {
         // Collect results by object.
         // For hooks with array results, we merge results together.
@@ -303,9 +339,9 @@ function _field_invoke_multiple($op, $ob
 
     // Populate field values back in the objects, but avoid replacing missing
     // fields with an empty array (those are not equivalent on update).
-    foreach ($grouped_objects[$field_name] as $id => $object) {
-      if ($grouped_items[$field_name][$id] !== array() || property_exists($object, $field_name)) {
-        $object->$field_name = $grouped_items[$field_name][$id];
+    foreach ($grouped_objects[$field_id] as $id => $object) {
+      if ($grouped_items[$field_id][$id] !== array() || property_exists($object, $field_name)) {
+        $object->$field_name = $grouped_items[$field_id][$id];
       }
     }
   }
@@ -384,10 +420,15 @@ function field_attach_form($obj_type, $o
  *   field_attach_load_revision() instead of passing FIELD_LOAD_REVISION.
  * @param $options
  *   An associative array of additional options, with the following keys:
- *   - 'field_name'
- *     The field name 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.
+ *  - '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 TRUE, the function will operate on deleted fields as well as
+ *    non-deleted fields.  If unset or FALSE, 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.
@@ -395,9 +436,16 @@ function field_attach_form($obj_type, $o
 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' => FALSE,
+  );
+  $options += $default_options;
+
   $info = field_info_fieldable_types($obj_type);
-  // Only the most current revision of cacheable fieldable types can be cached.
-  $cache_read = $load_current && $info['cacheable'];
+  // Only the most current revision of non-deleted fields for
+  // cacheable fieldable types can be cached.
+  $cache_read = $load_current && $info['cacheable'] && empty($options['deleted']);
   // In addition, do not write to the cache when loading a single field.
   $cache_write = $cache_read && !isset($options['field_name']);
 
@@ -747,8 +795,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).
@@ -761,6 +809,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:
@@ -813,7 +863,7 @@ function field_attach_delete_revision($o
  *   Throws a FieldQueryException if the field's storage doesn't support the
  *   specified conditions.
  */
-function field_attach_query($field_name, $conditions, $count, &$cursor = NULL, $age = FIELD_LOAD_CURRENT) {
+function field_attach_query($field_id, $conditions, $count, &$cursor = NULL, $age = FIELD_LOAD_CURRENT) {
   if (!isset($cursor)) {
     $cursor = 0;
   }
@@ -823,7 +873,7 @@ function field_attach_query($field_name,
   $skip_field = FALSE;
   foreach (module_implements('field_attach_pre_query') as $module) {
     $function = $module . '_field_attach_pre_query';
-    $results = $function($field_name, $conditions, $count, $cursor, $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;
@@ -832,7 +882,7 @@ function field_attach_query($field_name,
   // If the request hasn't been handled, let the storage engine handle it.
   if (!$skip_field) {
     $function = variable_get('field_storage_module', 'field_sql_storage') . '_field_storage_query';
-    $results = $function($field_name, $conditions, $count, $cursor, $age);
+    $results = $function($field_id, $conditions, $count, $cursor, $age);
   }
 
   return $results;
@@ -843,8 +893,8 @@ 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 $count
@@ -861,8 +911,8 @@ function field_attach_query($field_name,
  * @return
  *   See field_attach_query().
  */
-function field_attach_query_revisions($field_name, $conditions, $count, &$cursor = NULL) {
-  return field_attach_query($field_name, $conditions, $count, $cursor, 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.21
diff -u -F^[fc] -r1.21 field.crud.inc
--- modules/field/field.crud.inc	14 Jul 2009 10:27:29 -0000	1.21
+++ modules/field/field.crud.inc	22 Jul 2009 04:17:40 -0000
@@ -264,6 +264,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);
@@ -440,7 +443,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);
@@ -694,3 +697,232 @@ 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.
+ *
+ * @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'], 'deleted' => TRUE);
+  _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_info_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_info_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.8
diff -u -F^[fc] -r1.8 field.info.inc
--- modules/field/field.info.inc	10 Jul 2009 05:58:13 -0000	1.8
+++ modules/field/field.info.inc	22 Jul 2009 04:17:40 -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_cache_clear() {
+  _field_info_collate_types(TRUE);
+  _field_info_collate_fields(TRUE);
+}
+
+/**
  * Collate all information on field types, widget types and related structures.
  *
  * @param $reset
@@ -185,6 +197,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()),
       );
 
@@ -193,6 +206,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');
@@ -400,7 +414,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
@@ -414,6 +429,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.20
diff -u -F^[fc] -r1.20 field.module
--- modules/field/field.module	15 Jul 2009 17:55:18 -0000	1.20
+++ modules/field/field.module	22 Jul 2009 04:17:40 -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().
  */
 
 /**
@@ -337,8 +341,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_cache_clear();
 
   // 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.34
diff -u -F^[fc] -r1.34 field.test
--- modules/field/field.test	15 Jul 2009 17:55:18 -0000	1.34
+++ modules/field/field.test	22 Jul 2009 04:17:40 -0000
@@ -135,7 +135,8 @@ class FieldAttachTestCase extends Drupal
     for ($i = 1; $i <= 3; $i++) {
       $field_names[$i] = 'field_' . $i;
       $field = array('field_name' => $field_names[$i], 'type' => 'test_field');
-      field_create_field($field);
+      $field = field_create_field($field);
+      $field_ids[$i] = $field['id'];
       foreach ($field_bundles_map[$i] as $bundle) {
         $instance = array(
           'field_name' => $field_names[$i],
@@ -176,7 +177,7 @@ class FieldAttachTestCase extends Drupal
 
     // Check that the single-field load option works.
     $entity = field_test_create_stub_entity(1, 1, $bundles[1]);
-    field_attach_load($entity_type, array(1 => $entity), FIELD_LOAD_CURRENT, array('field_name' => $field_names[1]));
+    field_attach_load($entity_type, array(1 => $entity), FIELD_LOAD_CURRENT, array('field_id' => $field_ids[1]));
     $this->assertEqual($entity->{$field_names[1]}[0]['value'], $values[1][$field_names[1]], t('Entity %index: expected value was found.', array('%index' => 1)));
     $this->assertEqual($entity->{$field_names[1]}[0]['additional_key'], 'additional_value', t('Entity %index: extra information was found', array('%index' => 1)));
     $this->assert(!isset($entity->{$field_names[2]}), t('Entity %index: field %field_name is not loaded.', array('%index' => 2, '%field_name' => $field_names[2])));
@@ -305,7 +306,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, FIELD_QUERY_NO_LIMIT);
+      $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)));
     }
 
@@ -314,31 +315,31 @@ 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, FIELD_QUERY_NO_LIMIT);
+    $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, FIELD_QUERY_NO_LIMIT);
+    $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, FIELD_QUERY_NO_LIMIT);
+    $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, FIELD_QUERY_NO_LIMIT);
+    $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, FIELD_QUERY_NO_LIMIT);
+    $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 result format.
     $conditions = array(array('value', $values[0]));
-    $result = field_attach_query($this->field_name, $conditions, FIELD_QUERY_NO_LIMIT);
+    $result = field_attach_query($this->field_id, $conditions, FIELD_QUERY_NO_LIMIT);
     $expected = array(
       $entity_types[1] => array(
         $entities[1]->ftid => field_test_create_stub_entity($entities[1]->ftid, $entities[1]->ftvid),
@@ -371,7 +372,7 @@ class FieldAttachTestCase extends Drupal
     // back the right ones.
     $cursor = 0;
     foreach (array(1 => 1, 3 => 3, 5 => 5, 8 => 8, 13 => 3) as $count => $expect) {
-      $found = field_attach_query($this->field_name, array(array('bundle', 'offset_bundle')), $count, $cursor);
+      $found = field_attach_query($this->field_id, array(array('bundle', 'offset_bundle')), $count, $cursor);
       if (isset($found['test_entity'])) {
         $this->assertEqual(count($found['test_entity']), $expect, t('Requested @count, expected @expect, got @found, cursor @cursor', array('@count' => $count, '@expect' => $expect, '@found' => count($found['test_entity']), '@cursor' => $cursor)));
         foreach ($found['test_entity'] as $id => $entity) {
@@ -414,7 +415,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, FIELD_QUERY_NO_LIMIT);
+      $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)));
     }
 
@@ -423,23 +424,23 @@ 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, FIELD_QUERY_NO_LIMIT);
+    $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, FIELD_QUERY_NO_LIMIT);
+    $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, FIELD_QUERY_NO_LIMIT);
+    $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, FIELD_QUERY_NO_LIMIT);
+    $result = field_attach_query_revisions($this->field_id, $conditions, FIELD_QUERY_NO_LIMIT);
     $expected = array(
       $entity_type => array(
         $entities[1]->ftid => field_test_create_stub_entity($entities[1]->ftid, $entities[1]->ftvid),
@@ -1701,3 +1702,202 @@ class FieldInstanceCrudTestCase extends 
     $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'  => 'Field bulk delete tests',
+      'description'=> 'Bulk delete fields and instances, and clean up afterwards.',
+      'group' => '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;
+  }
+
+  /**
+   * Given an array of potentially fully-populated objects and an
+   * optional field name, generate an array of stub objects of the
+   * same fieldable type which contains the data for the field name
+   * (if given).
+   *
+   * @param $obj_type
+   *   The entity type of $objects.
+   * @param $objects
+   *   An array of objects of type $obj_type.
+   * @param $field_name
+   *   Optional; a field name whose data should be copied from
+   *   $objects into the returned stub objects.
+   * @return
+   *   An array of stub objects corresponding to $objects.
+   */
+  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.
+
+    // Clean up data from previous test cases.
+    $this->fields = array();
+    $this->instances = array();
+
+    // 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++;
+      }
+    }
+  }
+
+  /**
+   * Verify that deleting an instance leaves the field data items in
+   * the database and that the appropriate Field API functions can
+   * operate on the deleted data and instance.
+   *
+   * This tests how field_attach_query() interacts with
+   * field_delete_instance() and could be moved to FieldCrudTestCase,
+   * but depends on this class' setUp().
+   */
+  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 with non-deleted data.
+    $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 objects of this bundle when deleted fields are
+    // allowed, and their values are correct.
+    $found = field_attach_query($field['id'], array(array('bundle', $bundle), array('deleted', 1)), 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']}, "Object $id with deleted data 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.17
diff -u -F^[fc] -r1.17 field_sql_storage.module
--- modules/field/modules/field_sql_storage/field_sql_storage.module	15 Jul 2009 17:55:18 -0000	1.17
+++ modules/field/modules/field_sql_storage/field_sql_storage.module	22 Jul 2009 04:17:40 -0000
@@ -211,26 +211,39 @@ 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]) && (!isset($options['field_name']) || $options['field_name'] == $instance['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 $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);
 
     $query = db_select($table, 't')
       ->fields('t')
       ->condition('etid', $etid)
       ->condition($load_current ? 'entity_id' : 'revision_id', $ids, 'IN')
-      ->condition('deleted', 0)
       ->orderBy('delta');
 
+    if (empty($options['deleted'])) {
+      $query->condition('deleted', 0);
+    }
+
     $results = $query->execute();
 
     foreach ($results as $row) {
@@ -329,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);
@@ -337,39 +350,54 @@ 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, $count, &$cursor, $age) {
+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'))
     ->fields('e', array('type'))
-    ->condition('deleted', 0)
+    // We need to require objects arrive in a consistent order for
+    // range() operation to work.
     ->orderBy('t.etid')
     ->orderBy('t.entity_id');
 
@@ -400,6 +428,15 @@ function field_sql_storage_field_storage
       $column = _field_sql_storage_columnname($field_name, $column);
     }
     $query->condition($column, $value, $operator);
+
+    if ($column == 'deleted') {
+      $deleted = $value;
+    }
+  }
+
+  // Exclude deleted data unless we have a condition on it.
+  if (!isset($deleted)) {
+    $query->condition('deleted', 0);
   }
 
   // Initialize results array
@@ -420,7 +457,6 @@ function field_sql_storage_field_storage
     foreach ($results as $row) {
       $row_count++;
       $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);
@@ -503,4 +539,19 @@ function field_sql_storage_field_storage
       ->condition('bundle', $bundle_old)
       ->execute();
   }
-}
\ No newline at end of file
+}
+
+/**
+ * Implement hook_field_storage_purge_field().
+ *
+ * All field data items and instances have already been purged, so all
+ * that is left is to delete the table.
+ */
+function field_sql_storage_field_storage_purge_field($field) {
+  $ret = array();
+  $table_name = _field_sql_storage_tablename($field);
+  $revision_name = _field_sql_storage_revision_tablename($field);
+  db_drop_table($ret, $table_name);
+  db_drop_table($ret, $revision_name);
+}
+
Index: modules/simpletest/tests/field_test.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/field_test.module,v
retrieving revision 1.13
diff -u -F^[fc] -r1.13 field_test.module
--- modules/simpletest/tests/field_test.module	10 Jul 2009 05:58:13 -0000	1.13
+++ modules/simpletest/tests/field_test.module	22 Jul 2009 04:17:40 -0000
@@ -369,6 +369,7 @@ function field_test_field_schema($field)
   );
 }
 
+
 /**
  * Implement hook_field_load().
  */
@@ -640,4 +641,28 @@ function field_test_memorize($key = NULL
 function field_test_field_create_field($field) {
   $args = func_get_args();
   field_test_memorize(__FUNCTION__, $args);
-}
\ No newline at end of file
+}
+
+/**
+ * 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);
+}
