Squeel: How bindings work
by Matthias Geier
In Rails 3 any association could be accessed inside a model instance and be modified to various extents. Accessing it worked similar to this:
class Foo < ActiveRecord::Base
has_many :bars
end
assoc = Foo.new.association(:bars).scoped
puts assoc.to_sql
A pretty useful operation on an association was fiddling with the where_values to delete, inject or replace certain expressions in the sql.
As of Rails 4 the Squeel gem introduces bindings to internal sql calls generated by associations.
SELECT * FROM bars WHERE foo_id = $1 --{ $1 => 1 }
In the rails console queries now look similar to the above example. While it has certainly a few advantages, working with the associations inside rails has become slightly more difficult.
To replace values inside bindings, they have to be bound manually now. A brief example shows how: (Please note that Rails 4 has replaced scoped with scope)
class Foo < ActiveRecord::Base
has_many :bars
end
assoc = Foo.new.association(:bars).scope
bindings = assoc.bind_values
old_where_vals = assoc.where_values
A part of the snipplet is copied from the old example above which worked in Rails 3 and the last 2 lines are new. bind_values offers read-only access to the bindings and the last line just stores the old values away so we can restore them later on.
In the next step, the where-values are loops and bound by hand.
assoc.where_values = assoc.where_values.map do |w|
begin # w can be a proxy class that does not care about .duplicable?
node = w.dup
rescue TypeError
node = w
end
if node.is_a?(Arel::Nodes::Binary) && node.right =~ /^\$\d+$/
value = bindings.detect{ |bind, _| bind.name == node.left.name.to_s }
if value.present?
node.right = value.last.to_s
end
end
next node
end
All where_values are nodes from arel or proxy objects. Unfortunately it is not possible to duplicate a proxy object which requires special handling in the loop.
Nodes in arel usually inherit from a binary type that has properties for left and right, traditionally with the column name on the left and either another node or a binding on the right.
Squeel stores bindings by remembering the column name from the binary node’s left side. For nodes with a binding on the right, the bind value is detected and inserted. This emulates the behaviour before squeel.
puts assoc.to_sql
assoc.where_values = old_where_vals
In the last step, the modified association can be transformed to sql and the original Squeel values are restored. Without this step, the association stops working properly. It is shared between all model instances.