pub(crate) mod advisory;
pub(crate) mod lease;
pub(crate) mod row_lock;
use std::sync::Arc;
use ruststream_sqlx_dialect::{
Advisory, ClaimShape, Dialect, Lease, RowLock, Statement, StatementError, TableSpec,
};
#[doc(hidden)]
pub trait FormOn<D> {
fn erase(dialect: &Arc<D>) -> FormDialect;
}
#[doc(hidden)]
#[derive(Debug, Clone, Copy, Default)]
pub struct RowLockForm;
#[doc(hidden)]
#[derive(Debug, Clone, Copy, Default)]
pub struct LeaseForm;
#[doc(hidden)]
#[derive(Debug, Clone, Copy, Default)]
pub struct AdvisoryForm;
impl<D: RowLock + 'static> FormOn<D> for RowLockForm {
fn erase(dialect: &Arc<D>) -> FormDialect {
FormDialect::RowLock(Arc::clone(dialect) as Arc<dyn RowLock>)
}
}
impl<D: Lease + 'static> FormOn<D> for LeaseForm {
fn erase(dialect: &Arc<D>) -> FormDialect {
FormDialect::Lease(Arc::clone(dialect) as Arc<dyn Lease>)
}
}
impl<D: Advisory + 'static> FormOn<D> for AdvisoryForm {
fn erase(dialect: &Arc<D>) -> FormDialect {
FormDialect::Advisory(Arc::clone(dialect) as Arc<dyn Advisory>)
}
}
#[doc(hidden)]
#[derive(Debug, Clone)]
pub enum FormDialect {
RowLock(Arc<dyn RowLock>),
Lease(Arc<dyn Lease>),
Advisory(Arc<dyn Advisory>),
}
impl FormDialect {
pub(crate) fn dialect(&self) -> &dyn Dialect {
match self {
Self::RowLock(dialect) => dialect.as_ref(),
Self::Lease(dialect) => dialect.as_ref(),
Self::Advisory(dialect) => dialect.as_ref(),
}
}
pub(crate) fn lease(&self) -> Option<&dyn Lease> {
match self {
Self::Lease(dialect) => Some(dialect.as_ref()),
Self::RowLock(_) | Self::Advisory(_) => None,
}
}
pub(crate) fn advisory(&self) -> Option<&dyn Advisory> {
match self {
Self::Advisory(dialect) => Some(dialect.as_ref()),
Self::RowLock(_) | Self::Lease(_) => None,
}
}
pub(crate) fn claim(
&self,
spec: &TableSpec<'_>,
shape: ClaimShape,
) -> Result<Statement, StatementError> {
match self {
Self::RowLock(dialect) => dialect.lock_claim(spec, shape),
Self::Lease(dialect) => dialect.lease_claim(spec, shape),
Self::Advisory(dialect) => dialect.advisory_claim(spec),
}
}
pub(crate) fn begin_claim(
&self,
spec: &TableSpec<'_>,
) -> Result<Option<&'static str>, StatementError> {
let opening = self.dialect().begin(spec.opening())?;
Ok(match self {
Self::RowLock(_) => opening,
Self::Lease(dialect) => dialect.begin_lease_claim(),
Self::Advisory(_) => None,
})
}
}
#[cfg(test)]
pub(in crate::inbox) mod tests {
use ruststream_sqlx_dialect::{Column, Form, TableSpec};
pub(in crate::inbox) const JOBS: TableSpec<'static> =
TableSpec::new("jobs", Column::new("job_id"), Form::RowLock).payload(Column::new("body"));
#[cfg(feature = "postgres")]
pub(in crate::inbox) mod reshaped {
use std::num::NonZeroUsize;
use std::sync::Arc;
use ruststream_sqlx_dialect::{
Advisory, ClaimShape, Column, Dialect, Form, Isolation, KeyPart, Lease, Opening,
Postgres, RowLock, Statement, StatementError, TableName, TableSpec,
};
use crate::inbox::FormDialect;
#[derive(Debug)]
pub(in crate::inbox) struct Reshaped {
pub(in crate::inbox) parts: usize,
pub(in crate::inbox) writes_lease: bool,
}
fn marked(
trait_name: &str,
claim: Result<Statement, StatementError>,
) -> Result<Statement, StatementError> {
let claim = claim?;
Ok(Statement::new(
format!("/* {trait_name} */ {}", claim.sql()),
claim.params().iter().copied(),
))
}
impl Dialect for Reshaped {
fn name(&self) -> &'static str {
"reshaped"
}
fn quote_into(&self, ident: &str, out: &mut String) {
Postgres.quote_into(ident, out);
}
fn placeholder_into(&self, index: NonZeroUsize, out: &mut String) {
Postgres.placeholder_into(index, out);
}
fn fetch(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
Postgres.fetch(spec)
}
fn ack(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
Postgres.ack(spec)
}
fn retry(&self, spec: &TableSpec<'_>) -> Result<Option<Statement>, StatementError> {
Postgres.retry(spec)
}
fn retry_after(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
Postgres.retry_after(spec)
}
fn discard(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
Postgres.discard(spec)
}
fn dead_letter_group(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
Postgres.dead_letter_group(spec)
}
fn dead_letter_table(
&self,
spec: &TableSpec<'_>,
target: TableName<'_>,
) -> Result<Vec<Statement>, StatementError> {
let mut moves = Postgres.dead_letter_table(spec, target)?;
moves.resize(self.parts, moves[0].clone());
Ok(moves)
}
fn insert(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
Postgres.insert(spec)
}
fn begin(&self, opening: Opening) -> Result<Option<&'static str>, StatementError> {
match opening {
Opening::Default => Ok(Some("BEGIN /* Dialect */")),
Opening::Isolation(Isolation::Serializable) => {
Ok(Some("BEGIN ISOLATION LEVEL SERIALIZABLE /* Dialect */"))
}
_ => Err(StatementError::UnsupportedOpening {
dialect: self.name(),
opening: opening.name(),
}),
}
}
}
impl RowLock for Reshaped {
fn lock_claim(
&self,
spec: &TableSpec<'_>,
shape: ClaimShape,
) -> Result<Statement, StatementError> {
marked("RowLock", Postgres.lock_claim(spec, shape))
}
}
impl Lease for Reshaped {
fn lease_claim(
&self,
spec: &TableSpec<'_>,
shape: ClaimShape,
) -> Result<Statement, StatementError> {
marked("Lease", Postgres.lease_claim(spec, shape))
}
fn extend(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
Postgres.extend(spec)
}
fn stamp(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
Postgres.stamp(spec)
}
fn claim_writes_lease(&self) -> bool {
self.writes_lease
}
fn begin_lease_claim(&self) -> Option<&'static str> {
Some("BEGIN /* Lease */")
}
}
impl Advisory for Reshaped {
fn advisory_claim(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
marked("Advisory", Postgres.advisory_claim(spec))
}
fn lock(&self) -> Option<Statement> {
Postgres.lock()
}
fn unlock(&self) -> Option<Statement> {
Postgres.unlock()
}
fn take(
&self,
spec: &TableSpec<'_>,
shape: ClaimShape,
) -> Result<Vec<Statement>, StatementError> {
let mut takes = Postgres.take(spec, shape)?;
takes.resize(self.parts, takes[0].clone());
Ok(takes)
}
}
pub(in crate::inbox) const LEASED: TableSpec<'static> = TableSpec::new(
"jobs",
Column::new("job_id"),
Form::Lease(Column::new("locked_until")),
)
.payload(Column::new("body"));
pub(in crate::inbox) const KEY: &[KeyPart<'static>] =
&[KeyPart::Literal("jobs-"), KeyPart::Column("job_id")];
pub(in crate::inbox) const ADVISED: TableSpec<'static> =
TableSpec::new("jobs", Column::new("job_id"), Form::Advisory(KEY))
.attempt(Column::new("attempt"))
.payload(Column::new("body"));
pub(in crate::inbox) const RESHAPED: Reshaped = Reshaped {
parts: 1,
writes_lease: true,
};
pub(in crate::inbox) fn form_of(dialect: Reshaped, spec: &TableSpec<'_>) -> FormDialect {
match spec.form() {
Form::RowLock => FormDialect::RowLock(Arc::new(dialect)),
Form::Lease(_) => FormDialect::Lease(Arc::new(dialect)),
_ => FormDialect::Advisory(Arc::new(dialect)),
}
}
#[test]
fn each_form_opens_its_claim_with_the_statement_of_its_trait() -> Result<(), StatementError>
{
assert_eq!(
form_of(RESHAPED, &super::JOBS).begin_claim(&super::JOBS)?,
Some("BEGIN /* Dialect */")
);
let serializable = super::JOBS.isolation(Isolation::Serializable);
assert_eq!(
form_of(RESHAPED, &serializable).begin_claim(&serializable)?,
Some("BEGIN ISOLATION LEVEL SERIALIZABLE /* Dialect */")
);
let leased = LEASED.isolation(Isolation::Serializable);
assert_eq!(
form_of(RESHAPED, &leased).begin_claim(&leased)?,
Some("BEGIN /* Lease */")
);
let advised = ADVISED.isolation(Isolation::Serializable);
assert_eq!(form_of(RESHAPED, &advised).begin_claim(&advised)?, None);
Ok(())
}
#[test]
fn a_table_at_an_opening_its_dialect_lacks_is_refused_whatever_its_form() {
for spec in [super::JOBS, LEASED, ADVISED] {
let uncommitted = spec.isolation(Isolation::ReadUncommitted);
assert_eq!(
form_of(RESHAPED, &uncommitted).begin_claim(&uncommitted),
Err(StatementError::UnsupportedOpening {
dialect: "reshaped",
opening: "isolation `read_uncommitted`",
}),
"{}",
spec.form().name()
);
}
}
}
#[cfg(feature = "mysql")]
mod on_mysql {
use std::sync::Arc;
use ruststream_sqlx_dialect::{
Column, Form, Isolation, Mode, MySql, StatementError, TableSpec,
};
use crate::inbox::BuiltIn;
use crate::inbox::form::{FormOn, LeaseForm, RowLockForm};
const LEASED: TableSpec<'static> = TableSpec::new(
"jobs",
Column::new("job_id"),
Form::Lease(Column::new("locked_until")),
)
.payload(Column::new("body"));
fn built_in() -> Arc<BuiltIn<sqlx::MySql>> {
Arc::new(BuiltIn::new(MySql))
}
#[test]
fn a_row_lock_claim_opens_at_the_tables_isolation() -> Result<(), StatementError> {
let form = <RowLockForm as FormOn<BuiltIn<sqlx::MySql>>>::erase(&built_in());
let cases = [
(
super::JOBS,
"SET TRANSACTION ISOLATION LEVEL READ COMMITTED; START TRANSACTION",
),
(
super::JOBS.isolation(Isolation::RepeatableRead),
"SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; START TRANSACTION",
),
(
super::JOBS.isolation(Isolation::Serializable),
"SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; START TRANSACTION",
),
];
for (spec, begin) in cases {
assert_eq!(
form.begin_claim(&spec)?,
Some(begin),
"{:?}",
spec.opening()
);
}
Ok(())
}
#[test]
fn a_lease_claim_keeps_read_committed_and_still_refuses_a_mode()
-> Result<(), StatementError> {
let form = <LeaseForm as FormOn<BuiltIn<sqlx::MySql>>>::erase(&built_in());
let serializable = LEASED.isolation(Isolation::Serializable);
assert_eq!(
form.begin_claim(&serializable)?,
Some("SET TRANSACTION ISOLATION LEVEL READ COMMITTED; START TRANSACTION")
);
let immediate = LEASED.mode(Mode::Immediate);
assert_eq!(
form.begin_claim(&immediate),
Err(StatementError::UnsupportedOpening {
dialect: "mysql",
opening: "mode `immediate`",
})
);
Ok(())
}
}
#[cfg(all(feature = "any", feature = "sqlite"))]
mod on_any {
use std::sync::Arc;
use ruststream_sqlx_dialect::{Column, Form, Isolation, Mode, StatementError, TableSpec};
use sqlx::Any;
use crate::inbox::database::built_in::SQLITE_BACKEND;
use crate::inbox::form::{FormOn, LeaseForm};
use crate::inbox::{AnyDialect, BuiltIn};
const LEASED: TableSpec<'static> = TableSpec::new(
"jobs",
Column::new("job_id"),
Form::Lease(Column::new("locked_until")),
)
.payload(Column::new("body"));
#[test]
fn a_sqlite_backend_refuses_an_isolation_level_whatever_the_form()
-> Result<(), StatementError> {
let picked = AnyDialect::of(SQLITE_BACKEND).expect("the sqlite feature is on");
let form = <LeaseForm as FormOn<BuiltIn<Any>>>::erase(&Arc::new(BuiltIn::new(picked)));
let serializable = LEASED.isolation(Isolation::Serializable);
assert_eq!(
form.begin_claim(&serializable),
Err(StatementError::UnsupportedOpening {
dialect: "sqlite",
opening: "isolation `serializable`",
})
);
assert_eq!(
form.begin_claim(&LEASED.mode(Mode::Exclusive))?,
Some("BEGIN IMMEDIATE")
);
Ok(())
}
}
}