This guide covers the core day-to-day API surface in Arex: read helpers, document-style CRUD helpers, boundary behavior, and batching.
Read Helpers
Arex.Query is the direct path when you already know the statement you want ArcadeDB to execute.
| Helper | Use it when |
|---|---|
run/3 | you want to use the resolved language from opts or config |
sql/3 | you want an explicit SQL query |
first/3 | you want the first row or nil |
one/3 | you expect zero or one row and want ambiguity to fail |
page/3 | you want one page of results plus paging metadata |
stream_pages/3 | you want to consume multiple pages lazily |
Example:
{:ok, page} =
Arex.Query.page(
"select from Customer where tenant = :tenant and scope = :scope order by @rid",
%{"tenant" => "ankara", "scope" => "sales"},
db: "crm",
limit: 100,
offset: 0
)Notes:
limitmust be a positive integer.offsetmust be a non-negative integer.- the Elixir API accepts
offset, but Arex emits ArcadeDBskipandlimitinternally because that is what the tested HTTP API accepts reliably.
Raw Write Helpers
Arex.Command is the escape hatch when a write does not fit the higher-level APIs.
| Helper | Use it when |
|---|---|
run/3 | you want the resolved language for a command |
sql/3 | you want an explicit SQL command |
sqlscript/3 | you need multiple command steps or explicit transaction blocks |
Arex normalizes raw command results to %{count: ..., records: ...}.
Example:
{:ok, %{count: count, records: rows}} =
Arex.Command.sql(
"select count(*) as count from Customer where tenant = :tenant and scope = :scope",
%{"tenant" => "ankara", "scope" => "sales"},
db: "crm"
)Document-Style CRUD Helpers
Arex.Record exists so application code does not need to rebuild the same insert, update, boundary, and cardinality logic over and over.
Insert Or Update With persist/2
persist/2 chooses its behavior from the presence of @rid:
- without
@rid, it inserts a new record - with
@rid, it updates the existing record by RID
Insert example:
{:ok, customer} =
Arex.Record.persist(
%{external_id: "cust-1", name: "Ada Lovelace"},
db: "crm",
type: "Customer",
tenant: "ankara",
scope: "sales"
)Update example:
{:ok, updated} =
Arex.Record.persist(
%{"@rid" => customer["@rid"], "name" => "Ada Byron"},
db: "crm",
tenant: "ankara",
scope: "sales"
)persist_new/2 is the clone-like variant. It removes any existing @rid and always inserts a new record.
Fetch By RID
fetch/2 returns one record and enforces boundary checks when tenant or scope are active.
{:ok, record} =
Arex.Record.fetch(customer["@rid"], db: "crm", tenant: "ankara", scope: "sales")fetch_multi/2 is useful when you want positional results back. Missing or out-of-boundary rows come back as nil entries instead of failing the whole call.
Find By Filters
get/2, get_one/2, and is_there?/2 are type-aware query helpers.
{:ok, matches} =
Arex.Record.get(
%{status: "active"},
db: "crm",
type: "Customer",
tenant: "ankara",
scope: "sales"
)
{:ok, maybe_customer} =
Arex.Record.get_one(
%{external_id: "cust-1"},
db: "crm",
type: "Customer",
tenant: "ankara",
scope: "sales"
)Important behavior:
typeis required- filters cannot be empty
get_one/2returns{:ok, nil}when nothing matchesget_one/2fails with:multiple_resultswhen the filter is ambiguous- boundary filters are appended automatically when
tenantorscopeare present
Targeted Mutation Helpers
Arex includes convenience helpers for common mutations:
update_property/4push/4pop/4switch_on/3switch_off/3merge/3replace/3
Example:
{:ok, updated} =
Arex.Record.update_property(
customer["@rid"],
:city,
"Ankara",
db: "crm",
tenant: "ankara",
scope: "sales"
)Protected fields such as tenant, scope, @rid, @type, @in, and @out are rejected when a helper is documented as protecting them.
Upserts
upsert/3 is useful when your logical identity is a filter rather than a known RID.
{:ok, customer} =
Arex.Record.upsert(
"Customer",
%{name: "Ada Lovelace", status: "active"},
db: "crm",
where: %{external_id: "cust-1"},
tenant: "ankara",
scope: "sales"
)Rules:
where:is requiredwhere:cannot be empty- attributes cannot be empty
- the operation fails if more than one row matches the
where:clause
Deletes
vaporize/2 deletes a record map that contains @rid.
vaporize_by_id/2 deletes directly by RID.
Both variants enforce the active boundary before deleting.
Batch Writes And Atomicity
persist_multi/2 stores multiple records inside one SQLScript transaction.
{:ok, [updated, inserted]} =
Arex.Record.persist_multi(
[
%{"@rid" => existing_rid, "name" => "Updated Name"},
%{"@type" => "Customer", "external_id" => "cust-2", "name" => "New Customer"}
],
db: "crm",
tenant: "ankara",
scope: "sales"
)If one operation fails, the batch fails as a unit.
Boundary Semantics
Boundaries are a major part of the record API and they apply consistently:
- inserts stamp
tenantandscopewhen present - reads only return rows that match the active boundary
- RID-based helpers still enforce tenant and scope validation after fetch
- a boundary mismatch is reported as
:not_found scopecannot be used withouttenant
This means identical logical records can exist across tenants or scopes without leaking into one another.
When To Use Query Or Command Instead
Prefer the high-level record API when you want:
- automatic boundary stamping and filtering
- normalized CRUD behavior
where:-based upserts- list and boolean convenience helpers
Drop to Arex.Query or Arex.Command when you need:
- a statement that is easier to express directly in SQL
- schema or traversal logic not covered by the higher-level helpers
- SQLScript control over multi-step writes
Related Guides
This guide stays focused on document-style CRUD and raw query/command
helpers. For boundary-aware key/value and time-series workflows, see
README.md and Getting Started, then use
Arex.KV and Arex.TimeSeries directly.