1#[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
5use std::collections::BTreeSet;
6use std::future::Future;
7#[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
8use std::sync::{Mutex, PoisonError};
9
10#[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
11use ruststream_sqlx_dialect::{self as dialect, OutboxDialect};
12use ruststream_sqlx_dialect::{StatementError, TableSpec};
13
14#[cfg(feature = "any")]
15use sqlx::Any;
16#[cfg(feature = "any")]
17use sqlx::AnyConnection;
18#[cfg(any(
19 feature = "postgres",
20 feature = "mysql",
21 feature = "sqlite",
22 feature = "any"
23))]
24use sqlx::Arguments as _;
25use sqlx::{Database, Error, FromRow};
26#[cfg(feature = "mysql")]
27use sqlx::{MySql, MySqlConnection};
28#[cfg(feature = "postgres")]
29use sqlx::{PgConnection, Postgres};
30#[cfg(feature = "sqlite")]
31use sqlx::{Sqlite, SqliteConnection};
32
33#[doc(hidden)]
36#[derive(Debug, Clone, Copy)]
37pub struct Statements {
38 pub(super) fetch: &'static str,
40 pub(super) mark: &'static str,
42 pub(super) recover: &'static str,
44}
45
46#[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
47impl Statements {
48 fn of(dialect: &impl OutboxDialect, spec: &TableSpec<'_>) -> Result<Self, StatementError> {
50 Ok(Self {
51 fetch: intern(dialect.outbox_fetch(spec)?.sql()),
52 mark: intern(dialect.outbox_mark(spec)?.sql()),
53 recover: intern(dialect.outbox_recover(spec)?.sql()),
54 })
55 }
56}
57
58#[doc(hidden)]
62#[derive(Debug, Clone, Copy, Default)]
63pub struct Defaults {
64 #[cfg(feature = "postgres")]
65 postgres: Option<Statements>,
66 #[cfg(feature = "mysql")]
67 mysql: Option<Statements>,
68 #[cfg(feature = "sqlite")]
69 sqlite: Option<Statements>,
70}
71
72#[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
79fn intern(text: &str) -> &'static str {
80 static TEXTS: Mutex<BTreeSet<&'static str>> = Mutex::new(BTreeSet::new());
81 let mut texts = TEXTS.lock().unwrap_or_else(PoisonError::into_inner);
82 if let Some(interned) = texts.get(text) {
83 return interned;
84 }
85 let interned: &'static str = Box::leak(text.into());
86 texts.insert(interned);
87 interned
88}
89
90#[cfg(all(feature = "any", feature = "postgres"))]
92const POSTGRES_BACKEND: &str = "PostgreSQL";
93#[cfg(all(feature = "any", feature = "mysql"))]
94const MYSQL_BACKEND: &str = "MySQL";
95#[cfg(all(feature = "any", feature = "sqlite"))]
96const SQLITE_BACKEND: &str = "SQLite";
97
98#[cfg(feature = "any")]
99impl Defaults {
100 fn for_backend(&self, backend: &str) -> Option<&Statements> {
102 match backend {
103 #[cfg(feature = "postgres")]
104 POSTGRES_BACKEND => self.postgres.as_ref(),
105 #[cfg(feature = "mysql")]
106 MYSQL_BACKEND => self.mysql.as_ref(),
107 #[cfg(feature = "sqlite")]
108 SQLITE_BACKEND => self.sqlite.as_ref(),
109 _ => None,
110 }
111 }
112}
113
114pub trait OutboxDatabase: Database {
175 #[doc(hidden)]
178 fn defaults(spec: &TableSpec<'_>) -> Result<Defaults, StatementError>;
179
180 #[doc(hidden)]
183 fn statements<'d>(conn: &Self::Connection, defaults: &'d Defaults) -> Option<&'d Statements>;
184
185 #[doc(hidden)]
187 fn bind_name(arguments: &mut Self::Arguments, name: &str) -> Result<(), Error>;
188
189 #[doc(hidden)]
191 fn execute<'c>(
192 conn: &'c mut Self::Connection,
193 sql: &'static str,
194 arguments: Self::Arguments,
195 ) -> impl Future<Output = Result<(), Error>> + Send + 'c;
196
197 #[doc(hidden)]
199 fn fetch_optional<'c, Record>(
200 conn: &'c mut Self::Connection,
201 sql: &'static str,
202 arguments: Self::Arguments,
203 ) -> impl Future<Output = Result<Option<Record>, Error>> + Send + 'c
204 where
205 Record: for<'r> FromRow<'r, Self::Row> + Send + Unpin + 'c;
206
207 #[doc(hidden)]
209 fn fetch_all<'c, Record>(
210 conn: &'c mut Self::Connection,
211 sql: &'static str,
212 arguments: Self::Arguments,
213 ) -> impl Future<Output = Result<Vec<Record>, Error>> + Send + 'c
214 where
215 Record: for<'r> FromRow<'r, Self::Row> + Send + Unpin + 'c;
216}
217
218#[cfg(any(
221 feature = "postgres",
222 feature = "mysql",
223 feature = "sqlite",
224 feature = "any"
225))]
226macro_rules! outbox_database {
227 (
228 $database:ty, $connection:ty,
229 |$spec:ident| $build:expr,
230 |$conn:ident, $defaults:ident| $pick:expr
231 ) => {
232 impl OutboxDatabase for $database {
233 fn defaults($spec: &TableSpec<'_>) -> Result<Defaults, StatementError> {
234 $build
235 }
236
237 fn statements<'d>(
238 $conn: &$connection,
239 $defaults: &'d Defaults,
240 ) -> Option<&'d Statements> {
241 $pick
242 }
243
244 fn bind_name(arguments: &mut Self::Arguments, name: &str) -> Result<(), Error> {
245 arguments.add(name).map_err(Error::Encode)
246 }
247
248 async fn execute(
249 conn: &mut $connection,
250 sql: &'static str,
251 arguments: Self::Arguments,
252 ) -> Result<(), Error> {
253 sqlx::query_with(sql, arguments).execute(conn).await?;
254 Ok(())
255 }
256
257 async fn fetch_optional<'c, Record>(
258 conn: &'c mut $connection,
259 sql: &'static str,
260 arguments: Self::Arguments,
261 ) -> Result<Option<Record>, Error>
262 where
263 Record: for<'r> FromRow<'r, Self::Row> + Send + Unpin + 'c,
264 {
265 sqlx::query_as_with(sql, arguments)
266 .fetch_optional(conn)
267 .await
268 }
269
270 async fn fetch_all<'c, Record>(
271 conn: &'c mut $connection,
272 sql: &'static str,
273 arguments: Self::Arguments,
274 ) -> Result<Vec<Record>, Error>
275 where
276 Record: for<'r> FromRow<'r, Self::Row> + Send + Unpin + 'c,
277 {
278 sqlx::query_as_with(sql, arguments).fetch_all(conn).await
279 }
280 }
281 };
282}
283
284#[cfg(feature = "postgres")]
285outbox_database!(
286 Postgres,
287 PgConnection,
288 |spec| Ok(Defaults {
289 postgres: Some(Statements::of(&dialect::Postgres, spec)?),
290 ..Defaults::default()
291 }),
292 |_conn, defaults| defaults.postgres.as_ref()
293);
294#[cfg(feature = "mysql")]
295outbox_database!(
296 MySql,
297 MySqlConnection,
298 |spec| Ok(Defaults {
299 mysql: Some(Statements::of(&dialect::MySql, spec)?),
300 ..Defaults::default()
301 }),
302 |_conn, defaults| defaults.mysql.as_ref()
303);
304#[cfg(feature = "sqlite")]
305outbox_database!(
306 Sqlite,
307 SqliteConnection,
308 |spec| Ok(Defaults {
309 sqlite: Some(Statements::of(&dialect::Sqlite, spec)?),
310 ..Defaults::default()
311 }),
312 |_conn, defaults| defaults.sqlite.as_ref()
313);
314#[cfg(feature = "any")]
316outbox_database!(
317 Any,
318 AnyConnection,
319 |spec| Ok(Defaults {
320 #[cfg(feature = "postgres")]
321 postgres: Some(Statements::of(&dialect::Postgres, spec)?),
322 #[cfg(feature = "mysql")]
323 mysql: Some(Statements::of(&dialect::MySql, spec)?),
324 #[cfg(feature = "sqlite")]
325 sqlite: Some(Statements::of(&dialect::Sqlite, spec)?),
326 }),
327 |conn, defaults| defaults.for_backend(conn.backend_name())
328);
329
330pub(super) fn no_outbox_statement(record: &'static str, event: &'static str) -> Error {
332 Error::Configuration(
333 format!("`{record}` has no {event} statement for this connection's database").into(),
334 )
335}
336
337#[cfg(test)]
338mod tests {
339 #[cfg(feature = "postgres")]
340 use std::ptr;
341
342 #[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
343 use ruststream_sqlx_dialect::{Column, Form, OutboxDialect, StatementError, TableSpec};
344
345 use super::no_outbox_statement;
346 #[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
347 use super::{Defaults, OutboxDatabase, dialect};
348
349 #[cfg(any(feature = "postgres", feature = "mysql", feature = "sqlite"))]
350 const TABLE: TableSpec<'static> = TableSpec::new("outbox", Column::new("id"), Form::RowLock)
351 .group(Column::new("name"))
352 .payload(Column::new("payload"))
353 .processed_at(Column::new("processed_at"))
354 .database_clock();
355
356 #[test]
357 fn the_missing_statement_names_the_record_and_the_event() {
358 assert_eq!(
359 no_outbox_statement("OrderEvent", "fetch").to_string(),
360 "error with configuration: `OrderEvent` has no fetch statement for this connection's \
361 database"
362 );
363 }
364
365 #[cfg(all(
366 feature = "any",
367 feature = "postgres",
368 feature = "mysql",
369 feature = "sqlite"
370 ))]
371 #[test]
372 fn an_any_backend_runs_the_statements_of_its_database() -> Result<(), StatementError> {
373 use sqlx::Any;
374
375 use super::{MYSQL_BACKEND, POSTGRES_BACKEND, SQLITE_BACKEND};
376
377 let defaults = <Any as OutboxDatabase>::defaults(&TABLE)?;
378 let mark = |backend| {
380 defaults
381 .for_backend(backend)
382 .map(|statements| statements.mark)
383 };
384 assert_eq!(
385 mark(POSTGRES_BACKEND),
386 Some(dialect::Postgres.outbox_mark(&TABLE)?.sql())
387 );
388 assert_eq!(
389 mark(MYSQL_BACKEND),
390 Some(dialect::MySql.outbox_mark(&TABLE)?.sql())
391 );
392 assert_eq!(
393 mark(SQLITE_BACKEND),
394 Some(dialect::Sqlite.outbox_mark(&TABLE)?.sql())
395 );
396 assert_eq!(mark("MSSQL"), None);
397 Ok(())
398 }
399
400 #[cfg(feature = "postgres")]
401 #[test]
402 fn a_statement_built_twice_is_kept_once() -> Result<(), StatementError> {
403 use sqlx::Postgres;
404
405 let first = <Postgres as OutboxDatabase>::defaults(&TABLE)?;
406 let second = <Postgres as OutboxDatabase>::defaults(&TABLE)?;
407 let mark = |defaults: &Defaults| defaults.postgres.map(|statements| statements.mark);
408 let (Some(first), Some(second)) = (mark(&first), mark(&second)) else {
409 panic!("Postgres builds its own statements");
410 };
411 assert!(
412 ptr::eq(first, second),
413 "the second build reuses the first's text"
414 );
415 Ok(())
416 }
417
418 #[cfg(all(
419 feature = "any",
420 feature = "postgres",
421 feature = "mysql",
422 feature = "sqlite"
423 ))]
424 #[test]
425 fn the_backend_names_are_sqlxs() {
426 use sqlx::Database;
427
428 use super::{MYSQL_BACKEND, POSTGRES_BACKEND, SQLITE_BACKEND};
429
430 assert_eq!(POSTGRES_BACKEND, <sqlx::Postgres as Database>::NAME);
431 assert_eq!(MYSQL_BACKEND, <sqlx::MySql as Database>::NAME);
432 assert_eq!(SQLITE_BACKEND, <sqlx::Sqlite as Database>::NAME);
433 }
434}