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}