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.
Summary
Functions
Decodes a JSON string, or an already-decoded map, back into an expression, whichever version wrote it.
Decodes a nil-aware JSON binary.
Returns the decoder options for :json.decode/3 that map JSON null
to nil.
Encodes an expression 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
Decodes a JSON string, or an already-decoded map, back into an expression, whichever version wrote it.
Arguments
jsonis the encoded document, either as a binary or as the map Postgrex hands back from ajsonbcolumn.
Returns
{:ok, expression}whereexpressionis aTempo.t/0.{:error, reason}when the document is nonetempo_sqlwrote ({: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}}
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}
@spec decode_options() :: map()
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.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
expressionis aTempo.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"
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})
@spec nil_aware_encoder(term(), :json.encoder()) :: iodata()
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})