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

Issue fork drupal-3384999

Command icon 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

mondrake created an issue. See original summary.

mondrake’s picture

Issue summary: View changes

mondrake’s picture

Status: Active » Postponed
daffie’s picture

Title: [PP-1] Introduce a Connection::executeDdlStatement method » Introduce a Connection::executeDdlStatement method
Status: Postponed » Needs work
mondrake’s picture

Title: Introduce a Connection::executeDdlStatement method » [PP-2] Introduce a Connection::executeDdlStatement method
Status: Needs work » Postponed

Another 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

mondrake’s picture

Title: [PP-2] Introduce a Connection::executeDdlStatement method » [PP-1] Introduce a Connection::executeDdlStatement method
mondrake’s picture

Title: [PP-1] Introduce a Connection::executeDdlStatement method » Introduce a Connection::executeDdlStatement method
Status: Postponed » Needs review

Rebased, now up for review.

mondrake’s picture

Issue summary: View changes
smustgrave’s picture

Status: Needs review » Needs work
Issue tags: +Needs change record

Can we get a change record for the new method.

mondrake’s picture

Status: Needs work » Needs review
mondrake’s picture

Issue tags: -Needs change record
smustgrave’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: +Needs Review Queue Initiative

CR reads well, thanks for including the before/after examples I find those help a bunch.

mondrake’s picture

rebased

mondrake’s picture

quietone’s picture

Status: Reviewed & tested by the community » Needs work

I'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.

mondrake’s picture

Status: Needs work » Needs review

Rebased. Cannot find in the MR the comments mentioned in #16. Maybe a review not published?

quietone’s picture

@mondrake, thanks for the ping. Yes, I did forget to publish my comments. There are here now.

mondrake’s picture

Status: Needs review » Reviewed & tested by the community

Applied the suggestions and answered the question. Back to RTBC, no code was changed.

daffie’s picture

Priority: Normal » Critical

@mondrake said in #3405976-84: Transaction autocommit during shutdown relies on unreliable object destruction order (xdebug 3.3+ enabled):

We are currently blocked by #3384999: Introduce a Schema::executeDdlStatement method

As that issue is critical and this issue is a blocker for that issue, this issue should also be critical.

andypost’s picture

mondrake’s picture

Assigned: Unassigned » mondrake
Status: Reviewed & tested by the community » Needs work

Needs to be adjusted after #3410476: Add event for statement execution failure, on it.

mondrake’s picture

Assigned: mondrake » Unassigned
Status: Needs work » Needs review

Done. Added dispatching of StatementExecutionFailureEvent for the case of failure when executing SQL via Connection::executeSql()

mondrake’s picture

Replied 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!

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new90 bytes

The 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.

sakthi_dev made their first commit to this issue’s fork.

mondrake’s picture

Status: Needs work » Needs review
smustgrave’s picture

Is it an issue that the mariaDB job failed?

mondrake’s picture

Failures seem rather unrelated,

PHP Fatal error: Constant expression contains invalid operations in core/modules/datetime_range/tests/src/Functional/DateRangeFieldTest.php on line 43

smustgrave’s picture

Status: Needs review » Reviewed & tested by the community

That case seems all threads have been addressed. Seems to have gone through a few eyes and being crticial going to mark now.

mondrake’s picture

Priority: Critical » Normal

This 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.

alexpott’s picture

This 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?

alexpott’s picture

And in fact can we do that on all databases just so it's the same everywhere?

mondrake’s picture

I 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…

alexpott’s picture

@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.

mondrake’s picture

FWIW 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.

alexpott’s picture

Status: Reviewed & tested by the community » Needs review

The CR says

This will allow proper handling of transactions when executed for databases that do not support transactional DDL

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.

alexpott’s picture

Looking 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?

mondrake’s picture

@alexpott

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

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.

alexpott’s picture

I 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 :)

mondrake’s picture

Assigned: Unassigned » mondrake
Status: Needs review » Needs work

Working on #40.

mondrake’s picture

If I am not mistaken, the behavior in #38

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?

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.

mondrake’s picture

Status: Needs work » Postponed
mondrake’s picture

Assigned: mondrake » Unassigned
daffie’s picture

mondrake’s picture

Status: Needs work » Needs review
mondrake’s picture

Assigned: Unassigned » mondrake
Status: Needs review » Needs work

I'd like to add more specific tests for the case of attempted rollback after a DDL statement is executed. On that.

mondrake’s picture

Assigned: mondrake » Unassigned
Status: Needs work » Needs review

Back to NR.

smustgrave’s picture

Status: Needs review » Reviewed & tested by the community

MariaDB failure appears to be random.

Believe all feedback on this one has been addressed

mondrake’s picture

Adjusted draft CR to address #37.

larowlan’s picture

Status: Reviewed & tested by the community » Needs review

It 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.

mondrake’s picture

#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.

alexpott’s picture

Status: Needs review » Needs work

Posted 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.

mondrake’s picture

alexpott’s picture

So 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.

mondrake’s picture

Assigned: Unassigned » mondrake
Status: Needs review » Needs work

Working on this.

mondrake’s picture

@alexpott thanks for the question in #55

what happens in head when we try to rollback after a DDL query on MySQL?

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.

mondrake’s picture

Looking at the MR currently, I realize that we may avoid entirely surfacing a new API method: Connection::executeDdlStatement is only called

  • from Schema methods, and a protected method on the abstract class could do instead
  • from Truncate, and voiding directly the transaction is also possible with existing API

I'll implement this in the next iteration of the MR.

mondrake’s picture

Title: Introduce a Connection::executeDdlStatement method » Introduce a Schema::executeDdlStatement method
Issue summary: View changes
Status: Needs work » Needs review

Done #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.

alexpott’s picture

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.

Hmmm... 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.

alexpott’s picture

@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?

mondrake’s picture

Status: Needs review » Needs work

Addressed #60, changed the deprecation to a user warning. Now to #61!

mondrake’s picture

Assigned: mondrake » Unassigned
Status: Needs work » Needs review
StatusFileSize
new8.55 KB

@alexpott re #61

what happens to post transaction callbacks when the rollback fails. Ie. what happens in HEAD and what happens after this MR?

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 = FALSE to indicate a rollback happened; mysql processes the callback with $success = TRUE to indicate a commit happened. Which, it's the reality in fact.

So core databases currently react differently. See the attached test only patch, testRootTransactionEndCallbackCalledAfterDdlAndRollbackForTransactionalDdlDatabase and testRootTransactionEndCallbackFailureUponDdlAndRollbackForNonTransactionalDdlDatabase.

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.

alexpott’s picture

So 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...

mondrake’s picture

#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.

mondrake’s picture

So, implemented #64 in the MR.

alexpott’s picture

But given allegedly trying to a rollback how does triggering with $success = true make sense? There's been an exception thrown during the transaction and we can't be sure what's happened.

mondrake’s picture

What 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.

mondrake’s picture

Reverted not relevant code that was emptying post-transaction callbacks.

alexpott’s picture

But $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.

mondrake’s picture

#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.

alexpott’s picture

We have

   * An argument is passed to the callbacks that indicates whether the
   * transaction was successful or not.

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.

mondrake’s picture

Assigned: Unassigned » mondrake
Status: Needs review » Needs work

OK 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.

mondrake’s picture

Assigned: mondrake » Unassigned
Status: Needs work » Needs review

Documented #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.

daffie’s picture

Status: Needs review » Reviewed & tested by the community

Looks good to me.
The CI pipeline is green for all supported databases.
For me it is RTBC.

alexpott’s picture

Status: Reviewed & tested by the community » Needs review

I have one concern left - see question on MR. Anyone can rtbc once the question is answered.

mondrake’s picture

Assigned: Unassigned » mondrake
Status: Needs review » Needs work

Working on latest comment.

mondrake’s picture

Assigned: mondrake » Unassigned
Status: Needs work » Needs review
daffie’s picture

Status: Needs review » Reviewed & tested by the community

The point of @alexpott has been addressed.
Back to RTBC.

mondrake’s picture

Updated CR to reflect the new method is now a protected method of the Schema class.

mondrake’s picture

Issue summary: View changes
alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Getting 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!

  • alexpott committed 3004fc8a on 11.x
    Issue #3384999 by mondrake, alexpott, daffie, smustgrave, quietone:...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.