# Factos Sqlight

`factos_sqlight` stores Factos events in SQLite through
[`sqlight`](https://hex.pm/packages/sqlight). It is intended for embedded,
single-process applications that want the same model and decision-context API as
other Factos backends.

## Setup

Apply `priv/migrations.sql`, or copy it into your application migration history.
The schema contains one append-only `factos_events` table.

## Configure and dispatch

```gleam
let model = user.model()

let configuration = factos_sqlight.configure(model, connection: connection)

configuration
|> factos_sqlight.dispatch(
  command,
  decision_context: user.decision_context(command),
  event_id: new_event_id,
)
```

Dispatch uses `BEGIN IMMEDIATE`, so the context read, event append, and
subscriptions share one SQLite transaction.

## Transactional subscriptions

```gleam
let projection =
  factos.subscription(fn(connection, recorded) {
    use _ <- result.try(projection.apply(connection, recorded))
    Ok(connection)
  })

let configuration =
  factos_sqlight.Configuration(
    ..factos_sqlight.configure(model, connection: connection),
    subscriptions: [projection],
  )
```

A subscription failure rolls back the event append and all subscription writes.

## Simulate projections

Use `factos/factos_sqlight/simulate` to run the same subscriptions from an
in-memory domain scenario against a real SQLite projection database:

```gleam
import factos/factos_sqlight/simulate as simulate_sqlight
import factos/simulate

let simulation =
  simulate_sqlight.new(
    model,
    with: existing_events,
    connection:,
    subscriptions: [projection],
  )
  |> simulate.dispatch(decision_context:, command:)

assert simulate.errors(simulation) == []

assert Ok(rows) = projection.load(connection)
```

The initial event batch and each accepted dispatch batch run in their own
`BEGIN IMMEDIATE` transaction. Subscription or transaction failure rolls back
projection writes and leaves the batch out of simulated history. The simulator
does not write the SQLite event log; event-store atomicity and locking remain
dispatch integration concerns.

## Durable work

There is no built-in M25 equivalent for SQLite. Persist application-owned jobs
through a transactional subscription, then process them after commit with a
supervised worker. External IO must not run inside a retryable dispatch.

## Recovery reads

`read_after` returns a bounded page in global position order for rebuilds and
application-owned recovery.

## Development

```sh
trellis run test factos_sqlight
```

See [How factos_sqlight works](docs/how-it-works.md) for locking and retry
details.
