AshArcadic.Cast (AshArcadic v0.1.0)

Copy Markdown View Source

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

binary_storage?(type, constraints)

@spec binary_storage?(
  Ash.Type.t(),
  keyword()
) :: boolean()

Whether the attribute's storage type is :binary (drives sensitive verifier + sort/range rejection).

load_value(value, spec)

@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}.

numeric_storage?(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).

orderable_storage?(storage)

@spec orderable_storage?(term()) :: boolean()

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.

range_comparable?(type, constraints)

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

row_to_attrs(row, attribute_map, attribute_types)

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

serialize_value(dt, spec)

@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}.

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