Ecto.ParameterizedType for persisting an interval as a
PostgreSQL tstzrange value, or as a tsrange value when it is
floating.
It takes any expression that is one span — an interval, or a value
(~o"2026-06") as the interval it names — and loads an interval.
Usage
schema "meetings" do
field :window, Tempo.Ecto.Interval
endand in the migration:
add :window, :tstzrange(or use Tempo.SQL.Migration.add_interval/2).
A field of floating values (with no zone and no time shift, such as
a shop's opening hours wherever it is) is declared on the floating
frame, and stored in a tsrange:
field :opening_hours, Tempo.Ecto.Interval, frame: :floating
add :opening_hours, :tsrangeOptions
: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:frame— the line the column's instants lie on.:utc(the default) stores a value in a zone or at a time shift as the UTC instants it is, in atstzrange;:floatingstores a value with neither, in atsrange. A value of the other frame is refused bycast/2, as a changeset error that names the frame to declare, and bydump/3.
Storage contract
This type refuses to store values it cannot round-trip
semantically. The dump callback returns :error for:
A value of the other frame: a floating value for a column on the UTC line, or a value in a zone or at a time shift for a floating column.
An expression of several spans: a recurrence, a set, or a value naming several (
~o"2026Y{6,8}M"). Store those asTempo.Ecto.IntervalSetinstead.A qualification (
:uncertain,:approximate) or a calendar other than the Gregorian, which a range of instants cannot keep.Tempo.Ecto.TempoRangekeeps both.An interval unbounded on both sides. One open end is stored as a
(,b)or[a,)range 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 [lower, upper)
by shifting the offending endpoint one second (Tempo is
second-resolution so this is exact).
Summary
Functions
Builds the parameterized-type tuple for use outside a schema.
Functions
@spec cast_type(keyword()) :: Ecto.Type.t()
Builds the parameterized-type tuple for use outside a schema.
Inside a schema, field :window, Tempo.Ecto.Interval, resolution: :day
does this for you. Reach for this when calling Ecto.Type.cast/2
and friends directly.
Arguments
optionsis a keyword list of options.
Options
:resolutiontruncates both endpoints on load to the given component. One of:year,:month,:day,:hour,:minute, or:second. Defaults to:second.:frameis:utc(the default) for atstzrangecolumn or:floatingfor atsrangecolumn.
Returns
- A
Ecto.ParameterizedType.opts/0tuple.
Examples
iex> Tempo.Ecto.Interval.cast_type()
{:parameterized, {Tempo.Ecto.Interval, %{frame: :utc, resolution: :second}}}
iex> Tempo.Ecto.Interval.cast_type(resolution: :year, frame: :floating)
{:parameterized, {Tempo.Ecto.Interval, %{frame: :floating, resolution: :year}}}