Tempo.SQL.Meta (Tempo SQL v0.2.0)

Copy Markdown View Source

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

decode_interval(json)

@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

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

Returns

  • {:ok, interval} where interval is a Tempo.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}}

decode_json(binary)

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

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(interval)

@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

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"

encode_json(term)

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(value, encode)

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})