Internal helpers that translate between Tempo.Interval.t/0
endpoints and the DateTime / :unbound values that
Postgrex.Range understands.
The storage contract this module enforces — see the README for the full rationale:
%Tempo{}endpoints must carry a fully anchored year/month/day/hour/minute/second time slot (Tempo's highest resolution). Partial values must be materialised viaTempo.to_interval/1first.:qualification,:qualifications, and:extendedmetadata are dropped on storage — round-tripping is lossy by design for round 1.Non-Gregorian calendars are rejected.
Multi-valued token slots (lists like
day_of_week: [1, 3, 5]or ranges likeday: 1..15) are rejected.Tempo.Intervalrecurrence (:recurrence,:repeat_rule) is rejected — callers must materialise recurring intervals to aTempo.IntervalSetviaTempo.to_interval/1and store that aststzmultirangeinstead.
Summary
Functions
Convert a Tempo.Interval.t/0 into a %Postgrex.Range{}
tolerant enough for the composite tempo_range type.
Converts a Tempo.Interval.t/0 into a %Postgrex.Range{} with
DateTime bounds in UTC.
Convert a %Postgrex.Range{} back into a Tempo.Interval.t/0.
Truncates a Tempo.t/0 token list to the given resolution,
dropping all sub-resolution components.
Returns the valid values for the :resolution option on the Ecto
types.
Validates a :resolution option value.
Functions
@spec interval_to_queryable_range(Tempo.Interval.t()) :: {:ok, Postgrex.Range.t()} | {:error, Tempo.SQL.UnsupportedValueError.t()}
Convert a Tempo.Interval.t/0 into a %Postgrex.Range{}
tolerant enough for the composite tempo_range type.
Unlike interval_to_range/1, this variant accepts Tempo
endpoints the plain tstzrange encoder rejects — qualifications,
non-Gregorian calendars, multi-valued slots, and endpoints still
carrying week or day-of-year tokens — because the composite's
meta column carries the original
shape losslessly. The range is still populated from the
materialised endpoints so Postgres range-operator queries work.
Endpoints are materialised via Tempo.to_interval/1 where
needed to produce usable DateTime bounds.
Arguments
intervalis theTempo.Interval.t/0to convert.
Returns
{:ok, range}whererangeis aPostgrex.Range.t/0. An open endpoint becomes:unbound, so an interval with no stated end is still queryable.{:error, exception}when an endpoint has no position on the time line at all — a bare duration, for instance.
Examples
iex> workday = Tempo.Interval.new!(
...> from: ~o"2027-06-15T09:00:00",
...> to: ~o"2027-06-15T17:00:00"
...> )
iex> {:ok, range} = Tempo.SQL.Conversion.interval_to_queryable_range(workday)
iex> range.upper
~U[2027-06-15 17:00:00Z]An open-ended interval keeps a usable lower bound:
iex> {:ok, ongoing} = Tempo.Interval.new(from: ~o"2027-06-15T09:00:00", to: :undefined)
iex> {:ok, range} = Tempo.SQL.Conversion.interval_to_queryable_range(ongoing)
iex> range.upper
:unbound
@spec interval_to_range(Tempo.Interval.t()) :: {:ok, Postgrex.Range.t()} | {:error, Tempo.SQL.UnsupportedValueError.t()}
Converts a Tempo.Interval.t/0 into a %Postgrex.Range{} with
DateTime bounds in UTC.
The range is half-open — lower inclusive, upper exclusive — matching
Tempo's [from, to) convention exactly, so no boundary is gained or
lost in storage.
Arguments
intervalis theTempo.Interval.t/0to convert.
Returns
{:ok, range}whererangeis aPostgrex.Range.t/0.{:error, exception}whereexceptionis aTempo.SQL.UnsupportedValueError.t/0, when the interval holds something atstzrangecannot — see the storage-contract guide.
Examples
iex> workday = Tempo.Interval.new!(
...> from: ~o"2027-06-15T09:00:00",
...> to: ~o"2027-06-15T17:00:00"
...> )
iex> {:ok, range} = Tempo.SQL.Conversion.interval_to_range(workday)
iex> {range.lower, range.upper}
{~U[2027-06-15 09:00:00Z], ~U[2027-06-15 17:00:00Z]}
iex> {range.lower_inclusive, range.upper_inclusive}
{true, false}
iex> uncertain = %Tempo.Interval{from: ~o"1984?", to: ~o"2004"}
iex> {:error, error} = Tempo.SQL.Conversion.interval_to_range(uncertain)
iex> error.reason
:qualification
@spec range_to_interval( Postgrex.Range.t(), keyword() ) :: {:ok, Tempo.Interval.t()} | {:error, Tempo.SQL.UnsupportedValueError.t()}
Convert a %Postgrex.Range{} back into a Tempo.Interval.t/0.
Unlike discrete range types (int4range, daterange), PostgreSQL
does not canonicalise tstzrange to [lower, upper) on
output — a range written as [a, b] round-trips as [a, b].
This loader therefore normalises any non-half-open range to
Tempo's [first, last) convention by shifting the offending
endpoint one second:
[a, b]→[a, b + 1s)(a, b)→[a + 1s, b)(a, b]→[a + 1s, b + 1s)
Tempo is second-resolution so the shift is exact — the loaded interval covers the same instants as the stored range.
Arguments
rangeis thePostgrex.Range.t/0to convert.optionsis a keyword list of options.
Options
:resolutiontruncates both endpoints to the given component, dropping all sub-components. Must be one of:year,:month,:day,:hour,:minute, or:second. Defaults to:second(no truncation). See the storage contract guide for semantics.
Returns
{:ok, interval}whereintervalis aTempo.Interval.t/0.{:error, exception}when an endpoint is not a time, or when truncating to the requested resolution would collapse the span to zero extent.
Examples
iex> range = %Postgrex.Range{
...> lower: ~U[2027-06-15 09:00:00Z],
...> upper: ~U[2027-06-15 17:00:00Z],
...> lower_inclusive: true,
...> upper_inclusive: false
...> }
iex> {:ok, interval} = Tempo.SQL.Conversion.range_to_interval(range)
iex> {Tempo.hour(interval.from), Tempo.hour(interval.to)}
{9, 17}An inclusive upper bound is shifted out by a second, so the loaded interval covers the same instants as the stored range:
iex> closed = %Postgrex.Range{
...> lower: ~U[2027-06-15 09:00:00Z],
...> upper: ~U[2027-06-15 17:00:00Z],
...> lower_inclusive: true,
...> upper_inclusive: true
...> }
iex> {:ok, interval} = Tempo.SQL.Conversion.range_to_interval(closed)
iex> Tempo.second(interval.to)
1
Truncates a Tempo.t/0 token list to the given resolution,
dropping all sub-resolution components.
Arguments
tempois theTempo.t/0to truncate.resolutionis the coarsest component to keep — one of those returned byvalid_resolutions/0.
Returns
- A
Tempo.t/0carrying only components at or aboveresolution. A value already coarser than the requested resolution is returned unchanged.
Examples
iex> Tempo.SQL.Conversion.truncate_tempo(~o"2027-06-15T09:30:45", :day)
~o"2027Y6M15D"
iex> Tempo.SQL.Conversion.truncate_tempo(~o"2027Y", :second)
~o"2027Y"
Returns the valid values for the :resolution option on the Ecto
types.
Returns
- A list of resolution atoms, coarsest first.
Examples
iex> Tempo.SQL.Conversion.valid_resolutions()
[:year, :month, :day, :hour, :minute, :second]
Validates a :resolution option value.
Arguments
resolutionis the value to validate.
Returns
The resolution atom unchanged, when it is one of those returned by
valid_resolutions/0.Raises
ArgumentErrorotherwise, naming the value it was given.
Examples
iex> Tempo.SQL.Conversion.validate_resolution!(:day)
:day
iex> Tempo.SQL.Conversion.validate_resolution!(:fortnight)
** (ArgumentError) expected :resolution to be one of [:year, :month, :day, :hour, :minute, :second], got: :fortnight