Active Record Query Plugin
Imaginatively named, I know.
This is a proof-of-concept I want to be considered for inclusion in Rails 4.
Synopsis
Person.where { |q| q.name == 'Jon' && q.age == 22 }
# => SELECT * FROM people WHERE people.name = 'Jon' AND people.age = 22
Person.any { |q| q.name == 'Jon' || q.age == 22 }
# => SELECT * FROM people WHERE people.name = 'Jon' OR people.age = 22
Person.joins(:projects).where { |q| q.projects.name == 'Rails' }
# => SELECT * FROM people INNER JOIN projects WHERE projecs.name = 'Rails'
Person.any { |q| q.name = 'Jon' || q.and { q.age >= 10 && q.age < 30 } }
# => SELECT * FROM people WHERE people.name = 'Jon' OR (people.age >= 10 AND people.age < 30)
Why?
-
Makes people (who use it) less vulnerable to accidentally introducing SQL injection points
-
If we have the AST, we can draw inferences from it in Active Record. For example, in Rails 4,
Post.includes(:comments).where('comments.created_at > x')
will no longer
JOIN
thecomments
table. You have to do:Post.includes(:comments).where('comments.created_at > x').references(:comments)
With the AST available to us, we can automatically infer that
comments
is referenced.
Prior art
Design goals
-
Don't use
instance_eval
. Here be dragons. -
Make the syntax as easy on the eyes as possible.
&&
and||
cannot be redefined as methods (more about that below), but&
and|
can be. However, they bind tighter than comparison operators, resulting in lots of unpleasant parentheses:Person.where { |q| (q.name == 'Jon') & (q.age == 22) }
&
and|
are also commonly used for set operations, which have an existing meaning in SQL.
Implementation
The q
object is mutable.
Writing,
Post.where { |q| q.name == 'Jon' && q.age == 22 }
has the identical effect as writing,
Post.where do |q|
q.name == 'Jon'
q.age == 22
end
The ==
method return true
to prevent the &&
operator from
short-circuiting. Using &&
is purely syntactical sugar.
Note that,
Post.where { |q| q.name == 'Jon' || q.age == 22 }
would short-circuit.
So whilst this wouldn't throw an error, it at
least would result in something that the user would (hopefully) notice
is wrong quite quickly (because age
would be missing from the query
entirely).
This 'hack' is definitely the worst thing about the idea, but I think that with adaquate documentation it wouldn't pose too much problem, and it reads quite naturally.
Supported operators
q.name == 'Jon'
q.name != 'Jon'
q.name =~ 'J%'
q.name !~ 'J%'
q.name > 22
q.name < 22
q.name >= 22
q.name <= 22
q.name.in ['Jon', 'Emily']
q.name.not_in ['Jon', 'Emily']
Possible alternative syntaxes
Option 1 (Squeel syntax)
Person.where { |q| (q.name == 'Jon') | (q.age == 22) }
- Doesn't require the
q.and { ... }
thing forAND
-within-OR
orOR
-within-AND
- Possibly confusing use of set operators
- Lots of parentheses
Option 2
Person.where { |q| q.name == 'Jon'; q.age == 22 }
- This works already, it's a question of what we advocate / document.
Option 3
Person.any { |q| q[:name] == 'Jon' || q.projects[:name] == 'Rails' }
- Draws a clearer distinction between table and column names
- A bit more noisy