# Getting started with Arcadic

## Setup

`Arcadic` is a lean, framework-agnostic Elixir client for
[ArcadeDB](https://arcadedb.com) over its HTTP Cypher command API, with an
optional Bolt transport for the query hot path. It ships Cypher/SQL to ArcadeDB
and manages connections, sessions, and transactions — the "`postgrex` of
ArcadeDB".

This notebook needs a running **ArcadeDB**. The quickest way is Docker:

```
docker run -d --name arcadic-quickstart \
  -p 2480:2480 -p 7687:7687 \
  -e JAVA_OPTS="-Darcadedb.server.rootPassword=playwithdata \
                -Darcadedb.server.plugins=Bolt:com.arcadedb.bolt.BoltProtocolPlugin" \
  arcadedata/arcadedb:latest
```

Two ports are published: **2480** (the HTTP command API — the default transport)
and **7687** (Bolt — used by the streaming section below). The
`-Darcadedb.server.plugins=Bolt:...` option enables ArcadeDB's Bolt protocol,
which is off by default; without it the streaming section can't connect.

> **Heads-up on port 7687.** That is also Neo4j's default Bolt port. If you run a
> local Neo4j, it likely already owns 7687 and the streaming section would end up
> talking to *it* instead of ArcadeDB, giving a confusing handshake or auth error.
> Map ArcadeDB's Bolt to a free host port instead (e.g. `-p 7690:7687`) and set
> `ARCADIC_BOLT_PORT=7690` before launching Livebook.

> **Re-running cells.** The setup cells (connect, create-database) are safe to
> re-run — the database is created only if it does not already exist, and the data
> cells use `MERGE`, so they are idempotent. To start completely fresh, run the
> **Cleanup** cell at the bottom, then run all cells top to bottom.

Every connection value can be overridden with an environment variable set before
you launch Livebook (`ARCADIC_HTTP_URL`, `ARCADIC_BOLT_HOST`, `ARCADIC_BOLT_PORT`,
`ARCADIC_PASSWORD`); otherwise the defaults match the `docker run` above.

## Install

```elixir
Mix.install([
  {:arcadic, "~> 0.7"},
  {:boltx, "~> 0.0.6"},
  {:kino, "~> 0.14"}
])
```

## Connect

`Arcadic.connect/3` builds a pure-data connection handle — no process to
supervise. We create a throwaway `arcadic_quickstart` database to play in (only if
it does not already exist, so this cell is safe to re-run).

```elixir
http_url = System.get_env("ARCADIC_HTTP_URL", "http://localhost:2480")
password = System.get_env("ARCADIC_PASSWORD", "playwithdata")
database = "arcadic_quickstart"

admin = Arcadic.connect(http_url, database, auth: {"root", password})
{:ok, true} = Arcadic.Server.ready?(admin)

unless match?({:ok, true}, Arcadic.Server.database_exists?(admin, database)) do
  :ok = Arcadic.Server.create_database(admin, database)
end

conn = Arcadic.connect(http_url, database, auth: {"root", password})
```

## Query and command (parameters only)

Every dynamic value reaches ArcadeDB **only as a bound parameter** (`$name`) —
never string interpolation. `command/4` writes; `query/4` reads. `MERGE` keeps the
write idempotent, so re-running this cell does not create duplicate people.

```elixir
{:ok, _} =
  Arcadic.command(
    conn,
    "MERGE (p:Person {name: $name}) ON CREATE SET p.role = $role RETURN p.name AS name",
    %{"name" => "Alice", "role" => "engineer"}
  )

{:ok, rows} = Arcadic.query(conn, "MATCH (p:Person) RETURN p.name AS name, p.role AS role", %{})
rows
```

## Transactions

`Arcadic.transaction/3` opens an ArcadeDB session, runs the function with a
session-scoped connection, and commits on normal return. Returning normally
commits; raising rolls back and reraises; `Arcadic.rollback/2` aborts on purpose
and yields `{:error, reason}`.

```elixir
{:ok, total} =
  Arcadic.transaction(conn, fn tx ->
    Arcadic.command!(tx, "MERGE (p:Person {name: $name})", %{"name" => "Bob"})
    [%{"c" => c}] = Arcadic.query!(tx, "MATCH (p:Person) RETURN count(p) AS c", %{})
    c
  end)

total
```

```elixir
# An intentional rollback discards everything the transaction did.
result =
  Arcadic.transaction(conn, fn tx ->
    Arcadic.command!(tx, "CREATE (p:Person {name: $name})", %{"name" => "Ghost"})
    Arcadic.rollback(tx, :changed_my_mind)
  end)

{:ok, [%{"c" => ghosts}]} =
  Arcadic.query(conn, "MATCH (p:Person {name: $name}) RETURN count(p) AS c", %{"name" => "Ghost"})

{result, ghosts}
```

## Other query languages

Cypher is the default, but ArcadeDB is multi-model. Opt into another engine per
call with `language:` — here, SQL.

```elixir
Arcadic.query!(conn, "SELECT name, role FROM Person ORDER BY name", %{}, language: "sql")
```

## Inspecting the plan (EXPLAIN)

> **Requires arcadic ≥ 0.5** (satisfied by the `~> 0.5` pin above).

`Arcadic.explain!/4` prepends `EXPLAIN` and returns the query plan **without running
the statement** — handy for checking how ArcadeDB will resolve a query (by type scan,
bucket, or index) before you run it for real. It returns a `%{plan, plan_tree, rows}`
map; `.plan` is the human-readable summary (`plan_tree` holds the raw, transport-defined
tree and `rows` is empty for EXPLAIN).

> **Careful with its sibling.** `Arcadic.profile!/4` looks similar but **executes** the
> statement to collect real runtime metrics — on a write it mutates. Don't profile a
> write unless you want the effect.

```elixir
Arcadic.explain!(conn, "SELECT FROM Person", %{}, language: "sql").plan
```

## Streaming large results over HTTP

`Arcadic.query_stream/4` streams a large read as a lazy `Stream` over the default
**HTTP** transport — no Bolt setup required (it runs over Bolt too, shown next). It
pages a WHERE-less `SELECT`/`MATCH` behind the scenes (an `@rid` keyset for SQL) so the
whole result never has to sit in memory at once, and returns `{:ok, stream}`.
`chunk_size: 5` is the page size — the rows fetched per round-trip. With only the two
people seeded above the whole result fits one page, but the cursor pages automatically
as the type grows.

```elixir
{:ok, page_stream} =
  Arcadic.query_stream(conn, "SELECT FROM Person", %{}, language: "sql", chunk_size: 5)

page_stream |> Enum.to_list() |> Kino.DataTable.new()
```

## Streaming large results over Bolt

For large result sets, `Arcadic.query_stream/4` returns a lazy `Stream` of rows
over Bolt, paged with `PULL` so the whole result never has to sit in memory at
once. `Arcadic.Transport.Bolt.setup/1` builds the transport options in one call.

```elixir
bolt_host = System.get_env("ARCADIC_BOLT_HOST", "localhost")
bolt_port = String.to_integer(System.get_env("ARCADIC_BOLT_PORT", "7687"))

{:ok, transport_options} =
  Arcadic.Transport.Bolt.setup(
    hostname: bolt_host,
    port: bolt_port,
    username: "root",
    password: password
  )

bolt_conn =
  Arcadic.connect(http_url, database,
    auth: {"root", password},
    transport: Arcadic.Transport.Bolt,
    transport_options: transport_options
  )

{:ok, stream} =
  Arcadic.query_stream(bolt_conn, "UNWIND range(1, 25) AS i RETURN i AS n, i * i AS square", %{},
    chunk_size: 5
  )

stream |> Enum.to_list() |> Kino.DataTable.new()
```

> **Bolt over TLS.** `Arcadic.Transport.Bolt.setup(scheme: "bolt+s", ...)` is secure by
> default (verifies the server certificate). This notebook does not demo it: a
> verify_peer handshake against an untrusted cert can currently wedge ArcadeDB's
> **shared** Bolt listener server-side (an upstream ArcadeDB defect, not arcadic's — see
> the [README's TLS section](../README.md#bolt-transport-optional) and
> [ArcadeData/arcadedb#5106](https://github.com/ArcadeData/arcadedb/issues/5106)).
> Don't paste a `bolt+s` connect into this notebook against a server you don't control.

## Migrations

`Arcadic.Migrator` runs `Arcadic.Migration`s in order and records applied versions
in the `_arcadic_migrations` type, so re-running is a no-op. In a real app each
migration is its own module; here we define and run one inline.

```elixir
defmodule Quickstart.Migrations.V1 do
  @behaviour Arcadic.Migration
  @impl true
  def version, do: 1
  @impl true
  def up(c), do: Arcadic.command!(c, "CREATE VERTEX TYPE Widget IF NOT EXISTS", %{}, language: "sql") && :ok
  @impl true
  def down(c), do: Arcadic.command!(c, "DROP TYPE Widget IF EXISTS", %{}, language: "sql") && :ok
end

defmodule Quickstart.Migrations do
  use Arcadic.MigrationRegistry
  migrations([Quickstart.Migrations.V1])
end

{:ok, applied} = Arcadic.Migrator.migrate(conn, Quickstart.Migrations)
{:ok, status} = Arcadic.Migrator.status(conn, Quickstart.Migrations)
{applied, status}
```

## Schema introspection

`Arcadic.Schema` reads the database's own catalog — the `Widget` type the migration
above just created shows up here too.

```elixir
Arcadic.Schema.types!(conn)
```

## Server admin

`Arcadic.Server` covers server-level operations. Database identifiers are
allowlist-validated before they reach the wire.

```elixir
{:ok, databases} = Arcadic.Server.list_databases(admin)
{:ok, exists?} = Arcadic.Server.database_exists?(admin, database)
{databases, exists?}
```

## Beyond this notebook

Two more areas `Arcadic` covers that don't fit a quickstart cell:

- **`Arcadic.Vector`** builds and queries ArcadeDB's `LSM_VECTOR`-backed indexes for
  similarity search — e.g.
  `Arcadic.Vector.create_dense_index!(conn, "Person", "embedding", 384)`, then
  `Arcadic.Vector.neighbors!/5` for a k-NN lookup.
- **`Arcadic.Import`/`Arcadic.Export`** wrap ArcadeDB's bulk `IMPORT DATABASE`/`EXPORT
  DATABASE` commands — e.g.
  `Arcadic.Export.database!(conn, "backup", with: [format: "jsonl"])` writes a bare,
  allowlisted export name server-side (never a path).

## Cleanup

Drop the throwaway database when you are done (or before a fresh top-to-bottom
run).

```elixir
Arcadic.Server.drop_database(admin, database)
```
