From a7e19b30ca71f62af516675023659be061b2b70a Mon Sep 17 00:00:00 2001 From: Jon Leighton Date: Fri, 11 Feb 2011 22:22:19 +0000 Subject: Add interpolation of association conditions back in, in the form of proc { ... } rather than instance_eval-ing strings --- activerecord/CHANGELOG | 23 ++++++++++++++++++++-- .../lib/active_record/association_preload.rb | 12 +++++++++-- .../associations/association_collection.rb | 8 +++----- .../associations/association_proxy.rb | 11 ++++++++--- .../join_dependency/join_association.rb | 4 ++++ .../has_and_belongs_to_many_association.rb | 4 ++-- .../associations/through_association.rb | 4 ++-- activerecord/lib/active_record/base.rb | 6 ------ activerecord/test/cases/associations/eager_test.rb | 8 ++++++++ .../has_and_belongs_to_many_associations_test.rb | 2 +- .../associations/has_many_associations_test.rb | 1 - .../has_many_through_associations_test.rb | 7 +++++++ .../associations/has_one_associations_test.rb | 8 ++++++++ activerecord/test/cases/base_test.rb | 6 ------ activerecord/test/models/company.rb | 15 ++++++-------- activerecord/test/models/post.rb | 7 +++++++ activerecord/test/models/project.rb | 17 ++++++++-------- activerecord/test/models/tagging.rb | 1 + 18 files changed, 97 insertions(+), 47 deletions(-) diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index e49915a8cd..72bbeeec61 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -50,8 +50,27 @@ (for example, add_name_to_users) use the reversible migration's `change` method instead of the ordinary `up` and `down` methods. [Prem Sichanugrist] -* Removed support for interpolated SQL conditions. Please use scoping -along with attribute conditionals as a replacement. +* Removed support for interpolating string SQL conditions on associations. Instead, you should + use a proc, like so: + + Before: + + has_many :things, :conditions => 'foo = #{bar}' + + After: + + has_many :things, :conditions => proc { "foo = #{bar}" } + + Inside the proc, 'self' is the object which is the owner of the association, unless you are + eager loading the association, in which case 'self' is the class which the association is within. + + You can have any "normal" conditions inside the proc, so the following will work too: + + has_many :things, :conditions => proc { ["foo = ?", bar] } + + Previously :insert_sql and :delete_sql on has_and_belongs_to_many association allowed you to call + 'record' to get the record being inserted or deleted. This is now passed as an argument to + the proc. * Added ActiveRecord::Base#has_secure_password (via ActiveModel::SecurePassword) to encapsulate dead-simple password usage with BCrypt encryption and salting [DHH]. Example: diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index b83c00e9f8..52d2c49836 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -399,10 +399,18 @@ module ActiveRecord end end + def process_conditions(conditions, klass = self) + if conditions.respond_to?(:to_proc) + conditions = instance_eval(&conditions) + end + + klass.send(:sanitize_sql, conditions) + end + def append_conditions(reflection, preload_options) [ - ("(#{reflection.sanitized_conditions})" if reflection.sanitized_conditions), - ("(#{sanitize_sql preload_options[:conditions]})" if preload_options[:conditions]), + ('(' + process_conditions(reflection.options[:conditions], reflection.klass) + ')' if reflection.options[:conditions]), + ('(' + process_conditions(preload_options[:conditions]) + ')' if preload_options[:conditions]), ].compact.map { |x| Arel.sql x } end diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index 3e3237c348..8e006e4d9d 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -374,17 +374,15 @@ module ActiveRecord def custom_counter_sql if @reflection.options[:counter_sql] - counter_sql = @reflection.options[:counter_sql] + interpolate(@reflection.options[:counter_sql]) else # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */ - counter_sql = @reflection.options[:finder_sql].sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } + interpolate(@reflection.options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" } end - - interpolate_sql(counter_sql) end def custom_finder_sql - interpolate_sql(@reflection.options[:finder_sql]) + interpolate(@reflection.options[:finder_sql]) end def find_target diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 2832f49c23..fc03a4b619 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -184,7 +184,8 @@ module ActiveRecord def association_scope scope = target_klass.unscoped scope = scope.create_with(creation_attributes) - scope = scope.apply_finder_options(@reflection.options.slice(:conditions, :readonly, :include)) + scope = scope.apply_finder_options(@reflection.options.slice(:readonly, :include)) + scope = scope.where(interpolate(@reflection.options[:conditions])) if select = select_value scope = scope.select(select) end @@ -240,8 +241,12 @@ module ActiveRecord !loaded? && (!@owner.new_record? || foreign_key_present?) && target_klass end - def interpolate_sql(sql, record = nil) - @owner.send(:interpolate_sql, sql, record) + def interpolate(sql, record = nil) + if sql.respond_to?(:to_proc) + @owner.send(:instance_exec, record, &sql) + else + sql + end end def select_value diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb index 856d826d67..aaa475109e 100644 --- a/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/class_methods/join_dependency/join_association.rb @@ -108,6 +108,10 @@ module ActiveRecord end def process_conditions(conditions, table_name) + if conditions.respond_to?(:to_proc) + conditions = instance_eval(&conditions) + end + Arel.sql(sanitize_sql(conditions, table_name)) end diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index b0ccab2de6..b5ec45159b 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -17,7 +17,7 @@ module ActiveRecord end if @reflection.options[:insert_sql] - @owner.connection.insert(interpolate_sql(@reflection.options[:insert_sql], record)) + @owner.connection.insert(interpolate(@reflection.options[:insert_sql], record)) else stmt = join_table.compile_insert( join_table[@reflection.foreign_key] => @owner.id, @@ -42,7 +42,7 @@ module ActiveRecord def delete_records(records, method) if sql = @reflection.options[:delete_sql] - records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) } + records.each { |record| @owner.connection.delete(interpolate(sql, record)) } else relation = join_table stmt = relation.where(relation[@reflection.foreign_key].eq(@owner.id). diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index ab593d09b9..0550bae408 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -119,14 +119,14 @@ module ActiveRecord scope = scope.where(@reflection.through_reflection.klass.send(:type_condition)) end - scope = scope.where(@reflection.source_reflection.options[:conditions]) + scope = scope.where(interpolate(@reflection.source_reflection.options[:conditions])) scope.where(through_conditions) end # If there is a hash of conditions then we make sure the keys are scoped to the # through table name if left ambiguous. def through_conditions - conditions = @reflection.through_reflection.options[:conditions] + conditions = interpolate(@reflection.through_reflection.options[:conditions]) if conditions.is_a?(Hash) Hash[conditions.map { |key, value| diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 16bdd7bb58..3d48ab89ac 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -1790,12 +1790,6 @@ MSG self.class.connection.quote(value, column) end - # Interpolate custom SQL string in instance context. - # Optional record argument is meant for custom insert_sql. - def interpolate_sql(sql, record = nil) - instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__) - end - # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done # by calling new on the column type or aggregation type (through composed_of) object with these parameters. # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 3074648d59..42dd612083 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -668,6 +668,14 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal people(:david, :susan), Person.find(:all, :include => [:readers, :primary_contact, :number1_fan], :conditions => "number1_fans_people.first_name like 'M%'", :order => 'people.id', :limit => 2, :offset => 0) end + def test_preload_with_interpolation + post = Post.includes(:comments_with_interpolated_conditions).find(posts(:welcome).id) + assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions + + post = Post.joins(:comments_with_interpolated_conditions).find(posts(:welcome).id) + assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions + end + def test_polymorphic_type_condition post = Post.find(posts(:thinking).id, :include => :taggings) assert post.taggings.include?(taggings(:thinking_general)) diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index 5dd2c9861e..dc382c3007 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -72,7 +72,7 @@ class DeveloperWithCounterSQL < ActiveRecord::Base :join_table => "developers_projects", :association_foreign_key => "project_id", :foreign_key => "developer_id", - :counter_sql => 'SELECT COUNT(*) AS count_all FROM projects INNER JOIN developers_projects ON projects.id = developers_projects.project_id WHERE developers_projects.developer_id =#{id}' + :counter_sql => proc { "SELECT COUNT(*) AS count_all FROM projects INNER JOIN developers_projects ON projects.id = developers_projects.project_id WHERE developers_projects.developer_id =#{id}" } end class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 23b777ac6d..ad774eb9ce 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -279,7 +279,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_counting_using_finder_sql assert_equal 2, Firm.find(4).clients_using_sql.count - assert_equal 2, Firm.find(4).clients_using_multiline_sql.count end def test_belongs_to_sanity diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index c58068ef75..73b1d6c500 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -711,4 +711,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase post.author_addresses.delete(address) assert post[:author_count].nil? end + + def test_interpolated_conditions + post = posts(:welcome) + assert !post.tags.empty? + assert_equal post.tags, post.interpolated_tags + assert_equal post.tags, post.interpolated_tags_2 + end end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index dbf6dfe20d..6b73a35905 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -243,6 +243,14 @@ class HasOneAssociationsTest < ActiveRecord::TestCase firm.destroy end + def test_finding_with_interpolated_condition + firm = Firm.find(:first) + superior = firm.clients.create(:name => 'SuperiorCo') + superior.rating = 10 + superior.save + assert_equal 10, firm.clients_with_interpolated_conditions.first.rating + end + def test_assignment_before_child_saved firm = Firm.find(1) firm.account = a = Account.new("credit_limit" => 1000) diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 6a61dcdb47..0ad20bb9bc 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1295,12 +1295,6 @@ class BasicsTest < ActiveRecord::TestCase assert_equal res6, res7 end - def test_interpolate_sql - assert_nothing_raised { Category.new.send(:interpolate_sql, 'foo@bar') } - assert_nothing_raised { Category.new.send(:interpolate_sql, 'foo bar) baz') } - assert_nothing_raised { Category.new.send(:interpolate_sql, 'foo bar} baz') } - end - def test_scoped_find_conditions scoped_developers = Developer.send(:with_scope, :find => { :conditions => 'salary > 90000' }) do Developer.find(:all, :conditions => 'id < 5') diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 3e219fbe4a..e0b30efd51 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -48,19 +48,16 @@ class Firm < Company has_many :dependent_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :destroy has_many :exclusively_dependent_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :order => "id", :dependent => :delete_all has_many :limited_clients, :class_name => "Client", :limit => 1 + has_many :clients_with_interpolated_conditions, :class_name => "Client", :conditions => proc { "rating > #{rating}" } has_many :clients_like_ms, :conditions => "name = 'Microsoft'", :class_name => "Client", :order => "id" has_many :clients_like_ms_with_hash_conditions, :conditions => { :name => 'Microsoft' }, :class_name => "Client", :order => "id" - has_many :clients_using_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}' - has_many :clients_using_multiline_sql, :class_name => "Client", :finder_sql => ' - SELECT - companies.* - FROM companies WHERE companies.client_of = #{id}' + has_many :clients_using_sql, :class_name => "Client", :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id}" } has_many :clients_using_counter_sql, :class_name => "Client", - :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}', - :counter_sql => 'SELECT COUNT(*) FROM companies WHERE client_of = #{id}' + :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id} " }, + :counter_sql => proc { "SELECT COUNT(*) FROM companies WHERE client_of = #{id}" } has_many :clients_using_zero_counter_sql, :class_name => "Client", - :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}', - :counter_sql => 'SELECT 0 FROM companies WHERE client_of = #{id}' + :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id}" }, + :counter_sql => proc { "SELECT 0 FROM companies WHERE client_of = #{id}" } has_many :no_clients_using_counter_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE client_of = 1000', :counter_sql => 'SELECT COUNT(*) FROM companies WHERE client_of = 1000' diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index fd8cd2244a..5f29196790 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -41,6 +41,9 @@ class Post < ActiveRecord::Base has_many :author_categorizations, :through => :author, :source => :categorizations has_many :author_addresses, :through => :author + has_many :comments_with_interpolated_conditions, :class_name => 'Comment', + :conditions => proc { ["#{"#{aliased_table_name}." rescue ""}body = ?", 'Thank you for the welcome'] } + has_one :very_special_comment has_one :very_special_comment_with_post, :class_name => "VerySpecialComment", :include => :post has_many :special_comments @@ -57,6 +60,10 @@ class Post < ActiveRecord::Base end end + has_many :interpolated_taggings, :class_name => 'Tagging', :as => :taggable, :conditions => proc { "1 = #{1}" } + has_many :interpolated_tags, :through => :taggings + has_many :interpolated_tags_2, :through => :interpolated_taggings, :source => :tag + has_many :taggings_with_delete_all, :class_name => 'Tagging', :as => :taggable, :dependent => :delete_all has_many :taggings_with_destroy, :class_name => 'Tagging', :as => :taggable, :dependent => :destroy diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb index 8a53a8f803..efe1ce67da 100644 --- a/activerecord/test/models/project.rb +++ b/activerecord/test/models/project.rb @@ -7,14 +7,15 @@ class Project < ActiveRecord::Base has_and_belongs_to_many :developers_named_david, :class_name => "Developer", :conditions => "name = 'David'", :uniq => true has_and_belongs_to_many :developers_named_david_with_hash_conditions, :class_name => "Developer", :conditions => { :name => 'David' }, :uniq => true has_and_belongs_to_many :salaried_developers, :class_name => "Developer", :conditions => "salary > 0" - has_and_belongs_to_many :developers_with_finder_sql, :class_name => "Developer", :finder_sql => 'SELECT t.*, j.* FROM developers_projects j, developers t WHERE t.id = j.developer_id AND j.project_id = #{id} ORDER BY t.id' - has_and_belongs_to_many :developers_with_multiline_finder_sql, :class_name => "Developer", :finder_sql => ' - SELECT - t.*, j.* - FROM - developers_projects j, - developers t WHERE t.id = j.developer_id AND j.project_id = #{id} ORDER BY t.id' - has_and_belongs_to_many :developers_by_sql, :class_name => "Developer", :delete_sql => "DELETE FROM developers_projects WHERE project_id = \#{id} AND developer_id = \#{record.id}" + has_and_belongs_to_many :developers_with_finder_sql, :class_name => "Developer", :finder_sql => proc { "SELECT t.*, j.* FROM developers_projects j, developers t WHERE t.id = j.developer_id AND j.project_id = #{id} ORDER BY t.id" } + has_and_belongs_to_many :developers_with_multiline_finder_sql, :class_name => "Developer", :finder_sql => proc { + "SELECT + t.*, j.* + FROM + developers_projects j, + developers t WHERE t.id = j.developer_id AND j.project_id = #{id} ORDER BY t.id" + } + has_and_belongs_to_many :developers_by_sql, :class_name => "Developer", :delete_sql => proc { |record| "DELETE FROM developers_projects WHERE project_id = #{id} AND developer_id = #{record.id}" } has_and_belongs_to_many :developers_with_callbacks, :class_name => "Developer", :before_add => Proc.new {|o, r| o.developers_log << "before_adding#{r.id || ''}"}, :after_add => Proc.new {|o, r| o.developers_log << "after_adding#{r.id || ''}"}, :before_remove => Proc.new {|o, r| o.developers_log << "before_removing#{r.id}"}, diff --git a/activerecord/test/models/tagging.rb b/activerecord/test/models/tagging.rb index 33ffc623d7..231d2b5890 100644 --- a/activerecord/test/models/tagging.rb +++ b/activerecord/test/models/tagging.rb @@ -6,6 +6,7 @@ class Tagging < ActiveRecord::Base belongs_to :tag, :include => :tagging belongs_to :super_tag, :class_name => 'Tag', :foreign_key => 'super_tag_id' belongs_to :invalid_tag, :class_name => 'Tag', :foreign_key => 'tag_id' + belongs_to :interpolated_tag, :class_name => 'Tag', :foreign_key => :tag_id, :conditions => proc { "1 = #{1}" } belongs_to :taggable, :polymorphic => true, :counter_cache => true has_many :things, :through => :taggable end -- cgit v1.2.3