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.