factos_pog

PostgreSQL backend for Factos using pog.

This backend follows the "Simply Event Sourcing" shape used by Factos: accepted facts are stored in an append-only event table, command handlers select the facts relevant to their decision, and appends are accepted only when that command context is still stable.

Design Notes

The event table stores opaque event bytes plus store-visible query metadata: event type and tags. Tags are also mirrored into factos_event_tags so context reads and context-stability checks can use indexed SQL predicates instead of decoding unrelated rows in the application. PostgreSQL does not need to understand payloads, but any payload value needed by future context queries must be exposed as a tag when the event is written.

dispatch_with_query runs inside a PostgreSQL transaction and locks the event table before reading, deciding, checking, and appending. This is intentionally conservative. It makes arbitrary FailIfEventsMatch(query, after) checks correct without trying to infer lock keys from dynamic query metadata. A higher-throughput backend could replace the table lock with advisory locks or more granular query-specific locks, but only if it preserves the same context-stability guarantee.

dispatch is also available for applications where one stream revision really is the intended consistency boundary. It is an implementation strategy, not the definition of Event Sourcing.

Both dispatch functions return factos_pog.Dispatch(event): the append metadata plus the committed factos.Recorded(event) values for that dispatch. Application code can feed those records into factos.Reactor values after the transaction has accepted the facts.

Usage

Start a pog pool in your application supervision tree, run migrate, build a codec with factos_pog.codec, then call dispatch_with_query or dispatch with your domain decider and command.

let connection = pog.named_connection(pool_name)
let assert Ok(Nil) = factos_pog.migrate(connection)
let assert Ok(dispatch) =
factos_pog.dispatch_with_query(
connection,
stream: "tickets",
query: sale_query(),
decider: ticket_decider(),
codec: ticket_codec(),
command: BuyTicket("renata"),
)
let effects = factos.react_all(ticket_reactor(), dispatch.events)

Your codec owns event serialization. The backend only persists bytes and query metadata. Your application owns any effects returned by reactors: it may run them immediately, write them to durable infrastructure, retry them, or ignore them during replay.