Current section
Files
Jump to
Current section
Files
guides/sqlite.md
# SQLite
Continuum can keep its durable journal in SQLite, through the
[`ecto_sqlite3`](https://hex.pm/packages/ecto_sqlite3) adapter, for
applications that run on **one node**. Workflows behave exactly as they do on
PostgreSQL: the same replay, drift detection, lease fencing, activities and
retries, timers, signals, child workflows, sagas, `continue_as_new`, schedules,
search attributes, Observer pages, and operator tooling.
PostgreSQL remains the reference backend and the right choice when more than
one node runs Continuum. Choose SQLite when a single BEAM owns the database
file: embedded and edge deployments, small services, desktop applications, or
development environments without a database server.
## Install
Add the adapter next to Continuum:
```elixir
def deps do
[
{:continuum, "~> 0.9.0"},
{:ecto_sqlite3, "~> 0.25"}
]
end
```
`ecto_sqlite3` bundles SQLite. If you build `exqlite` against a system SQLite
instead, it must be version 3.43 or newer. Postgrex is still installed as a
Continuum dependency, but no PostgreSQL server is needed.
## Configure the repo
```elixir
defmodule MyApp.Repo do
use Ecto.Repo, otp_app: :my_app, adapter: Ecto.Adapters.SQLite3
end
```
```elixir
config :my_app, MyApp.Repo,
database: "priv/my_app.db",
pool_size: 5,
busy_timeout: 10_000,
custom_pragmas: [busy_timeout: 10_000],
default_transaction_mode: :immediate
```
- `journal_mode` stays at the adapter's default, WAL, so readers do not block
the writer.
- `exqlite` applies `:busy_timeout` after its other connection pragmas. Setting
it again in `:custom_pragmas`, which run first, lets a connection that opens
during a write wait instead of failing with `database is locked`.
- Continuum runs its own transactions as `BEGIN IMMEDIATE` regardless of this
setting. `default_transaction_mode: :immediate` gives your application's
transactions — including `Continuum.Multi` ingress — the same behavior, so a
transaction that reads and then writes cannot fail when another writer
commits in between. Alternatively pass `mode: :immediate` to
`Repo.transaction/2` for those transactions.
- Keep `ecto_sqlite3`'s default `:iso8601` datetime storage. Continuum compares
timestamps as text and raises if `config :ecto_sqlite3, datetime_type:` is
changed.
## Configure Continuum
For the default instance, select the SQLite journal explicitly:
```elixir
config :continuum, repo: MyApp.Repo, journal: Continuum.Runtime.Journal.SQLite
```
Named instances choose the journal from the repo's adapter:
```elixir
Continuum.children(name: :jobs, repo: MyApp.Repo)
```
Pairing a durable journal with a repo of the other SQL dialect raises an
`ArgumentError` when the instance is built.
Add the runtime children after the repo, exactly as for PostgreSQL:
```elixir
children = [MyApp.Repo] ++ Continuum.children() ++ [MyAppWeb.Endpoint]
```
## Create the schema
```bash
mix continuum.gen.migration --repo MyApp.Repo
mix ecto.migrate
```
The generator detects the SQLite adapter and writes the SQLite schema: the
same tables and partial indexes, a single `continuum_events` table, and a
`continuum_sequences` counter in place of the PostgreSQL lease-token sequence.
SQLite support starts with the current schema, so the `--from` upgrade
migrations are PostgreSQL-only.
## How the runtime adapts
- **Fencing.** SQLite has one writer and no row locks. Every Continuum
transaction that reads, validates a lease token, and then writes starts with
`BEGIN IMMEDIATE`, so it holds the writer lock from its first read. The CAS
checks are the same ones PostgreSQL runs; lease tokens come from
`continuum_sequences` inside the transaction that stores them.
- **Clock.** The database and the application share a host, so lease expiry,
retry backoff, and timer due times use the application's UTC clock.
- **Wakeups.** SQLite has no `LISTEN`/`NOTIFY`. After a timer is armed, the
journal notifies the local timer wheel once the transaction commits; signal
deliveries and child completions wake local engines directly. The
dispatcher poll, timer refresh, and signal catch-up scan remain the
backstops, as they are on PostgreSQL.
- **Events.** The events table is not partitioned. Bound its size with the
retention tools (`Continuum.Pruner`, `mix continuum.runs.prune`,
`mix continuum.archive_continued_chains`).
## Limits
- **One node.** Clustering, cross-node wake routing, and the cluster test
suite are PostgreSQL-only. If several BEAM nodes or OS processes open the
same file, lease fencing still keeps every write correct, but no process is
notified of another's commits, so wakeups wait for the polls. Never place
the database on a network file system; SQLite's locking is not reliable
there.
- **Write throughput.** All writes serialize on one writer. Workloads with many
concurrent activities complete correctly but contend for that lock.
- **Partitions.** `Continuum.Partitions` and the partition mix tasks return or
raise `:partitions_unsupported_on_sqlite`; leave `partition_maintainer`
disabled. The health report shows partition coverage as not applicable.
- **Attribute filters.** `:eq` and `:neq` filters on search attributes support
scalar values and maps of scalars. List values return
`{:error, {:unsupported_attribute_value, value}}`. Numbers compare by their
JSON text, so `4` and `4.0` are different values.
- **Oban executor.** `activity_executor: {:oban, opts}` has no SQLite-specific
code, but requires Oban's own SQLite engine and is not covered by Continuum's
test suite on SQLite.
## Testing
`Continuum.Test` works with SQLite instances. Pass `journal: :sqlite` where a
helper takes a journal, and `instance:` for a named instance:
```elixir
{:ok, run_id} = Continuum.start(MyApp.Checkout, order, instance: :jobs)
:ok = Continuum.Test.drive_until_state(run_id, [:suspended], instance: :jobs)
:ok = Continuum.Test.inject_signal(run_id, :approved, true, journal: :sqlite, instance: :jobs)
assert {:ok, %{state: :completed}} = Continuum.Test.drive(run_id, instance: :jobs)
```
SQLite's single writer and the SQL Sandbox do not combine well for concurrent
tests. Continuum's own suite uses a file database rebuilt at the start of each
run, synchronous test modules, and table cleanup between tests.
## Why not a SQLite queue extension?
Extensions such as [Honker](https://hex.pm/packages/honker) add
`NOTIFY`-style notifications and queues to SQLite. Continuum does not need
them for a single node: its activity tasks, timers, and signal mailbox are
already durable tables guarded by lease fencing, every write goes through the
application's Ecto repo, and in-process notifications are immediate. Such an
extension could later supply cross-process wakeups for deployments where
several OS processes share one database file.