-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Isolation level support #481
Comments
It is currently missing, yea. I'm open to API ideas. // would rename existing ::transaction to ::with_transaction
let mut tx = conn.transaction() // -> TransactionOptions
.read_only()
.isolation_level(IsolationLevel::RepeatableRead)
.begin()
.await?; let mut tx = TransactionOptions::new(&mut conn)
.isolation_level(IsolationLevel::Snapshot)
.begin()
.await?; |
What are the pros and cons of the different approaches? Just from the first look at it i have no strong feelings one way or the other. The first one looks just fine and is maybe more discoverable? |
Is there any workaround for this currently? |
I was wondering if we could start with something like For sanity, we can assert that the connection was put into a transaction if it wasn't already. Both Postgres and MySQL tell us after executing a command whether the connection is in a transaction, and SQLite has |
@LucasPickering if you're using PostgreSQL, I believe you can do use sqlx::postgres::PgPoolOptions;
let pg_pool = PgPoolOptions::new()
.after_connect(|conn| Box::pin(async move {
conn.execute("SET default_transaction_isolation TO 'repeatable read'").await?;
Ok(())
}))
.connect(&uri).await?; |
@ivan And for different isolation levels create different pools and use the pool that is "holding" the right isolation level? Or is this global? |
I like the |
I'd be happy to pick this up if we landed on an API design - is there a consensus among |
By choosing the "first" approach we would change the current one from conn.transaction(|conn|Box::pin(async move {
query("select * from ..").fetch_all(conn).await
})).await into conn.with_transaction(|conn|Box::pin(async move {
query("select * from ..").fetch_all(conn).await
}), IsolationLevel::RepeatableRead).await right? |
You can set the isolation level by executing let mut tx = conn.begin().await?;
tx.execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;")
.await?; Documentation for SQLite does not have It may still be necessary to add a custom Hope this helps! |
@LukeMathWalker I think my proposal for If nothing else it's something we can build on if it's not satisfactory. |
I've started to work on this 👍🏻 |
Question: what should happen when the user tries to invoke |
Maybe it's too tricky given the way sqlx is written, but would this be a good place for a typestate pattern (http://cliffle.com/blog/rust-typestate/) so that calling begin_with after a transaction has begun is a compile time error? |
Using this in MySQL:
Gives:
Is there another way? |
Is there any plan to finish even a limited SQLite-specific version of this? It is, as far as I can tell, not possible to do safe read-modify-write in SQLite without the ability to use |
For MySQL, the use sqlx::Executor;
let mut con = self.pool.acquire().await?;
con.execute("SET TRANSACTION ISOLATION LEVEL READ_COMMITED");
let mut tx =
sqlx_core::transaction::Transaction::begin(MaybePoolConnection::PoolConnection(con))
.await?; Note that for this to work, you'd have to add |
Hi, have you found a solution to use |
Yes! I now use rusqlite and sea-query, where I have proper control over transactions. |
I have created the PR, which supports transactions with custom SQL. PR link. This generally work for my usecase (MariaDB), but probably requires more testing and it's unclear how to handle this for sqlite. Please comment on the PR if something specific should be addressed. |
API wise, the feature is more useful if it's generalized to not just be about isolation level. For example, I also set session variables per transaction and the workflow for this is cumbersome. Unrelated, but PoolConnection can be owned, yet Connection is a mutable borrow. So it's challenging to actually pass that tx around besides by explicit parameter to a function call. I found a way around all this but it involves accessing the underlying connection and basically not using the native transaction capability in sqlx at all. If creating a transaction could have it take ownership of the underlying executable connection that would be amazing for my use case (If I understand the API properly). Anyway... tldr; more flexibility in creating the transaction, whatever it may involve, is appreciated. Isolation level is a great start because it is pretty common. |
Just wanted to post my workaround for sqlite in case it's helpful. Basically I have a dummy table called "acquire_write_lock" with one row that I populate on pool creation and I write to it before making my reads. let mut transaction = pool.begin().await?;
sqlx::query!("UPDATE acquire_write_lock SET lock = TRUE WHERE id = 1")
.execute(transaction.as_mut())
.await?; |
Haha, I also do like this. |
For anyone else finding this issue because you're looking for SQLite's |
I need SQLite's |
After ignoring the database settings for a while the it came back to bite us when implementing the new extension paradigm. Sqlite started emitting database busy errors when a long running transaction was interrupted by some other write. This is because when you start a transaction in sqlite by default it occurs as DEFFERED. This means that sqlite does not try to lock th database until it comes across a write call. This means that some other thread could possibly come in and make a write call before the current transaction is finished. When this happens sqlite will see that the underlying data has changed and return an error for the open transaction. This is obviously bad because losing transactions is not a current situation we recover gracefully from. Instead it would be better if sqlite simply followed suite with other databases and just immediatley held a lock when opening a transaction. Which is actually able to be enabled by simply using the `BEGIN IMMEDIATE` statement when starting a transaction. But nothing is ever that simple. The library that we're using sqlx [does not yet support](launchbadge/sqlx#481) the ability to call `BEGIN IMMEDIATE` on transactions. Thus we had to come up with a hack. The hack being that when opening a transaction we immediately call a write to a dummy table as our first call. This should immediatley cause sqlite to hold a lock and more or less mimic the behavior of `BEGIN IMMEDIATE`.
There are four transaction isolation levels in SQL language and most of database support them. But I can't find how to set isolation level for transactions in sqlx.
Is it a missing feature?
The text was updated successfully, but these errors were encountered: