Posted over 4 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.
Growing Rails Applications in Practice
Check out our new e-book:
Learn to structure large Ruby on Rails codebases with the tools you already know and love.

Author of this card:

Avatar
Arne Hartherz
About this deck:
We are makandra and do test-driven, agile Ruby on Rails software development.
License for source code

License for source code

All source code included in the card Know the side effects of using hashes for conditions is licensed under the license stated below. This includes both code snippets embedded in the card text and code that is included as a file attachment. Excepted from this license are code snippets that are explicitely marked as citations from another source.

The MIT License (MIT)

Copyright (c) 2010-2015 makandra GmbH

Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the
following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
Posted by Arne Hartherz to makandropedia