Skip to main content

ruststream_sqlx/inbox/spec/
builder.rs

1//! `InboxSpec`, the typed builder of a queue table's description, and `InboxTable`, the trait a
2//! table described by hand implements.
3
4use std::fmt::{self, Debug, Formatter};
5use std::marker::PhantomData;
6
7use ruststream_sqlx_dialect::{Column, Form, KeyPart, Opening, Role, TableSpec};
8
9use super::{
10    Advisory, Attempt, AttemptFrom, Clock, Declaration, Fifo, HeaderFields, Headers, Key, Lease,
11    OpeningLevel, Opens, OwnEvent, Payload, ProcessedAt, Push, RetryAfter, Valid,
12};
13use crate::TimeSource;
14
15/// A queue table described by hand.
16///
17/// A struct implements it with the table's description in `TABLE`, built by [`InboxSpec`], and
18/// the builder's final type in `type Table`. The typed setters of the chain decide the type, and
19/// the compiler holds the chain to it, so the time types of the chain come from `type Table`.
20///
21/// # Examples
22///
23/// ```
24/// # #[cfg(all(feature = "sqlite", feature = "chrono"))]
25/// # mod demo {
26/// use chrono::{DateTime, Utc};
27/// use std::time::Duration;
28///
29/// use ruststream_sqlx::dialect::Column;
30/// use ruststream_sqlx::prelude::*;
31/// use ruststream_sqlx::spec::{Attempt, Lease, Payload, ProcessedAt, RetryAfter};
32/// use ruststream_sqlx::{AttemptRow, InboxSpec, InboxTable, PayloadRow};
33/// use serde::Deserialize;
34/// use sqlx::{Sqlite, SqlitePool};
35///
36/// #[derive(sqlx::FromRow)]
37/// pub struct EmailJob {
38///     job_id: i64,
39///     attempt: i16,
40///     payload: Vec<u8>,
41/// }
42///
43/// impl InboxTable for EmailJob {
44///     type Id = i64;
45///     type Table = InboxSpec<(
46///         Lease<DateTime<Utc>>,
47///         Payload,
48///         Attempt,
49///         RetryAfter<DateTime<Utc>>,
50///         ProcessedAt<DateTime<Utc>>,
51///     )>;
52///     const TABLE: Self::Table = InboxSpec::new("email_jobs", Column::new("job_id").generated())
53///         .group(Column::new("name"))
54///         .lease(Column::new("locked_until"))
55///         .payload(Column::new("payload"))
56///         .attempt(Column::new("attempt").generated())
57///         .retry_after(Column::new("retry_after"))
58///         .processed_at(Column::new("processed_at"));
59///
60///     fn id(&self) -> &i64 {
61///         &self.job_id
62///     }
63/// }
64///
65/// impl PayloadRow for EmailJob {
66///     type Column = Vec<u8>;
67///
68///     fn payload(&self) -> &[u8] {
69///         &self.payload
70///     }
71/// }
72///
73/// impl AttemptRow for EmailJob {
74///     type Attempt = i16;
75///
76///     fn attempt(&self) -> &i16 {
77///         &self.attempt
78///     }
79/// }
80///
81/// #[derive(Deserialize)]
82/// struct Email {
83///     to: String,
84/// }
85///
86/// // An address the mail server refuses comes back a minute later.
87/// #[subscriber(InboxQueue::<EmailJob>::new("emails"))]
88/// async fn send(email: &Email) -> HandlerOutcome {
89///     if email.to.ends_with("@example.com") {
90///         HandlerOutcome::ack()
91///     } else {
92///         HandlerOutcome::retry_after(Duration::from_secs(60))
93///     }
94/// }
95///
96/// pub fn app(pool: SqlitePool) -> RustStream {
97///     RustStream::new(AppInfo::new("mailer", "1.0.0")).with_broker(
98///         SqlxBroker::<Sqlite>::new(pool),
99///         |b| {
100///             b.include(send);
101///         },
102///     )
103/// }
104/// # }
105/// # fn main() {}
106/// ```
107pub trait InboxTable: Sized + Send + Sync + 'static {
108    /// The type of the column that identifies a row.
109    type Id: Clone + Debug + Send + Sync + 'static;
110
111    /// The table's description as a type: `InboxSpec<(..)>`, listing the markers the chain of
112    /// `TABLE` sets, in the order it sets them.
113    type Table: Valid;
114
115    /// The table's description.
116    const TABLE: Self::Table;
117
118    /// The row's id.
119    fn id(&self) -> &Self::Id;
120}
121
122/// The description of a queue table, with its settings as types.
123///
124/// `new` starts a table in the row lock form, in row mode. The column-only setters (`within`,
125/// `group`, `priority`, `data`, `fetching`, `selecting_all`) keep the type. Each typed setter
126/// adds its marker of [`spec`](crate::spec) to `Settings`, so a table's `type Table` lists the
127/// markers its chain sets, in that order. A setting set twice does not compile. Every setter is a
128/// `const fn`, and [`spec`](Self::spec) hands back the dialect's [`TableSpec`].
129///
130/// # Examples
131///
132/// ```
133/// # #[cfg(all(feature = "sqlite", feature = "chrono"))]
134/// # mod demo {
135/// use chrono::{DateTime, Utc};
136/// use ruststream_sqlx::dialect::{Column, KeyPart, level};
137/// use ruststream_sqlx::prelude::*;
138/// use ruststream_sqlx::spec::{Advisory, Clock, Opens, Payload, RetryAfter};
139/// use ruststream_sqlx::{DatabaseClock, InboxSpec, InboxTable, PayloadRow};
140/// use serde::Deserialize;
141/// use sqlx::{Sqlite, SqlitePool};
142///
143/// #[derive(sqlx::FromRow)]
144/// pub struct SyncJob {
145///     id: i64,
146///     payload: Vec<u8>,
147/// }
148///
149/// impl InboxTable for SyncJob {
150///     type Id = i64;
151///     type Table = InboxSpec<(
152///         Advisory,
153///         Clock<DatabaseClock>,
154///         Opens<level::Immediate>,
155///         RetryAfter<DateTime<Utc>>,
156///         Payload,
157///     )>;
158///     const TABLE: Self::Table = InboxSpec::new("sync_jobs", Column::new("id").generated())
159///         .advisory(&[KeyPart::Literal("sync-"), KeyPart::Column("tenant")])
160///         .clock::<DatabaseClock>()
161///         .opens::<level::Immediate>()
162///         .retry_after(Column::new("retry_after"))
163///         .payload(Column::new("payload"));
164///
165///     fn id(&self) -> &i64 {
166///         &self.id
167///     }
168/// }
169///
170/// impl PayloadRow for SyncJob {
171///     type Column = Vec<u8>;
172///
173///     fn payload(&self) -> &[u8] {
174///         &self.payload
175///     }
176/// }
177///
178/// #[derive(Deserialize)]
179/// struct Sync {
180///     tenant: String,
181/// }
182///
183/// // One tenant syncs at a time: the lock key holds the tenant.
184/// #[subscriber(InboxQueue::<SyncJob>::new("sync"))]
185/// async fn sync(job: &Sync) -> HandlerOutcome {
186///     tracing::info!(tenant = %job.tenant, "syncing");
187///     HandlerOutcome::ack()
188/// }
189///
190/// pub fn app(pool: SqlitePool) -> RustStream {
191///     RustStream::new(AppInfo::new("sync", "1.0.0")).with_broker(
192///         SqlxBroker::<Sqlite>::new(pool),
193///         |b| {
194///             b.include(sync);
195///         },
196///     )
197/// }
198/// # }
199/// # fn main() {}
200/// ```
201pub struct InboxSpec<Settings = ()> {
202    spec: TableSpec<'static>,
203    settings: PhantomData<fn() -> Settings>,
204}
205
206// By hand: a derive would require each marker of `Settings` to implement the trait, and the
207// markers are never values.
208impl<Settings> Debug for InboxSpec<Settings> {
209    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
210        formatter
211            .debug_struct("InboxSpec")
212            .field("spec", &self.spec)
213            .finish_non_exhaustive()
214    }
215}
216
217impl<Settings> Clone for InboxSpec<Settings> {
218    fn clone(&self) -> Self {
219        *self
220    }
221}
222
223impl<Settings> Copy for InboxSpec<Settings> {}
224
225impl<Settings> PartialEq for InboxSpec<Settings> {
226    fn eq(&self, other: &Self) -> bool {
227        self.spec == other.spec
228    }
229}
230
231impl<Settings> Eq for InboxSpec<Settings> {}
232
233impl InboxSpec {
234    /// A table in the connection's default schema, with the column that identifies a row: in the
235    /// row lock form, in row mode, with no other setting.
236    ///
237    /// # Examples
238    ///
239    /// ```
240    /// # #[cfg(feature = "postgres")]
241    /// # mod demo {
242    /// use ruststream_sqlx::dialect::Column;
243    /// use ruststream_sqlx::spec::Payload;
244    /// use ruststream_sqlx::{InboxSpec, InboxTable, PayloadRow};
245    /// # use ruststream_sqlx::prelude::*;
246    /// # use serde::Deserialize;
247    /// # use sqlx::PgPool;
248    ///
249    /// // webhooks: id BIGSERIAL PRIMARY KEY, body BYTEA NOT NULL
250    /// #[derive(sqlx::FromRow)]
251    /// pub struct Webhook {
252    ///     id: i64,
253    ///     body: Vec<u8>,
254    /// }
255    ///
256    /// impl InboxTable for Webhook {
257    ///     type Id = i64;
258    ///     type Table = InboxSpec<(Payload,)>;
259    ///     // The database numbers the rows, so an insert leaves `id` out.
260    ///     const TABLE: Self::Table = InboxSpec::new("webhooks", Column::new("id").generated())
261    ///         .payload(Column::new("body"));
262    ///
263    ///     fn id(&self) -> &i64 {
264    ///         &self.id
265    ///     }
266    /// }
267    ///
268    /// impl PayloadRow for Webhook {
269    ///     type Column = Vec<u8>;
270    ///
271    ///     fn payload(&self) -> &[u8] {
272    ///         &self.body
273    ///     }
274    /// }
275    /// # #[derive(Deserialize)]
276    /// # struct Event { kind: String }
277    /// # #[subscriber(InboxQueue::<Webhook>::new("webhooks"))]
278    /// # async fn receive(event: &Event) -> HandlerOutcome {
279    /// #     tracing::info!(kind = %event.kind, "received");
280    /// #     HandlerOutcome::ack()
281    /// # }
282    /// # pub fn app(pool: PgPool) -> RustStream {
283    /// #     RustStream::new(AppInfo::new("hooks", "1.0.0")).with_broker(SqlxBroker::new(pool), |b| {
284    /// #         b.include(receive);
285    /// #     })
286    /// # }
287    /// # }
288    /// # fn main() {}
289    /// ```
290    #[must_use]
291    pub const fn new(table: &'static str, id: Column<'static>) -> Self {
292        Self {
293            spec: TableSpec::new(table, id, Form::RowLock),
294            settings: PhantomData,
295        }
296    }
297}
298
299impl<Settings> InboxSpec<Settings> {
300    /// The table's description, which every statement is built from.
301    ///
302    /// # Examples
303    ///
304    /// ```
305    /// # #[cfg(feature = "postgres")]
306    /// # mod demo {
307    /// use ruststream_sqlx::dialect::Column;
308    /// use ruststream_sqlx::dialect::insert::{self, Sql};
309    /// use ruststream_sqlx::spec::Payload;
310    /// use ruststream_sqlx::{InboxSpec, InboxTable, PayloadRow};
311    /// use sqlx::PgConnection;
312    /// # use ruststream_sqlx::prelude::*;
313    /// # use serde::Deserialize;
314    /// # use sqlx::PgPool;
315    ///
316    /// #[derive(sqlx::FromRow)]
317    /// pub struct ReceiptJob {
318    ///     id: i64,
319    ///     payload: Vec<u8>,
320    /// }
321    ///
322    /// impl InboxTable for ReceiptJob {
323    ///     type Id = i64;
324    ///     type Table = InboxSpec<(Payload,)>;
325    ///     const TABLE: Self::Table = InboxSpec::new("receipt_jobs", Column::new("id").generated())
326    ///         .group(Column::new("name"))
327    ///         .payload(Column::new("payload"));
328    ///
329    ///     fn id(&self) -> &i64 {
330    ///         &self.id
331    ///     }
332    /// }
333    ///
334    /// impl PayloadRow for ReceiptJob {
335    ///     type Column = Vec<u8>;
336    ///
337    ///     fn payload(&self) -> &[u8] {
338    ///         &self.payload
339    ///     }
340    /// }
341    ///
342    /// // INSERT INTO "receipt_jobs" ("name", "payload") VALUES ($1, $2)
343    /// const INSERT: Sql<128> = insert::postgres(&ReceiptJob::TABLE.spec());
344    ///
345    /// /// Queues a receipt in the order's transaction, so the two commit together.
346    /// pub async fn enqueue(tx: &mut PgConnection, receipt: &[u8]) -> Result<(), sqlx::Error> {
347    ///     sqlx::query(INSERT.as_str())
348    ///         .bind("receipts")
349    ///         .bind(receipt)
350    ///         .execute(tx)
351    ///         .await?;
352    ///     Ok(())
353    /// }
354    /// # #[derive(Deserialize)]
355    /// # struct Receipt { order: i64 }
356    /// # #[subscriber(InboxQueue::<ReceiptJob>::new("receipts"))]
357    /// # async fn send(receipt: &Receipt) -> HandlerOutcome {
358    /// #     tracing::info!(order = receipt.order, "sending a receipt");
359    /// #     HandlerOutcome::ack()
360    /// # }
361    /// # pub fn app(pool: PgPool) -> RustStream {
362    /// #     RustStream::new(AppInfo::new("shop", "1.0.0")).with_broker(SqlxBroker::new(pool), |b| {
363    /// #         b.include(send);
364    /// #     })
365    /// # }
366    /// # }
367    /// # fn main() {}
368    /// ```
369    #[must_use]
370    pub const fn spec(&self) -> TableSpec<'static> {
371        self.spec
372    }
373
374    /// The same table, inside `schema`.
375    ///
376    /// # Examples
377    ///
378    /// ```
379    /// # #[cfg(feature = "postgres")]
380    /// # mod demo {
381    /// use ruststream_sqlx::dialect::Column;
382    /// use ruststream_sqlx::spec::Payload;
383    /// use ruststream_sqlx::{InboxSpec, InboxTable, PayloadRow};
384    /// # use ruststream_sqlx::prelude::*;
385    /// # use serde::Deserialize;
386    /// # use sqlx::PgPool;
387    ///
388    /// #[derive(sqlx::FromRow)]
389    /// pub struct InvoiceJob {
390    ///     id: i64,
391    ///     payload: Vec<u8>,
392    /// }
393    ///
394    /// impl InboxTable for InvoiceJob {
395    ///     type Id = i64;
396    ///     type Table = InboxSpec<(Payload,)>;
397    ///     // The queue lives beside the tables it serves: `billing.invoice_jobs`.
398    ///     const TABLE: Self::Table = InboxSpec::new("invoice_jobs", Column::new("id").generated())
399    ///         .within("billing")
400    ///         .payload(Column::new("payload"));
401    ///
402    ///     fn id(&self) -> &i64 {
403    ///         &self.id
404    ///     }
405    /// }
406    ///
407    /// impl PayloadRow for InvoiceJob {
408    ///     type Column = Vec<u8>;
409    ///
410    ///     fn payload(&self) -> &[u8] {
411    ///         &self.payload
412    ///     }
413    /// }
414    /// # #[derive(Deserialize)]
415    /// # struct Invoice { number: String }
416    /// # #[subscriber(InboxQueue::<InvoiceJob>::new("invoices"))]
417    /// # async fn issue(invoice: &Invoice) -> HandlerOutcome {
418    /// #     tracing::info!(number = %invoice.number, "issuing");
419    /// #     HandlerOutcome::ack()
420    /// # }
421    /// # pub fn app(pool: PgPool) -> RustStream {
422    /// #     RustStream::new(AppInfo::new("billing", "1.0.0")).with_broker(SqlxBroker::new(pool), |b| {
423    /// #         b.include(issue);
424    /// #     })
425    /// # }
426    /// # }
427    /// # fn main() {}
428    /// ```
429    #[must_use]
430    pub const fn within(self, schema: &'static str) -> Self {
431        Self {
432            spec: self.spec.within(schema),
433            ..self
434        }
435    }
436
437    /// The same table, split into groups by `column`; a subscription reads one group.
438    ///
439    /// # Examples
440    ///
441    /// ```
442    /// # #[cfg(feature = "postgres")]
443    /// # mod demo {
444    /// use ruststream_sqlx::dialect::Column;
445    /// use ruststream_sqlx::prelude::*;
446    /// use ruststream_sqlx::spec::Payload;
447    /// use ruststream_sqlx::{InboxSpec, InboxTable, PayloadRow};
448    /// use serde::Deserialize;
449    /// # use sqlx::PgPool;
450    ///
451    /// #[derive(sqlx::FromRow)]
452    /// pub struct MediaJob {
453    ///     id: i64,
454    ///     payload: Vec<u8>,
455    /// }
456    ///
457    /// impl InboxTable for MediaJob {
458    ///     type Id = i64;
459    ///     type Table = InboxSpec<(Payload,)>;
460    ///     // One table holds two queues: the `kind` column names the queue of each row.
461    ///     const TABLE: Self::Table = InboxSpec::new("media_jobs", Column::new("id").generated())
462    ///         .group(Column::new("kind"))
463    ///         .payload(Column::new("payload"));
464    ///
465    ///     fn id(&self) -> &i64 {
466    ///         &self.id
467    ///     }
468    /// }
469    ///
470    /// impl PayloadRow for MediaJob {
471    ///     type Column = Vec<u8>;
472    ///
473    ///     fn payload(&self) -> &[u8] {
474    ///         &self.payload
475    ///     }
476    /// }
477    ///
478    /// #[derive(Deserialize)]
479    /// struct Upload {
480    ///     file: String,
481    /// }
482    ///
483    /// #[subscriber(InboxQueue::<MediaJob>::new("thumbnails"))]
484    /// async fn thumbnail(upload: &Upload) -> HandlerOutcome {
485    ///     tracing::info!(file = %upload.file, "drawing a thumbnail");
486    ///     HandlerOutcome::ack()
487    /// }
488    ///
489    /// #[subscriber(InboxQueue::<MediaJob>::new("transcodes"))]
490    /// async fn transcode(upload: &Upload) -> HandlerOutcome {
491    ///     tracing::info!(file = %upload.file, "transcoding");
492    ///     HandlerOutcome::ack()
493    /// }
494    /// # pub fn app(pool: PgPool) -> RustStream {
495    /// #     RustStream::new(AppInfo::new("media", "1.0.0")).with_broker(SqlxBroker::new(pool), |b| {
496    /// #         b.include(thumbnail);
497    /// #         b.include(transcode);
498    /// #     })
499    /// # }
500    /// # }
501    /// # fn main() {}
502    /// ```
503    #[must_use]
504    pub const fn group(self, column: Column<'static>) -> Self {
505        Self {
506            spec: self.spec.group(column),
507            ..self
508        }
509    }
510
511    /// The same table, with rows claimed in the order of `column`, a smaller value first.
512    ///
513    /// # Examples
514    ///
515    /// ```
516    /// # #[cfg(feature = "postgres")]
517    /// # mod demo {
518    /// use ruststream_sqlx::dialect::Column;
519    /// use ruststream_sqlx::spec::Payload;
520    /// use ruststream_sqlx::{InboxSpec, InboxTable, PayloadRow};
521    /// # use ruststream_sqlx::prelude::*;
522    /// # use serde::Deserialize;
523    /// # use sqlx::PgPool;
524    ///
525    /// // tickets: id BIGSERIAL PRIMARY KEY, severity SMALLINT NOT NULL, payload BYTEA NOT NULL
526    /// #[derive(sqlx::FromRow)]
527    /// pub struct Ticket {
528    ///     id: i64,
529    ///     payload: Vec<u8>,
530    /// }
531    ///
532    /// impl InboxTable for Ticket {
533    ///     type Id = i64;
534    ///     type Table = InboxSpec<(Payload,)>;
535    ///     // An outage (severity 0) is answered before a question (severity 3).
536    ///     const TABLE: Self::Table = InboxSpec::new("tickets", Column::new("id").generated())
537    ///         .priority(Column::new("severity"))
538    ///         .payload(Column::new("payload"));
539    ///
540    ///     fn id(&self) -> &i64 {
541    ///         &self.id
542    ///     }
543    /// }
544    ///
545    /// impl PayloadRow for Ticket {
546    ///     type Column = Vec<u8>;
547    ///
548    ///     fn payload(&self) -> &[u8] {
549    ///         &self.payload
550    ///     }
551    /// }
552    /// # #[derive(Deserialize)]
553    /// # struct Request { subject: String }
554    /// # #[subscriber(InboxQueue::<Ticket>::new("support"))]
555    /// # async fn triage(request: &Request) -> HandlerOutcome {
556    /// #     tracing::info!(subject = %request.subject, "triaging");
557    /// #     HandlerOutcome::ack()
558    /// # }
559    /// # pub fn app(pool: PgPool) -> RustStream {
560    /// #     RustStream::new(AppInfo::new("support", "1.0.0"))
561    /// #         .with_broker(SqlxBroker::new(pool), |b| {
562    /// #             b.include(triage);
563    /// #         })
564    /// # }
565    /// # }
566    /// # fn main() {}
567    /// ```
568    #[must_use]
569    pub const fn priority(self, column: Column<'static>) -> Self {
570        Self {
571            spec: self.spec.priority(column),
572            ..self
573        }
574    }
575
576    /// The same table, with the columns of the message's own data: the columns without a role.
577    ///
578    /// # Examples
579    ///
580    /// ```
581    /// # #[cfg(feature = "postgres")]
582    /// # mod demo {
583    /// use ruststream::runtime::{Input, SoloCarried};
584    /// use ruststream_sqlx::dialect::Column;
585    /// use ruststream_sqlx::prelude::*;
586    /// use ruststream_sqlx::{InboxSpec, InboxTable};
587    /// # use sqlx::PgPool;
588    ///
589    /// // shipments: id BIGSERIAL PRIMARY KEY, carrier TEXT NOT NULL, tracking TEXT NOT NULL
590    /// #[derive(Debug, Clone, sqlx::FromRow)]
591    /// pub struct Shipment {
592    ///     id: i64,
593    ///     carrier: String,
594    ///     tracking: String,
595    /// }
596    ///
597    /// impl InboxTable for Shipment {
598    ///     type Id = i64;
599    ///     type Table = InboxSpec;
600    ///     // Row mode: the claim reads these columns, and the handler takes the row.
601    ///     const TABLE: Self::Table = InboxSpec::new("shipments", Column::new("id").generated())
602    ///         .data(&[Column::new("carrier"), Column::new("tracking")]);
603    ///
604    ///     fn id(&self) -> &i64 {
605    ///         &self.id
606    ///     }
607    /// }
608    ///
609    /// impl Input for Shipment {
610    ///     type Axis = SoloCarried<Self>;
611    /// }
612    ///
613    /// #[subscriber(InboxQueue::<Shipment>::new("shipments"))]
614    /// async fn track(shipment: &Shipment) -> HandlerOutcome {
615    ///     tracing::info!(carrier = %shipment.carrier, tracking = %shipment.tracking, "tracking");
616    ///     HandlerOutcome::ack()
617    /// }
618    /// # pub fn app(pool: PgPool) -> RustStream {
619    /// #     RustStream::new(AppInfo::new("logistics", "1.0.0"))
620    /// #         .with_broker(SqlxBroker::new(pool), |b| {
621    /// #             b.include(track);
622    /// #         })
623    /// # }
624    /// # }
625    /// # fn main() {}
626    /// ```
627    #[must_use]
628    pub const fn data(self, columns: &'static [Column<'static>]) -> Self {
629        Self {
630            spec: self.spec.data(columns),
631            ..self
632        }
633    }
634
635    /// The same table, with the columns a message assembled from header fields reads beside
636    /// them.
637    ///
638    /// # Examples
639    ///
640    /// ```
641    /// # #[cfg(feature = "postgres")]
642    /// # mod demo {
643    /// use ruststream::HeaderMap;
644    /// use ruststream::runtime::{Input, SoloCarried};
645    /// use ruststream_sqlx::dialect::Column;
646    /// use ruststream_sqlx::spec;
647    /// use ruststream_sqlx::{HeaderFields, InboxSpec, InboxTable, put_header};
648    /// # use ruststream_sqlx::prelude::*;
649    /// # use sqlx::PgPool;
650    ///
651    /// #[derive(Debug, Clone, sqlx::FromRow)]
652    /// pub struct RefundHeaders {
653    ///     id: i64,
654    ///     tenant: String,
655    ///     order_id: i64,
656    /// }
657    ///
658    /// #[derive(Debug, Clone, sqlx::FromRow)]
659    /// pub struct Refund {
660    ///     #[sqlx(flatten)]
661    ///     headers: RefundHeaders,
662    ///     reason: Option<String>,
663    /// }
664    ///
665    /// impl InboxTable for Refund {
666    ///     type Id = i64;
667    ///     type Table = InboxSpec<(spec::HeaderFields,)>;
668    ///     // `tenant` and `order_id` become headers; the claim reads `reason` for the message.
669    ///     const TABLE: Self::Table = InboxSpec::new("refunds", Column::new("id").generated())
670    ///         .data(&[Column::new("tenant"), Column::new("order_id")])
671    ///         .fetching(&[Column::new("reason")])
672    ///         .header_fields();
673    ///
674    ///     fn id(&self) -> &i64 {
675    ///         &self.headers.id
676    ///     }
677    /// }
678    ///
679    /// impl Input for Refund {
680    ///     type Axis = SoloCarried<Self>;
681    /// }
682    ///
683    /// impl HeaderFields for Refund {
684    ///     const NAMES: &'static [&'static str] = &["tenant", "order_id"];
685    ///
686    ///     fn header_map(&self) -> HeaderMap {
687    ///         let mut headers = HeaderMap::with_capacity(Self::NAMES.len());
688    ///         put_header(&mut headers, "tenant", &self.headers.tenant);
689    ///         put_header(&mut headers, "order_id", &self.headers.order_id);
690    ///         headers
691    ///     }
692    /// }
693    /// # #[subscriber(InboxQueue::<Refund>::new("refunds"))]
694    /// # async fn pay_back(job: &Refund) -> HandlerOutcome {
695    /// #     tracing::info!(order = job.headers.order_id, reason = ?job.reason, "refunding");
696    /// #     HandlerOutcome::ack()
697    /// # }
698    /// # pub fn app(pool: PgPool) -> RustStream {
699    /// #     RustStream::new(AppInfo::new("payments", "1.0.0"))
700    /// #         .with_broker(SqlxBroker::new(pool), |b| {
701    /// #             b.include(pay_back);
702    /// #         })
703    /// # }
704    /// # }
705    /// # fn main() {}
706    /// ```
707    #[must_use]
708    pub const fn fetching(self, columns: &'static [Column<'static>]) -> Self {
709        Self {
710            spec: self.spec.fetching(columns),
711            ..self
712        }
713    }
714
715    /// The same table, read with `*`: the row flattens another struct, so the columns are not
716    /// all known.
717    ///
718    /// # Examples
719    ///
720    /// ```
721    /// # #[cfg(feature = "postgres")]
722    /// # mod demo {
723    /// use ruststream::runtime::{Input, SoloCarried};
724    /// use ruststream_sqlx::dialect::Column;
725    /// use ruststream_sqlx::{InboxSpec, InboxTable};
726    /// # use ruststream_sqlx::prelude::*;
727    /// # use sqlx::PgPool;
728    ///
729    /// /// The address type the service shares with its other tables.
730    /// #[derive(Debug, Clone, sqlx::FromRow)]
731    /// pub struct Address {
732    ///     street: String,
733    ///     city: String,
734    /// }
735    ///
736    /// #[derive(Debug, Clone, sqlx::FromRow)]
737    /// pub struct Parcel {
738    ///     id: i64,
739    ///     #[sqlx(flatten)]
740    ///     address: Address,
741    /// }
742    ///
743    /// impl InboxTable for Parcel {
744    ///     type Id = i64;
745    ///     type Table = InboxSpec;
746    ///     // The claim reads every column, whatever `Address` holds.
747    ///     const TABLE: Self::Table =
748    ///         InboxSpec::new("parcels", Column::new("id").generated()).selecting_all();
749    ///
750    ///     fn id(&self) -> &i64 {
751    ///         &self.id
752    ///     }
753    /// }
754    ///
755    /// impl Input for Parcel {
756    ///     type Axis = SoloCarried<Self>;
757    /// }
758    /// # #[subscriber(InboxQueue::<Parcel>::new("parcels"))]
759    /// # async fn route(parcel: &Parcel) -> HandlerOutcome {
760    /// #     tracing::info!(street = %parcel.address.street, city = %parcel.address.city, "routing");
761    /// #     HandlerOutcome::ack()
762    /// # }
763    /// # pub fn app(pool: PgPool) -> RustStream {
764    /// #     RustStream::new(AppInfo::new("logistics", "1.0.0"))
765    /// #         .with_broker(SqlxBroker::new(pool), |b| {
766    /// #             b.include(route);
767    /// #         })
768    /// # }
769    /// # }
770    /// # fn main() {}
771    /// ```
772    #[must_use]
773    pub const fn selecting_all(self) -> Self {
774        Self {
775            spec: self.spec.selecting_all(),
776            ..self
777        }
778    }
779
780    /// The same table in the lease form: a claim sets `column`, the lease's expiry in `Time`, and
781    /// commits. Adds [`Lease<Time>`](Lease).
782    ///
783    /// # Examples
784    ///
785    /// ```
786    /// # #[cfg(all(feature = "postgres", feature = "chrono"))]
787    /// # mod demo {
788    /// use chrono::{DateTime, Utc};
789    /// use ruststream_sqlx::dialect::Column;
790    /// use ruststream_sqlx::spec::{Lease, Payload};
791    /// use ruststream_sqlx::{InboxSpec, InboxTable, PayloadRow};
792    /// # use ruststream_sqlx::prelude::*;
793    /// # use serde::Deserialize;
794    /// # use sqlx::PgPool;
795    ///
796    /// // report_jobs: id BIGSERIAL PRIMARY KEY, locked_until TIMESTAMPTZ NULL, payload BYTEA NOT NULL
797    /// #[derive(sqlx::FromRow)]
798    /// pub struct ReportJob {
799    ///     id: i64,
800    ///     payload: Vec<u8>,
801    /// }
802    ///
803    /// impl InboxTable for ReportJob {
804    ///     type Id = i64;
805    ///     type Table = InboxSpec<(Lease<DateTime<Utc>>, Payload)>;
806    ///     // A render that takes minutes holds its row by `locked_until`, not by an open transaction.
807    ///     const TABLE: Self::Table = InboxSpec::new("report_jobs", Column::new("id").generated())
808    ///         .lease(Column::new("locked_until"))
809    ///         .payload(Column::new("payload"));
810    ///
811    ///     fn id(&self) -> &i64 {
812    ///         &self.id
813    ///     }
814    /// }
815    ///
816    /// impl PayloadRow for ReportJob {
817    ///     type Column = Vec<u8>;
818    ///
819    ///     fn payload(&self) -> &[u8] {
820    ///         &self.payload
821    ///     }
822    /// }
823    /// # #[derive(Deserialize)]
824    /// # struct Report { month: String }
825    /// # #[subscriber(InboxQueue::<ReportJob>::new("reports"))]
826    /// # async fn render(report: &Report) -> HandlerOutcome {
827    /// #     tracing::info!(month = %report.month, "rendering");
828    /// #     HandlerOutcome::ack()
829    /// # }
830    /// # pub fn app(pool: PgPool) -> RustStream {
831    /// #     RustStream::new(AppInfo::new("reports", "1.0.0")).with_broker(SqlxBroker::new(pool), |b| {
832    /// #         b.include(render);
833    /// #     })
834    /// # }
835    /// # }
836    /// # fn main() {}
837    /// ```
838    #[must_use]
839    pub const fn lease<Time>(
840        self,
841        column: Column<'static>,
842    ) -> InboxSpec<<Settings as Push<Lease<Time>>>::Out>
843    where
844        Settings: Push<Lease<Time>>,
845        <Settings as Push<Lease<Time>>>::Out: Declaration,
846    {
847        Self::with(&self.reformed(Form::Lease(column)))
848    }
849
850    /// The same table in the advisory lock form: a session lock on the key these parts build from
851    /// the row holds the row. Adds [`Advisory`].
852    ///
853    /// # Examples
854    ///
855    /// ```
856    /// # #[cfg(feature = "postgres")]
857    /// # mod demo {
858    /// use ruststream_sqlx::dialect::{Column, KeyPart};
859    /// use ruststream_sqlx::spec::{Advisory, Payload};
860    /// use ruststream_sqlx::{InboxSpec, InboxTable, PayloadRow};
861    /// # use ruststream_sqlx::prelude::*;
862    /// # use serde::Deserialize;
863    /// # use sqlx::PgPool;
864    ///
865    /// // postings: id BIGSERIAL PRIMARY KEY, account TEXT NOT NULL, payload BYTEA NOT NULL
866    /// #[derive(sqlx::FromRow)]
867    /// pub struct Posting {
868    ///     id: i64,
869    ///     payload: Vec<u8>,
870    /// }
871    ///
872    /// impl InboxTable for Posting {
873    ///     type Id = i64;
874    ///     type Table = InboxSpec<(Advisory, Payload)>;
875    ///     // One account's postings run one at a time across every process: the key is `ledger-<account>`.
876    ///     const TABLE: Self::Table = InboxSpec::new("postings", Column::new("id").generated())
877    ///         .advisory(&[KeyPart::Literal("ledger-"), KeyPart::Column("account")])
878    ///         .payload(Column::new("payload"));
879    ///
880    ///     fn id(&self) -> &i64 {
881    ///         &self.id
882    ///     }
883    /// }
884    ///
885    /// impl PayloadRow for Posting {
886    ///     type Column = Vec<u8>;
887    ///
888    ///     fn payload(&self) -> &[u8] {
889    ///         &self.payload
890    ///     }
891    /// }
892    /// # #[derive(Deserialize)]
893    /// # struct Entry { amount: i64 }
894    /// # #[subscriber(InboxQueue::<Posting>::new("ledger"))]
895    /// # async fn post(entry: &Entry) -> HandlerOutcome {
896    /// #     tracing::info!(amount = entry.amount, "posting");
897    /// #     HandlerOutcome::ack()
898    /// # }
899    /// # pub fn app(pool: PgPool) -> RustStream {
900    /// #     RustStream::new(AppInfo::new("ledger", "1.0.0")).with_broker(SqlxBroker::new(pool), |b| {
901    /// #         b.include(post);
902    /// #     })
903    /// # }
904    /// # }
905    /// # fn main() {}
906    /// ```
907    #[must_use]
908    pub const fn advisory(
909        self,
910        key: &'static [KeyPart<'static>],
911    ) -> InboxSpec<<Settings as Push<Advisory>>::Out>
912    where
913        Settings: Push<Advisory>,
914        <Settings as Push<Advisory>>::Out: Declaration,
915    {
916        Self::with(&self.reformed(Form::Advisory(key)))
917    }
918
919    /// The same table in payload mode, the message's bytes read from `column`. Adds [`Payload`].
920    ///
921    /// # Examples
922    ///
923    /// ```
924    /// # #[cfg(feature = "mysql")]
925    /// # mod demo {
926    /// use ruststream_sqlx::dialect::Column;
927    /// use ruststream_sqlx::spec::Payload;
928    /// use ruststream_sqlx::{InboxSpec, InboxTable, PayloadRow};
929    /// # use ruststream_sqlx::prelude::*;
930    /// # use serde::Deserialize;
931    /// # use sqlx::{MySql, MySqlPool};
932    ///
933    /// // audit_events: id BIGINT AUTO_INCREMENT PRIMARY KEY, body TEXT NOT NULL
934    /// #[derive(sqlx::FromRow)]
935    /// pub struct AuditEvent {
936    ///     id: i64,
937    ///     body: String,
938    /// }
939    ///
940    /// impl InboxTable for AuditEvent {
941    ///     type Id = i64;
942    ///     type Table = InboxSpec<(Payload,)>;
943    ///     // The message is the JSON text in `body`, which the subscription's codec decodes.
944    ///     const TABLE: Self::Table = InboxSpec::new("audit_events", Column::new("id").generated())
945    ///         .payload(Column::new("body"));
946    ///
947    ///     fn id(&self) -> &i64 {
948    ///         &self.id
949    ///     }
950    /// }
951    ///
952    /// impl PayloadRow for AuditEvent {
953    ///     type Column = String;
954    ///
955    ///     fn payload(&self) -> &[u8] {
956    ///         self.body.as_bytes()
957    ///     }
958    /// }
959    /// # #[derive(Deserialize)]
960    /// # struct Login { user: String }
961    /// # #[subscriber(InboxQueue::<AuditEvent>::new("logins"))]
962    /// # async fn record(login: &Login) -> HandlerOutcome {
963    /// #     tracing::info!(user = %login.user, "logged in");
964    /// #     HandlerOutcome::ack()
965    /// # }
966    /// # pub fn app(pool: MySqlPool) -> RustStream {
967    /// #     RustStream::new(AppInfo::new("audit", "1.0.0")).with_broker(
968    /// #         SqlxBroker::<MySql>::new(pool),
969    /// #         |b| {
970    /// #             b.include(record);
971    /// #         },
972    /// #     )
973    /// # }
974    /// # }
975    /// # fn main() {}
976    /// ```
977    #[must_use]
978    pub const fn payload(
979        self,
980        column: Column<'static>,
981    ) -> InboxSpec<<Settings as Push<Payload>>::Out>
982    where
983        Settings: Push<Payload>,
984        <Settings as Push<Payload>>::Out: Declaration,
985    {
986        Self::with(&self.spec.payload(column))
987    }
988
989    /// The same table, with the delivery's partition key read from `column`. Adds [`Key`].
990    ///
991    /// # Examples
992    ///
993    /// ```
994    /// # #[cfg(feature = "postgres")]
995    /// # mod demo {
996    /// use ruststream_sqlx::dialect::Column;
997    /// use ruststream_sqlx::spec::{Key, Payload};
998    /// use ruststream_sqlx::{InboxSpec, InboxTable, KeyRow, PayloadRow};
999    /// # use ruststream_sqlx::prelude::*;
1000    /// # use serde::Deserialize;
1001    /// # use sqlx::PgPool;
1002    ///
1003    /// // export_jobs: id BIGSERIAL PRIMARY KEY, customer TEXT NOT NULL, payload BYTEA NOT NULL
1004    /// #[derive(sqlx::FromRow)]
1005    /// pub struct ExportJob {
1006    ///     id: i64,
1007    ///     customer: String,
1008    ///     payload: Vec<u8>,
1009    /// }
1010    ///
1011    /// impl InboxTable for ExportJob {
1012    ///     type Id = i64;
1013    ///     type Table = InboxSpec<(Key, Payload)>;
1014    ///     // Each delivery carries its customer as the partition key, which the dispatch keeps in order.
1015    ///     const TABLE: Self::Table = InboxSpec::new("export_jobs", Column::new("id").generated())
1016    ///         .partition_key(Column::new("customer"))
1017    ///         .payload(Column::new("payload"));
1018    ///
1019    ///     fn id(&self) -> &i64 {
1020    ///         &self.id
1021    ///     }
1022    /// }
1023    ///
1024    /// impl KeyRow for ExportJob {
1025    ///     type Key = String;
1026    ///
1027    ///     fn partition_key(&self) -> &String {
1028    ///         &self.customer
1029    ///     }
1030    /// }
1031    ///
1032    /// impl PayloadRow for ExportJob {
1033    ///     type Column = Vec<u8>;
1034    ///
1035    ///     fn payload(&self) -> &[u8] {
1036    ///         &self.payload
1037    ///     }
1038    /// }
1039    /// # #[derive(Deserialize)]
1040    /// # struct Export { file: String }
1041    /// # #[subscriber(InboxQueue::<ExportJob>::new("exports"))]
1042    /// # async fn export(job: &Export) -> HandlerOutcome {
1043    /// #     tracing::info!(file = %job.file, "exporting");
1044    /// #     HandlerOutcome::ack()
1045    /// # }
1046    /// # pub fn app(pool: PgPool) -> RustStream {
1047    /// #     RustStream::new(AppInfo::new("exports", "1.0.0")).with_broker(SqlxBroker::new(pool), |b| {
1048    /// #         b.include(export);
1049    /// #     })
1050    /// # }
1051    /// # }
1052    /// # fn main() {}
1053    /// ```
1054    #[must_use]
1055    pub const fn partition_key(
1056        self,
1057        column: Column<'static>,
1058    ) -> InboxSpec<<Settings as Push<Key>>::Out>
1059    where
1060        Settings: Push<Key>,
1061        <Settings as Push<Key>>::Out: Declaration,
1062    {
1063        Self::with(&self.spec.partition_key(column))
1064    }
1065
1066    /// The same table, counting a row's attempts in `column`. Adds [`Attempt`].
1067    ///
1068    /// # Examples
1069    ///
1070    /// ```
1071    /// # #[cfg(feature = "postgres")]
1072    /// # mod demo {
1073    /// use ruststream_sqlx::dialect::Column;
1074    /// use ruststream_sqlx::prelude::*;
1075    /// use ruststream_sqlx::spec::{Attempt, Payload};
1076    /// use ruststream_sqlx::{AttemptRow, InboxSpec, InboxTable, PayloadRow};
1077    /// use serde::Deserialize;
1078    /// # use sqlx::PgPool;
1079    ///
1080    /// // downloads: id BIGSERIAL PRIMARY KEY, tries INT NOT NULL DEFAULT 1, payload BYTEA NOT NULL
1081    /// #[derive(sqlx::FromRow)]
1082    /// pub struct Download {
1083    ///     id: i64,
1084    ///     tries: i32,
1085    ///     payload: Vec<u8>,
1086    /// }
1087    ///
1088    /// impl InboxTable for Download {
1089    ///     type Id = i64;
1090    ///     type Table = InboxSpec<(Attempt, Payload)>;
1091    ///     // A retry counts in `tries`, and the handler reads the count.
1092    ///     const TABLE: Self::Table = InboxSpec::new("downloads", Column::new("id").generated())
1093    ///         .attempt(Column::new("tries").generated())
1094    ///         .payload(Column::new("payload"));
1095    ///
1096    ///     fn id(&self) -> &i64 {
1097    ///         &self.id
1098    ///     }
1099    /// }
1100    ///
1101    /// impl AttemptRow for Download {
1102    ///     type Attempt = i32;
1103    ///
1104    ///     fn attempt(&self) -> &i32 {
1105    ///         &self.tries
1106    ///     }
1107    /// }
1108    ///
1109    /// impl PayloadRow for Download {
1110    ///     type Column = Vec<u8>;
1111    ///
1112    ///     fn payload(&self) -> &[u8] {
1113    ///         &self.payload
1114    ///     }
1115    /// }
1116    ///
1117    /// #[derive(Deserialize)]
1118    /// struct Image {
1119    ///     url: String,
1120    /// }
1121    ///
1122    /// # async fn fetch(_: &str) -> bool { true }
1123    /// // An image the fifth attempt cannot fetch is given up.
1124    /// #[subscriber(InboxQueue::<Download>::new("images"))]
1125    /// async fn download(image: &Image, Ctx(attempt): Ctx<keys::Attempt>) -> HandlerOutcome {
1126    ///     if fetch(&image.url).await {
1127    ///         HandlerOutcome::ack()
1128    ///     } else if attempt.unwrap_or(1) >= 5 {
1129    ///         HandlerOutcome::drop()
1130    ///     } else {
1131    ///         HandlerOutcome::retry()
1132    ///     }
1133    /// }
1134    /// # pub fn app(pool: PgPool) -> RustStream {
1135    /// #     RustStream::new(AppInfo::new("images", "1.0.0")).with_broker(SqlxBroker::new(pool), |b| {
1136    /// #         b.include(download);
1137    /// #     })
1138    /// # }
1139    /// # }
1140    /// # fn main() {}
1141    /// ```
1142    #[must_use]
1143    pub const fn attempt(
1144        self,
1145        column: Column<'static>,
1146    ) -> InboxSpec<<Settings as Push<Attempt>>::Out>
1147    where
1148        Settings: Push<Attempt>,
1149        <Settings as Push<Attempt>>::Out: Declaration,
1150    {
1151        Self::with(&self.spec.attempt(column))
1152    }
1153
1154    /// The same table, with `column` counting the row's attempts, decoded as `Decoded` and
1155    /// converted into the row's attempt field: what `#[sqlx(try_from = "..")]` on the derive's
1156    /// `attempt` field spells. Adds [`AttemptFrom`].
1157    #[doc(hidden)]
1158    #[must_use]
1159    pub const fn attempt_from<Decoded>(
1160        self,
1161        column: Column<'static>,
1162    ) -> InboxSpec<<Settings as Push<AttemptFrom<Decoded>>>::Out>
1163    where
1164        Settings: Push<AttemptFrom<Decoded>>,
1165        <Settings as Push<AttemptFrom<Decoded>>>::Out: Declaration,
1166    {
1167        Self::with(&self.spec.attempt(column))
1168    }
1169
1170    /// The same table, with the delivery's headers read from `column`. Adds [`Headers`].
1171    ///
1172    /// # Examples
1173    ///
1174    /// ```
1175    /// # #[cfg(all(feature = "postgres", feature = "json"))]
1176    /// # mod demo {
1177    /// use std::collections::BTreeMap;
1178    ///
1179    /// use ruststream_sqlx::dialect::Column;
1180    /// use ruststream_sqlx::prelude::*;
1181    /// use ruststream_sqlx::spec::{Headers, Payload};
1182    /// use ruststream_sqlx::{HeaderRow, InboxSpec, InboxTable, PayloadRow};
1183    /// use serde::Deserialize;
1184    /// use sqlx::types::Json;
1185    /// # use sqlx::PgPool;
1186    ///
1187    /// // pushes: id BIGSERIAL PRIMARY KEY, meta JSONB NULL, payload BYTEA NOT NULL
1188    /// #[derive(sqlx::FromRow)]
1189    /// pub struct PushJob {
1190    ///     id: i64,
1191    ///     meta: Option<Json<BTreeMap<String, String>>>,
1192    ///     payload: Vec<u8>,
1193    /// }
1194    ///
1195    /// impl InboxTable for PushJob {
1196    ///     type Id = i64;
1197    ///     type Table = InboxSpec<(Headers, Payload)>;
1198    ///     // The JSON object in `meta` becomes the delivery's headers.
1199    ///     const TABLE: Self::Table = InboxSpec::new("pushes", Column::new("id").generated())
1200    ///         .headers(Column::new("meta"))
1201    ///         .payload(Column::new("payload"));
1202    ///
1203    ///     fn id(&self) -> &i64 {
1204    ///         &self.id
1205    ///     }
1206    /// }
1207    ///
1208    /// impl HeaderRow for PushJob {
1209    ///     type Column = Option<Json<BTreeMap<String, String>>>;
1210    ///
1211    ///     fn headers_mut(&mut self) -> &mut Self::Column {
1212    ///         &mut self.meta
1213    ///     }
1214    /// }
1215    ///
1216    /// impl PayloadRow for PushJob {
1217    ///     type Column = Vec<u8>;
1218    ///
1219    ///     fn payload(&self) -> &[u8] {
1220    ///         &self.payload
1221    ///     }
1222    /// }
1223    ///
1224    /// #[derive(Deserialize)]
1225    /// struct Notification {
1226    ///     title: String,
1227    /// }
1228    ///
1229    /// #[subscriber(InboxQueue::<PushJob>::new("pushes"))]
1230    /// async fn push(note: &Notification, ctx: &mut Context<'_>) -> HandlerOutcome {
1231    ///     let device = ctx.headers().get_str("device").unwrap_or("all");
1232    ///     tracing::info!(title = %note.title, device, "pushing");
1233    ///     HandlerOutcome::ack()
1234    /// }
1235    /// # pub fn app(pool: PgPool) -> RustStream {
1236    /// #     RustStream::new(AppInfo::new("push", "1.0.0")).with_broker(SqlxBroker::new(pool), |b| {
1237    /// #         b.include(push);
1238    /// #     })
1239    /// # }
1240    /// # }
1241    /// # fn main() {}
1242    /// ```
1243    #[must_use]
1244    pub const fn headers(
1245        self,
1246        column: Column<'static>,
1247    ) -> InboxSpec<<Settings as Push<Headers>>::Out>
1248    where
1249        Settings: Push<Headers>,
1250        <Settings as Push<Headers>>::Out: Declaration,
1251    {
1252        Self::with(&self.spec.headers(column))
1253    }
1254
1255    /// The same table, with the delivery's headers built from the row's header fields, whose
1256    /// columns [`data`](Self::data) and [`fetching`](Self::fetching) name. Adds
1257    /// [`HeaderFields`].
1258    ///
1259    /// # Examples
1260    ///
1261    /// ```
1262    /// # #[cfg(feature = "postgres")]
1263    /// # mod demo {
1264    /// use ruststream::HeaderMap;
1265    /// use ruststream::runtime::{Input, SoloCarried};
1266    /// use ruststream_sqlx::dialect::Column;
1267    /// use ruststream_sqlx::prelude::*;
1268    /// use ruststream_sqlx::spec;
1269    /// use ruststream_sqlx::{HeaderFields, InboxSpec, InboxTable, put_header};
1270    /// # use sqlx::PgPool;
1271    ///
1272    /// // sms_jobs: id BIGSERIAL PRIMARY KEY, phone TEXT NOT NULL, locale TEXT NOT NULL, text TEXT NOT NULL
1273    /// #[derive(Debug, Clone, sqlx::FromRow)]
1274    /// pub struct Sms {
1275    ///     id: i64,
1276    ///     phone: String,
1277    ///     locale: String,
1278    ///     text: String,
1279    /// }
1280    ///
1281    /// impl InboxTable for Sms {
1282    ///     type Id = i64;
1283    ///     type Table = InboxSpec<(spec::HeaderFields,)>;
1284    ///     // The `locale` column reads as a header, as it would on any other broker.
1285    ///     const TABLE: Self::Table = InboxSpec::new("sms_jobs", Column::new("id").generated())
1286    ///         .data(&[Column::new("phone"), Column::new("locale"), Column::new("text")])
1287    ///         .header_fields();
1288    ///
1289    ///     fn id(&self) -> &i64 {
1290    ///         &self.id
1291    ///     }
1292    /// }
1293    ///
1294    /// impl Input for Sms {
1295    ///     type Axis = SoloCarried<Self>;
1296    /// }
1297    ///
1298    /// impl HeaderFields for Sms {
1299    ///     const NAMES: &'static [&'static str] = &["locale"];
1300    ///
1301    ///     fn header_map(&self) -> HeaderMap {
1302    ///         let mut headers = HeaderMap::with_capacity(Self::NAMES.len());
1303    ///         put_header(&mut headers, "locale", &self.locale);
1304    ///         headers
1305    ///     }
1306    /// }
1307    ///
1308    /// #[subscriber(InboxQueue::<Sms>::new("sms"))]
1309    /// async fn send(sms: &Sms, ctx: &mut Context<'_>) -> HandlerOutcome {
1310    ///     let locale = ctx.headers().get_str("locale").unwrap_or("en");
1311    ///     tracing::info!(phone = %sms.phone, locale, len = sms.text.len(), "sending");
1312    ///     HandlerOutcome::ack()
1313    /// }
1314    /// # pub fn app(pool: PgPool) -> RustStream {
1315    /// #     RustStream::new(AppInfo::new("sms", "1.0.0")).with_broker(SqlxBroker::new(pool), |b| {
1316    /// #         b.include(send);
1317    /// #     })
1318    /// # }
1319    /// # }
1320    /// # fn main() {}
1321    /// ```
1322    #[must_use]
1323    pub const fn header_fields(self) -> InboxSpec<<Settings as Push<HeaderFields>>::Out>
1324    where
1325        Settings: Push<HeaderFields>,
1326        <Settings as Push<HeaderFields>>::Out: Declaration,
1327    {
1328        Self::with(&self.spec)
1329    }
1330
1331    /// The same table, with the time a retried row is due in `column`, in `Time`. Adds
1332    /// [`RetryAfter<Time>`](RetryAfter).
1333    ///
1334    /// # Examples
1335    ///
1336    /// ```
1337    /// # #[cfg(all(feature = "postgres", feature = "chrono"))]
1338    /// # mod demo {
1339    /// use std::time::Duration;
1340    ///
1341    /// use chrono::{DateTime, Utc};
1342    /// use ruststream_sqlx::dialect::Column;
1343    /// use ruststream_sqlx::prelude::*;
1344    /// use ruststream_sqlx::spec::{Payload, RetryAfter};
1345    /// use ruststream_sqlx::{InboxSpec, InboxTable, PayloadRow};
1346    /// use serde::Deserialize;
1347    /// # use sqlx::PgPool;
1348    ///
1349    /// // crm_pushes: id BIGSERIAL PRIMARY KEY, due TIMESTAMPTZ NOT NULL DEFAULT now(),
1350    /// // payload BYTEA NOT NULL
1351    /// #[derive(sqlx::FromRow)]
1352    /// pub struct CrmPush {
1353    ///     id: i64,
1354    ///     payload: Vec<u8>,
1355    /// }
1356    ///
1357    /// impl InboxTable for CrmPush {
1358    ///     type Id = i64;
1359    ///     type Table = InboxSpec<(RetryAfter<DateTime<Utc>>, Payload)>;
1360    ///     // A row is claimed once `due` has passed, so a delayed retry waits in the table.
1361    ///     const TABLE: Self::Table = InboxSpec::new("crm_pushes", Column::new("id").generated())
1362    ///         .retry_after(Column::new("due").generated())
1363    ///         .payload(Column::new("payload"));
1364    ///
1365    ///     fn id(&self) -> &i64 {
1366    ///         &self.id
1367    ///     }
1368    /// }
1369    ///
1370    /// impl PayloadRow for CrmPush {
1371    ///     type Column = Vec<u8>;
1372    ///
1373    ///     fn payload(&self) -> &[u8] {
1374    ///         &self.payload
1375    ///     }
1376    /// }
1377    ///
1378    /// #[derive(Deserialize)]
1379    /// struct Contact {
1380    ///     email: String,
1381    /// }
1382    ///
1383    /// # async fn upsert(_: &str) -> Result<(), Duration> { Ok(()) }
1384    /// #[subscriber(InboxQueue::<CrmPush>::new("crm"))]
1385    /// async fn sync(contact: &Contact) -> HandlerOutcome {
1386    ///     match upsert(&contact.email).await {
1387    ///         Ok(()) => HandlerOutcome::ack(),
1388    ///         // The CRM's rate limit says how long to wait.
1389    ///         Err(wait) => HandlerOutcome::retry_after(wait),
1390    ///     }
1391    /// }
1392    /// # pub fn app(pool: PgPool) -> RustStream {
1393    /// #     RustStream::new(AppInfo::new("crm", "1.0.0")).with_broker(SqlxBroker::new(pool), |b| {
1394    /// #         b.include(sync);
1395    /// #     })
1396    /// # }
1397    /// # }
1398    /// # fn main() {}
1399    /// ```
1400    #[must_use]
1401    pub const fn retry_after<Time>(
1402        self,
1403        column: Column<'static>,
1404    ) -> InboxSpec<<Settings as Push<RetryAfter<Time>>>::Out>
1405    where
1406        Settings: Push<RetryAfter<Time>>,
1407        <Settings as Push<RetryAfter<Time>>>::Out: Declaration,
1408    {
1409        Self::with(&self.spec.retry_after(column))
1410    }
1411
1412    /// The same table, with the time a row was processed written into `column`, in `Time`, so a
1413    /// processed row stays in the table. Adds [`ProcessedAt<Time>`](ProcessedAt).
1414    ///
1415    /// # Examples
1416    ///
1417    /// ```
1418    /// # #[cfg(all(feature = "postgres", feature = "time"))]
1419    /// # mod demo {
1420    /// use ruststream_sqlx::dialect::Column;
1421    /// use ruststream_sqlx::spec::{Payload, ProcessedAt};
1422    /// use ruststream_sqlx::{InboxSpec, InboxTable, PayloadRow};
1423    /// use time::OffsetDateTime;
1424    /// # use ruststream_sqlx::prelude::*;
1425    /// # use serde::Deserialize;
1426    /// # use sqlx::PgPool;
1427    ///
1428    /// // payments: id BIGSERIAL PRIMARY KEY, processed_at TIMESTAMPTZ NULL, payload BYTEA NOT NULL
1429    /// #[derive(sqlx::FromRow)]
1430    /// pub struct PaymentJob {
1431    ///     id: i64,
1432    ///     payload: Vec<u8>,
1433    /// }
1434    ///
1435    /// impl InboxTable for PaymentJob {
1436    ///     type Id = i64;
1437    ///     type Table = InboxSpec<(ProcessedAt<OffsetDateTime>, Payload)>;
1438    ///     // A settled payment stays in the table for the auditors, stamped in `processed_at`.
1439    ///     const TABLE: Self::Table = InboxSpec::new("payments", Column::new("id").generated())
1440    ///         .processed_at(Column::new("processed_at").generated())
1441    ///         .payload(Column::new("payload"));
1442    ///
1443    ///     fn id(&self) -> &i64 {
1444    ///         &self.id
1445    ///     }
1446    /// }
1447    ///
1448    /// impl PayloadRow for PaymentJob {
1449    ///     type Column = Vec<u8>;
1450    ///
1451    ///     fn payload(&self) -> &[u8] {
1452    ///         &self.payload
1453    ///     }
1454    /// }
1455    /// # #[derive(Deserialize)]
1456    /// # struct Payment { cents: i64 }
1457    /// # #[subscriber(InboxQueue::<PaymentJob>::new("payments"))]
1458    /// # async fn settle(payment: &Payment) -> HandlerOutcome {
1459    /// #     tracing::info!(cents = payment.cents, "settling");
1460    /// #     HandlerOutcome::ack()
1461    /// # }
1462    /// # pub fn app(pool: PgPool) -> RustStream {
1463    /// #     RustStream::new(AppInfo::new("payments", "1.0.0")).with_broker(SqlxBroker::new(pool), |b| {
1464    /// #         b.include(settle);
1465    /// #     })
1466    /// # }
1467    /// # }
1468    /// # fn main() {}
1469    /// ```
1470    #[must_use]
1471    pub const fn processed_at<Time>(
1472        self,
1473        column: Column<'static>,
1474    ) -> InboxSpec<<Settings as Push<ProcessedAt<Time>>>::Out>
1475    where
1476        Settings: Push<ProcessedAt<Time>>,
1477        <Settings as Push<ProcessedAt<Time>>>::Out: Declaration,
1478    {
1479        Self::with(&self.spec.processed_at(column))
1480    }
1481
1482    /// The same table, split into groups by `column`, with each group in order: at most one row
1483    /// of a group is in work, taken in claim order. Adds [`Fifo`].
1484    ///
1485    /// # Examples
1486    ///
1487    /// ```
1488    /// # #[cfg(feature = "postgres")]
1489    /// # mod demo {
1490    /// use ruststream_sqlx::dialect::Column;
1491    /// use ruststream_sqlx::prelude::*;
1492    /// use ruststream_sqlx::spec::{Fifo, Payload};
1493    /// use ruststream_sqlx::{InboxSpec, InboxTable, PayloadRow};
1494    /// use serde::Deserialize;
1495    /// # use sqlx::PgPool;
1496    ///
1497    /// // stock_updates: id BIGSERIAL PRIMARY KEY, warehouse TEXT NOT NULL, payload BYTEA NOT NULL
1498    /// #[derive(sqlx::FromRow)]
1499    /// pub struct StockUpdate {
1500    ///     id: i64,
1501    ///     payload: Vec<u8>,
1502    /// }
1503    ///
1504    /// impl InboxTable for StockUpdate {
1505    ///     type Id = i64;
1506    ///     type Table = InboxSpec<(Fifo, Payload)>;
1507    ///     // Each warehouse is a group, and its updates arrive one at a time, in the order written.
1508    ///     const TABLE: Self::Table = InboxSpec::new("stock_updates", Column::new("id").generated())
1509    ///         .fifo_group(Column::new("warehouse"))
1510    ///         .payload(Column::new("payload"));
1511    ///
1512    ///     fn id(&self) -> &i64 {
1513    ///         &self.id
1514    ///     }
1515    /// }
1516    ///
1517    /// impl PayloadRow for StockUpdate {
1518    ///     type Column = Vec<u8>;
1519    ///
1520    ///     fn payload(&self) -> &[u8] {
1521    ///         &self.payload
1522    ///     }
1523    /// }
1524    ///
1525    /// #[derive(Deserialize)]
1526    /// struct Level {
1527    ///     sku: String,
1528    ///     count: i64,
1529    /// }
1530    ///
1531    /// #[subscriber(InboxQueue::<StockUpdate>::new("berlin"))]
1532    /// async fn berlin(level: &Level) -> HandlerOutcome {
1533    ///     tracing::info!(sku = %level.sku, level.count, "berlin stock");
1534    ///     HandlerOutcome::ack()
1535    /// }
1536    /// # pub fn app(pool: PgPool) -> RustStream {
1537    /// #     RustStream::new(AppInfo::new("stock", "1.0.0")).with_broker(SqlxBroker::new(pool), |b| {
1538    /// #         b.include(berlin);
1539    /// #     })
1540    /// # }
1541    /// # }
1542    /// # fn main() {}
1543    /// ```
1544    #[must_use]
1545    pub const fn fifo_group(
1546        self,
1547        column: Column<'static>,
1548    ) -> InboxSpec<<Settings as Push<Fifo>>::Out>
1549    where
1550        Settings: Push<Fifo>,
1551        <Settings as Push<Fifo>>::Out: Declaration,
1552    {
1553        Self::with(&self.spec.fifo_group(column))
1554    }
1555
1556    /// The same table, reading now from `Source`: the statements read the database's own clock
1557    /// where `Source` is [`DatabaseClock`](crate::DatabaseClock). Adds
1558    /// [`Clock<Source>`](Clock).
1559    ///
1560    /// # Examples
1561    ///
1562    /// ```
1563    /// # #[cfg(all(feature = "postgres", feature = "chrono"))]
1564    /// # mod demo {
1565    /// use chrono::{DateTime, Utc};
1566    /// use ruststream_sqlx::dialect::Column;
1567    /// use ruststream_sqlx::spec::{Clock, Payload, RetryAfter};
1568    /// use ruststream_sqlx::{DatabaseClock, InboxSpec, InboxTable, PayloadRow};
1569    /// # use std::time::Duration;
1570    /// # use ruststream_sqlx::prelude::*;
1571    /// # use serde::Deserialize;
1572    /// # use sqlx::PgPool;
1573    ///
1574    /// // reminders: id BIGSERIAL PRIMARY KEY, retry_after TIMESTAMPTZ NOT NULL DEFAULT now(),
1575    /// // payload BYTEA NOT NULL
1576    /// #[derive(sqlx::FromRow)]
1577    /// pub struct Reminder {
1578    ///     id: i64,
1579    ///     payload: Vec<u8>,
1580    /// }
1581    ///
1582    /// impl InboxTable for Reminder {
1583    ///     type Id = i64;
1584    ///     type Table = InboxSpec<(Clock<DatabaseClock>, RetryAfter<DateTime<Utc>>, Payload)>;
1585    ///     // Due times count from the database's `now()`, so hosts whose clocks drift still agree.
1586    ///     const TABLE: Self::Table = InboxSpec::new("reminders", Column::new("id").generated())
1587    ///         .clock::<DatabaseClock>()
1588    ///         .retry_after(Column::new("retry_after").generated())
1589    ///         .payload(Column::new("payload"));
1590    ///
1591    ///     fn id(&self) -> &i64 {
1592    ///         &self.id
1593    ///     }
1594    /// }
1595    ///
1596    /// impl PayloadRow for Reminder {
1597    ///     type Column = Vec<u8>;
1598    ///
1599    ///     fn payload(&self) -> &[u8] {
1600    ///         &self.payload
1601    ///     }
1602    /// }
1603    /// # #[derive(Deserialize)]
1604    /// # struct Note { text: String }
1605    /// # #[subscriber(InboxQueue::<Reminder>::new("reminders"))]
1606    /// # async fn remind(note: &Note) -> HandlerOutcome {
1607    /// #     tracing::info!(text = %note.text, "reminding");
1608    /// #     HandlerOutcome::retry_after(Duration::from_secs(3600))
1609    /// # }
1610    /// # pub fn app(pool: PgPool) -> RustStream {
1611    /// #     RustStream::new(AppInfo::new("reminders", "1.0.0")).with_broker(SqlxBroker::new(pool), |b| {
1612    /// #         b.include(remind);
1613    /// #     })
1614    /// # }
1615    /// # }
1616    /// # fn main() {}
1617    /// ```
1618    #[must_use]
1619    pub const fn clock<Source: TimeSource>(
1620        self,
1621    ) -> InboxSpec<<Settings as Push<Clock<Source>>>::Out>
1622    where
1623        Settings: Push<Clock<Source>>,
1624        <Settings as Push<Clock<Source>>>::Out: Declaration,
1625    {
1626        if Source::DATABASE {
1627            Self::with(&self.spec.database_clock())
1628        } else {
1629            Self::with(&self.spec)
1630        }
1631    }
1632
1633    /// The same table, with its transactions opened at `Level`, an isolation level or a SQLite
1634    /// mode. Adds [`Opens<Level>`](Opens).
1635    ///
1636    /// # Examples
1637    ///
1638    /// ```
1639    /// # #[cfg(feature = "postgres")]
1640    /// # mod demo {
1641    /// use ruststream_sqlx::dialect::{Column, level};
1642    /// use ruststream_sqlx::spec::{Opens, Payload};
1643    /// use ruststream_sqlx::{InboxSpec, InboxTable, PayloadRow};
1644    /// # use ruststream_sqlx::prelude::*;
1645    /// # use serde::Deserialize;
1646    /// # use sqlx::PgPool;
1647    ///
1648    /// #[derive(sqlx::FromRow)]
1649    /// pub struct PayoutJob {
1650    ///     id: i64,
1651    ///     payload: Vec<u8>,
1652    /// }
1653    ///
1654    /// impl InboxTable for PayoutJob {
1655    ///     type Id = i64;
1656    ///     type Table = InboxSpec<(Opens<level::ReadCommitted>, Payload)>;
1657    ///     // The claim's transaction runs at READ COMMITTED on a database whose default is SERIALIZABLE.
1658    ///     const TABLE: Self::Table = InboxSpec::new("payout_jobs", Column::new("id").generated())
1659    ///         .opens::<level::ReadCommitted>()
1660    ///         .payload(Column::new("payload"));
1661    ///
1662    ///     fn id(&self) -> &i64 {
1663    ///         &self.id
1664    ///     }
1665    /// }
1666    ///
1667    /// impl PayloadRow for PayoutJob {
1668    ///     type Column = Vec<u8>;
1669    ///
1670    ///     fn payload(&self) -> &[u8] {
1671    ///         &self.payload
1672    ///     }
1673    /// }
1674    /// # #[derive(Deserialize)]
1675    /// # struct Payout { cents: i64 }
1676    /// # #[subscriber(InboxQueue::<PayoutJob>::new("payouts"))]
1677    /// # async fn pay(payout: &Payout) -> HandlerOutcome {
1678    /// #     tracing::info!(cents = payout.cents, "paying out");
1679    /// #     HandlerOutcome::ack()
1680    /// # }
1681    /// # pub fn app(pool: PgPool) -> RustStream {
1682    /// #     RustStream::new(AppInfo::new("payouts", "1.0.0")).with_broker(SqlxBroker::new(pool), |b| {
1683    /// #         b.include(pay);
1684    /// #     })
1685    /// # }
1686    /// # }
1687    /// # fn main() {}
1688    /// ```
1689    #[must_use]
1690    pub const fn opens<Level: OpeningLevel>(
1691        self,
1692    ) -> InboxSpec<<Settings as Push<Opens<Level>>>::Out>
1693    where
1694        Settings: Push<Opens<Level>>,
1695        <Settings as Push<Opens<Level>>>::Out: Declaration,
1696    {
1697        Self::with(&opened(&self.spec, Level::OPENING))
1698    }
1699
1700    /// The same table, with `Event` written by the service itself: the row implements the
1701    /// event's trait. Adds `Event`, a marker of [`own`](super::own).
1702    ///
1703    /// # Examples
1704    ///
1705    /// ```
1706    /// # #[cfg(feature = "postgres")]
1707    /// # mod demo {
1708    /// use ruststream_sqlx::dialect::Column;
1709    /// use ruststream_sqlx::spec::{Payload, own};
1710    /// use ruststream_sqlx::{Discard, InboxSpec, InboxTable, PayloadRow};
1711    /// use sqlx::{PgConnection, Postgres};
1712    /// # use ruststream_sqlx::prelude::*;
1713    /// # use serde::Deserialize;
1714    /// # use sqlx::PgPool;
1715    ///
1716    /// // signups: id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL, payload BYTEA NOT NULL
1717    /// #[derive(sqlx::FromRow)]
1718    /// pub struct Signup {
1719    ///     id: i64,
1720    ///     payload: Vec<u8>,
1721    /// }
1722    ///
1723    /// impl InboxTable for Signup {
1724    ///     type Id = i64;
1725    ///     type Table = InboxSpec<(Payload, own::Discard)>;
1726    ///     const TABLE: Self::Table = InboxSpec::new("signups", Column::new("id").generated())
1727    ///         .group(Column::new("name"))
1728    ///         .payload(Column::new("payload"))
1729    ///         .own::<own::Discard>();
1730    ///
1731    ///     fn id(&self) -> &i64 {
1732    ///         &self.id
1733    ///     }
1734    /// }
1735    ///
1736    /// impl PayloadRow for Signup {
1737    ///     type Column = Vec<u8>;
1738    ///
1739    ///     fn payload(&self) -> &[u8] {
1740    ///         &self.payload
1741    ///     }
1742    /// }
1743    ///
1744    /// // A dropped signup moves to the `rejected` group, where support reviews it.
1745    /// impl Discard<Postgres> for Signup {
1746    ///     async fn discard(conn: &mut PgConnection, id: &i64) -> Result<(), sqlx::Error> {
1747    ///         sqlx::query("UPDATE signups SET name = 'rejected' WHERE id = $1")
1748    ///             .bind(id)
1749    ///             .execute(conn)
1750    ///             .await?;
1751    ///         Ok(())
1752    ///     }
1753    /// }
1754    /// # #[derive(Deserialize)]
1755    /// # struct Form { email: String }
1756    /// # #[subscriber(InboxQueue::<Signup>::new("signups"))]
1757    /// # async fn check(form: &Form) -> HandlerOutcome {
1758    /// #     if form.email.contains('@') { HandlerOutcome::ack() } else { HandlerOutcome::drop() }
1759    /// # }
1760    /// # pub fn app(pool: PgPool) -> RustStream {
1761    /// #     RustStream::new(AppInfo::new("signup", "1.0.0")).with_broker(SqlxBroker::new(pool), |b| {
1762    /// #         b.include(check);
1763    /// #     })
1764    /// # }
1765    /// # }
1766    /// # fn main() {}
1767    /// ```
1768    #[must_use]
1769    pub const fn own<Event: OwnEvent>(self) -> InboxSpec<<Settings as Push<Event>>::Out>
1770    where
1771        Settings: Push<Event>,
1772        <Settings as Push<Event>>::Out: Declaration,
1773    {
1774        Self::with(&self.spec)
1775    }
1776
1777    /// The same columns as `spec`, under the settings `Next`.
1778    const fn with<Next>(spec: &TableSpec<'static>) -> InboxSpec<Next> {
1779        InboxSpec {
1780            spec: *spec,
1781            settings: PhantomData,
1782        }
1783    }
1784
1785    /// The same table in `form`: `TableSpec` takes its form when it is built, so the description
1786    /// is built again with every column set so far.
1787    const fn reformed(self, form: Form<'static>) -> TableSpec<'static> {
1788        let old = self.spec;
1789        let mut spec = TableSpec::new(old.table(), old.id(), form);
1790        if let Some(schema) = old.schema() {
1791            spec = spec.within(schema);
1792        }
1793        if let Some(column) = old.column(Role::Group) {
1794            spec = if old.is_fifo() {
1795                spec.fifo_group(column)
1796            } else {
1797                spec.group(column)
1798            };
1799        }
1800        if let Some(column) = old.column(Role::PartitionKey) {
1801            spec = spec.partition_key(column);
1802        }
1803        if let Some(column) = old.column(Role::Priority) {
1804            spec = spec.priority(column);
1805        }
1806        if let Some(column) = old.column(Role::RetryAfter) {
1807            spec = spec.retry_after(column);
1808        }
1809        if let Some(column) = old.column(Role::Attempt) {
1810            spec = spec.attempt(column);
1811        }
1812        if let Some(column) = old.column(Role::ProcessedAt) {
1813            spec = spec.processed_at(column);
1814        }
1815        if let Some(column) = old.column(Role::Headers) {
1816            spec = spec.headers(column);
1817        }
1818        if let Some(column) = old.column(Role::Payload) {
1819            spec = spec.payload(column);
1820        }
1821        if !old.data_columns().is_empty() {
1822            spec = spec.data(old.data_columns());
1823        }
1824        if !old.fetched_columns().is_empty() {
1825            spec = spec.fetching(old.fetched_columns());
1826        }
1827        if old.selects_all() {
1828            spec = spec.selecting_all();
1829        }
1830        if old.uses_database_clock() {
1831            spec = spec.database_clock();
1832        }
1833        opened(&spec, old.opening())
1834    }
1835}
1836
1837/// `spec`, opening its transactions at `opening`.
1838const fn opened(spec: &TableSpec<'static>, opening: Opening) -> TableSpec<'static> {
1839    let spec = *spec;
1840    match opening {
1841        Opening::Isolation(isolation) => spec.isolation(isolation),
1842        Opening::Mode(mode) => spec.mode(mode),
1843        _ => spec,
1844    }
1845}