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, interval
metadata, and the implicit-vs-explicit-span distinction.
On store, the meta is a JSON document with this shape:
%{
"v" => 2,
"from" => iso8601_string | ".." | null,
"to" => iso8601_string | ".." | null,
"recurrence" => pos_integer | "infinity" | 1,
"direction" => 1 | -1,
"duration" => iso8601_string | null,
"repeat_rule" => iso8601_string | null,
"metadata" => map # must be JSON-serialisable
}On load, the endpoints are reconstituted via
Tempo.from_iso8601/1, which faithfully recovers every
Tempo feature ISO 8601 / ISO 8601-2 / IXDTF can express.
Format versions
Version 2 writes ".." for an explicitly open-ended endpoint
and null for an absent one, keeping apart two states that a
recurring interval depends on: ~o"2022Y1M1D/.." has an open
to, while R5/2022-01-01/P1M has no to at all and takes
its extent from the duration.
Version 1 wrote null for both and read it back as
open-ended, which left a stored recurring interval unable to
render. Version 1 rows still decode as they always did — that
reading is all the information they carry — so no data
migration is needed, but a recurring interval written by
0.1.0 should be rewritten to regain its to.
Encoding uses Erlang's built-in :json module (OTP 27+);
the jsonb column type handles storage and indexing on the
Postgres side.
Summary
Functions
Decodes a JSON string, or an already-decoded map, back into a
Tempo.Interval.t/0.
Decodes a nil-aware JSON binary.
Returns the decoder options for :json.decode/3 that map JSON null
to nil.
Encodes a Tempo.Interval.t/0 to a JSON string for storage in a
jsonb column.
Encodes an arbitrary term as a nil-aware JSON binary.
Encoder callback for :json.encode/2 that treats nil as JSON
null.
Functions
@spec decode_interval(String.t() | map()) :: {:ok, Tempo.Interval.t()} | {:error, term()}
Decodes a JSON string, or an already-decoded map, back into a
Tempo.Interval.t/0.
Arguments
jsonis the encoded document, either as a binary or as the map Postgrex hands back from ajsonbcolumn.
Returns
{:ok, interval}whereintervalis aTempo.Interval.t/0.{:error, reason}when the document is not an interval, or holds a field that does not decode.
Examples
iex> interval = Tempo.Interval.new!(from: ~o"1984?", to: ~o"2004~")
iex> json = interval |> Tempo.SQL.Meta.encode_interval() |> IO.iodata_to_binary()
iex> {:ok, loaded} = Tempo.SQL.Meta.decode_interval(json)
iex> {loaded.from.qualification, loaded.to.qualification}
{:uncertain, :approximate}
iex> Tempo.SQL.Meta.decode_interval(~s({"from":"2026","direction":7}))
{:error, {:invalid_direction, 7}}
Decodes a nil-aware JSON binary.
Arguments
binaryis 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}
Returns the decoder options for :json.decode/3 that map JSON null
to nil.
Returns
- A map of
:jsondecoder options.
Examples
iex> Tempo.SQL.Meta.decode_options()
%{null: nil}
@spec encode_interval(Tempo.Interval.t()) :: iodata()
Encodes a Tempo.Interval.t/0 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
intervalis theTempo.Interval.t/0to encode.
Returns
- The encoded document as iodata, suitable for handing to Postgrex.
Examples
iex> interval = Tempo.Interval.new!(from: ~o"2027-06-15", to: ~o"2027-06-16")
iex> interval
...> |> Tempo.SQL.Meta.encode_interval()
...> |> IO.iodata_to_binary()
...> |> Tempo.SQL.Meta.decode_json()
...> |> Map.get("from")
"2027Y6M15D"
Encodes an arbitrary term as a nil-aware JSON binary.
Arguments
termis any JSON-serialisable term.
Returns
- The encoded JSON as a binary.
Examples
iex> Tempo.SQL.Meta.encode_json(%{"hours" => 8})
~s({"hours":8})
Encoder callback for :json.encode/2 that treats nil as JSON
null.
Arguments
valueis the term being encoded.encodeis the encoder function:jsonpasses 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})