All execution in beancount_ex flows through the Beancount.Engine behaviour.
This is the seam that lets the backend change without breaking the public API.
defmodule Beancount.Engine do
@callback render(term()) :: binary()
@callback check(binary()) ::
{:ok, Beancount.Result.t()} | {:error, Beancount.Result.t()}
@callback query(binary(), binary()) ::
{:ok, Beancount.Query.Result.t()} | {:error, Beancount.Result.t()}
endSelecting an engine
config :beancount_ex, engine: Beancount.Engine.CLIBeancount.render/1 and Beancount.check/1 dispatch to
Beancount.Engine.configured/0, so applications never call an engine directly.
The CLI engine (default)
Beancount.Engine.CLI is the default engine. It:
- delegates
render/1toBeancount.Renderer, - delegates
check/1toBeancount.Checker, which shells out tobean-check, and - delegates
query/2toBeancount.Query, which shells out tobean-query.
The binaries are configurable:
config :beancount_ex,
bean_check_path: "bean-check",
bean_query_path: "bean-query"If a binary cannot be found, the relevant wrapper raises
Beancount.Checker.NotInstalledError / Beancount.Query.NotInstalledError.
This is deliberately distinct from a ledger that fails validation or a query
that fails (which return {:error, %Beancount.Result{}}) so that environment
problems are never confused with accounting errors.
Results are engine-independent
Every engine populates the same Beancount.Result and Beancount.Query.Result
structs, and Beancount.Normalizer produces a stable, backend-independent view
of the output. This normalization is what makes cross-engine comparison
possible.
Because query/2 is part of the behaviour, native engines must implement it
too - keeping the oracle contract uniform across backends.
The Elixir engine
Beancount.Engine.Elixir is a native engine with golden-fixture parity
against the CLI oracle for check and canned reports:
config :beancount_ex, engine: Beancount.Engine.Elixir| Callback | Behaviour |
|---|---|
render/1 | delegates to Beancount.Renderer |
check/1 | booking, balance assertions, pad resolution, tolerance inference |
query/2 | canned reports (balances, balance_sheet, income_statement, holdings, journal) |
For BQL queries beyond the canned set, use Engine.CLI (bean-query).
For ad-hoc queries against stored directives, use Beancount.Queries
(Ecto.Query). See Queries and Storage.
See Booking and Reconciliation.
Beancount.check_file/1 routes through the configured engine. The CLI engine
passes the file path to bean-check (so include resolves relative to the
file). With Beancount.Engine.Elixir configured, the file is read into text
before check/1 runs, so include directives are not resolved relative to
the original path.
Future engines
Additional engines (e.g. native Rust via NIF/port) can implement the same
behaviour. Because they share the Beancount.Result shape, swapping engines
requires no changes to Beancount.* callers.