Packages

PostgreSQL backend for Factos context-first Event Sourcing using pog.

Current section

Files

Jump to
factos_pog README.md
Raw

README.md

# Factos PostgreSQL
`factos_pog` stores Factos events in PostgreSQL through
[`pog`](https://hex.pm/packages/pog). Dispatches run in serializable transactions
and retry serialization or deadlock conflicts.
## Setup
Apply `priv/migrations.sql`, or copy the SQL into your migration history. The
schema contains:
- `factos.factos_events`, the globally ordered event log;
- `factos.factos_event_tags`, the selective context index.
Runtime code should not execute migrations.
## Configure and dispatch
```gleam
let model = user.model()
let configuration = factos_pog.configure(model, connection: connection)
configuration
|> factos_pog.dispatch(
command,
decision_context: user.decision_context(command),
event_id: new_event_id,
)
```
A successful dispatch returns append-ordered `factos.Recorded(event)` values and
the final global position.
## Transactional subscriptions
Subscriptions run after the event insert and before commit. They receive the
same Pog transaction connection, in subscription order and event order.
```gleam
let projection =
factos.subscription(fn(connection, recorded) {
use _ <- result.try(user_projection.apply(connection, recorded))
Ok(connection)
})
let configuration =
factos_pog.Configuration(
..factos_pog.configure(model, connection: connection),
subscriptions: [projection],
)
```
A subscription error rolls back its writes, earlier subscription writes, and
the event append.
## Simulate projections
Use `factos/factos_pog/simulate` to run the same subscriptions from an
in-memory domain scenario against a real PostgreSQL projection database:
```gleam
import factos/factos_pog/simulate as simulate_pog
simulate_pog.new(
model,
connection:,
subscriptions: [projection],
)
|> simulate.given(existing_events)
|> simulate.dispatch(decision_context:, command:)
|> simulate.assert_errors([])
assert Ok(rows) = user_projection.load(connection)
```
Each accepted event batch runs in its own serializable transaction.
Subscription or transaction failure rolls back projection writes and leaves the
batch out of simulated history. The simulator does not write the PostgreSQL
event log; event-store atomicity and concurrency remain dispatch integration
concerns.
## Durable work with M25
[`m25`](https://hexdocs.pm/m25/) is a natural companion when an accepted event
must create durable background work. Enqueue the M25 job from a Factos
subscription using the supplied transaction connection:
```gleam
let durable_job =
factos.subscription(fn(connection, recorded) {
use _ <- result.try(
recorded
|> job_for_event
|> m25.new_job
|> m25.enqueue(connection, queue, _),
)
Ok(connection)
})
```
The event and job commit together. M25 then provides supervised, at-least-once
execution and retries after commit. External IO does not run inside the Factos
transaction.
## Recovery reads
`read_after` returns a bounded, globally ordered page after a sequence position.
Use it for projection rebuilds and application-owned recovery workers.
## Development
From the repository root:
```sh
docker compose up --wait -d
trellis run test factos_pog
```
See [How factos_pog works](docs/how-it-works.md) for transaction and schema
details. The stress benchmark lives in
[`benchmark/`](https://tangled.org/renatillas.dev/factos/tree/main/backends/factos_pog/benchmark).