Packages

Ecto types and migration helpers for persisting Tempo intervals and interval sets as PostgreSQL tstzrange/tstzmultirange values.

Current section

Files

Jump to
ex_tempo_sql lib tempo ecto interval.ex
Raw

lib/tempo/ecto/interval.ex

if Code.ensure_loaded?(Ecto.Type) do
defmodule Tempo.Ecto.Interval do
@moduledoc """
Ecto.ParameterizedType for persisting a `t:Tempo.Interval.t/0`
as a PostgreSQL `tstzrange` value.
## Usage
schema "meetings" do
field :window, Tempo.Ecto.Interval
end
and in the migration:
add :window, :tstzrange
(or use `Tempo.SQL.Migration.add_interval/2`).
## Options
* `:resolution` — on load, truncate both endpoints to the
given component and drop all sub-components. One of
`:year`, `:month`, `:day`, `:hour`, `:minute`, or
`:second`. Defaults to `:second` (no truncation). See
the storage contract guide for the semantics and
caveats.
field :reporting_period, Tempo.Ecto.Interval, resolution: :year
## Storage contract
This type refuses to store values it cannot round-trip
semantically. The `dump` callback returns `:error` for:
* Intervals with a recurrence count (`recurrence != 1`) or
a `repeat_rule`. Materialise into a
`t:Tempo.IntervalSet.t/0` via `Tempo.to_interval/1` and
store the set as `Tempo.Ecto.IntervalSet` instead.
* Tempo endpoints with a `:qualification` (`:uncertain`,
`:approximate`), a non-Gregorian calendar, or
multi-valued token slots (`day_of_week: [1, 3, 5]`,
`day: 1..15`).
* Intervals that are unbounded on both sides. Partial open
ends (`from: :undefined` or `to: :undefined`) are stored
as `(,b]` / `[a,)` ranges in Postgres.
## Bracket normalisation on load
PostgreSQL does not canonicalise `tstzrange` values on
output — a column populated by another writer might hold
`[a, b]`, `(a, b)`, or `(a, b]`. The loader normalises any
non-half-open range to Tempo's `[first, last)` convention
by shifting the offending endpoint one second (Tempo is
second-resolution so this is exact).
"""
use Ecto.ParameterizedType
alias Ecto.ParameterizedType
alias Tempo.SQL.Conversion
@doc """
Helper for use outside a schema — returns a `t:Ecto.ParameterizedType.opts/0`
tuple that can be passed to `Ecto.Type.cast/2` etc.
### Examples
params = Tempo.Ecto.Interval.cast_type(resolution: :year)
Ecto.Type.cast(Tempo.Ecto.Interval, range, params)
"""
def cast_type(options \\ []) do
ParameterizedType.init(__MODULE__, options)
end
@impl Ecto.ParameterizedType
def type(_params), do: :tstzrange
@impl Ecto.ParameterizedType
def init(options) do
resolution =
options
|> Keyword.get(:resolution, :second)
|> Conversion.validate_resolution!()
%{resolution: resolution}
end
@impl Ecto.ParameterizedType
def cast(nil, _params), do: {:ok, nil}
def cast(%Tempo.Interval{} = interval, _params), do: materialise(interval)
def cast(%Tempo{} = tempo, _params), do: materialise(tempo)
def cast(%Postgrex.Range{} = range, params) do
case Conversion.range_to_interval(range, resolution: params.resolution) do
{:ok, interval} -> {:ok, interval}
{:error, _} -> :error
end
end
def cast(_, _params), do: :error
@impl Ecto.ParameterizedType
def load(value, loader \\ nil, params \\ %{resolution: :second})
def load(nil, _loader, _params), do: {:ok, nil}
def load(%Postgrex.Range{} = range, _loader, params) do
case Conversion.range_to_interval(range, resolution: params.resolution) do
{:ok, interval} -> {:ok, interval}
{:error, _} -> :error
end
end
def load(_, _, _), do: :error
@impl Ecto.ParameterizedType
def dump(value, dumper \\ nil, params \\ %{resolution: :second})
def dump(nil, _, _), do: {:ok, nil}
def dump(%Tempo.Interval{} = interval, _, _) do
case Conversion.interval_to_range(interval) do
{:ok, range} -> {:ok, range}
{:error, _} -> :error
end
end
def dump(_, _, _), do: :error
@impl Ecto.ParameterizedType
def equal?(a, b, _params), do: a == b
@impl Ecto.ParameterizedType
def embed_as(_format, _params), do: :self
defp materialise(%Tempo.Interval{from: %Tempo{}, to: %Tempo{}} = interval),
do: {:ok, interval}
defp materialise(value) do
case Tempo.to_interval(value) do
{:ok, %Tempo.Interval{} = interval} -> {:ok, interval}
{:ok, %Tempo.IntervalSet{}} -> :error
_ -> :error
end
end
end
end