Skip to content

Commit

Permalink
fix: fix isolation level issue in mssql
Browse files Browse the repository at this point in the history
  • Loading branch information
jacek-prisma committed Nov 26, 2024
1 parent d90ca61 commit 018eae3
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 20 deletions.
22 changes: 15 additions & 7 deletions quaint/src/single.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use crate::{
ast,
connector::{self, impl_default_TransactionCapable, ConnectionInfo, IsolationLevel, Queryable, TransactionCapable},
connector::{self, ConnectionInfo, IsolationLevel, Queryable, TransactionCapable},
};
use async_trait::async_trait;
use std::{fmt, sync::Arc};
Expand All @@ -16,7 +16,7 @@ use crate::connector::NativeConnectionInfo;
/// The main entry point and an abstraction over a database connection.
#[derive(Clone)]
pub struct Quaint {
inner: Arc<dyn Queryable>,
inner: Arc<dyn TransactionCapable>,
connection_info: Arc<ConnectionInfo>,
}

Expand All @@ -26,7 +26,15 @@ impl fmt::Debug for Quaint {
}
}

impl_default_TransactionCapable!(Quaint);
#[async_trait]
impl TransactionCapable for Quaint {
async fn start_transaction<'a>(
&'a self,
isolation: Option<IsolationLevel>,
) -> crate::Result<Box<dyn connector::Transaction + 'a>> {
self.inner.start_transaction(isolation).await
}
}

impl Quaint {
/// Create a new connection to the database. The connection string
Expand Down Expand Up @@ -137,28 +145,28 @@ impl Quaint {
let params = connector::SqliteParams::try_from(s)?;
let sqlite = connector::Sqlite::new(&params.file_path)?;

Arc::new(sqlite) as Arc<dyn Queryable>
Arc::new(sqlite) as Arc<dyn TransactionCapable>
}
#[cfg(feature = "mysql-native")]
s if s.starts_with("mysql") => {
let url = connector::MysqlUrl::new(url::Url::parse(s)?)?;
let mysql = connector::Mysql::new(url).await?;

Arc::new(mysql) as Arc<dyn Queryable>
Arc::new(mysql) as Arc<dyn TransactionCapable>
}
#[cfg(feature = "postgresql-native")]
s if s.starts_with("postgres") || s.starts_with("postgresql") => {
let url = connector::PostgresNativeUrl::new(url::Url::parse(s)?)?;
let tls_manager = connector::MakeTlsConnectorManager::new(url.clone());
let psql = connector::PostgreSql::new(url, &tls_manager).await?;
Arc::new(psql) as Arc<dyn Queryable>
Arc::new(psql) as Arc<dyn TransactionCapable>
}
#[cfg(feature = "mssql-native")]
s if s.starts_with("jdbc:sqlserver") | s.starts_with("sqlserver") => {
let url = connector::MssqlUrl::new(s)?;
let psql = connector::Mssql::new(url).await?;

Arc::new(psql) as Arc<dyn Queryable>
Arc::new(psql) as Arc<dyn TransactionCapable>
}
_ => unimplemented!("Supported url schemes: file or sqlite, mysql, postgresql or jdbc:sqlserver."),
};
Expand Down
28 changes: 15 additions & 13 deletions quaint/src/tests/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,27 +117,29 @@ async fn transactions_with_isolation_works(api: &mut dyn TestApi) -> crate::Resu

#[test_each_connector(tags("mssql"))]
async fn mssql_transaction_isolation_level(api: &mut dyn TestApi) -> crate::Result<()> {
// For Mssql, the isolation level is set on the connection and lives until it's changed.
// We need to test that the isolation level set per transaction is ignored.
let table = api.create_temp_table("id int, value int").await?;

// The connection default is "READ COMMITTED"
let conn = api.conn();

// Start a transaction with the default isolation level of "READ COMMITTED"
// and insert a row, but do not commit the transaction
let tx_a = conn.start_transaction(None).await?;
let conn_a = api.conn();
// Start a transaction with the default isolation level, which in tests is
// set to READ UNCOMMITED via the DB url and insert a row, but do not commit the transaction
let tx_a = conn_a.start_transaction(None).await?;
let insert = Insert::single_into(&table).value("value", 3).value("id", 4);
let rows_affected = tx_a.execute(insert.into()).await?;
assert_eq!(1, rows_affected);

// Start a transaction with an explicit isolation level of "READ UNCOMMITTED", which would ordinarily allow you to read
// rows that have been added but not committed.
let tx_b = conn.start_transaction(Some(IsolationLevel::ReadUncommitted)).await?;
let conn_b = api.create_additional_connection().await?;
// Start a transaction that explicitly sets the isolation level to "SNAPSHOT" and query the table
// expecting to see the old state.
let tx_b = conn_b.start_transaction(Some(IsolationLevel::Snapshot)).await?;
let res = tx_b.query(Select::from_table(&table).into()).await?;

// The query should return an empty result set because the isolation level is set on the connection
assert_eq!(0, res.len());

// Start a transaction without an explicit isolation level, it should be run with the default
// again, which is set to READ UNCOMMITED here.
let tx_c = conn_b.start_transaction(None).await?;
let res = tx_c.query(Select::from_table(&table).into()).await?;
assert_eq!(1, res.len());

Ok(())
}

Expand Down
4 changes: 4 additions & 0 deletions quaint/src/tests/test_api/mssql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ impl<'a> MsSql<'a> {
let names = Generator::default();
let conn = Quaint::new(&CONN_STR).await?;

// snapshot isolation enables us to test isolation levels easily
conn.raw_cmd(&format!("ALTER DATABASE tempdb SET ALLOW_SNAPSHOT_ISOLATION ON",))

Check failure on line 25 in quaint/src/tests/test_api/mssql.rs

View workflow job for this annotation

GitHub Actions / clippy linting

useless use of `format!`
.await?;

Ok(Self { names, conn })
}
}
Expand Down

0 comments on commit 018eae3

Please sign in to comment.