Value serialization and flat-JSON-row decode for the ArcadeDB wire.
ArcadeDB rows are flat JSON maps (%{"@rid" => .., "@cat" => "v", <props>}), not
AGE agtype text — so there is NO tag: values are typed-decoded from the resource's
attribute map. serialize_value/2 and load_value/2 dispatch by the attribute's
STORAGE class (Ash.Type.storage_type/2) so a NewType wrapper round-trips like
the builtin it stores as. Binary-storage values (app-side-encrypted bytes, D3) are
Base.encode64/decode64 — plaintext in JSON is impossible, and equality matches
because both the stored form and a filter param serialize identically.
Summary
Functions
Whether the attribute's storage type is :binary (drives sensitive verifier + sort/range rejection).
Decodes a JSON scalar back to its Ash attribute value, typed by {type, constraints}.
Whether an attribute's storage type is numerically summable/averageable
(:integer/:float). False for :decimal (stored as an exact string, so ArcadeDB
sum/avg would concatenate/error, D27) and every non-numeric class. Drives the
aggregate sum/avg guard (a sum over a non-numeric attr fails LOUD, never silently
wrong or leaking).
Whether a STORAGE type has a total order ArcadeDB compares correctly — the allowlist above; any
storage NOT on it (binary/decimal/map/array/vector/duration/custom) fails CLOSED. Shared by the
can?({:sort, storage}) gate and range_comparable?/2 so sort and range stay symmetric.
Whether an attribute's storage type has a TOTAL ORDER ArcadeDB compares correctly for a range/keyset
op (gt/lt/gte/lte) — orderable_storage?/1 over the attribute's resolved storage type. An
ALLOWLIST (fail-closed): a range/keyset op on a non-orderable attr fails LOUD
(UnsupportedFilter/UnsortableField), never silently wrong/truncated rows.
Builds resource attributes from a flat ArcadeDB row map. Routes STRICTLY by
attribute_map (declared attr → property name): reads only declared-attribute
keys, load_value-coerces each by its type, and ignores every @-prefixed
identity key (@rid/@cat/@type/@in/@out, kept by Arcadic.Result) and any
undeclared property. No resource attribute can collide with an @-key.
Serializes an attribute value to a JSON-safe param, typed by {type, constraints}.
The Cypher temporal constructor a bound comparison param must be wrapped in for a temporal
attribute, or nil. ArcadeDB auto-coerces stored ISO8601 datetime/time strings to its native
temporal types on write, so a prop OP $stringparam comparison silently matched NOTHING (the
string param never equals a coerced temporal value — probe-verified). Wrapping the param —
datetime($p) for datetime storage, localtime($p) for time storage — makes ArcadeDB compare
temporal-to-temporal (probe-verified for fractional-second/usec values too). The _usec storage
classes are covered SYMMETRICALLY with the decode side (classify/1) — omitting them reintroduces
the silent-[] mis-page for a :datetime/:time attr declared precision: :microsecond (storage
:utc_datetime_usec/:time_usec). :date is the exception: ArcadeDB does NOT coerce date-only
strings, so it stays a string and compares correctly UNWRAPPED (nil).
Functions
@spec binary_storage?( Ash.Type.t(), keyword() ) :: boolean()
Whether the attribute's storage type is :binary (drives sensitive verifier + sort/range rejection).
@spec load_value(term(), {Ash.Type.t(), keyword()} | Ash.Type.t() | nil) :: term()
Decodes a JSON scalar back to its Ash attribute value, typed by {type, constraints}.
@spec numeric_storage?( Ash.Type.t(), keyword() ) :: boolean()
Whether an attribute's storage type is numerically summable/averageable
(:integer/:float). False for :decimal (stored as an exact string, so ArcadeDB
sum/avg would concatenate/error, D27) and every non-numeric class. Drives the
aggregate sum/avg guard (a sum over a non-numeric attr fails LOUD, never silently
wrong or leaking).
Whether a STORAGE type has a total order ArcadeDB compares correctly — the allowlist above; any
storage NOT on it (binary/decimal/map/array/vector/duration/custom) fails CLOSED. Shared by the
can?({:sort, storage}) gate and range_comparable?/2 so sort and range stay symmetric.
@spec range_comparable?( Ash.Type.t(), keyword() ) :: boolean()
Whether an attribute's storage type has a TOTAL ORDER ArcadeDB compares correctly for a range/keyset
op (gt/lt/gte/lte) — orderable_storage?/1 over the attribute's resolved storage type. An
ALLOWLIST (fail-closed): a range/keyset op on a non-orderable attr fails LOUD
(UnsupportedFilter/UnsortableField), never silently wrong/truncated rows.
@spec row_to_attrs(map(), %{required(atom()) => String.t()}, %{ required(atom()) => {Ash.Type.t(), keyword()} }) :: map()
Builds resource attributes from a flat ArcadeDB row map. Routes STRICTLY by
attribute_map (declared attr → property name): reads only declared-attribute
keys, load_value-coerces each by its type, and ignores every @-prefixed
identity key (@rid/@cat/@type/@in/@out, kept by Arcadic.Result) and any
undeclared property. No resource attribute can collide with an @-key.
@spec serialize_value(term(), {Ash.Type.t(), keyword()} | Ash.Type.t() | nil) :: term()
Serializes an attribute value to a JSON-safe param, typed by {type, constraints}.
@spec temporal_cypher_fn( Ash.Type.t(), keyword() ) :: String.t() | nil
The Cypher temporal constructor a bound comparison param must be wrapped in for a temporal
attribute, or nil. ArcadeDB auto-coerces stored ISO8601 datetime/time strings to its native
temporal types on write, so a prop OP $stringparam comparison silently matched NOTHING (the
string param never equals a coerced temporal value — probe-verified). Wrapping the param —
datetime($p) for datetime storage, localtime($p) for time storage — makes ArcadeDB compare
temporal-to-temporal (probe-verified for fractional-second/usec values too). The _usec storage
classes are covered SYMMETRICALLY with the decode side (classify/1) — omitting them reintroduces
the silent-[] mis-page for a :datetime/:time attr declared precision: :microsecond (storage
:utc_datetime_usec/:time_usec). :date is the exception: ArcadeDB does NOT coerce date-only
strings, so it stays a string and compares correctly UNWRAPPED (nil).