Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Stop writing SQL in your ActiveRecord scopes: embrace Ruby!
branch: master

This branch is 64 commits behind samleb:master

Fetching latest commit…

Cannot retrieve the latest commit at this time

Failed to load latest commit information.
gemfiles
lib
spec
.gitignore
.travis.yml
Appraisals
CHANGELOG.md
Gemfile
LICENSE
README.md
Rakefile
sexy_scopes.gemspec

README.md

SexyScopes

Gem Version Dependencies Code Climate Build Status Coverage Status

Write beautiful and expressive ActiveRecord scopes without SQL

SexyScopes is a gem that adds syntactic sugar for creating ActiveRecord scopes in Ruby instead of SQL. This allows for more expressive, less error-prone and database independent conditions.

WARNING: This gem requires Ruby >= 1.9.2 and ActiveRecord >= 3.1.

Usage & Examples

Let's define a Product model with this schema:

# price     :integer
# category  :string
# visible   :boolean
# draft     :boolean
class Product < ActiveRecord::Base
end

Now take a look at the following scope:

scope :visible, where("category IS NOT NULL AND draft = ? AND price > 0", false)

Hum, lots of SQL, not very Ruby-esque...

With SexyScopes

scope :visible, where((category != nil) & (draft == false) & (price > 0))

Much better! Looks like magic? It's not.

category, draft and price in this context are methods representing your model's columns. They respond to Ruby operators (like <, ==, etc.) and can be combined with logical operators (& and |) to express complex predicates.


Let's take a look at another example with these relations:

# rating:  integer
class Post < ActiveRecord::Base
  has_many :comments
end

# post_id:  integer
# rating:   integer
class Comment < ActiveRecord::Base
  belongs_to :post
end

Now let's find posts having comments with a rating > 3.

Without SexyScopes

Post.joins(:comments).merge Comment.where("rating > ?", 3)
# ActiveRecord::StatementInvalid: ambiguous column name: rating

Because both Post and Comment have a rating column, you have to give the table name explicitly:

Post.joins(:comments).merge Comment.where("comments.rating > ?", 3)

Not very DRY, isn't it ?

With SexyScopes

Post.joins(:comments).where Comment.rating > 3

Here you have it, clear as day.

How does it work ?

SexyScopes is essentially a wrapper around Arel attribute nodes.

It introduces a ActiveRecord::Base.attribute(name) class method returning an Arel::Attribute object, which represent a table column with the given name, that is extended to support Ruby operators.

For convenience, SexyScopes dynamically resolves methods whose name is an existing table column: i.e. Product.price is actually a shortcut for Product.attribute(:price).

Please note that this mechanism won't override any of the existing ActiveRecord::Base class methods, so if you have a column named name for instance, you'll have to use Product.attribute(:name) instead of Product.name (which is the class actual name, i.e. "Product").

Here is a complete list of operators, and their Arel::Attribute equivalents:

  • For predicates:

    • ==: eq
    • =~: matches
    • !~: does_not_match
    • >=: gteq
    • > : gt
    • < : lt
    • <=: lteq
    • !=: not_eq
  • For combination

    • &: and
    • |: or
    • ~: not (unfortunately, unary prefix ! doesn't work with ActiveRecord)

Advanced Examples

# radius:  integer
class Circle < ActiveRecord::Base
  # Attributes can be coerced in arithmetic operations
  def self.perimeter
    2 * Math::PI * radius
  end

  def self.area
    Math::PI * radius * radius
  end
end

Circle.where Circle.perimeter > 42
# SQL: SELECT `circles`.* FROM `circles`  WHERE (6.283185307179586 * `circles`.`radius` > 42)
Circle.where Circle.area < 42
# SQL: SELECT `circles`.* FROM `circles`  WHERE (3.141592653589793 * `circles`.`radius` * `circles`.`radius` < 42)

class Product < ActiveRecord::Base
  predicate = (attribute(:name) == nil) & ~category.in(%w( shoes shirts ))
  puts predicate.to_sql
  # `products`.`name` IS NULL AND NOT (`products`.`category` IN ('shoes', 'shirts'))

  where(predicate).all
  # SQL: SELECT `products`.* FROM `products` WHERE `products`.`name` IS NULL AND 
  #      NOT (`products`.`category` IN ('shoes', 'shirts'))
end

Installation

Add this line to your application's Gemfile:

gem 'sexy_scopes'

And then execute:

bundle

Or install it yourself as:

gem install sexy_scopes

Then require it in your application code:

require 'sexy_scopes'

Contributing

Report bugs or suggest features using GitHub issues.

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

TODO

  • Document the sql_literal method and how it can be used to create complex subqueries
  • Handle associations (i.e. Post.comments == Comment.joins(:posts) ?)

Copyright

SexyScopes is released under the MIT License.

Copyright (c) 2010-2013 Samuel Lebeau, See LICENSE for details.

Something went wrong with that request. Please try again.