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 (UnsupportedValueError reason :frame), where it was stored as though at UTC. Declare its column frame: :floating.

Added

  • A :frame option on every type and migration helper: frame: :floating stores floating values in tsrange, tsmultirange, tempo_floating_range and tempo_floating_multirange columns, refusing values in a zone or at a time shift.

  • SQLite storage: Tempo.Ecto.SQLite.TempoRange keeps an expression whole in a TEXT column as JSON beside its integer bounds to the second, with Tempo.Ecto.SQLite.QueryAPI (the five Allen-named macros and probe/2) and Tempo.SQL.SQLite.Migration.

  • Tempo.SQL.JSON.decode/1 returns an error for text that is not one JSON document, where decode!/1 raises.

  • cast/2 refuses a value of the other frame as a changeset error naming the frame to declare; Tempo.SQL.Conversion.on_frame/2 is the check.

  • create_tempo_types/1 and drop_tempo_types/1 take frame: :floating for the floating composite types; Tempo.SQL.Conversion.valid_frames/0 and validate_frame!/1.

Fixed

  • A composite meta value that is not JSON loads as an error, where Tempo.SQL.Meta.decode_interval/1 raised.

  • The stretch a composite type stores for a set or a recurrence is an UnsupportedValueError where an instant cannot be placed at UTC, where it raised a MatchError.

[v0.3.0] — 2026-10-28

Breaking changes

  • Requires ex_tempo ~> 2.1. Every type takes and returns a Tempo.t/0 expression; %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 :resolution ends through the last unit it holds: a range of 2026 at :year is 2026/2026.

Added

  • Each member of a set stored in a tempo_multirange column keeps its own metadata.

Changed

  • Tempo.Ecto.Interval takes any expression of one span, and Tempo.Ecto.IntervalSet any expression with a last span: a set, a bounded recurrence or a value that names several.

  • Tempo.Ecto.Tempo is Tempo.Ecto.Interval under 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 meta format 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.PostgresTypes is 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' load returns :error for 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 ### Examples section, 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_range or tempo_multirange column now load back intact. The meta format could not tell an open-ended endpoint from an absent one, so a value like R5/2022-01-01/P1M returned with to: :undefined and 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 meta format 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.16 constraint 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-tempo organisation, 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 to tstzrange / tstzmultirange natively.

  • Ecto types. Tempo.Ecto.Interval stores a %Tempo.Interval{} as tstzrange; Tempo.Ecto.IntervalSet stores a %Tempo.IntervalSet{} as tstzmultirange (PG 14+); Tempo.Ecto.Tempo materialises 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 :error from dump/1 rather than silently losing information. See the "Storage contract" section of the README.

  • Migration helpers. Tempo.SQL.Migration exposes add_interval/2, add_interval_set/2, and create_interval_index/3. Range queries require GiST indexes for speed; the helper handles that.

  • Query API. Tempo.Ecto.QueryAPI wraps 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 :resolution field option. For full round-trip fidelity, use the composite types below.

  • Composite tempo_range / tempo_multirange types. Tempo.Ecto.TempoRange and Tempo.Ecto.TempoMultirange persist the full Tempo shape byte-for-byte via a PostgreSQL composite type pairing a tstzrange / tstzmultirange with a jsonb meta 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.Composite provides auto-unwrapping query macros. Uses Erlang's built-in :json (OTP 27+) via the Tempo.SQL.PostgresTypes module — no Jason dependency.