use crate::conformance::{Leaf, Sql, Ty, drop_tables};
use crate::contract;
use crate::{Dialect, RelError, RelationalBackend, RelationalTx, Value};
use persistant_core::choose::Eligibility;
use persistant_core::{Config, Reason};
use std::time::Instant;
pub use persistant_core::bench::{
Comparison, Environment, Left, Outcome, Ranked, SpeedParseError as ParseError, SpeedRow,
SpeedTable, Stats, compare,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum Workload {
Insert,
SelectByKey,
Update,
Transaction,
Savepoint,
UpsertWhere,
}
impl Workload {
pub const ALL: [Workload; 6] = [
Workload::Insert,
Workload::SelectByKey,
Workload::Update,
Workload::Transaction,
Workload::Savepoint,
Workload::UpsertWhere,
];
pub fn id(self) -> &'static str {
match self {
Workload::Insert => "speed.rel.insert",
Workload::SelectByKey => "speed.rel.select_by_key",
Workload::Update => "speed.rel.update",
Workload::Transaction => "speed.rel.transaction",
Workload::Savepoint => "speed.rel.savepoint",
Workload::UpsertWhere => "speed.rel.upsert_where",
}
}
pub fn required(self) -> Leaf {
match self {
Workload::Insert | Workload::SelectByKey | Workload::Update => Leaf::Values,
Workload::Transaction => Leaf::Transaction,
Workload::Savepoint => Leaf::Savepoint,
Workload::UpsertWhere => Leaf::UpsertWhere,
}
}
}
pub async fn run<R: RelationalBackend>(
target: &R,
config: &Config,
env: &Environment,
workloads: &[Workload],
iterations: u32,
) -> Vec<SpeedRow> {
let mut out = Vec::with_capacity(workloads.len());
for w in workloads.iter().copied() {
let leaf = w.required();
let outcome = match contract::check(target, &[leaf]).await {
Eligibility::Eligible => match time(target, w, iterations).await {
Ok(Some(s)) => Outcome::Measured(s),
Ok(None) => Outcome::Excluded {
leaf: leaf.id().to_string(),
reason: Reason::Untested,
note: "no operations were timed".to_string(),
},
Err(e) => Outcome::Excluded {
leaf: leaf.id().to_string(),
reason: Reason::Missing,
note: format!("the workload failed on a backend that passed the leaf: {e}"),
},
},
Eligibility::Excluded(ex) => {
let e = &ex[0];
Outcome::Excluded {
leaf: e.leaf.clone(),
reason: e.reason,
note: e.note.clone(),
}
}
};
out.push(SpeedRow {
workload: w.id().to_string(),
backend: target.backend().to_string(),
version: target.version().to_string(),
config: config.clone(),
env: env.clone(),
outcome,
});
}
out
}
const TABLE: &str = "persistant_speed_rel";
const SEED_ROWS: i64 = 100;
async fn time<R: RelationalBackend>(
b: &R,
w: Workload,
iterations: u32,
) -> Result<Option<Stats>, RelError> {
let s = Sql(b.dialect());
drop_tables(b, &[TABLE]).await?;
b.execute(
&format!(
"CREATE TABLE {TABLE} ({}, n BIGINT NOT NULL, t {} NOT NULL){}",
s.pk("id"),
s.ty(Ty::Text),
s.tail()
),
&[],
)
.await?;
let result = timed_loop(b, &s, w, iterations).await;
let _ = drop_tables(b, &[TABLE]).await;
result
}
async fn timed_loop<R: RelationalBackend>(
b: &R,
s: &Sql,
w: Workload,
iterations: u32,
) -> Result<Option<Stats>, RelError> {
let ins = format!(
"INSERT INTO {TABLE} (id, n, t) VALUES ({}, {}, {})",
s.p(1),
s.p(2),
s.p(3)
);
let sel = format!("SELECT n, t FROM {TABLE} WHERE id = {}", s.p(1));
let upd = format!("UPDATE {TABLE} SET n = n + 1 WHERE id = {}", s.p(1));
let row = |id: i64| [Value::Int(id), Value::Int(id), Value::Text("x".repeat(64))];
let tail = if matches!(s.0, Dialect::MySql | Dialect::MariaDb) {
format!("ON DUPLICATE KEY UPDATE n = IF({TABLE}.n < VALUES(n), VALUES(n), {TABLE}.n)")
} else {
format!("ON CONFLICT (id) DO UPDATE SET n = EXCLUDED.n WHERE {TABLE}.n < EXCLUDED.n")
};
let ups = format!(
"INSERT INTO {TABLE} (id, n, t) VALUES ({}, {}, {}) {tail}",
s.p(1),
s.p(2),
s.p(3)
);
if matches!(
w,
Workload::SelectByKey | Workload::Update | Workload::UpsertWhere
) {
for id in 0..SEED_ROWS {
b.execute(&ins, &row(id)).await?;
}
}
let mut us: Vec<u64> = Vec::with_capacity(iterations as usize);
macro_rules! timed {
($e:expr) => {{
let t = Instant::now();
let r = $e;
us.push(u64::try_from(t.elapsed().as_nanos()).unwrap_or(u64::MAX));
r
}};
}
let mut sp_tx = if w == Workload::Savepoint {
Some(b.begin(crate::Isolation::ReadCommitted).await?)
} else {
None
};
for i in 0..i64::from(iterations) {
match w {
Workload::Insert => {
timed!(b.execute(&ins, &row(i)).await?);
}
Workload::SelectByKey => {
let rows = timed!(b.query(&sel, &[Value::Int(i % SEED_ROWS)]).await?);
if rows.len() != 1 {
return Err(RelError::Backend(
"select by key did not return its row".to_string(),
));
}
}
Workload::Update => {
let n = timed!(b.execute(&upd, &[Value::Int(i % SEED_ROWS)]).await?);
if n != 1 {
return Err(RelError::Backend(
"update by key did not touch one row".to_string(),
));
}
}
Workload::Transaction => {
let t = Instant::now();
let mut tx = b.begin(crate::Isolation::ReadCommitted).await?;
tx.execute(&ins, &row(i)).await?;
tx.commit().await?;
us.push(u64::try_from(t.elapsed().as_nanos()).unwrap_or(u64::MAX));
}
Workload::Savepoint => {
let tx = sp_tx.as_mut().expect("opened above for this workload");
let t = Instant::now();
tx.execute("SAVEPOINT persistant_speed_sp", &[]).await?;
tx.execute(&ins, &row(i)).await?;
tx.execute("ROLLBACK TO SAVEPOINT persistant_speed_sp", &[])
.await?;
us.push(u64::try_from(t.elapsed().as_nanos()).unwrap_or(u64::MAX));
}
Workload::UpsertWhere => {
let id = i % SEED_ROWS;
let r = [
Value::Int(id),
Value::Int(1000 + i),
Value::Text("x".repeat(64)),
];
timed!(b.execute(&ups, &r).await?);
let got = b
.query(
&format!("SELECT n FROM {TABLE} WHERE id = {}", s.p(1)),
&[Value::Int(id)],
)
.await?;
if got.first().and_then(|r| r.int(0)) != Some(1000 + i) {
return Err(RelError::Backend(
"the upsert did not take the update branch".to_string(),
));
}
}
}
}
if let Some(tx) = sp_tx {
tx.rollback().await?;
}
Ok(Stats::from_samples(us))
}