Current section
Files
Jump to
Current section
Files
lib/ecto_fragment_extras.ex
defmodule EctoFragmentExtras do
@moduledoc ~S"""
`EctoFragmentExtras` is a collection of macros to enhance Ecto fragments.
Instead of using Ectos `fragment` with ?-based interpolation, this module
provides macros (that expand into regular Ecto fragments) which allow
named or inline params.
"""
alias EctoFragmentExtras.ConvertInlineParams
alias EctoFragmentExtras.ConvertNamedParams
@doc """
A variant of Ecto fragment() which supports named params.
Nameds params are provided in a keyword list as the last argument.
The fragments query string can reference named params using the
`#{:name}` syntax where `name` is a key in the keyword list.
So `named_fragment("coalesce(#{:a}, #{:b}, #{:a})", a: 1, b: 2)` will
be expanded to `fragment("coalesce(?, ?, ?)", 1, 2, 1)` at compile-time.
```elixir
defmodule TestQuery do
import Ecto.Query
import EctoFragmentExtras
def test_query do
default = "Jane Doe"
query = from users in "users",
select: named_fragment("coalesce(#{:a}, #{:b})", a: users.name, b: ^default)
Repo.all(query)
end
end
```
"""
defmacro named_fragment(query, params) when is_list(params) do
{query, frags} = ConvertNamedParams.call(query, params)
quote do
fragment(unquote(query), unquote_splicing(frags))
end
end
@doc ~S"""
A variant of Ecto fragment() which supports params inlined in the query string.
Inline params are provided within the fragment query string as
`#{}`-style string interpolation. Since this is a macro which expands to a regular
Ecto fragment, those interpolations are safe (they are no real string interpolations,
and pose no extra SQL injection risk compared to regular fragments).
So `inline_fragment("coalesce(#{user.name}, #{^default})")` will
be expanded to `fragment("coalesce(?, ?)", user.name, ^default)` at compile-time.
```elixir
defmodule TestQuery do
import Ecto.Query
import EctoFragmentExtras
def test_query do
default = "Jane Doe"
query = from users in "users",
select: inline_fragment("coalesce(#{users.name}, #{^default})")
Repo.all(query)
end
end
```
SAFETY WARNING: Please note that it is NOT safe to first interpolate a string which is passed in to this macro.
Unsafe example:
```elixir
def unsafe_query(user_name) do
default = "Jane Doe"
fragment_string = "coalesce(#{user_name}, #{^default})"
query = from users in "users",
select: inline_fragment(fragment_string)
Repo.all(query)
end
end
```
The code above is UNSAFE, because the interpolation is done before the macro is called and the macro
receives the already interpolated string. This prohibits the macro from safely escaping the query params.
If the `user_name` param comes from untrusted user input, this can lead to SQL injection.
"""
defmacro inline_fragment(query) do
{query, frags} = ConvertInlineParams.call(query)
quote do
fragment(unquote(query), unquote_splicing(frags))
end
end
@doc ~S"""
A variant of Ecto fragment() which supports params inlined in the query string or named params depending on the number of arguments.
When called with only a query string, this macro behaves like `inline_fragment/1`.
When called with a query string and a keyword list, this macro behaves like `named_fragment/2`.
```elixir
defmodule TestQuery do
import Ecto.Query
import EctoFragmentExtras
@default_name "Jane Doe"
def test_query_named do
query = from users in "users",
select: frag("coalesce(#{:a}, #{:b})", a: users.name, b: ^@default_name)
Repo.all(query)
end
def test_query_inline do
query = from users in "users",
select: frag("coalesce(#{users.name}, #{^@default_name})")
Repo.all(query)
end
end
```
"""
defmacro frag(query) do
quote do
inline_fragment(unquote(query))
end
end
defmacro frag(query, params) do
quote do
named_fragment(unquote(query), unquote(params))
end
end
end