Arcadic (Arcadic v0.7.0)

Copy Markdown View Source

A lean, framework-agnostic Elixir client for ArcadeDB over the HTTP Cypher command API.

Arcadic is the "postgrex of ArcadeDB": it ships Cypher/SQL, manages connections and session transactions, and normalizes responses — nothing more. It is deliberately tenant-blind and framework-agnostic. Multitenancy, classification, and Ash resources live one layer up, in ash_arcadic.

conn = Arcadic.connect("http://localhost:2480", "mydb", auth: {"root", pass})
{:ok, rows} = Arcadic.query(conn, "MATCH (n:User) RETURN n LIMIT $lim", %{"lim" => 10})
{:ok, [user]} = Arcadic.command(conn, "CREATE (u:User {name:$n}) RETURN u", %{"n" => "Jo"})

{:ok, result} = Arcadic.transaction(conn, fn tx ->
  Arcadic.command!(tx, "MERGE (u:User {id:$id})", %{"id" => "u1"})
end)

All dynamic values reach ArcadeDB only as bound parameters$name for Cypher, :name for SQL (the language: opt selects the dialect) — never string interpolation. Use explain/4/profile/4 to inspect a statement's execution plan without guessing: explain is plan-only, profile executes the statement (a write mutates).

Summary

Functions

Run a write statement (POST /api/v1/command). Returns {:ok, rows} or {:error, Arcadic.Error.t() | Arcadic.TransportError.t()}.

Run a write statement, returning the rows or raising.

Fire-and-forget write: sends awaitResponse: false; the server enqueues and returns HTTP 202. Returns :ok on enqueue — the caller CANNOT confirm the write landed (that is the defined semantic; use command/4 for confirmable writes).

