diff options
author | David Heinemeier Hansson <david@loudthinking.com> | 2006-02-26 20:12:09 +0000 |
---|---|---|
committer | David Heinemeier Hansson <david@loudthinking.com> | 2006-02-26 20:12:09 +0000 |
commit | 1215d54c2f31c4d19b2432ab05751fe2eaec0319 (patch) | |
tree | 52274812525e365405b0e7a769e0a7e3d03f4e9d /activerecord | |
parent | 3cfbb4f374cee2d193b80d4cd373e3fd05997027 (diff) | |
download | rails-1215d54c2f31c4d19b2432ab05751fe2eaec0319.tar.gz rails-1215d54c2f31c4d19b2432ab05751fe2eaec0319.tar.bz2 rails-1215d54c2f31c4d19b2432ab05751fe2eaec0319.zip |
Added support for nested scopes (closes #3407) [anna@wota.jp]
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@3671 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
Diffstat (limited to 'activerecord')
-rw-r--r-- | activerecord/CHANGELOG | 16 | ||||
-rwxr-xr-x | activerecord/lib/active_record/base.rb | 89 | ||||
-rw-r--r-- | activerecord/test/method_scoping_test.rb | 176 |
3 files changed, 248 insertions, 33 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 45480170e7..5b5beab6eb 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,21 @@ *SVN* +* Added support for nested scopes #3407 [anna@wota.jp]. Examples: + + Developer.with_scope(:find => { :conditions => "salary > 10000", :limit => 10 }) do + Developer.find(:all) # => SELECT * FROM developers WHERE (salary > 10000) LIMIT 10 + + # inner rule is used. (all previous parameters are ignored) + Developer.with_exclusive_scope(:find => { :conditions => "name = 'Jamis'" }) do + Developer.find(:all) # => SELECT * FROM developers WHERE (name = 'Jamis') + end + + # parameters are merged + Developer.with_scope(:find => { :conditions => "name = 'Jamis'" }) do + Developer.find(:all) # => SELECT * FROM developers WHERE (( salary > 10000 ) AND ( name = 'Jamis' )) LIMIT 10 + end + end + * Fixed db2 connection with empty user_name and auth options #3622 [phurley@gmail.com] * Fixed validates_length_of to work on UTF-8 strings by using characters instead of bytes #3699 [Masao Mutoh] diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 829d4f0a5d..441b555721 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -828,20 +828,40 @@ module ActiveRecord #:nodoc: end # Scope parameters to method calls within the block. Takes a hash of method_name => parameters hash. - # method_name may be :find or :create. - # :find parameters may include the <tt>:conditions</tt>, <tt>:joins</tt>, - # <tt>:offset</tt>, <tt>:limit</tt>, and <tt>:readonly</tt> options. - # :create parameters are an attributes hash. + # method_name may be :find or :create. :find parameters may include the <tt>:conditions</tt>, <tt>:joins</tt>, + # <tt>:offset</tt>, <tt>:limit</tt>, and <tt>:readonly</tt> options. :create parameters are an attributes hash. # # Article.with_scope(:find => { :conditions => "blog_id = 1" }, :create => { :blog_id => 1 }) do # Article.find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1 # a = Article.create(1) - # a.blog_id == 1 + # a.blog_id # => 1 # end - def with_scope(method_scoping = {}) + # + # In nested scopings, all previous parameters are overwritten by inner rule + # except :conditions in :find, that are merged as hash. + # + # Article.with_scope(:find => { :conditions => "blog_id = 1", :limit => 1 }, :create => { :blog_id => 1 }) do + # Article.with_scope(:find => { :limit => 10}) + # Article.find(:all) # => SELECT * from articles WHERE blog_id = 1 LIMIT 10 + # end + # Article.with_scope(:find => { :conditions => "author_id = 3" }) + # Article.find(:all) # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1 + # end + # end + # + # You can ignore any previous scopings by using <tt>with_exclusive_scope</tt> method. + # + # Article.with_scope(:find => { :conditions => "blog_id = 1", :limit => 1 }) do + # Article.with_exclusive_scope(:find => { :limit => 10 }) + # Article.find(:all) # => SELECT * from articles LIMIT 10 + # end + # end + def with_scope(method_scoping = {}, action = :merge, &block) + method_scoping = method_scoping.method_scoping if method_scoping.respond_to?(:method_scoping) + # Dup first and second level of hash (method and params). method_scoping = method_scoping.inject({}) do |hash, (method, params)| - hash[method] = params.dup + hash[method] = (params == true) ? params : params.dup hash end @@ -852,12 +872,40 @@ module ActiveRecord #:nodoc: f[:readonly] = true if !f[:joins].blank? && !f.has_key?(:readonly) end - raise ArgumentError, "Nested scopes are not yet supported: #{scoped_methods.inspect}" unless scoped_methods.nil? + # Merge scopings + if action == :merge && current_scoped_methods + method_scoping = current_scoped_methods.inject(method_scoping) do |hash, (method, params)| + case hash[method] + when Hash + if method == :find && hash[method][:conditions] && params[:conditions] + (hash[method].keys + params.keys).uniq.each do |key| + if key == :conditions + hash[method][key] = [params[key], hash[method][key]].collect{|sql| "( %s )" % sanitize_sql(sql)}.join(" AND ") + else + hash[method][key] = hash[method][key] || params[key] + end + end + else + hash[method] = params.merge(hash[method]) + end + else + hash[method] = params + end + hash + end + end - self.scoped_methods = method_scoping - yield - ensure - self.scoped_methods = nil + self.scoped_methods << method_scoping + + begin + yield + ensure + self.scoped_methods.pop + end + end + + def with_exclusive_scope(method_scoping = {}, &block) + with_scope(method_scoping, :overwrite, &block) end # Overwrite the default class equality method to provide support for association proxies. @@ -1076,12 +1124,12 @@ module ActiveRecord #:nodoc: # Test whether the given method and optional key are scoped. def scoped?(method, key = nil) - scoped_methods and scoped_methods.has_key?(method) and (key.nil? or scope(method).has_key?(key)) + current_scoped_methods && current_scoped_methods.has_key?(method) && (key.nil? || scope(method).has_key?(key)) end # Retrieve the scope for the given method and optional key. def scope(method, key = nil) - if scoped_methods and scope = scoped_methods[method] + if current_scoped_methods && scope = current_scoped_methods[method] key ? scope[key] : scope end end @@ -1089,19 +1137,14 @@ module ActiveRecord #:nodoc: def scoped_methods if allow_concurrency Thread.current[:scoped_methods] ||= {} - Thread.current[:scoped_methods][self] ||= nil + Thread.current[:scoped_methods][self] ||= [] else - @scoped_methods ||= nil + @scoped_methods ||= [] end end - def scoped_methods=(value) - if allow_concurrency - Thread.current[:scoped_methods] ||= {} - Thread.current[:scoped_methods][self] = value - else - @scoped_methods = value - end + def current_scoped_methods + scoped_methods.last end # Returns the class type of the record using the current module as a prefix. So descendents of diff --git a/activerecord/test/method_scoping_test.rb b/activerecord/test/method_scoping_test.rb index 6e843eab0b..cb8b6a05e9 100644 --- a/activerecord/test/method_scoping_test.rb +++ b/activerecord/test/method_scoping_test.rb @@ -9,7 +9,7 @@ class MethodScopingTest < Test::Unit::TestCase def test_set_conditions Developer.with_scope(:find => { :conditions => 'just a test...' }) do - assert_equal 'just a test...', Thread.current[:scoped_methods][Developer][:find][:conditions] + assert_equal 'just a test...', Thread.current[:scoped_methods][Developer][-1][:find][:conditions] end end @@ -64,7 +64,7 @@ class MethodScopingTest < Test::Unit::TestCase new_comment = nil VerySpecialComment.with_scope(:create => { :post_id => 1 }) do - assert_equal({ :post_id => 1 }, Thread.current[:scoped_methods][VerySpecialComment][:create]) + assert_equal({ :post_id => 1 }, Thread.current[:scoped_methods][VerySpecialComment][-1][:create]) new_comment = VerySpecialComment.create :body => "Wonderful world" end @@ -87,11 +87,167 @@ class MethodScopingTest < Test::Unit::TestCase end end - def test_raise_on_nested_scope - Developer.with_scope(:find => { :conditions => '1=1' }) do - assert_raise(ArgumentError) do - Developer.with_scope(:find => { :conditions => '2=2' }) { } + def test_scoped_with_duck_typing + scoping = Struct.new(:method_scoping).new(:find => { :conditions => ["name = ?", 'David'] }) + Developer.with_scope(scoping) do + assert_equal %w(David), Developer.find(:all).map { |d| d.name } + end + end + + def test_ensure_that_method_scoping_is_correctly_restored + scoped_methods = Developer.instance_eval('current_scoped_methods') + + begin + Developer.with_scope(:find => { :conditions => "name = 'Jamis'" }) do + raise "an exception" + end + rescue + end + assert_equal scoped_methods, Developer.instance_eval('current_scoped_methods') + end +end + +class NestedScopingTest < Test::Unit::TestCase + fixtures :developers, :comments, :posts + + def test_merge_options + Developer.with_scope(:find => { :conditions => 'salary = 80000' }) do + Developer.with_scope(:find => { :limit => 10 }) do + merged_option = Developer.instance_eval('current_scoped_methods')[:find] + assert_equal({ :conditions => 'salary = 80000', :limit => 10 }, merged_option) + end + end + end + + def test_replace_options + Developer.with_scope(:find => { :conditions => "name = 'David'" }) do + Developer.with_exclusive_scope(:find => { :conditions => "name = 'Jamis'" }) do + assert_equal({:find => { :conditions => "name = 'Jamis'" }}, Developer.instance_eval('current_scoped_methods')) + assert_equal({:find => { :conditions => "name = 'Jamis'" }}, Thread.current[:scoped_methods][Developer][-1]) + end + end + end + + def test_append_conditions + Developer.with_scope(:find => { :conditions => "name = 'David'" }) do + Developer.with_scope(:find => { :conditions => 'salary = 80000' }) do + appended_condition = Developer.instance_eval('current_scoped_methods')[:find][:conditions] + assert_equal("( name = 'David' ) AND ( salary = 80000 )", appended_condition) + assert_equal(1, Developer.count) + end + Developer.with_scope(:find => { :conditions => "name = 'Maiha'" }) do + assert_equal(0, Developer.count) + end + end + end + + def test_merge_and_append_options + Developer.with_scope(:find => { :conditions => 'salary = 80000', :limit => 10 }) do + Developer.with_scope(:find => { :conditions => "name = 'David'" }) do + merged_option = Developer.instance_eval('current_scoped_methods')[:find] + assert_equal({ :conditions => "( salary = 80000 ) AND ( name = 'David' )", :limit => 10 }, merged_option) + end + end + end + + def test_nested_scoped_find + Developer.with_scope(:find => { :conditions => "name = 'Jamis'" }) do + Developer.with_exclusive_scope(:find => { :conditions => "name = 'David'" }) do + assert_nothing_raised { Developer.find(1) } + assert_equal('David', Developer.find(:first).name) + end + assert_equal('Jamis', Developer.find(:first).name) + end + end + + def test_three_level_nested_exclusive_scoped_find + Developer.with_scope(:find => { :conditions => "name = 'Jamis'" }) do + assert_equal('Jamis', Developer.find(:first).name) + + Developer.with_exclusive_scope(:find => { :conditions => "name = 'David'" }) do + assert_equal('David', Developer.find(:first).name) + + Developer.with_exclusive_scope(:find => { :conditions => "name = 'Maiha'" }) do + assert_equal(nil, Developer.find(:first)) + end + + # ensure that scoping is restored + assert_equal('David', Developer.find(:first).name) + end + + # ensure that scoping is restored + assert_equal('Jamis', Developer.find(:first).name) + end + end + + def test_merged_scoped_find + poor_jamis = developers(:poor_jamis) + Developer.with_scope(:find => { :conditions => "salary < 100000" }) do + Developer.with_scope(:find => { :offset => 1 }) do + assert_equal(poor_jamis, Developer.find(:first)) + end + end + end + + def test_merged_scoped_find_sanitizes_conditions + Developer.with_scope(:find => { :conditions => ["name = ?", 'David'] }) do + Developer.with_scope(:find => { :conditions => ['salary = ?', 9000] }) do + assert_raise(ActiveRecord::RecordNotFound) { developers(:poor_jamis) } + end + end + end + + def test_nested_scoped_find_combines_and_sanitizes_conditions + Developer.with_scope(:find => { :conditions => ["name = ?", 'David'] }) do + Developer.with_exclusive_scope(:find => { :conditions => ['salary = ?', 9000] }) do + assert_equal developers(:poor_jamis), Developer.find(:first) + assert_equal developers(:poor_jamis), Developer.find(:first, :conditions => ['name = ?', 'Jamis']) + end + end + end + + def test_merged_scoped_find_combines_and_sanitizes_conditions + Developer.with_scope(:find => { :conditions => ["name = ?", 'David'] }) do + Developer.with_scope(:find => { :conditions => ['salary > ?', 9000] }) do + assert_equal %w(David), Developer.find(:all).map { |d| d.name } + end + end + end + + def test_immutable_nested_scope + options1 = { :conditions => "name = 'Jamis'" } + options2 = { :conditions => "name = 'David'" } + Developer.with_scope(:find => options1) do + Developer.with_exclusive_scope(:find => options2) do + assert_equal %w(David), Developer.find(:all).map { |d| d.name } + options1[:conditions] = options2[:conditions] = nil + assert_equal %w(David), Developer.find(:all).map { |d| d.name } + end + end + end + + def test_immutable_merged_scope + options1 = { :conditions => "name = 'Jamis'" } + options2 = { :conditions => "salary > 10000" } + Developer.with_scope(:find => options1) do + Developer.with_scope(:find => options2) do + assert_equal %w(Jamis), Developer.find(:all).map { |d| d.name } + options1[:conditions] = options2[:conditions] = nil + assert_equal %w(Jamis), Developer.find(:all).map { |d| d.name } + end + end + end + + def test_ensure_that_method_scoping_is_correctly_restored + Developer.with_scope(:find => { :conditions => "name = 'David'" }) do + scoped_methods = Developer.instance_eval('current_scoped_methods') + begin + Developer.with_scope(:find => { :conditions => "name = 'Maiha'" }) do + raise "an exception" + end + rescue end + assert_equal scoped_methods, Developer.instance_eval('current_scoped_methods') end end end @@ -118,9 +274,9 @@ class HasManyScopingTest< Test::Unit::TestCase assert_equal 2, @welcome.comments.find_all_by_type('Comment').size end - def test_raise_on_nested_scope + def test_nested_scope Comment.with_scope(:find => { :conditions => '1=1' }) do - assert_raise(ArgumentError) { @welcome.comments.what_are_you } + assert_equal 'a comment...', @welcome.comments.what_are_you end end end @@ -144,9 +300,9 @@ class HasAndBelongsToManyScopingTest< Test::Unit::TestCase assert_equal 2, @welcome.categories.find_all_by_type('Category').size end - def test_raise_on_nested_scope + def test_nested_scope Category.with_scope(:find => { :conditions => '1=1' }) do - assert_raise(ArgumentError) { @welcome.categories.what_are_you } + assert_equal 'a comment...', @welcome.comments.what_are_you end end end |