Problem/Motivation
If a database does not support transactional DDL, executing statements that alter the database structure in the midst of a transaction lead to the transaction to self-commit.
In our data processing layer, this is less of an edge case than we may think, especially since when we have started creating tables dynamically and not via the hook_schema() called at installation time.
While PostgreSql and SQLite allow transactional DDL, MySQL does not. In the current PDO MySQL driver we can adjust (=reset) the transaction stack when this happens, because it allows to detect if a transaction is active on the client directly. However, in the to-be mysqli driver this is not possible because there is not such a method - we will have to try and interpret exceptions (if they are thrown).
In both cases, though, this would be reactive rather than proactive.
Proposed resolution
1) Introduce a Schema::executeDdlStatement method that, in case of unsupported transactional DDL, will reflect in Drupal's transaction stack that a commit in an active client transaction occurred and make the necessary adjustments in TransactionManager so that the Transaction objects going out of scope do not try to perform database operations. In other words, after execution of the DDL statement, there is NO Drupal transaction active.
2) Change in the Schema classes the Connection::query calls that execute DDL to use Schema::executeDdlStatement instead.
Remaining tasks
User interface changes
API changes
Data model changes
Release notes snippet
| Comment | File | Size | Author |
|---|
Issue fork drupal-3384999
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:
- 3384999-pp-1-introduce-a
changes, plain diff MR !4699
Comments
Comment #2
mondrakeComment #4
mondrakeComment #5
daffie commentedComment #6
mondrakeAnother opportunity for such a method: disable square brackets replacement, and fix #3391222: Postgres Connection::quoteIdentifiers() should not quote square brackets when they are part of normal SQL
Comment #7
mondrakeComment #8
mondrakeRebased, now up for review.
Comment #9
mondrakeComment #10
smustgrave commentedCan we get a change record for the new method.
Comment #11
mondrakeAdded draft CR https://www.drupal.org/node/3402926
Comment #12
mondrakeComment #13
smustgrave commentedCR reads well, thanks for including the before/after examples I find those help a bunch.
Comment #14
mondrakerebased
Comment #15
mondrakeRebased, after #3159113: PostgreSQL Duplicate table error for alter index queries
Comment #16
quietone commentedI'm triaging RTBC issues. I read the IS and the comments. Thank you for the well written issue summary. I didn't find any unanswered questions.
I read the MR (not a code review). I enjoyed reading the very clear comments, especially in an area that I don't know. I thought some comments needed a little grammar tweak so I left some suggestions. However, one is a question. I am going to set need work for these points.
Comment #17
mondrakeRebased. Cannot find in the MR the comments mentioned in #16. Maybe a review not published?
Comment #18
quietone commented@mondrake, thanks for the ping. Yes, I did forget to publish my comments. There are here now.
Comment #19
mondrakeApplied the suggestions and answered the question. Back to RTBC, no code was changed.
Comment #20
daffie commented@mondrake said in #3405976-84: Transaction autocommit during shutdown relies on unreliable object destruction order (xdebug 3.3+ enabled):
As that issue is critical and this issue is a blocker for that issue, this issue should also be critical.
Comment #21
andypostPHP 8.4 just commited https://wiki.php.net/rfc/pdo_driver_specific_subclasses
Comment #22
mondrakeNeeds to be adjusted after #3410476: Add event for statement execution failure, on it.
Comment #23
mondrakeDone. Added dispatching of
StatementExecutionFailureEventfor the case of failure when executing SQL viaConnection::executeSql()Comment #24
mondrakeReplied to the inline comment. Please post something on the d.o. issue too when commenting the MR so that it becomes visible on the d.o. dashboard, otherwise it's just casual to get to know about the MR comment. Thanks!
Comment #25
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. 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 #27
mondrakeComment #28
smustgrave commentedIs it an issue that the mariaDB job failed?
Comment #29
mondrakeFailures seem rather unrelated,
PHP Fatal error: Constant expression contains invalid operations in core/modules/datetime_range/tests/src/Functional/DateRangeFieldTest.php on line 43
Comment #30
smustgrave commentedThat case seems all threads have been addressed. Seems to have gone through a few eyes and being crticial going to mark now.
Comment #31
mondrakeThis was set to Critical because of #3405976: Transaction autocommit during shutdown relies on unreliable object destruction order (xdebug 3.3+ enabled), but that got solved anyway differently in the meantime. Being a Feature request, I think "Normal" is appropriate at this stage.
Comment #32
alexpottThis is just a thought... but can we open another connection to do the DDL on MySQL so that we don't have to think what happens to in play transactions?
Comment #33
alexpottAnd in fact can we do that on all databases just so it's the same everywhere?
Comment #34
mondrakeI think I tried that somewhere. It doesn’t work. You get anyway an error on the DML transaction, even if you keep the DDL one separate.
In fact what happens when you perform a DDL is that you are changing (MySql) the internal information schema of the database. That is enough to cause an error 1412 - Table definition has changed, please retry transaction - on ANY other active transaction in the server in that moment.
That’s IMHO a sufficient argument to reconsider, for example, doing on-the-fly table creation. DDL should be really limited to maintenance time…
Comment #35
alexpott@mondrake doesn't that mean we should consider not doing this and coming up with something else for the auto-table generation stuff. It feels like this approach introduces more complexity but doesn't actually give us a solution because we just can't do what we want to do. Which is have transactions and DDL at the same time on all DBs... there's no way to make it work the same or even similarly on all db types. And doing a commit and then re-opening the transaction feels like a not good place to be.
Comment #36
mondrakeFWIW I do not really feel this as complex, honestly. It only affects database drivers - hopefully no one performs DDL from outer code, only via calling Schema methods.
Vice versa, not doing this makes life complicated for the potential mysqli driver.
And yes, #3410312-13: Flood database backend ::isAllowed() should call ::ensureTableExists() and #3410480: [META] Use events in Database Schema operations have started a discussion for doing something different for delivering table creation; that for me is independent and should happen anyway. But I see that quite far away for the moment.
Comment #37
alexpottThe CR says
But it does not detail what that proper handling is. Afaics it's not going to make MySQL actually do transactional DDL, I think what it does, is commit any open transactions, do the DDL, and then re-open the transactions. I think what it means by proper needs to be detailed on the CR. Also I think that as this still does not not ensure matching behaviour between the drivers we need to be very careful as to how we proceed. Putting back to needs review in order to get the updates to the CR and for more review and thought about this issue.
Comment #38
alexpottLooking at the test I think this issue will introduce new failure modes that I'm not sure about. If DDL occurs mid node save when do the post transaction callbacks fire? Are they moved from after the entity has completed being saved and are now triggered in the middle?
Comment #39
mondrake@alexpott
There's no way to make MySQL do transactional DDL, and we should not assume this will happen.
What happens today in HEAD is that once a DDL is executed in the middle of a transaction, MySQL (the database engine, not the driver) actually closes that transaction. How we react to that fact is what we can work on.
The MR does not do any commit/reopening of the transaction, only accounts for the fact that the client transaction got closed by ensuring the TransactionManager voids any open Drupal transaction object so that when they get destructed, they will not try to perform SQL transaction statements on the client connection anymore. IIRC, PDO MySql would fail explicitly if it tried that (because it keeps track of the state of the client transaction in the driver), Mysqli would fail silently (because it does not).
Now time has passed since I wrote this - I think yes, the MR would run the post transaction callbacks when the transaction manager detects the DDL (since the client transaction is closed at that point). If it's more desirable to wait until destruction of all voided Drupal transaction objects, I think we can still do that.
Comment #40
alexpottI think we do want to ensure that the moment the post transaction callback runs it consistent accross db types. And regardless of whether DDL has occurred. Interestingly I think that most things that cause DDL in a transaction should probably be moved to a post transaction callback :)
Comment #41
mondrakeWorking on #40.
Comment #42
mondrakeIf I am not mistaken, the behavior in #38
is confusing even in HEAD today, for MySQL -
$this->voidClientTransaction(), that is processing the callbacks, is called not upon commit of the root transaction but also in other cases.We are missing proper test coverage.
Comment #43
mondrakeFiled #3440848: Ensure post transaction callbacks are only at the end of the root Drupal transaction, needs to be solved before this.
Comment #44
mondrakeComment #45
daffie commented#3440848: Ensure post transaction callbacks are only at the end of the root Drupal transaction has landed.
Comment #46
mondrakeThe SQLite test failure is unrelated; should be fixed by #3458975: GenerateThemeTest::testContribStarterkitDevSnapshotWithGitNotInstalled fails on sqlite.
Comment #47
mondrakeI'd like to add more specific tests for the case of attempted rollback after a DDL statement is executed. On that.
Comment #48
mondrakeBack to NR.
Comment #49
smustgrave commentedMariaDB failure appears to be random.
Believe all feedback on this one has been addressed
Comment #50
mondrakeAdjusted draft CR to address #37.
Comment #51
larowlanIt feels like @alexpott still has doubts/open questions here. Not sure of what the correct status is, but I don't think this is RTBC just yet.
Comment #52
mondrake#40 was addressed by #3440848: Ensure post transaction callbacks are only at the end of the root Drupal transaction.
If open questions/doubts yet, can they please be summarized again.
From my POV, this is blocking #3398767: Allow returning explicitly to the prior nesting level in transactions (aka allow explicit COMMIT in Transaction objects) and the stream to remove commit-on-destruction that caused a lot of troubles recently with XDebug 3 unpredictably changing object destruction order, and #3259709: Create the database driver for MySQLi and the stream to introduce async querying.
Comment #53
alexpottPosted some more thoughts. My main concern now is the code duplication and way this is mixing responsibilities that were \Drupal\Core\Database\StatementWrapperIterator::execute() in the Connection class.
Comment #54
mondrakeAddressed comments and opened #3482452: Revise Truncate with regards to autoincrement reset and returned value for follow-up.
Comment #55
alexpottSo the code looks much better now - thanks @mondrake for addressing my feedback.
I guess my question now is what happens in head when we try to rollback after a DDL query on MySQL? We're adding a test here that shows with this change an exception is thrown. My concern is that this does not happen in HEAD and this behaviour change will have consequences.
Comment #56
mondrakeWorking on this.
Comment #57
mondrake@alexpott thanks for the question in #55
In HEAD a rollback after a DDL silently fails, without exception thrown. At that point in code the autocommit occurred already, and any DML change was committed.
IMHO trying a rollback on a transaction that was already committed SHOULD throw an exception, as obviously there's an inconsistency between the transaction state and the intent of the rollback, and by letting it fail silently we can incur in data corruption.
The test for the earlier version of the MR demonstrate that, at least in test covered HEAD core, we were not having the case of a rollback after a DDL, as no exceptions appeared during the test runs (besides those captured in the new tests).
However, rightly, throwing an exception means a behavior change, and we cannot assume that it will not affect contrib. So in the latest version of the MR I propose to trigger a deprecation for now, to be converted to an exception in drupal 12.
Comment #58
mondrakeLooking at the MR currently, I realize that we may avoid entirely surfacing a new API method:
Connection::executeDdlStatementis only calledI'll implement this in the next iteration of the MR.
Comment #59
mondrakeDone #58. Now the changes are really reduced to the Schema class only, and internal to it. Plus obviously tracking of the occurred autocommit in the transaction manager. It even turns out we do not need any change to Truncate, since there is safety code that uses the 'TRUNCATE' DDL statement only if the Drupal truncate happens OUTSIDE of a transaction.
Comment #60
alexpottHmmm... this is really really hard because we cannot possibly make our db drivers work the same way. I think we should not be doing a deprecation here - the rollback is silently failing at the moment. I think it would be better to log a warning that the transaction rollback has failed. And then in a follow-up we can consider turning this into an exception.
Comment #61
alexpott@mondrake the latest MR is getting really nice from a public API change perspective. Great work!
One more question related to #60 what happens to post transaction callbacks when the rollback fails. Ie. what happens in HEAD and what happens after this MR?
Comment #62
mondrakeAddressed #60, changed the deprecation to a user warning. Now to #61!
Comment #63
mondrake@alexpott re #61
I was expecting this question... :)
In HEAD it's a mess now, and test coverage is missing: pgsql and sqlite, allowing transactional ddl, process the callback with
$success = FALSEto indicate a rollback happened; mysql processes the callback with$success = TRUEto indicate a commit happened. Which, it's the reality in fact.So core databases currently react differently. See the attached test only patch,
testRootTransactionEndCallbackCalledAfterDdlAndRollbackForTransactionalDdlDatabaseandtestRootTransactionEndCallbackFailureUponDdlAndRollbackForNonTransactionalDdlDatabase.Good news is that the MR adds test coverage, and when we have an agreed solution we can adjust the test accordingly and have a lighthouse for the future.
EDIT - ah sorry, what the MR is currently doing is cleaning up the callbacks so they are NOT executed at all. fwiw. Sure we need to decide what to do and change accordingly.
Comment #64
alexpottSo for #63 for me I think the correct behaviour is psql and sqlite's but the problem is when the MySQL behaviour happens we're essentially broken. I'm really not sure about what we should do when the MySQL behaviour happens and we're trying to rollback a non existing transaction. The commit that has occurred means that the site is in an inconsistent state. I think the most consistent thing is to trigger the callbacks with success = false too...
Comment #65
mondrake#64 or alternatively, fail (silently for now) any attempt of rollback after a DDL in any database, commit the transaction on the spot on the attempt and run the callbacks with $success = TRUE.
I know it's on the opposite side of things but at least it would make the behaviour (and data) consistent across all dbs.
Comment #66
mondrakeSo, implemented #64 in the MR.
Comment #67
alexpottBut given allegedly trying to a rollback how does triggering with
$success = truemake sense? There's been an exception thrown during the transaction and we can't be sure what's happened.Comment #68
mondrakeWhat I mean is 'hey, you cannot rollback, we have already committed your transaction and will process your callbacks as per successful commit mode'. Note that callbacks are only processed upon destruction of all transaction objects in the stack.
Anyway, #64 is OK for me, implemented in the MR and a step forward vs current. This can also unblock working on explicit commit vs commit-on-destruct and maybe in the future once that will be in we may reconsider.
Comment #69
mondrakeReverted not relevant code that was emptying post-transaction callbacks.
Comment #70
alexpottBut $success is for post transaction callbacks is supposed to indicate whether it's been successful. It hasn't been even though on mysql there's been a partial commit.
Comment #71
mondrake#70 yes, from that perspective I agree. If $success === TRUE --> the transaction was successfully committed; $success === FALSE --> transaction was rolled back or failed for any other reason. Is that it? If so then I think we should document it clearly somehow - it's not ATM, unless I am mistaken.
Comment #72
alexpottWe have
In the docs for \Drupal\Core\Database\Transaction\TransactionManagerInterface::addPostTransactionCallback. If we're calling them after a rollback - even if the rollback cannot be performed because MySQL does not support transactional DDL then I think $success should be FALSE.
Comment #73
mondrakeOK we are on the same page. My point is to clarify what ‘transaction was successful’ means. It means, as we speak, ‘transaction was successfully committed’. Will adjust in next iteration of the MR.
Comment #74
mondrakeDocumented #72 #73, did also some more doc edits - the Transaction class was still hinting at dbs that do not support transactions which is no longer a thing in Drupal.
Comment #75
daffie commentedLooks good to me.
The CI pipeline is green for all supported databases.
For me it is RTBC.
Comment #76
alexpottI have one concern left - see question on MR. Anyone can rtbc once the question is answered.
Comment #77
mondrakeWorking on latest comment.
Comment #78
mondrakeComment #79
daffie commentedThe point of @alexpott has been addressed.
Back to RTBC.
Comment #80
mondrakeUpdated CR to reflect the new method is now a protected method of the Schema class.
Comment #81
mondrakeComment #82
alexpottGetting this into 11.2.x nice and early - before it is open. So it has a long time in the up-coming branch.
Committed 3004fc8 and pushed to 11.x. Thanks!