#[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
use std::collections::BTreeSet;
use std::future::Future;
#[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
use std::sync::{Mutex, PoisonError};
#[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
use ruststream_sqlx_dialect::{self as dialect, OutboxDialect};
use ruststream_sqlx_dialect::{StatementError, TableSpec};
#[cfg(feature = "any")]
use sqlx::Any;
#[cfg(feature = "any")]
use sqlx::AnyConnection;
#[cfg(any(
feature = "postgres",
feature = "mysql",
feature = "sqlite",
feature = "any"
))]
use sqlx::Arguments as _;
use sqlx::{Database, Error, FromRow};
#[cfg(feature = "mysql")]
use sqlx::{MySql, MySqlConnection};
#[cfg(feature = "postgres")]
use sqlx::{PgConnection, Postgres};
#[cfg(feature = "sqlite")]
use sqlx::{Sqlite, SqliteConnection};
#[doc(hidden)]
#[derive(Debug, Clone, Copy)]
pub struct Statements {
pub(super) fetch: &'static str,
pub(super) mark: &'static str,
pub(super) recover: &'static str,
}
#[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
impl Statements {
fn of(dialect: &impl OutboxDialect, spec: &TableSpec<'_>) -> Result<Self, StatementError> {
Ok(Self {
fetch: intern(dialect.outbox_fetch(spec)?.sql()),
mark: intern(dialect.outbox_mark(spec)?.sql()),
recover: intern(dialect.outbox_recover(spec)?.sql()),
})
}
}
#[doc(hidden)]
#[derive(Debug, Clone, Copy, Default)]
pub struct Defaults {
#[cfg(feature = "postgres")]
postgres: Option<Statements>,
#[cfg(feature = "mysql")]
mysql: Option<Statements>,
#[cfg(feature = "sqlite")]
sqlite: Option<Statements>,
}
#[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
fn intern(text: &str) -> &'static str {
static TEXTS: Mutex<BTreeSet<&'static str>> = Mutex::new(BTreeSet::new());
let mut texts = TEXTS.lock().unwrap_or_else(PoisonError::into_inner);
if let Some(interned) = texts.get(text) {
return interned;
}
let interned: &'static str = Box::leak(text.into());
texts.insert(interned);
interned
}
#[cfg(all(feature = "any", feature = "postgres"))]
const POSTGRES_BACKEND: &str = "PostgreSQL";
#[cfg(all(feature = "any", feature = "mysql"))]
const MYSQL_BACKEND: &str = "MySQL";
#[cfg(all(feature = "any", feature = "sqlite"))]
const SQLITE_BACKEND: &str = "SQLite";
#[cfg(feature = "any")]
impl Defaults {
fn for_backend(&self, backend: &str) -> Option<&Statements> {
match backend {
#[cfg(feature = "postgres")]
POSTGRES_BACKEND => self.postgres.as_ref(),
#[cfg(feature = "mysql")]
MYSQL_BACKEND => self.mysql.as_ref(),
#[cfg(feature = "sqlite")]
SQLITE_BACKEND => self.sqlite.as_ref(),
_ => None,
}
}
}
pub trait OutboxDatabase: Database {
#[doc(hidden)]
fn defaults(spec: &TableSpec<'_>) -> Result<Defaults, StatementError>;
#[doc(hidden)]
fn statements<'d>(conn: &Self::Connection, defaults: &'d Defaults) -> Option<&'d Statements>;
#[doc(hidden)]
fn bind_name(arguments: &mut Self::Arguments, name: &str) -> Result<(), Error>;
#[doc(hidden)]
fn execute<'c>(
conn: &'c mut Self::Connection,
sql: &'static str,
arguments: Self::Arguments,
) -> impl Future<Output = Result<(), Error>> + Send + 'c;
#[doc(hidden)]
fn fetch_optional<'c, Record>(
conn: &'c mut Self::Connection,
sql: &'static str,
arguments: Self::Arguments,
) -> impl Future<Output = Result<Option<Record>, Error>> + Send + 'c
where
Record: for<'r> FromRow<'r, Self::Row> + Send + Unpin + 'c;
#[doc(hidden)]
fn fetch_all<'c, Record>(
conn: &'c mut Self::Connection,
sql: &'static str,
arguments: Self::Arguments,
) -> impl Future<Output = Result<Vec<Record>, Error>> + Send + 'c
where
Record: for<'r> FromRow<'r, Self::Row> + Send + Unpin + 'c;
}
#[cfg(any(
feature = "postgres",
feature = "mysql",
feature = "sqlite",
feature = "any"
))]
macro_rules! outbox_database {
(
$database:ty, $connection:ty,
|$spec:ident| $build:expr,
|$conn:ident, $defaults:ident| $pick:expr
) => {
impl OutboxDatabase for $database {
fn defaults($spec: &TableSpec<'_>) -> Result<Defaults, StatementError> {
$build
}
fn statements<'d>(
$conn: &$connection,
$defaults: &'d Defaults,
) -> Option<&'d Statements> {
$pick
}
fn bind_name(arguments: &mut Self::Arguments, name: &str) -> Result<(), Error> {
arguments.add(name).map_err(Error::Encode)
}
async fn execute(
conn: &mut $connection,
sql: &'static str,
arguments: Self::Arguments,
) -> Result<(), Error> {
sqlx::query_with(sql, arguments).execute(conn).await?;
Ok(())
}
async fn fetch_optional<'c, Record>(
conn: &'c mut $connection,
sql: &'static str,
arguments: Self::Arguments,
) -> Result<Option<Record>, Error>
where
Record: for<'r> FromRow<'r, Self::Row> + Send + Unpin + 'c,
{
sqlx::query_as_with(sql, arguments)
.fetch_optional(conn)
.await
}
async fn fetch_all<'c, Record>(
conn: &'c mut $connection,
sql: &'static str,
arguments: Self::Arguments,
) -> Result<Vec<Record>, Error>
where
Record: for<'r> FromRow<'r, Self::Row> + Send + Unpin + 'c,
{
sqlx::query_as_with(sql, arguments).fetch_all(conn).await
}
}
};
}
#[cfg(feature = "postgres")]
outbox_database!(
Postgres,
PgConnection,
|spec| Ok(Defaults {
postgres: Some(Statements::of(&dialect::Postgres, spec)?),
..Defaults::default()
}),
|_conn, defaults| defaults.postgres.as_ref()
);
#[cfg(feature = "mysql")]
outbox_database!(
MySql,
MySqlConnection,
|spec| Ok(Defaults {
mysql: Some(Statements::of(&dialect::MySql, spec)?),
..Defaults::default()
}),
|_conn, defaults| defaults.mysql.as_ref()
);
#[cfg(feature = "sqlite")]
outbox_database!(
Sqlite,
SqliteConnection,
|spec| Ok(Defaults {
sqlite: Some(Statements::of(&dialect::Sqlite, spec)?),
..Defaults::default()
}),
|_conn, defaults| defaults.sqlite.as_ref()
);
#[cfg(feature = "any")]
outbox_database!(
Any,
AnyConnection,
|spec| Ok(Defaults {
#[cfg(feature = "postgres")]
postgres: Some(Statements::of(&dialect::Postgres, spec)?),
#[cfg(feature = "mysql")]
mysql: Some(Statements::of(&dialect::MySql, spec)?),
#[cfg(feature = "sqlite")]
sqlite: Some(Statements::of(&dialect::Sqlite, spec)?),
}),
|conn, defaults| defaults.for_backend(conn.backend_name())
);
pub(super) fn no_outbox_statement(record: &'static str, event: &'static str) -> Error {
Error::Configuration(
format!("`{record}` has no {event} statement for this connection's database").into(),
)
}
#[cfg(test)]
mod tests {
#[cfg(feature = "postgres")]
use std::ptr;
#[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
use ruststream_sqlx_dialect::{Column, Form, OutboxDialect, StatementError, TableSpec};
use super::no_outbox_statement;
#[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
use super::{Defaults, OutboxDatabase, dialect};
#[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
const TABLE: TableSpec<'static> = TableSpec::new("outbox", Column::new("id"), Form::RowLock)
.group(Column::new("name"))
.payload(Column::new("payload"))
.processed_at(Column::new("processed_at"))
.database_clock();
#[test]
fn the_missing_statement_names_the_record_and_the_event() {
assert_eq!(
no_outbox_statement("OrderEvent", "fetch").to_string(),
"error with configuration: `OrderEvent` has no fetch statement for this connection's \
database"
);
}
#[cfg(all(
feature = "any",
feature = "postgres",
feature = "mysql",
feature = "sqlite"
))]
#[test]
fn an_any_backend_runs_the_statements_of_its_database() -> Result<(), StatementError> {
use sqlx::Any;
use super::{MYSQL_BACKEND, POSTGRES_BACKEND, SQLITE_BACKEND};
let defaults = <Any as OutboxDatabase>::defaults(&TABLE)?;
let mark = |backend| {
defaults
.for_backend(backend)
.map(|statements| statements.mark)
};
assert_eq!(
mark(POSTGRES_BACKEND),
Some(dialect::Postgres.outbox_mark(&TABLE)?.sql())
);
assert_eq!(
mark(MYSQL_BACKEND),
Some(dialect::MySql.outbox_mark(&TABLE)?.sql())
);
assert_eq!(
mark(SQLITE_BACKEND),
Some(dialect::Sqlite.outbox_mark(&TABLE)?.sql())
);
assert_eq!(mark("MSSQL"), None);
Ok(())
}
#[cfg(feature = "postgres")]
#[test]
fn a_statement_built_twice_is_kept_once() -> Result<(), StatementError> {
use sqlx::Postgres;
let first = <Postgres as OutboxDatabase>::defaults(&TABLE)?;
let second = <Postgres as OutboxDatabase>::defaults(&TABLE)?;
let mark = |defaults: &Defaults| defaults.postgres.map(|statements| statements.mark);
let (Some(first), Some(second)) = (mark(&first), mark(&second)) else {
panic!("Postgres builds its own statements");
};
assert!(
ptr::eq(first, second),
"the second build reuses the first's text"
);
Ok(())
}
#[cfg(all(
feature = "any",
feature = "postgres",
feature = "mysql",
feature = "sqlite"
))]
#[test]
fn the_backend_names_are_sqlxs() {
use sqlx::Database;
use super::{MYSQL_BACKEND, POSTGRES_BACKEND, SQLITE_BACKEND};
assert_eq!(POSTGRES_BACKEND, <sqlx::Postgres as Database>::NAME);
assert_eq!(MYSQL_BACKEND, <sqlx::MySql as Database>::NAME);
assert_eq!(SQLITE_BACKEND, <sqlx::Sqlite as Database>::NAME);
}
}