backbone_outbox/error.rs
1//! Error type for the durable outbox/inbox/relay.
2
3/// Errors from staging, relaying, or deduping durable events.
4#[derive(Debug, thiserror::Error)]
5pub enum OutboxError {
6 /// A database error.
7 #[error("db: {0}")]
8 Db(#[from] sqlx::Error),
9 /// A schema name that is not a safe SQL identifier (`^[a-z_][a-z0-9_]*$`).
10 #[error("invalid schema identifier: {0:?}")]
11 InvalidSchema(String),
12 /// The relay's `publish` callback failed for a record; the row is left unpublished for retry.
13 #[error("publish failed: {0}")]
14 Publish(String),
15 /// The relay's `publish` callback GAVE UP on a record — its transport exhausted its own retries
16 /// or dead-lettered it, and will not deliver this event however many times it is handed over.
17 ///
18 /// Distinct from [`OutboxError::Publish`] on purpose. A transport that has given up used to have
19 /// no way to say so: returning `Ok` marked the row published, which reads as delivered while the
20 /// effect was dropped, and returning `Publish` re-handed the same doomed record on every pass
21 /// forever. This variant marks the row dead — not published, visible in the table, and
22 /// re-emittable once the cause is fixed.
23 #[error("publish exhausted: {0}")]
24 Exhausted(String),
25}
26
27/// Result alias for outbox operations.
28pub type Result<T> = std::result::Result<T, OutboxError>;
29
30/// Reject a schema name that is not a safe, lowercase SQL identifier. Schema names come from trusted
31/// module config, but they are interpolated into DDL/DML (Postgres has no bind parameter for an
32/// identifier), so we validate defensively.
33pub(crate) fn validate_schema(schema: &str) -> Result<()> {
34 let ok = !schema.is_empty()
35 && schema.len() <= 63
36 && schema.bytes().next().map(|b| b == b'_' || b.is_ascii_lowercase()).unwrap_or(false)
37 && schema.bytes().all(|b| b == b'_' || b.is_ascii_lowercase() || b.is_ascii_digit());
38 if ok {
39 Ok(())
40 } else {
41 Err(OutboxError::InvalidSchema(schema.to_string()))
42 }
43}