summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
authorDries Buytaert <dries@buytaert.net>2009-08-11 14:59:40 +0000
committerDries Buytaert <dries@buytaert.net>2009-08-11 14:59:40 +0000
commit9c0e6e92426a061f78e6dfe685c7c37c7f72bc62 (patch)
tree77be265c750278d74f0a822f934ee37631848fd0 /modules
parent9a8cfc2fd10bd5d66ec0b73824be90d328f97781 (diff)
downloadbrdo-9c0e6e92426a061f78e6dfe685c7c37c7f72bc62.tar.gz
brdo-9c0e6e92426a061f78e6dfe685c7c37c7f72bc62.tar.bz2
- Patch #367753 by yched, bjaspan: add support for bulk deletion to Fields API.
Diffstat (limited to 'modules')
-rw-r--r--modules/field/field.api.php16
-rw-r--r--modules/field/field.attach.inc143
-rw-r--r--modules/field/field.crud.inc219
-rw-r--r--modules/field/field.info.inc98
-rw-r--r--modules/field/field.module17
-rw-r--r--modules/field/field.test279
-rw-r--r--modules/field/modules/field_sql_storage/field_sql_storage.module101
-rw-r--r--modules/simpletest/tests/field_test.module26
-rw-r--r--modules/taxonomy/taxonomy.module2
9 files changed, 777 insertions, 124 deletions
diff --git a/modules/field/field.api.php b/modules/field/field.api.php
index 02e9878c0..6f40c2009 100644
--- a/modules/field/field.api.php
+++ b/modules/field/field.api.php
@@ -847,13 +847,13 @@ function hook_field_attach_form($obj_type, $object, &$form, &$form_state) {
* 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 @@ function hook_field_attach_presave($obj_type, $object) {
* @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 @@ function hook_field_attach_pre_insert($obj_type, $object, &$skip_fields) {
* @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 @@ function hook_field_attach_delete_bundle($bundle, $instances) {
* 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 @@ function hook_field_storage_load($obj_type, $objects, $age, $skip_fields) {
* 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.
*/
diff --git a/modules/field/field.attach.inc b/modules/field/field.attach.inc
index 9326638c0..3f37fe02c 100644
--- a/modules/field/field.attach.inc
+++ b/modules/field/field.attach.inc
@@ -151,34 +151,50 @@ define('FIELD_STORAGE_INSERT', 'insert');
* - Otherwise NULL.
* @param $options
* An associative array of additional options, with the following keys:
- * - 'field_name'
- * The name of the field whose operation should be invoked. By default, the
- * operation is invoked on all the fields in the object's bundle.
- * - 'default'
- * A boolean value, specifying which implementation of the operation should
- * be invoked.
+ * - '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.
* - if FALSE (default), the field types implementation of the operation
* will be invoked (hook_field_[op])
* - If TRUE, the default field implementation of the field operation
* 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 ((!isset($options['field_id']) || $options['field_id'] == $instance['field_id']) && (!isset($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,18 +247,24 @@ function _field_invoke($op, $obj_type, $object, &$a = NULL, &$b = NULL, $options
* Currently always NULL.
* @param $options
* An associative array of additional options, with the following keys:
- * - 'field_name'
- * The name of the field whose operation should be invoked. By default, the
- * operation is invoked on all the fields in the objects' bundles.
- * - 'default'
- * A boolean value, specifying which implementation of the operation should
- * be invoked.
+ * - '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.
* - if FALSE (default), the field types implementation of the operation
* will be invoked (hook_field_[op])
* - If TRUE, the default field implementation of the field operation
* 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.
*/
@@ -250,6 +272,7 @@ function _field_invoke_multiple($op, $obj_type, $objects, &$a = NULL, &$b = NULL
// Merge default options.
$default_options = array(
'default' => FALSE,
+ 'deleted' => FALSE,
);
$options += $default_options;
@@ -261,22 +284,37 @@ function _field_invoke_multiple($op, $obj_type, $objects, &$a = NULL, &$b = NULL
// 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.
@@ -284,10 +322,11 @@ function _field_invoke_multiple($op, $obj_type, $objects, &$a = NULL, &$b = NULL
}
// 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.
@@ -305,9 +344,9 @@ function _field_invoke_multiple($op, $obj_type, $objects, &$a = NULL, &$b = NULL
// 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];
}
}
}
@@ -442,10 +481,13 @@ function field_attach_form($obj_type, $object, &$form, &$form_state) {
* 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.
@@ -453,11 +495,18 @@ function field_attach_form($obj_type, $object, &$form, &$form_state) {
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;
@@ -549,10 +598,10 @@ function field_attach_load($obj_type, $objects, $age = FIELD_LOAD_CURRENT, $opti
* 'revision' keys filled.
* @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_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.
* @returns
* On return, the objects in $objects are modified by having the
* appropriate set of fields added.
@@ -805,8 +854,8 @@ function field_attach_delete_revision($obj_type, $object) {
* 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).
@@ -819,6 +868,8 @@ function field_attach_delete_revision($obj_type, $object) {
* - '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:
@@ -871,7 +922,7 @@ function field_attach_delete_revision($obj_type, $object) {
* 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;
}
@@ -881,7 +932,7 @@ function field_attach_query($field_name, $conditions, $count, &$cursor = NULL, $
$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;
@@ -890,7 +941,7 @@ function field_attach_query($field_name, $conditions, $count, &$cursor = NULL, $
// 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;
@@ -901,8 +952,8 @@ function field_attach_query($field_name, $conditions, $count, &$cursor = NULL, $
*
* 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
@@ -919,8 +970,8 @@ function field_attach_query($field_name, $conditions, $count, &$cursor = NULL, $
* @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);
}
/**
diff --git a/modules/field/field.crud.inc b/modules/field/field.crud.inc
index 2b71f52f9..dc3da56fc 100644
--- a/modules/field/field.crud.inc
+++ b/modules/field/field.crud.inc
@@ -271,6 +271,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);
@@ -443,7 +446,7 @@ function field_create_instance($instance) {
// TODO : do we want specific messages when clashing with a disabled or inactive instance ?
$prior_instance = field_read_instance($instance['field_name'], $instance['bundle'], array('include_inactive' => TRUE));
if (!empty($prior_instance)) {
- throw new FieldException('Attempt to create a field instance which already exists.');
+ throw new FieldException(t('Attempt to create a field instance %field_name,%bundle which already exists.', array('%field_name' => $instance['field_name'], '%bundle' => $instance['bundle'])));
}
_field_write_instance($instance);
@@ -688,3 +691,217 @@ function field_delete_instance($field_name, $bundle) {
/**
* @} 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($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_batch().
+ *
+ * @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_batch().
+ *
+ * @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".
+ */
+
diff --git a/modules/field/field.info.inc b/modules/field/field.info.inc
index 0b2dcbb8e..5507f548d 100644
--- a/modules/field/field.info.inc
+++ b/modules/field/field.info.inc
@@ -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 @@ function _field_info_collate_types($reset = FALSE) {
* @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 @@ function _field_info_collate_fields($reset = FALSE) {
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 @@ 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
@@ -455,6 +485,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.
*
diff --git a/modules/field/field.module b/modules/field/field.module
index 18a77b412..2be05058d 100644
--- a/modules/field/field.module
+++ b/modules/field/field.module
@@ -55,6 +55,10 @@ require(DRUPAL_ROOT . '/modules/field/field.attach.inc');
* 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 @@ function field_theme() {
}
/**
+ * 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_batch($limit);
+}
+
+/**
* Implement hook_modules_installed().
*/
function field_modules_installed($modules) {
@@ -337,8 +351,7 @@ function field_cache_clear($rebuild_schema = FALSE) {
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
diff --git a/modules/field/field.test b/modules/field/field.test
index 7b61a4410..e2a4674d8 100644
--- a/modules/field/field.test
+++ b/modules/field/field.test
@@ -135,7 +135,8 @@ class FieldAttachTestCase extends DrupalWebTestCase {
for ($i = 1; $i <= 3; $i++) {
$field_names[$i] = 'field_' . $i;
$field = array('field_name' => $field_names[$i], 'type' => 'test_field');
- field_create_field($field);
+ $field = field_create_field($field);
+ $field_ids[$i] = $field['id'];
foreach ($field_bundles_map[$i] as $bundle) {
$instance = array(
'field_name' => $field_names[$i],
@@ -176,7 +177,7 @@ class FieldAttachTestCase extends DrupalWebTestCase {
// Check that the single-field load option works.
$entity = field_test_create_stub_entity(1, 1, $bundles[1]);
- field_attach_load($entity_type, array(1 => $entity), FIELD_LOAD_CURRENT, array('field_name' => $field_names[1]));
+ field_attach_load($entity_type, array(1 => $entity), FIELD_LOAD_CURRENT, array('field_id' => $field_ids[1]));
$this->assertEqual($entity->{$field_names[1]}[0]['value'], $values[1][$field_names[1]], t('Entity %index: expected value was found.', array('%index' => 1)));
$this->assertEqual($entity->{$field_names[1]}[0]['additional_key'], 'additional_value', t('Entity %index: extra information was found', array('%index' => 1)));
$this->assert(!isset($entity->{$field_names[2]}), t('Entity %index: field %field_name is not loaded.', array('%index' => 2, '%field_name' => $field_names[2])));
@@ -305,7 +306,7 @@ class FieldAttachTestCase extends DrupalWebTestCase {
// Query on the object's values.
for ($delta = 0; $delta < $cardinality; $delta++) {
$conditions = array(array('value', $values[$delta]));
- $result = field_attach_query($this->field_name, $conditions, FIELD_QUERY_NO_LIMIT);
+ $result = field_attach_query($this->field_id, $conditions, FIELD_QUERY_NO_LIMIT);
$this->assertTrue(isset($result[$entity_types[1]][1]), t('Query on value %delta returns the object', array('%delta' => $delta)));
}
@@ -314,31 +315,31 @@ class FieldAttachTestCase extends DrupalWebTestCase {
$different_value = mt_rand(1, 127);
} while (in_array($different_value, $values));
$conditions = array(array('value', $different_value));
- $result = field_attach_query($this->field_name, $conditions, FIELD_QUERY_NO_LIMIT);
+ $result = field_attach_query($this->field_id, $conditions, FIELD_QUERY_NO_LIMIT);
$this->assertFalse(isset($result[$entity_types[1]][1]), t("Query on a value that is not in the object doesn't return the object"));
// Query on the value shared by both objects, and discriminate using
// additional conditions.
$conditions = array(array('value', $common_value));
- $result = field_attach_query($this->field_name, $conditions, FIELD_QUERY_NO_LIMIT);
+ $result = field_attach_query($this->field_id, $conditions, FIELD_QUERY_NO_LIMIT);
$this->assertTrue(isset($result[$entity_types[1]][1]) && isset($result[$entity_types[2]][2]), t('Query on a value common to both objects returns both objects'));
$conditions = array(array('type', $entity_types[1]), array('value', $common_value));
- $result = field_attach_query($this->field_name, $conditions, FIELD_QUERY_NO_LIMIT);
+ $result = field_attach_query($this->field_id, $conditions, FIELD_QUERY_NO_LIMIT);
$this->assertTrue(isset($result[$entity_types[1]][1]) && !isset($result[$entity_types[2]][2]), t("Query on a value common to both objects and a 'type' condition only returns the relevant object"));
$conditions = array(array('bundle', $entities[1]->fttype), array('value', $common_value));
- $result = field_attach_query($this->field_name, $conditions, FIELD_QUERY_NO_LIMIT);
+ $result = field_attach_query($this->field_id, $conditions, FIELD_QUERY_NO_LIMIT);
$this->assertTrue(isset($result[$entity_types[1]][1]) && !isset($result[$entity_types[2]][2]), t("Query on a value common to both objects and a 'bundle' condition only returns the relevant object"));
$conditions = array(array('entity_id', $entities[1]->ftid), array('value', $common_value));
- $result = field_attach_query($this->field_name, $conditions, FIELD_QUERY_NO_LIMIT);
+ $result = field_attach_query($this->field_id, $conditions, FIELD_QUERY_NO_LIMIT);
$this->assertTrue(isset($result[$entity_types[1]][1]) && !isset($result[$entity_types[2]][2]), t("Query on a value common to both objects and an 'entity_id' condition only returns the relevant object"));
// Test result format.
$conditions = array(array('value', $values[0]));
- $result = field_attach_query($this->field_name, $conditions, FIELD_QUERY_NO_LIMIT);
+ $result = field_attach_query($this->field_id, $conditions, FIELD_QUERY_NO_LIMIT);
$expected = array(
$entity_types[1] => array(
$entities[1]->ftid => field_test_create_stub_entity($entities[1]->ftid, $entities[1]->ftvid),
@@ -371,7 +372,7 @@ class FieldAttachTestCase extends DrupalWebTestCase {
// back the right ones.
$cursor = 0;
foreach (array(1 => 1, 3 => 3, 5 => 5, 8 => 8, 13 => 3) as $count => $expect) {
- $found = field_attach_query($this->field_name, array(array('bundle', 'offset_bundle')), $count, $cursor);
+ $found = field_attach_query($this->field_id, array(array('bundle', 'offset_bundle')), $count, $cursor);
if (isset($found['test_entity'])) {
$this->assertEqual(count($found['test_entity']), $expect, t('Requested @count, expected @expect, got @found, cursor @cursor', array('@count' => $count, '@expect' => $expect, '@found' => count($found['test_entity']), '@cursor' => $cursor)));
foreach ($found['test_entity'] as $id => $entity) {
@@ -414,7 +415,7 @@ class FieldAttachTestCase extends DrupalWebTestCase {
// Query on the object's values.
for ($delta = 0; $delta < $cardinality; $delta++) {
$conditions = array(array('value', $values[$delta]));
- $result = field_attach_query_revisions($this->field_name, $conditions, FIELD_QUERY_NO_LIMIT);
+ $result = field_attach_query_revisions($this->field_id, $conditions, FIELD_QUERY_NO_LIMIT);
$this->assertTrue(isset($result[$entity_type][1]), t('Query on value %delta returns the object', array('%delta' => $delta)));
}
@@ -423,23 +424,23 @@ class FieldAttachTestCase extends DrupalWebTestCase {
$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 @@ class FieldAttachTestCase extends DrupalWebTestCase {
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));
@@ -1782,3 +1789,245 @@ class FieldInstanceCrudTestCase extends DrupalWebTestCase {
$this->assertTrue(!empty($another_instance) && empty($another_instance['deleted']), t('A non-deleted field instance is not marked for deletion.'));
}
}
+
+/**
+ * Unit test class for field bulk delete and batch purge functionality.
+ */
+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;
+ }
+
+ /**
+ * Convenience function for Field API tests.
+ *
+ * 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');
+
+ // 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's 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");
+ }
+ }
+
+ /**
+ * Verify that field data items and instances are purged when an
+ * instance is deleted.
+ */
+ 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($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');
+ 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($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');
+ }
+
+ /**
+ * Verify that fields are preserved and purged correctly as multiple
+ * instances are deleted and purged.
+ */
+ 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_batch(10);
+
+ // Purge again to purge the instance.
+ field_purge_batch(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_batch(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.');
+ }
+}
diff --git a/modules/field/modules/field_sql_storage/field_sql_storage.module b/modules/field/modules/field_sql_storage/field_sql_storage.module
index df2fa40bb..db4c5fcdd 100644
--- a/modules/field/modules/field_sql_storage/field_sql_storage.module
+++ b/modules/field/modules/field_sql_storage/field_sql_storage.module
@@ -211,26 +211,39 @@ function field_sql_storage_field_storage_load($obj_type, $objects, $age, $skip_f
$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 @@ function field_sql_storage_field_storage_write($obj_type, $object, $op, $skip_fi
$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 @@ function field_sql_storage_field_storage_write($obj_type, $object, $op, $skip_fi
/**
* Implement hook_field_storage_delete().
*
- * This function actually deletes the data from the database.
+ * This function deletes data for all fields for an object from the database.
*/
function field_sql_storage_field_storage_delete($obj_type, $object) {
list($id, $vid, $bundle) = field_attach_extract_ids($obj_type, $object);
@@ -337,39 +350,54 @@ function field_sql_storage_field_storage_delete($obj_type, $object) {
$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 ensure objects arrive in a consistent order for the
+ // range() operation to work.
->orderBy('t.etid')
->orderBy('t.entity_id');
@@ -400,6 +428,15 @@ function field_sql_storage_field_storage_query($field_name, $conditions, $count,
$column = _field_sql_storage_columnname($field_name, $column);
}
$query->condition($column, $value, $operator);
+
+ if ($column == 'deleted') {
+ $deleted = $value;
+ }
+ }
+
+ // Exclude deleted data unless we have a condition on it.
+ if (!isset($deleted)) {
+ $query->condition('deleted', 0);
}
// Initialize results array
@@ -420,7 +457,6 @@ function field_sql_storage_field_storage_query($field_name, $conditions, $count,
foreach ($results as $row) {
$row_count++;
$cursor++;
-
// If querying all revisions and the entity type has revisions, we need
// to key the results by revision_ids.
$entity_type = field_info_fieldable_types($row->type);
@@ -503,4 +539,19 @@ function field_sql_storage_field_storage_rename_bundle($bundle_old, $bundle_new)
->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);
+}
+
diff --git a/modules/simpletest/tests/field_test.module b/modules/simpletest/tests/field_test.module
index 94efd014c..524a74ac9 100644
--- a/modules/simpletest/tests/field_test.module
+++ b/modules/simpletest/tests/field_test.module
@@ -640,4 +640,28 @@ function field_test_memorize($key = NULL, $value = NULL) {
function field_test_field_create_field($field) {
$args = func_get_args();
field_test_memorize(__FUNCTION__, $args);
-} \ No newline at end of file
+}
+
+/**
+ * Memorize calls to hook_field_insert().
+ */
+function field_test_field_insert($obj_type, $object, $field, $instance, $items) {
+ $args = func_get_args();
+ field_test_memorize(__FUNCTION__, $args);
+}
+
+/**
+ * Memorize calls to hook_field_update().
+ */
+function field_test_field_update($obj_type, $object, $field, $instance, $items) {
+ $args = func_get_args();
+ field_test_memorize(__FUNCTION__, $args);
+}
+
+/**
+ * Memorize calls to hook_field_delete().
+ */
+function field_test_field_delete($obj_type, $object, $field, $instance, $items) {
+ $args = func_get_args();
+ field_test_memorize(__FUNCTION__, $args);
+}
diff --git a/modules/taxonomy/taxonomy.module b/modules/taxonomy/taxonomy.module
index 578293eba..ba349e623 100644
--- a/modules/taxonomy/taxonomy.module
+++ b/modules/taxonomy/taxonomy.module
@@ -2015,7 +2015,7 @@ function _taxonomy_clean_field_cache($term) {
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";