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
//! `reliar-store-postgres` is Reliar's PostgreSQL provider: the schema, the explicit
//! [`migrate`] API, and [`PostgresOutboxStore`] — the only crate where an `sqlx`/Postgres type
//! appears (ADR 0002).
//!
//! # Quickstart
//!
//! `PostgresOutboxStore::new` is the default-type-param constructor, gated on the default
//! `json` feature; without it this block still shows the shape but is not compiled.
//! # async fn run() -> Result<(), Box<dyn std::error::Error>> {
//! use reliar_store_postgres::{PostgresOutboxStore, migrate};
//! use reliar_outbox::OutboxEnqueue;
//! use reliar_core::Message;
//! use sqlx::postgres::PgPoolOptions;
//!
//! #[derive(serde::Serialize, serde::Deserialize)]
//! struct OrderPlaced {
//! order_id: String,
//! }
//!
//! impl Message for OrderPlaced {
//! const TYPE: &'static str = "orders.placed";
//! const VERSION: u16 = 1;
//! }
//!
//! let database_url = std::env::var("DATABASE_URL")?;
//! let pool = PgPoolOptions::new().connect(&database_url).await?;
//!
//! // Run once, out of band — never implicitly at startup.
//! migrate(&pool, Default::default()).await?;
//!
//! let store = PostgresOutboxStore::new(pool.clone()).await?;
//!
//! let mut tx = pool.begin().await?;
//! store.enqueue(&mut tx, OrderPlaced { order_id: "ord_1".into() }).await?;
//! tx.commit().await?;
//!
//! // Hand `store` to an `OutboxDispatcher` to publish what was just enqueued.
//! # Ok(())
//! # }
//! ```
//!
//! # MSRV
//!
//! This crate declares `rust-version = "1.94"`, six releases above the workspace floor
//! (`1.88`): `sqlx` 0.9 requires it. Pure crates (`reliar-core`, `reliar-outbox`) stay reachable
//! on `1.88` for hosts bringing their own store (ADR 0025).
//!
//! # Features
//!
//! - `json` (**default**) — [`PostgresOutboxStore<JsonSerializer>`]'s default type parameter
//! and the [`PostgresOutboxStore::new`]/[`PostgresOutboxStore::with_settings`] convenience
//! constructors (forwards `reliar-core/json`). Not hard-enabled: a deployment supplying its
//! own [`reliar_core::Serializer`] should not have to pull in `serde_json`. Under
//! `--no-default-features`, [`PostgresOutboxStore::connect`] is the only constructor.
//! - `serde` (off by default) — `Serialize`/`Deserialize` on [`PostgresOutboxSettings`],
//! `#[serde(default, deny_unknown_fields)]` so a typo'd config key is a hard error, durations
//! as integer milliseconds. `serde` itself is always a dependency regardless of this feature —
//! it also drives the crate's private `MetadataRest` JSONB contract (ADR 0012), which is not
//! feature-gated.
//!
//! # `search_path` setup
//!
//! Every Reliar object lives in **one configurable schema, `reliar` by default**, with
//! unprefixed table names (`outbox`). `sqlx::query!` checks SQL at compile time, so every
//! identifier in every statement is a static, unqualified literal — the schema is resolved at
//! connection time through `search_path`, never compiled in (ADR 0017).
//!
//! - **The host puts `reliar` first** on the connection URL: `?options=-c%20search_path%3Dreliar,public`.
//! - **Behind a transaction-mode pooler that drops startup `options`** (some reject the
//! parameter outright with `08P01`), use a server-side default instead:
//! `ALTER ROLE <app> SET search_path = reliar, public`. This is the portable mechanism —
//! verify it against your own pooler build/version rather than assuming: `PgDog`
//! (`ghcr.io/pgdogdev/pgdog:v0.1.46`, the pooler this crate's suite runs behind) was found to
//! **pass the `options` parameter through** to the upstream server instead of dropping it, so
//! the URL-`options` path above works unmodified behind it too, with no `ALTER ROLE` required
//! — but a different pooler, or a different `PgDog` configuration, could behave either way.
//! - [`PostgresOutboxStore::connect`]/[`PostgresOutboxStore::new`] verify **once at
//! construction** that the unqualified name `outbox` resolves to the configured schema, and
//! fail fast — naming the configured schema, the observed `search_path`, and the `ALTER ROLE`
//! remedy — rather than surprise-failing on the first `acquire`.
//! - [`migrate`] does not depend on the caller's `search_path`: it creates the schema itself and
//! qualifies its own bookkeeping table name (ADR 0018).
//!
//! # PostgreSQL version floor
//!
//! **PostgreSQL 18 or later is a hard requirement, with no older-version fallback** (ADR 0015,
//! amended by ADR 0041). [`PostgresOutboxStore::connect`] and [`migrate`]
//! each check the connected server's `server_version_num` against [`MIN_SERVER_VERSION_NUM`] —
//! once per entry point, never per pooled connection — and fail with
//! [`PostgresOutboxError::UnsupportedServerVersion`] / [`MigrateError::UnsupportedServerVersion`]
//! below it, naming the required and detected version. There is no conditional DDL, no
//! `uuidv7()` substitute, and no degraded mode.
//!
//! # Guarantees
//!
//! - **Migrations never run implicitly.** [`migrate`] is the only entry point, and it is
//! idempotent and safe under concurrent callers.
//! - **The claim is one statement.** [`PostgresOutboxStore`]'s `acquire` (via
//! [`reliar_outbox::OutboxStore`]) uses a `FOR UPDATE SKIP LOCKED` claim that commits before
//! the call returns; no network I/O ever happens while a Reliar transaction is open (ADR 0006).
//! - **Enqueuing joins the caller's own transaction** — [`PostgresOutboxStore`] implements
//! [`reliar_outbox::OutboxEnqueue`] directly, no facade type in between, and atomicity is
//! visible in the signature — and performs no I/O beyond the one `INSERT` (plus, opt-in, a
//! `search_path` wrap).
pub use MIN_SERVER_VERSION_NUM;
pub use ;
pub use ;
pub use ;
pub use SettingsError;
pub use ;
pub use JsonSerializer;
// The README's Usage block drives `PostgresOutboxStore::new`, the default-type-param
// constructor that only exists under `json`; gate the whole module rather than editing static
// markdown to carry a per-block cfg_attr.