use crate::client::Client;
use crate::command::Command;
use crate::dataset::DataSet;
use crate::error::{Error, Result};
use crate::row::Row;
use crate::value::{FromSql, ToSql};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum IsolationLevel {
#[default]
Unspecified,
ReadUncommitted,
ReadCommitted,
RepeatableRead,
Serializable,
Snapshot,
}
impl From<IsolationLevel> for tiberius::IsolationLevel {
fn from(level: IsolationLevel) -> Self {
match level {
IsolationLevel::Unspecified => tiberius::IsolationLevel::Unspecified,
IsolationLevel::ReadUncommitted => tiberius::IsolationLevel::ReadUncommitted,
IsolationLevel::ReadCommitted => tiberius::IsolationLevel::ReadCommitted,
IsolationLevel::RepeatableRead => tiberius::IsolationLevel::RepeatableRead,
IsolationLevel::Serializable => tiberius::IsolationLevel::Serializable,
IsolationLevel::Snapshot => tiberius::IsolationLevel::Snapshot,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Scope {
Root,
Savepoint(String),
}
#[derive(Debug)]
pub struct Transaction<'a> {
client: &'a mut Client,
scope: Scope,
done: bool,
}
impl<'a> Transaction<'a> {
pub(crate) async fn begin(client: &'a mut Client, level: IsolationLevel) -> Result<Self> {
client.begin_transaction(level).await?;
Ok(Self {
client,
scope: Scope::Root,
done: false,
})
}
pub async fn commit(mut self) -> Result<()> {
self.done = true;
match &self.scope {
Scope::Root => self.client.commit_transaction().await,
Scope::Savepoint(_) => Ok(()),
}
}
pub async fn rollback(mut self) -> Result<()> {
self.done = true;
match &self.scope {
Scope::Root => self.client.rollback_transaction().await,
Scope::Savepoint(name) => {
let name = name.clone();
self.client.rollback_to_savepoint(&name).await
}
}
}
pub async fn transaction(&mut self) -> Result<Transaction<'_>> {
let name = format!("_tdsql_sp{}", self.client.next_savepoint_id());
self.savepoint(name).await
}
pub async fn savepoint(&mut self, name: impl Into<String>) -> Result<Transaction<'_>> {
let name = name.into();
validate_savepoint_name(&name)?;
self.client.save_transaction(&name).await?;
Ok(Transaction {
client: &mut *self.client,
scope: Scope::Savepoint(name),
done: false,
})
}
pub async fn query(&mut self, sql: &str, params: &[&dyn ToSql]) -> Result<Vec<Row>> {
self.client.query(sql, params).await
}
pub async fn query_one(&mut self, sql: &str, params: &[&dyn ToSql]) -> Result<Row> {
self.client.query_one(sql, params).await
}
pub async fn query_opt(&mut self, sql: &str, params: &[&dyn ToSql]) -> Result<Option<Row>> {
self.client.query_opt(sql, params).await
}
pub async fn query_scalar<T: FromSql>(
&mut self,
sql: &str,
params: &[&dyn ToSql],
) -> Result<T> {
self.client.query_scalar(sql, params).await
}
pub async fn execute(&mut self, sql: &str, params: &[&dyn ToSql]) -> Result<u64> {
self.client.execute(sql, params).await
}
pub async fn batch(&mut self, sql: &str) -> Result<Vec<Row>> {
self.client.batch(sql).await
}
pub async fn batch_dataset(&mut self, sql: &str) -> Result<DataSet> {
self.client.batch_dataset(sql).await
}
pub async fn query_dataset(&mut self, command: &Command) -> Result<DataSet> {
self.client.query_dataset(command).await
}
pub async fn execute_command(&mut self, command: &Command) -> Result<u64> {
self.client.execute_command(command).await
}
}
impl Drop for Transaction<'_> {
fn drop(&mut self) {
if self.done {
return;
}
match &self.scope {
Scope::Root => self.client.enqueue_rollback(),
Scope::Savepoint(name) => self.client.enqueue_rollback_to(name.clone()),
}
}
}
fn validate_savepoint_name(name: &str) -> Result<()> {
let valid = !name.is_empty()
&& name.len() <= 32
&& name
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
&& name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
if valid {
Ok(())
} else {
Err(Error::InvalidSavepointName(name.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_plain_identifiers() {
assert!(validate_savepoint_name("sp1").is_ok());
assert!(validate_savepoint_name("_tdsql_sp0").is_ok());
assert!(validate_savepoint_name("Before_Risky_Bit").is_ok());
}
#[test]
fn rejects_injection_and_malformed_names() {
for bad in [
"",
"1sp",
"sp name",
"sp;DROP TABLE users",
"sp'--",
"sp-1",
"spécial",
&"x".repeat(33),
] {
assert!(
validate_savepoint_name(bad).is_err(),
"should have rejected {bad:?}"
);
}
}
#[test]
fn rejects_at_the_length_boundary() {
assert!(validate_savepoint_name(&"x".repeat(32)).is_ok());
assert!(validate_savepoint_name(&"x".repeat(33)).is_err());
}
#[test]
fn maps_isolation_levels() {
use tiberius::IsolationLevel as T;
assert_eq!(T::from(IsolationLevel::Unspecified), T::Unspecified);
assert_eq!(T::from(IsolationLevel::ReadCommitted), T::ReadCommitted);
assert_eq!(T::from(IsolationLevel::Serializable), T::Serializable);
assert_eq!(T::from(IsolationLevel::Snapshot), T::Snapshot);
}
#[test]
fn defaults_to_unspecified() {
assert_eq!(IsolationLevel::default(), IsolationLevel::Unspecified);
}
}