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.
*/
......
......@@ -151,34 +151,50 @@ class FieldQueryException extends FieldException {}
* - 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);
}
/**
......
......@@ -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) {