Arcadic (Arcadic v0.4.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), never string interpolation.

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).

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.

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()}.

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).

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

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

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

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_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.