All notable changes to ex_tempo_sql are documented here, following Keep a Changelog.
[v0.4.0] — 2026-11-01
Breaking changes
- A floating value, with no zone and no time shift, is refused by a column on the UTC line (
UnsupportedValueErrorreason:frame), where it was stored as though at UTC. Declare its columnframe: :floating.
Added
A
:frameoption on every type and migration helper:frame: :floatingstores floating values intsrange,tsmultirange,tempo_floating_rangeandtempo_floating_multirangecolumns, refusing values in a zone or at a time shift.SQLite storage:
Tempo.Ecto.SQLite.TempoRangekeeps an expression whole in aTEXTcolumn as JSON beside its integer bounds to the second, withTempo.Ecto.SQLite.QueryAPI(the five Allen-named macros andprobe/2) andTempo.SQL.SQLite.Migration.Tempo.SQL.JSON.decode/1returns an error for text that is not one JSON document, wheredecode!/1raises.cast/2refuses a value of the other frame as a changeset error naming the frame to declare;Tempo.SQL.Conversion.on_frame/2is the check.create_tempo_types/1anddrop_tempo_types/1takeframe: :floatingfor the floating composite types;Tempo.SQL.Conversion.valid_frames/0andvalidate_frame!/1.
Fixed
A composite
metavalue that is not JSON loads as an error, whereTempo.SQL.Meta.decode_interval/1raised.The stretch a composite type stores for a set or a recurrence is an
UnsupportedValueErrorwhere an instant cannot be placed at UTC, where it raised aMatchError.
[v0.3.0] — 2026-10-28
Breaking changes
Requires
ex_tempo ~> 2.1. Every type takes and returns aTempo.t/0expression;%Tempo.Interval{}and%Tempo.IntervalSet{}are no more.An end written to a date is read through it, as Tempo 2 reads it:
~o"2026-06-15/2026-06-20"is stored as[2026-06-15, 2026-06-21).A loaded range written to a calendar unit by
:resolutionends through the last unit it holds: a range of 2026 at:yearis2026/2026.
Added
- Each member of a set stored in a
tempo_multirangecolumn keeps its own metadata.
Changed
Tempo.Ecto.Intervaltakes any expression of one span, andTempo.Ecto.IntervalSetany expression with a last span: a set, a bounded recurrence or a value that names several.Tempo.Ecto.TempoisTempo.Ecto.Intervalunder its old name.The composite types keep any expression with ISO 8601 text, stored with the range it covers from its first span to its last.
The composite
metaformat is version 3, holding the expression's ISO 8601 text. Version 1 and 2 rows are read as Tempo 1.x meant them; no migration is needed.
Fixed
Tempo.SQL.PostgresTypesis in the package, as the storage contract guide has said since 0.1.0; it was compiled only in development and test.The composite types'
loadreturns:errorfor metadata that does not decode, as Ecto expects, where it returned{:error, reason}.
[v0.2.0] — 2026-08-03
Added
- Every public function now carries an
### Examplessection, and every example that can run without a database runs as a doctest on each test build. The library previously had none, so its documentation was never checked against its code.
Fixed
Recurring intervals stored in a
tempo_rangeortempo_multirangecolumn now load back intact. The meta format could not tell an open-ended endpoint from an absent one, so a value likeR5/2022-01-01/P1Mreturned withto: :undefinedand could no longer be rendered at all.Documented the storage contract accurately for week and day-of-year endpoints. Ordinal dates and full week dates are resolved to calendar dates while parsing and store normally; only a value still carrying those tokens — such as a bare
~o"2026-W10"— is refused.
Changed
The composite
metaformat is now version 2, which writes".."for an explicitly open-ended endpoint. Version 1 rows keep decoding exactly as before, so no migration is required, but a recurring interval written by 0.1.0 should be rewritten to regain its endpoint.Requires
ex_tempo ~> 1.1 and >= 1.1.1. The previous~> 0.16constraint could not resolve alongside Tempo 1.x, so the two libraries could not be used together. The 1.1.1 floor matters on its own: before it, casting a list whose members are not intervals raised rather than returning:error, so bad changeset input crashed instead of failing validation.Repository links point at the
elixir-tempoorganisation, and the README links the storage-contract guide on HexDocs rather than a GitHub blob.Requires Erlang/OTP 27 or later, inherited from
ex_tempo— OTP 26 is no longer supported and has been dropped from CI.
[v0.1.0] — 2026-07-05
Highlights
Tempo SQL persists Tempo intervals and interval sets as PostgreSQL range types, wiring the half-open
[from, to)convention through totstzrange/tstzmultirangenatively.Ecto types.
Tempo.Ecto.Intervalstores a%Tempo.Interval{}aststzrange;Tempo.Ecto.IntervalSetstores a%Tempo.IntervalSet{}aststzmultirange(PG 14+);Tempo.Ecto.Tempomaterialises a bare%Tempo{}implicit span before delegating to the interval type.Storage contract. Each type is explicit about what it refuses to store: recurrence rules, qualifications, non-Gregorian calendars, multi-valued token slots, and ordinal/week-date endpoints all return
:errorfromdump/1rather than silently losing information. See the "Storage contract" section of the README.Migration helpers.
Tempo.SQL.Migrationexposesadd_interval/2,add_interval_set/2, andcreate_interval_index/3. Range queries require GiST indexes for speed; the helper handles that.Query API.
Tempo.Ecto.QueryAPIwraps Postgres range operators (@>,&&,-|-,<<,>>) under Allen's interval-algebra names —contains,overlaps,meets,strictly_before,strictly_after.Round-trip note. The plain-range types (
Tempo.Ecto.Interval,.IntervalSet,.Tempo) are lossy on metadata — qualifications, recurrence rules, calendars, zone identifiers, and the implicit-vs-explicit-span distinction are dropped on store. Partial resolution is recoverable via the:resolutionfield option. For full round-trip fidelity, use the composite types below.Composite
tempo_range/tempo_multirangetypes.Tempo.Ecto.TempoRangeandTempo.Ecto.TempoMultirangepersist the full Tempo shape byte-for-byte via a PostgreSQL composite type pairing atstzrange/tstzmultirangewith ajsonbmeta column. One-time migration creates the types (Tempo.SQL.Migration.create_tempo_types/0); field helpers (add_tempo_range/2,add_tempo_multirange/2) declare the columns;Tempo.Ecto.QueryAPI.Compositeprovides auto-unwrapping query macros. Uses Erlang's built-in:json(OTP 27+) via theTempo.SQL.PostgresTypesmodule — no Jason dependency.