Tempo.SQL.Meta (Tempo SQL v0.4.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, 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

decode_interval(json)

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

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

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

@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

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

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

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