Skip to main content

ruststream_sqlx/
lib.rs

1#![doc = include_str!("README.md")]
2#![forbid(unsafe_code)]
3
4pub use ruststream_sqlx_dialect as dialect;
5
6#[cfg(any(feature = "inbox", feature = "outbox"))]
7mod header_column;
8#[cfg(any(feature = "inbox", feature = "outbox"))]
9mod home;
10#[cfg(feature = "outbox")]
11pub mod outbox;
12#[cfg(any(feature = "inbox", feature = "outbox"))]
13mod settings;
14
15#[cfg(any(feature = "inbox", feature = "outbox"))]
16pub use header_column::{HeaderColumn, HeaderRow};
17#[cfg(feature = "outbox")]
18pub use outbox::{Outbox, OutboxDatabase, OutboxRow, OutboxSpec, OutboxTable};
19
20#[cfg(feature = "inbox")]
21mod inbox;
22#[cfg(feature = "inbox")]
23pub mod prelude;
24
25/// Machinery behind [`RowBatch`]'s deliveries; a service never names it.
26#[cfg(feature = "inbox")]
27#[doc(hidden)]
28pub use inbox::RowDeliveries;
29/// Machinery behind [`InboxSettings::on_threads`]; a service never names it.
30#[cfg(feature = "inbox")]
31#[doc(hidden)]
32pub use inbox::ThreadsStep;
33/// Machinery behind [`InboxSettings::transactional`]; a service never names it.
34#[cfg(feature = "inbox")]
35#[doc(hidden)]
36pub use inbox::TransactionalStep;
37#[cfg(feature = "inbox")]
38pub use inbox::{
39    Ack, AttemptColumn, AttemptRow, BuiltIn, BuiltInDialect, ByName, Claim, Clock,
40    ClosedSqlxBroker, ConnectedSqlxBroker, DatabaseClock, DeadLetter, Discard, Extend, Fetch,
41    HeaderField, HeaderFields, InboxDelivery, InboxHeaders, InboxQueue, InboxRow, InboxSettings,
42    InboxSpec, InboxSubscriber, InboxTable, InboxThreads, Insert, KeyColumn, KeyRow, LeaseRow,
43    Lock, NamedDelivery, NamedSubscriber, NamedTime, Notifies, PayloadRow, Plain, Publish,
44    QueueDatabase, QueueTime, Repository, RepositoryPublisher, Retry, RetryAfter, Routed,
45    RoutedPublisher, RowBatch, SqlxBroker, SqlxBrokerError, SystemClock, TimeColumn, TimeSource,
46    Transactional, Tx, Unlock, put_header,
47};
48
49/// What a handler reads off the delivery it handles, through `Ctx<Key>`.
50#[cfg(feature = "inbox")]
51pub use inbox::keys;
52#[cfg(feature = "inbox")]
53pub use inbox::spec;
54
55#[cfg(any(feature = "inbox", feature = "outbox"))]
56#[doc(hidden)]
57pub mod __private {
58    pub use ruststream::HeaderMap;
59    pub use sqlx;
60
61    #[cfg(feature = "inbox")]
62    pub use inbox::*;
63
64    /// What the inbox derives expand to.
65    #[cfg(feature = "inbox")]
66    mod inbox {
67        pub use ruststream::runtime::{Input, SoloCarried};
68        pub use ruststream_sqlx_dialect::Param;
69
70        #[cfg(feature = "any")]
71        pub use crate::inbox::AnyDialect;
72        pub use crate::inbox::batch::{BatchClaim, BatchLane};
73        pub use crate::inbox::engine::{
74            Claimed, Claiming, Event, Events, IdAt, Leasing, Now, Prepared, Savepoint, Settled,
75            Settling, Shape, Stmt, TimeFor, Values, Via, put,
76        };
77        pub use crate::inbox::form::advisory::events::Candidates;
78        pub use crate::inbox::headers::HeaderCell;
79        pub use crate::inbox::manual::{
80            AckAxis, AttemptAxis, Axes, ClaimAxes, ClockAxis, DeadLetterAxis, DiscardAxis,
81            ExtendAxis, FormAxis, FormBind, HeadersAxis, KeyAxis, LeaseAxis, LockAxis,
82            ManualHeaders, MessageAxis, Named, OpeningAxis, RetryAfterAxis, RetryAxis, TimeAxis,
83            UnlockAxis,
84        };
85        pub use crate::inbox::named::kinds::{Kinds, KindsOf};
86        pub use crate::inbox::named::{NamedBytes, NamedId, NamedRow, RoleColumns};
87        pub use crate::inbox::queue::Queue;
88        pub use crate::inbox::{
89            AdvisoryForm, FormDialect, FormOn, InboxMode, InsertSql, Lane, LeaseForm, OnConnection,
90            PayloadLane, QueueDatabase, QueueRow, RowLane, RowLockForm, no_insert,
91        };
92    }
93}
94
95/// Describes a queue table with a struct: writes its [`InboxTable`] impl, the description a table
96/// described by hand writes, and the traits its roles need.
97///
98/// The struct is an ordinary sqlx struct: `#[inbox(..)]` names the table, sqlx's own
99/// attributes name the columns, and `#[field(..)]` marks the columns that run the queue. The
100/// [`InboxSpec` page](https://powersemmi.github.io/ruststream-sqlx/latest/components/tables/inbox-spec/) shows the same
101/// table written by hand.
102///
103/// # Examples
104///
105/// ```
106/// # #[cfg(feature = "postgres")]
107/// # mod demo {
108/// use ruststream_sqlx::prelude::*;
109/// use serde::Deserialize;
110/// use sqlx::PgPool;
111///
112/// // app.email_jobs: job_id BIGSERIAL PRIMARY KEY, name TEXT, attempt SMALLINT DEFAULT 1,
113/// // payload BYTEA
114/// #[derive(Inbox, sqlx::FromRow)]
115/// #[inbox(table = "email_jobs", schema = "app")]
116/// pub struct SendEmail {
117///     #[field(id, generated)]
118///     job_id: i64,
119///     #[field(group)]
120///     name: String,
121///     #[field(attempt, generated)]
122///     attempt: i16,
123///     #[field(payload)]
124///     payload: Vec<u8>,
125/// }
126///
127/// #[derive(Deserialize)]
128/// struct Email {
129///     to: String,
130/// }
131///
132/// // The subscription claims the rows of group `emails`, oldest first, and skips the rows
133/// // another worker holds.
134/// #[subscriber(InboxQueue::<SendEmail>::new("emails"))]
135/// async fn send(email: &Email) -> HandlerOutcome {
136///     tracing::info!(to = %email.to, "sending");
137///     HandlerOutcome::ack()
138/// }
139///
140/// pub fn app(pool: PgPool) -> RustStream {
141///     RustStream::new(AppInfo::new("mailer", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
142///         b.include(send);
143///     })
144/// }
145/// # }
146/// # fn main() {}
147/// ```
148///
149/// # The table
150///
151/// `#[inbox(table = "..")]` names the table and is required. `schema = ".."` places it in a
152/// schema. Each names one thing and holds no dot: the schema goes into `schema`, never into the
153/// table's name. `advisory_lock = "jobs-{job_id}"` selects the advisory lock form, with a key built
154/// from the fields named between braces. A placeholder names a field as written in Rust, without
155/// `r#` (`{type}` for `r#type`), and the key reads that field's column. In that form a group
156/// keeps its order through the key, as in `advisory_lock = "jobs-{name}"`, so `fifo = true` does
157/// not apply there. The form selects its candidates with their keys itself, so `custom(..)` does
158/// not take `claim` beside it; `custom(lock, unlock)` hands the lock and the unlock of each key to
159/// the service ([`Lock`], [`Unlock`]). Every built-in dialect builds the lease and advisory lock
160/// forms; Postgres and MySQL build the row lock form too.
161///
162/// A table in the advisory lock form holds each row by a lock on its key while the handler runs,
163/// with no transaction open:
164///
165/// ```no_run
166/// # #[cfg(all(feature = "inbox", feature = "postgres"))]
167/// # mod demo {
168/// use ruststream_sqlx::prelude::*;
169/// use serde::Deserialize;
170/// use sqlx::PgPool;
171///
172/// // jobs: job_id BIGSERIAL PRIMARY KEY, attempt SMALLINT NOT NULL DEFAULT 1,
173/// // payload BYTEA NOT NULL
174/// #[derive(Inbox, sqlx::FromRow)]
175/// #[inbox(table = "jobs", advisory_lock = "jobs-{job_id}")]
176/// pub struct Transcode {
177///     #[field(id, generated)]
178///     job_id: i64,
179///     #[field(attempt, generated)]
180///     attempt: i16,
181///     #[field(payload)]
182///     payload: Vec<u8>,
183/// }
184///
185/// #[derive(Deserialize)]
186/// pub struct Video {
187///     path: String,
188/// }
189///
190/// # async fn encode(video: &Video) -> bool { !video.path.is_empty() }
191/// // A video takes minutes to encode. The delivery's connection keeps the job's lock while the
192/// // handler runs, with no transaction open. If the process dies, the database ends that session,
193/// // and the job returns.
194/// #[subscriber(InboxQueue::<Transcode>::new("videos"))]
195/// async fn transcode(video: &Video) -> HandlerOutcome {
196///     if encode(video).await {
197///         return HandlerOutcome::ack();
198///     }
199///     HandlerOutcome::retry()
200/// }
201///
202/// pub fn app(pool: PgPool) -> RustStream {
203///     RustStream::new(AppInfo::new("transcoder", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
204///         b.include(transcode);
205///     })
206/// }
207/// # }
208/// # fn main() {}
209/// ```
210///
211/// # Isolation and mode
212///
213/// `isolation = <level>` opens the table's transactions at an isolation level:
214/// `read_uncommitted`, `read_committed`, `repeatable_read` or `serializable`. `mode = <mode>`
215/// opens them in a SQLite mode instead: `deferred`, `immediate` or `exclusive`. A table names one
216/// of the two, or neither and opens at its database's default (READ COMMITTED on MySQL and
217/// MariaDB). The row lock claim's transaction opens at it, and so does the transaction a delivery
218/// lends its handler in transactional mode, in every form.
219///
220/// ```
221/// # #[cfg(feature = "postgres")]
222/// # mod demo {
223/// use ruststream_sqlx::prelude::*;
224/// use serde::Deserialize;
225/// use sqlx::{PgPool, Postgres};
226///
227/// #[derive(Inbox, sqlx::FromRow)]
228/// #[inbox(table = "ledger_jobs", advisory_lock = "ledger-{id}", isolation = repeatable_read)]
229/// pub struct Posting {
230///     #[field(id)]
231///     id: i64,
232///     #[field(payload)]
233///     payload: Vec<u8>,
234/// }
235///
236/// #[derive(Deserialize)]
237/// struct Entry {
238///     account: String,
239///     cents: i64,
240/// }
241///
242/// // The handler's transaction opens with `BEGIN ISOLATION LEVEL REPEATABLE READ`: the balance it
243/// // checks stays as its first statement read it.
244/// #[subscriber(InboxQueue::<Posting>::new("postings"))]
245/// async fn post(entry: &Entry, Ctx(mut tx): Ctx<keys::Tx<Postgres>>) -> HandlerOutcome {
246///     let posted = sqlx::query(
247///         "INSERT INTO ledger (account, cents) SELECT $1, $2 \
248///          WHERE (SELECT sum(cents) FROM ledger WHERE account = $1) + $2 >= 0",
249///     )
250///     .bind(&entry.account)
251///     .bind(entry.cents)
252///     .execute(&mut *tx)
253///     .await;
254///     if posted.is_ok() { HandlerOutcome::ack() } else { HandlerOutcome::retry() }
255/// }
256///
257/// pub fn app(pool: PgPool) -> RustStream {
258///     RustStream::new(AppInfo::new("ledger", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
259///         b.include(post.transactional());
260///     })
261/// }
262/// # }
263/// # fn main() {}
264/// ```
265///
266/// The broker's dialect opens what its database keeps: Postgres `read_committed`,
267/// `repeatable_read` and `serializable`, MySQL and MariaDB all four levels, SQLite the three
268/// modes. A subscription to a table its dialect does not open does not compile, and the error
269/// names the levels the dialect opens. An `AnyPool` reaches a database named only when the broker
270/// connects, so there the subscription stops when it starts instead. On Postgres a row lock table
271/// at `repeatable_read` or `serializable` fails claims with serialization errors when claims and
272/// settlements of its rows run at once; `read_committed` is the practical level there. A lease
273/// table at either level on Postgres refuses transactional mode when it starts: its transaction
274/// reads every row as its first statement found it, and would not see the lease extended later.
275///
276/// # Roles
277///
278/// `#[field(..)]` gives a field one role:
279///
280/// - `id`, required: the row's identity. Its type is `Clone`: a lease subscription keeps a copy
281///   of each id in work to extend its lease.
282/// - `group`: the group a subscription reads; `#[field(group, fifo = true)]` keeps each group in
283///   order.
284/// - `partition_key`: the delivery's partition key.
285/// - `priority`: the claim order, a smaller value first.
286/// - `retry_after`: the time before which a row is not claimed.
287/// - `attempt`: the attempt number.
288/// - `locked_until`: the lease's expiry; it selects the lease form.
289/// - `processed_at`: acknowledgement marks the row instead of deleting it.
290/// - `headers` and `payload`: the delivery's headers and the message bytes.
291///
292/// `generated`, alone or beside a role, marks a column the database fills in.
293///
294/// Each role turns on one behaviour of the queue, and a table without it keeps the default. The
295/// subscription below reads the `billing` group, takes urgent tickets first, keeps each customer's
296/// tickets in order on one worker, delays a failed ticket by five minutes, and moves it to the
297/// `escalated` group at the fifth failure. An acknowledged ticket stays in the table with its
298/// `processed_at` set.
299///
300/// ```
301/// # #[cfg(all(feature = "postgres", feature = "chrono"))]
302/// # mod demo {
303/// use std::time::Duration;
304///
305/// use chrono::{DateTime, Utc};
306/// use ruststream_sqlx::prelude::*;
307/// use serde::Deserialize;
308/// use sqlx::PgPool;
309///
310/// // support_tickets: id BIGSERIAL PRIMARY KEY, queue TEXT NOT NULL, urgency SMALLINT NOT NULL,
311/// // customer TEXT NOT NULL, retry_after TIMESTAMPTZ NOT NULL DEFAULT now(),
312/// // attempt SMALLINT NOT NULL DEFAULT 1, processed_at TIMESTAMPTZ, payload BYTEA NOT NULL
313/// #[derive(Inbox, sqlx::FromRow)]
314/// #[inbox(table = "support_tickets")]
315/// pub struct Ticket {
316///     #[field(id, generated)]
317///     id: i64,
318///     #[field(group)]
319///     queue: String,
320///     #[field(priority)]
321///     urgency: i16,
322///     #[field(partition_key)]
323///     customer: String,
324///     #[field(retry_after, generated)]
325///     retry_after: DateTime<Utc>,
326///     #[field(attempt, generated)]
327///     attempt: i16,
328///     #[field(processed_at, generated)]
329///     processed_at: Option<DateTime<Utc>>,
330///     #[field(payload)]
331///     payload: Vec<u8>,
332/// }
333///
334/// #[derive(Deserialize)]
335/// pub struct Question {
336///     text: String,
337/// }
338///
339/// # async fn answer(_: &Question) -> bool { true }
340/// #[subscriber(InboxQueue::<Ticket>::new("billing"))]
341/// async fn triage(question: &Question) -> HandlerOutcome {
342///     if answer(question).await {
343///         return HandlerOutcome::ack();
344///     }
345///     HandlerOutcome::retry_after(Duration::from_secs(300))
346/// }
347///
348/// pub fn app(pool: PgPool) -> RustStream {
349///     RustStream::new(AppInfo::new("support", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
350///         b.include(triage.workers_by_key(nonzero!(4)))
351///             .max_attempts(nonzero!(5u32))
352///             .dead_letter("escalated");
353///     })
354/// }
355/// # }
356/// # fn main() {}
357/// ```
358///
359/// `#[field(group, fifo = true)]` keeps each group in order: one row of a group is in work at a
360/// time, however many brokers read it, and a claim takes the group's head alone:
361///
362/// ```no_run
363/// # #[cfg(all(feature = "postgres", feature = "chrono"))]
364/// # mod demo {
365/// use chrono::{DateTime, Utc};
366/// use ruststream_sqlx::prelude::*;
367/// use serde::Deserialize;
368/// use sqlx::PgPool;
369///
370/// // sync_jobs: id BIGSERIAL PRIMARY KEY, target TEXT NOT NULL,
371/// // retry_after TIMESTAMPTZ NOT NULL DEFAULT now(), payload BYTEA NOT NULL
372/// // Each group holds the changes bound for one system.
373/// #[derive(Inbox, sqlx::FromRow)]
374/// #[inbox(table = "sync_jobs")]
375/// pub struct SyncChange {
376///     #[field(id, generated)]
377///     id: i64,
378///     #[field(group, fifo = true)]
379///     target: String,
380///     #[field(retry_after, generated)]
381///     retry_after: DateTime<Utc>,
382///     #[field(payload)]
383///     payload: Vec<u8>,
384/// }
385///
386/// #[derive(Deserialize)]
387/// pub struct Change {
388///     record: String,
389/// }
390///
391/// # async fn push(change: &Change) -> bool { !change.record.is_empty() }
392/// // The CRM receives its changes one at a time, in the order they were queued.
393/// #[subscriber(InboxQueue::<SyncChange>::new("crm"))]
394/// async fn sync(change: &Change) -> HandlerOutcome {
395///     if push(change).await {
396///         return HandlerOutcome::ack();
397///     }
398///     // At once, so the change keeps its place at the head of the group.
399///     HandlerOutcome::retry()
400/// }
401///
402/// pub fn app(pool: PgPool) -> RustStream {
403///     RustStream::new(AppInfo::new("crm-sync", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
404///         b.include(sync);
405///     })
406/// }
407/// # }
408/// # fn main() {}
409/// ```
410///
411/// # The headers layout
412///
413/// A `#[field(headers)]` field with `#[sqlx(flatten)]` holds a headers struct, which derives
414/// [`InboxHeaders`](derive@InboxHeaders) and describes the queue table: its attributes, the
415/// fields that play a role, and the service's own headers. The struct that flattens it is the
416/// message, which a handler takes itself, `&Message` or `&[Message]`, as in row mode. It takes
417/// `#[inbox(custom(..))]` alone, and no role beside its headers field. Its other fields are its
418/// own data: the default fetch reads them from the queue table, by name, so a column the table
419/// lacks stops the subscription at startup; with `custom(fetch)` the service's [`Fetch`] reads
420/// them from wherever they live. The delivery's headers hold the headers struct's fields without a
421/// role, built on the first read. The headers struct gets the generated insert, the message none.
422///
423/// ```
424/// # #[cfg(feature = "postgres")]
425/// # mod demo {
426/// use ruststream_sqlx::prelude::*;
427/// use sqlx::PgPool;
428///
429/// // order_jobs: job_id BIGSERIAL PRIMARY KEY, name TEXT, attempt SMALLINT DEFAULT 1,
430/// // tenant TEXT, note TEXT
431/// #[derive(Debug, Clone, InboxHeaders, sqlx::FromRow)]
432/// #[inbox(table = "order_jobs")]
433/// pub struct OrderHeaders {
434///     #[field(id, generated)]
435///     job_id: i64,
436///     #[field(group)]
437///     name: String,
438///     #[field(attempt, generated)]
439///     attempt: i16,
440///     tenant: String,
441/// }
442///
443/// #[derive(Debug, Clone, Inbox, sqlx::FromRow)]
444/// pub struct OrderJob {
445///     #[field(headers)]
446///     #[sqlx(flatten)]
447///     headers: OrderHeaders,
448///     note: Option<String>,
449/// }
450///
451/// // The header `tenant` comes from the headers struct, the note from the message's own column.
452/// #[subscriber(InboxQueue::<OrderJob>::new("orders"))]
453/// async fn ship(job: &OrderJob, ctx: &mut Context<'_>) -> HandlerOutcome {
454///     let tenant = ctx.headers().get_str("tenant").unwrap_or_default();
455///     tracing::info!(tenant, attempt = job.headers.attempt, note = ?job.note, "shipping");
456///     HandlerOutcome::ack()
457/// }
458///
459/// pub fn app(pool: PgPool) -> RustStream {
460///     RustStream::new(AppInfo::new("shop", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
461///         b.include(ship);
462///     })
463/// }
464/// # }
465/// # fn main() {}
466/// ```
467///
468/// A struct with a `payload` field is in payload mode: its handler takes the payload, decoded by a
469/// codec. A struct without one is in row mode: its handler takes the struct itself, as the driver
470/// read it. A struct in row mode derives `Clone`, because the test harness keeps a copy of each
471/// value. It does not derive `Deserialize`: a type that deserializes rides the codec.
472///
473/// A generic struct keeps its parameters: the impl requires `Send + Sync + 'static` of the struct
474/// and `Clone + Debug + Send + Sync + 'static` of the `id` field's type. In row mode a handler
475/// takes it where its parameters make it `Clone`.
476///
477/// # Events of the service's own
478///
479/// `custom(..)` names the events the struct writes itself, and every other event keeps the
480/// statement its dialect builds. The struct implements each named event's trait for its database
481/// ([`Claim`], [`Fetch`], [`Ack`], [`Retry`], [`RetryAfter`], [`Discard`], [`DeadLetter`],
482/// [`Extend`], and [`Lock`] and [`Unlock`] in the advisory lock form). A settlement of the service's
483/// own runs in the transaction the crate commits:
484///
485/// ```no_run
486/// # #[cfg(all(feature = "inbox", feature = "postgres"))]
487/// # mod demo {
488/// use ruststream_sqlx::Ack;
489/// use ruststream_sqlx::prelude::*;
490/// use serde::Deserialize;
491/// use sqlx::{PgConnection, PgPool, Postgres};
492///
493/// // email_jobs: job_id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL, payload BYTEA NOT NULL
494/// #[derive(Inbox, sqlx::FromRow)]
495/// #[inbox(table = "email_jobs", custom(ack))]
496/// pub struct SendEmail {
497///     #[field(id, generated)]
498///     job_id: i64,
499///     #[field(group)]
500///     name: String,
501///     #[field(payload)]
502///     payload: Vec<u8>,
503/// }
504///
505/// // A sent email stays in its table, in the `sent` group, for an audit.
506/// impl Ack<Postgres> for SendEmail {
507///     async fn ack(conn: &mut PgConnection, id: &i64) -> Result<(), sqlx::Error> {
508///         sqlx::query("UPDATE email_jobs SET name = 'sent' WHERE job_id = $1")
509///             .bind(id)
510///             .execute(conn)
511///             .await?;
512///         Ok(())
513///     }
514/// }
515///
516/// #[derive(Deserialize)]
517/// pub struct Email {
518///     to: String,
519/// }
520///
521/// #[subscriber(InboxQueue::<SendEmail>::new("emails"))]
522/// async fn send(email: &Email) -> HandlerOutcome {
523///     tracing::info!(to = %email.to, "sending");
524///     HandlerOutcome::ack()
525/// }
526///
527/// pub fn app(pool: PgPool) -> RustStream {
528///     RustStream::new(AppInfo::new("mailer", "0.1.0"))
529///         .with_broker(SqlxBroker::new(pool), |b| {
530///             b.include(send);
531///         })
532/// }
533/// # }
534/// # fn main() {}
535/// ```
536///
537/// # Column names
538///
539/// A column is named in one place, sqlx's attributes. `#[sqlx(rename = "..")]` names a field's
540/// column as written, `#[sqlx(rename_all = "..")]` recases every other field's name, a raw
541/// identifier loses its `r#`, and a `#[sqlx(skip)]` field reads no column. Outside the headers
542/// layout, a `#[sqlx(flatten)]` field reads columns the derive cannot see, so the statements
543/// select `*`, and a dead-letter move copies the row by position: the dead-letter table has the
544/// same columns in the same order.
545/// The other options of `#[sqlx(..)]`, such as `json`, `try_from` and `default`, belong to sqlx's
546/// own derive and pass through untouched.
547///
548/// # Checked at compile time
549///
550/// `#[inbox(checked, db = postgres)]` (or `mysql`, `sqlite`) also puts the statements the struct
551/// determines into sqlx's compile-time check, against `DATABASE_URL` or the `.sqlx` data of
552/// `cargo sqlx prepare`; the service's sqlx needs its `macros` feature. In the headers layout
553/// `checked` goes on the headers struct. [The checked mode's
554/// page](https://powersemmi.github.io/ruststream-sqlx/latest/components/tables/checked/) lists what it covers and
555/// what it refuses.
556///
557/// ```
558/// # #[cfg(all(feature = "inbox", feature = "postgres"))]
559/// # mod demo {
560/// use ruststream_sqlx::prelude::*;
561/// use serde::Deserialize;
562/// use sqlx::PgPool;
563///
564/// // email_jobs: job_id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL,
565/// // attempt SMALLINT NOT NULL DEFAULT 1, payload BYTEA NOT NULL
566/// #[derive(Inbox, sqlx::FromRow)]
567/// #[inbox(table = "email_jobs", checked, db = postgres)]
568/// pub struct SendEmail {
569///     #[field(id, generated)]
570///     job_id: i64,
571///     #[field(group)]
572///     name: String,
573///     #[field(attempt, generated)]
574///     attempt: i16,
575///     #[field(payload)]
576///     payload: Vec<u8>,
577/// }
578///
579/// #[derive(Deserialize)]
580/// struct Email {
581///     to: String,
582/// }
583///
584/// #[subscriber(InboxQueue::<SendEmail>::new("emails"))]
585/// async fn send(email: &Email) -> HandlerOutcome {
586///     tracing::info!(to = %email.to, "sending");
587///     HandlerOutcome::ack()
588/// }
589///
590/// pub fn app(pool: PgPool) -> RustStream {
591///     RustStream::new(AppInfo::new("mailer", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
592///         b.include(send);
593///     })
594/// }
595/// # }
596/// # fn main() {}
597/// ```
598///
599/// # Compile errors
600///
601/// A struct that cannot drive a queue does not compile, and the error points at the field or
602/// the name that causes it: no `id`, a role played twice, a column named twice, a role or
603/// `generated` on a field without a column, `fifo` outside the `group` role, `locked_until`,
604/// `fifo = true` or `claim` in `custom(..)` beside `advisory_lock`, `extend` in `custom(..)`
605/// without `locked_until`, `lock` or `unlock` in `custom(..)` without `advisory_lock` or without
606/// each other, `locked_until` on `clock = DatabaseClock`, a lock key naming no field, a dot in
607/// `table` or `schema`, an unknown isolation level or mode, `isolation` beside `mode`. In the
608/// headers layout: a flattened headers field whose type does not derive `InboxHeaders`, a role or
609/// a table attribute on the message, and a custom event the headers struct's form does not take.
610///
611/// A struct in row mode without `Clone` does not compile. The error points at its name and
612/// suggests the derive:
613///
614/// ```compile_fail,E0277
615/// use ruststream_sqlx::Inbox;
616///
617/// #[derive(Inbox)]
618/// #[inbox(table = "email_jobs")]
619/// struct SendEmail {
620///     #[field(id, generated)]
621///     job_id: i64,
622///     to: String,
623/// }
624/// ```
625///
626/// A struct in row mode that derives `Deserialize` does not compile either. Such a type rides the
627/// codec, and rustc reports conflicting implementations of
628/// [`Input`](ruststream::runtime::Input):
629///
630/// ```compile_fail,E0119
631/// use ruststream_sqlx::Inbox;
632/// use serde::Deserialize;
633///
634/// #[derive(Inbox, Clone, Deserialize)]
635/// #[inbox(table = "email_jobs")]
636/// struct SendEmail {
637///     #[field(id, generated)]
638///     job_id: i64,
639///     to: String,
640/// }
641/// ```
642///
643/// A handler that does not fit its table's mode meets the core's errors at the mount site: a
644/// handler of rows on a table in payload mode, a batch handler of rows with a reply or `Out`
645/// slots:
646///
647// The compile errors of row mode need a driver: without one each example would fail for that
648// reason alone. They render where it is on, as on docs.rs.
649#[cfg_attr(feature = "postgres", doc = include_str!("inbox/row_mode_errors.md"))]
650#[cfg(feature = "inbox")]
651pub use ruststream_sqlx_macros::Inbox;
652/// Describes the queue table of a message assembled from it, and implements [`InboxHeaders`]
653/// for the struct.
654///
655/// The struct takes the table's attributes and the fields that play a role, as a struct deriving
656/// [`Inbox`](derive@Inbox) does; every field without a role is a header (see [`InboxHeaders`]).
657/// `custom(..)`, `payload` and `headers` belong to the message struct, which flattens this one
658/// into its `#[field(headers)]` field. `#[inbox(checked, db = ..)]` checks the table's statements
659/// at compile time, and the message then reads its rows in a `Fetch` of its own ([the headers
660/// layout, checked](https://powersemmi.github.io/ruststream-sqlx/latest/components/tables/checked/)).
661///
662/// # Examples
663///
664/// ```
665/// # #[cfg(feature = "postgres")]
666/// # mod demo {
667/// use ruststream_sqlx::prelude::*;
668/// use sqlx::PgPool;
669///
670/// #[derive(Debug, Clone, InboxHeaders, sqlx::FromRow)]
671/// #[inbox(table = "order_jobs")]
672/// pub struct OrderHeaders {
673///     #[field(id, generated)]
674///     job_id: i64,
675///     #[field(group)]
676///     name: String,
677///     tenant: String,
678/// }
679///
680/// #[derive(Debug, Clone, Inbox, sqlx::FromRow)]
681/// pub struct OrderJob {
682///     #[field(headers)]
683///     #[sqlx(flatten)]
684///     headers: OrderHeaders,
685///     note: Option<String>,
686/// }
687///
688/// #[subscriber(InboxQueue::<OrderJob>::new("orders"))]
689/// async fn ship(job: &OrderJob) -> HandlerOutcome {
690///     tracing::info!(tenant = %job.headers.tenant, note = ?job.note, "shipping");
691///     HandlerOutcome::ack()
692/// }
693///
694/// pub fn app(pool: PgPool) -> RustStream {
695///     RustStream::new(AppInfo::new("shop", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
696///         b.include(ship);
697///     })
698/// }
699/// # }
700/// # fn main() {}
701/// ```
702///
703/// With `checked` on the headers struct, the message lists `fetch` in `custom(..)` and reads its
704/// rows in a [`Fetch`] of its own, where `sqlx::query!` checks that statement too:
705///
706/// ```
707/// # #[cfg(all(feature = "inbox", feature = "postgres", feature = "chrono"))]
708/// # mod demo {
709/// use chrono::{DateTime, Utc};
710/// use ruststream_sqlx::Fetch;
711/// use ruststream_sqlx::prelude::*;
712/// use sqlx::{Error, FromRow, PgConnection, Postgres};
713///
714/// // order_jobs: job_id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL, processed_at TIMESTAMPTZ,
715/// // trace TEXT, total BIGINT NOT NULL
716/// #[derive(Debug, Clone, InboxHeaders, FromRow)]
717/// #[inbox(table = "order_jobs", checked, db = postgres)]
718/// pub struct OrderHeaders {
719///     #[field(id, generated)]
720///     pub job_id: i64,
721///     #[field(group)]
722///     pub name: String,
723///     #[field(processed_at)]
724///     pub processed_at: Option<DateTime<Utc>>,
725///     pub trace: Option<String>,
726/// }
727///
728/// #[derive(Debug, Clone, Inbox, FromRow)]
729/// #[inbox(custom(fetch))]
730/// pub struct Order {
731///     #[field(headers)]
732///     #[sqlx(flatten)]
733///     pub headers: OrderHeaders,
734///     pub total: i64,
735/// }
736///
737/// impl Fetch<Postgres> for Order {
738///     async fn fetch(conn: &mut PgConnection, ids: &[i64]) -> Result<Vec<Self>, Error> {
739///         let rows = sqlx::query!(
740///             "SELECT job_id, name, processed_at, trace, total FROM order_jobs \
741///              WHERE job_id = ANY($1)",
742///             ids
743///         )
744///         .fetch_all(conn)
745///         .await?;
746///         Ok(rows
747///             .into_iter()
748///             .map(|row| Self {
749///                 headers: OrderHeaders {
750///                     job_id: row.job_id,
751///                     name: row.name,
752///                     processed_at: row.processed_at,
753///                     trace: row.trace,
754///                 },
755///                 total: row.total,
756///             })
757///             .collect())
758///     }
759/// }
760/// # }
761/// # fn main() {}
762/// ```
763#[cfg(feature = "inbox")]
764pub use ruststream_sqlx_macros::InboxHeaders;
765
766/// Describes a service's outbox table with a struct: implements [`OutboxTable`], the description
767/// a record by hand writes, and [`HeaderRow`] for a `headers` field.
768///
769/// The struct is an ordinary sqlx struct: `#[outbox(..)]` names the table, sqlx's own attributes
770/// name the columns, and `#[field(..)]` marks the columns the outbox reads. The service writes the
771/// record of a published message itself, in [`outbox::Publish`]; the registry runs the other
772/// events on every [`OutboxDatabase`], from statements it builds from the description when the
773/// record type is registered. The derive checks the description against each built-in dialect
774/// while the service compiles.
775///
776/// # Examples
777///
778/// ```
779/// # #[cfg(all(feature = "postgres", feature = "json"))]
780/// # mod demo {
781/// use ruststream::OutgoingMessage;
782/// use ruststream_sqlx::{Outbox, outbox};
783/// use sqlx::types::Json;
784/// use sqlx::{PgConnection, Postgres};
785/// use std::collections::BTreeMap;
786///
787/// // app.outbox: id BIGSERIAL PRIMARY KEY, name TEXT, payload BYTEA, headers JSONB,
788/// // processed_at TIMESTAMPTZ
789/// #[derive(Outbox, sqlx::FromRow)]
790/// #[outbox(table = "outbox", schema = "app")]
791/// pub struct OrderEvent {
792///     #[field(id)]
793///     id: i64,
794///     #[field(name)]
795///     name: String,
796///     #[field(payload)]
797///     payload: Vec<u8>,
798///     #[field(headers)]
799///     headers: Option<Json<BTreeMap<String, String>>>,
800///     #[field(processed_at)]
801///     processed_at: Option<chrono::DateTime<chrono::Utc>>,
802/// }
803///
804/// impl outbox::Publish<Postgres> for OrderEvent {
805///     async fn publish(
806///         conn: &mut PgConnection,
807///         msg: &OutgoingMessage<'_>,
808///     ) -> Result<i64, sqlx::Error> {
809///         let headers: BTreeMap<String, String> = msg
810///             .headers()
811///             .iter()
812///             .map(|(name, value)| (name.to_owned(), String::from_utf8_lossy(value).into_owned()))
813///             .collect();
814///         sqlx::query_scalar(
815///             "INSERT INTO app.outbox (name, payload, headers) VALUES ($1, $2, $3) RETURNING id",
816///         )
817///         .bind(msg.name())
818///         .bind(msg.payload())
819///         .bind(Json(headers))
820///         .fetch_one(conn)
821///         .await
822///     }
823/// }
824/// # }
825/// # fn main() {}
826/// ```
827///
828/// # The table
829///
830/// `#[outbox(table = "..")]` names the table and is required; `schema = ".."` places it in a
831/// schema. Each names one thing and holds no dot.
832///
833/// # Roles
834///
835/// `#[field(..)]` gives a field one role:
836///
837/// - `id`, required: the record's identity, which a tracked message carries in
838///   [`OUTBOX_ID_HEADER`](outbox::OUTBOX_ID_HEADER) through its `Display` and `FromStr`, and which
839///   the default events bind as its own type.
840/// - `name`, required: the name the record was published under, read through `AsRef<str>`.
841/// - `payload`, required: the published bytes, read through `AsRef<[u8]>`.
842/// - `headers`: the published headers, a [`HeaderColumn`] type.
843/// - `processed_at`: the mark of a processed record, written from the database's clock, so any
844///   time type fits. Without it a processed record is deleted.
845///
846/// Every other field is a plain column sqlx reads. Column names come from sqlx's attributes:
847/// `rename`, `rename_all`, a raw identifier without `r#`, and `skip` for a field without a column;
848/// a `flatten` field makes the statements select `*`.
849///
850/// # Events
851///
852/// The default `Fetch` reads the record while it is unprocessed, `Ack` and `Discard` mark it
853/// processed, `Retry` leaves it as it is and runs no statement, and `Recover` reads the
854/// unprocessed records of one name. `#[outbox(custom(fetch, ack, retry, discard, recover))]`
855/// lists the events the service implements itself instead; `publish` has no default and is never
856/// listed.
857///
858/// # Compile errors
859///
860/// A struct the outbox cannot read does not compile, and the error points at the struct, the
861/// field or the word that causes it: no `id`, `name` or `payload`, a role played twice, a column
862/// named twice, a role on a field without a column, an unknown role or event, a dot in `table` or
863/// `schema`, an `id` read through `#[sqlx(json)]`.
864#[cfg(feature = "outbox")]
865pub use ruststream_sqlx_macros::Outbox;
866/// Registers outbox records under the names they track, and returns the registry.
867///
868/// `outbox! { pool: pool, "orders" => OrderEvent, "refunds" => RefundEvent }` is
869/// `Outbox::new(pool).register::<OrderEvent>("orders").register::<RefundEvent>("refunds")`.
870/// Without `pool:` the registry starts as `Outbox::deferred()`, and the pool is set once it is
871/// built. A name written twice does not compile, and the error points at its second literal.
872///
873/// # Examples
874///
875/// ```
876/// # #[cfg(all(feature = "outbox", feature = "postgres"))]
877/// # mod demo {
878/// # use ruststream::OutgoingMessage;
879/// # use ruststream::memory::prelude::*;
880/// # use ruststream_sqlx::{Outbox, outbox};
881/// # use serde::{Deserialize, Serialize};
882/// # use sqlx::postgres::{PgConnection, PgPool, Postgres};
883/// # #[derive(Outbox, sqlx::FromRow)]
884/// # #[outbox(table = "outbox")]
885/// # pub struct OrderOutbox { #[field(id)] id: i64, #[field(name)] name: String, #[field(payload)] payload: Vec<u8> }
886/// # impl outbox::Publish<Postgres> for OrderOutbox {
887/// #     async fn publish(conn: &mut PgConnection, msg: &OutgoingMessage<'_>) -> sqlx::Result<i64> {
888/// #         sqlx::query_scalar("INSERT INTO outbox (name, payload) VALUES ($1, $2) RETURNING id")
889/// #             .bind(msg.name()).bind(msg.payload()).fetch_one(conn).await
890/// #     }
891/// # }
892/// # #[derive(Deserialize)] pub struct PlaceOrder { id: u64 }
893/// # #[derive(Serialize, Deserialize, Outgoing)] #[outgoing(name = "orders")] pub struct OrderPlaced { id: u64 }
894/// # #[subscriber("checkout", reply)] async fn place(cmd: &PlaceOrder) -> OrderPlaced { OrderPlaced { id: cmd.id } }
895/// # #[subscriber("orders")] async fn fulfil(_: &OrderPlaced) -> HandlerOutcome { HandlerOutcome::ack() }
896/// use std::io;
897///
898/// /// Inside `#[ruststream::app]`: no `pool:`, and the pool arrives in `on_startup`.
899/// pub fn app() -> impl App {
900///     let tracking = outbox! { "orders" => OrderOutbox };
901///     let registry = tracking.clone();
902///     RustStream::new(AppInfo::new("orders", "0.1.0"))
903///         .on_startup(async move |()| {
904///             let pool = PgPool::connect("postgres://localhost/orders")
905///                 .await
906///                 .map_err(io::Error::other)?;
907///             registry.set_pool(pool.clone()).map_err(io::Error::other)?;
908///             Ok::<_, io::Error>(pool)
909///         })
910///         .layer(tracking.layer())
911///         .publish_layer(tracking.publish_layer())
912///         .with_broker(MemoryBroker::new(), |b| {
913///             b.include(place).out_reply(Publish);
914///             b.include(fulfil);
915///             b.after_startup(Publish, tracking.republish());
916///         })
917/// }
918///
919/// /// An app built inside a running runtime takes the pool it has.
920/// pub fn app_with(pool: PgPool) -> impl App {
921///     let tracking = outbox! { pool: pool, "orders" => OrderOutbox };
922///     RustStream::new(AppInfo::new("orders", "0.1.0"))
923///         .publish_layer(tracking.publish_layer())
924///         .with_broker(MemoryBroker::new(), |b| {
925///             b.include(place).out_reply(Publish);
926///         })
927/// }
928/// # }
929/// # fn main() {}
930/// ```
931#[cfg(feature = "outbox")]
932pub use ruststream_sqlx_macros::outbox;