Skip to main content

docs/runtime_behavior.md

# Runtime Behavior

This guide documents the parts of Arex that affect production behavior: option resolution, retries, timeouts, normalized errors, observability, and ArcadeDB-specific quirks.

## Option Resolution

Arex resolves options in a predictable order:

1. per-call options
2. application config for `:arex`
3. environment variables for `url`, `user`, `pwd`, and `db`

`language` does not use environment fallback. It comes from call options or application config and otherwise defaults to `"sql"`.

This means you can keep stable connection defaults in config while overriding them per call for tests, admin jobs, or multi-database workloads.

## Important Call Options

| Option                | Meaning                                                         |
| --------------------- | --------------------------------------------------------------- |
| `db`                  | target database for the call                                    |
| `type`                | type name for type-aware record helpers                         |
| `tenant`              | tenant boundary                                                 |
| `scope`               | scope boundary inside a tenant                                  |
| `language`            | query or command language, defaulting to `sql`                  |
| `receive_timeout`     | HTTP receive timeout in milliseconds                            |
| `retry`               | read retry policy such as `[max: 3, backoff_ms: 200]`           |
| `transaction`         | write transaction mode, one of `:auto`, `:required`, or `false` |
| `transaction_timeout` | positive integer timeout for transactional helpers              |
| `headers`             | extra request headers, merged without overriding auth           |
| `req_options`         | sanitized Req options merged into the request                   |

Validation rules worth remembering:

- `scope` requires `tenant`
- `receive_timeout` and `transaction_timeout` must be positive integers when present
- `retry` must be `false` or a keyword list with non-negative `max` and `backoff_ms`
- `headers` must be a map or keyword list
- `req_options` must be a map or keyword list

## Timeouts And Retries

Arex makes retry behavior explicit instead of hiding it behind transport defaults.

- `receive_timeout` defaults to `60_000` milliseconds when omitted
- read helpers can opt into retry with `retry: [max: n, backoff_ms: ms]`
- write helpers reject `retry:` with `{:error, %{kind: :bad_opts, ...}}`
- `req_options` retry-related keys are stripped so callers cannot override helper retry policy indirectly

Example read tuning:

```elixir
Arex.Query.sql(
  "select from Customer where external_id = :external_id",
  %{"external_id" => "cust-1"},
  db: "crm",
  receive_timeout: 15_000,
  retry: [max: 2, backoff_ms: 100]
)
```

## Return Contract

Every public helper returns one of two shapes:

- `{:ok, value}`
- `{:error, error_map}`

Arex uses normalized error maps so application code can branch on `error.kind` instead of parsing raw HTTP bodies.

Example:

```elixir
{:error,
 %{
   kind: :arcadedb,
   message: "Database 'crm' is not available",
   status: 500,
   arcade_code: nil,
   details: nil,
   body: %{},
   request: %{method: :post, path: "/api/v1/query/crm"}
 }}
```

Common `kind` values:

- `:arcadedb`
- `:database_required`
- `:type_required`
- `:scope_without_tenant`
- `:invalid_identifier`
- `:multiple_results`
- `:transaction_required`
- `:bad_opts`
- `:not_found`

## Boundary Behavior

Boundary rules are runtime behavior, not just convenience syntax.

- insert-like helpers stamp `tenant` and `scope` into stored content
- boundary-aware reads filter by `tenant` and `scope`
- `Arex.KV` applies boundary namespaces on wrapped key helpers such as `get/2`, `set/3`, and `exists?/2`
- `Arex.TimeSeries` stamps `tenant` and `scope` into boundary-aware writes and filters wrapped SQL/latest reads through those tags
- RID-based reads and writes still enforce boundary visibility
- crossing a boundary is treated as `:not_found`

That behavior is what lets Arex provide safe multi-tenant helpers without exposing cross-boundary existence through helper APIs.

Raw escape hatches stay raw:

- `Arex.KV.run/2` and `Arex.KV.batch/2` do not rewrite arbitrary Redis command strings
- hand-written TimeSeries SQL, PromQL, JSON payloads, or remote-read/write payloads are still caller-controlled unless a wrapper explicitly adds boundary tags or filters

## Transport Details

Arex builds on Req and ArcadeDB's HTTP API.

Important details:

- caller headers are merged on top of Arex defaults, except `authorization`, which is protected
- `req_options` are merged after sanitization
- low-level helpers use `/api/v1/query/:db`, `/api/v1/command/:db`, and `/api/v1/server`
- `Arex.Query.sql/3` and `Arex.Command.sql/3` always force `language: "sql"`
- `Arex.Command.sqlscript/3` forces `language: "sqlscript"`

## Observability And Logging

Arex does not emit Telemetry events or structured logs on its own.

Recommended practice:

- wrap Arex calls in your own tracing, logging, or telemetry spans
- attach `error.kind`, `error.status`, and `error.request` to logs or spans
- redact passwords, auth headers, and other secrets before logging inputs or failures
- keep any Req or Finch instrumentation at your application boundary rather than expecting Arex-specific events

## ArcadeDB-Specific Behavior

Arex documents the ArcadeDB behavior it relies on explicitly:

- pagination uses `skip` and `limit`, not raw `offset`, in generated SQL
- non-unique index creation requires explicit `notunique`
- dropping bracketed index names requires backtick quoting
- SQLScript scalar results come back as rows such as `%{"value" => 5}`

These are not theoretical notes. They are behaviors observed against the live HTTP API and encoded into helper behavior.

## Production Notes

For release and rollout checks, use [production_readiness.md](production_readiness.md) alongside this guide.