ruststream-sqlx 0.7.0

SQL databases for the RustStream messaging framework through sqlx: a transactional outbox over any RustStream broker and task queues in Postgres, MySQL/MariaDB and SQLite tables.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
//! The headers layout: a headers struct describes the queue table, and the message struct a
//! handler takes flattens it into its `#[field(headers)]` field beside data of its own. The
//! delivery's header map holds the headers struct's fields without a role, built on the first
//! `headers()` call.

use std::fmt::Debug;
use std::io::Write;
#[cfg(any(feature = "chrono", feature = "time"))]
use std::io::{self, Cursor};
use std::sync::OnceLock;

#[cfg(feature = "chrono")]
use chrono::format::{Fixed, Item};
#[cfg(feature = "chrono")]
use chrono::{DateTime, Utc};
use ruststream::{HeaderMap, Str};
#[cfg(feature = "time")]
use time::OffsetDateTime;
#[cfg(feature = "time")]
use time::format_description::well_known::Rfc3339;

use super::InboxSpec;
use super::database::QueueDatabase;
use super::engine::Events;
use super::spec::Declaration;

/// A struct that describes a queue table whose message is assembled from it: the mechanics a
/// subscription runs the queue by, and the service's own headers. `#[derive(InboxHeaders)]`
/// implements it.
///
/// The headers struct takes the table's attributes (`#[inbox(table, schema, advisory_lock,
/// clock, isolation, mode)]`) and the fields that play a role. Every field without a role is a
/// header: the delivery's header map holds it under its column's name, built the first time
/// something reads the delivery's headers, and a field that holds `None` is left out. Its type
/// implements [`HeaderField`]. The derive also writes the generated
/// [`insert`](crate::Insert::insert) of the table's columns.
///
/// The message struct derives [`Inbox`](crate::Inbox) and holds the headers struct in a
/// `#[field(headers)] #[sqlx(flatten)]` field beside its own data. A handler takes the message
/// struct itself, `&Message`, or `&[Message]` in a batch, as in row mode. Its rows are read
/// by the default fetch, which names the headers struct's columns and the message's own, or by
/// the message's own [`Fetch`](crate::Fetch) with `#[inbox(custom(fetch))]`, which may read the
/// data from other tables.
///
/// # Examples
///
/// ```
/// # #[cfg(feature = "postgres")]
/// # mod demo {
/// use ruststream_sqlx::prelude::*;
/// use serde::Deserialize;
/// use sqlx::PgPool;
///
/// /// The queue table: the mechanics, and the service's own headers `tenant` and `trace`.
/// #[derive(Debug, Clone, InboxHeaders, sqlx::FromRow)]
/// #[inbox(table = "order_jobs")]
/// pub struct OrderHeaders {
///     #[field(id, generated)]
///     job_id: i64,
///     #[field(group)]
///     name: String,
///     #[field(attempt, generated)]
///     attempt: i16,
///     tenant: String,
///     trace: Option<String>,
/// }
///
/// /// The message a handler takes: the headers and the order's note, read from the same row.
/// #[derive(Debug, Clone, Inbox, sqlx::FromRow)]
/// pub struct OrderJob {
///     #[field(headers)]
///     #[sqlx(flatten)]
///     headers: OrderHeaders,
///     note: Option<String>,
/// }
///
/// #[derive(Deserialize)]
/// struct Tenant {
///     tenant: String,
/// }
///
/// // The header map is built from `OrderHeaders` on the first read: here, by `Headers<Tenant>`.
/// #[subscriber(InboxQueue::<OrderJob>::new("orders"))]
/// async fn ship(job: &OrderJob, Headers(tenant): Headers<Tenant>) -> HandlerOutcome {
///     tracing::info!(tenant = %tenant.tenant, note = ?job.note, "shipping");
///     HandlerOutcome::ack()
/// }
///
/// pub fn app(pool: PgPool) -> RustStream {
///     RustStream::new(AppInfo::new("shop", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
///         b.include(ship);
///     })
/// }
/// # }
/// # fn main() {}
/// ```
#[diagnostic::on_unimplemented(
    message = "`{Self}` is not a headers struct: a flattened `#[field(headers)]` field holds one",
    label = "this type does not derive `InboxHeaders`",
    note = "derive `InboxHeaders` for `{Self}` and describe the queue table on it, or drop \
            `#[sqlx(flatten)]` to read the field as a header column"
)]
pub trait InboxHeaders: Sized + Send + Sync + 'static {
    /// The type of the field that plays `id`. Machinery; the derive sets it.
    #[doc(hidden)]
    type Id: Clone + Debug + Send + Sync + 'static;

    /// The queue table's settings, as the markers of [`spec`](crate::spec): the mechanics and
    /// the header fields. Machinery; the derive sets it.
    #[doc(hidden)]
    type Settings: Declaration;

    /// The queue table: the mechanics and the headers, without the message's own columns, which
    /// the message's own description adds. Machinery; the derive sets it.
    #[doc(hidden)]
    const TABLE: InboxSpec<Self::Settings>;

    /// The headers the fields without a role hold, by their columns' names. Machinery; the
    /// derive sets it.
    #[doc(hidden)]
    const NAMES: &'static [&'static str];

    /// The row's id. Machinery; the derive writes it.
    #[doc(hidden)]
    fn id(&self) -> &Self::Id;

    /// The header map of the fields without a role. Machinery; the derive writes it.
    #[doc(hidden)]
    fn header_map(&self) -> HeaderMap;
}

