use std::fmt;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use keelson_core::Value;
use tokio::sync::Mutex;
use crate::error::ExecError;
use crate::executor::{ExecFuture, ExecResult, Executor, Family, Statement};
use crate::row::Row;
pub trait RawConnection: Send + fmt::Debug {
fn family(&self) -> Family;
fn fetch<'a>(
&'a mut self,
sql: &'a str,
args: Vec<Value>,
) -> ExecFuture<'a, Result<Vec<Row>, ExecError>>;
fn execute<'a>(
&'a mut self,
sql: &'a str,
args: Vec<Value>,
) -> ExecFuture<'a, Result<ExecResult, ExecError>>;
fn abandon(self: Box<Self>);
}
pub struct Transaction {
conn: Mutex<Option<Box<dyn RawConnection>>>,
family: Family,
opts: TxOptions,
finished: AtomicBool,
depth: AtomicU32,
}
impl fmt::Debug for Transaction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Transaction")
.field("family", &self.family)
.field("options", &self.opts)
.field("finished", &self.finished.load(Ordering::Relaxed))
.finish_non_exhaustive()
}
}
impl Transaction {
pub async fn begin_on(conn: Box<dyn RawConnection>) -> Result<Self, ExecError> {
Transaction::begin_on_with(conn, TxOptions::new()).await
}
pub async fn begin_on_with(
mut conn: Box<dyn RawConnection>,
opts: TxOptions,
) -> Result<Self, ExecError> {
let family = conn.family();
let plan = opts.plan(family)?;
for sql in &plan {
if let Err(e) = conn.execute(sql, Vec::new()).await {
conn.abandon();
return Err(e);
}
}
#[cfg(feature = "tracing")]
tracing::debug!(
target: "keelson",
family = family.as_str(),
isolation = opts.get_isolation().map(Isolation::as_sql),
"transaction begun"
);
Ok(Transaction {
conn: Mutex::new(Some(conn)),
family,
opts,
finished: AtomicBool::new(false),
depth: AtomicU32::new(0),
})
}
pub fn options(&self) -> TxOptions {
self.opts
}
pub async fn commit(self) -> Result<(), ExecError> {
self.end("COMMIT").await
}
pub async fn rollback(self) -> Result<(), ExecError> {
self.end("ROLLBACK").await
}
async fn end(&self, sql: &str) -> Result<(), ExecError> {
self.finished.store(true, Ordering::Relaxed);
let mut guard = self.conn.lock().await;
let mut conn = guard
.take()
.ok_or_else(|| ExecError::other("transaction connection missing"))?;
let res = conn.execute(sql, Vec::new()).await.map(|_| ());
#[cfg(feature = "tracing")]
tracing::debug!(
target: "keelson",
family = self.family.as_str(),
outcome = if sql == "COMMIT" { "commit" } else { "rollback" },
"transaction finished"
);
if res.is_err() {
conn.abandon();
}
res
}
pub async fn savepoint<T, E, F>(&self, f: F) -> Result<T, E>
where
F: AsyncFnOnce(&Transaction) -> Result<T, E>,
E: From<ExecError>,
{
let level = self.depth.fetch_add(1, Ordering::Relaxed) + 1;
let name = format!("keelson_sp_{level}");
if let Err(e) = self.raw(&format!("SAVEPOINT {name}")).await {
self.depth.fetch_sub(1, Ordering::Relaxed);
return Err(E::from(e));
}
let out = f(self).await;
let cleanup = match &out {
Ok(_) => self.raw(&format!("RELEASE SAVEPOINT {name}")).await,
Err(_) => match self.raw(&format!("ROLLBACK TO SAVEPOINT {name}")).await {
Ok(()) => self.raw(&format!("RELEASE SAVEPOINT {name}")).await,
Err(e) => Err(e),
},
};
self.depth.fetch_sub(1, Ordering::Relaxed);
match (out, cleanup) {
(Ok(v), Ok(())) => Ok(v),
(Ok(_), Err(e)) => Err(E::from(e)),
(Err(e), _) => Err(e),
}
}
async fn raw(&self, sql: &str) -> Result<(), ExecError> {
let mut guard = self.conn.lock().await;
let conn = guard
.as_mut()
.ok_or_else(|| ExecError::other("transaction already finished"))?;
conn.execute(sql, Vec::new()).await.map(|_| ())
}
}
impl Executor for Transaction {
fn family(&self) -> Family {
self.family
}
fn fetch(&self, stmt: Statement) -> ExecFuture<'_, Result<Vec<Row>, ExecError>> {
Box::pin(async move {
let Statement { sql, args, .. } = stmt;
let mut guard = self.conn.lock().await;
let conn = guard
.as_mut()
.ok_or_else(|| ExecError::other("transaction already finished"))?;
conn.fetch(&sql, args).await
})
}
fn execute(&self, stmt: Statement) -> ExecFuture<'_, Result<ExecResult, ExecError>> {
Box::pin(async move {
let Statement { sql, args, .. } = stmt;
let mut guard = self.conn.lock().await;
let conn = guard
.as_mut()
.ok_or_else(|| ExecError::other("transaction already finished"))?;
conn.execute(&sql, args).await
})
}
}
impl Drop for Transaction {
fn drop(&mut self) {
if self.finished.load(Ordering::Relaxed) {
return;
}
if let Ok(mut guard) = self.conn.try_lock()
&& let Some(conn) = guard.take()
{
conn.abandon();
#[cfg(feature = "tracing")]
tracing::debug!(
target: "keelson",
family = self.family.as_str(),
outcome = "abandoned",
"transaction dropped without commit; connection abandoned"
);
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Isolation {
ReadUncommitted,
ReadCommitted,
RepeatableRead,
Serializable,
}
impl Isolation {
pub fn as_sql(self) -> &'static str {
match self {
Isolation::ReadUncommitted => "READ UNCOMMITTED",
Isolation::ReadCommitted => "READ COMMITTED",
Isolation::RepeatableRead => "REPEATABLE READ",
Isolation::Serializable => "SERIALIZABLE",
}
}
}
impl fmt::Display for Isolation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_sql())
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Access {
ReadWrite,
ReadOnly,
}
impl Access {
pub fn as_sql(self) -> &'static str {
match self {
Access::ReadWrite => "READ WRITE",
Access::ReadOnly => "READ ONLY",
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SqliteBegin {
Deferred,
Immediate,
Exclusive,
}
impl SqliteBegin {
pub fn as_sql(self) -> &'static str {
match self {
SqliteBegin::Deferred => "DEFERRED",
SqliteBegin::Immediate => "IMMEDIATE",
SqliteBegin::Exclusive => "EXCLUSIVE",
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct TxOptions {
isolation: Option<Isolation>,
access: Option<Access>,
sqlite_begin: Option<SqliteBegin>,
}
impl TxOptions {
pub const fn new() -> Self {
TxOptions {
isolation: None,
access: None,
sqlite_begin: None,
}
}
pub const fn isolation(mut self, level: Isolation) -> Self {
self.isolation = Some(level);
self
}
pub const fn access(mut self, mode: Access) -> Self {
self.access = Some(mode);
self
}
pub const fn read_only(self) -> Self {
self.access(Access::ReadOnly)
}
pub const fn sqlite_begin(mut self, mode: SqliteBegin) -> Self {
self.sqlite_begin = Some(mode);
self
}
pub const fn get_isolation(self) -> Option<Isolation> {
self.isolation
}
pub const fn get_access(self) -> Option<Access> {
self.access
}
pub const fn get_sqlite_begin(self) -> Option<SqliteBegin> {
self.sqlite_begin
}
pub fn check(&self, family: Family) -> Result<(), ExecError> {
self.plan(family).map(|_| ())
}
pub fn plan(&self, family: Family) -> Result<Vec<String>, ExecError> {
match family {
Family::Postgres => self.plan_postgres(),
Family::MySql => self.plan_mysql(),
Family::Sqlite => self.plan_sqlite(),
}
}
fn plan_postgres(&self) -> Result<Vec<String>, ExecError> {
self.reject_sqlite_begin(Family::Postgres)?;
let mut sql = String::from("BEGIN");
if let Some(level) = self.isolation {
if level == Isolation::ReadUncommitted {
return Err(ExecError::other(
"PostgreSQL accepts READ UNCOMMITTED and then runs the transaction as \
READ COMMITTED — it has no weaker level. keelson refuses the request \
rather than hand back an isolation level the engine does not \
implement; ask for Isolation::ReadCommitted if that is the behaviour \
you want.",
));
}
sql.push_str(" ISOLATION LEVEL ");
sql.push_str(level.as_sql());
}
if let Some(mode) = self.access {
sql.push(' ');
sql.push_str(mode.as_sql());
}
Ok(vec![sql])
}
fn plan_mysql(&self) -> Result<Vec<String>, ExecError> {
self.reject_sqlite_begin(Family::MySql)?;
let mut out = Vec::with_capacity(2);
if let Some(level) = self.isolation {
out.push(format!(
"SET TRANSACTION ISOLATION LEVEL {}",
level.as_sql()
));
}
out.push(match self.access {
None => "BEGIN".to_owned(),
Some(mode) => format!("START TRANSACTION {}", mode.as_sql()),
});
Ok(out)
}
fn plan_sqlite(&self) -> Result<Vec<String>, ExecError> {
if let Some(level) = self.isolation
&& level != Isolation::Serializable
{
return Err(ExecError::other(format!(
"SQLite has exactly one isolation level — serializable — and cannot be \
weakened to {level}: a transaction asking for it would silently run \
serializable, which is a different set of permitted anomalies from the \
one you asked for. Ask for Isolation::Serializable (SQLite's only level, \
and what a plain BEGIN already gives), or use TxOptions::sqlite_begin for \
SQLite's own DEFERRED / IMMEDIATE / EXCLUSIVE begin modes."
)));
}
if self.access == Some(Access::ReadOnly) {
return Err(ExecError::other(
"SQLite has no per-transaction read-only mode. `PRAGMA query_only` is \
connection-level state, and keelson will not set connection state behind \
a pooled connection's back; open the database read-only instead \
(`sqlite://file?mode=ro`).",
));
}
Ok(vec![match self.sqlite_begin {
None => "BEGIN".to_owned(),
Some(mode) => format!("BEGIN {}", mode.as_sql()),
}])
}
fn reject_sqlite_begin(&self, family: Family) -> Result<(), ExecError> {
match self.sqlite_begin {
None => Ok(()),
Some(mode) => Err(ExecError::other(format!(
"SqliteBegin::{mode:?} is SQLite's own begin-mode vocabulary and has no \
meaning on {family}; it is refused rather than ignored. Use \
TxOptions::isolation and TxOptions::access there."
))),
}
}
}
impl From<Isolation> for TxOptions {
fn from(level: Isolation) -> Self {
TxOptions::new().isolation(level)
}
}
impl From<Access> for TxOptions {
fn from(mode: Access) -> Self {
TxOptions::new().access(mode)
}
}
impl From<SqliteBegin> for TxOptions {
fn from(mode: SqliteBegin) -> Self {
TxOptions::new().sqlite_begin(mode)
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TxConflict {
Serialization,
Deadlock,
LockTimeout,
Busy,
}
impl TxConflict {
pub fn as_str(self) -> &'static str {
match self {
TxConflict::Serialization => "serialization failure",
TxConflict::Deadlock => "deadlock",
TxConflict::LockTimeout => "lock timeout",
TxConflict::Busy => "database busy",
}
}
pub fn of(e: &ExecError) -> Option<TxConflict> {
match e {
ExecError::Driver(d) => d.downcast_ref::<TxConflictError>().map(|c| c.kind),
_ => None,
}
}
}
impl fmt::Display for TxConflict {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug)]
pub struct TxConflictError {
kind: TxConflict,
code: String,
message: String,
source: Option<Box<dyn std::error::Error + Send + Sync>>,
}
impl TxConflictError {
pub fn new(kind: TxConflict, code: impl Into<String>, message: impl Into<String>) -> Self {
TxConflictError {
kind,
code: code.into(),
message: message.into(),
source: None,
}
}
pub fn with_source(mut self, e: impl std::error::Error + Send + Sync + 'static) -> Self {
self.source = Some(Box::new(e));
self
}
pub fn kind(&self) -> TxConflict {
self.kind
}
pub fn code(&self) -> &str {
&self.code
}
pub fn into_exec_error(self) -> ExecError {
ExecError::Driver(Box::new(self))
}
}
impl fmt::Display for TxConflictError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} [{}]: {}", self.kind, self.code, self.message)
}
}
impl std::error::Error for TxConflictError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source
.as_ref()
.map(|e| &**e as &(dyn std::error::Error + 'static))
}
}
pub trait Begin: Executor {
fn begin(&self) -> ExecFuture<'_, Result<Transaction, ExecError>>;
}
impl<B: Begin + ?Sized> Begin for &B {
fn begin(&self) -> ExecFuture<'_, Result<Transaction, ExecError>> {
(**self).begin()
}
}
impl<B: Begin + ?Sized> Begin for Arc<B> {
fn begin(&self) -> ExecFuture<'_, Result<Transaction, ExecError>> {
(**self).begin()
}
}
pub trait BeginWith: Begin {
fn begin_with(&self, opts: TxOptions) -> ExecFuture<'_, Result<Transaction, ExecError>>;
}
impl<B: BeginWith + ?Sized> BeginWith for &B {
fn begin_with(&self, opts: TxOptions) -> ExecFuture<'_, Result<Transaction, ExecError>> {
(**self).begin_with(opts)
}
}
impl<B: BeginWith + ?Sized> BeginWith for Arc<B> {
fn begin_with(&self, opts: TxOptions) -> ExecFuture<'_, Result<Transaction, ExecError>> {
(**self).begin_with(opts)
}
}
pub trait BeginWithExt: BeginWith {
fn within_with<T, E, F>(
&self,
opts: TxOptions,
f: F,
) -> impl std::future::Future<Output = Result<T, E>>
where
F: AsyncFnOnce(&Transaction) -> Result<T, E>,
E: From<ExecError>,
{
async move {
let tx = self.begin_with(opts).await.map_err(E::from)?;
match f(&tx).await {
Ok(v) => {
tx.commit().await.map_err(E::from)?;
Ok(v)
}
Err(e) => {
let _ = tx.rollback().await;
Err(e)
}
}
}
}
}
impl<B: BeginWith + ?Sized> BeginWithExt for B {}
pub trait BeginExt: Begin {
fn within<T, E, F>(&self, f: F) -> impl std::future::Future<Output = Result<T, E>>
where
F: AsyncFnOnce(&Transaction) -> Result<T, E>,
E: From<ExecError>,
{
async move {
let tx = self.begin().await.map_err(E::from)?;
match f(&tx).await {
Ok(v) => {
tx.commit().await.map_err(E::from)?;
Ok(v)
}
Err(e) => {
let _ = tx.rollback().await;
Err(e)
}
}
}
}
}
impl<B: Begin + ?Sized> BeginExt for B {}
pub trait Atomic: Executor {
fn atomic<T, E, F>(&self, f: F) -> impl std::future::Future<Output = Result<T, E>>
where
F: AsyncFnOnce(&Transaction) -> Result<T, E>,
E: From<ExecError>;
}
impl<B: Begin + ?Sized> Atomic for B {
fn atomic<T, E, F>(&self, f: F) -> impl std::future::Future<Output = Result<T, E>>
where
F: AsyncFnOnce(&Transaction) -> Result<T, E>,
E: From<ExecError>,
{
self.within(f)
}
}
impl Atomic for Transaction {
fn atomic<T, E, F>(&self, f: F) -> impl std::future::Future<Output = Result<T, E>>
where
F: AsyncFnOnce(&Transaction) -> Result<T, E>,
E: From<ExecError>,
{
self.savepoint(f)
}
}
impl Atomic for &Transaction {
fn atomic<T, E, F>(&self, f: F) -> impl std::future::Future<Output = Result<T, E>>
where
F: AsyncFnOnce(&Transaction) -> Result<T, E>,
E: From<ExecError>,
{
(**self).savepoint(f)
}
}
pub type ExecHook =
Arc<dyn for<'a> Fn(&'a dyn Executor) -> ExecFuture<'a, Result<(), ExecError>> + Send + Sync>;
pub type ExecLoader = Arc<
dyn for<'a> Fn(&'a dyn Executor, &'a [Row]) -> ExecFuture<'a, Result<(), ExecError>>
+ Send
+ Sync,
>;
#[cfg(test)]
mod tests {
use std::sync::Mutex as StdMutex;
use super::*;
#[derive(Debug)]
struct Script {
family: Family,
log: Arc<StdMutex<Vec<String>>>,
abandoned: Arc<StdMutex<bool>>,
fail_from: Option<usize>,
}
impl RawConnection for Script {
fn family(&self) -> Family {
self.family
}
fn fetch<'a>(
&'a mut self,
sql: &'a str,
_args: Vec<Value>,
) -> ExecFuture<'a, Result<Vec<Row>, ExecError>> {
self.log.lock().unwrap().push(sql.to_owned());
Box::pin(async { Ok(Vec::new()) })
}
fn execute<'a>(
&'a mut self,
sql: &'a str,
_args: Vec<Value>,
) -> ExecFuture<'a, Result<ExecResult, ExecError>> {
let n = {
let mut log = self.log.lock().unwrap();
log.push(sql.to_owned());
log.len()
};
let fails = self.fail_from.is_some_and(|from| n > from);
Box::pin(async move {
if fails {
Err(ExecError::other("statement refused"))
} else {
Ok(ExecResult::default())
}
})
}
fn abandon(self: Box<Self>) {
*self.abandoned.lock().unwrap() = true;
}
}
type Log = Arc<StdMutex<Vec<String>>>;
type Abandoned = Arc<StdMutex<bool>>;
fn script_of(family: Family, fail_from: Option<usize>) -> (Script, Log, Abandoned) {
let log: Log = Arc::default();
let abandoned: Abandoned = Arc::default();
(
Script {
family,
log: log.clone(),
abandoned: abandoned.clone(),
fail_from,
},
log,
abandoned,
)
}
fn script() -> (Script, Log, Abandoned) {
script_of(Family::Sqlite, None)
}
#[tokio::test]
async fn commit_speaks_begin_then_commit_and_keeps_the_connection() {
let (conn, log, abandoned) = script();
let tx = Transaction::begin_on(Box::new(conn)).await.unwrap();
tx.execute(Statement::new("INSERT 1", vec![]))
.await
.unwrap();
tx.commit().await.unwrap();
assert_eq!(*log.lock().unwrap(), vec!["BEGIN", "INSERT 1", "COMMIT"]);
assert!(!*abandoned.lock().unwrap());
}
#[tokio::test]
async fn drop_without_commit_abandons_the_connection() {
let (conn, log, abandoned) = script();
let tx = Transaction::begin_on(Box::new(conn)).await.unwrap();
drop(tx);
assert_eq!(*log.lock().unwrap(), vec!["BEGIN"]);
assert!(
*abandoned.lock().unwrap(),
"the connection must not be reused"
);
}
#[derive(Debug)]
struct Handle(StdMutex<Option<Box<dyn RawConnection>>>);
impl Executor for Handle {
fn family(&self) -> Family {
Family::Sqlite
}
fn fetch(&self, _: Statement) -> ExecFuture<'_, Result<Vec<Row>, ExecError>> {
Box::pin(async { Err(ExecError::other("not the point of this fixture")) })
}
fn execute(&self, _: Statement) -> ExecFuture<'_, Result<ExecResult, ExecError>> {
Box::pin(async { Err(ExecError::other("not the point of this fixture")) })
}
}
impl Begin for Handle {
fn begin(&self) -> ExecFuture<'_, Result<Transaction, ExecError>> {
let conn = self.0.lock().unwrap().take();
Box::pin(async move {
Transaction::begin_on(conn.ok_or_else(|| ExecError::other("checked out twice"))?)
.await
})
}
}
async fn unit_of_work(db: impl Atomic, fail: bool) -> Result<(), ExecError> {
db.atomic(async |tx| {
tx.execute(Statement::new("WORK", vec![])).await?;
if fail {
return Err(ExecError::other("no"));
}
Ok(())
})
.await
}
#[tokio::test]
async fn one_helper_is_a_transaction_at_the_top_and_a_savepoint_inside_one() {
let (conn, log, _) = script();
let pool = Handle(StdMutex::new(Some(Box::new(conn))));
unit_of_work(&pool, false).await.unwrap();
assert_eq!(*log.lock().unwrap(), vec!["BEGIN", "WORK", "COMMIT"]);
let (conn, log, _) = script();
let tx = Transaction::begin_on(Box::new(conn)).await.unwrap();
unit_of_work(&tx, false).await.unwrap();
tx.commit().await.unwrap();
assert_eq!(
*log.lock().unwrap(),
vec![
"BEGIN",
"SAVEPOINT keelson_sp_1",
"WORK",
"RELEASE SAVEPOINT keelson_sp_1",
"COMMIT",
]
);
}
#[tokio::test]
async fn a_nested_failure_costs_the_block_and_not_the_callers_transaction() {
let (conn, log, _) = script();
let tx = Transaction::begin_on(Box::new(conn)).await.unwrap();
let err = unit_of_work(&tx, true).await.unwrap_err();
assert_eq!(err.to_string(), "no");
tx.execute(Statement::new("AFTER", vec![])).await.unwrap();
tx.commit().await.unwrap();
assert_eq!(
*log.lock().unwrap(),
vec![
"BEGIN",
"SAVEPOINT keelson_sp_1",
"WORK",
"ROLLBACK TO SAVEPOINT keelson_sp_1",
"RELEASE SAVEPOINT keelson_sp_1",
"AFTER",
"COMMIT",
]
);
}
#[tokio::test]
async fn savepoints_release_on_ok_and_roll_back_on_err() {
let (conn, log, _) = script();
let tx = Transaction::begin_on(Box::new(conn)).await.unwrap();
tx.savepoint(async |sp| {
sp.execute(Statement::new("GOOD", vec![])).await?;
Ok::<_, ExecError>(())
})
.await
.unwrap();
let err = tx
.savepoint(async |sp| {
sp.execute(Statement::new("BAD", vec![])).await?;
Err::<(), _>(ExecError::other("boom"))
})
.await
.unwrap_err();
assert_eq!(err.to_string(), "boom");
tx.savepoint(async |sp| {
sp.savepoint(async |sp2| {
sp2.execute(Statement::new("DEEP", vec![])).await?;
Ok::<_, ExecError>(())
})
.await
})
.await
.unwrap();
tx.commit().await.unwrap();
assert_eq!(
*log.lock().unwrap(),
vec![
"BEGIN",
"SAVEPOINT keelson_sp_1",
"GOOD",
"RELEASE SAVEPOINT keelson_sp_1",
"SAVEPOINT keelson_sp_1",
"BAD",
"ROLLBACK TO SAVEPOINT keelson_sp_1",
"RELEASE SAVEPOINT keelson_sp_1",
"SAVEPOINT keelson_sp_1",
"SAVEPOINT keelson_sp_2",
"DEEP",
"RELEASE SAVEPOINT keelson_sp_2",
"RELEASE SAVEPOINT keelson_sp_1",
"COMMIT",
]
);
}
#[test]
fn default_options_send_exactly_what_a_plain_begin_sends() {
for family in [Family::Postgres, Family::MySql, Family::Sqlite] {
assert_eq!(TxOptions::new().plan(family).unwrap(), vec!["BEGIN"]);
}
}
#[test]
fn postgres_puts_the_modes_on_begin_itself() {
assert_eq!(
TxOptions::from(Isolation::Serializable)
.plan(Family::Postgres)
.unwrap(),
vec!["BEGIN ISOLATION LEVEL SERIALIZABLE"]
);
assert_eq!(
TxOptions::new()
.isolation(Isolation::RepeatableRead)
.read_only()
.plan(Family::Postgres)
.unwrap(),
vec!["BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY"]
);
}
#[test]
fn mysql_sets_the_level_first_then_starts() {
assert_eq!(
TxOptions::from(Isolation::ReadCommitted)
.plan(Family::MySql)
.unwrap(),
vec!["SET TRANSACTION ISOLATION LEVEL READ COMMITTED", "BEGIN"]
);
assert_eq!(
TxOptions::new()
.isolation(Isolation::Serializable)
.read_only()
.plan(Family::MySql)
.unwrap(),
vec![
"SET TRANSACTION ISOLATION LEVEL SERIALIZABLE",
"START TRANSACTION READ ONLY",
]
);
}
#[test]
fn sqlite_gets_begin_modes_and_nothing_pretending_to_be_a_level() {
assert_eq!(
TxOptions::from(SqliteBegin::Immediate)
.plan(Family::Sqlite)
.unwrap(),
vec!["BEGIN IMMEDIATE"]
);
assert_eq!(
TxOptions::from(Isolation::Serializable)
.plan(Family::Sqlite)
.unwrap(),
vec!["BEGIN"]
);
}
#[test]
fn every_level_an_engine_would_only_pretend_to_honour_is_refused() {
let e = TxOptions::from(Isolation::ReadUncommitted)
.check(Family::Postgres)
.unwrap_err()
.to_string();
assert!(e.contains("READ UNCOMMITTED"), "{e}");
assert!(e.contains("READ COMMITTED"), "{e}");
for level in [
Isolation::ReadUncommitted,
Isolation::ReadCommitted,
Isolation::RepeatableRead,
] {
let e = TxOptions::from(level)
.check(Family::Sqlite)
.unwrap_err()
.to_string();
assert!(e.contains(level.as_sql()), "{e}");
assert!(e.contains("sqlite_begin"), "{e}");
}
let e = TxOptions::new()
.read_only()
.check(Family::Sqlite)
.unwrap_err()
.to_string();
assert!(e.contains("query_only"), "{e}");
for family in [Family::Postgres, Family::MySql] {
let e = TxOptions::from(SqliteBegin::Exclusive)
.check(family)
.unwrap_err()
.to_string();
assert!(e.contains("Exclusive"), "{e}");
assert!(e.contains(family.as_str()), "{e}");
}
for level in [
Isolation::ReadUncommitted,
Isolation::ReadCommitted,
Isolation::RepeatableRead,
Isolation::Serializable,
] {
TxOptions::from(level).check(Family::MySql).unwrap();
}
}
#[tokio::test]
async fn a_refused_option_never_reaches_the_wire_and_keeps_the_connection() {
let (conn, log, abandoned) = script_of(Family::Sqlite, None);
let err = Transaction::begin_on_with(Box::new(conn), Isolation::ReadCommitted.into())
.await
.unwrap_err();
assert!(err.to_string().contains("one isolation level"));
assert!(log.lock().unwrap().is_empty(), "nothing may be sent");
assert!(
!*abandoned.lock().unwrap(),
"an untouched connection goes back to its pool"
);
}
#[tokio::test]
async fn the_whole_plan_runs_on_the_transactions_own_connection_before_any_statement() {
let (conn, log, _) = script_of(Family::MySql, None);
let tx = Transaction::begin_on_with(
Box::new(conn),
TxOptions::new()
.isolation(Isolation::Serializable)
.read_only(),
)
.await
.unwrap();
assert_eq!(
tx.options(),
TxOptions::new()
.isolation(Isolation::Serializable)
.access(Access::ReadOnly)
);
tx.execute(Statement::new("SELECT 1", vec![]))
.await
.unwrap();
tx.commit().await.unwrap();
assert_eq!(
*log.lock().unwrap(),
vec![
"SET TRANSACTION ISOLATION LEVEL SERIALIZABLE",
"START TRANSACTION READ ONLY",
"SELECT 1",
"COMMIT",
]
);
}
#[tokio::test]
async fn a_half_applied_plan_abandons_the_connection() {
let (conn, log, abandoned) = script_of(Family::MySql, Some(1));
let err = Transaction::begin_on_with(Box::new(conn), Isolation::Serializable.into())
.await
.unwrap_err();
assert_eq!(err.to_string(), "statement refused");
assert_eq!(
*log.lock().unwrap(),
vec!["SET TRANSACTION ISOLATION LEVEL SERIALIZABLE", "BEGIN"]
);
assert!(
*abandoned.lock().unwrap(),
"the connection carries state nobody can see; it must not be reused"
);
}
#[test]
fn conflicts_are_matchable_rather_than_stringly_typed() {
let e = TxConflictError::new(
TxConflict::Serialization,
"40001",
"could not serialize access due to concurrent update",
)
.with_source(ExecError::other("driver"))
.into_exec_error();
assert_eq!(TxConflict::of(&e), Some(TxConflict::Serialization));
assert!(e.to_string().contains("40001"), "{e}");
assert!(std::error::Error::source(&e).is_some());
assert_eq!(TxConflict::of(&ExecError::RowNotFound), None);
}
#[test]
fn every_way_of_holding_a_scope_is_atomic() {
fn takes(_: impl Atomic) {}
fn prove(pool: Handle, shared: Arc<Handle>, erased: &dyn Begin, tx: &Transaction) {
takes(&pool);
takes(shared);
takes(erased);
takes(tx);
takes(pool);
}
let _ = prove;
}
#[test]
fn a_transaction_is_a_dyn_executor() {
fn takes(_: &dyn Executor) {}
fn prove(tx: &Transaction) {
takes(tx);
}
let _ = prove;
}
}