use std::time::Duration;
use tdsql::{Client, Config, Error, IsolationLevel};
const LOCK_TIMEOUT: u32 = 1222;
fn test_config() -> Config {
Config::new()
.host("localhost")
.port(1433)
.database("master")
.auth("sa", "YourStrong!Passw0rd")
.trust_cert()
}
async fn connect() -> Client {
Client::connect(&test_config())
.await
.expect("failed to connect; is SQL Server running on localhost:1433?")
}
async fn fresh_table(client: &mut Client, name: &str) {
client
.batch(&format!(
"IF OBJECT_ID('dbo.{name}', 'U') IS NOT NULL DROP TABLE dbo.{name};
CREATE TABLE dbo.{name} (id INT PRIMARY KEY, val INT NOT NULL)"
))
.await
.unwrap();
}
async fn seed(client: &mut Client, name: &str, rows: &[i32]) {
for id in rows {
client
.execute(
&format!("INSERT INTO dbo.{name} (id, val) VALUES (@P1, @P2)"),
&[id, id],
)
.await
.unwrap();
}
}
async fn ids(client: &mut Client, name: &str) -> Vec<i32> {
client
.query(&format!("SELECT id FROM dbo.{name} ORDER BY id"), &[])
.await
.unwrap()
.iter()
.map(|r| r.get::<i32, _>("id"))
.collect()
}
async fn count(client: &mut Client, name: &str) -> i32 {
client
.query_scalar(&format!("SELECT COUNT(*) FROM dbo.{name}"), &[])
.await
.unwrap()
}
async fn drop_table(client: &mut Client, name: &str) {
client
.batch(&format!("DROP TABLE IF EXISTS dbo.{name}"))
.await
.unwrap();
}
#[tokio::test]
#[ignore]
async fn uncommitted_insert_is_not_visible_to_another_client() {
let mut a = connect().await;
let mut b = connect().await;
fresh_table(&mut a, "cc_dirty").await;
b.batch("SET LOCK_TIMEOUT 2000").await.unwrap();
let mut tx = a.transaction().await.unwrap();
tx.execute(
"INSERT INTO dbo.cc_dirty (id, val) VALUES (@P1, @P2)",
&[&1i32, &10i32],
)
.await
.unwrap();
let err = b
.query_scalar::<i32>("SELECT COUNT(*) FROM dbo.cc_dirty", &[])
.await
.expect_err("a read-committed reader must not return the uncommitted row");
assert_eq!(
err.code(),
Some(LOCK_TIMEOUT),
"expected to be blocked by A's lock, got: {err}"
);
tx.rollback().await.unwrap();
assert_eq!(
count(&mut b, "cc_dirty").await,
0,
"a rolled-back insert must never be visible"
);
drop_table(&mut a, "cc_dirty").await;
}
#[tokio::test]
#[ignore]
async fn committed_insert_becomes_visible_to_another_client() {
let mut a = connect().await;
let mut b = connect().await;
fresh_table(&mut a, "cc_commit").await;
assert_eq!(count(&mut b, "cc_commit").await, 0);
let mut tx = a.transaction().await.unwrap();
tx.execute(
"INSERT INTO dbo.cc_commit (id, val) VALUES (@P1, @P2)",
&[&1i32, &10i32],
)
.await
.unwrap();
tx.execute(
"INSERT INTO dbo.cc_commit (id, val) VALUES (@P1, @P2)",
&[&2i32, &20i32],
)
.await
.unwrap();
tx.commit().await.unwrap();
assert_eq!(ids(&mut b, "cc_commit").await, vec![1, 2]);
drop_table(&mut a, "cc_commit").await;
}
#[tokio::test]
#[ignore]
async fn rollback_by_one_client_leaves_another_untouched() {
let mut a = connect().await;
let mut b = connect().await;
fresh_table(&mut a, "cc_rollback").await;
seed(&mut b, "cc_rollback", &[1]).await;
let mut tx = a.transaction().await.unwrap();
tx.execute(
"INSERT INTO dbo.cc_rollback (id, val) VALUES (@P1, @P2)",
&[&2i32, &20i32],
)
.await
.unwrap();
tx.rollback().await.unwrap();
assert_eq!(
ids(&mut b, "cc_rollback").await,
vec![1],
"B's committed row survives, A's rolled-back row is gone"
);
drop_table(&mut a, "cc_rollback").await;
}
#[tokio::test]
#[ignore]
async fn read_uncommitted_sees_another_clients_dirty_row() {
let mut a = connect().await;
let mut b = connect().await;
fresh_table(&mut a, "cc_dirty_read").await;
let mut tx_a = a.transaction().await.unwrap();
tx_a.execute(
"INSERT INTO dbo.cc_dirty_read (id, val) VALUES (@P1, @P2)",
&[&1i32, &10i32],
)
.await
.unwrap();
let mut tx_b = b
.transaction_with_isolation(IsolationLevel::ReadUncommitted)
.await
.unwrap();
let n: i32 = tx_b
.query_scalar("SELECT COUNT(*) FROM dbo.cc_dirty_read", &[])
.await
.unwrap();
assert_eq!(n, 1, "read uncommitted should see A's in-flight insert");
tx_b.rollback().await.unwrap();
tx_a.rollback().await.unwrap();
assert_eq!(
count(&mut b, "cc_dirty_read").await,
0,
"and the dirty row disappears once A rolls back"
);
drop_table(&mut a, "cc_dirty_read").await;
}
#[tokio::test]
#[ignore]
async fn write_write_contention_blocks_until_commit() {
let mut a = connect().await;
let mut b = connect().await;
fresh_table(&mut a, "cc_contend").await;
seed(&mut a, "cc_contend", &[1]).await;
b.batch("SET LOCK_TIMEOUT 2000").await.unwrap();
let mut tx = a.transaction().await.unwrap();
tx.execute("UPDATE dbo.cc_contend SET val = 100 WHERE id = 1", &[])
.await
.unwrap();
let err = b
.execute("UPDATE dbo.cc_contend SET val = 200 WHERE id = 1", &[])
.await
.expect_err("B must not overwrite a row A has locked");
assert_eq!(
err.code(),
Some(LOCK_TIMEOUT),
"expected to wait on A's write lock, got: {err}"
);
tx.commit().await.unwrap();
let affected = b
.execute("UPDATE dbo.cc_contend SET val = 200 WHERE id = 1", &[])
.await
.expect("the update should succeed once A has committed");
assert_eq!(affected, 1);
let val: i32 = a
.query_scalar("SELECT val FROM dbo.cc_contend WHERE id = 1", &[])
.await
.unwrap();
assert_eq!(val, 200, "B's write is the surviving one");
drop_table(&mut a, "cc_contend").await;
}
#[tokio::test]
#[ignore]
async fn deadlock_victim_is_reported_and_connections_survive() {
let mut a = connect().await;
let mut b = connect().await;
fresh_table(&mut a, "cc_deadlock").await;
seed(&mut a, "cc_deadlock", &[1, 2]).await;
let mut tx_a = a.transaction().await.unwrap();
let mut tx_b = b.transaction().await.unwrap();
tx_a.execute("UPDATE dbo.cc_deadlock SET val = 10 WHERE id = 1", &[])
.await
.unwrap();
tx_b.execute("UPDATE dbo.cc_deadlock SET val = 20 WHERE id = 2", &[])
.await
.unwrap();
let (ra, rb) = tokio::time::timeout(Duration::from_secs(30), async {
tokio::join!(
tx_a.execute("UPDATE dbo.cc_deadlock SET val = 11 WHERE id = 2", &[]),
tx_b.execute("UPDATE dbo.cc_deadlock SET val = 21 WHERE id = 1", &[]),
)
})
.await
.expect("the server should have picked a deadlock victim");
let victims: Vec<&Error> = [ra.as_ref(), rb.as_ref()]
.into_iter()
.filter_map(|r| r.err())
.collect();
assert_eq!(
victims.len(),
1,
"exactly one of the two statements should have been killed"
);
assert!(
victims[0].is_deadlock(),
"the victim should report error 1205, got: {}",
victims[0]
);
drop(tx_a);
drop(tx_b);
assert_eq!(a.query_scalar::<i32>("SELECT 1", &[]).await.unwrap(), 1);
assert_eq!(b.query_scalar::<i32>("SELECT 1", &[]).await.unwrap(), 1);
drop_table(&mut a, "cc_deadlock").await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[ignore]
async fn many_clients_commit_in_parallel() {
const CLIENTS: i32 = 8;
const ROWS_EACH: i32 = 5;
let mut setup = connect().await;
fresh_table(&mut setup, "cc_parallel").await;
let tasks: Vec<_> = (0..CLIENTS)
.map(|worker| {
tokio::spawn(async move {
let mut client = connect().await;
let mut tx = client.transaction().await.unwrap();
for row in 0..ROWS_EACH {
tx.execute(
"INSERT INTO dbo.cc_parallel (id, val) VALUES (@P1, @P2)",
&[&(worker * ROWS_EACH + row), &worker],
)
.await
.unwrap();
}
tx.commit().await.unwrap();
client.close().await.unwrap();
})
})
.collect();
for task in tasks {
task.await.expect("worker panicked");
}
assert_eq!(
ids(&mut setup, "cc_parallel").await,
(0..CLIENTS * ROWS_EACH).collect::<Vec<_>>(),
"every worker's committed rows should be present exactly once"
);
drop_table(&mut setup, "cc_parallel").await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[ignore]
async fn parallel_transactions_are_isolated_from_each_other() {
const CLIENTS: i32 = 4;
let mut setup = connect().await;
fresh_table(&mut setup, "cc_isolated").await;
setup
.batch("CREATE INDEX ix_cc_isolated_val ON dbo.cc_isolated (val)")
.await
.unwrap();
let tasks: Vec<_> = (0..CLIENTS)
.map(|worker| {
tokio::spawn(async move {
let mut client = connect().await;
let mut tx = client.transaction().await.unwrap();
tx.execute(
"INSERT INTO dbo.cc_isolated (id, val) VALUES (@P1, @P2)",
&[&worker, &worker],
)
.await
.unwrap();
let seen: i32 = tx
.query_scalar(
"SELECT COUNT(*) FROM dbo.cc_isolated WHERE val = @P1",
&[&worker],
)
.await
.unwrap();
assert_eq!(seen, 1, "worker {worker} should see only its own row");
tx.rollback().await.unwrap();
client.close().await.unwrap();
})
})
.collect();
for task in tasks {
task.await.expect("worker panicked");
}
assert_eq!(
count(&mut setup, "cc_isolated").await,
0,
"every worker rolled back, so the table should be empty"
);
drop_table(&mut setup, "cc_isolated").await;
}