Arex is an ArcadeDB-native Elixir client for applications that want a direct, idiomatic API over ArcadeDB's HTTP interface.

It is built around a small set of promises:

  • plain Elixir maps in and out
  • one-off function calls instead of a public client struct
  • tenant and scope boundaries enforced by the high-level helpers
  • normalized {:ok, value} and {:error, error_map} return shapes
  • practical coverage for document, graph, schema, database, key/value, time-series, and vector workflows

Highlights

Documentation Map

  • Getting Started covers installation, configuration, first reads, and first writes.
  • Records and Queries explains CRUD helpers, paging, batching, upserts, and when to drop to raw SQL.
  • Graph and Schema documents provisioning, schema changes, graph helpers, and traversal patterns.
  • Runtime Behavior explains option resolution, retries, timeouts, normalized errors, and observability expectations.
  • AI Skill Guide summarizes safe usage rules for automation and agent workflows.

Installation

Add Arex to your dependencies:

defp deps do
  [
    {:arex, "~> 0.1.0"}
  ]
end

Then fetch dependencies:

mix deps.get

Configuration

Arex resolves connection settings in this order:

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

language works a little differently. It is resolved from call options or application config and otherwise defaults to "sql".

Recommended runtime.exs configuration:

import Config

config :arex,
  url: System.fetch_env!("ARCADEDB_URL"),
  user: System.fetch_env!("ARCADEDB_USER"),
  pwd: System.fetch_env!("ARCADEDB_PASSWORD"),
  db: System.fetch_env!("ARCADEDB_DATABASE"),
  language: "sql"

If you want Arex's built-in environment fallback, export these variables:

  • AREX_URL
  • AREX_USER
  • AREX_PWD
  • AREX_DB

Common call options:

  • db selects the ArcadeDB database when you do not want to use the configured default.
  • type supplies the record type for helpers such as Arex.Record.get/2.
  • tenant and scope define the application boundary for boundary-aware helpers.
  • receive_timeout sets the HTTP receive timeout in milliseconds.
  • retry enables read retries with a value such as [max: 3, backoff_ms: 200].
  • transaction and transaction_timeout tune write helpers that need transactional behavior.
  • headers merges extra request headers without allowing callers to override auth headers.
  • req_options merges sanitized Req options after Arex strips retry settings that would bypass helper policy.

Quick Start

The following example assumes you have a running ArcadeDB server and want to store tenant-scoped customer records in a crm database.

alias Arex.{Query, Record, Schema}

# Run this once when bootstrapping an empty database.
{:ok, _} = Schema.create_document_type("Customer", db: "crm")
{:ok, _} = Schema.create_property("Customer", "external_id", :string, db: "crm")
{:ok, _} = Schema.create_index("Customer", ["external_id"], db: "crm", unique: true)

{:ok, customer} =
  Record.persist(
    %{external_id: "cust-1", name: "Ada Lovelace"},
    db: "crm",
    type: "Customer",
    tenant: "ankara",
    scope: "sales"
  )

{:ok, same_customer} =
  Record.fetch(customer["@rid"], db: "crm", tenant: "ankara", scope: "sales")

{:ok, page} =
  Query.page(
    "select from Customer where tenant = :tenant and scope = :scope order by @rid",
    %{"tenant" => "ankara", "scope" => "sales"},
    db: "crm",
    limit: 25
  )

Boundary Model

Arex treats db, tenant, and scope as separate layers of isolation.

  • db chooses the ArcadeDB database.
  • tenant scopes records inside a database.
  • scope refines data inside a tenant and always requires tenant.

Boundary rules are consistent across the high-level APIs:

  • insert-like helpers stamp tenant and scope into written content when present
  • boundary-aware reads automatically filter by tenant and scope
  • Arex.KV namespaces wrapped key helpers by tenant and scope
  • Arex.TimeSeries stamps tenant and scope as tags on boundary-aware writes and filters wrapped SQL/latest reads by those tags
  • attempts to mutate protected boundary fields through helper APIs are rejected
  • cross-boundary access behaves as :not_found rather than leaking existence

Raw escape hatches such as Arex.KV.run/2, Arex.KV.batch/2, and hand-written TimeSeries SQL or PromQL remain caller-controlled.

