diff options
author | wangjohn <wangjohn@mit.edu> | 2013-02-11 23:23:01 -0500 |
---|---|---|
committer | wangjohn <wangjohn@mit.edu> | 2013-03-03 20:42:01 -0500 |
commit | 293875457bc5b0fccbf3e64bcd275cdac252f98c (patch) | |
tree | 5f8385454788673ac00004cd9c494b5df02d3cfc /activerecord | |
parent | 9ee6f3cc8ef1cd50648ec2882803943d3bd1f24a (diff) | |
download | rails-293875457bc5b0fccbf3e64bcd275cdac252f98c.tar.gz rails-293875457bc5b0fccbf3e64bcd275cdac252f98c.tar.bz2 rails-293875457bc5b0fccbf3e64bcd275cdac252f98c.zip |
Created an unscope method for removing relations from a chain of
relations. Specific where values can be unscoped, and the unscope method
still works when relations are merged or combined.
Diffstat (limited to 'activerecord')
-rw-r--r-- | activerecord/CHANGELOG.md | 16 | ||||
-rw-r--r-- | activerecord/lib/active_record/relation/query_methods.rb | 94 | ||||
-rw-r--r-- | activerecord/test/cases/relation_scoping_test.rb | 144 |
3 files changed, 254 insertions, 0 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 2eaf388095..d265f04562 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,21 @@ ## Rails 4.0.0 (unreleased) ## +* Added functionality to unscope relations in a relations chain. For + instance, if you are passed in a chain of relations as follows: + + Posts.select(:name => "John").order('id DESC') + + but you want to get rid of order, then this feature allows you to do: + + Posts.select(:name => "John").order("id DESC").unscope(:order) + == Posts.select(:name => "John") + + The .unscope() function is more general than the .except() method because + .except() only works on the relation it is acting on. However, .unscope() + works for any relation in the entire relation chain. + + *John Wang* + * Postgresql timestamp with time zone (timestamptz) datatype now returns a ActiveSupport::TimeWithZone instance instead of a string diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 4b8c40592e..881ac687b3 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -317,6 +317,67 @@ module ActiveRecord self end + VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock, + :limit, :offset, :joins, :includes, :from, + :readonly, :having]) + + # Removes an unwanted relation that is already defined on a chain of relations. + # This is useful when passing around chains of relations and would like to + # modify the relations without reconstructing the entire chain. + # + # User.all.order('email DESC').unscope(:order) == User.all + # + # The method arguments are symbols which correspond to the names of the methods + # which should be unscoped. The valid arguments are given in VALID_UNSCOPING_VALUES. + # The method can also be called with multiple arguments. For example: + # + # User.all.order('email DESC').select('id').where(:name => "John") + # .unscope(:order, :select, :where) == User.all + # + # One can additionally pass a hash as an argument to unscope specific :where values. + # This is done by passing a hash with a single key-value pair. The key should be + # :where and the value should be the where value to unscope. For example: + # + # User.all.where(:name => "John", :active => true).unscope(:where => :name) + # == User.all.where(:active => true) + # + # Note that this method is more generalized than ActiveRecord::SpawnMethods#except + # because #except will only affect a particular relation's values. It won't wipe + # the order, grouping, etc. when that relation is merged. For example: + # + # Post.comments.except(:order) + # + # will still have an order if it comes from the default_scope on Comment. + def unscope(*args) + check_if_method_has_arguments!("unscope", args) + spawn.unscope!(*args) + end + + def unscope!(*args) + args.flatten! + + args.each do |scope| + case scope + when Symbol + symbol_unscoping(scope) + when Hash + scope.each do |key, target_value| + if key != :where + raise ArgumentError, "Hash arguments in .unscope(*args) must have :where as the key." + end + + Array(target_value).each do |val| + where_unscoping(val) + end + end + else + raise ArgumentError, "Unrecognized scoping: #{args.inspect}. Use .unscope(where: :attribute_name) or .unscope(:order), for example." + end + end + + self + end + # Performs a joins on +args+: # # User.joins(:posts) @@ -762,6 +823,39 @@ module ActiveRecord private + def symbol_unscoping(scope) + if !VALID_UNSCOPING_VALUES.include?(scope) + raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}." + end + + single_val_method = Relation::SINGLE_VALUE_METHODS.include?(scope) + unscope_code = :"#{scope}_value#{'s' unless single_val_method}=" + + case scope + when :order + self.send(:reverse_order_value=, false) + result = [] + else + result = [] unless single_val_method + end + + self.send(unscope_code, result) + end + + def where_unscoping(target_value) + target_value_sym = target_value.to_sym + + where_values.reject! do |rel| + case rel + when Arel::Nodes::In, Arel::Nodes::Equality + subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right) + subrelation.name.to_sym == target_value_sym + else + raise "unscope(where: #{target_value.inspect}) failed: unscoping #{rel.class} is unimplemented." + end + end + end + def custom_join_ast(table, joins) joins = joins.reject { |join| join.blank? } diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index 7388324a0d..2b4aadc7ed 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -424,6 +424,150 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal expected, received end + def test_unscope_overrides_default_scope + expected = Developer.all.collect { |dev| [dev.name, dev.id] } + received = Developer.order('name ASC, id DESC').unscope(:order).collect { |dev| [dev.name, dev.id] } + assert_equal expected, received + end + + def test_unscope_after_reordering_and_combining + expected = Developer.order('id DESC, name DESC').collect { |dev| [dev.name, dev.id] } + received = DeveloperOrderedBySalary.reorder('name DESC').unscope(:order).order('id DESC, name DESC').collect { |dev| [dev.name, dev.id] } + assert_equal expected, received + + expected_2 = Developer.all.collect { |dev| [dev.name, dev.id] } + received_2 = Developer.order('id DESC, name DESC').unscope(:order).collect { |dev| [dev.name, dev.id] } + assert_equal expected_2, received_2 + + expected_3 = Developer.all.collect { |dev| [dev.name, dev.id] } + received_3 = Developer.reorder('name DESC').unscope(:order).collect { |dev| [dev.name, dev.id] } + assert_equal expected_3, received_3 + end + + def test_unscope_with_where_attributes + expected = Developer.order('salary DESC').collect { |dev| dev.name } + received = DeveloperOrderedBySalary.where(name: 'David').unscope(where: :name).collect { |dev| dev.name } + assert_equal expected, received + + expected_2 = Developer.order('salary DESC').collect { |dev| dev.name } + received_2 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope({:where => :name}, :select).collect { |dev| dev.name } + assert_equal expected_2, received_2 + + expected_3 = Developer.order('salary DESC').collect { |dev| dev.name } + received_3 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope(:select, :where).collect { |dev| dev.name } + assert_equal expected_3, received_3 + end + + def test_unscope_multiple_where_clauses + expected = Developer.order('salary DESC').collect { |dev| dev.name } + received = DeveloperOrderedBySalary.where(name: 'Jamis').where(id: 1).unscope(where: [:name, :id]).collect { |dev| dev.name } + assert_equal expected, received + end + + def test_unscope_with_grouping_attributes + expected = Developer.order('salary DESC').collect { |dev| dev.name } + received = DeveloperOrderedBySalary.group(:name).unscope(:group).collect { |dev| dev.name } + assert_equal expected, received + + expected_2 = Developer.order('salary DESC').collect { |dev| dev.name } + received_2 = DeveloperOrderedBySalary.group("name").unscope(:group).collect { |dev| dev.name } + assert_equal expected_2, received_2 + end + + def test_unscope_with_limit_in_query + expected = Developer.order('salary DESC').collect { |dev| dev.name } + received = DeveloperOrderedBySalary.limit(1).unscope(:limit).collect { |dev| dev.name } + assert_equal expected, received + end + + def test_order_to_unscope_reordering + expected = DeveloperOrderedBySalary.all.collect { |dev| [dev.name, dev.id] } + received = DeveloperOrderedBySalary.order('salary DESC, name ASC').reverse_order.unscope(:order).collect { |dev| [dev.name, dev.id] } + assert_equal expected, received + end + + def test_unscope_reverse_order + expected = Developer.all.collect { |dev| dev.name } + received = Developer.order('salary DESC').reverse_order.unscope(:order).collect { |dev| dev.name } + assert_equal expected, received + end + + def test_unscope_select + expected = Developer.order('salary ASC').collect { |dev| dev.name } + received = Developer.order('salary DESC').reverse_order.select(:name => "Jamis").unscope(:select).collect { |dev| dev.name } + assert_equal expected, received + + expected_2 = Developer.all.collect { |dev| dev.id } + received_2 = Developer.select(:name).unscope(:select).collect { |dev| dev.id } + assert_equal expected_2, received_2 + end + + def test_unscope_offset + expected = Developer.all.collect { |dev| dev.name } + received = Developer.offset(5).unscope(:offset).collect { |dev| dev.name } + assert_equal expected, received + end + + def test_unscope_joins_and_select_on_developers_projects + expected = Developer.all.collect { |dev| dev.name } + received = Developer.joins('JOIN developers_projects ON id = developer_id').select(:id).unscope(:joins, :select).collect { |dev| dev.name } + assert_equal expected, received + end + + def test_unscope_includes + expected = Developer.all.collect { |dev| dev.name } + received = Developer.includes(:projects).select(:id).unscope(:includes, :select).collect { |dev| dev.name } + assert_equal expected, received + end + + def test_unscope_having + expected = DeveloperOrderedBySalary.all.collect { |dev| dev.name } + received = DeveloperOrderedBySalary.having("name IN ('Jamis', 'David')").unscope(:having).collect { |dev| dev.name } + assert_equal expected, received + end + + def test_unscope_errors_with_invalid_value + assert_raises(ArgumentError) do + Developer.includes(:projects).where(name: "Jamis").unscope(:stupidly_incorrect_value) + end + + assert_raises(ArgumentError) do + Developer.all.unscope(:includes, :select, :some_broken_value) + end + + assert_raises(ArgumentError) do + Developer.order('name DESC').reverse_order.unscope(:reverse_order) + end + + assert_raises(ArgumentError) do + Developer.order('name DESC').where(name: "Jamis").unscope() + end + end + + def test_unscope_errors_with_non_where_hash_keys + assert_raises(ArgumentError) do + Developer.where(name: "Jamis").limit(4).unscope(limit: 4) + end + + assert_raises(ArgumentError) do + Developer.where(name: "Jamis").unscope("where" => :name) + end + end + + def test_unscope_errors_with_non_symbol_or_hash_arguments + assert_raises(ArgumentError) do + Developer.where(name: "Jamis").limit(3).unscope("limit") + end + + assert_raises(ArgumentError) do + Developer.select("id").unscope("select") + end + + assert_raises(ArgumentError) do + Developer.select("id").unscope(5) + end + end + def test_order_in_default_scope_should_not_prevail expected = Developer.all.merge!(:order => 'salary').to_a.collect { |dev| dev.salary } received = DeveloperOrderedBySalary.all.merge!(:order => 'salary').to_a.collect { |dev| dev.salary } |