Commit 9c0e6e92 authored by Dries's avatar Dries

- Patch #367753 by yched, bjaspan: add support for bulk deletion to Fields API.

parent 9a8cfc2f
......@@ -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.
*/
......
This diff is collapsed.
......@@ -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".
*/
......@@ -16,6 +16,18 @@
* and settings defined by or with the Field API.
*/
/**
* 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.
*
......@@ -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
......@@ -454,6 +484,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.
......
......@@ -55,6 +55,10 @@
* pluggable back-end storage system for actual field data. The
* default implementation, field_sql_storage.module, stores field data
* in the local SQL database.
* - @link field_purge Field API bulk data deletion @endlink. Cleans
* up after bulk deletion operations such as field_delete_field()
* and field_delete_instance().
*/
/**
......@@ -177,6 +181,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().
*/
......@@ -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
......
This diff is collapsed.
......@@ -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