? 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 04:09:18 -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 04:09:19 -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 04:09:19 -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 04:09:19 -0000
@@ -1425,11 +1425,11 @@ class FieldFormTestCase extends FieldTes
   }
 }
 
-class FieldCrudTestCase extends FieldTestCase {
+class FieldCrdTestCase extends FieldTestCase {
   public static function getInfo() {
     return array(
-      'name' => 'Field CRUD tests',
-      'description' => 'Create / read /update fields.',
+      'name' => 'Field CRD tests',
+      'description' => 'Test field create, read, and delete.',
       'group' => 'Field',
     );
   }
@@ -1699,6 +1699,114 @@ class FieldCrudTestCase extends FieldTes
   }
 }
 
+class FieldUpdateFieldTestCase extends FieldTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => 'Field Update tests',
+      'description' => 'Test field update.',
+      'group' => 'Field',
+    );
+  }
+
+  function setUp() {
+    parent::setUp('field_test', 'number');
+  }
+
+  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 {
   protected $field;
 
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 04:09:19 -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.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 04:09:19 -0000
@@ -246,6 +246,9 @@ function field_ui_field_ui_build_modes_t
  *
  * Field API does not allow field updates, so we create a method here to
  * update a field if no data is created yet.
+ * 
+ * TODO We must update the data columns if needed, for instance
+ * to alter the decimal field position and scale attributes.
  *
  * @see field_create_field()
  */
@@ -262,6 +265,8 @@ function field_ui_update_field($field) {
   unset($data['id'], $data['columns'], $data['field_name'], $data['type'], $data['locked'], $data['module'], $data['cardinality'], $data['active'], $data['deleted']);
   $field['data'] = $data;
 
+  // We update the field settings here but we are not
+  // doing any update of the actual database columns
   drupal_write_record('field_config', $field, array('field_name'));
 
   // Clear caches.
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 04:09:19 -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) {
