use Vtc.Ecto.Postgres.Utils
defpgmodule Vtc.Ecto.Postgres.PgRational.Migrations do
@moduledoc """
Migrations for adding rational types, casts, functions and constraints to a
Postgres database.
"""
use Vtc.Ecto.Postgres.PgTypeMigration
alias Ecto.Migration
alias Ecto.Migration.Constraint
alias Vtc.Ecto.Postgres.Fragments
alias Vtc.Ecto.Postgres.PgRational
alias Vtc.Ecto.Postgres.PgTypeMigration
require Ecto.Migration
@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 includes all migrations under the
[Pg Types](Vtc.Ecto.Postgres.PgRational.Migrations.html#pg-types),
[Pg Operators](Vtc.Ecto.Postgres.PgRational.Migrations.html#pg-operators),
[Pg Operator Classes](Vtc.Ecto.Postgres.PgRational.Migrations.html#pg-operator-classes),
[Pg Functions](Vtc.Ecto.Postgres.PgRational.Migrations.html#pg-functions),
[Pg Private Functions](Vtc.Ecto.Postgres.PgRational.Migrations.html#pg-private-functions),
headings, and
[Pg Casts](Vtc.Ecto.Postgres.PgRational.Migrations.html#pg-casts)
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 include. 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 public.rational AS (
numerator bigint,
denominator bigint
);
```
## Schemas Created
Up to two schemas are created as detailed by the
[Configuring Database Objects](Vtc.Ecto.Postgres.PgRational.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 configuration:
```elixir
config :vtc, Vtc.Test.Support.Repo,
adapter: Ecto.Adapters.Postgres,
...
vtc: [
rational: [
functions_schema: :rational,
functions_prefix: "rational"
]
]
```
Option definitions are as follows:
- `functions_schema`: The postgres schema to store rational-related custom functions.
- `functions_prefix`: A prefix to add before all functions. Defaults to "rational"
for any function created in the `:public` schema, and "" otherwise.
## Examples
```elixir
defmodule MyMigration do
use Ecto.Migration
alias Vtc.Ecto.Postgres.PgRational
require PgRational.Migrations
def change do
PgRational.Migrations.run()
end
end
```
"""
@spec run(include: Keyword.t(atom()), exclude: Keyword.t(atom())) :: :ok
def run(opts \\ []), do: PgTypeMigration.run_for(__MODULE__, opts)
@doc false
@impl PgTypeMigration
def ecto_type, do: PgRational
@doc false
@impl PgTypeMigration
def migrations_list do
[
&create_type/0,
&create_function_schemas/0,
&create_func_simplify/0,
&create_func_minus/0,
&create_func_abs/0,
&create_func_sign/0,
&create_func_round/0,
&create_func_floor/0,
&create_func_add/0,
&create_func_sub/0,
&create_func_mult/0,
&create_func_div/0,
&create_func_floor_div/0,
&create_func_modulo/0,
&create_op_abs/0,
&create_op_minus/0,
&create_op_add/0,
&create_op_sub/0,
&create_op_mult/0,
&create_op_div/0,
&create_op_modulo/0,
&create_func_cmp/0,
&create_func_eq/0,
&create_func_neq/0,
&create_func_lt/0,
&create_func_lte/0,
&create_func_gt/0,
&create_func_gte/0,
&create_op_eq/0,
&create_op_neq/0,
&create_op_lt/0,
&create_op_lte/0,
&create_op_gt/0,
&create_op_gte/0,
&create_op_class_btree/0,
&create_func_cast_to_double_precision/0,
&create_func_cast_bigint_to_rational/0,
&create_cast_double_precision/0,
&create_cast_bigint_to_rational/0
]
end
@doc section: :migrations_types
@doc """
Adds:
- `rational` composite type
- `rationals` schema
- `rationals_helpers` schema
"""
@spec create_type() :: migration_info()
def create_type do
PgTypeMigration.create_type(:rational,
numerator: :bigint,
denominator: :bigint
)
end
@doc section: :migrations_types
@doc """
Creates function schema as described by the
[Configuring Database Objects](Vtc.Ecto.Postgres.PgRational.Migrations.html#create_all/0-configuring-database-objects)
section above.
"""
@spec create_function_schemas() :: migration_info()
def create_function_schemas, do: PgTypeMigration.create_type_schema(:rational)
@doc section: :migrations_private_functions
@doc """
Creates `rational.__private__simplify(rat)` function that simplifies a rational. Used at
the end of every rational operation to avoid overflows.
"""
@spec create_func_simplify() :: migration_info()
def create_func_simplify do
PgTypeMigration.create_plpgsql_function(
private_function(:simplify, Migration.repo()),
args: [input: :rational],
returns: :rational,
declares: [
greatest_denom: {:bigint, "GCD(input.numerator, input.denominator)"},
denominator: {:bigint, "ABS(input.denominator / greatest_denom)"},
numerator: {:bigint, "input.numerator / greatest_denom"}
],
body: """
numerator := numerator * SIGN((input).denominator);
RETURN (numerator, denominator);
"""
)
end
@doc section: :migrations_functions
@doc """
Creates `rational.__private__minus(rat)` function that flips the sign of the input
value -- makes a positive value negative and a negative value positive.
"""
@spec create_func_minus() :: migration_info()
def create_func_minus do
PgTypeMigration.create_plpgsql_function(
private_function(:minus, Migration.repo()),
args: [input: :rational],
returns: :rational,
body: """
RETURN ((input).numerator * -1, (input).denominator);
"""
)
end
@doc section: :migrations_functions
@doc """
Creates `ABS(rational)` function that returns the absolute value of the rational
value.
"""
@spec create_func_abs() :: migration_info()
def create_func_abs do
PgTypeMigration.create_plpgsql_function(
"ABS",
args: [input: :rational],
returns: :rational,
body: """
RETURN (ABS((input).numerator), ABS((input).denominator));
"""
)
end
@doc section: :migrations_functions
@doc """
Creates `ABS(rational)` function that returns the absolute value of the rational
value.
"""
@spec create_func_sign() :: migration_info()
def create_func_sign do
PgTypeMigration.create_plpgsql_function(
"SIGN",
args: [input: :rational],
returns: :integer,
body: """
RETURN SIGN((input).numerator * (input).denominator);
"""
)
end
@doc section: :migrations_functions
@doc """
Creates `ROUND(rational)` function that returns the rational input, rounded to the
nearest :bigint.
"""
@spec create_func_round() :: migration_info()
def create_func_round do
PgTypeMigration.create_plpgsql_function(
"ROUND",
args: [input: :rational],
returns: :bigint,
body: """
CASE
WHEN (input).numerator < 0 THEN
input := -input;
RETURN ROUND(input) * -1;
WHEN (((input).numerator % (input).denominator) * 2) < (input).denominator THEN
RETURN (input).numerator / (input).denominator;
ELSE
RETURN ((input).numerator / (input).denominator) + 1;
END CASE;
"""
)
end
@doc section: :migrations_functions
@doc """
Creates `FLOOR(rational)` function that returns the rational input as a `bigint`,
rounded towards zero, to match Postgres `FLOOR(real)` behavior.
"""
@spec create_func_floor() :: migration_info()
def create_func_floor do
PgTypeMigration.create_plpgsql_function(
"FLOOR",
args: [input: :rational],
returns: :bigint,
body: """
RETURN ((input).numerator / (input).denominator);
"""
)
end
@doc section: :migrations_private_functions
@doc """
Creates a native CAST from `rational` to `double precision`.
"""
@spec create_func_cast_to_double_precision() :: migration_info()
def create_func_cast_to_double_precision do
PgTypeMigration.create_plpgsql_function(
private_function(:cast_to_double, Migration.repo()),
args: [value: :rational],
returns: :"double precision",
body: """
RETURN CAST ((value).numerator AS double precision) / CAST ((value).denominator AS double precision);
"""
)
end
@doc section: :migrations_private_functions
@doc """
Creates a native CAST from `bigint` to `rational`.
"""
@spec create_func_cast_bigint_to_rational() :: migration_info()
def create_func_cast_bigint_to_rational do
PgTypeMigration.create_plpgsql_function(
private_function(:cast_bigint_to_rational, Migration.repo()),
args: [value: :bigint],
returns: :rational,
body: """
RETURN (value, 1)::rational;
"""
)
end
## ARITHMETIC BACKING FUNCS
@doc section: :migrations_private_functions
@doc """
Creates `rational.__private__add(a, b)` backing function for the `+` operator
between two rationals.
"""
@spec create_func_add() :: migration_info()
def create_func_add do
PgTypeMigration.create_plpgsql_function(
private_function(:add, Migration.repo()),
args: [a: :rational, b: :rational],
declares: [
numerator: {:bigint, "((a).numerator * (b).denominator) + ((b).numerator * (a).denominator)"},
denominator: {:bigint, "(a).denominator * (b).denominator"},
greatest_denom: {:bigint, "GCD(numerator, denominator)"}
],
returns: :rational,
body: """
#{Fragments.sql_inline_simplify(:numerator, :denominator, :greatest_denom)}
RETURN (numerator, denominator);
"""
)
end
@doc section: :migrations_private_functions
@doc """
Creates `rational.__private__sub(a, b)` backing function for the `-` operator
between two rationals.
"""
@spec create_func_sub() :: migration_info()
def create_func_sub do
PgTypeMigration.create_plpgsql_function(
private_function(:sub, Migration.repo()),
args: [a: :rational, b: :rational],
declares: [
numerator: {:bigint, "((a).numerator * (b).denominator) - ((b).numerator * (a).denominator)"},
denominator: {:bigint, "(a).denominator * (b).denominator"},
greatest_denom: {:bigint, "GCD(numerator, denominator)"}
],
returns: :rational,
body: """
#{Fragments.sql_inline_simplify(:numerator, :denominator, :greatest_denom)}
RETURN (numerator, denominator);
"""
)
end
@doc section: :migrations_private_functions
@doc """
Creates `rational.__private__mult(a, b)` backing function for the `*` operator
between two rationals.
"""
@spec create_func_mult() :: migration_info()
def create_func_mult do
PgTypeMigration.create_plpgsql_function(
private_function(:mult, Migration.repo()),
args: [a: :rational, b: :rational],
declares: [
numerator: {:bigint, "(a).numerator * (b).numerator"},
denominator: {:bigint, "(a).denominator * (b).denominator"},
greatest_denom: {:bigint, "GCD(numerator, denominator)"}
],
returns: :rational,
body: """
#{Fragments.sql_inline_simplify(:numerator, :denominator, :greatest_denom)}
RETURN (numerator, denominator);
"""
)
end
@doc section: :migrations_private_functions
@doc """
Creates `rational.__private__div(a, b)` backing function for the `/` operator
between two rationals.
"""
@spec create_func_div() :: migration_info()
def create_func_div do
PgTypeMigration.create_plpgsql_function(
private_function(:div, Migration.repo()),
args: [a: :rational, b: :rational],
declares: [
numerator: {:bigint, "(a).numerator * (b).denominator"},
denominator: {:bigint, "(a).denominator * (b).numerator"},
greatest_denom: {:bigint, "GCD(numerator, denominator)"}
],
returns: :rational,
body: """
#{Fragments.sql_inline_simplify(:numerator, :denominator, :greatest_denom)}
RETURN (numerator, denominator);
"""
)
end
@doc section: :migrations_private_functions
@doc """
Creates `DIV(a, b)` function, which executed integer floor division on rational
values.
Just like `DIV(real, real)`, `DIV(rational, rational)` floors towards zero.
"""
@spec create_func_floor_div() :: migration_info()
def create_func_floor_div do
PgTypeMigration.create_plpgsql_function(
"DIV",
args: [a: :rational, b: :rational],
declares: [
numerator: {:bigint, "(a).numerator * (b).denominator"},
denominator: {:bigint, "(a).denominator * (b).numerator"}
],
returns: :bigint,
body: """
RETURN numerator / denominator;
"""
)
end
@doc section: :migrations_private_functions
@doc """
Creates `rational.__private__modulo(a, b)` backing function for the `%` operator
between two rationals.
"""
@spec create_func_modulo() :: migration_info()
def create_func_modulo do
PgTypeMigration.create_plpgsql_function(
private_function(:modulo, Migration.repo()),
args: [dividend: :rational, divisor: :rational],
declares: [
numerator:
{:bigint,
"""
((dividend).numerator * (divisor).denominator)
% ((divisor).numerator * (dividend).denominator)
"""},
denominator: {:bigint, "(dividend).denominator * (divisor).denominator"},
greatest_denom: {:bigint, "GCD(numerator, denominator)"}
],
returns: :rational,
body: """
#{Fragments.sql_inline_simplify(:numerator, :denominator, :greatest_denom)}
RETURN (numerator, denominator);
"""
)
end
## COMPARISON BACKING FUNCS
@doc section: :migrations_private_functions
@doc """
Creates `rational.__private__cmp(a, b)` that returns:
- `1` if a > b
- `0` if a == b
- `-1` if a < b
Used to back equality operators.
"""
@spec create_func_cmp() :: migration_info()
def create_func_cmp do
PgTypeMigration.create_plpgsql_function(
private_function(:cmp, Migration.repo()),
args: [a: :rational, b: :rational],
declares: [
a_cmp: {:bigint, "((a).numerator * (b).denominator)"},
b_cmp: {:bigint, "((b).numerator * (a).denominator)"}
],
returns: :integer,
body: """
RETURN SIGN(a_cmp - b_cmp);
"""
)
end
@doc section: :migrations_private_functions
@doc """
Creates `rational.__private__eq(a, b)` that backs the `=` operator.
"""
@spec create_func_eq() :: migration_info()
def create_func_eq do
PgTypeMigration.create_plpgsql_function(
private_function(:eq, Migration.repo()),
args: [a: :rational, b: :rational],
declares: compare_declarations(),
returns: :boolean,
body: """
RETURN cmp_sign = 0;
"""
)
end
@doc section: :migrations_private_functions
@doc """
Creates `rational.__private__neq(a, b)` that backs the `<>` operator.
"""
@spec create_func_neq() :: migration_info()
def create_func_neq do
PgTypeMigration.create_plpgsql_function(
private_function(:neq, Migration.repo()),
args: [a: :rational, b: :rational],
declares: compare_declarations(),
returns: :boolean,
body: """
RETURN cmp_sign != 0;
"""
)
end
@doc section: :migrations_private_functions
@doc """
Creates `rational.__private__lt(a, b)` that backs the `<` operator.
"""
@spec create_func_lt() :: migration_info()
def create_func_lt do
PgTypeMigration.create_plpgsql_function(
private_function(:lt, Migration.repo()),
args: [a: :rational, b: :rational],
declares: compare_declarations(),
returns: :boolean,
body: """
RETURN cmp_sign = -1;
"""
)
end
@doc section: :migrations_private_functions
@doc """
Creates `rational.__private__lte(a, b)` that backs the `<=` operator.
"""
@spec create_func_lte() :: migration_info()
def create_func_lte do
PgTypeMigration.create_plpgsql_function(
private_function(:lte, Migration.repo()),
args: [a: :rational, b: :rational],
declares: compare_declarations(),
returns: :boolean,
body: """
RETURN cmp_sign = -1 or cmp_sign = 0;
"""
)
end
@doc section: :migrations_private_functions
@doc """
Creates `rational.__private__gt(a, b)` that backs the `>` operator.
"""
@spec create_func_gt() :: migration_info()
def create_func_gt do
PgTypeMigration.create_plpgsql_function(
private_function(:gt, Migration.repo()),
args: [a: :rational, b: :rational],
declares: compare_declarations(),
returns: :boolean,
body: """
RETURN cmp_sign = 1;
"""
)
end
@doc section: :migrations_private_functions
@doc """
Creates `rational.__private__gte(a, b)` that backs the `>=` operator.
"""
@spec create_func_gte() :: migration_info()
def create_func_gte do
PgTypeMigration.create_plpgsql_function(
private_function(:gte, Migration.repo()),
args: [a: :rational, b: :rational],
declares: compare_declarations(),
returns: :boolean,
body: """
RETURN cmp_sign = 1 or cmp_sign = 0;
"""
)
end
## ARITHMETIC OPS
@doc section: :migrations_operators
@doc """
Creates a custom unary :rational `@` unary operator.
Returns the absolute value of the input.
"""
@spec create_op_abs() :: migration_info()
def create_op_abs do
PgTypeMigration.create_operator(
:@,
nil,
:rational,
"ABS"
)
end
@doc section: :migrations_operators
@doc """
Creates a custom unary :rational `-` operator.
Flips the sign of `value`. Equivalent to `value * -1`.
"""
@spec create_op_minus() :: migration_info()
def create_op_minus do
PgTypeMigration.create_operator(
:-,
nil,
:rational,
private_function(:minus, Migration.repo())
)
end
@doc section: :migrations_operators
@doc """
Creates a custom :rational, :rational `+` operator.
"""
@spec create_op_add() :: migration_info()
def create_op_add do
PgTypeMigration.create_operator(
:+,
:rational,
:rational,
private_function(:add, Migration.repo()),
commutator: :+
)
end
@doc section: :migrations_operators
@doc """
Creates a custom :rational, :rational `-` operator.
"""
@spec create_op_sub() :: migration_info()
def create_op_sub do
PgTypeMigration.create_operator(
:-,
:rational,
:rational,
private_function(:sub, Migration.repo())
)
end
@doc section: :migrations_operators
@doc """
Creates a custom :rational, :rational `*` operator.
"""
@spec create_op_mult() :: migration_info()
def create_op_mult do
PgTypeMigration.create_operator(
:*,
:rational,
:rational,
private_function(:mult, Migration.repo()),
commutator: :*
)
end
@doc section: :migrations_operators
@doc """
Creates a custom :rational, :rational `/` operator.
"""
@spec create_op_div() :: migration_info()
def create_op_div do
PgTypeMigration.create_operator(
:/,
:rational,
:rational,
private_function(:div, Migration.repo())
)
end
@doc section: :migrations_operators
@doc """
Creates a custom :rational, :rational `%` operator.
"""
@spec create_op_modulo() :: migration_info()
def create_op_modulo do
PgTypeMigration.create_operator(
:%,
:rational,
:rational,
private_function(:modulo, Migration.repo())
)
end
## COMPARISON OPS
@doc section: :migrations_operators
@doc """
Creates a custom :rational, :rational `=` operator.
"""
@spec create_op_eq() :: migration_info()
def create_op_eq do
PgTypeMigration.create_operator(
:=,
:rational,
:rational,
private_function(:eq, Migration.repo()),
commutator: :=,
negator: :<>
)
end
@doc section: :migrations_operators
@doc """
Creates a custom :rational, :rational `<>` operator.
"""
@spec create_op_neq() :: migration_info()
def create_op_neq do
PgTypeMigration.create_operator(
:<>,
:rational,
:rational,
private_function(:neq, Migration.repo()),
commutator: :<>,
negator: :=
)
end
@doc section: :migrations_operators
@doc """
Creates a custom :rational, :rational `<` operator.
"""
@spec create_op_lt() :: migration_info()
def create_op_lt do
PgTypeMigration.create_operator(
:<,
:rational,
:rational,
private_function(:lt, Migration.repo()),
commutator: :>,
negator: :>=
)
end
@doc section: :migrations_operators
@doc """
Creates a custom :rational, :rational `<` operator.
"""
@spec create_op_lte() :: migration_info()
def create_op_lte do
PgTypeMigration.create_operator(
:<=,
:rational,
:rational,
private_function(:lte, Migration.repo()),
commutator: :>=,
negator: :>
)
end
@doc section: :migrations_operators
@doc """
Creates a custom :rational, :rational `<` operator.
"""
@spec create_op_gt() :: migration_info()
def create_op_gt do
PgTypeMigration.create_operator(
:>,
:rational,
:rational,
private_function(:gt, Migration.repo()),
commutator: :<,
negator: :<=
)
end
@doc section: :migrations_operators
@doc """
Creates a custom :rational, :rational `<` operator.
"""
@spec create_op_gte() :: migration_info()
def create_op_gte do
PgTypeMigration.create_operator(
:>=,
:rational,
:rational,
private_function(:gte, Migration.repo()),
commutator: :<=,
negator: :<
)
end
## OPERATOR CLASSES
@doc section: :migrations_operator_classes
@doc """
Creates a B-tree operator class to support indexing on comparison operations.
"""
@spec create_op_class_btree() :: migration_info()
def create_op_class_btree do
PgTypeMigration.create_operator_class(
:rational_ops_btree,
:rational,
:btree,
[
<: 1,
<=: 2,
=: 3,
>=: 4,
>: 5
],
[
{private_function(:cmp, Migration.repo()), 1}
]
)
end
## CASTS
@doc section: :migrations_casts
@doc """
Creates a native cast for:
```sql
rational AS double precision
```
"""
@spec create_cast_double_precision() :: migration_info()
def create_cast_double_precision do
PgTypeMigration.create_cast(
:rational,
:"double precision",
private_function(:cast_to_double, Migration.repo())
)
end
@doc section: :migrations_casts
@doc """
Creates a native cast for:
```sql
bigint AS rational
```
"""
@spec create_cast_bigint_to_rational() :: migration_info()
def create_cast_bigint_to_rational do
PgTypeMigration.create_cast(
:bigint,
:rational,
private_function(:cast_bigint_to_rational, Migration.repo()),
implicit: true
)
end
@doc section: :migrations_constraints
@doc """
Creates basic constraints for a `PgRational` database field.
## Constraints created:
- `{field_name}_denominator_positive`: Checks that the denominator of the field is
positive.
## Examples
```elixir
create table("rationals", primary_key: false) do
add(:id, :uuid, primary_key: true, null: false)
add(:a, PgRational.type())
add(:b, PgRational.type())
end
PgRational.migration_add_field_constraints(:rationals, :a)
PgRational.migration_add_field_constraints(:rationals, :b)
```
"""
@spec create_constraints(atom(), atom()) :: :ok
def create_constraints(table, field_name) do
sql_field = "#{table}.#{field_name}"
constraint =
Migration.constraint(
table,
"#{field_name}_denominator_positive",
check: """
(#{sql_field}).denominator > 0
"""
)
%Constraint{} = Migration.create(constraint)
:ok
end
# Returns declaration list for comparison operators.
@spec compare_declarations() :: PgTypeMigration.function_declarations()
defp compare_declarations do
[
a_cmp: {:bigint, "((a).numerator * (b).denominator)"},
b_cmp: {:bigint, "((b).numerator * (a).denominator)"},
cmp_sign: {:bigint, "SIGN(a_cmp - b_cmp)"}
]
end
end