-
Notifications
You must be signed in to change notification settings - Fork 240
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: add support for nested transaction rollbacks via savepoints in sql #4375
base: main
Are you sure you want to change the base?
feat: add support for nested transaction rollbacks via savepoints in sql #4375
Conversation
6f375b8
to
b984ae4
Compare
This should resolve the issue described here prisma/prisma#15212 |
Would you consider this a breaking change compared to current behavior @LucianBuzzo? It smells a bit like that to me because current functionality would change. Agree? |
@janpio I think that the current behaviour is unexpected, so this would be a bug fix or new "feature". It's possible that people have made applications that rely on the current behaviour, but this is true of any bug IMO. |
Thanks for the explanation @LucianBuzzo, I get it now! I could also connect it to prisma/prisma#19346 then which is about the same problem. Did you find a bug report about the incorrect behavior? Optimally this issue would close that one so we have a closed bug in our release notes when we add this, that makes it clearer that this is a "breaking change" only in the context of that it fixes a bug. (Maybe you can create the issue if non exists yet!? Thanks.) (There are also the related prisma/prisma#9710 and prisma/prisma#12898, but I think they want to completely replace transactions with This should probably be documented in the future with a new "Nested interactive transactions" sub headline under https://www.prisma.io/docs/concepts/components/prisma-client/transactions#interactive-transactions? What would be good test cases for prisma/prisma? (Optimally those fail right now, but will succeed when this PR here is merged to show that this properly improves things) |
You can ignore all the failing "Driver Adapters" tests, and also the Vitess and MySQL on Linux ones - those are currently flaky. But these look relevant and need to be fixed:
|
This comment was marked as off-topic.
This comment was marked as off-topic.
Thanks for the review @janpio I'll get the code issues resolved. The issue prisma/prisma#19346 describes the exact problem, I just haven't referenced it as I wanted to have this PR reviewed first. For prisma/prisma#9710 and prisma/prisma#12898 this PR will resolve those issues, as long as they are using an SQL DB. describe('some test', () => {
it('should work', async () => {
try {
await prisma.$transaction(async (tx) => {
//...
expect(x).toBe(y)
throw new Error('rollback')
})
} catch (e) {
// ignore e
}
})
}) This is a similar pattern to how I discovered this issue myself - trying to test if a user is able to perform an operation when running Yates RBAC. I'll add some lines about nested transactions to the docs. As for a test case in https://github.com/prisma/prisma a simple way to do it would be to create a client extension that wraps all queries in a transaction and then test that the interactive transaction example in the docs (transferring money between accounts) works as expected. |
Wouldn't it then also be possible to just create a standalone test case that wraps multiple transactions, without the need to change how we run tests or use an extension? (I would assume the extension just does that automatically, but it should be possible to also express that explicitly and simpler)
Don't these suggest to use just savepoints for the actual tests, instead of normal transactions? I am still a bit unclear about the approach. |
From my reading of those issues, it seems that you could achieve the result they want using a nested transaction that utilises savepoints. I would let the original authors correct me if this is not the case though! |
And yes you could not use a client extension in the test case, I simply provided the example above as one possibility 👍 |
CI run is done, some more Clippy stuff to make it able to compile I guess: https://github.com/prisma/prisma-engines/actions/runs/6682077529/job/18158479734?pr=4375 (I also don't know any Rust, so unfortunately can't be of help here) |
(Can you do a fake PR to README.md adding a newline or some other minimal, non-intrusive change that I can merge easily? Then your commits in this PR will automatically run tests going forward - and you don't have to wait for me to click the button😆) |
da206e7
to
a8af640
Compare
@janpio I've also opened a PR with a failing test case for the prisma client here prisma/prisma#21678 |
Note: I brought the branch into the repo, it will release the engines automatically after a while |
Happened, version shared here: prisma/prisma#21678 (comment) |
b0bab65
to
a220cf5
Compare
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.
First round of comments, didn't review the whole PR yet. I'd also like to chat with you about the overall design and what the API on TS side would look like because I don't have the full context here.
quaint/src/connector/transaction.rs
Outdated
@@ -36,23 +43,26 @@ pub(crate) struct TransactionOptions { | |||
/// transaction object will panic. | |||
pub struct DefaultTransaction<'a> { | |||
pub inner: &'a dyn Queryable, | |||
pub depth: Arc<Mutex<i32>>, |
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'd still prefer an AtomicU32
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.
So something like this?
pub depth: Arc<Mutex<i32>>, | |
pub depth: AtomicU32, |
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
Ok(this) | ||
} | ||
} | ||
|
||
#[async_trait] | ||
impl<'a> Transaction for DefaultTransaction<'a> { | ||
async fn begin(&mut self) -> crate::Result<()> { |
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.
Why do these methods take self
by mutable reference? You have interior mutability in depth
so this seems unnecessarily restrictive.
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 think this might be leftover from the initial version of the PR, before I encapsulated depth
inside the Transaction. I'll fix this 👍
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.
If I switch from the Mutex to AtomicU32 I think these will need to be a mutable reference, unless I'm misunderstanding
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.
No, atomics have interior mutability
query-engine/connectors/mongodb-query-connector/src/interface/transaction.rs
Show resolved
Hide resolved
f787095
to
b877e71
Compare
Still watching this initiative closely and with great interest |
@luisgrases Now that #5001 is merged I think this PR is very close to completion 🚀 |
b8ca794
to
670e5ad
Compare
Update from my end: all tests are passing except on Mssql, where the default transaction methods are used instead of the Mssql implementation. If anyone has any insight into how to debug this issue please let me know! |
This is my first OSS contribution for a Rust project, so I'm sure I've made some stupid mistakes, but I think it should mostly work :) This change adds a mutable depth counter, that can track how many levels deep a transaction is, and uses savepoints to implement correct rollback behaviour. Previously, once a nested transaction was complete, it would be saved with `COMMIT`, meaning that even if the outer transaction was rolled back, the operations in the inner transaction would persist. With this change, if the outer transaction gets rolled back, then all inner transactions will also be rolled back. Different flavours of SQL servers have different syntax for handling savepoints, so I've had to add new methods to the `Queryable` trait for getting the commit and rollback statements. These are both parameterized by the current depth. I've additionally had to modify the `begin_statement` method to accept a depth parameter, as it will need to conditionally create a savepoint. When opening a transaction via the transaction server, you can now pass the prior transaction ID to re-use the existing transaction, incrementing the depth. Signed-off-by: Lucian Buzzo <[email protected]>
cf219c7
to
8f090ee
Compare
// MSSQL doesn't have a "RELEASE SAVEPOINT" equivalent, so in a nested | ||
// transaction we just continue onwards | ||
fn release_savepoint_statement(&self, _depth: u32) -> Cow<'static, str> { | ||
Cow::Owned("".to_string()) |
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.
Cow::Owned("".to_string()) | |
Cow::Borrowed("") |
This is my first OSS contribution for a Rust project, so I'm sure I've made some stupid mistakes, but I think it should mostly work :)
This change adds a mutable depth counter, that can track how many levels deep a transaction is, and uses savepoints to implement correct rollback behaviour. Previously, once a nested transaction was complete, it would be saved with
COMMIT
, meaning that even if the outer transaction was rolled back, the operations in the inner transaction would persist. With this change, if the outer transaction gets rolled back, then all inner transactions will also be rolled back.Different flavours of SQL servers have different syntax for handling savepoints, so I've had to add new methods to the
Queryable
trait for getting the commit and rollback statements. These are both parameterized by the current depth.I've additionally had to modify the
begin_statement
method to accept a depth parameter, as it will need to conditionally create a savepoint.Client PR: prisma/prisma#21678