-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(database): add support for nested transactions using savepoints #8833
base: 4.6
Are you sure you want to change the base?
Conversation
4b94ec2
to
753765d
Compare
Maybe this should be targeted at 4.6, but IMHO since it fixes nested transactions for when using phpunit which uses the SQLite3 driver by default, and nested transactions are clearly documented, this may also be considered a fix? |
367d5d9
to
47db608
Compare
Thank you for sending this PR. This is not a bug fix. Read the documentation. The current behavior is clearly documented.
The problems:
Please fix the problems. If you have any questions, feel free to ask. |
Ah thanks for your quick and detailed feedback! I clearly overread the important part here with the outermost transaction. I'll definitly rebase and target 4.6 now. I also tested the MySQLi driver with mariadb on debian bookworm with the same SAVEPOINT (https://mariadb.com/kb/en/savepoint/) Queries as above, and they seem to be compatible. I was thinking about creating a trait like In order not to introduce a breaking change and be backwards-compatible, should I add a configuration option like Or do you prefer not cluttering? Thanks for your help! |
I might be able to implement this as well for PostgreSQL which seems to suppor the same savepoint syntax, but I have no access or experience to the other database systems. |
I've now implemented most of this on the BaseConnection directly, while also providing a configuration option with the very verbose name Nested transactions should now be supported by mysqli, sqlite3 as well as the postgre driver |
I'll now wait for some feedback, before cleaning up everything to hopefully make it ready to merge. |
whoops, missed the MockConnection |
this should fix most if not all of the linting mistakes |
app/Config/Database.php
Outdated
@@ -185,6 +185,7 @@ class Database extends Config | |||
'datetime' => 'Y-m-d H:i:s', | |||
'time' => 'H:i:s', | |||
], | |||
'enableNestedTransactions' => true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this config key is good for this change. I would like to hear others' opinions. > @codeigniter4/core-team @codeigniter4/database-team
Because we have already the transException()
method to change CI4's transaction behavior.
https://codeigniter4.github.io/CodeIgniter4/database/transactions.html#throwing-exceptions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Such a change is indeed problematic. I don't know how to reconcile exception handling with savepoints.
Anyway, maybe we should use something like enableSavepointTransactions
? Also, can we refer users to the same section in the user guide describing the differences between nested transactions and save point transactions?
Last thing. I haven't checked from which versions, but SQLSRV and OCI8 also support savepoints - possibly with different syntax, but that doesn't change the fact that support should be provided.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that one came to mind too. In the same vein I personally don't like the current behaviour of BaseConnection::query
, which rerolls ALL (outer) transactions when a single nested query fails, when DBDebug = true
+ transException
is set:
https://github.com/codeigniter4/CodeIgniter4/blob/develop/system/Database/BaseConnection.php#L658-L670
I don't like it as when the exception gets passed on, it might still be caught by code calling query() and get rerolled there.
If the point is to make sure we don't leave transactions in limbo, we should do this on a cleanup step (__destruct() or something).
I'm currently using a bad hack + another implementation of this PR & seperate driver classes with savepoint support for MySQLi + SQLite
in my project https://github.com/mkuettel/codeigniter4-services/blob/main/src/TransactionService.php#L55
There i set transDepth to 0 using reflection to after starting a transaction to avoid the complete rollback behaviour in the while loop with DBDebug = true + transException = true.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addionally, I actually like having DBDebug on, so more exceptions are thrown generally, so database errors not just get ignored, or the database even gets in a weird unexpected state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@michalsn What am I missing regarding the differences between nested transactions and savepoints, https://sqlite.org/lang_savepoint.html, look for "There are several ways of thinking about the RELEASE command", the first one sounds like a nested transaction, the second one is the way i like to think about it. Also they seem to be equivalent in some way?
Regarding SQLsrv + OCI8, I don't have access to those systems (except here in CI, when you guys run it), so this will be very labourious and spammy in here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have docker-compose.yaml
for all supported databases:
https://github.com/codeigniter4/devkit/tree/develop/src/docker
82c06cd
to
2f4bd3a
Compare
I've now added some more tests and the Although i removed the setting, because the BaseConnection just checks whether a property exists, its still settable via a BaseConnection option as before. If this is not desired, how should I avoid this? Additionally I deliberately only talk about transaction nesting instead of savepoints in the Context of BaseConnection as of now, as it technically doesn't need to know how nesting is implemented. Is this useful or just nonsense? Some of the tests may not check for the correct behaviour, regarding error handling and exception handling in the context of transactions. What should the interactions between DBDebug, transException and transNest look like? Addtionally there are probably some more details with each driver automatically |
I've added some fixes for the tests and I'm getting a clearer picture now. But I've been thinking about another thing: strict mode. Strict mode is turned on by default, which causes everything to be rolled back when it is enabled as soon as a single transaction (even nested at a deep level) causes all future ones to be rolled back. Is the solution to apply strict to a transaction layer, meaning that future transactions in the same or lower level automatically fail, when it is set? We'd have to keep track of transStatus (+ maybe transFailure) as well as the transStrict setting on a per "transaction layer" basis. |
0e1cdd4
to
ed7127b
Compare
I think we don't need to avoid it. |
It is recommended to always set DBDebug to true. During transactions, by default CI4 does not throw exceptions. |
Test whether we can start two nested transactions ... 1. ... complete them and see the result in the database 2. ... rollback the outer one after completing the inner one, and see that the result is _not_ in the database. 3. ... roll back the inner one and see if the result of the outer one is in the database after completing.
This option will be false by default, so the previous documented behaviour of CodeIgniter gets kept & to avoid introducing a breaking change. CodeIgniter allows for nesting transactions, but only ever cared about, actually committing/rolling back the most outer one. This commit keeps this behaviour by default, but adds the database driver option 'enableNestedTransactions' to the BaseConnection. When set to true, codeigniter now will call _transBeginNested()/_transCommitNested()/_transRollbackNested() on the respective driver implemented, when further transactions are nested into the outermost transaction.
By using SAVEPOINT SQL statements to manage transactions deeper than 1 level, it's possible to provide full nested transaction semantics for the following drivers: - SQLite3: - https://sqlite.org/lang_transaction.html - Savepoints: https://sqlite.org/lang_savepoint.html - MySQLi - https://mariadb.com/kb/en/savepoint/ - Postgre - https://www.postgresql.org/docs/current/sql-savepoint.html The implementation starts a transaction when transBegin() is first called as before. But then all nested transactions will be managed by creating, releasing (commiting) or rolling back savepoint. Only when the outermost transaction is completed a COMMIT or ROLLBACK will be executed, either committing the transaction and all inner _committed_ transactions, or rolling everything back. This feature is currently disabled for SQLSrv & OCI8 and they keep on. working with just 1 transaction layer & nested ones getting ignored. Revert "feat(sqlite3): add support for nested transactions using SAVEPOINT" This reverts commit 47db608. feat(database): use savepoints for nested transactions for MySQLi, SQLite3 + Postgre driver
…is enabled, plus an extreme example
…ust transRollback()
…are implemented now
1254e83
to
6970c48
Compare
Thanks a lot for your clarifications, this helps a lot. I've added Savepoints to the missing drivers OCI8 and SQLSRV as well. I'd be glad if you could run the workflows again, to know what to fix. |
👋 Hi, @mkuettel! |
It's possible to provide nested transaction semantics when using SQLite3
by using
SAVEPOINT
statements to manage transactions deeper than 1 level.The implementation starts a transaction when
transBegin()
is first called as before.But then all nested transactions will be managed by creating,
releasing (commiting) or rolling back savepoint.
Only when the outermost transaction is completed a COMMIT or ROLLBACK
will be executed, either committing the transaction and all inner
committed transactions, or rolling everything back.
Description
Explain what you have changed, and why.
Checklist: