# `Tempo.SQL.Meta`
[🔗](https://github.com/elixir-tempo/tempo_sql/blob/v0.4.0/lib/tempo/sql/meta.ex#L1)

JSON encoding and decoding for the `meta` column of the
`tempo_range` and `tempo_multirange` composite types.

The range column of a composite carries the queryable span.
The `meta` column carries everything else Tempo knows that
`tstzrange` cannot express — qualifications, non-Gregorian
calendars, zone identifiers, recurrence rules, metadata, the
resolution a value is written to, and whether it was written as a
value or as an interval.

On store, the meta is a JSON document with this shape:

    %{
      "v"          => 3,
      "expression" => iso8601_string,
      "metadata"   => map,             # must be JSON-serialisable
      "members"    => [map] | null     # each member's, for a set
    }

The expression is its whole ISO 8601 text, which Tempo reads back
as it was: every feature ISO 8601, ISO 8601-2 and IXDTF can
express. An expression with no text (a conditional member, a rule
read in another calendar, a set read as it is walked) is not
stored.

## Format versions

Versions 1 and 2 were written by `tempo_sql` 0.2.0 and earlier,
from Tempo 1.x, and held an interval end by end: `"from"`, `"to"`,
`"recurrence"`, `"direction"`, `"duration"`, `"repeat_rule"` and
`"metadata"`, and a set as `"intervals"`, a list of them. Tempo 1.x
read an interval's `to` as the end it stops before; Tempo 2.0 reads
an end written to a date through it. So a version 1 or 2 row is read
as it was written to mean: an end written to a calendar unit is the
unit before it (`"to": "2022Y1M5D"` is through the 4th), and a
recurrence is built from its start, its cadence and its count.
No row needs migrating, and each is written as version 3 when it is
next stored.

Version 2 writes `".."` for an explicitly open-ended endpoint and
`null` for an absent one; version 1 wrote `null` for both and is
read as open-ended.

Encoding uses Erlang's built-in `:json` module (OTP 27+);
the `jsonb` column type handles storage and indexing on the
Postgres side.

# `decode_interval`

```elixir
@spec decode_interval(String.t() | map()) :: {:ok, Tempo.t()} | {:error, term()}
```

Decodes a JSON string, or an already-decoded map, back into an
expression, whichever version wrote it.

### Arguments

* `json` is the encoded document, either as a binary or as the map
  Postgrex hands back from a `jsonb` column.

### Returns

* `{:ok, expression}` where `expression` is a `t:Tempo.t/0`.

* `{:error, reason}` when the document is none `tempo_sql` wrote
  (`{:invalid_meta_shape, document}`), or holds a field that does not
  decode.

### Examples

    iex> {:ok, json} = Tempo.SQL.Meta.encode_interval(~o"1984?/2004~")
    iex> Tempo.SQL.Meta.decode_interval(IO.iodata_to_binary(json))
    {:ok, ~o"1984?/2004~"}

A row `tempo_sql` 0.2.0 wrote is read as it was written to mean, its
end the day before the one written:

    iex> Tempo.SQL.Meta.decode_interval(~s({"v":2,"from":"2022Y1M1D","to":"2022Y1M5D","recurrence":1}))
    {:ok, ~o"2022-01-01/2022-01-04"}

    iex> Tempo.SQL.Meta.decode_interval(~s({"from":"2026","to":"2027","direction":7}))
    {:error, {:invalid_direction, 7}}

# `decode_json`

```elixir
@spec decode_json(binary()) :: term()
```

Decodes a nil-aware JSON binary.

### Arguments

* `binary` is the JSON document to decode.

### Returns

* The decoded term directly — not wrapped in a tuple — with JSON
  null becoming `nil`.

### Examples

    iex> Tempo.SQL.Meta.decode_json(~s({"hours":8}))
    %{"hours" => 8}

# `decode_options`

```elixir
@spec decode_options() :: map()
```

Returns the decoder options for `:json.decode/3` that map JSON null
to `nil`.

### Returns

* A map of `:json` decoder options.

### Examples

    iex> Tempo.SQL.Meta.decode_options()
    %{null: nil}

# `encode_interval`

```elixir
@spec encode_interval(Tempo.t()) :: {:ok, iodata()} | {:error, Exception.t()}
```

Encodes an expression to a JSON string for storage in a `jsonb`
column.

This is what makes the composite types lossless: everything a
`tstzrange` cannot express — qualifications, calendars, recurrence,
metadata — is carried here instead.

### Arguments

* `expression` is a `t:Tempo.t/0`.

### Returns

* `{:ok, iodata}`, the encoded document, suitable for handing to
  Postgrex.

* `{:error, exception}` for an expression with no ISO 8601 text.

### Examples

    iex> {:ok, json} = Tempo.SQL.Meta.encode_interval(~o"2027-06-15/2027-06-16")
    iex> json
    ...> |> IO.iodata_to_binary()
    ...> |> Tempo.SQL.Meta.decode_json()
    ...> |> Map.get("expression")
    "2027Y6M15D/16D"

# `encode_json`

```elixir
@spec encode_json(term()) :: binary()
```

Encodes an arbitrary term as a nil-aware JSON binary.

### Arguments

* `term` is any JSON-serialisable term.

### Returns

* The encoded JSON as a binary.

### Examples

    iex> Tempo.SQL.Meta.encode_json(%{"hours" => 8})
    ~s({"hours":8})

# `nil_aware_encoder`

```elixir
@spec nil_aware_encoder(term(), :json.encoder()) :: iodata()
```

Encoder callback for `:json.encode/2` that treats `nil` as JSON
null.

### Arguments

* `value` is the term being encoded.

* `encode` is the encoder function `:json` passes for recursion.

### Returns

* The encoded value as iodata.

### Examples

    iex> :json.encode(%{"ends" => nil}, &Tempo.SQL.Meta.nil_aware_encoder/2)
    ...> |> IO.iodata_to_binary()
    ~s({"ends":null})

---

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