Problem/Motivation
ContentEntityStorage base has a hasData() method that checks if any entities exist at all. This method is called during the uninstall process of a module to warn the user that data will be lost if the module is uninstalled.
When a module has deployment issues and the entity storage table has not been created, the hasData() method will throw an exception, completely blocking the uninstall process. So basically you end up with a broken module, that you cannot uninstall.
Proposed resolution
hasData() should return FALSE when the database table does not exist.
Remaining tasks
Address #18:
Determine how to know when hasData is failing specifically due to a missing database table and not for some other reason.
Suggested solution is to override the query since we can't assume a database backend.
Is it possible to examine the exception instead and return false for the specific missing table exception and throw the error otherwise
User interface changes
None
API changes
None
Data model changes
None
Original report
1. Create a content entity, and mess it up so much that the module that defines the entity installs but the entity table is not created.
(or just delete the entity table for a custom defined entity)
2. Try to uninstall the module... the uninstall page is all broken with a message such as this one:
Drupal\Core\Database\DatabaseExceptionWrapper: SQLSTATE[42S02]: [Microsoft][ODBC Driver 11 for SQL Server][SQL Server]Invalid object name 'powerlog'.: SELECT TOP(1) base_table.[id] AS [id], base_table.[id] AS [base_table_id] FROM powerlog base_table; Array ( ) : SELECT TOP(1) base_table.[id] AS [id], base_table.[id] AS [base_table_id] FROM powerlog base_table; Array ( ) in Drupal\Core\Entity\ContentEntityStorageBase->hasData() (line 74 of core\lib\Drupal\Core\Entity\ContentEntityStorageBase.php).
Drupal\Driver\Database\sqlsrv\Statement->execute(Array, Array)
Drupal\Driver\Database\sqlsrv\Connection->query('SELECT TOP(1) base_table.[id] AS [id], base_table.[id] AS [base_table_id]
FROM
{powerlog} base_table', Array, Array)
Drupal\Core\Database\Query\Select->execute()
Drupal\Core\Entity\Query\Sql\Query->result()
Drupal\Core\Entity\Query\Sql\Query->execute()
Drupal\Core\Entity\ContentEntityStorageBase->hasData()
Drupal\Core\Entity\ContentUninstallValidator->validate('powerlog')
Drupal\Core\Extension\ModuleInstaller->validateUninstall(Array)
Drupal\Core\ProxyClass\Extension\ModuleInstaller->validateUninstall(Array)
Drupal\system\Form\ModulesUninstallForm->buildForm(Array, Object)
Make the uninstall process more fault tolerant for entity management so that if these are crippled, it won't crash.
| Comment | File | Size | Author |
|---|---|---|---|
| #68 | 2563515-nr-bot.txt | 1.55 KB | needs-review-queue-bot |
Issue fork drupal-2563515
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
neclimdulMan that's annoying. Not being able to uninstall a broken module is a pretty big deal IMHO.
Comment #3
david_garcia commentedThis should do it.
Comment #4
david_garcia commentedComment #5
david_garcia commentedComment #6
berdirLooks fine, now we need a test. We should have plenty of uninstall tests, lets duplicate something and then we manually remove the table first.
Comment #8
berdirWhy I consider this to be major:
This typically happens when you create an entity with a new entity type. You add it, but drupal doesn't know about it. So you try to re-install your module. But that fails because it now tries to remove the entity tables that were never created.
There are pretty easy workarounds, like temporarily removing your entity class, or writing an update function or just running the necessary drush commands. But I've seen new developers getting stuck on this problem.
Comment #9
neclimdulI think I got stuck on this for a while actually while developing a custom entity where I made a typo and broke everything. Took me a while to get the magic set of commands and changes to recover. Pretty miserable DX.
Comment #11
cilefen commentedComment #12
xjmThis fix seems reasonable to me; if a module does not have a schema installed then it does not have data, so it's logical to catch that specific exception only and return false.
There is a slight chance of disruption here if extending code were already responding to
SchemaObjectDoesNotExistException, so if we are strict we might want to move this to 8.3.x. However, it's much less disruptive than (e.g.) throwing a new exception or catching exceptions more broadly, so we could discuss whether to backport it to a patch release if needed, but I don't think it's needed since this is a DX improvement that helps protect data integrity in case of miswritten code.Still needs tests though. They should be relatively straightforward and can be based on an existing test to uninstall a module. It's a slightly advanced novice task: as described in #6, find an existing test that tests uninstalling a module, copy it, add a couple lines that manually drop the database table, and test that the module is still uninstalled successfully. We should also potentially add unit tests for catching the exception.
Comment #13
cilefen commentedDiscussed with @xjm, @alexpott, @amateescu, @fago, @berdir and @plach. We
agreed that this is major because it is painful DX. Plus, it is easy to fix.
Comment #14
xjmNot being able to uninstall a module in this scenario can also be problematic for data integrity (in addition to it being a real blocker for developers).
Comment #15
berdirTest is not a in perfect place but works.
Noticed that the exception used there is only used in schema operations, it's just a standard database exception here.
Comment #17
mpdonadios/catched/caught
For the sake of discussion, is there anything else that can happen that would throw a DatabaseExceptionWrapper here?
Comment #18
plachWe are in the storage base class here, so we should not assume a database.
To alleviate @mpdonadio's concern, I'm wondering whether we could leave the entity query in place here (with no try/catch) and override it with a sql query in the SQL storage to minimize the possibility a different exception is unintentionally caught.
Comment #20
Torenware commented"Slightly advanced novice task", see comment 12.
Comment #21
lokapujyaComment #22
lokapujyaDoesn't apply to 8.4.
Comment #23
jofitzRe-rolled with slight fuzz.
Comment #24
lokapujyaI don't know how the sql query override in the 2nd part of #18 would be implemented. If we are going with this patch, need to make the change suggested in #17. And, I think that the first part of #18 is suggesting to use a different Exception type?
Comment #26
kenorb commentedRelated: Why wont drupal recognize my base table name?
Comment #27
jofitzPatch in #23 no longer applies - re-rolled.
Corrected spelling mistake identified in #17.
Comment #28
lokapujyaComment #29
lokapujyaTests already added in #15.
Comment #30
lokapujyaThere is no longer a novice task.
Comment #31
mpdonadioThis is a nice, simple solution that is well documented. It has a test that is also nicely documented. I queued up tests against all three database backend, and they came up green. #15 showed a test-only patch to demonstrate the problem and solution. This should be good to go.
Queueing up 8.4.x tests, too, since this should be eligible for that.
Comment #32
mpdonadioJust re-read this whole thread. The comments in #18 weren't discussed or addressed. So back to NW :(
Comment #38
dariogcode commentedI just encountered this error on our install. Running 8.8.5. I re-rolled patch for this version.
Comment #39
aleevasComment #40
johnwebdev commentedAdressing #18.
Comment #42
jibranThank you for addressing the feedback @johnwebdev patch looks great.
@plach also suggested in #18. Now that we are in
SqlContentEntityStoragewhereDatabaseExceptionWrapperis already used we don't need to change the Exception.Given #18 is the only outstanding feedback and we have red and green patches I think this is RTBC.
Comment #43
alexpottI think this is fixing this in the wrong location. I don't think that by default hasData() should be robust to sql error - if it experiences an error when called we should continue to throw an error - however I think there is a clear case for doing that in uninstall. So I think we should be catching the database exception in \Drupal\Core\Entity\ContentUninstallValidator::validate() and allowing uninstall to proceed in those cases.
Comment #44
ericjgruber commentedI can confirm this patch works on Drupal 8.8.2. I can see the module uninstall page again.
Comment #45
paulocsI will work on it
Comment #46
paulocsHello everyone,
I did a new patch with what @alexpott pointed on comment #43.
It is good to know that if the module has more than one entity, it will be able to uninstall the module even if one table from one entity is dropped and the other is not.
Cheers, Paulo.
Comment #47
paulocsThe correct interdiff.
Comment #48
lendudeWas going to ask for something more specific to catch, but since this is moved to ContentUninstallValidator we are back to @plach' point in #18 that we can't assume a database
As @Berdir pointed out when he added this, this might not be in the best place? With this addition we are now only testing the uninstall when there are missing tables and no longer testing it when all tables are present (which seems to me like the more common scenario). Maybe add a test case where we are removing the table but leave the existing test case as it is?
Comment #50
rymoPatch #46 works for me on 9.1.5.
Comment #53
quietone commentedSetting to NW for #48
Comment #56
akhil babuAs per second review comment in #48, Moving the test to a new function. I don't understand what the first suggestion in #48 or #18 means.
I found the following code in Drupal\mysql\Driver\Database\mysql\ExceptionHandler
SQLSTATE error code is checked here and if its in 23xxx pattern, IntegrityConstraintViolationException is thrown.
Similerly, "Base table or view not found" Error is having the error code "42S02". So, Couldn't we check if the code is in '42xxx' pattern and throw a new exception 'TableNotFoundException' , and then catch it during uninstall? (But the doumentation https://docstore.mik.ua/orelly/java-ent/jenut/ch08_06.htm says that return code 42 is for Syntax error or access rule violation :[ )
Comment #57
_utsavsharma commentedFixed CCF for #56.
Comment #59
mariskath commented#57 worked for me! Thanks!
Best,
Mariska
Comment #60
chikeI installed Drupal 10.2.4 and started building the site, just regular site building stuff - enabling contrib modules and creating pages. Then I decided to uninstall the 'contact' module and clicking /admin/modules/uninstall threw this error,
Drupal\Core\Database\DatabaseExceptionWrapper: SQLSTATE[42S02]: Base table or view not found: 1146 Table 'custom_theme.media' doesn't exist: SELECT "base_table"."vid" AS "vid", "base_table"."mid" AS "mid" FROM "media" "base_table" GROUP BY "base_table"."vid", "base_table"."mid" LIMIT 1 OFFSET 0; Array ( ) in Drupal\Core\Entity\Query\Sql\Query->result() (line 271 of C:\wamp64\www\custom-theme\public_html\core\lib\Drupal\Core\Entity\Query\Sql\Query.php).The 'media' module was auto-installed as a dependency for 'bootstrap_layout_builder' module. I have not added a media item yet.
Anyways patch #57 got me back to the Uninstall page and I was able to uninstall the 'contact' module.
Thanks @_utsavsharma
Comment #61
chikeMy issue wasn't solved. What happened in my case was somehow 'media' module did not install completely or something cos I found the media types were not available on the site and visiting 'admin/content/media' threw an error. So I rolled back the site till I uninstalled 'media' module. I removed patch #57 and re-installed the media module and it was now properly installed. I re-installed back all the other modules and site came back normal.
The thing is, without patch #57 I wouldn't have been able to access the Uninstall page to uninstall the media module and the other modules that required it.
Comment #64
nicxvan commentedI've converted this to an MR and reviewed it. It looks like the second question in #48 was taken care of in the latest patch.
I fixed some phpcs issues and updated the issue summary remaining tasks.
If someone can share some direction on #18 I'm happy to make an attempt.
Comment #65
nicxvan commentedI wonder if rather than just overriding the request and trying to narrow the scope, maybe in the catch we need to examine the exception and return false if it's due to a missing table, and throw the reason otherwise?
Comment #66
greg boggsI don't see code in the current changes that assume a database. So, perhaps the comment in #18 is about surrounding code outside of these changes. I am going to mark this needs review again until it's more clear what work needs to be done on this MR.
Comment #67
smustgrave commentedSo believe #18 was referring to the original fix at that time around ContentEntityStorageBase so would say may not be relevant with new approach?
Will leave in review but believe proposed solution may need to be tweaked.
Comment #68
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #69
catchI don't think there's a specific-enough exception to look for which only covers missing tables, but we could narrow it down to DatabaseException?
Comment #70
godotislateThe suggestion in #18 is that Database-specific exception handling shouldn't be done outside the SQL content entity storage class, and per #43, the exception handling was moved to the content uninstall validator.
At the very least, I don't think all the exceptions should be swallowed without at least logging an error or warning and perhaps also showing a message. Theoretically at least
Url::fromRoute()andEntityTypeManager::getStorage()could throw exceptions as well.An alternative could be something implementing
hasData()inSqlContentEntityStorageto be something like this:Then catch
EntityStorageExceptionin the uninstall validator.Comment #71
catchEntityStorageExceptioncould be worth a look yeah.If we know this situation can only happen when an install goes wrong in the first place, an E_USER_WARNING might be good?
Comment #73
xjmAmending attribution, and crediting the major triage:
Comment #75
xjm