# `Lotus.Runner`
[🔗](https://github.com/elixir-lotus/lotus/blob/v1.1.0/lib/lotus/runner.ex#L1)

Statement execution with safety checks, param binding, and result shaping.

By default, all statements are read-only. Destructive operations (writes,
schema changes — INSERT, UPDATE, DELETE and DDL in SQL terms) are blocked
at both the application and database level. Pass `read_only: false` to
allow write operations.

What counts as a write is the adapter's judgement, via
`c:Lotus.Source.Adapter.sanitize_query/3`.

# `assigns`

```elixir
@type assigns() :: map()
```

What the `:before_execute` plugs of a call left under `:assigns`, handed to
the `:after_query` plugs of the same call. It belongs to that call alone, so
the result cache never stores it.

# `authorization`

```elixir
@type authorization() :: %{relations: relations(), assigns: assigns()}
```

What authorizing a statement produced: the relations preflight proved it
touches, and the assigns its `:before_execute` plugs left.

# `execution`

```elixir
@type execution() :: %{result: query_result(), relations: relations()}
```

What executing a statement produced: the result, and the relations preflight
proved the statement touches.

This is the unit the result cache stores, so that a caller serving a result
from the cache can still hand the relations to `:before_execute`.

# `executor`

```elixir
@type executor() :: (Lotus.Query.Statement.t() -&gt; {:ok, outcome()} | {:error, term()})
```

The execute step of a run: takes the statement `:before_query` returned and
produces the outcome. `run/4` supplies one that executes against the source;
a caller that caches executions supplies one that consults the cache first.

A step that reports origin `:executed` has run `:before_execute` and returns
its assigns. A step that reports `:cached` has not, and returns none: `run/4`
runs the gate and takes the assigns from it.

# `opts`

```elixir
@type opts() :: [
  timeout: non_neg_integer(),
  statement_timeout_ms: non_neg_integer(),
  read_only: boolean(),
  search_path: String.t() | nil,
  scope: term(),
  context: term(),
  vars: map()
]
```

# `origin`

```elixir
@type origin() :: :executed | :cached
```

Where a result came from: executed against the source, or served from the
result cache.

# `outcome`

```elixir
@type outcome() :: %{
  :statement =&gt; Lotus.Query.Statement.t(),
  :result =&gt; query_result(),
  :relations =&gt; relations(),
  :origin =&gt; origin(),
  optional(:assigns) =&gt; assigns()
}
```

What a run produced, as `:after_query` sees it: the statement that ran, its
result, the relations, where the result came from, and the assigns of
`:before_execute`.

# `query_result`

```elixir
@type query_result() :: Lotus.Result.t()
```

# `relations`

```elixir
@type relations() :: Lotus.Preflight.Relations.outcome()
```

What preflight knows about the relations a statement touches.

A list is a proven set of `{schema, table}` pairs, and an empty list means
the statement touches no relation (`SELECT 1`). A tuple means nothing is
known: `{:unrestricted, reason}` when the adapter cannot name relations and
the host opted in, `{:skipped, reason}` when the adapter does not preflight
the statement at all. A plug that gates on tables matches `when is_list/1`
and treats any tuple as unknown.

# `after_query`

```elixir
@spec after_query(Lotus.Source.Adapter.t(), outcome(), opts()) ::
  {:ok, query_result()} | {:error, term()}
```

Runs the `:after_query` pipeline on an outcome and returns the result it
yields.

The payload carries the statement that ran, the result, the relations
preflight found for it — the same value `:before_execute` received — the
origin, `:executed` or `:cached`, and the assigns the `:before_execute` plugs
of this call left, `%{}` when the outcome has none. It runs whether the
result came from the cache or from the source. A plug that changes the result
changes what this call returns; the stored entry keeps the raw execution.

# `before_execute`

```elixir
@spec before_execute(
  Lotus.Source.Adapter.t(),
  Lotus.Query.Statement.t(),
  relations(),
  origin(),
  opts()
) ::
  {:ok, assigns()} | {:error, term()}
```

Runs the `:before_execute` pipeline for a statement and the relations it
touches.

`execute_statement/3` runs this itself, with the relations preflight just
found and origin `:executed`. `run/4` runs it with the relations stored
alongside a cached result and origin `:cached`, so the gate fires on every
call: a plug that authorizes a statement against its tables is an access
control, and an access control that a warm cache skips is no control at all.

The payload starts with `assigns: %{}`. Returns the `:assigns` of the payload
the last plug continued with, for `:after_query`; `%{}` when that value is not
a map. The other keys a plug changes are ignored: `:before_execute` decides
whether the statement proceeds, it does not rewrite it.

# `before_query`

```elixir
@spec before_query(Lotus.Source.Adapter.t(), Lotus.Query.Statement.t(), opts()) ::
  {:ok, Lotus.Query.Statement.t()} | {:error, term()}
```

Runs the `:before_query` pipeline and returns the statement to execute.

`:before_query` runs first because a plug may rewrite the statement — that is
the point of the hook, for row-level security and tenant predicates.
Sanitization and preflight then apply to what will actually execute, rather
than to the text the caller originally supplied.

A caller that caches the execution runs this phase outside the cache
callback, and builds the cache key from the statement returned here: a plug
that varies on `:context` must see every call, not only the one that fills
the cache, and two plugs that rewrite differently must not share an entry.

# `execute_statement`

```elixir
@spec execute_statement(Lotus.Source.Adapter.t(), Lotus.Query.Statement.t(), opts()) ::
  {:ok, %{result: query_result(), relations: relations(), assigns: assigns()}}
  | {:error, term()}
```

Executes a statement: sanitization, preflight, `:before_execute`, the query
itself and column policy enforcement.

Returns the result, the relations preflight proved the statement touches and
the assigns `:before_execute` left. The result cache stores the result and the
relations only — see `t:execution/0`. The query middleware around this phase
lives in `before_query/3` and `after_query/4`, and `[:lotus, :query, *]`
telemetry covers this phase only, so a statement served from the cache emits
no query events.

# `run`

```elixir
@spec run(
  Lotus.Source.Adapter.t(),
  Lotus.Query.Statement.t(),
  opts(),
  executor() | nil
) ::
  {:ok, query_result()} | {:error, term()}
```

Runs the phases of a statement run around an execute step.

This is the one place the phase order lives. `run_statement/3` uses it with
the default step, which executes against the source; `Lotus` uses it with a
step that consults the result cache. Both get the same phases, the same
payloads and the same telemetry:

1. `:before_query`, which may rewrite the statement.
2. The execute step, with the statement `:before_query` returned. The default
   step is `execute_statement/3`, which runs sanitization, preflight and
   `:before_execute` itself.
3. `:before_execute`, when the step served a cached result — the relations
   stored with the entry stand in for preflight, so the gate fires on a hit
   as it does on a miss.
4. `:after_query`, with the assigns `:before_execute` left on this call.

`[:lotus, :run, *]` telemetry brackets all of it: `:start` before the first
phase, `:stop` after `:after_query` with the origin and the relations, and
`:exception` when any phase fails, with the `:phase` that failed. It fires on
a cache hit and on a halt alike, so a consumer that records who ran what and
whether it was refused attaches to these three events and nothing else.

A step may return `{:error, {Lotus.Runner, phase, reason}}` to name the phase
that failed; the caller receives `{:error, reason}`.

# `run_statement`

```elixir
@spec run_statement(Lotus.Source.Adapter.t(), Lotus.Query.Statement.t(), opts()) ::
  {:ok, query_result()} | {:error, term()}
```

Runs a statement through the full pipeline: `:before_query`, sanitization,
preflight, `:before_execute`, execution, column policy enforcement and
`:after_query`. See `run/4`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
