EventCore PostgreSQL
eventcore-postgres provides EventCore's PostgreSQL event store and the
transactional PostgreSQL projection runner. Most applications enable
eventcore's postgres feature and use the facade:
use ;
The transactional projection API is additive for EventCore 2.1.0. The legacy
eventcore::Projector and eventcore::run_projection APIs from 2.0.1 remain
source-compatible and retain their existing non-atomic effect/checkpoint
semantics.
Transactional guarantee
run_transactional_projection owns a transaction on the read-model database,
lends it to PostgresProjector::apply, advances named progress through the same
transaction, and commits once. Acknowledged commits therefore provide
effect-plus-progress exactly-once behavior inside that read-model database.
This guarantee does not include the event-store database, network calls, or any other external system. The event source and read-model destination are separate arguments and may use different pools, schemas, or PostgreSQL databases.
Minimal wiring
Applications use their own SQLx API for pools and queries. Import Postgres and
Transaction from EventCore's facade so the projector signature cannot drift
from EventCore's SQLx version. This workspace currently resolves SQLx 0.8.6 in
Cargo.lock; downstream applications should use a compatible SQLx release for
their own query APIs. The complete compiled example is
eventcore/tests/postgres_transactional_projection_api_test.rs; its essential
wiring is:
use Future;
use ;
use Deserialize;
use ;
async
Before this wiring, run PostgresEventStore::migrate() on the source and the
application's read-model migration on the destination. Source and destination
projection migrations are separate and use an EventCore component ledger, not
the application's _sqlx_migrations history.
Treat the first PostgresProjectionSource::migrate() as a maintenance
operation. It takes an ACCESS EXCLUSIVE lock on eventcore_events and
backfills the complete event history, which can block reads and writes for a
substantial period on a large store. Measure it against representative data and
schedule an appropriate maintenance window. Historical rows are assigned a
deterministic backfill order of (stream_id, stream_version, event_id); that is
not their original commit order. Subsequent appends allocate the global
delivery frontier inside the event-store transaction, providing commit-safe
ordering for newly written events.
Operating the runner
- EventCore binds source and selection semantics only through caller-supplied
IDs. Keep
DeliverySourceId,ProjectorName, andProjectionSelectionIdstable while their meanings are stable. A changed stream filter or event-type set requires a new selection ID; changed source ordering semantics require a new source ID. Existing progress is rejected when the configured IDs differ. - The selection contains persisted event-type names plus an all/prefix/pattern
stream filter. By default,
PostgresProjector::decodedeserializes the envelope payload as JSON intoPostgresProjector::Event. Overridedecodewhen one selection contains multiple persisted event types that need the envelope'sevent_typeormetadatafor application-owned routing, including types with identical payload shapes. Any selected decode or discriminator error stops terminally withDecode; it does not invokeon_error, retry, or advance progress. - Batch mode captures a high-water mark and drains every page through it.
Continuous mode polls successive high-water marks until its
CancellationTokenis cancelled. Cancellation is observed while idle after the current bounded catch-up cycle; it does not interruptapply, retry delay, commit, orAfterCommitwork. - Application failures default to
Fatal.on_errormay explicitly chooseRetry,Skip,Stop, orFatal; retry attempts are bounded byProjectionRetryPolicy. OnlySkipcommits progress without the effect. - An
AfterCommitaction runs only after an acknowledged commit. If it fails, progress is already committed. Put durable external work in an outbox row written byapply; a callback or direct network call is not transactionally exactly once. CommitIndeterminatemeans PostgreSQL may have committed both effect and progress before the connection failed. Inspect durable progress/read-model or outbox state, then restart with the same identities.
A database rollback does not rewind fields mutated through the projector's
&mut self. Keep durable or retry-sensitive attempt state in the supplied
transaction, derive it again from the event/database, or make instance-state
changes explicitly rollback-safe.
Leadership, reset, and replay
The destination holds a session advisory lock on the same physical connection that owns every effect/progress transaction. Use a direct connection or session pool. Transaction-pooling middleware cannot preserve this fencing guarantee.
Reset uses the same named leadership. reset_transactional_projection resets
the model and deletes progress atomically;
reset_and_replay_transactional_projection retains leadership through the
subsequent replay. Stop other read-model writers and schedule downtime. Legacy
UUID checkpoints cannot be adopted as delivery positions: reset, choose stable
transactional identities, and replay. To change an existing transactional
source or selection, invoke reset with the old source/selection IDs persisted
in progress, then run with the new IDs. A new ProjectorName alone does not
empty an already-populated model and can duplicate non-idempotent effects; use
it only with a new or empty model. Reset operates on the live model and does not
provide shadow-generation rebuilding.
Release status
The additive API targets EventCore 2.1.0 but no crate containing it has been published. Foundry and other consumers must not select it until publication is explicitly approved. After that approved publication, Foundry's exact recommendation is:
= { = "=2.1.0", = ["postgres"] }