/// A row whose delivery builds its header map from its fields: where the map comes from.
/// Machinery; every [`HeaderFields`](crate::HeaderFields) row implements it.
#[doc(hidden)]
pub trait Assembled {
    /// The header map of the headers struct's fields without a role.
    fn header_map(&self) -> HeaderMap;
}

/// A value a field of a headers struct holds, as the header the delivery's header map carries.
///
/// A field of a struct deriving [`InboxHeaders`] without a role becomes the header of its
/// column's name. Strings and byte vectors are their bytes, integers and `bool` their decimal
/// text, and `chrono::DateTime<Utc>` (feature `chrono`) and `time::OffsetDateTime` (feature
/// `time`) their RFC 3339 text. `None` leaves the header out. A service implements it for a type
/// of its own.
///
/// # Examples
///
/// ```
/// # #[cfg(feature = "postgres")]
/// # mod demo {
/// use ruststream_sqlx::HeaderField;
/// use ruststream_sqlx::prelude::*;
///
/// /// A region code the table keeps as text.
/// #[derive(Debug, Clone, sqlx::Type)]
/// #[sqlx(transparent)]
/// pub struct Region(String);
///
/// // The header carries the code in capitals, as the service's consumers read it.
/// impl HeaderField for Region {
///     fn header(&self) -> Option<Vec<u8>> {
///         Some(self.0.to_uppercase().into_bytes())
///     }
/// }
///
/// #[derive(Debug, Clone, InboxHeaders, sqlx::FromRow)]
/// #[inbox(table = "shipments")]
/// pub struct ShipmentHeaders {
///     #[field(id, generated)]
///     id: i64,
///     region: Region,
/// }
///
/// #[derive(Debug, Clone, Inbox, sqlx::FromRow)]
/// pub struct Shipment {
///     #[field(headers)]
///     #[sqlx(flatten)]
///     headers: ShipmentHeaders,
///     address: String,
/// }
///
/// #[subscriber(InboxQueue::<Shipment>::new("shipments"))]
/// async fn ship(shipment: &Shipment, ctx: &mut Context<'_>) -> HandlerOutcome {
///     let region = ctx.headers().get_str("region").unwrap_or("none");
///     tracing::info!(region, to = %shipment.address, "shipping");
///     HandlerOutcome::ack()
/// }
///
/// pub fn app(pool: sqlx::PgPool) -> RustStream {
///     RustStream::new(AppInfo::new("shipping", "0.1.0")).with_broker(SqlxBroker::new(pool), |b| {
///         b.include(ship);
///     })
/// }
/// # }
/// # fn main() {}
/// ```
#[diagnostic::on_unimplemented(
    message = "`{Self}` is not a header value: a field of a headers struct without a role becomes \
               a header",
    label = "a header field of this type",
    note = "implement `HeaderField` for `{Self}`, or give the field a role with `#[field(..)]`"
)]
pub trait HeaderField {
    /// The header's value; `None` leaves the header out.
    fn header(&self) -> Option<Vec<u8>>;
}

impl HeaderField for String {
    fn header(&self) -> Option<Vec<u8>> {
        Some(self.clone().into_bytes())
    }
}

impl HeaderField for Vec<u8> {
    fn header(&self) -> Option<Vec<u8>> {
        Some(self.clone())
    }
}

