Skip to main content

ruststream_sqlx/
lib.rs

1#![doc = include_str!("README.md")]
2#![doc = include_str!("overview/manual.md")]
3#![doc = include_str!("overview/roles.md")]
4#![doc = include_str!("overview/time.md")]
5#![doc = include_str!("overview/row_mode.md")]
6// The compile errors of row mode need the inbox and a driver: without them each example would fail
7// for that reason alone. They render where both are on, as on docs.rs.
8#![cfg_attr(
9    all(feature = "inbox", feature = "postgres"),
10    doc = include_str!("overview/row_mode_errors.md")
11)]
12#![doc = include_str!("overview/headers.md")]
13#![doc = include_str!("overview/forms.md")]
14#![doc = include_str!("overview/transactional.md")]
15#![doc = include_str!("overview/isolation.md")]
16#![doc = include_str!("overview/fifo.md")]
17#![doc = include_str!("overview/databases.md")]
18#![doc = include_str!("overview/own_dialect.md")]
19#![doc = include_str!("overview/checked.md")]
20#![doc = include_str!("overview/decoding.md")]
21#![doc = include_str!("overview/batches.md")]
22#![doc = include_str!("overview/names.md")]
23#![doc = include_str!("overview/wake.md")]
24#![doc = include_str!("overview/testing.md")]
25#![cfg_attr(feature = "outbox", doc = include_str!("overview/outbox.md"))]
26#![forbid(unsafe_code)]
27
28pub use ruststream_sqlx_dialect as dialect;
29
30#[cfg(any(feature = "inbox", feature = "outbox"))]
31mod header_column;
32#[cfg(any(feature = "inbox", feature = "outbox"))]
33mod home;
34#[cfg(feature = "outbox")]
35pub mod outbox;
36#[cfg(any(feature = "inbox", feature = "outbox"))]
37mod settings;
38
39#[cfg(any(feature = "inbox", feature = "outbox"))]
40pub use header_column::{HeaderColumn, HeaderRow};
41#[cfg(feature = "outbox")]
42pub use outbox::{Outbox, OutboxDatabase, OutboxRow, OutboxSpec, OutboxTable};
43
44#[cfg(feature = "inbox")]
45mod inbox;
46#[cfg(feature = "inbox")]
47pub mod prelude;
48
49/// Machinery behind [`RowBatch`]'s deliveries; a service never names it.
50#[cfg(feature = "inbox")]
51#[doc(hidden)]
52pub use inbox::RowDeliveries;
53/// Machinery behind [`InboxSettings::on_threads`]; a service never names it.
54#[cfg(feature = "inbox")]
55#[doc(hidden)]
56pub use inbox::ThreadsStep;
57/// Machinery behind [`InboxSettings::transactional`]; a service never names it.
58#[cfg(feature = "inbox")]
59#[doc(hidden)]
60pub use inbox::TransactionalStep;
61#[cfg(feature = "inbox")]
62pub use inbox::{
63    Ack, AttemptColumn, AttemptRow, BuiltIn, BuiltInDialect, ByName, Claim, Clock,
64    ClosedSqlxBroker, ConnectedSqlxBroker, DatabaseClock, DeadLetter, Discard, Extend, Fetch,
65    HeaderField, HeaderFields, InboxDelivery, InboxHeaders, InboxQueue, InboxRow, InboxSettings,
66    InboxSpec, InboxSubscriber, InboxTable, InboxThreads, Insert, KeyColumn, KeyRow, LeaseRow,
67    Lock, NamedDelivery, NamedSubscriber, NamedTime, Notifies, PayloadRow, Plain, Publish,
68    QueueDatabase, QueueTime, Repository, RepositoryPublisher, Retry, RetryAfter, Routed,
69    RoutedPublisher, RowBatch, SqlxBroker, SqlxBrokerError, SystemClock, TimeColumn, TimeSource,
70    Transactional, Tx, Unlock, put_header,
71};
72
73/// What a handler reads off the delivery it handles, through `Ctx<Key>`.
74#[cfg(feature = "inbox")]
75pub use inbox::keys;
76#[cfg(feature = "inbox")]
77pub use inbox::spec;
78
79#[cfg(any(feature = "inbox", feature = "outbox"))]
80#[doc(hidden)]
81pub mod __private {
82    pub use ruststream::HeaderMap;
83    pub use sqlx;
84
85    #[cfg(feature = "inbox")]
86    pub use inbox::*;
87
88    /// What the inbox derives expand to.
89    #[cfg(feature = "inbox")]
90    mod inbox {
91        pub use ruststream::runtime::{Input, SoloCarried};
92        pub use ruststream_sqlx_dialect::Param;
93
94        #[cfg(feature = "any")]
95        pub use crate::inbox::AnyDialect;
96        pub use crate::inbox::batch::{BatchClaim, BatchLane};
97        pub use crate::inbox::engine::{
98            Claimed, Claiming, Event, Events, IdAt, Leasing, Now, Prepared, Savepoint, Settled,
99            Settling, Shape, Stmt, TimeFor, Values, Via, put,
100        };
101        pub use crate::inbox::form::advisory::events::Candidates;
102        pub use crate::inbox::headers::HeaderCell;
103        pub use crate::inbox::manual::{
104            AckAxis, AttemptAxis, Axes, ClaimAxes, ClockAxis, DeadLetterAxis, DiscardAxis,
105            ExtendAxis, FormAxis, FormBind, HeadersAxis, KeyAxis, LeaseAxis, LockAxis,
106            ManualHeaders, MessageAxis, Named, OpeningAxis, RetryAfterAxis, RetryAxis, TimeAxis,
107            UnlockAxis,
108        };
109        pub use crate::inbox::named::kinds::{Kinds, KindsOf};
110        pub use crate::inbox::named::{NamedBytes, NamedId, NamedRow, RoleColumns};
111        pub use crate::inbox::queue::Queue;
112        pub use crate::inbox::{
113            AdvisoryForm, FormDialect, FormOn, InboxMode, InsertSql, Lane, LeaseForm, OnConnection,
114            PayloadLane, QueueDatabase, QueueRow, RowLane, RowLockForm, no_insert,
115        };
116    }
117}
118
119/// Describes a queue table with a struct: writes its [`InboxTable`] impl, the description a table
120/// described by hand writes, and the traits its roles need.
121///
122/// The struct is an ordinary sqlx struct: `#[inbox(..)]` names the table, sqlx's own
123/// attributes name the columns, and `#[field(..)]` marks the columns that run the queue. The
124/// [macro or manual](crate#macro-or-manual) section shows the same table written by hand.
125///
126/// # Examples
127///
128/// ```
129/// # #[cfg(feature = "postgres")]
130/// # mod demo {
131/// use ruststream_sqlx::prelude::*;
132/// use serde::Deserialize;
133/// use sqlx::PgPool;
134///
135/// // app.email_jobs: job_id BIGSERIAL PRIMARY KEY, name TEXT, attempt SMALLINT DEFAULT 1,
136/// // payload BYTEA
137/// #[derive(Inbox, sqlx::FromRow)]
138/// #[inbox(table = "email_jobs", schema = "app")]
139/// pub struct SendEmail {
140///     #[field(id, generated)]
141///     job_id: i64,
142///     #[field(group)]
143///     name: String,
144///     #[field(attempt, generated)]
145///     attempt: i16,
146///     #[field(payload)]
147///     payload: Vec<u8>,
148/// }
149///
150/// #[derive(Deserialize)]
151/// struct Email {
152///     to: String,
153/// }
154///
155/// // The subscription claims the rows of group `emails`, oldest first, and skips the rows
156/// // another worker holds.
157/// #[subscriber(InboxQueue::<SendEmail>::new("emails"))]
158/// async fn send(email: &Email) -> HandlerOutcome {
159///     tracing::info!(to = %email.to, "sending");
160///     HandlerOutcome::ack()
161/// }
162///
163/// pub fn app(pool: PgPool) -> RustStream {
164///     RustStream::new(AppInfo::new("mailer", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
165///         b.include(send);
166///     })
167/// }
168/// # }
169/// # fn main() {}
170/// ```
171///
172/// # The table
173///
174/// `#[inbox(table = "..")]` names the table and is required. `schema = ".."` places it in a
175/// schema. Each names one thing and holds no dot: the schema goes into `schema`, never into the
176/// table's name. `advisory_lock = "jobs-{job_id}"` selects the advisory lock form, with a key built
177/// from the fields named between braces. A placeholder names a field as written in Rust, without
178/// `r#` (`{type}` for `r#type`), and the key reads that field's column. In that form a group
179/// keeps its order through the key, as in `advisory_lock = "jobs-{name}"`, so `fifo = true` does
180/// not apply there. The form selects its candidates with their keys itself, so `custom(..)` does
181/// not take `claim` beside it; `custom(lock, unlock)` hands the lock and the unlock of each key to
182/// the service ([`Lock`], [`Unlock`]). Every built-in dialect builds the lease and advisory lock
183/// forms; Postgres and MySQL build the row lock form too.
184///
185/// # Isolation and mode
186///
187/// `isolation = <level>` opens the table's transactions at an isolation level:
188/// `read_uncommitted`, `read_committed`, `repeatable_read` or `serializable`. `mode = <mode>`
189/// opens them in a SQLite mode instead: `deferred`, `immediate` or `exclusive`. A table names one
190/// of the two, or neither and opens at its database's default (READ COMMITTED on MySQL and
191/// MariaDB). The row lock claim's transaction opens at it, and so does the transaction a delivery
192/// lends its handler in transactional mode, in every form.
193///
194/// ```
195/// # #[cfg(feature = "postgres")]
196/// # mod demo {
197/// use ruststream_sqlx::prelude::*;
198/// use serde::Deserialize;
199/// use sqlx::{PgPool, Postgres};
200///
201/// #[derive(Inbox, sqlx::FromRow)]
202/// #[inbox(table = "ledger_jobs", advisory_lock = "ledger-{id}", isolation = repeatable_read)]
203/// pub struct Posting {
204///     #[field(id)]
205///     id: i64,
206///     #[field(payload)]
207///     payload: Vec<u8>,
208/// }
209///
210/// #[derive(Deserialize)]
211/// struct Entry {
212///     account: String,
213///     cents: i64,
214/// }
215///
216/// // The handler's transaction opens with `BEGIN ISOLATION LEVEL REPEATABLE READ`: the balance it
217/// // checks stays as its first statement read it.
218/// #[subscriber(InboxQueue::<Posting>::new("postings"))]
219/// async fn post(entry: &Entry, Ctx(mut tx): Ctx<keys::Tx<Postgres>>) -> HandlerOutcome {
220///     let posted = sqlx::query(
221///         "INSERT INTO ledger (account, cents) SELECT $1, $2 \
222///          WHERE (SELECT sum(cents) FROM ledger WHERE account = $1) + $2 >= 0",
223///     )
224///     .bind(&entry.account)
225///     .bind(entry.cents)
226///     .execute(&mut *tx)
227///     .await;
228///     if posted.is_ok() { HandlerOutcome::ack() } else { HandlerOutcome::retry() }
229/// }
230///
231/// pub fn app(pool: PgPool) -> RustStream {
232///     RustStream::new(AppInfo::new("ledger", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
233///         b.include(post.transactional());
234///     })
235/// }
236/// # }
237/// # fn main() {}
238/// ```
239///
240/// The broker's dialect opens what its database keeps: Postgres `read_committed`,
241/// `repeatable_read` and `serializable`, MySQL and MariaDB all four levels, SQLite the three
242/// modes. A subscription to a table its dialect does not open does not compile, and the error
243/// names the levels the dialect opens. An `AnyPool` reaches a database named only when the broker
244/// connects, so there the subscription stops when it starts instead. On Postgres a row lock table
245/// at `repeatable_read` or `serializable` fails claims with serialization errors when claims and
246/// settlements of its rows run at once; `read_committed` is the practical level there. A lease
247/// table at either level on Postgres refuses transactional mode when it starts: its transaction
248/// reads every row as its first statement found it, and would not see the lease extended later.
249///
250/// # Roles
251///
252/// `#[field(..)]` gives a field one role:
253///
254/// - `id`, required: the row's identity. Its type is `Clone`: a lease subscription keeps a copy
255///   of each id in work to extend its lease.
256/// - `group`: the group a subscription reads; `#[field(group, fifo = true)]` keeps each group in
257///   order.
258/// - `partition_key`: the delivery's partition key.
259/// - `priority`: the claim order, a smaller value first.
260/// - `retry_after`: the time before which a row is not claimed.
261/// - `attempt`: the attempt number.
262/// - `locked_until`: the lease's expiry; it selects the lease form.
263/// - `processed_at`: acknowledgement marks the row instead of deleting it.
264/// - `headers` and `payload`: the delivery's headers and the message bytes.
265///
266/// `generated`, alone or beside a role, marks a column the database fills in.
267///
268/// # The headers layout
269///
270/// A `#[field(headers)]` field with `#[sqlx(flatten)]` holds a headers struct, which derives
271/// [`InboxHeaders`](derive@InboxHeaders) and describes the queue table: its attributes, the
272/// fields that play a role, and the service's own headers. The struct that flattens it is the
273/// message, which a handler takes itself, `&Message` or `&[Message]`, as in row mode. It takes
274/// `#[inbox(custom(..))]` alone, and no role beside its headers field. Its other fields are its
275/// own data: the default fetch reads them from the queue table, by name, so a column the table
276/// lacks stops the subscription at startup; with `custom(fetch)` the service's [`Fetch`] reads
277/// them from wherever they live. The delivery's headers hold the headers struct's fields without a
278/// role, built on the first read. The headers struct gets the generated insert, the message none.
279///
280/// ```
281/// # #[cfg(feature = "postgres")]
282/// # mod demo {
283/// use ruststream_sqlx::prelude::*;
284/// use sqlx::PgPool;
285///
286/// // order_jobs: job_id BIGSERIAL PRIMARY KEY, name TEXT, attempt SMALLINT DEFAULT 1,
287/// // tenant TEXT, note TEXT
288/// #[derive(Debug, Clone, InboxHeaders, sqlx::FromRow)]
289/// #[inbox(table = "order_jobs")]
290/// pub struct OrderHeaders {
291///     #[field(id, generated)]
292///     job_id: i64,
293///     #[field(group)]
294///     name: String,
295///     #[field(attempt, generated)]
296///     attempt: i16,
297///     tenant: String,
298/// }
299///
300/// #[derive(Debug, Clone, Inbox, sqlx::FromRow)]
301/// pub struct OrderJob {
302///     #[field(headers)]
303///     #[sqlx(flatten)]
304///     headers: OrderHeaders,
305///     note: Option<String>,
306/// }
307///
308/// // The header `tenant` comes from the headers struct, the note from the message's own column.
309/// #[subscriber(InboxQueue::<OrderJob>::new("orders"))]
310/// async fn ship(job: &OrderJob, ctx: &mut Context<'_>) -> HandlerOutcome {
311///     let tenant = ctx.headers().get_str("tenant").unwrap_or_default();
312///     tracing::info!(tenant, attempt = job.headers.attempt, note = ?job.note, "shipping");
313///     HandlerOutcome::ack()
314/// }
315///
316/// pub fn app(pool: PgPool) -> RustStream {
317///     RustStream::new(AppInfo::new("shop", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
318///         b.include(ship);
319///     })
320/// }
321/// # }
322/// # fn main() {}
323/// ```
324///
325/// A struct with a `payload` field is in payload mode: its handler takes the payload, decoded by a
326/// codec. A struct without one is in row mode: its handler takes the struct itself, as the driver
327/// read it. A struct in row mode derives `Clone`, because the test harness keeps a copy of each
328/// value. It does not derive `Deserialize`: a type that deserializes rides the codec.
329///
330/// A generic struct keeps its parameters: the impl requires `Send + Sync + 'static` of the struct
331/// and `Clone + Debug + Send + Sync + 'static` of the `id` field's type. In row mode a handler
332/// takes it where its parameters make it `Clone`.
333///
334/// # Column names
335///
336/// A column is named in one place, sqlx's attributes. `#[sqlx(rename = "..")]` names a field's
337/// column as written, `#[sqlx(rename_all = "..")]` recases every other field's name, a raw
338/// identifier loses its `r#`, and a `#[sqlx(skip)]` field reads no column. Outside the headers
339/// layout, a `#[sqlx(flatten)]` field reads columns the derive cannot see, so the statements
340/// select `*`, and a dead-letter move copies the row by position: the dead-letter table has the
341/// same columns in the same order.
342/// The other options of `#[sqlx(..)]`, such as `json`, `try_from` and `default`, belong to sqlx's
343/// own derive and pass through untouched.
344///
345/// # Checked at compile time
346///
347/// `#[inbox(checked, db = postgres)]` (or `mysql`, `sqlite`) also puts the statements the struct
348/// determines into sqlx's compile-time check, against `DATABASE_URL` or the `.sqlx` data of
349/// `cargo sqlx prepare`; the service's sqlx needs its `macros` feature. In the headers layout
350/// `checked` goes on the headers struct. [Statements checked at compile
351/// time](crate#statements-checked-at-compile-time) shows what it covers and what it refuses.
352///
353/// # Compile errors
354///
355/// A struct that cannot drive a queue does not compile, and the error points at the field or
356/// the name that causes it: no `id`, a role played twice, a column named twice, a role or
357/// `generated` on a field without a column, `fifo` outside the `group` role, `locked_until`,
358/// `fifo = true` or `claim` in `custom(..)` beside `advisory_lock`, `extend` in `custom(..)`
359/// without `locked_until`, `lock` or `unlock` in `custom(..)` without `advisory_lock` or without
360/// each other, `locked_until` on `clock = DatabaseClock`, a lock key naming no field, a dot in
361/// `table` or `schema`, an unknown isolation level or mode, `isolation` beside `mode`. In the
362/// headers layout: a flattened headers field whose type does not derive `InboxHeaders`, a role or
363/// a table attribute on the message, and a custom event the headers struct's form does not take.
364///
365/// A struct in row mode without `Clone` does not compile. The error points at its name and
366/// suggests the derive:
367///
368/// ```compile_fail,E0277
369/// use ruststream_sqlx::Inbox;
370///
371/// #[derive(Inbox)]
372/// #[inbox(table = "email_jobs")]
373/// struct SendEmail {
374///     #[field(id, generated)]
375///     job_id: i64,
376///     to: String,
377/// }
378/// ```
379///
380/// A struct in row mode that derives `Deserialize` does not compile either. Such a type rides the
381/// codec, and rustc reports conflicting implementations of
382/// [`Input`](ruststream::runtime::Input):
383///
384/// ```compile_fail,E0119
385/// use ruststream_sqlx::Inbox;
386/// use serde::Deserialize;
387///
388/// #[derive(Inbox, Clone, Deserialize)]
389/// #[inbox(table = "email_jobs")]
390/// struct SendEmail {
391///     #[field(id, generated)]
392///     job_id: i64,
393///     to: String,
394/// }
395/// ```
396///
397/// A handler that does not fit its table's mode meets the core's errors at the mount site: a
398/// handler of rows on a table in payload mode, a batch handler of rows with a reply or `Out`
399/// slots. [Row mode](crate#row-mode) shows both.
400#[cfg(feature = "inbox")]
401pub use ruststream_sqlx_macros::Inbox;
402/// Describes the queue table of a message assembled from it, and implements [`InboxHeaders`]
403/// for the struct.
404///
405/// The struct takes the table's attributes and the fields that play a role, as a struct deriving
406/// [`Inbox`](derive@Inbox) does; every field without a role is a header (see [`InboxHeaders`]).
407/// `custom(..)`, `payload` and `headers` belong to the message struct, which flattens this one
408/// into its `#[field(headers)]` field. `#[inbox(checked, db = ..)]` checks the table's statements
409/// at compile time, and the message then reads its rows in a `Fetch` of its own ([the headers
410/// layout, checked](crate#the-headers-layout-checked)).
411///
412/// # Examples
413///
414/// ```
415/// # #[cfg(feature = "postgres")]
416/// # mod demo {
417/// use ruststream_sqlx::prelude::*;
418/// use sqlx::PgPool;
419///
420/// #[derive(Debug, Clone, InboxHeaders, sqlx::FromRow)]
421/// #[inbox(table = "order_jobs")]
422/// pub struct OrderHeaders {
423///     #[field(id, generated)]
424///     job_id: i64,
425///     #[field(group)]
426///     name: String,
427///     tenant: String,
428/// }
429///
430/// #[derive(Debug, Clone, Inbox, sqlx::FromRow)]
431/// pub struct OrderJob {
432///     #[field(headers)]
433///     #[sqlx(flatten)]
434///     headers: OrderHeaders,
435///     note: Option<String>,
436/// }
437///
438/// #[subscriber(InboxQueue::<OrderJob>::new("orders"))]
439/// async fn ship(job: &OrderJob) -> HandlerOutcome {
440///     tracing::info!(tenant = %job.headers.tenant, note = ?job.note, "shipping");
441///     HandlerOutcome::ack()
442/// }
443///
444/// pub fn app(pool: PgPool) -> RustStream {
445///     RustStream::new(AppInfo::new("shop", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
446///         b.include(ship);
447///     })
448/// }
449/// # }
450/// # fn main() {}
451/// ```
452#[cfg(feature = "inbox")]
453pub use ruststream_sqlx_macros::InboxHeaders;
454
455/// Describes a service's outbox table with a struct: implements [`OutboxTable`], the description
456/// a record by hand writes, and [`HeaderRow`] for a `headers` field.
457///
458/// The struct is an ordinary sqlx struct: `#[outbox(..)]` names the table, sqlx's own attributes
459/// name the columns, and `#[field(..)]` marks the columns the outbox reads. The service writes the
460/// record of a published message itself, in [`outbox::Publish`]; the registry runs the other
461/// events on every [`OutboxDatabase`], from statements it builds from the description when the
462/// record type is registered. The derive checks the description against each built-in dialect
463/// while the service compiles.
464///
465/// # Examples
466///
467/// ```
468/// # #[cfg(all(feature = "postgres", feature = "json"))]
469/// # mod demo {
470/// use ruststream::OutgoingMessage;
471/// use ruststream_sqlx::{Outbox, outbox};
472/// use sqlx::types::Json;
473/// use sqlx::{PgConnection, Postgres};
474/// use std::collections::BTreeMap;
475///
476/// // app.outbox: id BIGSERIAL PRIMARY KEY, name TEXT, payload BYTEA, headers JSONB,
477/// // processed_at TIMESTAMPTZ
478/// #[derive(Outbox, sqlx::FromRow)]
479/// #[outbox(table = "outbox", schema = "app")]
480/// pub struct OrderEvent {
481///     #[field(id)]
482///     id: i64,
483///     #[field(name)]
484///     name: String,
485///     #[field(payload)]
486///     payload: Vec<u8>,
487///     #[field(headers)]
488///     headers: Option<Json<BTreeMap<String, String>>>,
489///     #[field(processed_at)]
490///     processed_at: Option<chrono::DateTime<chrono::Utc>>,
491/// }
492///
493/// impl outbox::Publish<Postgres> for OrderEvent {
494///     async fn publish(
495///         conn: &mut PgConnection,
496///         msg: &OutgoingMessage<'_>,
497///     ) -> Result<i64, sqlx::Error> {
498///         let headers: BTreeMap<String, String> = msg
499///             .headers()
500///             .iter()
501///             .map(|(name, value)| (name.to_owned(), String::from_utf8_lossy(value).into_owned()))
502///             .collect();
503///         sqlx::query_scalar(
504///             "INSERT INTO app.outbox (name, payload, headers) VALUES ($1, $2, $3) RETURNING id",
505///         )
506///         .bind(msg.name())
507///         .bind(msg.payload())
508///         .bind(Json(headers))
509///         .fetch_one(conn)
510///         .await
511///     }
512/// }
513/// # }
514/// # fn main() {}
515/// ```
516///
517/// # The table
518///
519/// `#[outbox(table = "..")]` names the table and is required; `schema = ".."` places it in a
520/// schema. Each names one thing and holds no dot.
521///
522/// # Roles
523///
524/// `#[field(..)]` gives a field one role:
525///
526/// - `id`, required: the record's identity, which a tracked message carries in
527///   [`OUTBOX_ID_HEADER`](outbox::OUTBOX_ID_HEADER) through its `Display` and `FromStr`, and which
528///   the default events bind as its own type.
529/// - `name`, required: the name the record was published under, read through `AsRef<str>`.
530/// - `payload`, required: the published bytes, read through `AsRef<[u8]>`.
531/// - `headers`: the published headers, a [`HeaderColumn`] type.
532/// - `processed_at`: the mark of a processed record, written from the database's clock, so any
533///   time type fits. Without it a processed record is deleted.
534///
535/// Every other field is a plain column sqlx reads. Column names come from sqlx's attributes:
536/// `rename`, `rename_all`, a raw identifier without `r#`, and `skip` for a field without a column;
537/// a `flatten` field makes the statements select `*`.
538///
539/// # Events
540///
541/// The default `Fetch` reads the record while it is unprocessed, `Ack` and `Discard` mark it
542/// processed, `Retry` leaves it as it is and runs no statement, and `Recover` reads the
543/// unprocessed records of one name. `#[outbox(custom(fetch, ack, retry, discard, recover))]`
544/// lists the events the service implements itself instead; `publish` has no default and is never
545/// listed.
546///
547/// # Compile errors
548///
549/// A struct the outbox cannot read does not compile, and the error points at the struct, the
550/// field or the word that causes it: no `id`, `name` or `payload`, a role played twice, a column
551/// named twice, a role on a field without a column, an unknown role or event, a dot in `table` or
552/// `schema`, an `id` read through `#[sqlx(json)]`.
553#[cfg(feature = "outbox")]
554pub use ruststream_sqlx_macros::Outbox;
555/// Registers outbox records under the names they track, and returns the registry.
556///
557/// `outbox! { pool: pool, "orders" => OrderEvent, "refunds" => RefundEvent }` is
558/// `Outbox::new(pool).register::<OrderEvent>("orders").register::<RefundEvent>("refunds")`.
559/// Without `pool:` the registry starts as `Outbox::deferred()`, and the pool is set once it is
560/// built. A name written twice does not compile, and the error points at its second literal.
561///
562/// # Examples
563///
564/// ```
565/// # #[cfg(all(feature = "outbox", feature = "postgres"))]
566/// # mod demo {
567/// # use ruststream::OutgoingMessage;
568/// # use ruststream::memory::prelude::*;
569/// # use ruststream_sqlx::{Outbox, outbox};
570/// # use serde::{Deserialize, Serialize};
571/// # use sqlx::postgres::{PgConnection, PgPool, Postgres};
572/// # #[derive(Outbox, sqlx::FromRow)]
573/// # #[outbox(table = "outbox")]
574/// # pub struct OrderOutbox { #[field(id)] id: i64, #[field(name)] name: String, #[field(payload)] payload: Vec<u8> }
575/// # impl outbox::Publish<Postgres> for OrderOutbox {
576/// #     async fn publish(conn: &mut PgConnection, msg: &OutgoingMessage<'_>) -> sqlx::Result<i64> {
577/// #         sqlx::query_scalar("INSERT INTO outbox (name, payload) VALUES ($1, $2) RETURNING id")
578/// #             .bind(msg.name()).bind(msg.payload()).fetch_one(conn).await
579/// #     }
580/// # }
581/// # #[derive(Deserialize)] pub struct PlaceOrder { id: u64 }
582/// # #[derive(Serialize, Deserialize, Outgoing)] #[outgoing(name = "orders")] pub struct OrderPlaced { id: u64 }
583/// # #[subscriber("checkout", reply)] async fn place(cmd: &PlaceOrder) -> OrderPlaced { OrderPlaced { id: cmd.id } }
584/// # #[subscriber("orders")] async fn fulfil(_: &OrderPlaced) -> HandlerOutcome { HandlerOutcome::ack() }
585/// use std::io;
586///
587/// /// Inside `#[ruststream::app]`: no `pool:`, and the pool arrives in `on_startup`.
588/// pub fn app() -> impl App {
589///     let tracking = outbox! { "orders" => OrderOutbox };
590///     let registry = tracking.clone();
591///     RustStream::new(AppInfo::new("orders", "0.1.0"))
592///         .on_startup(async move |()| {
593///             let pool = PgPool::connect("postgres://localhost/orders")
594///                 .await
595///                 .map_err(io::Error::other)?;
596///             registry.set_pool(pool.clone()).map_err(io::Error::other)?;
597///             Ok::<_, io::Error>(pool)
598///         })
599///         .layer(tracking.layer())
600///         .publish_layer(tracking.publish_layer())
601///         .with_broker(MemoryBroker::new(), |b| {
602///             b.include(place).out_reply(Publish);
603///             b.include(fulfil);
604///             b.after_startup(Publish, tracking.republish());
605///         })
606/// }
607///
608/// /// An app built inside a running runtime takes the pool it has.
609/// pub fn app_with(pool: PgPool) -> impl App {
610///     let tracking = outbox! { pool: pool, "orders" => OrderOutbox };
611///     RustStream::new(AppInfo::new("orders", "0.1.0"))
612///         .publish_layer(tracking.publish_layer())
613///         .with_broker(MemoryBroker::new(), |b| {
614///             b.include(place).out_reply(Publish);
615///         })
616/// }
617/// # }
618/// # fn main() {}
619/// ```
620#[cfg(feature = "outbox")]
621pub use ruststream_sqlx_macros::outbox;