Current section
Files
Jump to
Current section
Files
lib/money/ecto/money_ecto_query_api.ex
if Code.ensure_loaded?(Ecto.Query.API) do
defmodule Money.Ecto.Query.API do
@moduledoc """
Provides several helpers to query DB for the `Money` type.
Under the hood it delegates to
[`Ecto.Query.API.fragment/1`](https://hexdocs.pm/ecto/Ecto.Query.API.html#fragment/1-defining-custom-functions-using-macros-and-fragment),
but might be helpful for compile-type sanity check for typos and
better language server support.
It is also designed to be an implementation-agnostic, meaning one can use
these helpers without a necessity to explicitly specify a backing type.
The default implementation recommends a `Composite` adapter, which is used by default.
To use it with, say, `MySQL`, one should implement this behaviour for `MySQL` and declare
the implementation as `use Money.Ecto.Query.API, adapter: MyImpl.MySQL.Adapter`
Although the library provides the MySQL adapter too (`Money.Ecto.Query.API.Map.MySQL`,)
but it is not actively maintained, so use it on your own.
If for some reason you use `Map` type with `Postgres`, helpers are still available
with `use Money.Ecto.Query.API, adapter: Money.Ecto.Query.API.Map.Postgres`
"""
@doc """
Native implementation of how to retrieve `amount` from the DB.
For `Postgres`, it delegates to the function on the composite type,
for other implementation it should return a `Ecto.Query.API.fragment/1`.
"""
@macrocallback amount(Macro.t()) :: Macro.t()
@doc """
Native implementation of how to retrieve `currency_code` from the DB.
For `Postgres`, it delegates to the function on the composite type,
for other implementation it should return a `Ecto.Query.API.fragment/1`.
"""
@macrocallback currency_code(Macro.t()) :: Macro.t()
@doc """
Native implementation of how to `sum` several records having a field
of the type `Money` in the DB.
For `Postgres`, it delegates to the function on the composite type,
for other implementation it should return a `Ecto.Query.API.fragment/1`.
"""
@macrocallback sum(Macro.t(), cast? :: boolean()) :: Macro.t()
@doc """
Cast decimal to the value accepted by the database.
"""
@callback cast_decimal(Decimal.t()) :: any()
@doc false
defmacro __using__(opts \\ [])
@doc false
defmacro __using__([]),
do: do_using(Money.Ecto.Query.API.Composite)
@doc false
defmacro __using__(adapter: adapter),
do: do_using(adapter)
defp do_using(adapter) do
quote do
import unquote(adapter)
import Money.Ecto.Query.API
end
end
@doc """
`Ecto.Query.API` helper, allowing to filter records having the same currency.
_Example:_
```elixir
iex> Organization
...> |> where([o], currency_eq(o.payroll, :AUD))
...> |> select([o], o.payroll)
...> |> Repo.all()
[Money.new(:AUD, "50"), Money.new(:AUD, "70")
```
"""
defmacro currency_eq(field, currency) when is_atom(currency) do
currency = currency |> to_string() |> String.upcase()
do_currency_eq(field, currency)
end
defmacro currency_eq(field, currency) when is_binary(currency) do
do_currency_eq(field, currency)
end
defp do_currency_eq(field, <<_::binary-size(3)>> = currency) do
quote do: currency_code(unquote(field)) == ^unquote(currency)
end
@doc """
`Ecto.Query.API` helper, allowing to filter records having the same amount.
_Example:_
```elixir
iex> Organization
...> |> where([o], amount_eq(o.payroll, 100))
...> |> select([o], o.payroll)
...> |> Repo.all()
[Money.new(:EUR, "100"), Money.new(:USD, "100")
```
"""
defmacro amount_eq(field, amount) when is_integer(amount) do
quote do
amount(unquote(field)) == ^unquote(amount)
end
end
@doc """
`Ecto.Query.API` helper, allowing to filter records having the same amount and currency.
_Example:_
```elixir
iex> Organization
...> |> where([o], money_eq(o.payroll, Money.new!(100, :USD)))
...> |> select([o], o.payroll)
...> |> Repo.all()
[Money.new(:USD, "100"), Money.new(:USD, "100")]
```
"""
defmacro money_eq(field, money) do
quote do
amount(unquote(field)) == ^cast_decimal(unquote(money).amount) and
currency_code(unquote(field)) == ^to_string(unquote(money).currency)
end
end
@doc """
`Ecto.Query.API` helper, allowing to filter records having one
of currencies given as an argument.
_Example:_
```elixir
iex> Organization
...> |> where([o], currency_in(o.payroll, [:USD, :EUR]))
...> |> select([o], o.payroll)
...> |> Repo.all()
[Money.new(:EUR, "100"), Money.new(:USD, "100")]
```
"""
defmacro currency_in(field, currencies) when is_list(currencies) do
currencies = currencies |> Enum.map(&to_string/1) |> Enum.map(&String.upcase/1)
quote do
currency_code(unquote(field)) in ^unquote(currencies)
end
end
@doc """
`Ecto.Query.API` helper, allowing to filter records having the amount
in the range given as an argument.
Accepts `[min, max]`, `{min. max}`, and `min..max` as a range.
_Example:_
```elixir
iex> Organization
...> |> where([o], amount_in(o.payroll, 90..110))
...> |> select([o], o.payroll)
...> |> Repo.all()
[Money.new(:EUR, "100"), Money.new(:USD, "100")]
```
"""
defmacro amount_in(field, [min, max]),
do: do_amount_in(field, min, max)
defmacro amount_in(field, {min, max}),
do: do_amount_in(field, min, max)
defmacro amount_in(field, {:.., _, [min, max]}),
do: do_amount_in(field, min, max)
defmacro amount_in(field, {:"..//", _, [min, max, 1]}),
do: do_amount_in(field, min, max)
defmacro amount_in(field, {:"..//", _, [min, max, {:-, _, [1]}]}),
do: do_amount_in(field, max, min)
defmacro amount_in(field, {:"..//", _, [_min, _max, step]}) do
raise CompileError,
file: __CALLER__.file,
line: __CALLER__.line,
description:
"Ranges with a step (#{step}) are not supported for [#{Macro.to_string(field)}]"
end
defp do_amount_in(field, min, max) do
quote do
amount_ge(unquote(field), unquote(min)) and amount_le(unquote(field), unquote(max))
end
end
@doc """
`Ecto.Query.API` helper, allowing to filter records having the amount
greater than or equal to the one given as an argument.
_Example:_
```elixir
iex> Organization
...> |> where([o], amount_ge(o.payroll, 90))
...> |> select([o], o.payroll)
...> |> Repo.all()
[Money.new(:AUD, "90"), Money.new(:USD, "100")]
```
"""
defmacro amount_ge(field, num) do
quote do
amount(unquote(field)) >= ^unquote(num)
end
end
@doc """
`Ecto.Query.API` helper, allowing to filter records having the amount
less than or equal to the one given as an argument.
_Example:_
```elixir
iex> Organization
...> |> where([o], amount_le(o.payroll, 110))
...> |> select([o], o.payroll)
...> |> Repo.all()
[Money.new(:EUR, "100"), Money.new(:USD, "110")]
```
"""
defmacro amount_le(field, num) do
quote do
amount(unquote(field)) <= ^unquote(num)
end
end
@doc """
`Ecto.Query.API` helper, allowing to aggregate by currency, suming amount.
For more sophisticated aggregation, resort to raw `fragment`.
_Example:_
```elixir
iex> Organization
...> |> where([o], o.name == ^"Lemon Inc.")
...> |> total_by([o], o.payroll)
...> |> Repo.all()
[Money.new(:EUR, "100"), Money.new(:USD, "210")]
```
"""
defmacro total_by(query, binding, field) do
quote do
unquote(query)
|> where(unquote(binding), not is_nil(unquote(field)))
|> group_by(unquote(binding), [currency_code(unquote(field))])
|> select(unquote(binding), sum(unquote(field), true))
end
end
@doc """
`Ecto.Query.API` helper, allowing to aggregate by currency, suming amount.
Same as `total_by/3`, but for the single currency only.
_Example:_
```elixir
iex> Organization
...> |> where([o], o.name == ^"Lemon Inc.")
...> |> total_by([o], o.payroll, :USD)
...> |> Repo.one()
[Money.new(:USD, "210")]
```
"""
defmacro total_by(query, binding, field, currency) do
currency = currency |> to_string() |> String.upcase() |> List.wrap()
quote do
unquote(query)
|> where(unquote(binding), not is_nil(unquote(field)))
|> where(unquote(binding), currency_in(unquote(field), unquote(currency)))
|> group_by(unquote(binding), [currency_code(unquote(field))])
|> select(unquote(binding), sum(unquote(field), true))
end
end
end
end