improvement: add comparison operators to the extension

improvement: add more operator overloads
This commit is contained in:
Zach Daniel 2024-06-18 16:44:18 -04:00
parent 019f1802f3
commit 5c6ea286df
4 changed files with 412 additions and 4 deletions

View file

@ -3,10 +3,23 @@ if Code.ensure_loaded?(AshPostgres.CustomExtension) do
@moduledoc """
Installs the `money_with_currency` type and operators/functions for Postgres.
"""
use AshPostgres.CustomExtension, name: :ash_money, latest_version: 3
use AshPostgres.CustomExtension, name: :ash_money, latest_version: 4
def install(3) do
"""
#{Money.DDL.execute_each(add_money_greater_than())}
#{Money.DDL.execute_each(add_money_greater_than_or_equal())}
#{Money.DDL.execute_each(add_money_less_than())}
#{Money.DDL.execute_each(add_money_less_than_or_equal())}
"""
end
def install(2) do
"""
#{Money.DDL.execute_each(add_money_greater_than())}
#{Money.DDL.execute_each(add_money_greater_than_or_equal())}
#{Money.DDL.execute_each(add_money_less_than())}
#{Money.DDL.execute_each(add_money_less_than_or_equal())}
#{Money.DDL.execute_each(add_money_sub())}
#{Money.DDL.execute_each(add_money_neg())}
"""
@ -14,6 +27,10 @@ if Code.ensure_loaded?(AshPostgres.CustomExtension) do
def install(1) do
"""
#{Money.DDL.execute_each(add_money_greater_than())}
#{Money.DDL.execute_each(add_money_greater_than_or_equal())}
#{Money.DDL.execute_each(add_money_less_than())}
#{Money.DDL.execute_each(add_money_less_than_or_equal())}
#{Money.DDL.execute_each(add_money_sub())}
#{Money.DDL.execute_each(add_money_mult())}
#{Money.DDL.execute_each(add_money_neg())}
@ -22,6 +39,10 @@ if Code.ensure_loaded?(AshPostgres.CustomExtension) do
def install(0) do
"""
#{Money.DDL.execute_each(add_money_greater_than())}
#{Money.DDL.execute_each(add_money_greater_than_or_equal())}
#{Money.DDL.execute_each(add_money_less_than())}
#{Money.DDL.execute_each(add_money_less_than_or_equal())}
#{Money.DDL.execute_each(Money.DDL.create_money_with_currency())}
#{Money.DDL.execute_each(add_money_sub())}
#{Money.DDL.execute_each(add_money_neg())}
@ -32,6 +53,16 @@ if Code.ensure_loaded?(AshPostgres.CustomExtension) do
"""
end
def uninstall(4) do
"""
#{Money.DDL.execute_each(remove_money_greater_than())}
#{Money.DDL.execute_each(remove_money_greater_than_or_equal())}
#{Money.DDL.execute_each(remove_money_less_than())}
#{Money.DDL.execute_each(remove_money_less_than_or_equal())}
#{uninstall(3)}
"""
end
def uninstall(3) do
"""
#{Money.DDL.execute_each(remove_money_sub())}
@ -50,6 +81,298 @@ if Code.ensure_loaded?(AshPostgres.CustomExtension) do
"""
end
defp add_money_greater_than do
"""
CREATE OR REPLACE FUNCTION money_gt(money_1 money_with_currency, money_2 money_with_currency)
RETURNS BOOLEAN
IMMUTABLE
STRICT
LANGUAGE plpgsql
AS $$
DECLARE
currency varchar;
result boolean;
BEGIN
IF currency_code(money_1) = currency_code(money_2) THEN
currency := currency_code(money_1);
result := amount(money_1) > amount(money_2);
return result;
ELSE
RAISE EXCEPTION
'Incompatible currency codes for > operator. Expected both currency codes to be %', currency_code(money_1)
USING HINT = 'Please ensure both columns have the same currency code',
ERRCODE = '22033';
END IF;
END;
$$;
CREATE OR REPLACE FUNCTION money_gt(money_1 money_with_currency, amount numeric)
RETURNS BOOLEAN
IMMUTABLE
STRICT
LANGUAGE plpgsql
AS $$
DECLARE
currency varchar;
result boolean;
BEGIN
currency := currency_code(money_1);
result := amount(money_1) > amount;
return result;
END;
$$;
CREATE OPERATOR > (
leftarg = money_with_currency,
rightarg = money_with_currency,
procedure = money_gt
);
CREATE OPERATOR > (
leftarg = money_with_currency,
rightarg = numeric,
procedure = money_gt
);
"""
end
defp add_money_greater_than_or_equal do
"""
CREATE OR REPLACE FUNCTION money_gte(money_1 money_with_currency, money_2 money_with_currency)
RETURNS BOOLEAN
IMMUTABLE
STRICT
LANGUAGE plpgsql
AS $$
DECLARE
currency varchar;
result boolean;
BEGIN
IF currency_code(money_1) = currency_code(money_2) THEN
currency := currency_code(money_1);
result := amount(money_1) >= amount(money_2);
return result;
ELSE
RAISE EXCEPTION
'Incompatible currency codes for >= operator. Expected both currency codes to be %', currency_code(money_1)
USING HINT = 'Please ensure both columns have the same currency code',
ERRCODE = '22033';
END IF;
END;
$$;
CREATE OR REPLACE FUNCTION money_gte(money_1 money_with_currency, amount numeric)
RETURNS BOOLEAN
IMMUTABLE
STRICT
LANGUAGE plpgsql
AS $$
DECLARE
currency varchar;
result boolean;
BEGIN
currency := currency_code(money_1);
result := amount(money_1) >= amount;
return result;
END;
$$;
CREATE OPERATOR >= (
leftarg = money_with_currency,
rightarg = money_with_currency,
procedure = money_gt
);
CREATE OPERATOR >= (
leftarg = money_with_currency,
rightarg = numeric,
procedure = money_gt
);
"""
end
defp add_money_less_than do
"""
CREATE OR REPLACE FUNCTION money_lt(money_1 money_with_currency, money_2 money_with_currency)
RETURNS BOOLEAN
IMMUTABLE
STRICT
LANGUAGE plpgsql
AS $$
DECLARE
currency varchar;
result boolean;
BEGIN
IF currency_code(money_1) = currency_code(money_2) THEN
currency := currency_code(money_1);
result := amount(money_1) < amount(money_2);
return result;
ELSE
RAISE EXCEPTION
'Incompatible currency codes for < operator. Expected both currency codes to be %', currency_code(money_1)
USING HINT = 'Please ensure both columns have the same currency code',
ERRCODE = '22033';
END IF;
END;
$$;
CREATE OR REPLACE FUNCTION money_lt(money_1 money_with_currency, amount numeric)
RETURNS BOOLEAN
IMMUTABLE
STRICT
LANGUAGE plpgsql
AS $$
DECLARE
currency varchar;
result boolean;
BEGIN
currency := currency_code(money_1);
result := amount(money_1) < amount;
return result;
END;
$$;
CREATE OPERATOR < (
leftarg = money_with_currency,
rightarg = money_with_currency,
procedure = money_lt
);
CREATE OPERATOR < (
leftarg = money_with_currency,
rightarg = numeric,
procedure = money_lt
);
"""
end
defp add_money_less_than_or_equal do
"""
CREATE OR REPLACE FUNCTION money_lte(money_1 money_with_currency, money_2 money_with_currency)
RETURNS BOOLEAN
IMMUTABLE
STRICT
LANGUAGE plpgsql
AS $$
DECLARE
currency varchar;
result boolean;
BEGIN
IF currency_code(money_1) = currency_code(money_2) THEN
currency := currency_code(money_1);
result := amount(money_1) <= amount(money_2);
return result;
ELSE
RAISE EXCEPTION
'Incompatible currency codes for <= operator. Expected both currency codes to be %', currency_code(money_1)
USING HINT = 'Please ensure both columns have the same currency code',
ERRCODE = '22033';
END IF;
END;
$$;
CREATE OR REPLACE FUNCTION money_lte(money_1 money_with_currency, amount numeric)
RETURNS BOOLEAN
IMMUTABLE
STRICT
LANGUAGE plpgsql
AS $$
DECLARE
currency varchar;
result boolean;
BEGIN
currency := currency_code(money_1);
result := amount(money_1) <= amount;
return result;
END;
$$;
CREATE OPERATOR <= (
leftarg = money_with_currency,
rightarg = money_with_currency,
procedure = money_lte
);
CREATE OPERATOR <= (
leftarg = money_with_currency,
rightarg = numeric,
procedure = money_lte
);
"""
end
defp remove_money_greater_than do
"""
DROP OPERATOR >(money_with_currency, money_with_currency);
DROP OPERATOR >(money_with_currency, numeric);
DROP FUNCTION IF EXISTS money_gt(money_1 money_with_currency, money_2 money_with_currency);
DROP FUNCTION IF EXISTS money_gt(money_1 money_with_currency, amount numeric);
"""
end
defp remove_money_greater_than_or_equal do
"""
DROP OPERATOR >=(money_with_currency, money_with_currency);
DROP OPERATOR >=(money_with_currency, numeric);
DROP FUNCTION IF EXISTS money_gte(money_1 money_with_currency, money_2 money_with_currency);
DROP FUNCTION IF EXISTS money_gte(money_1 money_with_currency, amount numeric);
"""
end
defp remove_money_less_than do
"""
DROP OPERATOR <(money_with_currency, money_with_currency);
DROP OPERATOR <(money_with_currency, numeric);
DROP FUNCTION IF EXISTS money_lt(money_1 money_with_currency, money_2 money_with_currency);
DROP FUNCTION IF EXISTS money_lt(money_1 money_with_currency, amount numeric);
"""
end
defp remove_money_less_than_or_equal do
"""
DROP OPERATOR <=(money_with_currency, money_with_currency);
DROP OPERATOR <=(money_with_currency, numeric);
DROP FUNCTION IF EXISTS money_lte(money_1 money_with_currency, money_2 money_with_currency);
DROP FUNCTION IF EXISTS money_lte(money_1 money_with_currency, amount numeric);
"""
end
defp add_money_neg do
"""
CREATE OR REPLACE FUNCTION money_neg(money_1 money_with_currency)

