Migration helpers for PostgreSQL range types.
PostgreSQL provides range types over numbers, dates and timestamps, so most range columns need no migration beyond naming the type:
add :period, :daterangeTwo things do need a migration. There is no built-in range over time
of day, which create_time_range/1 supplies, and the constraint that
makes ranges worth using — that no two rows may overlap — is an
exclusion constraint, which no_overlap/3 builds:
defmodule MyApp.Repo.Migrations.AddBookings do
use Ecto.Migration
import Localize.Ecto.Range.Migration
def change do
create_btree_gist()
create table(:bookings) do
add :room_id, references(:rooms), null: false
add :period, :tstzrange, null: false
end
create no_overlap(:bookings, :period, scope: [:room_id])
end
endWith that constraint in place, two bookings of the same room whose periods overlap are rejected by the database, whatever the application does.
Summary
Functions
Enables the btree_gist extension.
Creates a range type over time of day.
Drops a range type over time of day.
Returns an exclusion constraint forbidding overlapping ranges.
Functions
@spec create_btree_gist() :: :ok
Enables the btree_gist extension.
An exclusion constraint is enforced by a GiST index, and GiST cannot
index equality on ordinary scalar types such as integer or uuid
without this extension. Any no_overlap/3 constraint carrying a
:scope therefore needs it, and creating the constraint fails with
data type integer has no default operator class when it is absent.
The operation is reversible. Enabling an extension requires database superuser privileges.
Returns
:ok.
Examples
create_btree_gist()
@spec create_time_range(Keyword.t()) :: :ok
Creates a range type over time of day.
PostgreSQL ships ranges over integer, bigint, numeric, date,
timestamp and timestamptz, but not over time. This creates one,
which is what opening hours and shift patterns need.
Postgrex discovers the new type when a connection pool starts, and
decodes it to a Postgrex.Range.t/0 of Time.t/0 with no
further configuration. Because discovery happens at connection time,
the type must be created in a migration rather than at runtime — a
pool already running when the type is created does not know it.
The operation is reversible.
Arguments
optionsis a keyword list of options.
Options
:nameis the name of the range type. The default is"timerange".:subtypeis the SQL type the range spans. The default is"time"; pass"timetz"for a range over time with time zone.
Returns
:ok.
Examples
create_time_range()
create_time_range(name: "opening_hours")
@spec drop_time_range(Keyword.t()) :: :ok
Drops a range type over time of day.
The operation is reversible: rolling back recreates the type from the same arguments.
Arguments
optionsis a keyword list of options. Seecreate_time_range/1.
Returns
:ok.
Examples
drop_time_range()
drop_time_range(name: "opening_hours")
@spec no_overlap(atom() | String.t(), atom() | String.t(), Keyword.t()) :: Ecto.Migration.Constraint.t()
Returns an exclusion constraint forbidding overlapping ranges.
The constraint rejects any two rows whose ranges overlap. A :scope
narrows that to rows agreeing on other columns, which is almost
always what is wanted — one room's bookings may not overlap each
other, but they may freely overlap another room's.
Pass the result to Ecto.Migration.create/1. A scoped constraint
needs the btree_gist extension; see create_btree_gist/0.
Arguments
tableis the table name as an atom or string.columnis the range column as an atom or string.optionsis a keyword list of options.
Options
:scopeis a list of columns that must agree for two rows to be considered in conflict. The default is[], which forbids any two rows in the table from overlapping.:nameis the name of the constraint. The default is"<table>_<column>_no_overlap".:whereis a SQL predicate restricting the constraint to rows matching it, such as"status <> 'cancelled'", so that rows failing the predicate are exempt. The default isnil.
Returns
Examples
create no_overlap(:bookings, :period, scope: [:room_id])
create no_overlap(:bookings, :period, where: "status <> 'cancelled'")