lib/qlc.ex

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