esqueleto icon indicating copy to clipboard operation
esqueleto copied to clipboard

Query with variable number of joins

Open jolmg opened this issue 5 years ago • 3 comments

For example, let's say that I have a list representing a category path, like

Tool > Screwdriver > Phillips

and I want to find the corresponding record in a table "category" with columns "parent_id" and "name".

In Ruby with ActiveRecord, I could do something like:

category_path = ["Tool", "Screwdriver", "Phillips"]
leaf_cat_name, *parent_cat_names = category_path.reverse
query_init = Category.where(name: leaf_cat_name)
category = parent_cat_names.to_enum.with_index.reduce(query_init) do |query, (name, i)|
    query.joins(<<~SQL)
      join categories cat#{i + 1}
        on #{i.zero? ? "categories" : "cat#{i}"}.parent_id = cat#{i + 1}.id
    SQL
    .where("cat#{i + 1}.name = :name", name: name)
  end
  .first!

That results in a query like:

select  `categories`.*
  from `categories`
  join categories cat1
    on categories.parent_id = cat1.id
  join categories cat2
    on cat1.parent_id = cat2.id
  where `categories`.`name` = 'Phillips'
    and (cat1.name = 'Screwdriver')
    and (cat2.name = 'Tool')
  order by `categories`.`id` asc
  limit 1

Could something like that be done with Esqueleto? preferably without having to manually handle the aliasing. Right now, it seems Esqueleto's joins are limited to a static set based on the parameter of the function passed to from.

I was thinking of perhaps having functions like:

leftJoinOn
  :: PersistEntity a
  => (SqlExpr (Entity a) -> SqlExpr (Value Bool))
  -> SqlQuery (SqlExpr (Entity a))

to be able to write the equivalent to that Ruby code, which could be like

let catNames = ["Tool", "Screwdriver", "Phillips"]
    leafCatName : parentCatNames = reverse catNames

select $ from $ \ leafCat -> do
  where_ $ leafCat ^. CategoryName =. val leafCatName
  -- Please excuse the free-point
  rootCat <- ((.).(.)) flip flip foldlM leafCat parentCatNames $ \ subCat name -> do
    supCat <- joinOn $ \ supCat ->
      subCat ^. CategoryParentId =. just (supCat ^. CategoryId) 
    where_ $ supCat ^. CategoryName =. val name
    return supCat
  where_ $ rootCat ^. CategoryParentId =. nothing  
  return leafCat

I haven't read the source in detail, but I see that there's State IdentState in SqlQuery. If no aliasing is done automatically yet, perhaps the aliases could be kept track-of in there.

I could perhaps work on this next week, but I was wondering if there would be any complications this would imply or anything that I may have overlooked that someone here could advise me on.

jolmg avatar Feb 06 '20 19:02 jolmg

The work on #172 should address this. We started with a similar idea for the syntax but ran in to issues with mixing the old style of introducing tables into the query and these standalone joins. Please take a look at that PR and feel free to leave comments on anything that you feel is missing over there. Currently it is being held up by bad type errors but hopefully we can land it soon™️

belevy avatar Feb 06 '20 20:02 belevy

That's really exciting, especially the fact that it also seems to work towards being able to build subqueries in the joins, and unions! I'm practically salivating now. I'm going to look over it over the next few days.

jolmg avatar Feb 06 '20 20:02 jolmg

@jolmg Are you able to accomplish what you want using the new syntax or is this still an issue in your opinion?

belevy avatar Mar 31 '20 22:03 belevy