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;