mathieuprog

mathieuprog

I’ll start right with an example :backhand_index_pointing_down:

User
|> QueryBuilder.where(firstname: "John", city: "Anytown")
|> QueryBuilder.where({:age, :gt, 30})
|> QueryBuilder.order_by(lastname: :asc)
|> QueryBuilder.preload([:role, authored_articles: :comments])
|> Repo.all()

With associations:

User
|> QueryBuilder.where([role: :permissions], name@permissions: "delete")
|> Repo.all()

Query Builder allows to build and compose Ecto queries based on data.
Concise, no need to deal with bindings and macros.

Its primary goal is to allow Context functions to receive a set of filters and options:

# in a Controller
Blog.list_articles(preload: [:comments], order_by: [title: :asc])
Blog.list_articles(preload: [:category, comments: :user])

This avoids having to create many different functions in the Context for every combination of filters and options, or to create one general function that does too much to satisfy all the consumers.

The calling code (e.g. the Controllers), can now retrieve the list of articles with different options. In some part of the application, the category is needed; in other parts it is not; sometimes the articles must be sorted based on their title; other times it doesn’t matter, etc.

The options may be added to the query as shown below:

# in the Blog context
def get_article_by_id(id, opts \\ []) do
  QueryBuilder.where(Article, id: id)
  |> QueryBuilder.from_list(opts)
  |> Repo.one!()
end

Inspired by the libraries token_operator and ecto_filter.

More examples are available in the doc:

https://github.com/mathieuprog/query_builder

Showing Posts 21 to 12

mathieuprog

mathieuprog OP

QueryBuilder v1.0.0 has been released! :tada:

New features:

  • add the Extension module allowing to easily extend QueryBuilder with user’s own functions
  • add support for offset and limit operations
  • differentiate field names vs an atom value with the syntax :<field_name>@self vs :<atom_value>

Thanks to @onomated that did all of the work, of high quality!

Because QueryBuilder is considered stable and there have been breaking changes, it has been bumped to 1.0.

Breaking changes:

.1.
QueryBuilder.order_by(User, lastname: :asc) becomes
QueryBuilder.order_by(User, asc: :lastname)

.2.
QueryBuilder.where(User, {:name, :eq, :nickname}) becomes
QueryBuilder.where(User, {:name, :eq, :nickname@self})
(concerns a comparison of two fields of the root schema)

onomated

onomated

Ah, don’t know why I didn’t grok the extension approach you suggested at first. I like it! That should work well. I’ll give it a shot after we work out the resolution to Atom values in where args treated as schema fields · Issue #2 · mathieuprog/query_builder · GitHub

mathieuprog

mathieuprog OP

I thought rather something like:

defmodule MyApp.QueryBuilder do
  use QueryBuilder.Extension # inject defdelegate and from_list

  def search(query, field, search_term) do
    # code
  end
end

and in QueryBuilder.Extension have __using__ injecting the defdelegates and from_list.

Except if I’m missing something :smiley:

Note that it really is for the from_list function, otherwise I would have suggested to not extend QueryBuilder at all with macros, and just do something like:

User
|> QueryBuilder.where(firstname: "John")
|> QueryBuilderExt.search(...)

However the user might want to use from_list with the new functionality.

I prefer reflection because I think it’s not expensive at all, as I think the values from these functions for reflection are calculated at compile time.

onomated

onomated

Ah, so the thought is allow clients create an extension module that are passed in to QueryBuilder like so?

use QueryBuilder, extension: MyApp.QueryBuilder.SearchFunction

MyApp.QueryBuilder

defmacro __using__(opts) do
    extension = Keyword.get(opts, :extension, QueryBuilder.NullExtension)
    # confirm its a module with __using__
    quote do
      require QueryBuilder.Schema
      QueryBuilder.Schema.__using__(unquote(opts))
    
      require unquote(extension)
      unquote(extension).__using__(unquote(opts))
    end
  end

The clients can use just query builder as currently documented. No defdelegates required and no from_list overrides needed. Its worth a shot, though injecting macros can be complex if the extension macros have other macro operations such as setting module attributes which my extension module does. It’s worth a try to see how that plays out in my case.

That’s one approach, or introducing :self as a sentinel for the current schema to resolve to a field. So:

User
|> QueryBuilder.where([auth_method: :email@self, auth_val: "someone@example.com"])
|> Repo.one!()

Resolves to the current functionality as today, and:

User
|> QueryBuilder.where([auth_method: :email, auth_val: "someone@example.com"])
|> Repo.one!()

works like Ecto does today, while modeling the current association syntax you have.

