Problem/Motivation
Field purge can purge data from fields that have been recreated with the same name.
Proposed resolution
Remaining tasks
User interface changes
tba
API changes
tba
Data model changes
tba
Original issue summary
When developing a module .install you often have to uninstall/reinstall. When you uninstall the module, the fields are marked for deletion (see key_value table at the row with collection to state and field.field.deleted and field.storage.deleted). If you reinstall your module without running the cron manually, your fields values will be deleted at the next cron pass.
see http://drupal.stackexchange.com/questions/210824/field-values-added-prog...
REPRO STEPS
- install a module that programmatically inserts data in taxonomy term custom fields
- uninstall the module
- reinstall the module
- run cron
RESULT:
The values of the custom fields are deleted
EXPECTED:
The values are not deleted
class schedule_block_class{
const schedule_block_channels_terms = array(
array(
'name' => 'Brava',
'field_channel_id' => 'BRA001'
),
array(
'name' => 'DJazz',
'field_channel_id' => 'DJA001'
));
const schedule_block_channels_vid = 'schedule_channels';
}
function schedule_block_install(){
$schedule_block_terms = schedule_block_class::schedule_block_channels_terms;
$schedule_block_vid = schedule_block_class::schedule_block_channels_vid;
foreach($schedule_block_terms as $schedule_block_term){
$term = Term::create(array(
'name' => $schedule_block_term['name'],
'field_channel_id' => $schedule_block_term['field_channel_id'],
'description' => '',
'parent' => array(0),
'vid' => $schedule_block_vid
));
$term->save();
}
}
function schedule_block_uninstall(){
$schedule_block_terms = schedule_block_class::schedule_block_channels_terms;
$schedule_block_vid = schedule_block_class::schedule_block_channels_vid;
$terms = array();
foreach($schedule_block_terms as $schedule_block_term){
if($terms_name = taxonomy_term_load_multiple_by_name($schedule_block_term['name'], $schedule_block_vid)){
$terms = array_merge($terms, $terms_name);
}
}
foreach($terms as $term){
$term->delete();
}
$taxo = \Drupal\taxonomy\Entity\Vocabulary::load(schedule_block_class::schedule_block_channels_vid);
if(!is_null($taxo)){
$taxo->delete();
}
}
| Comment | File | Size | Author |
|---|---|---|---|
| #33 | 2782009-2-33.patch | 6.18 KB | alexpott |
| #33 | 13-33-interdiff.txt | 4.07 KB | alexpott |
| #30 | 2782009-30.patch | 18.2 KB | alexpott |
| #30 | 25-30-interdiff.txt | 1.17 KB | alexpott |
| #25 | 2782009-24.patch | 18.18 KB | alexpott |
Comments
Comment #2
reblutus commentedComment #3
catchField module should prevent uninstall if there are fields to purge, it could show a message telling you to run cron.
https://api.drupal.org/api/drupal/core%21modules%21file%21src%21Plugin%2...
Comment #4
catchComment #5
swentel commentedHmm, could this also mean that we have data loss when you'd add a field through the UI, add values, then delete the field and recreate it with the same name ? That would be pretty hilarious (and probably would be the same in D7). We could write a test for that to find out quickly.
Comment #6
mallezie@swentel that doesn't count as dataloss according to me, you're deleting the field?
Comment #7
swentel commented@mallezie sure, but if you recreate the field with the same name, and there is a bug in the purging (apparently) where it picks up the wrong table name ( the 'live' one instead of the 'deleted/renamed' table) you have data loss on your new field. And that's not good :)
Comment #8
swentel commentedRough test that proves the problem - create a field, add values, delete it, recreate, add values, call purge, values are deleted from the main table (for some reason I couldn't get it to replicate it using entity query, so it's a direct db query)
Comment #10
alexpottDiscussed with @xjm, @effulgentsia, @webchick and @Cottser. We agreed that this was super critical because unexpected field delete is really bad.
Comment #11
mile23field.purge.incsays this in a few places:#2080823: Create API to discover config entities' soft dependencies and use this to present a confirm form on module uninstall is closed, so time to revisit. :-)
That issue has this follow-up: #2343517: Cleanup @todo referring to the config dependencies API issue which deals with the @todo mentioned above.
Comment #12
alexpottSo this bug is happening because \Drupal\Core\Entity\Sql\SqlContentEntityStorage::readFieldItemsToPurge() works using the column name of the storage. I think this means we need to rename the field when it is deleted - that is the only way around this. Or we don't permit fields to be created when we have deleted fields which will end up with the same column name which will break module re-install.
Comment #13
alexpottAh I see - there's a bug in \Drupal\Core\Entity\Sql\SqlContentEntityStorage::storageDefinitionIsDeleted() - the way this answers this question is wrong for configurable fields which can be recreated with the same name.
I've also made some of the queries safer by adding a condition on the deleted column which we set when we delete fields.
Comment #14
swentel commentedSo with the fix underneath, this shouldn't be needed anymore normally right ? But it probably can't hurt to add these.
Nice catch, my brain hurt a bit at first because of the OR condition, but it's the right check. We should probably document this on the the interface docblock.
Comment #15
alexpott@swentel yep the delete checks are not strictly necessary as far as I know BUT they can't hurt and added an element of safety since ensuring we don't delete things which haven't been marked deleted seems sane.
Comment #16
swentel commentedOh, nice change in the method, way friendlier to read!
Guess we need a nice test comment of course.
I'm fairly confident about the test, don't really think we need more. The only thing that I'm not sure of is the last part which doesn't use the entity query factory as the other tests do in that class (in general, I feel this class could use some cleanup, but oh well).
Leaving on NR though to get more potential eyeballs on this one, the more the better as this is really tricky stuff.
Comment #17
swentel commentedActually, to answer myself on that, we might explicitly test that the data is removed from the deleted field table, so we're really sure that we're ok, even though we have other tests in the same class that test that behavior.
Comment #18
catchSo the patch in itself looks good, but do we have test coverage for things like updating a node with deleted field values to add new data etc. - i.e. are there any more queries missing deleted = 1?
This seems safer to me.
Comment #19
alexpottI considered
But ended up dismissing this as way too complex re module uninstall and re-install.
Comment #20
alexpottComment #21
alexpottThis is the most important part of the change - since the bug was not in the field being deleted but the field storage being deleted. The other conditions added here are for safety and can not do any harm.
Comment #22
alexpottLooking at the code some more I think being able to pass the $is_deleted flag into TableMapping is wrong headed. This thing has the field storages injected - it should be checking itself. Atm I'm checking it's stored field definitions - that might not be right though as the check in \Drupal\Core\Entity\Sql\SqlContentEntityStorage::storageDefinitionIsDeleted is using the last installed schema repository but the fact that \Drupal\Core\Entity\Sql\DefaultTableMapping::getDedicatedDataTableName and \Drupal\Core\Entity\Sql\DefaultTableMapping::getDedicatedRevisionTableName are not checking the field storages they have is kinda scary. Let's see what breaks.
Improved the test.
Comment #23
alexpottLet's just see if anything breaks if we don't rely on
$is_deletedat all - this would give us some idea if we can remove all usages in core.Comment #24
swentel commentedshould we make use of $active_table_name here ?
Comment #25
alexpottHere's what I think would be necessary for a proper deprecation of the $is_deleted parameter. Hopefully #23 fails.
Comment #26
alexpottre #24 @swentel I think it is ok hard coding that expectation there.
Comment #30
alexpottFixed the last fail.
Comment #32
alexpottHmmm...
The $deleted = !$this->originalDefinitions; is ugly here because it is depending on something outside of the field storage...
Comment #33
alexpottGiven the issues with #32 I think we should back-pedal and do #13 with more tests.
I think we should open a followup about deprecating the $is_deleted flag from getDedicatedDataTableName() and just have an explicit getDeletedDedicatedDataTableName() and make the calling code responsible for deciding which one to call.
Comment #34
swentel commentedMakes sense to me. #32 is tricky, and I haven't got full clue yet either what's going on there, or why it's doing the check like that. Last patch fixes the bug as is first and we can improve in 8.3.x further.
Comment #35
chx commented#2763279: Remove batched field purge from core, it's obsolete, buggy and dangerous
Comment #36
swentel commentedHmm, is #2763097: Field purge eats live data a duplicate then, or is there a subtle difference ?
Comment #37
alexpottI think it is a dupe - just another way of ending p with a deleted filed with the same name.
Comment #40
catchCommitted/pushed to all three 8.x branches, thanks!
Comment #43
chx commented