Tempo.SQL.Conversion (Tempo SQL v0.1.0)

View Source

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 via Tempo.to_interval/1 first.

  • :qualification, :qualifications, and :extended metadata 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 like day: 1..15) are rejected.

  • Tempo.Interval recurrence (:recurrence, :repeat_rule) is rejected — callers must materialise recurring intervals to a Tempo.IntervalSet via Tempo.to_interval/1 and store that as tstzmultirange instead.

Summary

Functions

Convert a Tempo.Interval.t/0 into a %Postgrex.Range{} tolerant enough for the composite tempo_range type.

Convert 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.

Truncate a Tempo.t/0 token list to the given resolution, dropping all sub-resolution components.

The valid values for the :resolution option on the Ecto types.

Validate a :resolution option value. Returns the atom unchanged or raises ArgumentError.

Functions

interval_to_queryable_range(interval)

@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, ordinal/week dates — 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.

interval_to_range(interval)

@spec interval_to_range(Tempo.Interval.t()) ::
  {:ok, Postgrex.Range.t()} | {:error, Tempo.SQL.UnsupportedValueError.t()}

Convert a Tempo.Interval.t/0 into a %Postgrex.Range{} with DateTime bounds in UTC.

Returns {:ok, range} on success, {:error, exception} when the interval violates the storage contract.

range_to_interval(range, options \\ [])

@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.

Options

  • :resolution truncates 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.

truncate_tempo(tempo, resolution)

@spec truncate_tempo(Tempo.t(), atom()) :: Tempo.t()

Truncate a Tempo.t/0 token list to the given resolution, dropping all sub-resolution components.

valid_resolutions()

The valid values for the :resolution option on the Ecto types.

validate_resolution!(resolution)

Validate a :resolution option value. Returns the atom unchanged or raises ArgumentError.