« back to all cards in this deck
Posted over 3 years ago. Visible to the public. Repeats.

Know the side effects of using hashes for conditions

Find conditions for scopes can be given either as an array (:conditions => ['state = ?', 'draft']) or a hash (:conditions => { 'state' => 'draft' }). Each of these options has considerable side effects you should know about. This applies to both Rails 2 and old patch levels of Rails 3. The issue is fixed in Rails 3.0.5, has been re-introduced in Rails 3.2 and has been fixed again in Rails 4. If you're using our Rails 3.x forks you are not affected. If you are using Rails 2.3.x you are always affected, even if you are using our forks or Rails LTS.

In a nutshell, when you chain two scopes with hash conditions on the same attribute, the second scope will overwrite (and not join!) conditions from the first scope:

>> Contract.scoped(:conditions => { :region_id => 5 }) => SELECT * FROM `contracts` WHERE (`contracts`.`region_id` = 5) >> Contract.scoped(:conditions => { :region_id => 5 }).scoped(:conditions => { :region_id => 7 }) => SELECT * FROM `contracts` WHERE (`contracts`.`region_id` = 7)

Note how the last query did not select on region_id = 5 AND region_id = 7 as expected.

Unfortunately we cannot use array-conditions all the time to work around this. The reason is that when you build a record from a scope with array conditions, the resulting record will not have the attributes that defined that scope, and this behavior can be quite useful.

Because you cannot have the best from both worlds you should understand this article and make informed choices about how to define your conditions. Also see the Best Practice section below.

The problem in detail

Consider this Rails class and the following articles:

class Article < ActiveRecord::Base named_scope :available, :conditions => { :state => [ 'unassigned', 'draft' ] } end #<Article id: 1, title: "Cooking noodles", state: "unassigned"> #<Article id: 2, title: "How to rock", state: "draft"> #<Article id: 3, title: "Skip work", state: "deleted">

If you want to fetch all available articles, you would do something like this:

>> Article.available # SELECT * FROM `articles` WHERE (`articles`.`state` IN ('unassigned','draft')) => [ #<Article id: 1, title: "Cooking noodles", state: "unassigned">, #<Article id: 2, title: "How to rock", state: "draft"> ]

Let's say you wanted to include deleted articles. Removing the available scope is one way but you might also add another scope – which results in an unexpected effect:

>> Article.available.scoped(:conditions => { :state => 'deleted' }) # SELECT * FROM `articles` WHERE ((`articles`.`state` = 'deleted')) => [ #<Article id: 3, title: "Skip work", state: "deleted"> ]

Here the conditions inside the last scope will overwrite any previous conditions on the state attribute; they will not be combined.

Note that the unexpected overwriting of conditions only happens when chaining scopes. Hash conditions are joined as expected when using find (but scope chains are not going away).

Security implications

This can be a security problem when you use scope to restrict what a controller can see and edit, but also allow the user to further filter that scope.

One possible scenario are filters defined through special patterns in a search field, like "state:draft". If your controller parses HTTP parameters and adds such filters as conditions to your scope chain you must not supply those conditions as a hash (as seen above). To be on the safe side, use SQL fragments which are never overridden:

>> Article.available.scoped(:conditions => [ 'articles.state = ?', 'deleted' ]) # SELECT * FROM `articles` WHERE ((articles.state = 'deleted') AND (`articles`.`state` IN ('unassigned','draft'))) => []

Unfortunately you can no longer build a record from a scope with array conditions and expect the resulting record to have the attributes that defined that scope. This is why hash conditions are not going away.

Situation in Rails 3

  • This issue is fixed in Rails 3.0.5+.
  • This is still broken in at least Rails 3.0.1., but in an even more devilish way.
  • When using scope chains with hash conditions in Rails 3.0.1, the last two conditions for the same attribute will be combined. When you add a third condition, the first one will be discarded.
  • Chains that use the new where scope seem to be unaffected, but the moment when you chain in scoped or a named scope you are back to the behavior above.

Best practice

  • We are trying to get this patched for a future 2.3.x maintenance release. Until then you need to work around this.
  • Keep using hash conditions for reasons of elegance and the ability to build records from such scopes.
  • As soon as user input comes into play you should use our chain_safely pseudo-scope to turn a scope with hash conditions into a scope with array conditions.

Author of this card:

Arne Hartherz
About this deck:
We are makandra and do test-driven, agile Ruby on Rails software development.
Posted by Arne Hartherz to makandropedia