Packages

Simple macro for defining macro that will return `fragment` with SQL function. A little bit Xzibit, but fun.

Current section

Files

Jump to
ecto_function lib ecto_function.ex
Raw

lib/ecto_function.ex

defmodule Ecto.Function do
@moduledoc """
Helper macro for defining helper macros for calling DB functions.
A little Xzibity, but helps.
"""
@doc """
Define new SQL function call.
## Options
Currently there is only one option allowed:
- `for` - define DB function name to call
## Examples
import Ecto.Function
defqueryfunc foo # Define function without params
defqueryfunc bar(a, b) # Explicit parameter names
defqueryfunc baz/1 # Define function using arity
defqueryfunc qux(a, b \\ 0) # Define function with default arguments
defqueryfunc quux/1, for: "db_quux" # Define with alternative DB call
Then calling such functions in query would be equivalent to:
from _ in "foos", select: %{foo: foo()}
# => SELECT foo() AS foo FROM foos
from q in "bars", select: %{bar: bar(q.a, q.b)}
# => SELECT bar(bars.a, bars.b) AS bar FROM bars
from q in "bazs", where: baz(q.a) == true
# => SELECT * FROM bazs WHERE baz(bazs.a) = TRUE
from q in "quxs", select: %{one: qux(q.a), two: qux(q.a, q.b)}
# => SELECT
# qux(quxs.a, 0) AS one,
# qux(quxs.a, quxs.b) AS two
# FROM "quxs"
from q in "quuxs", select: %{quux: quux(q.a)}
# => SELECT db_quux(quuxs.a) FROM quuxs
## Gotchas
If your function uses "special syntax" like PostgreSQL [`extract`][extract]
then this module won't help you and you will be required to write your own
macro that will handle such case.
defmacro extract(from, field) do
query do: fragment("extract(? FROM ?)", field, from)
end
This case probably will never be supported in this library and you should
handle it on your own.
"""
defmacro defqueryfunc(definition, opts \\ [])
defmacro defqueryfunc({:/, _, [{name, _, _}, params_count]}, opts)
when is_atom(name) and is_integer(params_count) do
opts = Keyword.put_new(opts, :for, name)
params = Macro.generate_arguments(params_count, Elixir)
macro(name, params, __CALLER__, opts)
end
defmacro defqueryfunc({name, _, params}, opts)
when is_atom(name) and is_list(params) do
opts = Keyword.put_new(opts, :for, name)
macro(name, params, __CALLER__, opts)
end
defmacro defqueryfunc(name, opts) when is_atom(name) do
opts = Keyword.put_new(opts, :for, name)
macro(name, [], __CALLER__, opts)
end
defmacro defqueryfunc(tree, _) do
raise CompileError,
file: __CALLER__.file,
line: __CALLER__.line,
description: "Unexpected query function definition #{Macro.to_string tree}."
end
defp macro(name, params, caller, opts) do
sql_name = Keyword.fetch!(opts, :for)
{query, args} = build_query(params, caller)
quote do
defmacro unquote(name)(unquote_splicing(params)) do
unquote(body(sql_name, query, args))
end
end
end
defp body(name, query, args) do
fcall = "#{name}(#{query})"
args = Enum.map(args, &{:unquote, [], [&1]})
{:quote, [], [[do: {:fragment, [], [fcall | args]}]]}
end
defp build_query(args, caller) do
query =
"?"
|> List.duplicate(Enum.count(args))
|> Enum.join(",")
args =
args
|> Enum.map(fn
{:\\, _, [{_, _, _} = arg, _default]} -> arg
{_, _, env} = arg when is_atom(env) -> arg
_token ->
raise CompileError,
file: caller.file,
line: caller.line,
description: "only variables and \\\\ are allowed as arguments in definition header."
end)
{query, args}
end
end