This gives application code a stable model without repeating the same predicates in every call site.

Module Guide

ModuleUse it for
Arexconnectivity checks and server metadata
Arex.Queryraw reads, paging, and streaming
Arex.Commandraw write commands and SQLScript execution
Arex.Recorddocument-style CRUD helpers
Arex.Schematypes, properties, indexes, and buckets
Arex.Databasedatabase creation, existence checks, and summaries
Arex.KVRedis-style key/value and hash helpers
Arex.TimeSeriesTimeSeries DDL, ingestion, and query endpoints
Arex.Vectordense, sparse, and hybrid vector search helpers
Arex.Vertexvertex creation, updates, and traversal
Arex.Edgeedge creation and lookups between vertices
Arex.Errornormalized error maps returned by all helpers

Common Workflows

Records And Queries

Use Arex.Record when you want the library to handle type resolution, boundary stamping, and common CRUD patterns for you.

{:ok, customer} =
  Arex.Record.upsert(
    "Customer",
    %{name: "Ada Lovelace", status: "active"},
    db: "crm",
    where: %{external_id: "cust-1"},
    tenant: "ankara",
    scope: "sales"
  )

{:ok, exists?} =
  Arex.Record.is_there?(
    %{external_id: "cust-1"},
    db: "crm",
    type: "Customer",
    tenant: "ankara",
    scope: "sales"
  )

{:ok, first_row} =
  Arex.Query.first(
    "select from Customer where tenant = :tenant and scope = :scope order by @rid",
    %{"tenant" => "ankara", "scope" => "sales"},
    db: "crm"
  )

Use Arex.Query or Arex.Command when you need explicit control over the statement you send to ArcadeDB.

Graph Workflows

Use Arex.Vertex and Arex.Edge when your application works with graph types but you still want the same boundary semantics as document helpers.

{:ok, alice} =
  Arex.Vertex.create(
    "Person",
    %{name: "Alice"},
    db: "social",
    tenant: "ankara",
    scope: "graph"
  )

{:ok, bob} =
  Arex.Vertex.create(
    "Person",
    %{name: "Bob"},
    db: "social",
    tenant: "ankara",
    scope: "graph"
  )

{:ok, _edge} =
  Arex.Edge.create(
    "Knows",
    alice["@rid"],
    bob["@rid"],
    %{},
    db: "social",
    tenant: "ankara",
    scope: "graph"
  )

{:ok, neighbors} =
  Arex.Vertex.out(alice["@rid"], "Knows", db: "social", tenant: "ankara", scope: "graph")

Schema And Database Administration

Use Arex.Database and Arex.Schema for setup, migrations, test provisioning, and operational inspection.

{:ok, :created} = Arex.Database.create("analytics")
{:ok, _} = Arex.Schema.create_document_type("Event", db: "analytics")
{:ok, _} = Arex.Schema.create_property("Event", "kind", :string, db: "analytics")
{:ok, _} = Arex.Schema.create_index("Event", ["kind"], db: "analytics")
{:ok, stats} = Arex.Database.stats(db: "analytics")

For a deeper walkthrough, see Graph and Schema.

Key/Value Workflows

Use Arex.KV when you want Redis-style helpers over ArcadeDB's Redis-language command surface without constructing raw command strings.

{:ok, "OK"} =
  Arex.KV.set(
    "session:ada",
    "online",
    db: "crm",
    tenant: "ankara",
    scope: "sales"
  )

{:ok, "online"} =
  Arex.KV.get(
    "session:ada",
    db: "crm",
    tenant: "ankara",
    scope: "sales"
  )

Wrapped key helpers namespace keys by active tenant and scope. Raw helpers such as Arex.KV.run/2 and Arex.KV.batch/2 stay raw and do not rewrite arbitrary Redis command strings.

Time-Series Workflows

Use Arex.TimeSeries when you want TimeSeries DDL, ingestion, and endpoint helpers without hand-building /ts requests or raw create timeseries type statements.

{:ok, _} =
  Arex.TimeSeries.create_type(
    "CpuMetric",
    "ts",
    [{"host", :string}],
    [{"value", :double}],
    db: "metrics",
    tenant: "ankara",
    scope: "ops"
  )

