Currently, it's not possible to change the storage type (e.g. decimal, integer) of a field that contains existing data using any "normal" method (for instance, importing the new field configuration and running entity updates). This will result in an exception well-known to most developers:
The SQL storage cannot change the schema for an existing field with data
I understand why Drupal core can't handle such a schema change on its own (for instance, how does one convert a multi-value text list to an integer?), but in general there needs to be a way to handle such an operation. For instance, on a current production application, I need to change an integer field to a decimal field. This seems like a reasonable expectation, but I can't figure out how to make it work. Here's what I've tried so far (as an update hook):
<?php
// Attempt to change field 'foo' on entity type 'product' from integer to decimal.
$fieldSpec = [
'type' => 'numeric',
'precision' => 10,
'scale' => 2,
];
db_change_field('product__field_foo', 'field_foo_value', 'field_foo_value', $fieldSpec);
\Drupal::service('config.factory')->getEditable('field.storage.product.field_foo')
->set('type', 'decimal')
->set('settings', [
'precision' => 10,
'scale' => 2,
])
->save();
\Drupal::entityManager()->clearCachedFieldDefinitions();
$key_value_store_definition = \Drupal::keyValue('entity.definitions.installed');
$storage_definitions = $key_value_store_definition->get('product.field_storage_definitions');
$storage_definitions['field_foo'] = \Drupal::entityManager()->getFieldStorageDefinitions('product')['field_foo'];
$key_value_store_definition->set('product.field_storage_definitions', $storage_definitions);
$key_value_store_schema = \Drupal::keyValue('entity.storage_schema.sql');
$schema = $key_value_store_schema->get('product.field_schema_data.field_foo');
$schema['product__field_foo']['fields']['field_foo_value'] = array(
'type' => 'numeric',
'precision' => 10,
'scale' => 2,
'not null' => TRUE,
);
$key_value_store_schema->set('product.field_schema_data.field_foo', $schema);
The problem is that Drupal still sees the entity as needing updates and fails with the above exception. Specifically, inspecting the FieldStorageConfig objects that get passed to the SQL schema update function, it appears that the only difference is the following property. I have no idea where it's coming from or how to set it:
[propertyDefinitions:protected] => Array
(
[value] => Drupal\Core\TypedData\DataDefinition Object
(
[definition:protected] => Array
(
[type] => string
[label] => Drupal\Core\StringTranslation\TranslatableMarkup Object
(
[string:protected] => Decimal value
[translatedMarkup:protected] =>
[options:protected] => Array
(
)
[stringTranslation:protected] =>
[arguments:protected] => Array
(
)
)
[required] => 1
)
)
)
Can anyone point me towards a solution to this, or (even better) a more general solution that doesn't require so much boilerplate code?
Comments
Comment #3
drugan commentedI had the same issue when worked on my patch to number field. The solution is to fetch the field data and save it in some variable, then truncate the field table to unlock it for changes, then make all the changes and after that insert earlier saved data in the field. Works pretty good, but the only thing you need to not forget is PRIMARY KEY and may be INDEXES. They also should be dropped before making changes on the field type/settings.
Read how to do it in the docs for db_change_field() function. Or look into the real working example in field_update_8004() here:
#2816859: Allow the 'step' to be configured as a NumberWidget setting
The update adds the unsigned storage setting for any type number fields with existing data. You can adjust the code with the settings you need to change on the field. Note that the field_update_8004() also updates number fields on the entity_type revisions if they exist.
To check if the update worked as expected run this code:
If there is no exception thrown then you are lucky enough and may proceed with you updated fields.
Comment #4
eric.chenchao commentedI have posted a Drupal 8 code example here:
http://drupal.stackexchange.com/questions/208946/how-do-i-change-a-field...
Comment #5
jaykandariSubscribing.
Comment #6
jaykandariI managed to change field type from plain text (string) to formatted text (text). by forking @eric.chenchao's solution given in #4.
Comment #7
nlisgo commented#6 did the trick for me. After I ran the update hooks locally I exported the configuration to code and this completed my fix. Our deploy steps ensured that the migration of content and configuration change could be handled in a single release.
Comment #9
mstrelan commentedCaution: deleting the field in Step 3 meant that some blocks were broken on my site. I'm not sure exactly why, but it's possible the block was from a view that was no longer valid, or some other configuration that was referencing the field. I didn't look in to it further.
Instead, I've written an amended script that will truncate the tables so you can update the field settings. Not sure if this will work for converting from one field type to another, but it worked for increasing the precision of a decimal field. I've also added a try/catch for inserting the rows, because some of the data in my tables was out of range.
Use at your own risk, obviously.
Comment #10
drugan commented@mstrelan
Basically, you did the same as on the patch from #3 comment.
See: #2816859: Allow the 'step' to be configured as a NumberWidget setting
Comment #12
sbs-jms commentedI'm trying to update a content type to move a field from text to decimal (to allow proper sorting). I've emptied the tables manually and via phpmyadmin truncate(), but an import of the revised configuration still fails with the same error. Is a PHP hook the only way to accomplish this change (that is, can it not be accomplished via a configuration import)? If so, what is required to run the patch as native PHP? (I'm familiar with PHP, but new to drupal).
Thanks,
Jack
Comment #13
drugan commented@sbs-jms
You can install https://www.drupal.org/project/devel module and then open http://example.com/devel/php page and paste in the text field your custom PHP code and press
Executebutton. The code will be executed the same way if it was in the file.php. For example, take the function on the #10, modify it for your needs then do this in the textfield:Comment #15
rogerpfaffAfter creating the new fields and deleting the old ones my form and view display modes are changed and the new fields are set to disabled. Is there a way to preserve the position of the field in the display modes?
Comment #16
rogerpfaffTo answer myself:
Add a loop in Step 3 to gather all the weights of the field:
Add another loop as Step 9 to set the weights
Comment #17
dpi#937442: Field type modules cannot maintain their field schema (field type schema change C(R)UD is needed)
Comment #21
Nacho2018 commentedComment #22
kevinc_ commentedI used an adaptation of #6 to convert a populated field from string to text_long.
When the field is deleted and then recreated, it is removed from all other contexts - so, as mentioned in #9, this will break all blocks where this field is used.
D8 makes it slightly easier to solve for this by using config export, config import and source control.
- After the
drush updbhas finished executedrush cex- Revert all config apart from the fields in question
- Run
drush cimThis has worked successfully for me, YMMV.
Comment #23
rahul.satija commented@JayKandari #6 thanks for your reference code.
I have different requirement,
I have a number(integer) field under a paragraph and i need to change field into number(decimal)
Can you share same code as you did for plain text to plain formatted under comment #6.
My current code.
/**
* Change field_fee_card_late_fee field from int to decimal(float).
*/
function nipr_post_blocks_update_8009() {
$fields = [
'field_fee_card_late_fee' => [
'table' => 'paragraph__field_fee_card_late_fee',
'revision_table' => 'paragraph_revision__field_fee_card_late_fee',
'format_col' => 'field_fee_card_late_fee_format',
],
];
$database = \Drupal::database();
foreach ($fields as $field_name => $f) {
$table = $f['table'];
$revision_table = $f['revision_table'];
// Entity type here.
$entity_type = 'paragraph';
// Step 1: Get field storage.
$field_storage = FieldStorageConfig::loadByName($entity_type, $field_name);
// Check if field not found.
if (is_null($field_storage)) {
continue;
}
// Step 2: Store data.
$rows = NULL;
$revision_rows = NULL;
if ($database->schema()->tableExists($table)) {
// The table data to restore after the update is completed.
$rows = $database->select($table, 'n')->fields('n')->execute()
->fetchAll();
$revision_rows = $database->select($revision_table, 'n')->fields('n')->execute()
->fetchAll();
}
// Step 3: Save new field configs & delete existing fields.
$new_fields = array();
foreach ($field_storage->getBundles() as $bundle => $label) {
$field = FieldConfig::loadByName($entity_type, $bundle, $field_name);
$new_field = $field->toArray();
$new_field['field_type'] = 'numeric';
$new_fields[] = $new_field;
// Delete field.
$field->delete();
}
// Step 4: Create new storage configs from existing.
$new_field_storage = $field_storage->toArray();
$new_field_storage['type'] = 'number_decimal';
$new_field_storage['module'] = 'numeric';
$new_field_storage['settings'] = [
'precision' => 10,
'scale' => 2,
'not null' => FALSE,
];
// Step 5: Purge deleted fields data.
// This is required to create new fields.
// field_purge_batch(250);
// Step 6: Create new fieldstorage.
FieldStorageConfig::create($new_field_storage)->save();
// Step 7: Create new fields for all bundles.
foreach ($new_fields as $new_field) {
$new_field = FieldConfig::create($new_field);
$new_field->save();
}
//Step 8: Restore existing data in fields & revision tables.
if (!is_null($rows)) {
foreach ($rows as $row) {
$row = (array) $row;
// $row[$f['format_col']] = 'static_html';
$database->insert($table)->fields($row)->execute();
}
}
if (!is_null($revision_rows)) {
foreach ($revision_rows as $row) {
$row = (array) $row;
// $row[$f['format_col']] = 'static_html';
$database->insert($revision_table)->fields($row)->execute();
}
}
}
}
thanks in advance
Comment #24
rahul.satija commented@JayKandari #6,
I want to change a number( integer) field to number(decimal) in a paragraph. Kindly share reference code.
Comment #25
gundartb commentedThere is a module that can help with this issue:
Change Field Type
https://www.drupal.org/project/change_field_type
Now - it changes number types to other number types (like changing Float to Decimal), there may be more functionality added in subsequent updates.
Comment #26
anybodySee https://www.drupal.org/docs/drupal-apis/update-api/updating-entities-and... for best practice.
Also please help to create a full-featured Contrib Module with UI and API (service) to use for such tasks: https://www.drupal.org/project/issues/change_field_type
I'm sure the Drupal community needs that functionality, help with coding and sponsorship!
Comment #27
anybodyIf others should run into the same issue, please see my comment #26 above for finding help. Also please help to create better support for such tasks in core (and contrib, see #26)
In #80 of #937442: Field type modules cannot maintain their field schema (field type schema change C(R)UD is needed) I started a discussion how to add a helper in core to make such field type conversions easier.
This is based on the "Closed (duplicate)" by @dpi in #17 here and the point that this is only one part of multiple core utilities needed for fields (and especially field updates) in #937442: Field type modules cannot maintain their field schema (field type schema change C(R)UD is needed).