Packages

Ecto query helper for iex (Rails style)

Current section

Files

Jump to
qh lib qh.ex
Raw

lib/qh.ex

defmodule Qh do
@moduledoc """
Query helper for iex
`.iex.exs`:
```
use Qh
Qh.configure(app: :my_app)
```
## Example:
iex>q User.where(age > 20 and age < 50).order(name).first
%MyApp.User%{id: 123, age: 21, name: "Anna"}
"""
defmacro __using__(_) do
quote do
require Qh
import Qh, only: [q: 1, q: 2]
end
end
@doc """
Run a query
Examples:
use Qh
Qh.configure(app: :my_app)
q User.first
q User.order(name).last(3)
q User.order(name: :asc, age: :desc).last
q User.order("lower(?)", name).last
q User.where(age > 20 and age <= 30).count
q User.where(age > 20 and age <= 30).limit(10).all
q User.where(age > 20 or name == "Bob").all
q User.where(age > 20 and (name == "Bob" or name == "Anna")).all
q User.where(age: 20, name: "Bob").count
q User.where("nicknames && ?", ["Bobby", "Bobi"]).count
q User.where("? = ANY(?)", age, [20, 30, 40]).count
q User.find(21)
q User.find_by(name: "Bob Foo")
q User.find_by(name == "Bob" or name == "Anna")
# Initialize new
user = q User.new(name: "Bob")
"""
defmacro q(q, opts \\ []) do
[schema | rest] = unwrap_nested(q)
code = [
quote do
require Ecto.Query
query = Qh.lookup_schema(unquote(schema), unquote(opts))
end
]
code =
code ++
Enum.map(rest, fn
{:order, _, [q | _rest] = fragment} when is_binary(q) ->
fragment = deep_prefix_binding(fragment)
quote do
query = Ecto.Query.order_by(query, [t], fragment(unquote_splicing(fragment)))
end
{:order, _, [order]} ->
order =
Enum.map(List.wrap(order), fn
{_k, _, nil} = part -> deep_prefix_binding(part)
{key, dir} -> {dir, deep_prefix_binding(key)}
end)
quote do
query = Ecto.Query.order_by(query, [t], unquote(order))
end
{:where, _, [q | _rest] = fragment} when is_binary(q) ->
fragment = deep_prefix_binding(fragment)
quote do
query = Ecto.Query.where(query, [t], fragment(unquote_splicing(fragment)))
end
{:where, _, [conditions]} ->
conditions = deep_prefix_binding(conditions)
quote do
query = Ecto.Query.where(query, [t], unquote(conditions))
end
{:limit, _, [n]} ->
quote do
query = Ecto.Query.limit(query, unquote(n))
end
{:find, _, [id]} ->
quote do
Qh.repo(unquote(opts)).get!(query, unquote(id))
end
{:find_by, _, [q | _rest] = fragment} when is_binary(q) ->
fragment = deep_prefix_binding(fragment)
quote do
Ecto.Query.where(query, [t], fragment(unquote_splicing(fragment)))
|> Ecto.Query.limit(1)
|> Qh.repo(unquote(opts)).all()
|> List.first()
end
{:find_by, _, [condition]} ->
condition = deep_prefix_binding(condition)
quote do
Ecto.Query.where(query, [t], unquote(condition))
|> Ecto.Query.limit(1)
|> Qh.repo(unquote(opts)).all()
|> List.first()
end
{:count, _, _} ->
quote do
Qh.repo(unquote(opts)).aggregate(query, :count)
end
{:first, _, []} ->
quote do
Ecto.Query.limit(query, 1)
|> Qh.default_primary_order()
|> Qh.repo(unquote(opts)).one()
end
{:first, _, [n]} ->
quote do
Ecto.Query.limit(query, unquote(n))
|> Qh.default_primary_order()
|> Qh.repo(unquote(opts)).all()
end
{:last, _, []} ->
quote do
Ecto.Query.reverse_order(query)
|> Ecto.Query.limit(1)
|> Qh.default_primary_order()
|> Qh.repo(unquote(opts)).one()
end
{:last, _, [n]} ->
quote do
Ecto.Query.reverse_order(query)
|> Ecto.Query.limit(unquote(n))
|> Qh.default_primary_order()
|> Qh.repo(unquote(opts)).all()
|> Enum.reverse()
end
{:select, _, target} ->
target =
target
|> unwrap_single()
|> deep_prefix_binding()
quote do
query
|> Ecto.Query.select([t], unquote(target))
|> Qh.repo(unquote(opts)).all()
end
{:all, _, []} ->
quote do
query
|> Qh.repo(unquote(opts)).all()
end
{:new, _, []} ->
quote do
schema = Qh.get_schema(query)
struct(schema)
end
{:new, _, [params]} ->
quote do
schema = Qh.get_schema(query)
struct(schema, unquote(params))
end
end)
quote do
(unquote_splicing(code))
end
end
@doc """
Update fields in a struct
Examples:
user = %MyApp.User{name: nil, age: nil}
user = Qh.assign(user, name: "Bob", age: 21)
# or initialize a new record
user = Qh.assign(:user, name: "Alice", age: 22)
"""
def assign(schema, params, opts \\ [])
def assign(%_{} = struct, params, _opts) do
struct(struct, params)
end
def assign(schema, params, opts) do
struct(Qh.lookup_schema(schema, opts), params)
end
@doc """
Save a struct to the database
Inserts a new record if no primary key is set
or no record exists with the current primary key.
Performs a get and an update if the record already exists.
Returns `{:ok, struct}` on success or `{:error, validation_errors}` on failure.
Examples:
user = %MyApp.User{id: nil, name: "Bob", age: 21}
{:ok, user} = Qh.save(user)
"""
def save(%schema{} = struct, opts \\ []) do
primary_key = schema.__schema__(:primary_key)
primary_key_values = Map.take(struct, primary_key)
primary_values = Map.values(primary_key_values)
if Enum.all?(primary_values, &is_nil/1) do
# no primary key set, create new new
params = Map.from_struct(struct)
changeset = schema.changeset(struct(schema), params)
Qh.repo(opts).insert(changeset)
|> handle_repo_result()
else
case Qh.repo(opts).get_by(schema, primary_key_values) do
nil ->
# no record found, create new
params = Map.from_struct(struct)
changeset = schema.changeset(struct(schema), params)
Qh.repo(opts).insert(changeset)
|> handle_repo_result()
current ->
# record found, update
params = Map.from_struct(struct)
changeset = schema.changeset(current, params)
Qh.repo(opts).update(changeset)
|> handle_repo_result()
end
end
end
@doc """
Save a struct to the database
Similar to `save/1` but will raise on failures and return only the
struct on success.
Examples:
user = %MyApp.User{id: nil, name: "Bob", age: 21}
user = Qh.save!(user)
"""
def save!(%_schema{} = struct, opts \\ []) do
save(struct, opts) |> ok_or_raise()
end
@doc """
Performs a database update for a struct and params
Returns `{:ok, struct}` on success or `{:error, validation_errors}` on failure.
Examples:
user = %MyApp.User{id: 2, name: "Bob", age: 21}
{:ok, user} = Qh.update(user, age: 22)
"""
def update(%schema{} = struct, params, opts \\ []) do
changeset = schema.changeset(struct, Map.new(params))
Qh.repo(opts).update(changeset)
|> handle_repo_result()
end
@doc """
Save a struct to the database
Similar to `update/1` but will raise on failures and return only the
struct on success.
Examples:
user = %MyApp.User{id: 2, name: "Bob", age: 21}
user = Qh.update!(user, age: 22)
"""
def update!(%_schema{} = struct, params, opts \\ []) do
update(struct, params, opts) |> ok_or_raise()
end
@doc """
Delete a struct in the database
Will raise if the record doesn't exists
Examples:
user = %MyApp.User{id: 2, name: "Bob", age: 21}
Qh.delete!(user)
"""
def delete!(%_schema{} = struct, opts \\ []) do
Qh.repo(opts).delete!(struct)
end
defp handle_repo_result({:ok, struct}) do
{:ok, struct}
end
defp handle_repo_result({:error, changeset}) do
{:error, changeset.errors}
end
defp ok_or_raise({:ok, struct}) do
struct
end
defp ok_or_raise({:error, error}) do
raise inspect(error)
end
defp deep_prefix_binding(tree) do
prewalk_cond(tree, fn
{:^, _, _} = node ->
{false, node}
{field, _, nil} ->
{false, {{:., [], [{:t, [], nil}, field]}, [no_parens: true], []}}
node ->
{true, node}
end)
end
defp unwrap_single([item]), do: item
defp unwrap_single(other), do: other
defp unwrap_nested({{:., _, [left, right]}, opts, children}) do
parens = if opts[:no_parens], do: false, else: true
unwrap_nested(left) ++ [{right, [parens: parens], children}]
end
defp unwrap_nested({k, _, nil}), do: [k]
defp unwrap_nested(v), do: [v]
def repo(opts \\ []) do
repo_mod(opts)
end
def lookup_schema(schema, opts \\ []) do
app_mod = app_mod(opts)
cond do
is_atom(schema) and first_uppercase?(schema) and ensure_compiled?(schema) ->
schema
first_uppercase?(schema) ->
module = Module.concat(app_mod, schema)
module_or_raise!(module, schema)
true ->
module = Module.concat(app_mod, Macro.camelize(to_string(schema)))
module_or_raise!(module, schema)
end
end
def repo_mod(opts) do
opts[:repo] || Application.get_env(:qh, :repo) || Module.concat(app_mod(opts), Repo)
end
def app_mod(opts) do
opts[:app_mod] || maybe_camelize(opts[:app]) || Application.get_env(:qh, :app_mod) || maybe_camelize(Application.get_env(:qh, :app))
end
def configure(opts) do
Enum.each(opts, fn {k, v} ->
Application.put_env(:qh, k, v)
end)
end
def default_primary_order(schema) when is_atom(schema) do
require Ecto.Query
primary_key = schema.__schema__(:primary_key)
Ecto.Query.order_by(schema, ^primary_key)
end
def default_primary_order(%{from: %{source: {_, schema}}, order_bys: []} = query) do
require Ecto.Query
primary_key = schema.__schema__(:primary_key)
Ecto.Query.order_by(query, ^primary_key)
end
def default_primary_order(unknown), do: unknown
def get_schema(schema) when is_atom(schema) do
schema
end
def get_schema(%{from: %{source: {_, schema}}}) do
schema
end
defp module_or_raise!(module, schema) do
if ensure_compiled?(module) do
module
else
raise "unable to find schema for #{inspect(schema)}, #{inspect(module)} does not exist"
end
end
defp first_uppercase?(val) do
String.at(to_string(val), 0) == String.capitalize(String.at(to_string(val), 0))
end
defp ensure_compiled?(module) do
case Code.ensure_compiled(module) do
{:module, _} ->
true
{:error, _} ->
false
end
end
defp maybe_camelize(nil), do: nil
defp maybe_camelize(val), do: Macro.camelize(to_string(val))
defp prewalk_cond(tree, fun) do
apply_and_maybe_descend(tree, fun)
end
defp do_prewalk_cond({form, meta, args}, fun) when is_atom(form) do
{form, meta, do_prewalk_cond_args(args, fun)}
end
defp do_prewalk_cond({form, meta, args}, fun) do
{apply_and_maybe_descend(form, fun), meta, do_prewalk_cond_args(args, fun)}
end
defp do_prewalk_cond({left, right}, fun) do
{apply_and_maybe_descend(left, fun), apply_and_maybe_descend(right, fun)}
end
defp do_prewalk_cond(list, fun) when is_list(list) do
do_prewalk_cond_args(list, fun)
end
defp do_prewalk_cond(x, _fun) do
x
end
defp do_prewalk_cond_args(args, _fun) when is_atom(args) do
args
end
defp do_prewalk_cond_args(args, fun) when is_list(args) do
Enum.map(args, fn node -> apply_and_maybe_descend(node, fun) end)
end
defp apply_and_maybe_descend(node, fun) do
case fun.(node) do
{true, node} ->
do_prewalk_cond(node, fun)
{false, node} ->
node
end
end
end