use Vtc.Ecto.Postgres.Utils
defpgmodule Vtc.Ecto.Postgres.PgFramerate.Migrations do
@moduledoc """
Migrations for adding framerate types, functions and constraints to a
Postgres database.
"""
alias Ecto.Migration
alias Vtc.Ecto.Postgres
require Ecto.Migration
@typedoc """
Indicates returned string is am SQL command.
"""
@type raw_sql() :: String.t()
@doc section: :migrations_full
@doc """
Adds raw SQL queries to a migration for creating the database types, associated
functions, casts, operators, and operator families.
This migration included all migraitons under the
[Pg Types](Vtc.Ecto.Postgres.PgFramerate.Migrations.html#pg-types),
[Pg Operators](Vtc.Ecto.Postgres.PgFramerate.Migrations.html#pg-operators),
[Pg Functions](Vtc.Ecto.Postgres.PgFramerate.Migrations.html#pg-functions), and
[Pg Private Functions](Vtc.Ecto.Postgres.PgFramerate.Migrations.html#pg-private-functions),
headings.
Safe to run multiple times when new functionality is added in updates to this library.
Existing values will be skipped.
Individual migration functions return raw sql commands in an
{up_command, down_command} tuple.
## Options
- `include`: A list of migration functions to inclide. If not set, all sub-migrations
will be included.
- `exclude`: A list of migration functions to exclude. If not set, no sub-migrations
will be excluded.
## Types Created
Calling this macro creates the following type definitions:
```sql
CREATE TYPE framerate_tags AS ENUM (
'drop',
'non_drop'
);
```
```sql
CREATE TYPE framerate AS (
playback rational,
tags framerate_tags[]
);
```
## Schemas Created
Up to two schemas are created as detailed by the
[Configuring Database Objects](Vtc.Ecto.Postgres.PgFramerate.Migrations.html#create_all/0-configuring-database-objects)
section below.
## Configuring Database Objects
To change where supporting functions are created, add the following to your
Repo confiugration:
```elixir
config :vtc, Vtc.Test.Support.Repo,
adapter: Ecto.Adapters.Postgres,
...
vtc: [
framerate: [
functions_schema: :framerate,
functions_prefix: "framerate"
]
]
```
Option definitions are as follows:
- `functions_schema`: The postgres schema to store framerate-related custom functions.
- `functions_prefix`: A prefix to add before all functions. Defaults to "framestamp"
for any function created in the `:public` schema, and "" otherwise.
## Private Functions
Some custom function names are prefaced with `__private__`. These functions should
not be called by end-users, as they are not subject to *any* API staility guarantees.
## Examples
```elixir
defmodule MyMigration do
use Ecto.Migration
alias Vtc.Ecto.Postgres.PgFramerate
require PgFramerate.Migrations
def change do
PgFramerate.Migrations.create_all()
end
end
```
"""
@spec create_all(include: Keyword.t(), exclude: Keyword.t()) :: :ok
def create_all(opts \\ []) do
migrations = [
&create_type_framerate_tags/0,
&create_type_framerate/0,
&create_function_schemas/0,
&create_func_is_ntsc/0,
&create_func_strict_eq/0,
&create_func_strict_neq/0,
&create_op_strict_eq/0,
&create_op_strict_neq/0
]
Postgres.Utils.run_migrations(migrations, opts)
end
@doc section: :migrations_types
@doc """
Adds `framerate_tgs` enum type.
"""
@spec create_type_framerate_tags() :: {raw_sql(), raw_sql()}
def create_type_framerate_tags, do: Postgres.Utils.create_type(:framerate_tags, :enum, [:drop, :non_drop])
@doc section: :migrations_types
@doc """
Adds `framerate` composite type.
"""
@spec create_type_framerate() :: {raw_sql(), raw_sql()}
def create_type_framerate do
Postgres.Utils.create_type(:framerate,
playback: :rational,
tags: "framerate_tags[]"
)
end
## FUNCTIONS
@doc section: :migrations_types
@doc """
Creates function schema as described by the
[Configuring Database Objects](Vtc.Ecto.Postgres.PgFramerate.Migrations.html#create_all/0-configuring-database-objects)
section above.
"""
@spec create_function_schemas() :: {raw_sql(), raw_sql()}
def create_function_schemas, do: Postgres.Utils.create_type_schema(:framerate)
@doc section: :migrations_functions
@doc """
Creates `framerate.is_ntsc(rat)` function that returns true if the framerate
is and NTSC drop or non-drop rate.
"""
@spec create_func_is_ntsc() :: {raw_sql(), raw_sql()}
def create_func_is_ntsc do
Postgres.Utils.create_plpgsql_function(
function(:is_ntsc, Migration.repo()),
args: [input: :framerate],
returns: :boolean,
body: """
RETURN (
(input).tags @> '{drop}'::framerate_tags[]
OR (input).tags @> '{non_drop}'::framerate_tags[]
);
"""
)
end
@doc section: :migrations_functions
@doc """
Creates `framerate.__private__strict_eq(a, b)` that backs the `===` operator.
"""
@spec create_func_strict_eq() :: {raw_sql(), raw_sql()}
def create_func_strict_eq do
Postgres.Utils.create_plpgsql_function(
private_function(:strict_eq, Migration.repo()),
args: [a: :framerate, b: :framerate],
returns: :boolean,
body: """
RETURN (a).playback = (b).playback
AND (a).tags <@ (b).tags
AND (a).tags @> (b).tags;
"""
)
end
@doc section: :migrations_functions
@doc """
Creates `framerate.__private__strict_eq(a, b)` that backs the `===` operator.
"""
@spec create_func_strict_neq() :: {raw_sql(), raw_sql()}
def create_func_strict_neq do
Postgres.Utils.create_plpgsql_function(
private_function(:strict_neq, Migration.repo()),
args: [a: :framerate, b: :framerate],
returns: :boolean,
body: """
RETURN (a).playback != (b).playback
OR NOT (a).tags <@ (b).tags
OR NOT (a).tags @> (b).tags;
"""
)
end
## OPERATORS
@doc section: :migrations_operators
@doc """
Creates a custom :framerate, :framerate `===` operator that returns true if *both*
the playback rate AND tags of a framerate are equal.
"""
@spec create_op_strict_eq() :: {raw_sql(), raw_sql()}
def create_op_strict_eq do
Postgres.Utils.create_operator(
:===,
:framerate,
:framerate,
private_function(:strict_eq, Migration.repo()),
commutator: :===,
negator: :"!==="
)
end
@doc section: :migrations_operators
@doc """
Creates a custom :framerate, :framerate `===` operator that returns true if *both*
the playback rate AND tags of a framerate are equal.
"""
@spec create_op_strict_neq() :: {raw_sql(), raw_sql()}
def create_op_strict_neq do
Postgres.Utils.create_operator(
:"!===",
:framerate,
:framerate,
private_function(:strict_neq, Migration.repo()),
commutator: :"!===",
negator: :===
)
end
## CONSTRAINTS
@doc section: :migrations_constraints
@doc """
Creates basic constraints for a [PgFramerate](`Vtc.Ecto.Postgres.PgFramerate`) /
[Framerate](`Vtc.Framerate`) database field.
## Arguments
- `table`: The table to make the constraint on.
- `target_value`: The target value to check. Can be be any sql fragment that resolves
to a `framerate` value.
- `field`: The name of the field being validated. Can be omitted if `target_value`
is itself a field on `table`. This name is not used for anything but the constraint
names.
## Constraints created:
- `{field}_positive`: Checks that the playback speed is positive.
- `{field}_ntsc_tags`: Checks that both `drop` and `non_drop` are not set at the same
time.
- `{field}_ntsc_valid`: Checks that NTSC framerates are mathematically sound, i.e.,
that the rate is equal to `(round(rate.playback) * 1000) / 1001`.
- `{field}_ntsc_drop_valid`: Checks that NTSC, drop-frame framerates are valid, i.e,
are cleanly divisible by `30_000/1001`.
## Examples
```elixir
create table("my_table", primary_key: false) do
add(:id, :uuid, primary_key: true, null: false)
add(:a, Framerate.type())
add(:b, Framerate.type())
end
PgRational.migration_add_field_constraints(:my_table, :a)
PgRational.migration_add_field_constraints(:my_table, :b)
```
"""
@spec create_field_constraints(atom(), atom() | String.t(), atom() | String.t()) :: :ok
def create_field_constraints(table, field_name, sql_value \\ nil) do
{field_name, sql_value} =
case {field_name, sql_value} do
{field_name, nil} -> {field_name, "#{table}.#{field_name}"}
{_, sql_value} -> {field_name, sql_value}
end
positive =
Migration.constraint(
table,
"#{field_name}_rate_positive",
check: """
(#{sql_value}).playback.denominator > 0
AND (#{sql_value}).playback.numerator > 0
"""
)
Migration.create(positive)
ntsc_tags =
Migration.constraint(
table,
"#{field_name}_ntsc_tags",
check: """
NOT (
((#{sql_value}).tags) @> '{drop}'::framerate_tags[]
AND ((#{sql_value}).tags) @> '{non_drop}'::framerate_tags[]
)
"""
)
Migration.create(ntsc_tags)
ntsc_valid =
Migration.constraint(
table,
"#{field_name}_ntsc_valid",
check: """
NOT #{function(:is_ntsc, Migration.repo())}(#{sql_value})
OR (
(
ROUND((#{sql_value}).playback) * 1000,
1001
)::rational
= (#{sql_value}).playback
)
"""
)
Migration.create(ntsc_valid)
drop_valid =
Migration.constraint(
table,
"#{field_name}_ntsc_drop_valid",
check: """
NOT (#{sql_value}).tags @> '{drop}'::framerate_tags[]
OR (#{sql_value}).playback % (30000, 1001)::rational = (0, 1)::rational
"""
)
Migration.create(drop_valid)
:ok
end
@doc """
Returns the config-qualified name of the function for this type.
"""
@spec function(atom(), Ecto.Repo.t()) :: String.t()
def function(name, repo), do: "#{Postgres.Utils.type_function_prefix(repo, :framerate)}#{name}"
# Returns the config-qualified name of the function for this type.
@spec private_function(atom(), Ecto.Repo.t()) :: String.t()
defp private_function(name, repo), do: "#{Postgres.Utils.type_private_function_prefix(repo, :framerate)}#{name}"
end