Index: modules/field/field.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.test,v
retrieving revision 1.36
diff -u -r1.36 field.test
--- modules/field/field.test	2 Aug 2009 11:24:21 -0000	1.36
+++ modules/field/field.test	5 Aug 2009 23:46:23 -0000
@@ -135,7 +135,8 @@
     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 @@
 
     // 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 @@
     // 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 @@
       $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 @@
     // 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 @@
     // 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 @@
       $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),
@@ -712,6 +713,12 @@
     field_attach_insert($cached_type, $entity);
     $this->assertFalse(cache_get($cid, 'cache_field'), t('Cached: no cache entry on insert'));
 
+    // Load a single field, and check that no cache entry is present.
+    $entity = clone($entity_init);
+    field_attach_load($cached_type, array($entity->ftid => $entity), FIELD_LOAD_CURRENT, array('field_id' => $this->field_id));
+    $cache = cache_get($cid, 'cache_field');
+    $this->assertFalse(cache_get($cid, 'cache_field'), t('Cached: no cache entry on loading a single field'));
+
     // Load, and check that a cache entry is present with the expected values.
     $entity = clone($entity_init);
     field_attach_load($cached_type, array($entity->ftid => $entity));
@@ -1755,3 +1762,235 @@
     $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, because it has a second instance.
