sqlx icon indicating copy to clipboard operation
sqlx copied to clipboard

Isolation level support

Open tuxzz opened this issue 4 years ago • 25 comments

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?

tuxzz avatar Jul 05 '20 17:07 tuxzz

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

mehcode avatar Jul 12 '20 12:07 mehcode

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?

pythoneer avatar Sep 08 '20 14:09 pythoneer

Is there any workaround for this currently?

LucasPickering avatar Nov 10 '20 02:11 LucasPickering

I was wondering if we could start with something like .begin_with("BEGIN READ ONLY, READ COMMITTED").await (on the Connection trait and Pool) which returns a Transaction since that would cover most use cases without needing to figure out database-specifics.

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 get_autocommit().

abonander avatar Nov 13 '20 19:11 abonander

@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 avatar Nov 13 '20 20:11 ivan

@ivan And for different isolation levels create different pools and use the pool that is "holding" the right isolation level? Or is this global?

pythoneer avatar Nov 13 '20 22:11 pythoneer

I like the begin_with approach.

Diggsey avatar Mar 14 '21 17:03 Diggsey

I'd be happy to pick this up if we landed on an API design - is there a consensus among sqlx's maintainers at this point? @abonander

LukeMathWalker avatar Apr 22 '21 08:04 LukeMathWalker

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?

pythoneer avatar Apr 22 '21 09:04 pythoneer

You can set the isolation level by executing SET TRANSACTION after creating the transaction:

let mut tx = conn.begin().await?;
tx.execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;")
    .await?;

Documentation for SET TRANSACTION:

SQLite does not have SET TRANSACTION, and uses SERIALIZABLE isolation except with PRAGMA read_uncommitted turned on, as documented.

It may still be necessary to add a custom BEGIN as with SQLite's BEGIN IMMEDIATE as mentioned in #1182.

Hope this helps!

patrick-gu avatar Dec 29 '21 00:12 patrick-gu

@LukeMathWalker I think my proposal for .begin_with("<BEGIN statement with flags>") would be a good start: https://github.com/launchbadge/sqlx/issues/481#issuecomment-726991429

If nothing else it's something we can build on if it's not satisfactory.

abonander avatar Dec 29 '21 00:12 abonander

I've started to work on this 👍🏻

LukeMathWalker avatar Feb 04 '22 09:02 LukeMathWalker

Question: what should happen when the user tries to invoke begin_with within the context of an existing transaction? Right now sqlx determines, based on the current depth, if it should issue a BEGIN or a SAVEPOINT statement. Do we want to keep the same behaviour? Or do we want to error, given that this is probably not what the user expects? @abonander

LukeMathWalker avatar Feb 04 '22 09:02 LukeMathWalker

Question: what should happen when the user tries to invoke begin_with within the context of an existing transaction? Right now sqlx determines, based on the current depth, if it should issue a BEGIN or a SAVEPOINT statement. Do we want to keep the same behaviour? Or do we want to error, given that this is probably not what the user expects? @abonander

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?

saward avatar Jul 01 '22 16:07 saward

Using this in MySQL:

let mut tx = conn.begin().await?;
tx.execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;")
    .await?;

Gives:

1568 (25001): Transaction characteristics can't be changed while a transaction is in progress

Is there another way?

davidbnk avatar Mar 09 '23 02:03 davidbnk

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 BEGIN IMMEDIATE.

jclulow avatar Apr 16 '23 23:04 jclulow

Using this in MySQL:

let mut tx = conn.begin().await?;
tx.execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;")
    .await?;

Gives:

1568 (25001): Transaction characteristics can't be changed while a transaction is in progress

Is there another way?

For MySQL, the SET TRANSACTION LEVEL should be done prior to starting a transaction. The workaround I have for now is:

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 sqlx-core to your cargo dependencies.

jvliwanag avatar Jan 11 '24 21:01 jvliwanag

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 BEGIN IMMEDIATE.

Hi, have you found a solution to use BEGIN IMMEDIATE?

Reknij avatar Jan 29 '24 08:01 Reknij

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 BEGIN IMMEDIATE.

Hi, have you found a solution to use BEGIN IMMEDIATE?

Yes! I now use rusqlite and sea-query, where I have proper control over transactions.

jclulow avatar Jan 29 '24 09:01 jclulow