Space Vatican

Ramblings of a curious coder

When Ducks Go Mutant

Ruby is a permissive language. In general we don’t care what objects are, as long as they respond in certain ways to certain methods: “If it walks like a duck and quacks like a duck, I would call it a duck”.

As you may recall, in Rails 2.1 there was a rewrite of the :include mechanism, however the old mechanism persists for compatiblity reasons. When using the new mechanism, the :included associations are not joined, and so if any part of your query looks references the tables that would formerly have been joined it won’t work.

To work around this Rails looks at the conditions, order, select statements and so on to see if any of them mention tables other than the main table. If they do then the fallback to the old code is triggered and everything works fine. The code that detects tables in the order clause looks something like

1
2
   return [] unless order && order.is_a?(String)
   order.scan(/([\.\w]+).?\./).flatten

The code is quite simple: if we ever have “something.” then that means we’re using the something table.

The code that eventually adds the order to the statement looks something like

1
  sql << " ORDER BY #{order}"

The code for the other options is similar.

So what’s the problem here? If as the api docs indicate, you pass a string containing a fragment of sql then nothing at all is wrong. However some people (I assume that the :conditions option is the origin of this habit) have taken to doing things like

1
  Foo.find :all, :order => ['bars']

Before 2.1, this happens to work, because the default to_s on an array just joins the strings together. However the table scanning code won’t scan an array (My guess is that the explicit check for string was because people quite legitimately write :order => :name and things like that). So if you’ve got a select or order clause specified as an array that depends on an included able it will break when you move to rails 2.1.

It’s a complete accident that this ever worked (and it breaks if you were to try anything like :order =>[‘name desc’, ‘age desc’]), but that isn’t a huge amount of comfort when code that has been working suddenly stops working. You could probably waste a lot of time before working out that it was specifying the order option as an array, which obviously is not a good thing (and makes people scared of upgrading). On the other hand it’s hard to anticipate how people will use things and explicitly checking types and so on isn’t a very rubyish thing to do and could get in the way of legitimate uses.

I’m not sure how a framework provider should handle this in the general case. It’s a delicate balance between not stifling some of flexibility ruby offers and helping programmers not rely on things that only work by accident.