? MISC-FIELD-CHANGES-REAPPLY.patch ? Makefile ? d6-50-nodes.sql.gz ? d7-50-nodes-new.sql.gz ? d7-50-nodes.sql.gz ? head.kpf ? patches ? modules/field/modules/options/options.test ? scripts/OLD-generate-autoload.pl ? scripts/generate-autoload.pl ? sites/all/cck ? sites/all/modules/color ? sites/all/modules/combofield ? sites/all/modules/devel ? sites/all/modules/pbs ? sites/all/modules/taint ? sites/default/files ? sites/default/private ? sites/default/settings.php Index: modules/field/field.api.php =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.api.php,v retrieving revision 1.34 diff -u -F^[fc] -r1.34 field.api.php --- modules/field/field.api.php 10 Sep 2009 06:31:38 -0000 1.34 +++ modules/field/field.api.php 20 Sep 2009 13:17:58 -0000 @@ -1274,6 +1274,34 @@ function hook_field_create_instance($ins } /** + * Forbid a field update from occuring. + * + * @param $field + * The field as it will be post-update. + * @param $prior_field + * The field as it is pre-update. + * @param $has_data + * Whether any data already exists for this field. + * @return + * Throws a FieldException to prevent the update from occuring. + */ +function hook_field_update_field_forbid($field, $prior_field, $has_data) { + // A 'list' field stores integer keys mapped to display values. If + // the new field will have fewer values, and any data exists for the + // abandonded keys, the field will have no way to display them. So, + // forbid such an update. + if ($has_data && count($field['settings']['allowed_values']) < count($prior_field['settings']['allowed_values'])) { + // Identify the keys that will be lost. + $lost_keys = array_diff(array_keys($field['settings']['allowed_values']), array_keys($prior_field['settings']['allowed_values'])); + // If any data exist for those keys, forbid the update. + $count = field_attach_query($prior_field['id'], array('value', $lost_keys, 'IN'), 1); + if ($count > 0) { + throw new FieldException("Cannot update a list field not to include keys with existing data"); + } + } +} + +/** * Act on a field being deleted. * * This hook is invoked just after field is deleted. Index: modules/field/field.attach.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.attach.inc,v retrieving revision 1.46 diff -u -F^[fc] -r1.46 field.attach.inc --- modules/field/field.attach.inc 10 Sep 2009 22:31:58 -0000 1.46 +++ modules/field/field.attach.inc 20 Sep 2009 13:17:58 -0000 @@ -1012,6 +1012,19 @@ function field_attach_query_revisions($f } /** + * Determine whether a field has any data. + * + * @param $field + * A field structure. + * @return + * TRUE if the field has data for any object; FALSE otherwise. + */ +function field_attach_field_has_data($field) { + $results = field_attach_query($field['id'], array(), 1); + return !empty($results); +} + +/** * Generate and return a structured content array tree suitable for * drupal_render() for all of the fields on an object. The format of * each field's rendered content depends on the display formatter and Index: modules/field/field.crud.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.crud.inc,v retrieving revision 1.31 diff -u -F^[fc] -r1.31 field.crud.inc --- modules/field/field.crud.inc 11 Sep 2009 03:42:34 -0000 1.31 +++ modules/field/field.crud.inc 20 Sep 2009 13:17:58 -0000 @@ -302,6 +302,87 @@ function field_create_field($field) { return $field; } +/* + * Update a field. + * + * Any update which changes the field's schema is forbidden if any + * data for the field already exists. Also, any module may forbid an + * update even if the update does not change the field's schema; this + * may be uesful if the update would change existing data's semantics + * without changing the schema, or if there are external dependencies + * on field settings that cannot be updated. + * + * @param $field + * A complete field structure. The field $field['field_name'] must + * exist and will be updated to this structure. Any properties of + * $field that are missing will be left unchanged. + * @throw + * FieldException + * @see field_create_field() + */ +function field_update_field($field) { + // Check that the specified field exists. + $prior_field = field_read_field($field['field_name']); + if (empty($prior_field)) { + throw new FieldException("Attempt to update a nonexistent field."); + } + + // Use the prior field values for anything not specifically set by the new + // field to be sure that all values are set. + $field += $prior_field; + $field['settings'] += $prior_field['settings']; + + // Field type cannot be changed. + if ($field['type'] != $prior_field['type']) { + throw new FieldException("Cannot change an existing field's type."); + } + + // Collect the new storage information, since what is in + // $prior_field may no longer be right. + $schema = (array) module_invoke($field['module'], 'field_schema', $field); + $schema += array('columns' => array(), 'indexes' => array()); + // 'columns' are hardcoded in the field type. + $field['columns'] = $schema['columns']; + // 'indexes' can be both hardcoded in the field type, and specified in the + // incoming $field definition. + $field += array( + 'indexes' => array(), + ); + $field['indexes'] += $schema['indexes']; + + $has_data = field_attach_field_has_data($field); + + // See if any module forbids the update by throwing an exception. + foreach (module_implements('field_update_forbid') as $module) { + $function = $module . '_field_update_forbid'; + $function($field, $prior_field, $has_data); + } + + // Tell the storage engine to update the field. Do this before + // saving the new definition since it still might fail. + module_invoke(variable_get('field_storage_module', 'field_sql_storage'), 'field_storage_update_field', $field, $prior_field, $has_data); + + // Save the new field definition. TODO: refactor with + // field_create_field. + + // The serialized 'data' column contains everything from $field that does not + // have its own column and is not automatically populated when the field is + // read. + $data = $field; + unset($data['columns'], $data['field_name'], $data['type'], $data['locked'], $data['module'], $data['cardinality'], $data['active'], $data['deleted']); + $field['data'] = $data; + + // Store the field and create the id. + $primary_key = array('id'); + drupal_write_record('field_config', $field, $primary_key); + + // Clear caches + field_cache_clear(TRUE); + + // Invoke external hooks after the cache is cleared for API consistency. + module_invoke_all('field_update_field', $field, $prior_field, $has_data); +} + /** * Read a single field record directly from the database. Generally, * you should use the field_info_field() instead. Index: modules/field/field.test =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.test,v retrieving revision 1.50 diff -u -F^[fc] -r1.50 field.test --- modules/field/field.test 18 Sep 2009 00:04:22 -0000 1.50 +++ modules/field/field.test 20 Sep 2009 13:17:58 -0000 @@ -1429,13 +1429,14 @@ class FieldCrudTestCase extends FieldTes public static function getInfo() { return array( 'name' => 'Field CRUD tests', - 'description' => 'Create / read /update fields.', + 'description' => 'Test field create, read, update, and delete.', 'group' => 'Field', ); } function setUp() { - parent::setUp('field_test'); + // field_update_field() tests use number.module + parent::setUp('field_test', 'number'); } // TODO : test creation with @@ -1697,6 +1698,100 @@ class FieldCrudTestCase extends FieldTes $this->assertEqual($entity->{$field['field_name']}[$langcode][$delta]['value'], $values[$delta]['value'], "Data in previously deleted field saves and loads correctly"); } } + + function testUpdateNonExistentField() { + $test_field = array('field_name' => 'does_not_exist', 'type' => 'number_decimal'); + try { + field_update_field($test_field); + $this->fail(t('Cannot update a field that does not exist.')); + } + catch (FieldException $e) { + $this->pass(t('Cannot update a field that does not exist.')); + } + } + + function testUpdateFieldType() { + $field = array('field_name' => 'field_type', 'type' => 'number_decimal'); + $field = field_create_field($field); + + $test_field = array('field_name' => 'field_type', 'type' => 'number_integer'); + try { + field_update_field($test_field); + $this->fail(t('Cannot update a field to a different type.')); + } + catch (FieldException $e) { + $this->pass(t('Cannot update a field to a different type.')); + } + } + + /** + * Test updating a field. + */ + function testUpdateField() { + // Create a decimal 5.2 field, update it to 5.3, and verify it + // rounds to 3 decimal places. + $field = array('field_name' => 'decimal53', 'type' => 'number_decimal', 'cardinality' => 3, 'settings' => array('precision' => 5, 'scale' => 2)); + $field = field_create_field($field); + $instance = array('field_name' => 'decimal53', 'bundle' => FIELD_TEST_BUNDLE); + $instance = field_create_instance($instance); + $field['settings']['scale'] = 3; + field_update_field($field); + $entity = field_test_create_stub_entity(0, 0, $instance['bundle']); + $entity->decimal53[FIELD_LANGUAGE_NONE][0]['value'] = '1.23'; + $entity->decimal53[FIELD_LANGUAGE_NONE][1]['value'] = '1.235'; + $entity->decimal53[FIELD_LANGUAGE_NONE][2]['value'] = '1.2355'; + field_attach_insert('test_entity', $entity); + $entity = field_test_create_stub_entity(0, 0, $instance['bundle']); + field_attach_load('test_entity', array(0 => $entity)); + $this->assertEqual($entity->decimal53[FIELD_LANGUAGE_NONE][0]['value'], '1.23', t('2 decimal places are left alone')); + $this->assertEqual($entity->decimal53[FIELD_LANGUAGE_NONE][1]['value'], '1.235', t('3 decimal places are left alone')); + $this->assertEqual($entity->decimal53[FIELD_LANGUAGE_NONE][2]['value'], '1.236', t('4 decimal places are rounded to 3')); + } + + /** + * Test updating a field with data. + */ + function testUpdateFieldSchemaWithData() { + // Create a decimal 5.2 field and add some data. + $field = array('field_name' => 'decimal52', 'type' => 'number_decimal', 'settings' => array('precision' => 5, 'scale' => 2)); + $field = field_create_field($field); + $instance = array('field_name' => 'decimal52', 'bundle' => FIELD_TEST_BUNDLE); + $instance = field_create_instance($instance); + $entity = field_test_create_stub_entity(0, 0, $instance['bundle']); + $entity->decimal52[FIELD_LANGUAGE_NONE][0]['value'] = '1.235'; + field_attach_insert('test_entity', $entity); + + // Attempt to update the field in a way that would work without data. + $field['settings']['scale'] = 3; + try { + field_update_field($field); + $this->fail(t('Cannot update field schema with data.')); + } + catch (FieldException $e) { + $this->pass(t('Cannot update field schema with data.')); + } + } + + function testUpdateFieldForbid() { + $field = array('field_name' => 'forbidden', 'type' => 'test_field', 'settings' => array('changeable' => 0, 'unchangeable' => 0)); + $field = field_create_field($field); + $field['settings']['changeable']++; + try { + field_update_field($field); + $this->pass(t("A changeable setting can be updated.")); + } + catch (FieldException $e) { + $this->fail(t("An unchangeable setting cannot be updated.")); + } + $field['settings']['unchangeable']++; + try { + field_update_field($field); + $this->fail(t("An unchangeable setting can be updated.")); + } + catch (FieldException $e) { + $this->pass(t("An unchangeable setting cannot be updated.")); + } + } } class FieldInstanceCrudTestCase extends FieldTestCase { 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.22 diff -u -F^[fc] -r1.22 field_sql_storage.module --- modules/field/modules/field_sql_storage/field_sql_storage.module 10 Sep 2009 22:31:58 -0000 1.22 +++ modules/field/modules/field_sql_storage/field_sql_storage.module 20 Sep 2009 13:17:58 -0000 @@ -204,6 +204,58 @@ function field_sql_storage_field_storage } /** + * Implement hook_field_update_field_forbid(). + * + * Forbid any field update that changes column definitions if there is + * any data. + */ +function field_sql_storage_field_update_forbid($field, $prior_field, $has_data) { + if ($has_data && $field['columns'] != $prior_field['columns']) { + throw new FieldException("field_sql_storage cannot change the schema for an existing field with data."); + } +} + +/** + * Implement hook_field_storage_update_field(). + */ +function field_sql_storage_field_storage_update_field($field, $prior_field, $has_data) { + $ret = array(); + + if (! $has_data) { + // There is no data. Re-create the tables completely. + $prior_schema = _field_sql_storage_schema($prior_field); + foreach ($prior_schema as $name => $table) { + db_drop_table($ret, $name, $table); + } + $schema = _field_sql_storage_schema($field); + foreach ($schema as $name => $table) { + db_create_table($ret, $name, $table); + } + } + else { + // There is data, so there are no column changes. Drop all the + // prior indexes and create all the new ones, except for all the + // priors that exist unchanged. + $table = _field_sql_storage_tablename($prior_field); + $revision_table = _field_sql_storage_revision_tablename($prior_field); + foreach ($prior_field['indexes'] as $name => $spec) { + if (!isset($field['indexes'][$name]) || $spec != $field['indexes'][$name]) { + db_drop_index($ret, $table, $name); + db_drop_index($ret, $revision_table, $name); + } + } + $table = _field_sql_storage_tablename($field); + $revision_table = _field_sql_storage_revision_tablename($field); + foreach ($field['indexes'] as $name => $spec) { + if (!isset($prior_field['indexes'][$name]) || $spec != $prior_field['indexes'][$name]) { + db_add_index($ret, $table, $name, $spec); + db_add_index($ret, $revision_table, $name, $spec); + } + } + } +} + +/** * Implement hook_field_storage_delete_field(). */ function field_sql_storage_field_storage_delete_field($field_name) { Index: modules/field_ui/field_ui.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field_ui/field_ui.admin.inc,v retrieving revision 1.15 diff -u -F^[fc] -r1.15 field_ui.admin.inc --- modules/field_ui/field_ui.admin.inc 18 Sep 2009 00:12:46 -0000 1.15 +++ modules/field_ui/field_ui.admin.inc 20 Sep 2009 13:17:58 -0000 @@ -888,7 +888,7 @@ function field_ui_field_settings_form_su // Update the field. $field = array_merge($field, $field_values); - field_ui_update_field($field); + field_update_field($field); drupal_set_message(t('Updated field %label field settings.', array('%label' => $instance['label']))); $form_state['redirect'] = field_ui_next_destination($bundle); @@ -1289,7 +1289,7 @@ function field_ui_field_edit_form_submit // Update any field settings that have changed. $field = field_info_field($instance_values['field_name']); $field = array_merge($field, $field_values); - field_ui_update_field($field); + field_update_field($field); // Move the default value from the sample widget to the default value field. if (isset($instance_values['default_value_widget'])) { Index: modules/field_ui/field_ui.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field_ui/field_ui.module,v retrieving revision 1.7 diff -u -F^[fc] -r1.7 field_ui.module --- modules/field_ui/field_ui.module 29 Aug 2009 04:42:56 -0000 1.7 +++ modules/field_ui/field_ui.module 20 Sep 2009 13:17:58 -0000 @@ -242,33 +242,6 @@ function field_ui_field_ui_build_modes_t } /** - * Updates a field. - * - * Field API does not allow field updates, so we create a method here to - * update a field if no data is created yet. - * - * @see field_create_field() - */ -function field_ui_update_field($field) { - $field_types = field_info_field_types(); - $module = $field_types[$field['type']]['module']; - - // If needed, remove the 'bundles' element added by field_info_field. - unset($field['bundles']); - - $defaults = field_info_field_settings($field['type']); - $field['settings'] = array_merge($defaults, (array) $field['settings']); - $data = $field; - unset($data['id'], $data['columns'], $data['field_name'], $data['type'], $data['locked'], $data['module'], $data['cardinality'], $data['active'], $data['deleted']); - $field['data'] = $data; - - drupal_write_record('field_config', $field, array('field_name')); - - // Clear caches. - field_cache_clear(TRUE); -} - -/** * Implement hook_field_attach_create_bundle(). */ function field_ui_field_attach_create_bundle($bundle) { Index: modules/simpletest/tests/field_test.module =================================================================== RCS file: /cvs/drupal/drupal/modules/simpletest/tests/field_test.module,v retrieving revision 1.23 diff -u -F^[fc] -r1.23 field_test.module --- modules/simpletest/tests/field_test.module 18 Sep 2009 00:12:47 -0000 1.23 +++ modules/simpletest/tests/field_test.module 20 Sep 2009 13:17:58 -0000 @@ -349,6 +349,8 @@ function field_test_field_info() { 'label' => t('Test Field'), 'settings' => array( 'test_field_setting' => 'dummy test string', + 'changeable' => 'a changeable field setting', + 'unchangeable' => 'an unchangeable field setting', ), 'instance_settings' => array( 'test_instance_setting' => 'dummy test string', @@ -361,6 +363,15 @@ function field_test_field_info() { } /** + * Implement hook_field_update_forbid(). + */ +function field_test_field_update_forbid($field, $prior_field, $has_data) { + if ($field['type'] == 'test_field' && $field['settings']['unchangeable'] != $prior_field['settings']['unchangeable']) { + throw new FieldException("field_test 'unchangeable' setting cannot be changed'"); + } +} + +/** * Implement hook_field_schema(). */ function field_test_field_schema($field) {