use std::borrow::Cow;
use std::fmt;
use std::num::NonZeroU32;
use std::sync::Arc;
use std::time::Duration;
use ruststream::Broker;
#[cfg(any(
feature = "postgres",
feature = "mysql",
feature = "sqlite",
feature = "any"
))]
use ruststream::{DescribeServer, ServerSpec};
use ruststream_sqlx_dialect::{Dialect, Opens};
#[cfg(feature = "any")]
use sqlx::Any;
#[cfg(any(feature = "postgres", feature = "mysql", feature = "any"))]
use sqlx::ConnectOptions;
#[cfg(feature = "mysql")]
use sqlx::MySql;
#[cfg(feature = "postgres")]
use sqlx::Postgres;
#[cfg(feature = "sqlite")]
use sqlx::Sqlite;
use sqlx::{Database, Pool};
use tokio::runtime::Handle;
use super::database::notify::{StartListening, start_listening};
use super::database::{BuiltIn, BuiltInDialect, Notifies, QueueDatabase};
use super::engine::Events;
use super::error::SqlxBrokerError;
use super::events::Publish;
use super::publish::Routes;
use super::{FormDialect, FormOn, PayloadRow};
mod connected;
pub(crate) use connected::Shared;
pub use connected::{ClosedSqlxBroker, ConnectedSqlxBroker};
const DEFAULT_POLL_INTERVAL: Duration = Duration::from_secs(1);
const DEFAULT_LEASE: Duration = Duration::from_secs(30);
pub(crate) enum DialectChoice<DB: Database, D> {
Picked(fn(&DB::Connection) -> Result<D, SqlxBrokerError>),
Given(Arc<D>),
}
impl<DB: Database, D: Dialect> DialectChoice<DB, D> {
pub(crate) fn resolve(&self, conn: &DB::Connection) -> Result<Arc<D>, SqlxBrokerError> {
match self {
Self::Picked(pick) => pick(conn).map(Arc::new),
Self::Given(dialect) => Ok(Arc::clone(dialect)),
}
}
fn name(&self) -> &'static str {
match self {
Self::Picked(_) => "built in, picked at connect",
Self::Given(dialect) => dialect.name(),
}
}
}
impl<DB: Database, D> Clone for DialectChoice<DB, D> {
fn clone(&self) -> Self {
match self {
Self::Picked(pick) => Self::Picked(*pick),
Self::Given(dialect) => Self::Given(Arc::clone(dialect)),
}
}
}
fn built_in<DB: BuiltInDialect>(conn: &DB::Connection) -> Result<BuiltIn<DB>, SqlxBrokerError> {
DB::dialect(conn).ok_or_else(|| SqlxBrokerError::Backend {
backend: DB::backend(conn).to_owned(),
})
}
pub(crate) type FormOf<D> = fn(&Arc<D>) -> FormDialect;
pub struct SqlxBroker<DB: Database, D = BuiltIn<DB>> {
pub(crate) pool: Pool<DB>,
pub(crate) dialect: DialectChoice<DB, D>,
routes: Routes<DB, FormOf<D>>,
poll_interval: Duration,
lease: Duration,
connection_limit: Option<NonZeroU32>,
pub(crate) listen: Option<StartListening<DB>>,
}
impl<DB: Database, D: Dialect> fmt::Debug for SqlxBroker<DB, D> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SqlxBroker")
.field("dialect", &self.dialect.name())
.field("routes", &self.routes)
.field("poll_interval", &self.poll_interval)
.field("lease", &self.lease)
.field("connection_limit", &self.connection_limit)
.field("listen_notify", &self.listen.is_some())
.finish_non_exhaustive()
}
}
impl<DB: BuiltInDialect> SqlxBroker<DB, BuiltIn<DB>> {
#[must_use]
pub fn new(pool: Pool<DB>) -> Self {
Self::built(pool, DialectChoice::Picked(built_in::<DB>))
}
}
impl<DB: QueueDatabase, D: Dialect + 'static> SqlxBroker<DB, D> {
#[must_use]
pub fn with_dialect(pool: Pool<DB>, dialect: D) -> Self {
Self::built(pool, DialectChoice::Given(Arc::new(dialect)))
}
fn built(pool: Pool<DB>, dialect: DialectChoice<DB, D>) -> Self {
Self {
pool,
dialect,
routes: Routes::default(),
poll_interval: DEFAULT_POLL_INTERVAL,
lease: DEFAULT_LEASE,
connection_limit: None,
listen: None,
}
}
#[must_use]
pub fn route<Row>(mut self, name: impl Into<Cow<'static, str>>) -> Self
where
Row: Publish<DB> + Events<DB> + PayloadRow,
Row::Form: FormOn<D>,
D: Opens<Row::Opening>,
{
self.routes
.add::<Row>(name.into(), <Row::Form as FormOn<D>>::erase);
self
}
#[must_use]
pub const fn poll_interval(mut self, interval: Duration) -> Self {
self.poll_interval = interval;
self
}
#[must_use]
pub const fn lease(mut self, lease: Duration) -> Self {
self.lease = lease;
self
}
#[must_use]
pub const fn connection_limit(mut self, limit: NonZeroU32) -> Self {
self.connection_limit = Some(limit);
self
}
#[must_use]
pub fn listen_notify(mut self) -> Self
where
DB: Notifies,
{
self.listen = Some(start_listening::<DB>());
self
}
}
impl<DB: QueueDatabase, D: Dialect + 'static> Broker for SqlxBroker<DB, D> {
type Error = SqlxBrokerError;
type Connected = ConnectedSqlxBroker<DB, D>;
async fn connect(self) -> Result<Self::Connected, Self::Error> {
let conn = self
.pool
.acquire()
.await
.map_err(|source| SqlxBrokerError::Connect { source })?;
let dialect = self.dialect.resolve(&conn)?;
drop(conn);
let listening = match self.listen {
Some(start) => Some(
start(self.pool.clone())
.await
.map_err(|source| SqlxBrokerError::Connect { source })?,
),
None => None,
};
Ok(ConnectedSqlxBroker::new(
self,
dialect,
Handle::current(),
listening,
))
}
}
#[cfg(feature = "postgres")]
impl<D: Dialect + 'static> DescribeServer for SqlxBroker<Postgres, D> {
fn describe_server(&self) -> ServerSpec {
ServerSpec::from_url(
self.pool.connect_options().to_url_lossy().as_str(),
"postgres",
)
}
}
#[cfg(feature = "mysql")]
impl<D: Dialect + 'static> DescribeServer for SqlxBroker<MySql, D> {
fn describe_server(&self) -> ServerSpec {
ServerSpec::from_url(self.pool.connect_options().to_url_lossy().as_str(), "mysql")
}
}
#[cfg(feature = "sqlite")]
impl<D: Dialect + 'static> DescribeServer for SqlxBroker<Sqlite, D> {
fn describe_server(&self) -> ServerSpec {
ServerSpec::in_process("sqlite")
}
}
#[cfg(feature = "any")]
impl<D: Dialect + 'static> DescribeServer for SqlxBroker<Any, D> {
fn describe_server(&self) -> ServerSpec {
let url = self.pool.connect_options().to_url_lossy();
match url.scheme() {
"sqlite" => ServerSpec::in_process("sqlite"),
"postgres" | "postgresql" => ServerSpec::from_url(url.as_str(), "postgres"),
"mysql" | "mariadb" => ServerSpec::from_url(url.as_str(), "mysql"),
scheme => ServerSpec::from_url(url.as_str(), scheme),
}
}
}