diff options
author | Dries Buytaert <dries@buytaert.net> | 2009-08-11 14:59:40 +0000 |
---|---|---|
committer | Dries Buytaert <dries@buytaert.net> | 2009-08-11 14:59:40 +0000 |
commit | 9c0e6e92426a061f78e6dfe685c7c37c7f72bc62 (patch) | |
tree | 77be265c750278d74f0a822f934ee37631848fd0 /modules | |
parent | 9a8cfc2fd10bd5d66ec0b73824be90d328f97781 (diff) | |
download | brdo-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.php | 16 | ||||
-rw-r--r-- | modules/field/field.attach.inc | 143 | ||||
-rw-r--r-- | modules/field/field.crud.inc | 219 | ||||
-rw-r--r-- | modules/field/field.info.inc | 98 | ||||
-rw-r--r-- | modules/field/field.module | 17 | ||||
-rw-r--r-- | modules/field/field.test | 279 | ||||
-rw-r--r-- | modules/field/modules/field_sql_storage/field_sql_storage.module | 101 | ||||
-rw-r--r-- | modules/simpletest/tests/field_test.module | 26 | ||||
-rw-r--r-- | modules/taxonomy/taxonomy.module | 2 |
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"; |