A catalog of the errors and sharp edges a host actually hits, each with its cause and fix. Deeper per-feature detail lives in usage-rules.md.

Multitenancy

tenant required for :context … (:tenant_required) — a :context-multitenant operation ran with a nil/blank tenant, so no query executed. This is fail-closed by design (never a base-database read). Fix: pass tenant: on every read/write/load/stream (Ash.read!(…, tenant: org)), or set a default tenant on the action/domain.

CDC :tenant_required halts the pipeline — a tenant-scoped mirror got a :delete/PK-changing :update whose old_record lacks the tenant_attribute column. Cause: the source table uses Postgres' default replica identity (key columns only). Fix: ALTER TABLE <table> REPLICA IDENTITY FULL upstream.

:cross_database_transaction — a transaction opened on one ArcadeDB database tried to write another (e.g. two :context tenants in one Ash.transaction). Single-database sessions are by construction; restructure so one transaction touches one tenant's database.

Sorts, filters, keyset

Ash.Error.Query.UnsortableField — sorting (or distinct_sort, or keyset paging) by a :binary (base64 — not byte-order-preserving), :decimal (lexicographic string order ≠ numeric), or composite-typed (:map, :struct, :union, {:array, _}) attribute. Fix: model money as integer minor units; sort by another field; see usage-rules "D27".

UnsupportedFilter — the filter used something ArcadeDB cannot push down: like/ilike, attribute-to-attribute comparisons, an aggregate or module calculation ref, a value comparison on a sensitive field (only is_nil is allowed), a non-stored (skip-ped) field, or a compound temporal RHS (if/arithmetic on the temporal side). The error is value-free (names operator

  • field only). Fix per usage-rules "Query & filter push-down"; for deterministic searchable encryption, model the column as a plain :binary (not sensitive).

"not filterable" on a relationship filter — filtering a source on a related field whose destination resource carries an authorizer. Ash's IN-rewrite reads the destination without per-hop authorization, so AshArcadic rejects it for every actor. Fix: filter/load the destination directly, or drop the authorizer on that destination if appropriate.

KeyError inside Ash core (scope_refs) on a relationship-path string function — an upstream Ash bug (documented); use a flat filter.

Keyset page 2 fails after a field policy — the cursor is computed from a field the actor cannot read (redacted). Sort keyset pages by a field the actor can read.

Writes & concurrency

StaleRecord on update/destroy — the tenant-scoped, filter-scoped match found zero rows: cross-tenant same-PK, already-deleted, or an upsert_condition that evaluated false (single-row contract). Expected behavior, not data loss.

HTTP 503 / ConcurrentModificationException — optimistic-lock contention on a vertex type's buckets. Autocommit statements already retry (server-side + client jittered backoff; knob config :ash_arcadic, :write_conflict_retries). Session (transaction: :batch) bulk conflicts surface at COMMIT where no statement retry is safe. Fixes: Ash.bulk_* with transaction: false (converges), pre-create hot types with more buckets (CREATE VERTEX TYPE X BUCKETS 32, host-side), and check result.status.

Duplicate rows from concurrent upserts of the same NEW identity — ArcadeDB enforces no identity uniqueness by default; two concurrent MERGEs can both create. Fix: a unique index on the identity (host-side DDL) or serialize writers.

upsert returned no row / AshArcadic requires a primary key… — an upsert returned nothing (delete raced the write), or update/destroy ran on a resource with no primary-key attribute to match. The latter is a resource definition bug.

Values & encoding

Write rejected value-free, naming an attribute — the value is not JSON-wire-encodable (typically a raw non-UTF8 binary nested inside a :map / :list value). Fix: encode app-side (Base.encode64) or use a :binary-typed attribute (top-level binaries are handled).

:decimal range filters rejected — decimals store as exact strings; range/order comparisons would be lexicographic. Fix: integer minor units for money that needs range/sort.

Case-sensitivity surprisecontains/string_starts_with/ string_ends_with map to ArcadeDB case-SENSITIVE predicates; :ci_string semantics are not preserved. Fix: normalize case app-side.

Fewer results than expected from a sparse index, silently — ArcadeDB sparse indexes do not cover rows written before the index was created. Create sparse indexes before loading data, or re-touch pre-existing rows.

max_vector_candidates exceeded (fails closed) — the tenant's candidate set for :attribute-scoped search exceeded the ceiling (default 10 000). Never truncates by design. Fix: narrow the pre-filter, raise config :ash_arcadic, :max_vector_candidates, or prefer :context (physical DB per tenant) for very large tenants.

Full-text index creation fails (SchemaException, HTTP 500) — ArcadeDB cannot build a FULL_TEXT index over a property that was auto-created implicitly by writes (a dynamic property). Declare it first: CREATE PROPERTY <Type>.<prop> STRING (host-side SQL), then Arcadic.FullText.create_index/4. Dense/sparse vector indexes tolerate implicit properties, but declare yours anyway — explicit schema-before-index is the reliable order.

Transactions

:transaction_begin_failed / :transaction_commit_failed — the ArcadeDB session could not begin (connection/availability) or the commit failed (e.g. MVCC conflict at commit). A failed commit rolls the session back automatically; retry the action.

A spawned task can't see the transaction — transaction sessions are owner-process-only by design (Ash keeps actions in-process). Do not hand transactional work to spawned tasks.

CDC sink (AshArcadic.Replicant.*)

:empty_index — the sink's domains contain no mirror resource, so the sink halts before opening a transaction (never silently advances the watermark). Register the mirror resource on a domain listed in the sink's domains:.

:sensitive_plaintext — an arriving Postgres column maps to a sensitive target attribute and is not in the replicant skip. Halted value-free. List the column in skip, or model the target as a plain :binary if it arrives already-encrypted (searchable-encryption escape hatch).

:checkpoint_read_fault at pipeline start — the watermark vertex could not be read, so the snapshot-vs-resume decision is undecidable; the pipeline halts (operator retry) rather than guessing. Check ArcadeDB availability and the checkpoint resource's client: (must target the same database as the mirrors).

:truncate_halt — upstream TRUNCATE on an on_truncate: :halt mirror. Either accept the halt (fail-closed default) or declare on_truncate :mirror.

AshArcadic.Replicant.Apply … is undefined after adding replicant later — the optional-dep compile gate needs a one-time rebuild: mix deps.get && mix deps.clean ash_arcadic --build && mix compile (see Upgrading).

Timeouts & hangs

An action hangs against a stalled ArcadeDB — arcadic commands have no timeout by default. Set timeout: <ms> (and transport pool options) in your client module's Arcadic.connect/3 — the data layer uses your conn as-is:

def conn do
  Arcadic.connect(url, db, auth: {"root", pass}, timeout: 15_000)
end