View file

@ -39,16 +39,101 @@ defmodule AshMoney.Types.Money do
[__MODULE__, __MODULE__] => __MODULE__
},
:- => %{
[__MODULE__, __MODULE__] => __MODULE__
[__MODULE__] => __MODULE__
},
:* => %{
[__MODULE__, :integer] => __MODULE__,
[__MODULE__, :decimal] => __MODULE__,
[:integer, __MODULE__] => __MODULE__,
[:decimal, __MODULE__] => __MODULE__
},
:< => %{
[__MODULE__, :integer] => __MODULE__,
[__MODULE__, :decimal] => __MODULE__,
[__MODULE__, __MODULE__] => __MODULE__,
[:decimal, __MODULE__] => __MODULE__,
[:integer, __MODULE__] => __MODULE__
},
:<= => %{
[__MODULE__, :integer] => __MODULE__,
[__MODULE__, :decimal] => __MODULE__,
[__MODULE__, __MODULE__] => __MODULE__,
[:decimal, __MODULE__] => __MODULE__,
[:integer, __MODULE__] => __MODULE__
},
:> => %{
[__MODULE__, :integer] => __MODULE__,
[__MODULE__, :decimal] => __MODULE__,
[__MODULE__, __MODULE__] => __MODULE__,
[:decimal, __MODULE__] => __MODULE__,
[:integer, __MODULE__] => __MODULE__
},
:>= => %{
[__MODULE__, :integer] => __MODULE__,
[__MODULE__, :decimal] => __MODULE__,
[__MODULE__, __MODULE__] => __MODULE__,
[:decimal, __MODULE__] => __MODULE__,
[:integer, __MODULE__] => __MODULE__
}
}
end
@impl true
def matches_type?(%Money{}, _), do: true
def matches_type?(_, _), do: false
@impl true
def evaluate_operator(%op{
left: %Money{} = left,
right: %Money{} = right
})
when op in [
Ash.Query.Operator.LessThan,
Ash.Query.Operator.GreaterThan,
Ash.Query.Operator.LessThanOrEqual,
Ash.Query.Operator.GreaterThanOrEqual
] do
requirement =
case op do
Ash.Query.Operator.LessThan -> [:lt]
Ash.Query.Operator.GreaterThan -> [:gt]
Ash.Query.Operator.LessThanOrEqual -> [:lt, :eq]
Ash.Query.Operator.GreaterThanOrEqual -> [:gt, :eq]
end
Money.compare!(left, right) in requirement
end
def evaluate_operator(
%op{
left: %Money{} = left,
right: right
} = operator
)
when op in [
Ash.Query.Operator.LessThan,
Ash.Query.Operator.GreaterThan,
Ash.Query.Operator.LessThanOrEqual,
Ash.Query.Operator.GreaterThanOrEqual
] do
evaluate_operator(%{operator | left: left, right: Money.new(left.currency, right)})
end
def evaluate_operator(
%op{
left: left,
right: %Money{} = right
} = operator
)
when op in [
Ash.Query.Operator.LessThan,
Ash.Query.Operator.GreaterThan,
Ash.Query.Operator.LessThanOrEqual,
Ash.Query.Operator.GreaterThanOrEqual
] do
evaluate_operator(%{operator | left: Money.new(right.currency, left), right: right})
end
def evaluate_operator(%Ash.Query.Operator.Basic.Plus{
left: %Money{} = left,
right: %Money{} = right

View file

@ -103,7 +103,7 @@ defmodule AshMoney.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:ash, ash_version("~> 3.0")},
{:ash, ash_version("~> 3.0 and >= 3.0.15")},
{:ex_money, "~> 5.15"},
{:ex_money_sql, "~> 1.0", optional: true},
{:ash_postgres, "~> 2.0", optional: true},

View file

@ -1,7 +1,7 @@
%{
"absinthe": {:hex, :absinthe, "1.7.6", "0b897365f98d068cfcb4533c0200a8e58825a4aeeae6ec33633ebed6de11773b", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7626951ca5eec627da960615b51009f3a774765406ff02722b1d818f17e5778"},
"absinthe_plug": {:hex, :absinthe_plug, "1.5.8", "38d230641ba9dca8f72f1fed2dfc8abd53b3907d1996363da32434ab6ee5d6ab", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bbb04176647b735828861e7b2705465e53e2cf54ccf5a73ddd1ebd855f996e5a"},
"ash": {:hex, :ash, "3.0.14", "0a76b8574a9bda07aacf4ab8c3cc5dd8a48bb76944d14a7f30165646cd072138", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, ">= 0.8.1 and < 1.0.0-0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.1.18 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a2ee23bb84b6abfafb1bdb6b0bb0bcd373c84947ce53efb1e83a22bd2f3452ad"},
"ash": {:hex, :ash, "3.0.15", "1cea8ca799dc8281d09e316a189fddfb27a2a29a0b0e5af369aa83d6f731ca75", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, ">= 0.8.1 and < 1.0.0-0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.1.18 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "97abfed57f2bf29c3889c51f0dfa23b382a1361a9d70e7d82d7e59a2be6bdc73"},
"ash_graphql": {:hex, :ash_graphql, "1.2.0", "b4b7a754ef722cff1c84cf35291e2ff0402fc91d805e2a01405157087f908a9b", [:mix], [{:absinthe, "~> 1.7", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.4", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d16986527788f74b2fe8085827d81bad08f1574d8562bc52619c00d43e75aa52"},
"ash_postgres": {:hex, :ash_postgres, "2.0.9", "e6036512e16e672b80c2a0154f3bbd4828b8a6619c03b97c81c3df51ee902023", [:mix], [{:ash, ">= 3.0.7 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, ">= 0.2.4 and < 1.0.0-0", [hex: :ash_sql, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "a362fd8a7922e0c21d8dccd6b5fd177b5ea62dbb223bbf04f92fed678d7cc405"},
"ash_sql": {:hex, :ash_sql, "0.2.5", "8b50c3178776263b912e1b60e161e2bcf08a907a38abf703edf8a8a0a51b3fe2", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "0d5d8606738a17c4e8c0be4244623df721abee5072cee69d31c2711c36d0548f"},