? 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/field.delete.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.23
diff -u -F^[fc] -r1.23 field.attach.inc
--- modules/field/field.attach.inc	22 Jun 2009 09:10:04 -0000	1.23
+++ modules/field/field.attach.inc	22 Jun 2009 20:57:08 -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.
@@ -168,18 +168,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.
@@ -248,6 +261,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.
  */
@@ -255,6 +272,7 @@ function _field_invoke_multiple($op, $ob
   // Merge default options.
   $default_options = array(
     'default' => FALSE,
+    'deleted' => 0,
   );
   $options += $default_options;
 
@@ -268,13 +286,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;
@@ -292,7 +318,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.
@@ -735,8 +761,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).
@@ -749,6 +775,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:
@@ -771,6 +799,13 @@ function field_attach_delete_revision($o
  *     array('value', 12, '>'),
  *   );
  *   @endcode
+ * @param $limit
+ *   The maximum number of results that is requested. This is only a
+ *   hint to the storage engine(s); callers should be prepared to
+ *   handle 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 $result_format
  *   - FIELD_QUERY_RETURN_IDS (default): return the ids of the objects matching the
  *     conditions.
@@ -788,7 +823,7 @@ function field_attach_delete_revision($o
  *   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,
+ *     $field_id 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
@@ -797,13 +832,13 @@ 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, $result_format = FIELD_QUERY_RETURN_IDS, $age = FIELD_LOAD_CURRENT) {
+function field_attach_query($field_id, $conditions, $limit, $result_format = FIELD_QUERY_RETURN_IDS, $age = FIELD_LOAD_CURRENT) {
   // Give a chance to 3rd party modules that bypass the storage engine to
   // handle the query.
   $skip_field = FALSE;
   foreach (module_implements('field_attach_pre_query') as $module) {
     $function = $module . '_field_attach_pre_query';
-    $results = $function($field_name, $conditions, $result_format, $age, $skip_field);
+    $results = $function($field_id, $conditions, $limit, $result_format, $age, $skip_field);
     // Stop as soon as a module claims it handled the query.
     if ($skip_field) {
       break;
@@ -811,7 +846,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);
+    $results = module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_query', $field_id, $conditions, $limit, $result_format, $age);
   }
 
   if ($result_format == FIELD_QUERY_RETURN_VALUES) {
@@ -819,7 +854,7 @@ function field_attach_query($field_name,
       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));
+        _field_invoke_multiple('load', $obj_type, $pseudo_objects, $age, $b, array('field_id' => $field_id));
 
         // Invoke hook_field_attach_load().
         foreach (module_implements('field_attach_load') as $module) {
@@ -838,7 +873,7 @@ function field_attach_query($field_name,
 
           // Invoke hook_field_load().
           $b = NULL;
-          _field_invoke_multiple('load', $obj_type, $objects, $age, $b, array('field_name' => $field_name));
+          _field_invoke_multiple('load', $obj_type, $objects, $age, $b, array('field_id' => $field_id));
 
           // Invoke hook_field_attach_load().
           foreach (module_implements('field_attach_load') as $module) {
@@ -858,10 +893,17 @@ 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 $limit
+ *   The maximum number of objects that is requested. This is only a
+ *   hint to the storage engine(s); callers should be prepared to
+ *   handle 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 $result_format
  *   See field_attach_query().
  *   Note that the FIELD_QUERY_RETURN_VALUES option might cause performance
@@ -869,8 +911,8 @@ function field_attach_query($field_name,
  * @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, $limit, $result_format = FIELD_QUERY_RETURN_IDS) {
+  return field_attach_query($field_id, $conditions, $limit, $result_format, FIELD_LOAD_REVISION);
 }
 
 /**
Index: modules/field/field.crud.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.crud.inc,v
retrieving revision 1.15
diff -u -F^[fc] -r1.15 field.crud.inc
--- modules/field/field.crud.inc	12 Jun 2009 08:39:36 -0000	1.15
+++ modules/field/field.crud.inc	22 Jun 2009 20:57:08 -0000
@@ -268,6 +268,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);
@@ -689,3 +692,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 along with field data for the
+    // instance.
+    $obj_types = field_attach_query($instance['field_id'], array(array('bundle', $instance['bundle']), array('deleted', 1)), $batch_size, FIELD_QUERY_RETURN_VALUES);
+
+    if (count($obj_types) > 0) {
+      // Field data for the instance still exists.
+      foreach ($obj_types as $obj_type => $objects) {
+        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.6
diff -u -F^[fc] -r1.6 field.info.inc
--- modules/field/field.info.inc	5 Jun 2009 18:25:41 -0000	1.6
+++ modules/field/field.info.inc	22 Jun 2009 20:57:08 -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
@@ -179,6 +191,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()),
       );
 
@@ -187,6 +200,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');
@@ -394,7 +408,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
@@ -408,6 +423,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.14
diff -u -F^[fc] -r1.14 field.module
--- modules/field/field.module	22 Jun 2009 09:10:04 -0000	1.14
+++ modules/field/field.module	22 Jun 2009 20:57:08 -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().
@@ -348,8 +358,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.27
diff -u -F^[fc] -r1.27 field.test
--- modules/field/field.test	16 Jun 2009 08:40:18 -0000	1.27
+++ modules/field/field.test	22 Jun 2009 20:57:08 -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,31 +306,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);
+    $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,
@@ -348,7 +349,7 @@ class FieldAttachTestCase extends Drupal
     // 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);
+    $result = field_attach_query($this->field_id, $conditions, FIELD_QUERY_NO_LIMIT, FIELD_QUERY_RETURN_VALUES);
     $expected = array(
       $entity_types[1] => array(
         $entities[1]->ftid => (object) array(
@@ -402,7 +403,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,23 +412,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);
+    $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,
@@ -444,7 +445,7 @@ class FieldAttachTestCase extends Drupal
     // 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);
+    $result = field_attach_query_revisions($this->field_id, $conditions, FIELD_QUERY_NO_LIMIT, FIELD_QUERY_RETURN_VALUES);
     $expected = array(
       $entity_type => array(
         $entities[1]->ftvid => (object) array(
@@ -1668,3 +1669,180 @@ 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
+    field_test_create_bundle('bb_1', 'bb_1');
+    field_test_create_bundle('bb_2', 'bb_2');
+
+    // 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_QUERY_RETURN_VALUES);
+    $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, FIELD_QUERY_RETURN_VALUES);
+      $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	22 Jun 2009 20:57:08 -0000
@@ -328,7 +328,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,30 +336,43 @@ 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) {
+function field_sql_storage_field_storage_query($field_id, $conditions, $limit, $result_format, $age) {
   $load_values = $result_format == FIELD_QUERY_RETURN_VALUES;
   $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']);
 
@@ -368,7 +381,6 @@ function field_sql_storage_field_storage
   $query->join('field_config_entity_type', 'e', 't.etid = e.etid');
   $query
     ->fields('e', array('type'))
-    ->condition('deleted', 0)
     ->orderBy('delta');
   // Add fields, depending on the return format.
   if ($load_values) {
@@ -404,6 +416,19 @@ 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);
+  }
+  
+  if ($limit != FIELD_QUERY_NO_LIMIT) {
+    $query->range(0, $limit);
   }
 
   $results = $query->execute();
Index: modules/simpletest/tests/field_test.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/tests/field_test.module,v
retrieving revision 1.9
diff -u -F^[fc] -r1.9 field_test.module
--- modules/simpletest/tests/field_test.module	27 May 2009 18:34:00 -0000	1.9
+++ modules/simpletest/tests/field_test.module	22 Jun 2009 20:57:08 -0000
@@ -365,6 +365,43 @@ function field_test_field_schema($field)
   );
 }
 
+function field_test_memorize($tag = NULL, $args = NULL) {
+  $memorize =& drupal_static(__FUNCTION__, NULL);
+
+  if (is_null($tag)) {
+    $ret = $memorize;
+    $memorize = array();
+    return $ret;
+  }
+  if (is_array($memorize)) {
+    $memorize[$tag][] = $args;
+  }
+}
+
+/**
+ * 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().
  *
