squeel icon indicating copy to clipboard operation
squeel copied to clipboard

Allow making operations on sifters and having them in a group

Open odedniv opened this issue 11 years ago • 11 comments

Operators don't work on sifters, so you always have to wrap them like so: _(sift(:my_sifter)). This is a patch I made that always returns the Sifter object wrapped withing Grouping.

Given the following model:

class User < ActiveRecord::Base
  sifter(:my_sifter) { id + 5 }
end

Here is some examples of the results before the patch:

User.where{sift(:my_sifter) > 5}.to_sql
 => "NoMethodError: undefined method `>' for #<Squeel::Nodes::Sifter:0x00000009bffbb8>"
User.select{sift(:my_sifter).as(column)}.to_sql
 => "NoMethodError: undefined method `as' for #<Squeel::Nodes::Sifter:0x00000009c50928>"
Account.joins(:user).where{user.sift(:my_sifter) > 2}.to_sql
 => "NoMethodError: undefined method `>' for #<Squeel::Nodes::KeyPath:0x00000009190218>"

Examples after the patch (and the expected result for the above examples):

User.where{sift(:my_sifter) > 5}.to_sql
 => "SELECT `users`.* FROM `users`  WHERE (`users`.`id` + 5) > 5"
User.select{sift(:my_sifter).as(column)}.to_sql
 => "SELECT (`users`.`id` + 5) AS column FROM `users` "
Account.joins(:user).where{user.sift(:my_sifter) > 2}.to_sql
 => "SELECT `accounts`.* FROM `accounts` INNER JOIN `users` ON `users`.`id` = `accounts`.`user_id` WHERE (`users`.`id` + 5) > 2"

Also, you can't have sifter within #group, so I added sifter visitation.

Before:

User.group{sift(:my_sifter)}.to_sql
 => "TypeError: Cannot visit Squeel::Nodes::Sifter"

After:

User.group{sift(:my_sifter)}.to_sql
 => "SELECT `users`.* FROM `users`  GROUP BY (`users`.`id` + 5)"

odedniv avatar Dec 02 '13 10:12 odedniv

Alternatively you could just simply create a grouping inside the sifter, since it gets instance-evaled in the DSL. There could be cases where adding a grouping could be harmful, e.g. in SQL function evaluation.

the8472 avatar Dec 04 '13 17:12 the8472

This doesn't work, still getting the error when doing the following:

class User < ActiveRecord::Base
  sifter(:my_sifter) { _(id + 5) }
end

If only it was that simple...

odedniv avatar Dec 05 '13 08:12 odedniv

Oh right, it gets lazy-evaluated. I guess in that case the modules and operator aliases should be included in the sifter node? Like https://github.com/activerecord-hackery/squeel/blob/master/lib/squeel/nodes/grouping.rb#L6-22

My main concern is that always wrapping things in a grouping might break in some edge cases.

the8472 avatar Dec 05 '13 08:12 the8472

I myself was more afraid of copying code from one place to another, felt safer to wrap with a grouping. I bet @ernie would have a much better idea on how to implement this

odedniv avatar Dec 05 '13 08:12 odedniv

Probably reasonable to include the operators on the sifters. I am trying to think of a situation in which (apart from a stylistic opposition to unnecessary parens) a user would actively want to avoid parentheses around the result of a sifter. If I can't come up with any, then it would make sense to update the visitor to wrap the result in an Arel grouping node, without putting a grouping node into the Squeel AST.

ernie avatar Dec 05 '13 15:12 ernie

Functions that take keyword operators as part of their syntax would explode with extra parens.

Consider COUNT(DISTINCT id) as foo vs. COUNT((DISTINCT id)) as foo. So if you use a sifter inside a function node, combined with operator nodes it would fail.

the8472 avatar Dec 05 '13 16:12 the8472

Bah. Good call, @the8472. So, I'd say the better option really would be to simply wrap the sifter in a grouping node inside the Squeel DSL itself, and do whatever comparisons you want to afterward, @odedniv. That wouldn't require any changes to Squeel and would make the intent more obvious in your own code (I think).

ernie avatar Dec 09 '13 14:12 ernie

@ernie, this means I have to wrap every use of that sifter with _(sift(...)), while grouping is not really what I wanted to achieve. I just want to be able to create a complicated equation in the model that I can later use anywhere. Example:

class User < ActiveRecord::Base
  sifter(:c) { a + b }
  def c
    a + b
  end
end

User.where{sift(:c) == 5}.count
User.first.c == 5

I don't want the outside world to know how c is calculated. Wrapping every use of c with a grouping is very tedious... (I would actually prefer it didn't know it was calculated at all and just use c == 5 withing the DSL)

odedniv avatar Dec 10 '13 07:12 odedniv

How is writing _() in every query any more "tedious" than writing .where{} for every query? It might not be pretty, but from my experience writing complex queries rarely is pretty. I often end up with multiple lines of arr.reduce(self){|a,b| a.__send__(b)}, subselects, literals, aliasing and what-not.

the8472 avatar Dec 10 '13 09:12 the8472

For the above example, I don't count where{c > 5} as a complex query, and the "outside" world doesn't know how c is implemented and therefore shouldn't need to know that it needs _() around it. If c wanted it around it should have added it itself (within the sifter).

I don't see a reason not to add the operator methods to Sifter, just like you have it for anything else (Literal etc).

odedniv avatar Dec 10 '13 10:12 odedniv

and the "outside" world doesn't know how c is implemented

well, squeel is an AST-abstraction over SQL/Arel/active-record relations. It's still relatively close to the database. I don't think you should try to hide logic/pretend that it behaves like a regular column when it isn't.

Of course you could monkey-patch some short-hand into the DSL that uses some operator character that's not in use or something like that. E.g. def !(sifter) ... end could map to _(sift(sifter))

Another idea would be to make squeel aware of aliased columns in the select clause, that way you could write:

User.select{_(complex_calculation).as(c)}.where{c == 3} and have a scope on the class level simply include the select clause. right now you have to write where{c == 3}, that could be improved i guess.

the8472 avatar Dec 10 '13 12:12 the8472