? FIELD-TO-COLUMN-PARTIAL.patch ? Makefile ? constants.pl ? d6-50-nodes.sql.gz ? d7-50-nodes-new.sql.gz ? d7-50-nodes.sql.gz ? head.kpf ? patches ? modules/field/field.cron.inc ? modules/field/field.delete.inc ? modules/field/modules/combo ? scripts/OLD-generate-autoload.pl ? scripts/generate-autoload.pl ? sites/all/cck ? sites/all/modules/devel ? sites/all/modules/pbs ? sites/all/modules/taint ? sites/default/files ? sites/default/settings.php Index: modules/field/field.attach.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.attach.inc,v retrieving revision 1.22 diff -u -F^[fc] -r1.22 field.attach.inc --- modules/field/field.attach.inc 7 Jun 2009 00:00:57 -0000 1.22 +++ modules/field/field.attach.inc 16 Jun 2009 03:39:39 -0000 @@ -264,17 +264,27 @@ function _field_invoke_multiple($op, $ob $grouped_items = array(); $return = array(); + $deleted = isset($options['deleted']) ? $options['deleted'] : 0; + // Go through the objects and collect the fields on which the hook should be // invoked. foreach ($objects as $object) { list($id, $vid, $bundle) = field_attach_extract_ids($obj_type, $object); - foreach (field_info_instances($bundle) as $instance) { + + if ($deleted) { + $instances = field_read_field(array('bundle' => $bundle, array('include_deleted' => $deleted))); + } + else { + $instances = field_info_instances($bundle); + } + + foreach ($instances as $instance) { $field_name = $instance['field_name']; // When in 'single field' mode, only act on the specified field. - if (empty($options['field_name']) || $options['field_name'] == $field_name) { + if (empty($options['field_id']) || $options['field_id'] == $instance['field_id']) { // 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); + $fields[$field_name] = field_info_field_by_id($instance['field_id']); } // Group the corresponding instances and objects. $grouped_instances[$field_name][$id] = $instance; @@ -292,7 +302,7 @@ function _field_invoke_multiple($op, $ob foreach ($fields as $field_name => $field) { $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_name], $field, $grouped_instances[$field_name], $grouped_items[$field_name], $options, $a, $b); if (isset($results)) { // Collect results by object. // For hooks with array results, we merge results together. @@ -735,8 +745,8 @@ function field_attach_delete_revision($o * 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). @@ -749,6 +759,8 @@ function field_attach_delete_revision($o * - '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: @@ -771,6 +783,13 @@ function field_attach_delete_revision($o * array('value', 12, '>'), * ); * @endcode + * @param $limit + * The maximum number of objects that is requested. This is only a + * hint to the storage engine(s); callers should be prepared to + * handle more results. Specify FIELD_QUERY_NO_LIMIT to retrieve + * all available objects. This parameter has no default value so + * callers must make an explicit choice to potentially retrieve an + * enormous result set. * @param $result_format * - FIELD_QUERY_RETURN_IDS (default): return the ids of the objects matching the * conditions. @@ -788,7 +807,7 @@ function field_attach_delete_revision($o * values depend on the $result_format parameter: * - FIELD_QUERY_RETURN_IDS: the object id. * - FIELD_QUERY_RETURN_VALUES: a pseudo-object with values for the - * $field_name field. This only includes values matching the conditions, + * $field_id field. This only includes values matching the conditions, * and thus might not contain all actual values and actual delta sequence * (although values oprder is preserved). * The pseudo-objects only include properties that the Field API knows @@ -797,13 +816,13 @@ function field_attach_delete_revision($o * Throws a FieldQueryException if the field's storage doesn't support the * specified conditions. */ -function field_attach_query($field_name, $conditions, $result_format = FIELD_QUERY_RETURN_IDS, $age = FIELD_LOAD_CURRENT) { +function field_attach_query($field_id, $conditions, $limit, $result_format = FIELD_QUERY_RETURN_IDS, $age = FIELD_LOAD_CURRENT) { // Give a chance to 3rd party modules that bypass the storage engine to // handle the query. $skip_field = FALSE; foreach (module_implements('field_attach_pre_query') as $module) { $function = $module . '_field_attach_pre_query'; - $results = $function($field_name, $conditions, $result_format, $age, $skip_field); + $results = $function($field_id, $conditions, $result_format, $age, $skip_field); // Stop as soon as a module claims it handled the query. if ($skip_field) { break; @@ -811,7 +830,7 @@ function field_attach_query($field_name, } // If the request hasn't been handled, let the storage engine handle it. if (!$skip_field) { - $results = module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_query', $field_name, $conditions, $result_format, $age); + $results = module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_query', $field_id, $conditions, $limit, $result_format, $age); } if ($result_format == FIELD_QUERY_RETURN_VALUES) { @@ -819,7 +838,7 @@ function field_attach_query($field_name, if ($age == FIELD_LOAD_CURRENT) { // Invoke hook_field_load(). $b = NULL; - _field_invoke_multiple('load', $obj_type, $pseudo_objects, $age, $b, array('field_name' => $field_name)); + _field_invoke_multiple('load', $obj_type, $pseudo_objects, $age, $b, array('field_id' => $field_id)); // Invoke hook_field_attach_load(). foreach (module_implements('field_attach_load') as $module) { @@ -838,7 +857,7 @@ function field_attach_query($field_name, // Invoke hook_field_load(). $b = NULL; - _field_invoke_multiple('load', $obj_type, $objects, $age, $b, array('field_name' => $field_name)); + _field_invoke_multiple('load', $obj_type, $objects, $age, $b, array('field_id' => $field_id)); // Invoke hook_field_attach_load(). foreach (module_implements('field_attach_load') as $module) { @@ -858,10 +877,17 @@ function field_attach_query($field_name, * * 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 $limit + * The maximum number of objects that is requested. This is only a + * hint to the storage engine(s); callers should be prepared to + * handle more results. Specify FIELD_QUERY_NO_LIMIT to retrieve + * all available objects. This parameter has no default value so + * callers must make an explicit choice to potentially retrieve an + * enormous result set. * @param $result_format * See field_attach_query(). * Note that the FIELD_QUERY_RETURN_VALUES option might cause performance @@ -869,8 +895,8 @@ function field_attach_query($field_name, * @return * See field_attach_query(). */ -function field_attach_query_revisions($field_name, $conditions, $result_format = FIELD_QUERY_RETURN_IDS) { - return field_attach_query($field_name, $conditions, $result_format, FIELD_LOAD_REVISION); +function field_attach_query_revisions($field_id, $conditions, $limit, $result_format = FIELD_QUERY_RETURN_IDS) { + return field_attach_query($field_id, $conditions, $limit, $result_format, FIELD_LOAD_REVISION); } /** Index: modules/field/field.crud.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.crud.inc,v retrieving revision 1.15 diff -u -F^[fc] -r1.15 field.crud.inc --- modules/field/field.crud.inc 12 Jun 2009 08:39:36 -0000 1.15 +++ modules/field/field.crud.inc 16 Jun 2009 03:39:39 -0000 @@ -268,6 +268,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); @@ -439,7 +442,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); @@ -689,3 +692,239 @@ function field_delete_instance($field_na /** * @} 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 field. + * + * This function will purge up to a specified maximum number of field + * data records and then return. If a deleted field instance with no + * remaining data records is found, the instance itself will be + * purged. If a deleted field with no remaining field instances is + * found, the field itself will be purged. + * + * @param $batch_size + * The maximum number of field data records to purge before returning. + */ +function field_purge($batch_size) { + // Retrieve all deleted field instances. We cannot use + // field_info_instances() because that function does not return + // deleted instances. + // + // TODO: Perhaps the include_deleted argument to this + // function should be replaced by inspecting the query parameters. + $instances = field_read_instances(array('deleted' => 1), array('include_deleted' => 1)); + + foreach ($instances as $instance) { + // Retrieve the field. We cannot use field_info_field() because + // the field might be deleted. + // + // TODO: See above comment. + $fields = field_read_fields(array('id' => $instance['field_id']), array('include_deleted' => 1)); + $field = $fields[$instance['field_id']]; + + // Retrieve some pseudo-objects along with field data for the + // instance. + $obj_types = field_attach_query($instance['field_id'], array(array('bundle', $instance['bundle']), array('deleted', 1)), $batch_size, FIELD_QUERY_RETURN_VALUES); + + if (count($obj_types) > 0) { + // Field data for the instance still exists. + foreach ($obj_types as $obj_type => $objects) { + foreach ($objects as $id => $object) { + + // field_attach_query() may return more results than we + // asked for. If we've done our batch size, stop. + 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. + // + // TODO: See above comment. + $fields = field_read_fields(array('deleted' => 1), array('include_deleted' => 1)); + foreach ($fields as $field) { + 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. + * + * TODO: refactor with field_attach_delete()? + * + * @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']); + _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); + } + + list(, , , $cacheable) = field_attach_extract_ids($obj_type, $object); + if ($cacheable) { + cache_clear_all("field:$obj_type:$id", 'cache_field'); + } +} + +/** + * Purge a field instance record from the database. + * + * This function assumes all data for the instance has already been + * purged, and should only be called by field_purge(). + * + * @param $instance + * The instance record to purge. + */ +function field_purge_instance($instance) { + db_delete('field_config_instance') + ->condition('id', $instance['id']) + ->execute(); + + // Notify the storage engine. + module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_purge_instance', $instance); + + // Clear the cache. + field_cache_clear(); + + // Invoke external hooks after the cache is cleared for API consistency. + module_invoke_all('field_purge_instance', $instance); +} + +/** + * Purge a field record from the database. + * + * This function assumes all instances for the field has already been + * purged, and should only be called by field_purge(). + * + * @param $field + * The field record to purge. + */ +function field_purge_field($field) { + $instances = field_read_instances(array('field_id' => $field['id']), array('include_deleted' => 1)); + if (count($instances) > 0) { + throw new FieldException("Attempt to purge a field that still has instances."); + } + + db_delete('field_config') + ->condition('id', $field['id']) + ->execute(); + + // Notify the storage engine. + module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_purge_field', $field); + + // Clear the cache. + field_cache_clear(); + + // Invoke external hooks after the cache is cleared for API consistency. + module_invoke_all('field_purge_field', $field); +} + +/** + * @} End of "defgroup field_purge". + */ + Index: modules/field/field.info.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.info.inc,v retrieving revision 1.6 diff -u -F^[fc] -r1.6 field.info.inc --- modules/field/field.info.inc 5 Jun 2009 18:25:41 -0000 1.6 +++ modules/field/field.info.inc 16 Jun 2009 03:39:39 -0000 @@ -179,6 +179,7 @@ function _field_info_collate_fields($res else { $info = array( 'fields' => field_read_fields(), + 'field_ids' => field_read_fields(array(), array('include_deleted' => 1)), 'instances' => array_fill_keys(array_keys(field_info_bundles()), array()), ); @@ -187,6 +188,7 @@ function _field_info_collate_fields($res foreach ($instances as $instance) { $info['instances'][$instance['bundle']][$instance['field_name']] = $instance; $info['fields'][$instance['field_name']]['bundles'][] = $instance['bundle']; + $info['field_ids'][$instance['field_id']]['bundles'][] = $instance['bundle']; } cache_set('field_info_fields', $info, 'cache_field'); @@ -394,7 +396,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 @@ -408,6 +411,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. * Index: modules/field/field.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.module,v retrieving revision 1.13 diff -u -F^[fc] -r1.13 field.module --- modules/field/field.module 6 Jun 2009 16:17:30 -0000 1.13 +++ modules/field/field.module 16 Jun 2009 03:39:39 -0000 @@ -55,6 +55,10 @@ * pluggable back-end storage system for actual field data. The * default implementation, field_sql_storage.module, stores field data * in the local SQL database. + + * - @link field_purge Field API bulk data deletion @endlink. Cleans + * up after bulk deletion operations such as field_delete_field() + * and field_delete_instance(). */ /** @@ -90,8 +94,14 @@ /** * @name Field query flags * @{ - * Flags for use in the $result_format parameter in field_attach_query(). + * Flags for field_attach_query(). + */ + +/** + * Limit argument for field_attach_query() to request all available + * objects instead of a limited number. */ +define('FIELD_QUERY_NO_LIMIT', 'FIELD_QUERY_NO_LIMIT'); /** * Result format argument for field_attach_query(). Index: modules/field/field.test =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.test,v retrieving revision 1.26 diff -u -F^[fc] -r1.26 field.test --- modules/field/field.test 12 Jun 2009 08:39:36 -0000 1.26 +++ modules/field/field.test 16 Jun 2009 03:39:39 -0000 @@ -26,7 +26,8 @@ class FieldAttachTestCase extends Drupal $this->field_name = drupal_strtolower($this->randomName() . '_field_name'); $this->field = array('field_name' => $this->field_name, 'type' => 'test_field', 'cardinality' => 4); - field_create_field($this->field); + $this->field = field_create_field($this->field); + $this->field_id = $this->field['id']; $this->instance = array( 'field_name' => $this->field_name, 'bundle' => 'test_bundle', @@ -296,7 +297,7 @@ class FieldAttachTestCase extends Drupal // 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); + $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))); } @@ -305,31 +306,31 @@ class FieldAttachTestCase extends Drupal $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); + $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); + $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); + $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); + $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); + $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 FIELD_QUERY_RETURN_IDS result format. $conditions = array(array('value', $values[0])); - $result = field_attach_query($this->field_name, $conditions); + $result = field_attach_query($this->field_id, $conditions, FIELD_QUERY_NO_LIMIT); $expected = array( $entity_types[1] => array( $entities[1]->ftid => $entities[1]->ftid, @@ -348,7 +349,7 @@ class FieldAttachTestCase extends Drupal // Query for one of the values in the 1st object and the value shared by // both objects. $conditions = array(array('value', array($values[0], $common_value))); - $result = field_attach_query($this->field_name, $conditions, FIELD_QUERY_RETURN_VALUES); + $result = field_attach_query($this->field_id, $conditions, FIELD_QUERY_NO_LIMIT, FIELD_QUERY_RETURN_VALUES); $expected = array( $entity_types[1] => array( $entities[1]->ftid => (object) array( @@ -402,7 +403,7 @@ class FieldAttachTestCase extends Drupal // 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); + $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))); } @@ -411,23 +412,23 @@ class FieldAttachTestCase extends Drupal $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); + $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); + $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); + $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); + $result = field_attach_query_revisions($this->field_id, $conditions, FIELD_QUERY_NO_LIMIT); $expected = array( $entity_type => array( $entities[1]->ftvid => $entities[1]->ftid, @@ -444,7 +445,7 @@ class FieldAttachTestCase extends Drupal // Query for one of the values in the 1st object and the value shared by // both objects. $conditions = array(array('value', array($values[0], $common_value))); - $result = field_attach_query_revisions($this->field_name, $conditions, FIELD_QUERY_RETURN_VALUES); + $result = field_attach_query_revisions($this->field_id, $conditions, FIELD_QUERY_NO_LIMIT, FIELD_QUERY_RETURN_VALUES); $expected = array( $entity_type => array( $entities[1]->ftvid => (object) array( @@ -1660,3 +1661,179 @@ class FieldInstanceTestCase extends Drup $this->assertTrue(!empty($another_instance) && empty($another_instance['deleted']), t('A non-deleted field instance is not marked for deletion.')); } } + +class FieldBulkDeleteTestCase extends DrupalWebTestCase { + protected $field; + + public static function getInfo() { + return array( + 'name' => t('Field bulk delete tests'), + 'description' => t("Bulk delete fields and instances, and clean up afterwards."), + 'group' => t('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; + } + + 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() { + // TODO: Create a second field that we do not delete so we can + // test that its data stays around. + + // Create two bundles + $this->bundles = array('bb_1' => 'bb_1', 'bb_2' => 'bb_2'); + variable_set('field_test_bundles', $this->bundles); + + parent::setUp('field_test'); + + // 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++; + } + } + } + + function tearDown() { + parent::tearDown(); + $this->fields = array(); + $this->instances = array(); + variable_del('field_test_bundles'); + } + + // TODO: This tests how field_attach_query() interacts with + // field_delete_instance(). Move to FieldCrudTestCase? + 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 + $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 deleted objects of this bundle, and their values + // are correct. + $found = field_attach_query($field['id'], array(array('bundle', $bundle), array('deleted', 1)), FIELD_QUERY_NO_LIMIT, FIELD_QUERY_RETURN_VALUES); + $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']}, "Deleted object $id loaded correctly"); + } + } + + function testPurgeInstance() { + field_test_memorize(); + + $bundle = reset($this->bundles); + $field = reset($this->fields); + + // Delete the instance + field_delete_instance($field['field_name'], $bundle); + + // No field hooks were called. + $mem = field_test_memorize(); + $this->assertEqual(count($mem), 0, 'No field hooks were called'); + + $batch_size = 2; + for ($count = 8; $count >= 0; $count -= 2) { + // Purge two objects + field_purge($batch_size); + + // There are $count deleted objects left. + $found = field_attach_query($field['id'], array(array('bundle', $bundle), array('deleted', 1)), FIELD_QUERY_NO_LIMIT, FIELD_QUERY_RETURN_VALUES); + $this->assertEqual($count ? count($found['test_entity']) : count($found), $count, 'Correct number of objects found after purging 2'); + } + + // hook_field_delete() was called on a pseudo-object for each + // object. Each pseudo object has a $field property that matches + // the original object, but no others. + $mem = field_test_memorize(); + $this->assertEqual(count($mem['field_test_field_delete']), 10, 'hook_field_delete was called for the right number of objects'); + $stubs = $this->_generateStubObjects($this->entity_type, $this->entities, $field['field_name']); + $count = count($stubs); + foreach ($mem['field_test_field_delete'] as $args) { + $obj = $args[1]; + $this->assertEqual($stubs[$obj->ftid], $obj, "hook_field_delete() called with the correct stub {$obj->ftid}"); + unset($stubs[$obj->ftid]); + } + $this->assertEqual(count($stubs), $count-10, 'hook_field_delete was called with each object once'); + + // The instance still exists, deleted. + $instances = field_read_instances(array('field_id' => $field['id'], 'deleted' => 1), array('include_deleted' => 1, 'include_inactive' => 1)); + $this->assertEqual(count($instances), 1, 'There is one deleted instance'); + + // Purge the instance + field_purge($batch_size); + + // The instance is gone. + $instances = field_read_instances(array('field_id' => $field['id'], 'deleted' => 1), array('include_deleted' => 1, 'include_inactive' => 1)); + $this->assertEqual(count($instances), 0, 'The instance is gone'); + + // The field still exists, not deleted. + $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'); + } +} Index: modules/field/modules/field_sql_storage/field_sql_storage.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/field_sql_storage/field_sql_storage.module,v retrieving revision 1.14 diff -u -F^[fc] -r1.14 field_sql_storage.module --- modules/field/modules/field_sql_storage/field_sql_storage.module 15 Jun 2009 10:10:46 -0000 1.14 +++ modules/field/modules/field_sql_storage/field_sql_storage.module 16 Jun 2009 03:39:39 -0000 @@ -328,7 +328,7 @@ function field_sql_storage_field_storage /** * 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); @@ -336,30 +336,43 @@ function field_sql_storage_field_storage $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, $result_format, $age) { +function field_sql_storage_field_storage_query($field_id, $conditions, $limit, $result_format, $age) { $load_values = $result_format == FIELD_QUERY_RETURN_VALUES; $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']); @@ -368,7 +381,6 @@ function field_sql_storage_field_storage $query->join('field_config_entity_type', 'e', 't.etid = e.etid'); $query ->fields('e', array('type')) - ->condition('deleted', 0) ->orderBy('delta'); // Add fields, depending on the return format. if ($load_values) { @@ -404,6 +416,19 @@ function field_sql_storage_field_storage $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); + } + + if ($limit != FIELD_QUERY_NO_LIMIT) { + $query->range(0, $limit); } $results = $query->execute(); Index: modules/node/node.install =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.install,v retrieving revision 1.23 diff -u -F^[fc] -r1.23 node.install --- modules/node/node.install 12 Jun 2009 08:39:38 -0000 1.23 +++ modules/node/node.install 16 Jun 2009 03:39:39 -0000 @@ -398,7 +398,7 @@ function node_update_7004() { // Map old preview setting to new values order. $original_preview ? $original_preview = 2 : $original_preview = 1; - $node_types_clear(); + node_type_clear(); $type_list = node_type_get_types(); // Apply original settings to all types. Index: modules/simpletest/tests/field_test.module =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/field_test.module,v retrieving revision 1.9 diff -u -F^[fc] -r1.9 field_test.module --- modules/simpletest/tests/field_test.module 27 May 2009 18:34:00 -0000 1.9 +++ modules/simpletest/tests/field_test.module 16 Jun 2009 03:39:39 -0000 @@ -365,6 +365,43 @@ function field_test_field_schema($field) ); } +function field_test_memorize($tag = NULL, $args = NULL) { + $memorize =& drupal_static(__FUNCTION__, NULL); + + if (is_null($tag)) { + $ret = $memorize; + $memorize = array(); + return $ret; + } + if (is_array($memorize)) { + $memorize[$tag][] = $args; + } +} + +/** + * 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); +} + /** * Implement hook_field_validate(). *