defmodule Queryable do @moduledoc """ Queryable injects query utility functions inside an Ecto Schema. ## Example defmodule Person do use Queryable #instead of Ecto.Schema schema "persons" do field :name, :string field :surname, :string field :age, :integer end criteria(under: age, where: el.age < ^age) criteria(ordered_by: field, order_by: ^field) end Using Queryable within an Ecto Schema creates a function `&query/1` that accepts a list of optional keywords, each one corresponding to a schema field or to a virtual field declared on a `criteria`, and returns an Ecto Query. A typical usage is the following: iex> Person.query(name: "John", under: 18) iex> #Ecto.Query Furthermore, for each field or virtual field, a builder function with the same name is created. For example, the previous query can be rewritten using builder: iex> Person.name("John") |> Person.under(18) iex> #Ecto.Query The latter option enables compile time checks (eg: dialyxir) if you want to be sure you are building the query properly. """ defmacro __using__(_opts) do quote do use Ecto.Schema require Queryable import Queryable import Ecto.Query @before_compile unquote(__MODULE__) def query(criteria) when is_list(criteria) do query = from(el in __MODULE__) Enum.reduce(criteria, query, &apply_criteria/2) end end end @doc """ Create a virtual field that can be queried. ## Example criteria(under: age, where: el.age < ^age) criteria(ordered_by: field, order_by: ^field) You can also use pattern matching. ## Example criteria(age: [from, to], where: el.age >= ^from and el.age <= ^to) criteria(age: age, where: el.age == ^age) If you define a custom criteria for schema field, default query for that field will be replaced. N.B. Object must be referred using `el` keyword. """ defmacro criteria([{key, value} | body]) do filters = Module.get_attribute(__CALLER__.module, :filters, []) if not Enum.member?(filters, key) do Module.put_attribute(__CALLER__.module, :filters, filters ++ [key]) end quote do defp apply_criteria({unquote(key), unquote(value)}, query) do from([el] in query, unquote(body)) end def unquote(key)(unquote(value)) do from([el] in __MODULE__, unquote(body)) end def unquote(key)(query, unquote(value)) do from([el] in query, unquote(body)) end end end defmacro __before_compile__(_env) do filters = Module.get_attribute(__CALLER__.module, :filters, []) quote do fields = __MODULE__ |> Module.get_attribute(:changeset_fields) |> Enum.map(fn {key, _} -> key end) |> Enum.filter(fn func -> not Enum.member?(unquote(filters), func) end) unquote(criteria_equal(quote do: fields)) end end defp criteria_equal(fields) do quote bind_quoted: [fields: fields] do Enum.each(fields, fn field -> criteria([{unquote(field), value}, where: field(el, ^unquote(field)) == ^value]) end) end end end