Arangox is a pooled Elixir driver for ArangoDB. It gives you a conventional resource API for everyday work, first-class AQL queries and transactions, and raw access to the HTTP API when you need it.
alias Arangox.API.{Collections, Documents}
{:ok, _collection} = Collections.create(db, %{name: "products"})
{:ok, created} = Documents.create(db, "products", %{name: "Keyboard"})
{:ok, product} = Documents.get(db, "products", created["_key"])Arangox 0.8 supports:
- ArangoDB 3.11 and 3.12
- Elixir 1.15 and later
- OTP 26 and later
- HTTP through Mint by default
- HTTP through Gun as an alternative
- VelocyStream for backward compatibility with ArangoDB 3.11 deployments
Getting Started
Add Arangox, Mint, and a JSON library to mix.exs:
def deps do
[
{:arangox, "~> 0.8.0"},
{:jason, "~> 1.4"},
{:mint, "~> 1.9"}
]
endMint and Jason are optional dependencies so applications can choose another transport or codec. Add them explicitly when using the default setup shown here.
For a local server on ArangoDB's default port:
{:ok, db} =
Arangox.start_link(
endpoints: "http://localhost:8529",
database: "store",
auth: {:basic, "root", ""}
)Arangox.start_link/1 starts a DBConnection pool. The defaults are
http://localhost:8529 and a pool size of 10, so a local server without
authentication needs only:
{:ok, db} = Arangox.start_link()In an application, put the pool under your supervision tree and give it a stable name:
children = [
{Arangox,
name: MyApp.Arango,
endpoints: System.fetch_env!("ARANGODB_URL"),
database: System.fetch_env!("ARANGODB_DATABASE"),
auth:
{:basic, System.fetch_env!("ARANGODB_USER"),
System.fetch_env!("ARANGODB_PASSWORD")}}
]
Supervisor.start_link(children, strategy: :one_for_one)Calls can now use MyApp.Arango anywhere they would use the db value from
start_link/1.
Resrouce API
The modules under Arangox.API cover ArangoDB's HTTP surface: 230 operations
across 22 resource modules. The pool is always the first argument, followed by
path parameters, a request body when the operation needs one, and options.
Here is a complete document lifecycle:
alias Arangox.API.{Collections, Documents}
{:ok, _collection} =
Collections.create(MyApp.Arango, %{
name: "products",
type: 2
})
{:ok, created} =
Documents.create(
MyApp.Arango,
"products",
%{name: "Mechanical keyboard", price_cents: 12_900, in_stock: true},
return_new: true
)
key = created["_key"]
{:ok, product} = Documents.get(MyApp.Arango, "products", key)
{:ok, updated} =
Documents.update(
MyApp.Arango,
"products",
key,
%{in_stock: false},
return_new: true
)
{:ok, _deleted} = Documents.delete(MyApp.Arango, "products", key)Operation options use Elixir names. For example, return_new: true is sent as
ArangoDB's returnNew=true. Every operation also accepts:
:databaseto override the pool's database for that call:headersas a list of{name, value}tuples:transactionwith an%Arangox.Transaction{}handle:timeoutfor the caller's complete time budget:request_timeoutfor the maximum time spent waiting on the socket
Resource operations return the decoded response body:
case Documents.get(MyApp.Arango, "products", key) do
{:ok, product} ->
{:found, product}
{:error, %Arangox.Error{reason: :arango_document_not_found}} ->
:not_found
{:error, %Arangox.Error{} = error} ->
{:failed, error}
endEvery operation has a bang form when raising is the clearer control flow:
product = Documents.get!(MyApp.Arango, "products", key)Useful starting points include:
Arangox.API.CollectionsandArangox.API.DocumentsArangox.API.Graphs,Arangox.API.Views, andArangox.API.IndexesArangox.API.QueriesandArangox.API.TransactionsArangox.API.Databases,Arangox.API.Users, andArangox.API.Administration
See the HexDocs API reference for the complete surface.
AQL Queries
Use Arangox.query/4 when AQL is the natural interface. Bind variables stay
separate from the statement, and query options use snake_case names:
{:ok, %Arangox.Response{body: %{"result" => products}}} =
Arangox.query(
MyApp.Arango,
"""
FOR product IN products
FILTER product.price_cents <= @maximum
SORT product.price_cents ASC
RETURN product
""",
%{maximum: 15_000},
batch_size: 100
)ArangoDB has no prepared statements. On ArangoDB 3.12.4 and later, opt into its server-side AQL plan cache when repeatedly executing eligible query text:
Arangox.query(
MyApp.Arango,
"FOR product IN products FILTER product._key == @key RETURN product",
%{key: key},
use_plan_cache: true
)Arangox.plan_cache/1 lists the current database's cached plans and
Arangox.clear_plan_cache/1 clears all of them. Both operations affect the
whole database, not only this pool.
For large results, stream cursor batches inside Arangox.run/3 or
Arangox.transaction/3:
{:ok, names} =
Arangox.transaction(MyApp.Arango, fn conn ->
conn
|> Arangox.cursor(
"FOR product IN products SORT product.name RETURN product.name",
%{},
batch_size: 100
)
|> Stream.flat_map(fn response -> response.body["result"] end)
|> Enum.to_list()
end, read: "products")Transactions
For work that fits naturally in one function, use the block form. Returning
commits the transaction; Arangox.abort/2, an exception, or an exit rolls it
back.
alias Arangox.API.Documents
Arangox.transaction(MyApp.Arango, fn conn ->
case Documents.update(conn, "accounts", from_key, %{active: false}) do
{:ok, source} -> source
{:error, error} -> Arangox.abort(conn, error)
end
end, write: "accounts"
)For a transaction that must span separate calls, use a handle:
{:ok, transaction} =
Arangox.begin_transaction(MyApp.Arango, write: "products")
{:ok, _product} =
Arangox.API.Documents.update(
MyApp.Arango,
"products",
key,
%{in_stock: true},
transaction: transaction
)
{:ok, %Arangox.Response{}} =
Arangox.commit_transaction(MyApp.Arango, transaction)
# or
{:ok, %Arangox.Response{}} =
Arangox.abort_transaction(MyApp.Arango, transaction)HTTP Primitives
Arangox exposes low-level HTTP primitives for unsupported or custom endpoints, Foxx services, etc:
{:ok, %Arangox.Response{status: 200, body: version}} =
Arangox.get(MyApp.Arango, "/_api/version")
{:ok, %Arangox.Response{}} =
Arangox.post(
MyApp.Arango,
"/_db/mydb/myservice/reindex",
%{full: true}
)Configuration
The options most applications need are:
| Option | Purpose | Default |
|---|---|---|
:endpoints | One endpoint or an ordered failover list | "http://localhost:8529" |
:database | Database prepended to database-scoped requests | Server default |
:auth | {:basic, user, password} or {:bearer, token} | No authentication |
:pool_size | Number of concurrently usable connections | 10 |
:headers | Headers sent with every request | [] |
:connect_timeout | Budget for each connection attempt | 5_000 ms |
:request_timeout | Per-request socket-wait ceiling | 15_000 ms |
:max_body_size | Largest response body accepted for decoding | 128 MiB |
:content_type | HTTP request-body codec | :json |
:client | Transport implementation | Arangox.MintClient |
The pool also accepts standard DBConnection start options.
Socket and transport options
Three options carry settings through to the socket and to the transport library. Which one you want depends on who reads it:
| Option | Goes to | Read when |
|---|---|---|
:tcp_opts | Erlang's :gen_tcp | The endpoint is cleartext — http://, tcp://, or a Unix socket |
:ssl_opts | Erlang's :ssl | The endpoint is encrypted — https://, ssl://, or tls:// |
:client_opts | The transport library itself | Always |
An option is read only when an endpoint's scheme matches, so :ssl_opts
behind an http:// endpoint never reaches a socket. Arangox warns at pool
start when you pass an option no configured endpoint can read. An endpoint
list may mix schemes, in which case both socket options are live and each
applies to the endpoints it matches.
:ssl_opts also accepts plain socket options. Erlang's :ssl forwards what
it does not recognise to :gen_tcp, so one list can carry both:
Arangox.start_link(
endpoints: "https://db.example.com:8529",
ssl_opts: [cacertfile: "/etc/my_app/ca.pem", nodelay: true]
):client_opts takes the shape the library itself takes — a keyword list for
Mint, a map for Gun. Each client's documentation says what it accepts and how
it combines with the two socket options. Both HTTP clients merge it per key,
so setting one transport option does not discard the rest.
Deadlines and retries
:timeout is the caller's total budget, beginning before it queues for a pool
connection. :request_timeout is a ceiling on the socket wait. Arangox uses
whichever limit expires first.
A request that times out after being written retires its connection and is not automatically retried. Retrying a write is an application decision because the server may already have applied it.
Multiple endpoints
Pass an ordered endpoint list to let each pooled connection find an available server:
Arangox.start_link(
endpoints: [
"https://coordinator-1.example.com:8529",
"https://coordinator-2.example.com:8529"
],
auth: {:bearer, token}
)Arangox checks availability before admitting a connection. In ArangoDB 3.11
active-failover deployments, leader redirects are accepted only when the
target is already configured or explicitly admitted by :endpoint_mapper.
Choose a transport
Most applications should keep the default Mint client.
| Client | Protocols | Server support | Additional dependency |
|---|---|---|---|
Arangox.MintClient | HTTP/1.1 by default; optional HTTP/2 | ArangoDB 3.11 and 3.12 | {:mint, "~> 1.9"} |
Arangox.GunClient | HTTP/1.1 and HTTP/2 | ArangoDB 3.11 and 3.12 | {:gun, "~> 2.0"} |
Arangox.VelocyClient | VelocyStream | ArangoDB 3.11 only | {:velocy, "~> 0.2"} |
Select another client per pool:
Arangox.start_link(client: Arangox.GunClient)Mint's HTTP/2 mode is explicit:
Arangox.start_link(client_opts: [protocols: [:http2]])The Mint client sends request bodies whole. Under HTTP/2, a body larger than
the peer's flow-control window is rejected instead of streamed; ArangoDB
typically advertises roughly 64 KiB. Keep Mint on its default HTTP/1.1 for
large documents and bulk writes, or choose Gun. Because Arangox 0.8 is built
on DBConnection, HTTP/2 does not yet multiplex concurrent requests on one
connection; concurrency still comes from :pool_size.
HTTP pools can encode bodies as VelocyPack independently of the transport:
Arangox.start_link(content_type: :velocypack)This requires the optional :velocy dependency. Responses are decoded from
their own content type, so JSON responses still work.
Security defaults
- TLS verifies certificates and hostnames using the system trust store.
- Credentials on a non-loopback cleartext endpoint are refused unless you
explicitly pass
allow_cleartext_auth: true. - Authorization and transaction header values are redacted from returned requests, inspected state, and errors.
- Header names and values containing carriage returns, line feeds, or nulls are rejected before reaching a transport.
- Response bodies larger than
:max_body_sizeare rejected before decoding.
If a private certificate authority signs your ArangoDB certificate, provide its CA file rather than disabling verification:
Arangox.start_link(
endpoints: "https://db.example.com:8529",
ssl_opts: [cacertfile: "/etc/my_app/arangodb-ca.pem"]
)Errors
Arangox reports driver, transport, and server failures with one structure:
%Arangox.Error{}.
:reasonis the stable atom to pattern-match on.:statusis the HTTP status when the server responded.:error_numis ArangoDB's numeric error code when present.:endpointis redacted.:messageis for people and logs, not branching.
case Arangox.API.Documents.get(MyApp.Arango, "products", key) do
{:ok, document} -> document
{:error, %Arangox.Error{reason: :arango_conflict}} -> retry_update()
{:error, %Arangox.Error{} = error} -> raise error
endUpgrading from 0.7
Version 0.8 modernizes the transport and public contracts. In particular,
Mint is now the default client, TLS verification is enabled, headers are
ordered {name, value} lists, and stream transactions use
%Arangox.Transaction{} handles.
Read the complete 0.8 migration guide — the "Migrating from v0.7" section — before upgrading an existing application.
Development
Unit and protocol tests run without Docker:
mix deps.get
mix format --check-formatted
mix test
The integration suite uses the services in docker-compose.yml:
docker compose up --detach --wait
mix test.integration
Documentation
License
Arangox is released under the MIT License.
Summary
Types
Remaps a server-advertised redirect endpoint to one this client can reach.
Functions
Aborts a transaction for the given reason.
Aborts the stream transaction the handle names, discarding its writes.
Aborts the stream transaction the handle names. Raises in the case of an error.
Begins a stream transaction and returns a handle to it.
Begins a stream transaction and returns a handle to it. Raises in the case of an error.
Returns a supervisor child specification for a DBConnection pool.
Discards every cached query plan in the current database.
Discards every cached query plan in the current database. Raises in the case of an error.
Commits the stream transaction the handle names.
Commits the stream transaction the handle names. Raises in the case of an error.
Creates a cursor and returns a DBConnection.Stream struct. Results are fetched
upon enumeration.
Runs a DELETE request against a connection pool.
Runs a DELETE request against a connection pool. Raises in the case of an error.
Runs a GET request against a connection pool.
Runs a GET request against a connection pool. Raises in the case of an error.
Runs a HEAD request against a connection pool.
Runs a HEAD request against a connection pool. Raises in the case of an error.
Returns the JSON library from the deprecated application config, or Jason.
Runs a OPTIONS request against a connection pool.
Runs a OPTIONS request against a connection pool. Raises in the case of an error.
Runs a PATCH request against a connection pool.
Runs a PATCH request against a connection pool. Raises in the case of an error.
Lists the current database's cached query plans.
Lists the current database's cached query plans. Raises in the case of an error.
Runs a POST request against a connection pool.
Runs a POST request against a connection pool. Raises in the case of an error.
Runs a PUT request against a connection pool.
Runs a PUT request against a connection pool. Raises in the case of an error.
Runs an AQL query and returns every row of the result.
Runs an AQL query and returns every row of the result. Raises on error.
Runs a request against a connection pool.
Runs a request against a connection pool. Raises in the case of an error.
Acquires a connection from a pool and runs a series of requests or cursors with it. If the connection disconnects, all future calls using that connection reference will fail.
Starts a connection pool.
Fetches the current status of a transaction from the database and returns its
corresponding DBconnection status.
Acquires a connection from a pool, begins a transaction in the database and runs a series of requests or cursors with it. If the connection disconnects, all future calls using that connection reference will fail.
Fetches the status of the stream transaction the handle names from the database.
Fetches the status of the stream transaction the handle names from the database. Raises in the case of an error.
Types
@type client() :: module()
@type conn() :: DBConnection.conn()
@type endpoint() :: binary()
@type endpoint_mapper() :: %{optional(endpoint()) => endpoint()} | (endpoint() -> endpoint() | nil) | {module(), atom(), [any()]}
Remaps a server-advertised redirect endpoint to one this client can reach.
A map is looked up by the advertised endpoint; a missing entry refuses the
redirect. A function (or {module, function, args}, which is applied with the
advertised endpoint prepended to args) returns the endpoint to use, and
anything other than an endpoint binary refuses the redirect.
@type method() :: :get | :head | :delete | :post | :put | :patch | :options
@type path() :: binary()
@type query() :: binary() | Arangox.Query.t()
@type start_option() :: {:client, module()} | {:endpoints, [endpoint()]} | {:endpoint_mapper, endpoint_mapper()} | {:auth, Arangox.Auth.t()} | {:database, binary()} | {:headers, headers()} | {:read_only?, boolean()} | {:connect_timeout, timeout()} | {:request_timeout, pos_integer()} | {:failover_callback, (Arangox.Error.t() -> any()) | {module(), atom(), [any()]}} | {:tcp_opts, [:gen_tcp.connect_option()]} | {:ssl_opts, [:ssl.tls_client_option()]} | {:client_opts, keyword() | map()} | {:json_library, module()} | {:content_type, :json | :velocypack} | {:max_body_size, pos_integer()} | {:vst_maxsize, pos_integer()} | DBConnection.start_option()
Functions
Aborts a transaction for the given reason.
Delegates to DBConnection.rollback/2.
Example
iex> {:ok, conn} = Arangox.start_link()
iex> Arangox.transaction(conn, fn c ->
iex> Arangox.abort(c, :reason)
iex> end)
{:error, :reason}
@spec abort_transaction(conn(), Arangox.Transaction.t(), [DBConnection.option()]) :: {:ok, Arangox.Response.t()} | {:error, any()}
Aborts the stream transaction the handle names, discarding its writes.
This is the handle form's counterpart to the closure form's abort/2, and
deliberately not the same function: abort/2 raises to roll the current
closure back through DBConnection.rollback/2, while this sends an abort
request for the named transaction and returns. It runs against any pooled
connection to the deployment; accepts only a Arangox.Transaction.t/0.
Accepts any of the options accepted by DBConnection.execute/4, plus
:database (pass the one the transaction was begun in).
@spec abort_transaction!(conn(), Arangox.Transaction.t(), [DBConnection.option()]) :: Arangox.Response.t()
Aborts the stream transaction the handle names. Raises in the case of an error.
See abort_transaction/3.
@spec begin_transaction(conn(), [transaction_option()]) :: {:ok, Arangox.Transaction.t()} | {:error, any()}
Begins a stream transaction and returns a handle to it.
This is the handle form of transactions. Where transaction/3 binds the
transaction to one checked-out connection for the span of a closure, the
Arangox.Transaction.t/0 returned here carries the transaction's identity
as a value: pass it to any request through the :transaction option and
that request joins the transaction, whichever pooled connection serves it —
then finish with commit_transaction/3 or abort_transaction/3, again on
any connection reaching the same deployment. Verified against an ArangoDB
3.12 cluster, this includes connections to different coordinators of that
deployment; see Arangox.Transaction for the measured scope.
The handle is a bearer capability — treat it like authentication material
and do not pass it across a trust boundary. See Arangox.Transaction.
Accepts the same options as transaction/3: :read, :write,
:exclusive, :properties, :database, and any of the options accepted by
DBConnection.execute/4. A transaction lives in the database it was begun
in, so pass the same :database option to the requests and to
commit/abort/status that you pass here.
Example
{:ok, trx} = Arangox.begin_transaction(conn, write: "coll")
Arangox.post!(conn, "/_api/document/coll", %{a: 1}, [], transaction: trx)
{:ok, :running} = Arangox.transaction_status(conn, trx)
{:ok, %Arangox.Response{}} = Arangox.commit_transaction(conn, trx)
@spec begin_transaction!(conn(), [transaction_option()]) :: Arangox.Transaction.t()
Begins a stream transaction and returns a handle to it. Raises in the case of an error.
See begin_transaction/2.
@spec child_spec([start_option()]) :: Supervisor.child_spec()
Returns a supervisor child specification for a DBConnection pool.
@spec clear_plan_cache(conn(), [DBConnection.option()]) :: :ok | {:error, any()}
Discards every cached query plan in the current database.
A database-wide administrative operation, and the blunt kind: the server
has no per-entry release, so this affects every caller of the database, whose
next execution of an affected statement is planned afresh. Requires write
privileges on the database; read alone is refused with a 403.
Clearing is the escape hatch for a plan that has gone stale — a plan is optimised against the indexes and statistics that existed when it was built, and nothing invalidates it when those change.
Accepts :database and any of the options accepted by
DBConnection.execute/4.
@spec clear_plan_cache!(conn(), [DBConnection.option()]) :: :ok
Discards every cached query plan in the current database. Raises in the case of an error.
See clear_plan_cache/2.
@spec commit_transaction(conn(), Arangox.Transaction.t(), [DBConnection.option()]) :: {:ok, Arangox.Response.t()} | {:error, any()}
Commits the stream transaction the handle names.
Runs against any pooled connection to the deployment the transaction was
begun on — the handle, not the connection, names the transaction. Accepts
only a Arangox.Transaction.t/0, never a bare identifier.
A failed commit leaves the handle usable: the transaction is still addressed
by the identifier, so a follow-up abort_transaction/3 reaches it.
Committing an already-committed transaction gets a 200 from the server
(idempotent); aborting or using a committed one is the server's error.
Accepts any of the options accepted by DBConnection.execute/4, plus
:database (pass the one the transaction was begun in).
@spec commit_transaction!(conn(), Arangox.Transaction.t(), [DBConnection.option()]) :: Arangox.Response.t()
Commits the stream transaction the handle names. Raises in the case of an error.
See commit_transaction/3.
@spec cursor(conn(), query(), bindvars(), [DBConnection.option()]) :: DBConnection.Stream.t()
Creates a cursor and returns a DBConnection.Stream struct. Results are fetched
upon enumeration.
The cursor is created, results fetched, then deleted from the database upon each enumeration (not to be confused with iteration). When a cursor is created, an initial result set is fetched from the database. The initial result is returned with the first iteration, subsequent iterations are fetched lazily.
Can only be used within a transaction/3 or run/3 call.
Accepts any of the options accepted by DBConnection.stream/4, as well as any of the
following:
:database- Sets what database to run the cursor query on:properties- A list or map of additional body attributes to append to the request body when creating the cursor.:transaction- AnArangox.Transaction.t/0handle frombegin_transaction/2; the cursor's requests join that stream transaction.
Delegates to DBConnection.stream/4.
Example
iex> {:ok, conn} = Arangox.start_link()
iex> Arangox.transaction(conn, fn c ->
iex> stream =
iex> Arangox.cursor(
iex> c,
iex> "FOR i IN [1, 2, 3] FILTER i == 1 || i == @num RETURN i",
iex> %{num: 2},
iex> properties: [batchSize: 1]
iex> )
iex>
iex> first_batch = Enum.at(stream, 0).body["result"]
iex>
iex> exhaust_cursor =
iex> Enum.reduce(stream, [], fn resp, acc ->
iex> acc ++ resp.body["result"]
iex> end)
iex>
iex> {first_batch, exhaust_cursor}
iex> end)
{:ok, {[1], [1, 2]}}
@spec delete(conn(), path(), headers(), [DBConnection.option()]) :: {:ok, Arangox.Response.t()} | {:error, any()}
Runs a DELETE request against a connection pool.
Accepts any of the options accepted by DBConnection.execute/4.
@spec delete!(conn(), path(), headers(), [DBConnection.option()]) :: Arangox.Response.t()
Runs a DELETE request against a connection pool. Raises in the case of an error.
Accepts any of the options accepted by DBConnection.execute!/4.
@spec get(conn(), path(), headers(), [DBConnection.option()]) :: {:ok, Arangox.Response.t()} | {:error, any()}
Runs a GET request against a connection pool.
Accepts any of the options accepted by DBConnection.execute/4.
@spec get!(conn(), path(), headers(), [DBConnection.option()]) :: Arangox.Response.t()
Runs a GET request against a connection pool. Raises in the case of an error.
Accepts any of the options accepted by DBConnection.execute!/4.
@spec head(conn(), path(), headers(), [DBConnection.option()]) :: {:ok, Arangox.Response.t()} | {:error, any()}
Runs a HEAD request against a connection pool.
Accepts any of the options accepted by DBConnection.execute/4.
@spec head!(conn(), path(), headers(), [DBConnection.option()]) :: Arangox.Response.t()
Runs a HEAD request against a connection pool. Raises in the case of an error.
Accepts any of the options accepted by DBConnection.execute!/4.
@spec json_library() :: module()
Returns the JSON library from the deprecated application config, or Jason.
Deprecated in v0.8 and to be removed in the next release, along with the
application-config read it reports on. It returns the fallback, not what any particular pool uses:
since v0.8 the JSON library is a per-pool start option, so two pools can disagree
and neither has to agree with this. Pass :json_library to start_link/1 instead.
Emits a deprecation warning on every call, because there is no correct number of times to call it.
@spec options(conn(), path(), headers(), [DBConnection.option()]) :: {:ok, Arangox.Response.t()} | {:error, any()}
Runs a OPTIONS request against a connection pool.
Accepts any of the options accepted by DBConnection.execute/4.
@spec options!(conn(), path(), headers(), [DBConnection.option()]) :: Arangox.Response.t()
Runs a OPTIONS request against a connection pool. Raises in the case of an error.
Accepts any of the options accepted by DBConnection.execute!/4.
@spec patch(conn(), path(), body(), headers(), [DBConnection.option()]) :: {:ok, Arangox.Response.t()} | {:error, any()}
Runs a PATCH request against a connection pool.
Accepts any of the options accepted by DBConnection.execute/4.
@spec patch!(conn(), path(), body(), headers(), [DBConnection.option()]) :: Arangox.Response.t()
Runs a PATCH request against a connection pool. Raises in the case of an error.
Accepts any of the options accepted by DBConnection.execute!/4.
@spec plan_cache(conn(), [DBConnection.option()]) :: {:ok, [map()]} | {:error, any()}
Lists the current database's cached query plans.
A database-wide administrative operation. The plan cache is shared by
every caller of the database, so this exposes the query text of statements
other callers ran, and clear_plan_cache/2 discards plans they are relying
on. Requires read privileges on the database.
Each entry is the server's own map, including "query", "hash" — the same
value Arangox.Response.plan_cache_key/1 returns — "hits", "memoryUsage"
and "created".
An empty list is ambiguous
The server returns only the plans whose collections the caller may read, and
it filters rather than refuses. A caller permitted on no collections is
given [], which is indistinguishable from an empty cache. A caller with
no privileges on the database is refused outright, which does surface as
an error — note that the refusal is a 401, and 401 is in the default
:disconnect_on_error_codes, so it also retires the connection. ArangoDB
uses the same status and error number for bad credentials and for
insufficient rights, so the driver has nothing to tell them apart by.
Accepts :database and any of the options accepted by
DBConnection.execute/4.
@spec plan_cache!(conn(), [DBConnection.option()]) :: [map()]
Lists the current database's cached query plans. Raises in the case of an error.
See plan_cache/2, including its warning about empty results.
@spec post(conn(), path(), body(), headers(), [DBConnection.option()]) :: {:ok, Arangox.Response.t()} | {:error, any()}
Runs a POST request against a connection pool.
Accepts any of the options accepted by DBConnection.execute/4.
@spec post!(conn(), path(), body(), headers(), [DBConnection.option()]) :: Arangox.Response.t()
Runs a POST request against a connection pool. Raises in the case of an error.
Accepts any of the options accepted by DBConnection.execute!/4.
@spec put(conn(), path(), body(), headers(), [DBConnection.option()]) :: {:ok, Arangox.Response.t()} | {:error, any()}
Runs a PUT request against a connection pool.
Accepts any of the options accepted by DBConnection.execute/4.
@spec put!(conn(), path(), body(), headers(), [DBConnection.option()]) :: Arangox.Response.t()
Runs a PUT request against a connection pool. Raises in the case of an error.
Accepts any of the options accepted by DBConnection.execute!/4.
@spec query(conn(), query(), bindvars(), [DBConnection.option()]) :: {:ok, Arangox.Response.t()} | {:error, any()}
Runs an AQL query and returns every row of the result.
ArangoDB executes AQL through a server-side cursor whichever of these two
functions you use — they send the same request. The difference is who drives
the cursor afterwards: this one drains it and hands back a single response
holding the complete result, while cursor/4 hands you the cursor to read a
batch at a time inside a run/3 or transaction/3 block.
A result that fits one batch costs exactly one request either way, because the
server keeps no cursor when it has nothing more to send. The extra requests
happen only when there is more, and then only here — a caller who stops
reading a cursor/4 stream never asks for the rest.
Takes an AQL binary or an Arangox.Query.t/0. AQL options may be given
here or carried by the query; see Arangox.Query for the names.
Does not ask the server to reuse a cached query plan unless told to. Pass
use_plan_cache: true when the same statement runs repeatedly; it is opt-in
because it has a server-version floor and the server refuses to cache some
statements outright. There is no prepare step to do it for you — see
Arangox.Query.
Example
iex> {:ok, conn} = Arangox.start_link()
iex> {:ok, response} =
iex> Arangox.query(conn, "FOR i IN [1, 2, 3] FILTER i != @skip RETURN i", %{skip: 2})
iex> response.body["result"]
[1, 3]
@spec query!(conn(), query(), bindvars(), [DBConnection.option()]) :: Arangox.Response.t()
Runs an AQL query and returns every row of the result. Raises on error.
See query/4.
@spec request(conn(), method(), path(), body(), headers(), [DBConnection.option()]) :: {:ok, Arangox.Request.t(), Arangox.Response.t()} | {:error, any()}
Runs a request against a connection pool.
Accepts any of the options accepted by DBConnection.execute/4, as well as:
:transaction- AnArangox.Transaction.t/0handle frombegin_transaction/2. The request joins that stream transaction: its identifier is sent in the x-arango-trx-id header for this request only — connection state is never written, so the handle applies on whichever pooled connection serves the request. Accepts only the handle struct, never a bare identifier binary. Every request-running function in this module accepts this option.
headers is a list of {name, value} string tuples, appended after the
pool's :headers and sent as given — order kept, duplicates kept, nothing
renamed or re-cased. See the README's "Headers" section for the exact wire
behavior per client. Maps are no longer accepted (0.8).
The request struct echoed back on success carries [redacted] in place of
the authorization and x-arango-trx-id header values; what went on the
wire is the real thing.
@spec request!(conn(), method(), path(), body(), headers(), [DBConnection.option()]) :: Arangox.Response.t()
Runs a request against a connection pool. Raises in the case of an error.
Accepts any of the options accepted by DBConnection.execute!/4.
@spec run(conn(), (DBConnection.t() -> result), [DBConnection.option()]) :: result when result: var
Acquires a connection from a pool and runs a series of requests or cursors with it. If the connection disconnects, all future calls using that connection reference will fail.
Runs can be nested multiple times if the connection reference is used to start a nested run (i.e. calling another function that calls this one). The top level run function will represent the actual run.
Delegates to DBConnection.run/3.
Example
result =
Arangox.run(conn, fn c ->
Arangox.request!(c, ...)
end)
@spec start_link([start_option()]) :: GenServer.on_start()
Starts a connection pool.
Options
Accepts any of the options accepted by DBConnection.start_link/2, as well as any of the
following:
:endpoints- Either a single ArangoDB endpoint binary, or a list of endpoints in order of presedence. Each process in a pool will individually attempt to establish a connection with and check the availablility of each endpoint in the order given until an available endpoint is found. Defaults to"http://localhost:8529".:database- Arangox will prepend/_db/:valueto the path of every request that isn't already prepended. If a value is not given, nothing is prepended (ArangoDB will assume the _system database).:headers- A list of{name, value}header tuples sent with every request, in the order given, before the request's own headers. Nothing is merged, deduplicated, renamed or re-cased; see the "Headers" section of the README for the exact wire behavior per client.:disconnect_on_error_codes- A list of status codes that will trigger a forced disconnect. Only integers within the range400..599are affected. Defaults to[401, 405, 503, 505].:allow_cleartext_auth- Permit:authcredentials to be sent to a cleartext (http://) endpoint that is not on this machine. Defaults tofalse, and a pool configured that way refuses to start: the credentials would be readable by anything on the network path. Loopback endpoints are exempt and need no opt-in.:auth- Configure whether to resolve authorization. Options are:{:basic, username, password},{:bearer, token}.:endpoint_mapper- Remaps the endpoint a server advertises in the x-arango-endpoint header of a503to an address this client can reach. Either a map ofadvertised endpoint => target endpoint, a one-argument function returning the target endpoint, or an{module, function, args}tuple (the advertised endpoint is prepended toargs). A map with no entry for the advertised endpoint, or a function returning anything that is not an endpoint binary, refuses the redirect. Defaults to no mapper. This is a trust boundary: see Leader redirects below before setting it.:read_only?- Read-only pools will only connect to followers in an active failover setup and add an x-arango-allow-dirty-read header to every request. Defaults tofalse. Read-only pools never follow a leader redirect — not landing on the leader is what the option is for.:connect_timeout- Sets the timeout for establishing connections with a database. It also bounds the probes the connect pipeline makes on the open socket (the authentication, availability and version stages) — all of them together, not one budget each — so a server that completes the handshake and then goes quiet cannot wedge a connection attempt, and adding a stage cannot extend the worst case. The budget is per endpoint attempt: each endpoint the failover walk reaches gets its own.:request_timeout- The longest arangox will wait on the socket for one request's response, in milliseconds. Must be a positive integer;0, negatives and:infinityare rejected here and per request. Defaults to15_000, the same asDBConnection's:timeout. It is a ceiling, not the whole story — see Request timeouts below. Can be overridden per request.:tcp_opts- Transport options for the tcp socket interface (:gen_tcpin the case of mint).:ssl_opts- Transport options for the ssl socket interface (:sslin the case of mint).:client- A module that implements theArangox.Clientbehaviour. Defaults toArangox.MintClient.Arangox.VelocyClientspeaks VelocyStream, which ArangoDB removed in server 3.12 — it remains supported as an explicit opt-in for 3.11 deployments.:client_opts- Options passed to the client library itself, for settings that belong to the library rather than to the socket. Its shape is the library's own: a keyword list for mint, a map for gun. See each client's moduledoc for what it accepts and how it combines with:tcp_optsand:ssl_opts.:failover_callback- A function to call every time arangox fails to establish a connection. This is only called if a list of endpoints is given, regardless of whether or not it's connecting to an endpoint in an active failover setup. Can be either an anonymous function that takes one argument (which is an%Arangox.Error{}struct), or a three-element tuple containing arguments to pass toapply/3(in which case an%Arangox.Error{}struct is always prepended to the arguments).:json_library- The module used to encode and decode JSON request and response bodies. Only the HTTP clients use it;Arangox.VelocyClientspeaks VelocyPack. Must exportencode!/1,decode/1anddecode!/1. Defaults toJason.:vst_maxsize- The maximum size, in bytes, of a single VelocyStream chunk written byArangox.VelocyClient. Must be an integer greater than 24, the size of a chunk header. Defaults to30_720.:content_type-:json(the default) or:velocypack, selecting how an HTTP client encodes request bodies.:velocypackrequires the:velocylibrary, sendscontent-type: application/x-velocypack, and asks for the same in return with anacceptheader. Responses are decoded by their own content type, so a server that declines VelocyPack and responds in JSON is still read correctly. Acontent-typeoracceptheader set among a request's own headers wins over the pool's setting — the codec is chosen from the firstcontent-typeentry there, any casing, and never from the pool's:headerslist — and one naming neither codec —text/plainfor/_api/import,application/octet-stream— sends the request body raw: it must already be a binary in its wire form. Ignored byArangox.VelocyClient, which carries its own body encoding.:max_body_size- The largest response body, in bytes, handed to a codec. A larger body is refused with a structured error (reason: :body_too_large) before any decoding runs, bounding what a hostile or broken server can make the pool parse. Defaults to134_217_728(128 MiB). Ignored byArangox.VelocyClient.
Leader redirects
In an active failover setup a follower responds 503 and names the current leader in an
x-arango-endpoint response header. Arangox follows that header during connect, but only
when the target passes an admission policy. The pool holds your credentials and would send
them to whatever host the server named, so:
- The advertised value must parse as an endpoint. One that does not is refused, not raised.
- Its normalized origin — encryption class, host and port — must already appear in
:endpoints, or:endpoint_mappermust admit it. Normalized meanstcp://compares equal tohttp://andssl:///tls://tohttps://, because the server advertises in its own scheme vocabulary rather than yours. The comparison uses the full origin and not just the host: a configuredhttp://db.internal:8529does not authorize a redirect tohttp://db.internal:9999, which is a different service on the same machine. - An encrypted connection is never redirected to a cleartext one, even to an endpoint that is configured.
- At most three redirects are followed per connect attempt, counted across the whole endpoint walk, so a pair of servers redirecting to each other terminates.
A refused redirect is treated as what it is — an endpoint that responded and is unusable. The
walk moves on to the next endpoint, :failover_callback fires, and the error says which
redirect was refused and why. A redirect that is followed is normal operation rather than a
failure and does not fire :failover_callback.
:endpoint_mapper is a trust boundary
:endpoint_mapper is the only way to follow a redirect to an address that is not in
:endpoints. Its output is validated the same way — it must parse, and it may not downgrade
encryption — but it is not re-checked against :endpoints, because remapping to an
address you never configured is the whole point. A mapper that returns its input unchanged
therefore switches the policy off entirely and lets any server that can satisfy a connect
probe send your credentials anywhere. Map the advertised endpoints you recognise and refuse
everything else; the map form does exactly that.
It exists for the containerized case. Running docker-compose.yml's resilient_single
service, the followers on host ports 8003 and 8005 advertise the leader as
http://localhost:8539 — 8539 is the leader's port inside the container, and from the host
the leader is on 8004. No host-side :endpoints list can contain localhost:8539, so the
default policy refuses that redirect even though it is legitimate. The mapper is how you say
where the leader really is:
Arangox.start_link(
endpoints: ["http://localhost:8003"],
endpoint_mapper: %{"http://localhost:8539" => "http://localhost:8004"}
)Map keys are matched on their normalized origin too, so the key above also matches an
advertised tcp://localhost:8539.
A topology whose members sit on their real published addresses — the usual bare-metal
active-failover deployment, where :endpoints lists the same addresses the servers
advertise — needs no mapper at all.
Request timeouts
Every request is bounded. Two numbers decide by how much, and they are not the same kind of number:
- your
:timeout(DBConnection's, per call, 15 seconds by default) is a budget that starts counting the moment you ask for a connection — including the time you spend queueing for one; :request_timeoutis a ceiling on how long any single request may sit on the socket.
Arangox waits for min(what is left of your budget, :request_timeout), less a small
margin. So a caller that waited 14 seconds for a free connection gets the last second,
not a fresh 15 — and Arangox.get(conn, path, [], timeout: 500) really does come back
within 500ms, with an Arangox.Error whose :reason is :timeout, rather than with
DBConnection's "queued and checked out for longer than" error some time later.
Raise the ceiling for one slow request without loosening the pool:
Arangox.post(conn, "/_api/cursor", body, [], request_timeout: 60_000, timeout: 65_000)Both numbers matter there: :request_timeout alone cannot buy more time than the
caller's budget allows.
If the budget is already gone by the time a connection frees up, the request is not
sent at all and the error's :reason is :deadline_exceeded. That connection is
healthy and stays in the pool.
timeout: :infinity waives the budget, not the ceiling: such a request still gets
:request_timeout on the socket. There is no way to configure an unbounded wait.
A request that times out on the socket takes its connection down with it. Arangox cannot tell where an abandoned response ended, so reusing the connection would serve the leftover bytes to whoever draws it next. The pool reconnects; the request is not retried, because it may well have been executed.
Inside run/3 and transaction/3 the block's own :timeout bounds every request made
on the block's connection, since that is the deadline DBConnection armed when the
block checked it out. Such a request never gets a budget longer than what is left of the
block's — including a cursor's per-batch fetches.
A request made from inside a block to a different pool is a different checkout with a deadline of its own, and keeps the budget it asked for:
Arangox.transaction(primary, fn conn ->
Arangox.get(conn, path) # bounded by the block
Arangox.get(read_replica, other_path) # bounded by its own :timeout
end)Deprecated application config
:json_library and :vst_maxsize used to be global application config:
config :arangox, :json_library, Poison
config :arangox, :vst_maxsize, 12_345Both reads still work in v0.8 and will be removed in the next release. A pool that
does not pass the start option falls back to the application config, and the fallback
logs a deprecation warning. A value read from the fallback is validated exactly like the start option and
raises here on the same inputs — :vst_maxsize used to be checked by
Application.compile_env/3 at compile time, and losing that check would only move the
failure to a MatchError in the middle of a request.
That warning is emitted once per pool: precisely, once per start_link/1 or
child_spec/1 call, in the calling process, and nowhere else. It is deliberately not
emitted from the DBConnection connect callback, which is not a once-per-pool function —
DBConnection re-enters it in the same process on every backoff cycle for as long as
the pool lives, in each of the pool's :pool_size processes. Warning there would log
several times a second, forever, against an unreachable server. Starting a pool through
DBConnection.start_link/2 directly bypasses this module and therefore warns not at
all, while still honouring the fallback.
@spec status(conn()) :: DBConnection.status()
Fetches the current status of a transaction from the database and returns its
corresponding DBconnection status.
Delegates to DBConnection.status/1.
@spec transaction(conn(), (DBConnection.t() -> result), [transaction_option()]) :: {:ok, result} | {:error, any()} when result: var
Acquires a connection from a pool, begins a transaction in the database and runs a series of requests or cursors with it. If the connection disconnects, all future calls using that connection reference will fail.
Transactions can be nested multiple times if the connection reference is used to start a
nested transactions (i.e. calling another function that calls this one). The top level
transaction function will represent the actual transaction and nested transactions will
be interpreted as a run/3, erego, any collections declared in nested transactions will
have no effect.
Accepts any of the options accepted by DBConnection.transaction/3, as well as any of the
following:
:read- An array of collection names or a single collection name as a binary.:write- An array of collection names or a single collection name as a binary.:exclusive- An array of collection names or a single collection name as a binary.:database- Sets what database to run the transaction on:properties- A list or map of additional body attributes to append to the request body when beginning a transaction.
Delegates to DBConnection.transaction/3.
Example
Arangox.transaction(conn, fn c ->
Arangox.status(c) #=> :transaction
# do stuff
end, [
write: "something",
properties: [waitForSync: true]
])
@spec transaction_status(conn(), Arangox.Transaction.t(), [DBConnection.option()]) :: {:ok, :running | :committed | :aborted | binary()} | {:error, any()}
Fetches the status of the stream transaction the handle names from the database.
Answers from the body of the server's reply, not from the HTTP status: the
server keeps responding 200 for a finished transaction within a retention
window, with the actual state in the body. Returns {:ok, :running},
{:ok, :committed} or {:ok, :aborted}; a status this driver does not
recognize is returned as its raw binary rather than turned into an atom.
This is the handle form's counterpart to status/1, which reports on the
closure transaction bound to a connection. It runs against any pooled
connection to the deployment; accepts only a Arangox.Transaction.t/0.
Accepts any of the options accepted by DBConnection.execute/4, plus
:database (pass the one the transaction was begun in).
@spec transaction_status!(conn(), Arangox.Transaction.t(), [DBConnection.option()]) :: :running | :committed | :aborted | binary()
Fetches the status of the stream transaction the handle names from the database. Raises in the case of an error.
See transaction_status/3.