+    $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');
+  }
+
+  function testPurgeField() {
+    $field = reset($this->fields);
+
+    foreach ($this->bundles as $bundle) {
+      // Delete the instance.
+      field_delete_instance($field['field_name'], $bundle);
+
+      // Purge the data.
+      field_purge(10);
+
+      // Purge again to purge the instance.
+      field_purge(0);
+
+      // The field still exists, not deleted, because it was never 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');
+    }
+
+    // Delete the field.
+    field_delete_field($field['field_name']);
+
+    // The field still exists, deleted.
+    $fields = field_read_fields(array('id' => $field['id']), array('include_deleted' => 1, 'include_inactive' => 1));
+    $this->assertEqual($fields[$field['id']]['deleted'], 1, 'The field exists and is deleted');
+
+    // Purge the field.
+    field_purge(0);
+
+    // The field is gone.
+    $fields = field_read_fields(array('id' => $field['id']), array('include_deleted' => 1, 'include_inactive' => 1));
+    $this->assertEqual(count($fields), 0, 'The field is purged.');
+  }
+}
Index: modules/field/field.info.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.info.inc,v
retrieving revision 1.11
diff -u -r1.11 field.info.inc
--- modules/field/field.info.inc	2 Aug 2009 11:24:21 -0000	1.11
+++ modules/field/field.info.inc	5 Aug 2009 23:46:22 -0000
@@ -17,6 +17,18 @@
  */
 
 /**
+ * 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
@@ -151,11 +163,15 @@
  * @return
  *   If $reset is TRUE, nothing.
  *   If $reset is FALSE, an array containing the following elements:
- *   - fields: array of all defined Field objects, keyed by field name. Each
- *     field has an additional element, bundles, which is an array of all
- *     bundles to which the field is assigned.
- *   - instances: array whose keys are bundle names and whose values are an
- *     array, keyed by field name, of all instances in that bundle.
+ *   - fields: Array of existing fields, keyed by field name. This entry only
+ *     lists non-deleted fields. Each field has an additional element,
+ *     'bundles', which is an array of all non-deleted instances to which the
+ *     field is assigned.
+ *   - fields_id: Array of existing fields, keyed by field id. This entry lists
+ *     both deleted and non-deleted fields. The bundles element is the same as
+ *     for 'fields'.
+ *   - instances: Array of existing instances, keyed by bundle name and field
+ *     name. This entry only lists non-deleted instances.
  */
 function _field_info_collate_fields($reset = FALSE) {
   static $info;
@@ -168,32 +184,45 @@
 
   if (!isset($info)) {
     if ($cached = cache_get('field_info_fields', 'cache_field')) {
-      $info = $cached->data;
+      $definitions = $cached->data;
     }
     else {
-      $info = array(
-        'fields' => array(),
-        'instances' => array(),
+      $definitions = array(
+        'field_ids' => field_read_fields(array(), array('include_deleted' => 1)),
+        'instances' => field_read_instances(),
       );
+      cache_set('field_info_fields', $definitions, 'cache_field');
+    }
 
-      // Populate fields
-      $fields = field_read_fields();
-      foreach ($fields as $field) {
-        $field = _field_info_prepare_field($field);
-        $info['fields'][$field['field_name']] = $field;
-      }
+    // Populate 'field_ids' with all fields.
+    $info['field_ids'] = array();
+    foreach ($definitions['field_ids'] as $key => $field) {
+      $info['field_ids'][$key] = $definitions['field_ids'][$key] = _field_info_prepare_field($field);
+    }
 
-      // Populate instances.
-      $info['instances'] = array_fill_keys(array_keys(field_info_bundles()), array());
-      $instances = field_read_instances();
-      foreach ($instances as $instance) {
-        $field = $info['fields'][$instance['field_name']];
-        $instance = _field_info_prepare_instance($instance, $field);
-        $info['instances'][$instance['bundle']][$instance['field_name']] = $instance;
-        $info['fields'][$instance['field_name']]['bundles'][] = $instance['bundle'];
+    // Populate 'fields' only with non-deleted fields.
+    $info['field'] = array();
+    foreach ($info['field_ids'] as $field) {
+      if (!$field['deleted']) {
+        $info['fields'][$field['field_name']] = $field;
       }
+    }
 
-      cache_set('field_info_fields', $info, 'cache_field');
+    // Populate 'instances'. Only non-deleted instances are considered.
+    $info['instances'] = array();
+    foreach (field_info_bundles() as $bundle => $bundle_info) {
+      $info['instances'][$bundle] = array();
+    }
+    foreach ($definitions['instances'] as $instance) {
+      $field = $info['fields'][$instance['field_name']];
+      $instance = _field_info_prepare_instance($instance, $field);
+      $info['instances'][$instance['bundle']][$instance['field_name']] = $instance;
+      // Enrich field definitions with the list of bundles where they have
+      // instances. NOTE: Deleted fields in $info['field_ids'] are not
+      // enriched because all of their instances are deleted, too, and
+      // are thus not in $definitions['instances'].
+      $info['fields'][$instance['field_name']]['bundles'][] = $instance['bundle'];
+      $info['field_ids'][$instance['field_id']]['bundles'][] = $instance['bundle'];
     }
   }
 
@@ -441,7 +470,8 @@
  * 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
@@ -455,6 +485,24 @@
 }
 
 /**
+ * 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.attach.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.attach.inc,v
retrieving revision 1.33
diff -u -r1.33 field.attach.inc
--- modules/field/field.attach.inc	16 Jul 2009 10:30:12 -0000	1.33
+++ modules/field/field.attach.inc	5 Aug 2009 23:46:21 -0000
@@ -152,6 +152,11 @@
  *  - '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: This option is not compatible with the 'deleted' option; the
+ *    'field_id' option should be used instead.
+ *  - 'field_id'
+ *    The id of the field whose operation should be invoked. By default, the
+ *    operation is invoked on all the fields in the objects' bundles.
  *  - 'default'
  *    A boolean value, specifying which implementation of the operation should
  *    be invoked.
@@ -161,22 +166,35 @@
  *      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
@@ -231,6 +249,11 @@
  *   An associative array of additional options, with the following keys:
  *  - 'field_name'
  *    The name of the field whose operation should be invoked. By default, the
+ *    operation is invoked on all the fields in the object's bundle.
+ *    NOTE: This option is not compatible with the 'deleted' option; the
+ *    'field_id' option should be used instead.
+ *  - 'field_id'
+ *    The id of the field whose operation should be invoked. By default, the
  *    operation is invoked on all the fields in the objects' bundles.
  *  - 'default'
  *    A boolean value, specifying which implementation of the operation should
@@ -241,6 +264,10 @@
  *      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 +275,7 @@
   // Merge default options.
   $default_options = array(
     'default' => FALSE,
+    'deleted' => FALSE,
   );
   $options += $default_options;
 
@@ -259,22 +287,36 @@
 
   // Go through the objects and collect the fields on which the hook should be
   // invoked.
+  //
+  // We group fields by id, not by name, because this function can operate on
+  // deleted fields which may have non-unique names. However, objects can only
+  // contain data for a single field for each name, even if that field is deleted,
+  // so we reference field data via the $object->$field_name property.
   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 +324,11 @@
   }
 
   // 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 +346,9 @@
 
     // 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 +427,14 @@
  *   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,11 +442,18 @@
 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']);
+  $cache_write = $cache_read && !isset($options['field_id']);
 
   if (empty($objects)) {
     return;
@@ -747,8 +801,8 @@
  * 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 +815,8 @@
  *     - '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 +869,7 @@
  *   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 +879,7 @@
   $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 +888,7 @@
   // 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 +899,8 @@
  *
  * 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 +917,8 @@
  * @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.22
diff -u -r1.22 field.crud.inc
--- modules/field/field.crud.inc	2 Aug 2009 11:24:21 -0000	1.22
+++ modules/field/field.crud.inc	5 Aug 2009 23:46:21 -0000
@@ -264,6 +264,9 @@
   // 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);
@@ -436,7 +439,7 @@
   // 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);
@@ -681,3 +684,218 @@
 /**
  * @} 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 fields.
+ *
+ * This function will purge deleted field data on up to a specified maximum
+ * number of objects 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.
+  $instances = field_read_instances(array('deleted' => 1), array('include_deleted' => 1));
+
+  foreach ($instances as $instance) {
+    $field = field_info_field_by_id($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.
+          // Stop when he have done our batch size.
+          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.
+  $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.api.php
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.api.php,v
retrieving revision 1.24
diff -u -r1.24 field.api.php
--- modules/field/field.api.php	1 Aug 2009 06:03:12 -0000	1.24
+++ modules/field/field.api.php	5 Aug 2009 23:46:21 -0000
@@ -847,13 +847,13 @@
  *   FIELD_LOAD_CURRENT to load the most recent revision for all fields, or
  *   FIELD_LOAD_REVISION to load the version indicated by each object.
  * @param $skip_fields
- *   An array keyed by names of fields whose data has already been loaded and
+ *   An array keyed by field ids whose data has already been loaded and
  *   therefore should not be loaded again. The values associated to these keys
  *   are not specified.
  * @return
  *   - Loaded field values are added to $objects. Fields with no values should be
  *   set as an empty array.
- *   - Loaded field names are set as keys in $skip_fields.
+ *   - Loaded field ids are set as keys in $skip_fields.
  */
 function hook_field_attach_pre_load($obj_type, $objects, $age, &$skip_fields) {
 }
@@ -922,11 +922,11 @@
  * @param $object
  *   The object with fields to save.
  * @param $skip_fields
- *   An array keyed by names of fields whose data has already been written and
+ *   An array keyed by field ids whose data has already been written and
  *   therefore should not be written again. The values associated to these keys
  *   are not specified.
  * @return
- *   Saved field names are set set as keys in $skip_fields.
+ *   Saved field ids are set set as keys in $skip_fields.
  */
 function hook_field_attach_pre_insert($obj_type, $object, &$skip_fields) {
 }
@@ -942,11 +942,11 @@
  * @param $object
  *   The object with fields to save.
  * @param $skip_fields
- *   An array keyed by names of fields whose data has already been written and
+ *   An array keyed by field ids whose data has already been written and
  *   therefore should not be written again. The values associated to these keys
  *   are not specified.
  * @return
- *   Saved field names are set set as keys in $skip_fields.
+ *   Saved field ids are set set as keys in $skip_fields.
  */
 function hook_field_attach_pre_update($obj_type, $object, &$skip_fields) {
 }
@@ -1081,7 +1081,7 @@
  *   fields, or FIELD_LOAD_REVISION to load the version indicated by
  *   each object.
  * @param $skip_fields
- *   An array keyed by names of fields whose data has already been loaded and
+ *   An array keyed by field ids whose data has already been loaded and
  *   therefore should not be loaded again. The values associated to these keys
  *   are not specified.
  * @return
@@ -1102,7 +1102,7 @@
  *   FIELD_STORAGE_UPDATE when updating an existing object,
  *   FIELD_STORAGE_INSERT when inserting a new object.
  * @param $skip_fields
- *   An array keyed by names of fields whose data has already been written and
+ *   An array keyed by field ids whose data has already been written and
  *   therefore should not be written again. The values associated to these keys
  *   are not specified.
  */
Index: modules/field/field.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/field.module,v
retrieving revision 1.21
diff -u -r1.21 field.module
--- modules/field/field.module	2 Aug 2009 11:24:21 -0000	1.21
+++ modules/field/field.module	5 Aug 2009 23:46:22 -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().
  */
 
 /**
@@ -178,6 +182,16 @@
 }
 
 /**
+ * Implement hook_cron().
+ *
+ * Purges some deleted Field API data, if any exists.
+ */
+function field_cron() {
+  $limit = variable_get('field_purge_batch_size', 10);
+  field_purge($limit);
+}
+
+/**
  * Implement hook_modules_installed().
  */
 function field_modules_installed($modules) {
@@ -337,8 +351,7 @@
   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/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 -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	5 Aug 2009 23:46:23 -0000
@@ -211,26 +211,39 @@
   $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[$instance['field_id']]) && (!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) {
@@ -261,7 +274,7 @@
   $instances = field_info_instances($bundle);
   foreach ($instances as $instance) {
     $field_name = $instance['field_name'];
-    if (isset($skip_fields[$field_name])) {
+    if (isset($skip_fields[$instance['field_id']])) {
       continue;
     }
 
@@ -329,7 +342,7 @@
 /**
  * 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 @@
 
   $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 @@
       $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 @@
     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 @@
       ->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 -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	5 Aug 2009 23:46:23 -0000
@@ -640,4 +640,28 @@
 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);
+}
Index: modules/taxonomy/taxonomy.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.module,v
retrieving revision 1.493
diff -u -r1.493 taxonomy.module
--- modules/taxonomy/taxonomy.module	4 Aug 2009 06:50:07 -0000	1.493
+++ modules/taxonomy/taxonomy.module	5 Aug 2009 23:46:24 -0000
@@ -2015,7 +2015,7 @@
       if ($obj_types) {
         $conditions[] = array('type', $obj_types, 'NOT IN');
       }
-      $results = field_attach_query($field['field_name'], $conditions, FIELD_QUERY_NO_LIMIT);
+      $results = field_attach_query($field['id'], $conditions, FIELD_QUERY_NO_LIMIT);
       foreach ($results as $obj_type => $objects) {
         foreach (array_keys($objects) as $id) {
           $cids[] = "field:$obj_type:$id";
