# `DrinkMeAlice.Ash.Codec`

Attribute ↔ SurrealDB value mapping for the Ash data layer, keyed on
`Ash.Type.storage_type/2`.

| storage type                               | Surreal field | written as                          |
|--------------------------------------------|---------------|-------------------------------------|
| `:string` `:citext` `:uuid` `:binary_id`   | `string`      | exact string (`DrinkMeAlice.exact_string/1`) |
| `:naive_datetime*` `:date` `:time*`        | `string`      | exact ISO-8601 string               |
| `:integer`                                 | `int`         | `$p` (i64)                          |
| `:boolean`                                 | `bool`        | `$p`                                |
| `:utc_datetime` `:utc_datetime_usec`       | `datetime`    | `<datetime>$p`                      |
| `:decimal`                                 | `decimal`     | `<decimal>$p` (decimal string)      |
| `:float`                                   | `float`       | `<float>$p` (float string)          |
| `:map`                                     | `string`      | exact canonical JSON text           |
| `{:array, scalar}`                         | `array<T>`    | element-wise as above               |

Why: SurrealDB 3.0.5 coerces typed-looking JSON strings — date-shaped
strings become datetimes and do not survive a `<string>` cast (see
`DrinkMeAlice.exact_string/1`) — so every string travels in the exact form.
The outbound encoder (`DrinkMeAlice.Json.encode/1`) carries only
canonical terms, so floats, decimals and datetimes travel as strings
cast in SurrealQL. Maps are stored as canonical JSON text rather than
as objects: an object's nested strings would be coerced, and there is
no server-side JSON parse to undo it; the cost is that map contents
are not filterable. `nil` is written as the literal `NONE`: a JSON
`null` is rejected by `option<T>` fields.

Anything else is `{:error, {:unsupported_type, name, storage}}` — at
schema generation and at write time — rather than a lossy guess.

# `kind`

```elixir
@type kind() :: :str | :plain | {:cast, binary()} | :json | {:array, kind()}
```

# `decode`

```elixir
@spec decode(map(), term()) :: {:ok, term()} | {:error, term()}
```

Decode one stored field value back into the attribute's Ash value
(`nil` for an absent field).

# `encode`

```elixir
@spec encode(map(), term()) :: {:ok, :none | {kind(), term()}} | {:error, term()}
```

Encode one attribute value for a write or a filter comparison:
`{:ok, :none}` for nil (write the literal `NONE`), or
`{:ok, {kind, wire_value}}` — pair with `param_sql/2`.

# `field_type`

```elixir
@spec field_type(map()) :: {:ok, binary()} | {:error, term()}
```

The Surreal field type for an attribute, without `option<>`:
`{:ok, "string"}` | `{:error, {:unsupported_type, name, storage}}`.

# `param_sql`

```elixir
@spec param_sql(kind(), binary()) :: binary()
```

The SurrealQL expression reading param `name` of `kind` back into the
stored value.

# `storage`

The storage type for an attribute.

# `wire_kind`

```elixir
@spec wire_kind(term()) :: {:ok, kind()} | :error
```

The wire kind for a storage type, or `:error`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