Like command/4 but returns {:ok, rows, conn'} with the updated read-your-writes bookmark. See query_bookmarked/4.

Return the execution plan for a statement WITHOUT running it: prepends EXPLAIN and returns {:ok, %{plan: <human string>, plan_tree: <raw, transport-defined map>, rows: []}}.

Like explain/4 but returns the plan map or raises.

Profile a statement by EXECUTING it, returning its plan annotated with real runtime metrics: prepends PROFILE and returns {:ok, %{plan: <human string>, plan_tree: <raw, transport-defined map>, rows: <executed rows>}}.

Like profile/4 but returns the plan map or raises.

Run a read statement (POST /api/v1/query). The server rejects non-idempotent statements. Returns {:ok, rows} or {:error, Arcadic.Error.t() | Arcadic.TransportError.t()}.

Run a read statement, returning the rows or raising.

Like query/4 but returns {:ok, rows, conn'} where conn' carries the updated read-your-writes bookmark (X-ArcadeDB-Commit-Index), monotonically advanced. Thread conn' into the next call to observe your own writes. HTTP-only (:not_supported otherwise). On a single-server the index is absent → the bookmark is unchanged.

Lazily stream a large read result as raw row maps. Returns {:ok, Stream.t()} or {:error, Arcadic.Error.t()} if the statement/opts don't fit the active transport's streaming contract.

Roll back the current transaction with a reason. See Arcadic.Transaction.rollback/2.

Run a function within a session transaction. See Arcadic.Transaction.transaction/3.

Derive a same-pool handle on another database. See Arcadic.Conn.with_database/2.

Functions

command(conn, statement, params \\ %{}, opts \\ [])

@spec command(Arcadic.Conn.t(), String.t(), map(), keyword()) ::
  {:ok, [map()]} | {:error, Exception.t()}

Run a write statement (POST /api/v1/command). Returns {:ok, rows} or {:error, Arcadic.Error.t() | Arcadic.TransportError.t()}.

:auto_commit (boolean) is forwarded as-is to ArcadeDB's autoCommit body param — a faithful passthrough, not arcadic-interpreted. auto_commit: false outside an explicit transaction/3 means the write is not auto-committed (the server's semantic, not arcadic's).

command!(conn, statement, params \\ %{}, opts \\ [])

@spec command!(Arcadic.Conn.t(), String.t(), map(), keyword()) :: [map()]

Run a write statement, returning the rows or raising.

command_async(conn, statement, params \\ %{}, opts \\ [])

@spec command_async(Arcadic.Conn.t(), String.t(), map(), keyword()) ::
  :ok | {:error, Exception.t()}

Fire-and-forget write: sends awaitResponse: false; the server enqueues and returns HTTP 202. Returns :ok on enqueue — the caller CANNOT confirm the write landed (that is the defined semantic; use command/4 for confirmable writes).

command_bookmarked(conn, statement, params \\ %{}, opts \\ [])

@spec command_bookmarked(Arcadic.Conn.t(), String.t(), map(), keyword()) ::
  {:ok, [map()], Arcadic.Conn.t()} | {:error, Exception.t()}

Like command/4 but returns {:ok, rows, conn'} with the updated read-your-writes bookmark. See query_bookmarked/4.

command_bookmarked!(conn, statement, params \\ %{}, opts \\ [])

@spec command_bookmarked!(Arcadic.Conn.t(), String.t(), map(), keyword()) ::
  {[map()], Arcadic.Conn.t()}

Like command_bookmarked/4 but returns {rows, conn'} or raises.

connect(base_url, database, opts \\ [])

@spec connect(String.t(), String.t(), keyword()) :: Arcadic.Conn.t()

Build a connection handle. See Arcadic.Conn.new/3.

explain(conn, statement, params \\ %{}, opts \\ [])

@spec explain(Arcadic.Conn.t(), String.t(), map(), keyword()) ::
  {:ok, map()} | {:error, Exception.t()}

Return the execution plan for a statement WITHOUT running it: prepends EXPLAIN and returns {:ok, %{plan: <human string>, plan_tree: <raw, transport-defined map>, rows: []}}.

EXPLAIN is plan-only and side-effect-free (portable across read/write statements); its telemetry span is read-labeled (mode: :read). Takes only :language (default "cypher") and :timeout; :retries/:limit/:serializer are rejected value-free (a plan is not a paged, retried, or serialized row set). Returns {:error, %Arcadic.Error{reason: :not_supported}} when the active transport does not implement explain/3.

explain!(conn, statement, params \\ %{}, opts \\ [])

@spec explain!(Arcadic.Conn.t(), String.t(), map(), keyword()) :: map()

Like explain/4 but returns the plan map or raises.

profile(conn, statement, params \\ %{}, opts \\ [])

@spec profile(Arcadic.Conn.t(), String.t(), map(), keyword()) ::
  {:ok, map()} | {:error, Exception.t()}

Profile a statement by EXECUTING it, returning its plan annotated with real runtime metrics: prepends PROFILE and returns {:ok, %{plan: <human string>, plan_tree: <raw, transport-defined map>, rows: <executed rows>}}.

PROFILE runs the statement — a write mutates — so it routes the write path and its telemetry span carries in_transaction? (like command/4). Takes only :language (default "cypher") and :timeout; :retries is EXCLUDED (a retry would double-run the write) and :limit/:serializer are meaningless for a plan — all rejected value-free. Returns {:error, %Arcadic.Error{reason: :not_supported}} when the active transport does not implement explain/3.

profile!(conn, statement, params \\ %{}, opts \\ [])

@spec profile!(Arcadic.Conn.t(), String.t(), map(), keyword()) :: map()

Like profile/4 but returns the plan map or raises.

query(conn, statement, params \\ %{}, opts \\ [])

@spec query(Arcadic.Conn.t(), String.t(), map(), keyword()) ::
  {:ok, [map()]} | {:error, Exception.t()}

Run a read statement (POST /api/v1/query). The server rejects non-idempotent statements. Returns {:ok, rows} or {:error, Arcadic.Error.t() | Arcadic.TransportError.t()}.

query!(conn, statement, params \\ %{}, opts \\ [])

@spec query!(Arcadic.Conn.t(), String.t(), map(), keyword()) :: [map()]

Run a read statement, returning the rows or raising.

query_bookmarked(conn, statement, params \\ %{}, opts \\ [])

@spec query_bookmarked(Arcadic.Conn.t(), String.t(), map(), keyword()) ::
  {:ok, [map()], Arcadic.Conn.t()} | {:error, Exception.t()}

Like query/4 but returns {:ok, rows, conn'} where conn' carries the updated read-your-writes bookmark (X-ArcadeDB-Commit-Index), monotonically advanced. Thread conn' into the next call to observe your own writes. HTTP-only (:not_supported otherwise). On a single-server the index is absent → the bookmark is unchanged.

The conn must be built with consistency: :read_your_writes (via connect(consistency: :read_your_writes) or Arcadic.Conn.with_consistency/2) for the bookmark to be SENT: the X-ArcadeDB-Read-After request header rides only a :read_your_writes conn. On the default :eventual conn the bookmark is still captured into conn', but it is never sent, so a lagging replica can serve a stale read and the read-your-writes guarantee does NOT hold — set the level, or bookmarking is inert.

Bookmarked calls target the primary host and do NOT participate in multi-host failover (the bookmark is host-relative); point base_url at a load balancer, or use the non-bookmarked query/4/command/4 for availability failover.

query_bookmarked!(conn, statement, params \\ %{}, opts \\ [])

@spec query_bookmarked!(Arcadic.Conn.t(), String.t(), map(), keyword()) ::
  {[map()], Arcadic.Conn.t()}

Like query_bookmarked/4 but returns {rows, conn'} or raises.

query_stream(conn, statement, params \\ %{}, opts \\ [])

@spec query_stream(Arcadic.Conn.t(), String.t(), map(), keyword()) ::
  {:ok, Enumerable.t()} | {:error, Arcadic.Error.t()}

Lazily stream a large read result as raw row maps. Returns {:ok, Stream.t()} or {:error, Arcadic.Error.t()} if the statement/opts don't fit the active transport's streaming contract.

:chunk_size (rows per round-trip, default 1000) must be a positive integer, else a value-free ArgumentError.

HTTP (the default transport): pages the statement itself behind the scenes via an arcadic-owned paging suffix — an O(n) @rid keyset for WHERE-less SQL, offset otherwise (see "The streamable statement class" below). @rid/id(<identifier>) is a total ORDER, so a page is stably ordered, but each page is an independent stateless request — it is NOT a consistent snapshot, so a concurrent write can change which rows a later page sees (the offset path can skip an already-emitted row on a concurrent delete; the keyset path avoids that offset-shift but is still not a snapshot); use a Bolt in-tx cursor when you need snapshot consistency. A statement carrying its own ORDER BY/SKIP/LIMIT, a comment (--//* for SQL, // for Cypher, which would neutralize the appended suffix), or a param named __arcadic_skip/__arcadic_limit (reserved), is rejected value-free (reason: :not_supported). :timeout bounds each page POST (default :infinity — a stream is long-running, so it does NOT inherit the conn's per-call timeout; set :timeout to bound a stalled server). Refuses inside a transaction (reason: :not_supported) — HTTP has no cursor to scope to a session.

The streamable statement class (HTTP)

A streamable HTTP statement carries NO ORDER BY / SKIP / LIMIT / comment anywhere (arcadic appends its own paging suffix; a caller clause or a --//*/// comment would collide with or neutralize it, so each is rejected value-free). Roughly a bare SELECT … FROM … (SQL) or MATCH … RETURN … (Cypher).

SQL pages by an arcadic-owned @rid keyset for a WHERE-less statement — WHERE @rid > <cursor> ORDER BY @rid LIMIT — which is O(n) and free of the offset-shift skip a concurrent delete causes on the offset path (though still not a snapshot); a statement with its own WHERE falls back to ORDER BY @rid SKIP/LIMIT offset (O(n²), arcadic cannot inject a keyset predicate without parsing). Because @rid is arcadic's SQL paging column, a streamed SQL statement must not alias an output column to @rid (a rebind would silently mis-page) — rejected value-free. Cypher requires :order_key (e.g. order_key: "id(v)"), restricted to id(<identifier>) — the only total, unique order — and pages by offset with Cypher $name placeholders; documents are Cypher-unmatchable, so stream them as SQL. HTTP streaming is stateless offset/keyset, not a consistent snapshot: for O(n) snapshot-consistent in-transaction Cypher streaming use the Bolt cursor (transport: Arcadic.Transport.Bolt inside transaction/3).

Bolt: opens a dedicated connection for the stream's lifetime and pulls :chunk_size rows per round-trip (default 1000). Inside transaction/3, streams over the transaction's own connection instead (so it sees the transaction's own uncommitted writes), guarded so an execute on that conn cannot interleave an open cursor on the shared socket. The in-transaction stream is lazy and bound to the transaction's connection — you MUST consume it (e.g. Enum.to_list/1) INSIDE the transaction/3 body; enumerating the returned stream after transaction/3 has returned fails (the connection is no longer checked out). :timeout bounds each RUN and PULL receive (default :infinity; set it to bound a stalled server) — a breach raises %Arcadic.TransportError{reason: :timeout}. Any protocol error mid-stream RAISES a typed error; the connection is always torn down on completion, early halt, or error.

rollback(tx, reason)

@spec rollback(Arcadic.Conn.t(), term()) :: no_return()

Roll back the current transaction with a reason. See Arcadic.Transaction.rollback/2.

transaction(conn, fun, opts \\ [])

@spec transaction(Arcadic.Conn.t(), (Arcadic.Conn.t() -> result), keyword()) ::
  {:ok, result} | {:error, term()}
when result: var

Run a function within a session transaction. See Arcadic.Transaction.transaction/3.

with_database(conn, database)

@spec with_database(Arcadic.Conn.t(), String.t()) :: Arcadic.Conn.t()

Derive a same-pool handle on another database. See Arcadic.Conn.with_database/2.