Thoughts? Either way, it’s worth creating issues, and I can take a stab at working on the functionality suggested in a new branch or just fork the repo and PR back in after implementing. Definitely need to address the atom field referencing to use this lib

mathieuprog

mathieuprog OP

The idea is to simply inject all those defdelegates and the from_list function into the custom module. Except if I missed something? use allows you to inject code.

Nice, obviously I didn’t think about that case:) I think it’s easily solved with some reflection:

As a note to self, I think I’d also like at some point to implement select, because QueryBuilder is limited to fetching full entities right now.

onomated

onomated

Interesting. Can you highlight a bit more about what you’re thinking here? This is a pattern I haven’t seen. Not as much of a power user of Elixir just yet, so it’ll be an interesting learning experience.

Great point. Yes this does align with the Ecto Query API arg scheme.

One issue I have run into is that QueryBuilder assumes atom values are fields. So queries like this:

args_collected_somewhere = [auth_method: :email, auth_val: "someone@example.com"]
User
|> QueryBuilder.where(args_collected_somewhere)
|> Repo.one!()

is resolving to the following sql and error:

SELECT a0.* FROM "users" AS a0 WHERE ((a0."auth_val" = $1) AND (a0."auth_method" = a0."email"))

** (Postgrex.Error) ERROR 42703 (undefined_column) column a0.email does not exist

This is problematic as I use enums (with GitHub - gjaldon/ecto_enum: Ecto extension to support enums in models · GitHub) somewhat extensively in my data model, so attempting to “know enum fields” and programmatically convert them to strings first will be tedious. Is there a way to be explicit about values actually being db columns and only then should they be auto-binded as fields? I can file an issue if there is no workaround for this at the moment. This was caught in my regression tests as I’m trying to incorporate QueryBuilder.to_list where this worked fine prior:

args_collected_somewhere = [auth_method: :email, auth_val: "someone@example.com"]
User
|> Repo.get_by!(args_collected_somewhere)
mathieuprog

mathieuprog OP

It’s a good idea to offer a module to allow extension! I don’t see how a behaviour would help here as you want to extend the QueryBuilder module with your own custom functions with arbitrary function names.
What we could offer though is some code injection: use QueryBuilder.Extension and this will allow to inject the from_list as well to accept the new custom functions.
If you want to change your codebase already you could make a PR, or you can just wait until I’ll get back into it:)

The lib is in alpha so I’m allowed to break it? :smiley: But what I will do I think is make a stable version 1.x and it won’t bother anyone. Having e.g. asc: :lastname also follows the Ecto Query API option.

onomated

onomated

@mathieuprog Tested out the custom sql functionality for order_bys and it works great. I’ve updated the gist above with my QueryBuilder extension module that utilizes dynamic bindings in where and order_by methods. Looking forward to the next release with these features.

Thanks once again for supporting the ask. Cheers!

onomated

onomated

@mathieuprog, here’s a gist where I started expanding the QueryBuilder functionality:

It’s currently work in progress as I haven’t added the necessary logic to utilize your changes to order_by in your master branch. Maybe making QueryBuilder a behavior with default implementations will make extending a lot more straightforward i.e. remove the need to defdelegate functions.

An option to avoid breaking your order_by interface would be to support a map of the order by spec, and just check if its a keyword list or map. So supporting something along the lines of:

QueryBuilder.order_by(User, %{direction: :asc, field: :last_name})

Just a thought.

I’ll expand my search functionality to also utilize your current changes for order_by as well and update the gist afterwards

onomated

onomated

Thanks @mathieuprog! That’s great. I’ll try out the master branch and share my implementation with you.

Where Next? Top

Trending in Announcing Top

woylie
Flop is an Elixir library that applies filtering, ordering and pagination parameters to your Ecto queries. offset-based pagination with...
New
MRdotB
I needed to reuse React components from my Chrome extension in my Phoenix/LiveView backend. I noticed that for Svelte/Vue, there are live...
New
woylie
I released Doggo, a collection of unstyled Phoenix components. https://github.com/woylie/doggo Features Unstyled Phoenix components....
New
JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
anuaralfetahe
Hello Published a new library - ProcessHub! ProcessHub is a library designed to manage process distribution within the Elixir cluster. ...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New
sergio
It’s not that it’s vocabulary is too advanced. It’s something worse. I get lost trying to follow even a paragraph written by Claude. It’...
New
AstonJ
This showed up on my feed.. anyone heard of it? Just hype? Ox Alpha is a reasoning model designed for coding, sustained ag...
New
bartblast
Hey folks, I just published a post about Hologram’s funding and where the project goes next - the short version: Curiosum as Main Spons...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews