Ex_sieve - Filtering solution for Ecto, it builds Ecto.Query structs from a ransack inspired query language

ex_sieve is a library offering a filtering solution for Ecto. It builds Ecto.Query structs from a ransack inspired query language.

It was originally developed by @valyukov, a few months ago I joined the project and started extending his great work. I have just released version 0.8.0, it includes a lot of new features and improvements, you can take a look at the changelog and at the documentation.

Any feedback is welcome!

7 Likes

I’m not familiar with ransack. Can you describe a good use case?

The documentation contains some example.

The main use case is to create a filter query on a list view taking as input user supplied URL query parameters. But this is not the only possible use, the Repo.filter function provided by the library takes as input a map that can be built in many different ways.

1 Like

I’m wondering why you didn’t implement Ecto.Queryable for a struct holding the filter input. then this would be usable with all Repo functions and not just your custom injected filter/2. Misunderstood Repo.filter, which does return a query.

1 Like

Yes, maybe the API it’s not the better one but I didn’t consider changing the original one before releasing 1.0.0. Your suggestion it’s indeed good, I will investigate on it, thanks!

Also for release 1.0 I would like to remove the remaining usage of Ecto internal APIs. In particular I didn’t find a way of building dynamic joins without relying on Ecto.Query.Builder.Join.join/10.

1 Like

I did not look into your code, but generally you can simply write a macro for it :wink:

I admit I am not the bigger expert on macros but I tried with no success. The main problem is that all parameters needed for building the join are known only at runtime.

Any suggestion is welcome!

Not sure if this code is best, but at least it does not depend on private API:

  defp do_apply_join({parent, relation} = parent_relation, query)
       when is_atom(relation) and (is_binary(parent) or is_nil(parent)) and
              (is_atom(query) or :erlang.map_get(:__struct__, query) == Ecto.Query) do
    as = join_as(parent_relation)
    query |> Macro.escape() |> join_build(parent_relation, as) |> elem(0)
  end

  defp join_build(query, {nil, relation}, as) do
    Code.eval_quoted(
      quote do
        join(unquote(query), :inner, [p], a in assoc(p, unquote(relation)), as: unquote(as))
      end
    )
  end

  defp join_build(query, {string, relation}, as) do
    parent = String.to_atom(string)

    Code.eval_quoted(
      quote do
        join(unquote(query), :inner, [{unquote(parent), p}], a in assoc(p, unquote(relation)), as: unquote(as))
      end
    )
  end

Notes:

  1. Since we are using eval here I have added some guards. To make sure nothing would explode you may add extra runtime checks (regex?) for parent and relation contents.
  2. Make sure to add import Ecto.Query and remove unused alias Ecto.Query.Builder.Join and join_binding/1 private function.
2 Likes

Nice! Thanks, I haven’t thought about using Code.eval_quoted

Ex_sieve seems very powerful, yet there’s also a simpler library - Flop - also in early dev stage. Kudos to authors and contributors!

Can someone well-versed in Ecto comment about the benefits and downsides in regard to how both libraries approach DB querying, eg. security, performance, query-composing and general dev experience?

2 Likes