Converting Your Code to ARel 2.0

Comments

If you haven’t read Aaron Patterson’s post about the massive rewrite of ARel that happened for version 2.0, do yourself a favor, and go read it. It’s good stuff, and Aaron’s work rewriting ARel was a monumental feat of awesomeness. Plus, it’s responsible for tipping me off that someone else did a momentous rewrite: porting the venerable zombo.com to HTML5. Back? Good. Aaron mentioned, “for people using ARel directly, some methods have been removed, but all previous functionality should be available in one way or another.” Since I spent some time making sure that the old functionality was available to support the arel-2.0 branch of MetaSearch and MetaWhere, I thought I might tackle explaining some of the changes you’ll see.

Is this for me?

As Aaron said, “What do I have to do to upgrade? Hopefully nothing! The ActiveRecord API has remained exactly the same as in Rails 3.0.0.” If you’re using ActiveRecord 3.0, nothing has changed. If you’re using MetaSearch or MetaWhere, I’ll merge the ARel 2.0 code in as soon as Rails 3.0.2 is released. (Incidentally, I’m hoping to release 1.0 of both gems at or around that same time. Get your feature requests and bug reports in!) If you’re actually using ARel to construct advanced queries, as in Advanced ActiveRecord 3 Queries with Arel or using it in your own gems, read on.

No more Arel::Attribute::PREDICATES

MetaWhere used to rely on ARel to tell it a bit about the methods available for creating conditions on attributes by looking at the Arel::Attribute::PREDICATES constant. This was handy, in that it allowed MetaWhere to pick up new capabilities as they were added to ARel, but it was largely unnecessary, since it’s unlikely that those predicates are going to change very frequently. If your app does something similar, it’s worth noting that you’ll have to define your own array of predicates:

    PREDICATES = [
        :eq, :eq_any, :eq_all,
        :not_eq, :not_eq_any, :not_eq_all,
        :matches, :matches_any, :matches_all,
        :does_not_match, :does_not_match_any, :does_not_match_all,
        :lt, :lt_any, :lt_all,
        :lteq, :lteq_any, :lteq_all,
        :gt, :gt_any, :gt_all,
        :gteq, :gteq_any, :gteq_all,
        :in, :in_any, :in_all,
        :not_in, :not_in_any, :not_in_all
      ]

not_matches renamed to does_not_match

You may have noticed the absence of not_matches in the list above. I took this opportunity to change the name of the method for creating NOT LIKE conditions on an attribute, because not_matches reads like caveman-speak. It works otherwise identically:

ruby-1.9.2-p0 > a = Article.arel_table
 => #<Arel::Table:0x00000101b5bfd8 @name="articles", @engine=ActiveRecord::Base,
    @columns=nil, @aliases=[], @table_alias=nil, @primary_key=nil>
ruby-1.9.2-p0 > a[:title].does_not_match('%lame%').to_sql
 => "\"articles\".\"title\" NOT LIKE '%lame%'"

Arel::Predicates -> Arel::Nodes

The various pieces of data that make up the query as represented in the AST are in the Arel::Nodes namespace, not Arel::Predicates, now, and some have had their names changed slightly. Arel::Predicates:: GreaterThanOrEqualTo is now Arel::Nodes::GreaterThanOrEqual, for instance. They’re all neatly organized in the lib/arel/nodes directory. Arel::SqlLiteral now lives under Arel::Nodes, as well, a nice move for consistency’s sake.

The death of not/complement

It was kind of neat, being able to toggle between a selection and its complement, but it wasn’t really being used by anyone that I know of, so I didn’t re-implement it in 2.0.

Arel::Predicates::Any/All -> Arel::Nodes::Grouping

In ARel 1.x, I had added a couple of predicate types to contain a bunch of ANDed or ORed conditions in one set of parentheses, because I didn’t like how noisy the continues nesting of parentheses got once you started chaining multiple Attribute#ors. These are gone, so I needed to change code like...

ARel 1.x

Arel::Predicates::All.new(*builder.build_predicates_from_hash(self, parent || builder.join_dependency.join_base))

...to...

ARel 2.x

    predicates = builder.build_predicates_from_hash(self, parent || builder.join_dependency.join_base)
    if predicates.size > 1
      first = predicates.shift
      Arel::Nodes::Grouping.new(predicates.inject(first) {|memo, expr| Arel::Nodes::And.new(memo, expr)})
    else
      predicates.first
    end

It gets the noisy parentheses back, but works just fine. Truth be told, it’s probably safer to keep the parentheses in place, anyway, since operator precedence is not generally something best left to chance. Speaking of any/all…

Changes to the any/all methods

In ARel 1.x, the *_any/all methods for OR/AND took multiple arguments, using the splat operator to convert them into an array. In ARel 2.x, we cut out the middleman and just take an array argument to begin with.

ARel 1.x

    ruby-1.9.2-p0 > a[:title].eq_any('Hello', 'World').to_sql
     => "("articles"."title" = 'Hello' OR "articles"."title" = 'World')"

ARel 2.x

    # WRONG!
    ruby-1.9.2-p0 > a[:title].eq_any('Hello','World').to_sql
    ArgumentError: wrong number of arguments (2 for 1)

    # WORKS!
    ruby-1.9.2-p0 > a[:title].eq_any(['Hello','World']).to_sql
     => "("articles"."title" = 'Hello' OR "articles"."title" = 'World')"

Conclusion

That’s all that comes to mind right now. Converting MetaWhere and MetaSearch to use ARel 2.x wasn’t nearly as painful as I’d expected it to be, and as I started hacking around in ARel to add the missing methods, I started to really appreciate the AST/Visitor design pattern that Aaron talked about yesterday in his post. Additions were largely painless to make, and if you ever want to teach ARel to handle a new type of node in its AST, it’s as simple as opening up a single class and writing one method.

But I’ll save that for another post.

comments powered by Disqus