impl HeaderField for bool {
    fn header(&self) -> Option<Vec<u8>> {
        Some(if *self {
            b"true".to_vec()
        } else {
            b"false".to_vec()
        })
    }
}

/// The longest decimal text of a 64-bit integer: `i64::MIN` and `u64::MAX` take 20 bytes.
const DECIMAL_TEXT: usize = 20;

/// `HeaderField` for integers: the decimal text.
macro_rules! decimal {
    ($($integer:ty),*) => {
        $(
            impl HeaderField for $integer {
                fn header(&self) -> Option<Vec<u8>> {
                    // The text goes into a buffer of its own length: a `Vec` with spare capacity
                    // costs the map one more allocation as it turns into `Bytes`.
                    let mut text = [0_u8; DECIMAL_TEXT];
                    let mut rest = &mut text[..];
                    // The buffer holds the longest decimal of a 64-bit integer, so the write
                    // cannot run out of room.
                    write!(rest, "{self}").ok()?;
                    let written = DECIMAL_TEXT - rest.len();
                    Some(text[..written].to_vec())
                }
            }
        )*
    };
}

decimal!(i8, i16, i32, i64, u8, u16, u32, u64);

impl<T: HeaderField> HeaderField for Option<T> {
    fn header(&self) -> Option<Vec<u8>> {
        self.as_ref().and_then(HeaderField::header)
    }
}

/// The longest RFC 3339 text of a time: a six-digit year with its sign, nanoseconds and an offset.
#[cfg(any(feature = "chrono", feature = "time"))]
const TIME_TEXT: usize = 48;

/// The text `write` puts into a buffer on the stack, copied into a `Vec` of its own length: a
/// formatter's `String` keeps spare capacity, which costs the map one more allocation as the
/// value turns into `Bytes`. `None` where the text does not fit or the formatter fails.
#[cfg(any(feature = "chrono", feature = "time"))]
fn exact_text(
    write: impl FnOnce(&mut Cursor<[u8; TIME_TEXT]>) -> io::Result<()>,
) -> Option<Vec<u8>> {
    let mut text = Cursor::new([0_u8; TIME_TEXT]);
    write(&mut text).ok()?;
    let written = usize::try_from(text.position()).ok()?;
    Some(text.get_ref()[..written].to_vec())
}

#[cfg(feature = "chrono")]
impl HeaderField for DateTime<Utc> {
    fn header(&self) -> Option<Vec<u8>> {
        // The items `to_rfc3339` writes, into a buffer of the text's own length.
        let rfc3339 = [Item::Fixed(Fixed::RFC3339)];
        exact_text(|text| write!(text, "{}", self.format_with_items(rfc3339.iter())))
    }
}

#[cfg(feature = "time")]
impl HeaderField for OffsetDateTime {
    fn header(&self) -> Option<Vec<u8>> {
        exact_text(|text| {
            self.format_into(text, &Rfc3339)
                .map(drop)
                .map_err(io::Error::other)
        })
    }
}

/// Puts the header `name` holding `field` into `headers`, unless `field` holds none. Machinery;
/// the derive's header map calls it for each field without a role.
#[doc(hidden)]
pub(crate) fn put_header(headers: &mut HeaderMap, name: &'static str, field: &impl HeaderField) {
    if let Some(value) = field.header() {
        // A column's name is a constant: the map holds it as it is, where a `&str` copies.
        headers.insert(Str::from_static(name), value);
    }
}

/// The first of `headers` whose name is not among `names`: the headers a headers struct's fields
/// hold. Machinery.
#[must_use]
#[doc(hidden)]
pub(crate) fn unnamed_header<'h>(headers: &'h HeaderMap, names: &[&str]) -> Option<&'h str> {
    headers
        .iter()
        .map(|(name, _)| name)
        .find(|name| !names.iter().any(|known| known.eq_ignore_ascii_case(name)))
}

/// Where a delivery keeps its header map. Machinery: a flat struct's delivery holds the map it
/// moved out of the row's headers column at the claim; a message assembled from a headers struct
/// holds a cell the map is built into on the first read.
#[doc(hidden)]
pub trait HeaderCell<DB: QueueDatabase, Row: Events<DB>>: Default + Send + Sync + 'static {
    /// The cell of `row`, as the claim hands the row over.
    fn take(row: &mut Row) -> Self;

    /// Whether the cell holds nothing yet: a batch keeps no room for such cells.
    fn is_unset(&self) -> bool;

    /// The header map; `row` is the delivery's row, `None` for an id without a row or a row that
    /// did not decode.
    fn read<'c>(&'c self, row: Option<&Row>) -> &'c HeaderMap;
}

