Skip to main content

reliar_store_postgres/
lib.rs

1//! `reliar-store-postgres` is Reliar's PostgreSQL provider: the schema, the explicit
2//! [`migrate`] API, and [`PostgresOutboxStore`] — the only crate where an `sqlx`/Postgres type
3//! appears (ADR 0002).
4//!
5//! # Quickstart
6//!
7//! `PostgresOutboxStore::new` is the default-type-param constructor, gated on the default
8//! `json` feature; without it this block still shows the shape but is not compiled.
9#![cfg_attr(not(feature = "json"), doc = "```ignore")]
10#![cfg_attr(feature = "json", doc = "```no_run")]
11//! # async fn run() -> Result<(), Box<dyn std::error::Error>> {
12//! use reliar_store_postgres::{PostgresOutboxStore, migrate};
13//! use reliar_outbox::OutboxEnqueue;
14//! use reliar_core::Message;
15//! use sqlx::postgres::PgPoolOptions;
16//!
17//! #[derive(serde::Serialize, serde::Deserialize)]
18//! struct OrderPlaced {
19//!     order_id: String,
20//! }
21//!
22//! impl Message for OrderPlaced {
23//!     const TYPE: &'static str = "orders.placed";
24//!     const VERSION: u16 = 1;
25//! }
26//!
27//! let database_url = std::env::var("DATABASE_URL")?;
28//! let pool = PgPoolOptions::new().connect(&database_url).await?;
29//!
30//! // Run once, out of band — never implicitly at startup.
31//! migrate(&pool, Default::default()).await?;
32//!
33//! let store = PostgresOutboxStore::new(pool.clone());
34//!
35//! let mut tx = pool.begin().await?;
36//! store.enqueue(&mut tx, OrderPlaced { order_id: "ord_1".into() }).await?;
37//! tx.commit().await?;
38//!
39//! // Hand `store` to an `OutboxDispatcher` to publish what was just enqueued.
40//! # Ok(())
41//! # }
42//! ```
43//!
44//! # MSRV
45//!
46//! This crate declares `rust-version = "1.94"`, six releases above the workspace floor
47//! (`1.88`): `sqlx` 0.9 requires it. Pure crates (`reliar-core`, `reliar-outbox`) stay reachable
48//! on `1.88` for hosts bringing their own store (ADR 0025).
49//!
50//! # Features
51//!
52//! - `json` (**default**) — [`PostgresOutboxStore<JsonSerializer>`]'s default type parameter
53//!   and the [`PostgresOutboxStore::new`]/[`PostgresOutboxStore::with_settings`] convenience
54//!   constructors (forwards `reliar-core/json`). Not hard-enabled: a deployment supplying its
55//!   own [`reliar_core::Serializer`] should not have to pull in `serde_json`. Under
56//!   `--no-default-features`, [`PostgresOutboxStore::with_serializer`] is the only constructor.
57//! - `serde` (off by default) — `Serialize`/`Deserialize` on [`PostgresOutboxSettings`],
58//!   `#[serde(default, deny_unknown_fields)]` so a typo'd config key is a hard error, durations
59//!   as integer milliseconds. `serde` itself is always a dependency regardless of this feature —
60//!   it also drives the crate's private `MetadataRest` JSONB contract (ADR 0012), which is not
61//!   feature-gated.
62//!
63//! # `search_path` setup
64//!
65//! Every Reliar object lives in **one configurable schema, `reliar` by default**, with
66//! unprefixed table names (`outbox`). `sqlx::query!` checks SQL at compile time, so every
67//! identifier in every statement is a static, unqualified literal — the schema is resolved at
68//! connection time through `search_path`, never compiled in (ADR 0017).
69//!
70//! - **The host puts `reliar` first** on the connection URL: `?options=-c%20search_path%3Dreliar,public`.
71//! - **Behind a transaction-mode pooler that drops startup `options`** (some reject the
72//!   parameter outright with `08P01`), use a server-side default instead:
73//!   `ALTER ROLE <app> SET search_path = reliar, public`. This is the portable mechanism —
74//!   verify it against your own pooler build/version rather than assuming: `PgDog`
75//!   (`ghcr.io/pgdogdev/pgdog:v0.1.46`, the pooler this crate's suite runs behind) was found to
76//!   **pass the `options` parameter through** to the upstream server instead of dropping it, so
77//!   the URL-`options` path above works unmodified behind it too, with no `ALTER ROLE` required
78//!   — but a different pooler, or a different `PgDog` configuration, could behave either way.
79//! - **Reliar does not verify this at startup** (ADR 0047). Constructing a store issues no
80//!   query; a `search_path` that does not resolve `outbox`/`inbox` surfaces at the first store
81//!   call as [`PostgresOutboxError::NotMigrated`]/[`PostgresInboxError::NotMigrated`], whose
82//!   message names both the `migrate()` and the `ALTER ROLE` remedy. Reliar never sets
83//!   `search_path` on a pool it does not own — not at construction, not per call.
84//! - [`migrate`] does not depend on the caller's `search_path`: it creates the schema itself and
85//!   qualifies its own bookkeeping table name (ADR 0018).
86//!
87//! # PostgreSQL version floor
88//!
89//! **Requirements: PostgreSQL 18 or later.** Reliar does not check the server version; behaviour
90//! on older servers is undefined. Neither a store constructor nor [`migrate`] issues a version
91//! probe: a server below the floor is unsupported and fails at whichever statement first needs a
92//! PostgreSQL 18 feature (`uuidv7()`, in practice) — there is no conditional DDL, no substitute,
93//! and no degraded mode (ADR 0015, ADR 0047 Amendment B).
94//!
95//! # Guarantees
96//!
97//! - **Migrations never run implicitly.** [`migrate`] is the only entry point, and it is
98//!   idempotent and safe under concurrent callers.
99//! - **A store constructor performs no I/O** (ADR 0047) — no query, no connection, no schema or
100//!   version check; it can run inside a `OnceLock`, a `Default` impl, or a synchronous `main()`.
101//! - **The claim is one statement.** [`PostgresOutboxStore`]'s `acquire` (via
102//!   [`reliar_outbox::OutboxStore`]) uses a `FOR UPDATE SKIP LOCKED` claim that commits before
103//!   the call returns; no network I/O ever happens while a Reliar transaction is open (ADR 0006).
104//! - **Enqueuing joins the caller's own transaction** — [`PostgresOutboxStore`] implements
105//!   [`reliar_outbox::OutboxEnqueue`] directly, no facade type in between, and atomicity is
106//!   visible in the signature — and performs no I/O beyond the one `INSERT`.
107
108#![cfg_attr(docsrs, feature(doc_cfg))]
109#![forbid(unsafe_code)]
110#![warn(missing_docs)]
111
112mod connection;
113mod duration_serde;
114mod error;
115mod inbox;
116mod migrate;
117mod outbox;
118mod records;
119mod settings;
120
121pub use inbox::{PostgresInboxError, PostgresInboxStore};
122pub use migrate::{MigrateError, MigrateOptions, migrate};
123pub use outbox::{EnqueueError, PostgresOutboxError, PostgresOutboxStore};
124pub use reliar_core::SettingsError;
125pub use settings::{PostgresInboxSettings, PostgresOutboxSettings};
126
127#[cfg(feature = "json")]
128#[cfg_attr(docsrs, doc(cfg(feature = "json")))]
129pub use reliar_core::JsonSerializer;
130
131// The README's Usage block drives `PostgresOutboxStore::new`, the default-type-param
132// constructor that only exists under `json`; gate the whole module rather than editing static
133// markdown to carry a per-block cfg_attr.
134#[cfg(all(doctest, feature = "json"))]
135mod readme_doctests;