# Changelog

All notable changes to `ex_tempo_sql` are documented here, following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [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 `t: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.
