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. """ alias Ecto.Migration alias Vtc.Ecto.Postgres 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 included all migraitons under the [PgTypes](Vtc.Ecto.Postgres.PgRational.Migrations.html#pgtypes) and [PgFunctions](Vtc.Ecto.Postgres.PgRational.Migrations.html#pgfunctions) headings. Safe to run multiple times when new functionality is added in updates to this library. Existing values will be skipped. ## 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 confiugration: ```elixir config :vtc, Vtc.Test.Support.Repo, adapter: Ecto.Adapters.Postgres, ... vtc: [ pg_rational: [ functions_schema: :rational, functions_private_schema: :rational_private, functions_prefix: "rational" ] ] ``` Option definitions are as follows: - `functions_schema`: The schema for "public" functions that will have backwards compatibility guarantees and application code support. Default: `:public`. - `functions_private_schema:` The schema for for developer-only "private" functions that support the functions in the "rational" schema. Will NOT havr backwards compatibility guarantees NOR application code support. Default: `:public`. - `functions_prefix`: A prefix to add before all functions. Defaults to "rational" for any function created in the "public" schema, and "" otherwise. ## Functions Created See [PgFunctions](Vtc.Ecto.Postgres.PgRational.Migrations.html#pgfunctions) section of these docs for details on native database functions created. ## Operators Created See [PgOperators](Vtc.Ecto.Postgres.PgRational.Migrations.html#pgoperators) section of these docs for details on native database operators created. ## Casts Created See [PgCasts](Vtc.Ecto.Postgres.PgRational.Migrations.html#pgcasts) section of these docs for details on native database operators created. ## Examples ```elixir defmodule MyMigration do use Ecto.Migration alias Vtc.Ecto.Postgres.PgRational require PgRational.Migrations def change do PgRational.Migrations.create_all() end end ``` """ @spec create_all() :: :ok def create_all do :ok = create_type() :ok = create_function_schemas() :ok = create_func_greatest_common_denominator() :ok = create_func_simplify() :ok = create_func_minus() :ok = create_func_round() :ok = create_func_add() :ok = create_func_sub() :ok = create_func_mult() :ok = create_func_div() :ok = create_op_add() :ok = create_op_sub() :ok = create_op_mult() :ok = create_op_div() :ok = create_func_cmp() :ok = create_func_eq() :ok = create_func_neq() :ok = create_func_lt() :ok = create_func_lte() :ok = create_func_gt() :ok = create_func_gte() :ok = create_op_eq() :ok = create_op_neq() :ok = create_op_neq2() :ok = create_op_lt() :ok = create_op_lte() :ok = create_op_gt() :ok = create_op_gte() :ok = create_op_class_btree() :ok = create_func_cast_to_double_precison() :ok = create_cast_double_precision() :ok end @doc section: :migrations_types @doc """ Adds: - `rational` composite type - `rationals` schema - `rationals_helpers` schema """ @spec create_type() :: :ok def create_type do :ok = Migration.execute(""" DO $$ BEGIN CREATE TYPE rational AS ( numerator bigint, denominator bigint ); EXCEPTION WHEN duplicate_object THEN null; END $$; """) :ok end @doc section: :migrations_types @doc """ Creates schemas to act as namespaces for timecode functions based on your repo's settings. """ @spec create_function_schemas() :: :ok def create_function_schemas do functions_schema = get_config(Migration.repo(), :functions_schema, :public) if functions_schema != :public do :ok = Migration.execute(""" DO $$ BEGIN CREATE SCHEMA #{functions_schema}; EXCEPTION WHEN duplicate_schema THEN null; END $$; """) end functions_private_schema = get_config(Migration.repo(), :functions_private_schema, :public) if functions_private_schema != :public do :ok = Migration.execute(""" DO $$ BEGIN CREATE SCHEMA #{functions_private_schema}; EXCEPTION WHEN duplicate_schema THEN null; END $$; """) end :ok end @doc section: :migrations_functions @doc """ Creates `rational_private.greatest_common_denominator(a, b)` function that finds the greatest common denominator between two bigint values. """ @spec create_func_greatest_common_denominator() :: :ok def create_func_greatest_common_denominator do create_func = Postgres.Utils.create_plpgsql_function( :"#{private_function_prefix(Migration.repo())}greatest_common_denominator", args: [a: :bigint, b: :bigint], returns: :bigint, declares: [result: :bigint], body: """ SELECT ( CASE WHEN b = 0 THEN ABS(a) WHEN a = 0 THEN ABS(b) ELSE #{private_function_prefix(Migration.repo())}greatest_common_denominator(b, a % b) END ) INTO result; RETURN result; """ ) :ok = Migration.execute(create_func) :ok end @doc section: :migrations_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() :: :ok def create_func_simplify do create_func = Postgres.Utils.create_plpgsql_function( :"#{private_function_prefix(Migration.repo())}simplify", args: [input: :rational], returns: :rational, declares: [ gcd: {:bigint, "#{private_function_prefix(Migration.repo())}greatest_common_denominator(input.numerator, input.denominator)"}, denominator: {:bigint, "ABS(input.denominator / gcd)"}, numerator: {:bigint, "input.numerator / gcd"} ], body: """ SELECT ( CASE WHEN input.denominator < 0 THEN numerator * -1 ELSE numerator END ) INTO numerator; RETURN (numerator, denominator)::rational; """ ) :ok = Migration.execute(create_func) :ok end @doc section: :migrations_functions @doc """ Creates `rational.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() :: :ok def create_func_minus do create_func = Postgres.Utils.create_plpgsql_function( :"#{function_prefix(Migration.repo())}minus", args: [input: :rational], returns: :rational, body: """ RETURN ((input).numerator * -1, (input).denominator)::rational; """ ) :ok = Migration.execute(create_func) :ok end @doc section: :migrations_functions @doc """ Creates `rational.round(rat)` function that returns the rational input, rounded to the nearest :bigint """ @spec create_func_round() :: :ok def create_func_round do create_func = Postgres.Utils.create_plpgsql_function( :"#{function_prefix(Migration.repo())}round", args: [input: :rational], declares: [result: :bigint], returns: :bigint, body: """ SELECT ( CASE WHEN (input).numerator < 0 THEN #{function_prefix(Migration.repo())}round( #{function_prefix(Migration.repo())}minus(input) ) * -1 WHEN (((input).numerator % (input).denominator) * 2) < (input).denominator THEN (input).numerator / (input).denominator ELSE ((input).numerator / (input).denominator) + 1 END ) INTO result; RETURN result; """ ) :ok = Migration.execute(create_func) :ok end @doc section: :migrations_functions @doc """ Creates a native CAST from `rational` to `double precision`. """ @spec create_func_cast_to_double_precison() :: :ok def create_func_cast_to_double_precison do create_func = Postgres.Utils.create_plpgsql_function( :"#{private_function_prefix(Migration.repo())}cast_to_float", args: [value: :rational], returns: :"double precision", body: """ RETURN CAST ((value).numerator AS double precision) / CAST ((value).denominator AS double precision); """ ) :ok = Migration.execute(create_func) :ok end ## ARITHMATIC BACKING FUNCS @doc section: :migrations_functions @doc """ Creates Creates `rational_private.add(a, b)` backing function for the `+` operator between two rationals. """ @spec create_func_add() :: :ok def create_func_add do create_func = Postgres.Utils.create_plpgsql_function( :"#{private_function_prefix(Migration.repo())}add", args: [a: :rational, b: :rational], declares: [ numerator: {:bigint, "((a).numerator * (b).denominator) + ((b).numerator * (a).denominator)"}, denominator: {:bigint, "(a).denominator * (b).denominator"}, result: {:rational, "#{private_function_prefix(Migration.repo())}simplify((numerator, denominator))"} ], returns: :rational, body: """ RETURN result; """ ) :ok = Migration.execute(create_func) :ok end @doc section: :migrations_functions @doc """ Creates Creates `rational_private.sub(a, b)` backing function for the `-` operator between two rationals. """ @spec create_func_sub() :: :ok def create_func_sub do create_func = Postgres.Utils.create_plpgsql_function( :"#{private_function_prefix(Migration.repo())}sub", args: [a: :rational, b: :rational], declares: [ b_numerator: {:bigint, "(b).numerator * -1"}, b_negated: {:rational, "(b_numerator, (b).denominator)"}, result: {:rational, "#{private_function_prefix(Migration.repo())}add(a, b_negated)"} ], returns: :rational, body: """ RETURN result; """ ) :ok = Migration.execute(create_func) :ok end @doc section: :migrations_functions @doc """ Creates Creates `rational_private.mult(a, b)` backing function for the `*` operator between two rationals. """ @spec create_func_mult() :: :ok def create_func_mult do create_func = Postgres.Utils.create_plpgsql_function( :"#{private_function_prefix(Migration.repo())}mult", args: [a: :rational, b: :rational], declares: [ numerator: {:bigint, "(a).numerator * (b).numerator"}, denominator: {:bigint, "(a).denominator * (b).denominator"}, result: {:rational, "#{private_function_prefix(Migration.repo())}simplify((numerator, denominator))"} ], returns: :rational, body: """ RETURN result; """ ) :ok = Migration.execute(create_func) :ok end @doc section: :migrations_functions @doc """ Creates Creates `rational_private.div(a, b)` backing function for the `/` operator between two rationals. """ @spec create_func_div() :: :ok def create_func_div do create_func = Postgres.Utils.create_plpgsql_function( :"#{private_function_prefix(Migration.repo())}div", args: [a: :rational, b: :rational], declares: [ numerator: {:bigint, "(a).numerator * (b).denominator"}, denominator: {:bigint, "(a).denominator * (b).numerator"}, result: {:rational, "#{private_function_prefix(Migration.repo())}simplify((numerator, denominator))"} ], returns: :rational, body: """ RETURN result; """ ) :ok = Migration.execute(create_func) :ok end ## COMPARISON BACKING FUNCS @doc section: :migrations_functions @doc """ Creates 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() :: :ok def create_func_cmp do create_func = Postgres.Utils.create_plpgsql_function( :"#{private_function_prefix(Migration.repo())}cmp", args: [a: :rational, b: :rational], declares: [ a_cmp: {:bigint, "((a).numerator * (b).denominator)"}, b_cmp: {:bigint, "((b).numerator * (a).denominator)"}, result: :integer ], returns: :integer, body: """ SELECT ( CASE WHEN a_cmp > b_cmp THEN 1 WHEN a_cmp < b_cmp THEN -1 ELSE 0 END ) INTO result; RETURN result; """ ) :ok = Migration.execute(create_func) :ok end @doc section: :migrations_functions @doc """ Creates Creates `rational_private.eq(a, b)` that backs the `=` operator. """ @spec create_func_eq() :: :ok def create_func_eq do create_func = Postgres.Utils.create_plpgsql_function( :"#{private_function_prefix(Migration.repo())}eq", args: [a: :rational, b: :rational], returns: :boolean, body: """ RETURN #{private_function_prefix(Migration.repo())}cmp(a, b) = 0; """ ) :ok = Migration.execute(create_func) :ok end @doc section: :migrations_functions @doc """ Creates Creates `rational_private.eq(a, b)` that backs the `=` operator. """ @spec create_func_neq() :: :ok def create_func_neq do create_func = Postgres.Utils.create_plpgsql_function( :"#{private_function_prefix(Migration.repo())}neq", args: [a: :rational, b: :rational], returns: :boolean, body: """ RETURN #{private_function_prefix(Migration.repo())}cmp(a, b) != 0; """ ) :ok = Migration.execute(create_func) :ok end @doc section: :migrations_functions @doc """ Creates Creates `rational_private.eq(a, b)` that backs the `=` operator. """ @spec create_func_lt() :: :ok def create_func_lt do create_func = Postgres.Utils.create_plpgsql_function( :"#{private_function_prefix(Migration.repo())}lt", args: [a: :rational, b: :rational], returns: :boolean, body: """ RETURN #{private_function_prefix(Migration.repo())}cmp(a, b) = -1; """ ) :ok = Migration.execute(create_func) :ok end @doc section: :migrations_functions @doc """ Creates Creates `rational_private.eq(a, b)` that backs the `=` operator. """ @spec create_func_lte() :: :ok def create_func_lte do create_func = Postgres.Utils.create_plpgsql_function( :"#{private_function_prefix(Migration.repo())}lte", args: [a: :rational, b: :rational], declares: [ cmp: {:integer, "#{private_function_prefix(Migration.repo())}cmp(a, b)"}, cmp_array: :"integer[]" ], returns: :boolean, body: """ cmp_array = ARRAY_APPEND(cmp_array, cmp); RETURN cmp_array <@ '{-1, 0}'; """ ) :ok = Migration.execute(create_func) :ok end @doc section: :migrations_functions @doc """ Creates Creates `rational_private.eq(a, b)` that backs the `=` operator. """ @spec create_func_gt() :: :ok def create_func_gt do create_func = Postgres.Utils.create_plpgsql_function( :"#{private_function_prefix(Migration.repo())}gt", args: [a: :rational, b: :rational], returns: :boolean, body: """ RETURN #{private_function_prefix(Migration.repo())}cmp(a, b) = 1; """ ) :ok = Migration.execute(create_func) :ok end @doc section: :migrations_functions @doc """ Creates Creates `rational_private.eq(a, b)` that backs the `=` operator. """ @spec create_func_gte() :: :ok def create_func_gte do create_func = Postgres.Utils.create_plpgsql_function( :"#{private_function_prefix(Migration.repo())}gte", args: [a: :rational, b: :rational], declares: [ cmp: {:integer, "#{private_function_prefix(Migration.repo())}cmp(a, b)"}, cmp_array: :"integer[]" ], returns: :boolean, body: """ cmp_array = ARRAY_APPEND(cmp_array, cmp); RETURN cmp_array <@ '{1, 0}'; """ ) :ok = Migration.execute(create_func) :ok end ## ARITHMATIC OPS @doc section: :migrations_operators @doc """ Creates a custom :rational, :rational `+` operator. """ @spec create_op_add() :: :ok def create_op_add do create_op = Postgres.Utils.create_operator( :+, :rational, :rational, :"#{private_function_prefix(Migration.repo())}add", commutator: :+ ) :ok = Migration.execute(create_op) :ok end @doc section: :migrations_operators @doc """ Creates a custom :rational, :rational `-` operator. """ @spec create_op_sub() :: :ok def create_op_sub do create_op = Postgres.Utils.create_operator( :-, :rational, :rational, :"#{private_function_prefix(Migration.repo())}sub" ) :ok = Migration.execute(create_op) :ok end @doc section: :migrations_operators @doc """ Creates a custom :rational, :rational `*` operator. """ @spec create_op_mult() :: :ok def create_op_mult do create_op = Postgres.Utils.create_operator( :*, :rational, :rational, :"#{private_function_prefix(Migration.repo())}mult", commutator: :* ) :ok = Migration.execute(create_op) :ok end @doc section: :migrations_operators @doc """ Creates a custom :rational, :rational `/` operator. """ @spec create_op_div() :: :ok def create_op_div do create_op = Postgres.Utils.create_operator( :/, :rational, :rational, :"#{private_function_prefix(Migration.repo())}div" ) :ok = Migration.execute(create_op) :ok end ## COMPARISON OPS @doc section: :migrations_operators @doc """ Creates a custom :rational, :rational `=` operator. """ @spec create_op_eq() :: :ok def create_op_eq do create_op = Postgres.Utils.create_operator( :=, :rational, :rational, :"#{private_function_prefix(Migration.repo())}eq", commutator: :=, negator: :<> ) :ok = Migration.execute(create_op) :ok end @doc section: :migrations_operators @doc """ Creates a custom :rational, :rational `<>` operator. """ @spec create_op_neq() :: :ok def create_op_neq do create_op = Postgres.Utils.create_operator( :<>, :rational, :rational, :"#{private_function_prefix(Migration.repo())}neq", commutator: :<>, negator: := ) :ok = Migration.execute(create_op) :ok end @doc section: :migrations_operators @doc """ Creates a custom :rational, :rational `!=` operator. """ @spec create_op_neq2() :: :ok def create_op_neq2 do create_op = Postgres.Utils.create_operator( :!=, :rational, :rational, :"#{private_function_prefix(Migration.repo())}neq", commutator: :!=, negator: := ) :ok = Migration.execute(create_op) :ok end @doc section: :migrations_operators @doc """ Creates a custom :rational, :rational `<` operator. """ @spec create_op_lt() :: :ok def create_op_lt do create_op = Postgres.Utils.create_operator( :<, :rational, :rational, :"#{private_function_prefix(Migration.repo())}lt", commutator: :>, negator: :>= ) :ok = Migration.execute(create_op) :ok end @doc section: :migrations_operators @doc """ Creates a custom :rational, :rational `<` operator. """ @spec create_op_lte() :: :ok def create_op_lte do create_op = Postgres.Utils.create_operator( :<=, :rational, :rational, :"#{private_function_prefix(Migration.repo())}lte", commutator: :>=, negator: :> ) :ok = Migration.execute(create_op) :ok end @doc section: :migrations_operators @doc """ Creates a custom :rational, :rational `<` operator. """ @spec create_op_gt() :: :ok def create_op_gt do create_op = Postgres.Utils.create_operator( :>, :rational, :rational, :"#{private_function_prefix(Migration.repo())}gt", commutator: :<, negator: :<= ) :ok = Migration.execute(create_op) :ok end @doc section: :migrations_operators @doc """ Creates a custom :rational, :rational `<` operator. """ @spec create_op_gte() :: :ok def create_op_gte do create_op = Postgres.Utils.create_operator( :>=, :rational, :rational, :"#{private_function_prefix(Migration.repo())}gte", commutator: :<=, negator: :< ) :ok = Migration.execute(create_op) :ok end ## OPERATOR CLASSES @spec create_op_class_btree() :: :ok def create_op_class_btree do create_class = Postgres.Utils.create_operator_class( :rational_ops_btree, :rational, :btree, [ <: 1, <=: 2, =: 3, >=: 4, >: 5 ], [ {:"#{private_function_prefix(Migration.repo())}cmp", 1} ] ) :ok = Migration.execute(create_class) :ok end ## CASTS @doc section: :migrations_casts @doc """ Creates a native cast for: ```sql rational AS double precision ``` """ @spec create_cast_double_precision() :: :ok def create_cast_double_precision do create_cast = Postgres.Utils.create_cast( :rational, :"double precision", :"#{private_function_prefix(Migration.repo())}cast_to_float" ) :ok = Migration.execute(create_cast) :ok 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_field_constraints(atom(), atom()) :: :ok def create_field_constraints(table, field_name) do Migration.create( Migration.constraint( table, "#{field_name}_denominator_positive", check: """ (#{field_name}).denominator > 0 """ ) ) :ok end # Fetches PgRational configuration option from `repo`'s configuration. @spec get_config(Ecto.Repo.t(), atom(), Keyword.value()) :: Keyword.value() defp get_config(repo, opt, default), do: repo.config() |> Keyword.get(:vtc, []) |> Keyword.get(:pg_rational) |> Keyword.get(opt, default) @spec private_function_prefix(Ecto.Repo.t()) :: String.t() defp private_function_prefix(repo) do functions_private_schema = get_config(repo, :functions_private_schema, :public) functions_prefix = get_config(repo, :functions_prefix, "") functions_prefix = if functions_prefix == "" and functions_private_schema == :public, do: "rational", else: functions_prefix functions_prefix = if functions_prefix == "", do: "", else: "#{functions_prefix}_" "#{functions_private_schema}.#{functions_prefix}" end @spec function_prefix(Ecto.Repo.t()) :: String.t() defp function_prefix(repo) do functions_schema = get_config(repo, :functions_schema, :public) functions_prefix = get_config(repo, :functions_prefix, "") functions_prefix = if functions_prefix == "" and functions_schema == :public, do: "rational", else: functions_prefix functions_prefix = if functions_prefix == "", do: "", else: "#{functions_prefix}_" "#{functions_schema}.#{functions_prefix}" end end