Current section

Files

Jump to
monet lib query select.ex
Raw

lib/query/select.ex

defmodule Monet.Query.Select do
@moduledoc """
A simple query builder.
rows = Select.new()
|> Select.columns("u.id, u.name")
|> Select.from("users u")
|> Select.join("roles r on u.role_id = r.id")
|> Select.where("u.power", :gt, 9000)
|> Select.limit(100)
|> Select.exec!() // returns a Monet.Result
|> Monet.rows()
"""
use Monet.Query.Where
defmacro __using__(_) do
quote do
use Monet.Query.Where
alias Monet.Query.Select
end
end
alias __MODULE__
alias Monet.Query.Cursor
@enforce_keys [:select, :from, :where, :order, :group, :limit, :offset]
defstruct @enforce_keys
def new() do
%Select{
from: [],
order: nil,
group: nil,
limit: nil,
offset: nil,
select: nil,
where: Where.new(),
}
end
@doc """
Columns to select. If not called, will select *.
Can can called multiple times. Can be called with an array, or a string.
This can really be anything and it's best to think of it as the test that
is placed between the `select` and the `from`.
"""
def columns(q, [first | columns]) do
columns = Enum.reduce(columns, [first], fn c, acc -> [acc, ", ", c] end)
append_columns(q, columns)
end
def columns(s, column), do: append_columns(s, column)
defp append_columns(%{select: nil} = s, columns), do: %Select{s | select: columns}
defp append_columns(s, columns), do: %Select{s | select: [s.select, ", ", columns]}
@doc """
Table to select from. Can be called multiple times. This essentially becomes
what gets placed between the "from" and the "where".
You could do:
Select.from(s, "users")
# OR
Select.from(s, "(select 1 from another) x")
"""
def from(s, from), do: %Select{s | from: [s.from, from]}
@doc """
Join tables. There's no magic here. Doesn't know anything
about your tables (aka, you need to tell it what to join on):
Select.join(s, "table b on a.id = b.id")
This is just a shorthand for `from/2` but it injects the word
" [left|right|full]? join " for you
"""
def join(s, table), do: %Select{s | from: [s.from, [" join ", table]]}
def join(s, :left, table), do: %Select{s | from: [s.from, [" left join ", table]]}
def join(s, :right, table), do: %Select{s | from: [s.from, [" right join ", table]]}
def join(s, :full, table), do: %Select{s | from: [s.from, [" full join ", table]]}
def order(s, order), do: append_order(s, order)
def order(s, order, true), do: append_order(s, order)
def order(s, order, false), do: append_order(s, [order, " desc"])
defp append_order(%{order: nil} = s, order), do: %Select{s | order: [order]}
defp append_order(%{order: existing} = s, order), do: %Select{s | order: [existing, ", ", order]}
def group(s, group), do: %Select{s | group: group}
def limit(s, limit) when is_integer(limit), do: %Select{s | limit: limit}
def offset(s, offset) when is_integer(offset), do: %Select{s | offset: offset}
def cursor(s, opts), do: Cursor.new(s, opts)
def reduce(%Cursor{} = cursor, acc, fun) do
step(Cursor.next(cursor), acc, fun)
end
defp step({:empty, _cursor}, acc, fun) do
fun.(:empty, acc)
end
defp step({:row, row, cursor}, acc, fun) do
acc = fun.({:row, row}, acc)
step(Cursor.next(cursor), acc, fun)
end
defp step({:last, row, cursor}, acc, fun) do
acc = fun.({:row, row}, acc)
fun.(Cursor.next(cursor), acc) # returns the {:page, _info result}
end
def exec(s, pool \\ Monet) do
{sql, args} = to_sql(s)
Monet.query(pool, sql, args)
end
def exec!(s, pool \\ Monet) do
case exec(s, pool) do
{:ok, result} -> result
{:error, %{code: 42000} = err} -> raise Map.put(err, :details, inspect(s))
{:error, err} -> raise err
end
end
def to_sql(s) do
{where, args} = Where.to_sql(s.where)
sql = ["select ", s.select || "*", " from ", s.from, where]
sql = case s.group do
nil -> sql
group -> [sql, " group by ", group]
end
sql = case s.order do
nil -> sql
order -> [sql, " order by ", order]
end
sql = case s.limit do
nil -> sql
limit -> [sql, " limit ", Integer.to_string(limit)]
end
sql = case s.offset do
nil -> sql
offset -> [sql, " offset ", Integer.to_string(offset)]
end
{sql, args}
end
end
defimpl Inspect, for: Monet.Query.Select do
def inspect(q, opts) do
import Inspect.Algebra
{sql, values} = Monet.Query.Select.to_sql(q)
sql = :erlang.iolist_to_binary(sql)
sql = Regex.split(~r/ (from|join|where|order by|limit|offset) /, sql, include_captures: true)
docs = fold_doc(sql, fn
<<" ", doc::binary>>, acc when doc in ["from ", "join", "where ", "group by", "order by ", "limit ", "offset "] -> concat([break("\n"), doc, acc])
doc, acc -> concat(doc, acc)
end)
concat [docs, break("\n"), to_doc(values, opts)]
end
end