{:ok, _} =
  Arex.TimeSeries.insert(
    "CpuMetric",
    %{"ts" => 1_715_000_001_000, "host" => "app-1", "value" => 0.42},
    db: "metrics",
    tenant: "ankara",
    scope: "ops"
  )

{:ok, rows} =
  Arex.TimeSeries.query_sql(
    "select from CpuMetric where host = :host order by ts desc",
    %{"host" => "app-1"},
    db: "metrics",
    tenant: "ankara",
    scope: "ops"
  )

Structured TimeSeries helpers stamp tenant and scope as tags when present. Raw SQL, PromQL, and raw payload endpoints remain available when you need full control over the underlying ArcadeDB surface.

Vector Workflows

Use Arex.Vector when you want a typed wrapper around ArcadeDB vector indexes and nearest-neighbor queries.

{:ok, _} = Arex.Schema.create_document_type("Doc", db: "search")
{:ok, _} = Arex.Vector.create_embedding_property("Doc", "embedding", db: "search")

{:ok, _} =
  Arex.Vector.create_dense_index(
    "Doc",
    "embedding",
    768,
    db: "search",
    similarity: :cosine
  )

{:ok, neighbors} =
  Arex.Vector.neighbors(
    "Doc[embedding]",
    [0.12, 0.34, 0.56],
    10,
    db: "search"
  )

The wrapper does not hide ArcadeDB vector concepts. It exists to make common index metadata and query construction easier to read and harder to get wrong.

Return Values And Errors

All public helpers return one of two shapes:

  • {:ok, value}
  • {:error, error_map}

Example error:

{:error,
 %{
   kind: :not_found,
   message: "record not found",
   status: nil,
   arcade_code: nil,
   details: nil,
   body: %{},
   request: %{method: :post, path: "/api/v1/query/mydb"}
 }}

Important contract notes:

  • persist_multi/2 runs inside one SQLScript transaction and is atomic.
  • fetch_multi/2 returns nil entries for missing or out-of-boundary records.
  • get_one/2 returns {:ok, nil} for no rows and {:error, %{kind: :multiple_results}} for ambiguous matches.
  • upsert/3 requires a non-empty where: clause and fails when more than one row matches.
  • Arex.Query.page/3 accepts offset: at the Elixir API boundary, but internally emits ArcadeDB skip and limit syntax.

Runtime Behavior

Arex exposes a small, explicit set of transport controls:

  • receive_timeout defaults to 60_000 milliseconds when omitted.
  • retry is disabled by default and is supported only on read helpers.
  • write helpers reject retry: instead of silently retrying writes.
  • req_options are sanitized before merge so callers cannot override helper retry policy.
  • headers can add request headers but cannot replace Arex's auth handling.

If you need the lower-level details, see Runtime Behavior.

ArcadeDB Compatibility Notes

Arex documents the ArcadeDB quirks it depends on rather than hiding them:

  • generated pagination uses skip and limit because direct offset SQL parsing failed in live ArcadeDB testing
  • non-unique index creation must emit ArcadeDB's explicit notunique keyword
  • dropping bracketed index names such as Customer[field] requires backtick quoting around the raw SQL index name
  • SQLScript scalar returns arrive through the HTTP API as rows such as %{"value" => 5} rather than bare integers

Observability

Arex does not emit its own Telemetry events or logs. Instrumentation belongs at the application boundary:

  • wrap Arex calls in your own logging, tracing, or telemetry spans
  • use error.request, error.status, and error.details when enriching logs
  • redact passwords, auth headers, and other secrets before logging inputs or failures

Local Development

The integration suite expects a live ArcadeDB server with an empty test_db database and the test_user account available. The official Docker image can provision that in one command:

docker run --rm -p 2480:2480 -p 2424:2424 \
  -e 'JAVA_OPTS=-Darcadedb.server.rootPassword=root_password -Darcadedb.server.defaultDatabases=test_db[test_user:test_password]' \
  arcadedata/arcadedb:latest

With the server running, export the values expected by local docs generation and the integration tests:

export AREX_URL=http://localhost:2480/
export AREX_USER=test_user
export AREX_PWD=test_password
export AREX_DB=test_db

Typical maintenance flow:

mix format
mix docs
mix test --cover

mix docs writes the generated site to doc/.

Additional Reading