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