impl<DB: QueueDatabase, Row: Events<DB>> HeaderCell<DB, Row> for HeaderMap {
    fn take(row: &mut Row) -> Self {
        Row::take_headers(row)
    }

    fn is_unset(&self) -> bool {
        self.is_empty()
    }

    fn read<'c>(&'c self, _: Option<&Row>) -> &'c HeaderMap {
        self
    }
}

/// The header map of a message assembled from a headers struct, built on the first read.
/// Machinery.
// A `OnceLock`, not a `OnceCell`: a delivery is `Sync`. A read after the first is one atomic
// load.
#[doc(hidden)]
#[derive(Debug, Default)]
pub struct LazyHeaders(OnceLock<HeaderMap>);

impl<DB, Row> HeaderCell<DB, Row> for LazyHeaders
where
    DB: QueueDatabase,
    Row: Events<DB> + Assembled,
{
    fn take(_: &mut Row) -> Self {
        Self::default()
    }

    fn is_unset(&self) -> bool {
        self.0.get().is_none()
    }

    fn read<'c>(&'c self, row: Option<&Row>) -> &'c HeaderMap {
        self.built(row)
    }
}

impl LazyHeaders {
    /// The header map of `row`, built on the first call.
    fn built<Row: Assembled>(&self, row: Option<&Row>) -> &HeaderMap {
        // A delivery without a row reads an empty map, which allocates nothing.
        self.0
            .get_or_init(|| row.map_or_else(HeaderMap::new, Assembled::header_map))
    }
}

#[cfg(test)]
mod tests {
    //! The lazy header map: built from the row on the first read, once.

    use std::cell::Cell;

    use ruststream::HeaderMap;

    use super::{Assembled, HeaderField, LazyHeaders, put_header, unnamed_header};

    /// A header value that counts how often it was read into a header.
    struct Tenant {
        name: &'static str,
        reads: Cell<usize>,
    }

    impl HeaderField for Tenant {
        fn header(&self) -> Option<Vec<u8>> {
            self.reads.set(self.reads.get() + 1);
            Some(self.name.as_bytes().to_vec())
        }
    }

    /// A message whose header map holds its tenant, and its trace where it has one.
    struct Job {
        tenant: Tenant,
        trace: Option<String>,
    }

    impl Assembled for Job {
        fn header_map(&self) -> HeaderMap {
            let mut headers = HeaderMap::new();
            put_header(&mut headers, "tenant", &self.tenant);
            put_header(&mut headers, "trace", &self.trace);
            headers
        }
    }

    fn job(trace: Option<&str>) -> Job {
        Job {
            tenant: Tenant {
                name: "acme",
                reads: Cell::new(0),
            },
            trace: trace.map(str::to_owned),
        }
    }

    #[test]
    fn the_map_is_built_on_the_first_read_and_only_once() {
        let job = job(Some("t-1"));
        let cell = LazyHeaders::default();
        assert_eq!(job.tenant.reads.get(), 0, "nothing is built before a read");
        let headers = cell.built(Some(&job));
        assert_eq!(headers.get_str("tenant"), Some("acme"));
        assert_eq!(headers.get_str("trace"), Some("t-1"));
        assert_eq!(cell.built(Some(&job)).len(), 2);
        assert_eq!(job.tenant.reads.get(), 1, "the second read reuses the map");
    }

    #[test]
    fn a_field_holding_none_is_left_out_and_a_missing_row_reads_empty() {
        let job = job(None);
        let cell = LazyHeaders::default();
        let headers = cell.built(Some(&job));
        assert_eq!(headers.get_str("tenant"), Some("acme"));
        assert!(!headers.contains("trace"), "a NULL trace is no header");
        assert!(LazyHeaders::default().built(None::<&Job>).is_empty());
    }

    #[test]
    fn a_header_no_field_is_named_for_is_unfit() {
        let headers: HeaderMap = [("Tenant", "acme"), ("x-other", "1")].into_iter().collect();
        assert_eq!(
            unnamed_header(&headers, &["tenant", "trace"]),
            Some("x-other")
        );
        assert_eq!(unnamed_header(&headers, &["tenant", "x-other"]), None);
    }
}