defmodule Qlc do
@type binding_struct :: :erl_eval.binding_struct()
@type bindings :: Keyword.t
@type query_cursor :: Qlc.Cursor.t
@type abstract_expr :: :erl_parse.abstract_expr()
@type error_info :: :erl_parse.error_info()
#@dialyzer [{:nowarn_function, :expr_to_handle, 3}]
@type expr :: :erl_parse.abstract_expr()
@type qlc_opt :: tuple()
@type qlc_opt_list :: [{atom(), any()}|atom()]
@type table_options :: Keyword.t | tuple()
@type query_handle() :: :qlc.query_handle()
@type qlc_lc :: any
@type traverse_fun :: traverse_fun0 | traverse_fun1
@type traverse_fun1 :: (:ets.match_spec() -> traverse_result())
@type traverse_fun0 :: ( () -> traverse_result() )
@type traverse_result :: term() | objects()
@type objects :: [] | [term() | object_list() ]
@type object_list :: traverse_fun0() | objects()
@typedoc """
order function
detect order if smaller =< bigger then return true else false.
"""
@type order_fun :: ((smaller :: term(), bigger :: term()) -> boolean())
@typedoc """
order option
order by elixir native ascending, descending or order_fun().
default is ascending.
"""
@type order_option :: :ascending | :descending | order_fun()
@typedoc """
unique option for sorting.
only the first of a sequence of terms that compare equal(==) is output
if this option is set to true. defaults to false.
"""
@type unique_option:: {:unique, boolean()}
@typedoc """
sorting option.
see `:qlc.sort/2`
"""
@type sort_option :: {:order, order_option()} |
unique_option() | any()
@type sort_options :: [sort_option()] |
sort_option()
@typedoc """
key position for sorting tuples.
indexed by 0 (elixir's tuple element indexing).
"""
@type key_pos :: non_neg_integer() | [ non_neg_integer() ]
require Record
@doc """
Returns a query handle.
When evaluating returned query handle,
the answers to query handle argument are sorted by the query_handle()
options.
## example
iex> list = [a: 3, b: 2, c: 1]
iex> Qlc.q("[ X || X <- L]", [L: list]) |>
...> Qlc.sort(order: :descending) |>
...> Qlc.e()
[c: 1, b: 2, a: 3]
"""
@spec sort(query_handle(), sort_options()) :: query_handle()
defdelegate sort(qh, opt), to: :qlc
@doc """
sorts tuples on query_handle.
The sort is performed on the element(s)
mentioned in key_pos. If two tuples compare equal (==) on one element,
the next element according to key_pos is compared. The sort is stable.
key_pos is indexing by 0.
## example
iex> list = [a: 3, b: 2, c: 1]
iex> Qlc.q("[ X || X <- L]", [L: list]) |>
...> Qlc.keysort(1, order: :ascending) |>
...> Qlc.e()
[c: 1, b: 2, a: 3]
iex> list = [a: 1, b: 2, c: 3, d: 2]
...> Qlc.q("[X || X <- L]", [L: list]) |>
...> Qlc.keysort(1, order: :descending, unique: true) |>
...> Qlc.e()
[c: 3, b: 2, a: 1]
"""
@spec keysort(query_handle(), key_pos(), sort_options()) :: query_handle()
def keysort(qh, keypos, opt \\ []) when keypos >= 0, do: :qlc.keysort(keypos + 1, qh, opt)
@doc """
create qlc handle for any. see `:qlc.table/2`
## example
iex> q = Qlc.table(fn() -> [a: 1, b: 2, c: 3] end, [])
...> Qlc.q("[X || X = {K, V} <- L, K =:= Y]", [L: q, Y: :a]) |>
...> Qlc.e()
[a: 1]
iex> tf = fn(r, f) ->
...> [r.first |
...> fn() ->
...> last = r.last
...> case r.first do
...> x when x < last ->
...> f.(Range.new(r.first+1, last), f)
...> _x -> []
...> end
...> end]
...> end
...> trf = fn(r) -> tf.(r, tf) end
...> q = Qlc.table(fn() -> trf.(1..3) end, [])
...> Qlc.q("[X || X <- Q, X > 2]", [Q: q]) |> Qlc.e()
[3]
"""
@spec table(traverse_fun(), table_options()) :: query_handle()
def table(traverse_fun, option \\ []), do: :qlc.table(traverse_fun, option)
try do
@qlc_handle_fields Record.extract(:qlc_handle, from_lib: "stdlib/src/qlc.erl")
@qlc_opt_fields Record.extract(:qlc_opt, from_lib: "stdlib/src/qlc.erl")
@qlc_lc_fields Record.extract(:qlc_lc, from_lib: "stdlib/src/qlc.erl")
rescue
RuntimeError ->
require Logger
Logger.info(
"Qlc: OTP source distribution not installed, use local definition")
@qlc_handle_fields [:h]
@qlc_opt_fields [unique: false, cache: false, max_lookup: -1, join: :any, tmpdir: '', lookup: :any, max_list: 512*1024, tmpdir_usage: :allowed ]
@qlc_lc_fields [:lc, :opt]
end
#@qlc_bool_opt_keys [:cache, :unique]
Record.defrecord :qlc_handle, @qlc_handle_fields
Record.defrecord :qlc_opt, @qlc_opt_fields
Record.defrecord :qlc_lc, @qlc_lc_fields
#@optkeys [:max_lookup,:cache, :join,:lookup,:unique]
@doc """
string to erlang ast
"""
@spec exprs(String.t) :: expr() | no_return()
def exprs(str) do
{:ok, m, _} =
str
|> String.to_charlist
|> :erl_scan.string
{:ok, [expr]} = :erl_parse.parse_exprs(m)
expr
end
@doc """
optoin list to record(:qlc_opt)
"""
@spec options(list, atom, Keyword.t) :: qlc_opt
def options(opt, tagname, field_defs) do
bool_opts = Enum.filter(field_defs,
fn({_k, v}) ->
v == false
end)
|> Keyword.keys()
v = Enum.map(opt, fn(e) ->
if (Enum.member?(bool_opts, e)) do
{e, true}
else
e
end
end)
|> Keyword.merge(field_defs, fn(_k, v1, _v2) -> v1 end)
|> Keyword.values()
List.to_tuple([tagname | v])
end
@doc """
erlang ast with binding variables to qlc_handle
"""
@spec expr_to_handle(expr(), binding_struct, qlc_opt_list) :: query_handle() | {:qlc_handle, tuple()}
def expr_to_handle(expr, bind, opt) do
{:ok, {:call, _, _q, handle}} = :qlc_pt.transform_expression(expr, bind)
{:value, q, _} = :erl_eval.exprs(handle, bind)
opt_r = options(opt, :qlc_opt, @qlc_opt_fields)
lc = qlc_lc(q, opt: opt_r)
ret = qlc_handle(h: lc)
ret
end
@doc """
variable binding list to erlang_binding list
"""
@spec bind(Keyword.t, binding_struct) :: binding_struct
def bind([], b), do: b
def bind([{k, v} | t], b) when is_atom(k) do
bind(t, :erl_eval.add_binding(k, v, b))
end
@spec bind(Keyword.t) :: binding_struct
#def bind(a) when Keyword.keyword?is_list(a),
def bind(a) when is_list(a),
do: bind(a, :erl_eval.new_bindings())
@doc """
string to qlc_handle with variable bindings
"""
@spec string_to_handle(String.t, binding_struct, list) :: query_handle() | {:error,:qlc,{non_neg_integer() | {non_neg_integer(),pos_integer()},atom(),any()}}
def string_to_handle(str, bindings, opt \\ []) when is_binary(str) do
(String.ends_with?(str, ".") && str || str <> ".")
|> String.to_charlist
|> :qlc.string_to_handle(opt, bindings)
end
@doc """
string to qlc_handle with variable bindings.
string may be literal or variable. If string is variable or
function call, then expanding to string_to_handle/3 automatically.
elixir expression using bang macro are available to interpolation,
but expanding to erlang expression string. see examples.
## syntax
[Expression || Qualifier1, Qualifier2, ...]
Expression :: arbitary Erlang term (the template)
Qualifier :: Filter or Generators
Fiilter :: Erlang expressions returning bool()
Generator :: Pattern <- ListExpression
ListExpression :: Qlc_handle or list()
Qlc_handle :: returned from Qlc.table/2, Qlc.sort/2, Qlc.keysort/3
Qlc.q/2, Qlc.string_to_handle/2
## example
iex> require Qlc
iex> list = [a: 1,b: 2,c: 3]
iex> qlc_handle = Qlc.q("[X || X = {K,V} <- L, K =/= Item]",
...> [L: list, Item: :b])
...> Qlc.e(qlc_handle)
[a: 1, c: 3]
...> Qlc.q("[X || X = {K, V} <- L, K =:= Item]",
...> [L: qlc_handle, Item: :c]) |>
...> Qlc.e
[c: 3]
...> query_string = "[X || X = {K, V} <- L, K =:= Item]"
...> bindings = [L: list, Item: :b]
...> Qlc.q(query_string, bindings) |> Qlc.e()
[b: 2]
iex> ## Qlc.Record.defrecord(:user, [id: nil, name: nil, age: nil])
iex> list = [user(id: 1, name: :foo, age: 10),
...> user(id: 2, name: :bar, age: 20),
...> user(id: 3, name: :baz, age: 30)]
...> query_string = "[X || X <- L, \#{user!(X, :age)} < Age]"
...> bindings = [L: list, Age: 20]
...> Qlc.q(query_string, bindings) |> Qlc.e()
[{:user, 1, :foo, 10}]
"""
#@spec q(String.t, bindings, list) :: query_handle
defmacro q(string, bindings, opt \\ []) do
case is_binary(string) do
true ->
exprl = (String.ends_with?(string, ".") && string || string <> ".")
|> exprs()
|> Macro.escape()
quote bind_quoted: [exprl: exprl, bindings: bindings, opt: opt] do
Qlc.expr_to_handle(exprl, Qlc.bind(bindings), opt)
end
false ->
quote bind_quoted: [string: string, bindings: bindings, opt: opt] do
Qlc.string_to_handle(string, Qlc.bind(bindings), opt)
end
end
end
@doc """
eval qlc_handle
"""
@spec e(query_handle) :: list | {:error, module, any}
defdelegate e(qh), to: :qlc
@doc """
fold qlc_handle with accumulator
## example
iex> require Qlc
iex> list = [a: 1,b: 2,c: 3]
iex> qlc_handle = Qlc.q("[X || X = {K,V} <- L, K =/= Item]",
...> [L: list, Item: :b])
...> Qlc.fold(qlc_handle, [], fn({k,v}, acc) ->
...> [{v, k}|acc]
...> end)
[{3, :c}, {1, :a}]
iex> :mnesia.create_table(:t1, [{:attributes, [:k, :v]}])
{:atomic, :ok}
iex> :mnesia.transaction(fn() ->
...> Qlc.fold(qlc_handle, [], fn({k, v}, acc) ->
...> :mnesia.write({:t1, k, v})
...> [{:t1, k, v}|acc]
...> end)
...> end)
{:atomic, [{:t1, :c, 3}, {:t1, :a, 1}]}
iex> qh = Qlc.q("[X || X = {T, K, V} <- L, K =/= Item]",
...> [L: :mnesia.table(:t1), Item: :a])
iex> :mnesia.transaction(fn() ->
...> Qlc.fold(qh, [], fn({t, k, v}, acc) ->
...> IO.inspect({t, k, v})
...> :ok = :mnesia.write({:t1, k, v+1})
...> [{:t1, k, v+1}|acc]
...> end)
...> end)
{:atomic, [{:t1, :c, 4}]}
"""
@spec fold(query_handle, any, (any, any -> any), [any]) :: any
def fold(qh, a, f, option \\ []) do
:qlc.fold(f, a, qh, option)
end
@doc """
create qlc cursor from qlc_handle
(create processes)
"""
@spec cursor(query_handle) :: query_cursor
def cursor(qh), do: %Qlc.Cursor{ c: :qlc.cursor(qh) }
@doc """
delete qlc cursor
(kill processes)
"""
@spec delete_cursor(Qlc.Cursor.t) :: :ok
def delete_cursor(qc), do: :qlc.delete_cursor(qc.c)
@doc """
Returns some or all of the remaining answers to a Qlc.Cursor. Only
the owner of Qlc.Cursor can retrieve answers.
Optional argument number_of_answers determines the maximum number of
answers returned. Defaults to 10. If less than the requested number
of answers is returned, subsequent calls to next_answers return [].
Examples:
iex> require Qlc
iex> list = [a: 1, b: 2,c: 3, d: 4, e: 5, f: 6]
iex> qlc_handle = Qlc.q("[X || X = {K,V} <- L]",
...> [L: list])
...> qc = Qlc.cursor(qlc_handle)
...> Qlc.next_answers(qc, 2)
[{:a, 1}, {:b, 2}]
...> Qlc.next_answers(qc, 2)
[{:c, 3}, {:d, 4}]
...> Qlc.delete_cursor(qc)
:ok
"""
@spec next_answers(Qlc.Cursor.t, non_neg_integer) :: [any]
def next_answers(%Qlc.Cursor{c: c} = _qc, number_of_answers \\ 10) do
:qlc.next_answers(c, number_of_answers)
end
end