Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Added support for options for atomic modifiers plugin. This enables pass... #395

Merged
merged 5 commits into from

3 participants

@hamin

...ing :upsert and :safe options to the modifier operation.

Since the mongo ruby driver already supports the upsert and safe options we should be passing those along. If there are any questions I'm more than happy to explain but these are definitely useful. We had to drop down to the ruby driver because we wanted to increment keys which might not exist yet in our use case. Once the pull request is approved and merged, I will can adjust the modifier plugin docs (which I wrote ;) ) to cover these options.

@hamin hamin Added support for options for atomic modifiers plugin. This enables p…
…assing :upsert and :safe options to the modifier operation.
f4ef5fb
@bkeepers

I've definitely been wanting this feature too.

I'm not sure that I love the options being passed in the same hash as the modifications. What about doing an additional hash with the options?

record.set({:foo => 'bar'}, {:safe => true})

@jnunemaker thoughts?

@hamin

@bkeepers @jnunemaker yeah i was thinking about that too, i could definitely do that, it would end up looking like this

# On a class
Foo.increment({:name => 'blah'}, {:votes => 1}, {:upsert => true, :safe => true})

# On an instance
foo.increment({:votes => 1}, {:upsert => true, :safe => true})

Basically i thought adding another hash, though more 'proper', was a lil annoying and ugly :). I know that's not a good excuse, but also didn't wanna break anything else.

After I submitted the pull request i was reconsidering what I'd done and wanted to add another hash. I can change it if need be...thoughts?

@hamin

@bkeepers @jnunemaker i feel pretty certain that we should be passing a proper options hash in the arguemnts. That is also how the ruby driver passing upsert and safe options to for modifier operations. So this is how it works with my last commit to this pull request:

# On a class
Foo.increment({:name => 'blah'}, {:votes => 1}, {:upsert => true, :safe => true})

# On an instance
foo.increment({:votes => 1}, {:upsert => true, :safe => true})

# No options still supports current syntax
Foo.increment({:name => 'blah'}, :votes => 1) # On a class

foo.increment(:votes => 1) # On an instance

This way won't be breaking any code that's not using the modifier options.

@hamin

@bkeepers @jnunemaker thoughts guys? would love to see this get pulled in, definitely needing it for a production app soon :)

@hamin

@bkeepers @jnunemaker mcflyyyy!!! :)

@jnunemaker

Looking good. A few nitpicky things and I'll pull. The coding style of the rest of MM has a space between parameters: hash, options=nil.

@jnunemaker

Hmm. Not a fan of explicitly allowing some keys. This would mean each time the driver updates we have to update this spot here. Also means each should be tested. Would be nice to instead somehow just work if we were dealing with options instead of criteria.

Any thoughts on how to make that work? It is difficult since we don't require passing the criteria.

@jnunemaker
Owner

Loving the idea. I think there are just a few tweaks left. Thanks for your work thus far! Curious to see what you think about my suggestions.

@hamin

@jnunemaker I understand your concerns regarding filtering specific keys so I implemented a way to pass criteria, updates, and options hash properly to the ruby driver method. It's again backwards compatible as the last update to the pull-request. This was actually a lot trickier than I expected and kept me up late at night :)

The only thing left at this point is adding the same support to the decrement, set, and unset modifiers since they're not using the same modifier_update method (rightly soI might add). I have to add some tests to cover them and I think we should be good.

Let me know what you think. /cc @bkeepers

@jnunemaker jnunemaker commented on the diff
lib/mongo_mapper/plugins/modifiers.rb
((12 lines not shown))
end
def criteria_and_keys_from_args(args)
- keys = args.pop
- criteria = args[0].is_a?(Hash) ? args[0] : {:id => args}
- [criteria_hash(criteria).to_hash, keys]
+ if args[0].is_a?(Hash)
+ criteria = args[0]
+ updates = args[1]
+ options = args[2]
+ else
+ split_args = args.partition{|a| a.is_a?(BSON::ObjectId)}
@jnunemaker Owner

This won't work with string _id's, will it?

@hamin
hamin added a note

i think you're right, but we can handle that pretty easily. I'll add something later tonight for this w/ test coverage of course.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@hamin

@jnunemaker what do u think of the solution? I already commented on that string id's comment which should be pretty easy. Comments on the rest of the solution? /cc @bkeepers

@bkeepers bkeepers merged commit 0d1de63 into from
@bkeepers

Tests passed on Harmony. Sorry it took so long to pull this!

@bkeepers

If you have time, one addition that would be awesome is having atomic modifiers automagically respect the save settings. So if you called safe! in your module, all operations would be safe.

@hamin

@bkeepers oh right, I will take a look at that later, how would one inspect if safe! is turned on within a model?

@hamin

@bkeepers btw thanks for accepting the pull request always feels good to give back :)

@bkeepers

See safe?.

And thanks for the contribution! Always appreciated, even if it's not pulled right away!

@hamin

@bkeepers i still have to add something to make sure it works for string ids, see @jnunemaker's last comment, but I think the plugin has had the issue all along, thoughts on that?

@bkeepers

@hamin not really. It should probably be fixed. But if it was already broken, I'm not too worried about it. It'd still be good to fix eventually.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Mar 3, 2012
  1. @hamin

    Added support for options for atomic modifiers plugin. This enables p…

    hamin authored
    …assing :upsert and :safe options to the modifier operation.
Commits on Mar 4, 2012
  1. @hamin

    now passing a proper separate options argument for modifier operation…

    hamin authored
    …s instead of forcing it in the keys argument hash
Commits on Mar 14, 2012
  1. @hamin
  2. @hamin

    Now properly inspecting for criteria, updates, and options hashes. If…

    hamin authored
    … any more options get added to the ruby driver, we won't have to do any additional logic to pass those options through
  3. @hamin
This page is out of date. Refresh to see the latest.
Showing with 184 additions and 121 deletions.
  1. +37 −25 lib/mongo_mapper/plugins/modifiers.rb
  2. +147 −96 test/functional/test_modifiers.rb
View
62 lib/mongo_mapper/plugins/modifiers.rb
@@ -10,14 +10,14 @@ def increment(*args)
end
def decrement(*args)
- criteria, keys = criteria_and_keys_from_args(args)
+ criteria, keys, options = criteria_and_keys_from_args(args)
values, to_decrement = keys.values, {}
keys.keys.each_with_index { |k, i| to_decrement[k] = -values[i].abs }
collection.update(criteria, {'$inc' => to_decrement}, :multi => true)
end
def set(*args)
- criteria, updates = criteria_and_keys_from_args(args)
+ criteria, updates, options = criteria_and_keys_from_args(args)
updates.each do |key, value|
updates[key] = keys[key.to_s].set(value) if key?(key)
end
@@ -64,14 +64,26 @@ def pop(*args)
private
def modifier_update(modifier, args)
- criteria, updates = criteria_and_keys_from_args(args)
- collection.update(criteria, {modifier => updates}, :multi => true)
+ criteria, updates, options = criteria_and_keys_from_args(args)
+ if options
+ collection.update(criteria, {modifier => updates}, options.merge(:multi => true))
+ else
+ collection.update(criteria, {modifier => updates}, :multi => true)
+ end
end
def criteria_and_keys_from_args(args)
- keys = args.pop
- criteria = args[0].is_a?(Hash) ? args[0] : {:id => args}
- [criteria_hash(criteria).to_hash, keys]
+ if args[0].is_a?(Hash)
+ criteria = args[0]
+ updates = args[1]
+ options = args[2]
+ else
+ split_args = args.partition{|a| a.is_a?(BSON::ObjectId)}
@jnunemaker Owner

This won't work with string _id's, will it?

@hamin
hamin added a note

i think you're right, but we can handle that pretty easily. I'll add something later tonight for this w/ test coverage of course.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ criteria = {:id => split_args[0]}
+ updates = split_args[1].first
+ options = split_args[1].last
+ end
+ [criteria_hash(criteria).to_hash, updates, options]
end
end
@@ -79,41 +91,41 @@ def unset(*keys)
self.class.unset(id, *keys)
end
- def increment(hash)
- self.class.increment(id, hash)
+ def increment(hash, options=nil)
+ self.class.increment(id, hash, options)
end
- def decrement(hash)
- self.class.decrement(id, hash)
+ def decrement(hash, options=nil)
+ self.class.decrement(id, hash, options)
end
- def set(hash)
- self.class.set(id, hash)
+ def set(hash, options=nil)
+ self.class.set(id, hash, options)
end
- def push(hash)
- self.class.push(id, hash)
+ def push(hash, options=nil)
+ self.class.push(id, hash, options)
end
- def push_all(hash)
- self.class.push_all(id, hash)
+ def push_all(hash, options=nil)
+ self.class.push_all(id, hash, options)
end
- def pull(hash)
- self.class.pull(id, hash)
+ def pull(hash, options=nil)
+ self.class.pull(id, hash, options)
end
- def pull_all(hash)
- self.class.pull_all(id, hash)
+ def pull_all(hash, options=nil)
+ self.class.pull_all(id, hash, options)
end
- def add_to_set(hash)
- self.class.push_uniq(id, hash)
+ def add_to_set(hash, options=nil)
+ self.class.push_uniq(id, hash, options)
end
alias push_uniq add_to_set
- def pop(hash)
- self.class.pop(id, hash)
+ def pop(hash, options=nil)
+ self.class.pop(id, hash, options)
end
end
end
View
243 test/functional/test_modifiers.rb
@@ -31,298 +31,323 @@ def assert_keys_removed(page, *keys)
@page = @page_class.create(:title => 'Home', :tags => %w(foo bar))
@page2 = @page_class.create(:title => 'Home')
end
-
+
should "work with criteria and keys" do
@page_class.unset({:title => 'Home'}, :title, :tags)
assert_keys_removed @page, :title, :tags
assert_keys_removed @page2, :title, :tags
end
-
+
should "work with ids and keys" do
@page_class.unset(@page.id, @page2.id, :title, :tags)
assert_keys_removed @page, :title, :tags
assert_keys_removed @page2, :title, :tags
end
end
-
+
context "increment" do
setup do
@page = @page_class.create(:title => 'Home')
@page2 = @page_class.create(:title => 'Home')
end
-
+
should "work with criteria and modifier hashes" do
@page_class.increment({:title => 'Home'}, :day_count => 1, :week_count => 2, :month_count => 3)
-
+
assert_page_counts @page, 1, 2, 3
assert_page_counts @page2, 1, 2, 3
end
-
+
should "work with ids and modifier hash" do
@page_class.increment(@page.id, @page2.id, :day_count => 1, :week_count => 2, :month_count => 3)
-
+
assert_page_counts @page, 1, 2, 3
assert_page_counts @page2, 1, 2, 3
end
end
-
+
context "decrement" do
setup do
@page = @page_class.create(:title => 'Home', :day_count => 1, :week_count => 2, :month_count => 3)
@page2 = @page_class.create(:title => 'Home', :day_count => 1, :week_count => 2, :month_count => 3)
end
-
+
should "work with criteria and modifier hashes" do
@page_class.decrement({:title => 'Home'}, :day_count => 1, :week_count => 2, :month_count => 3)
-
+
assert_page_counts @page, 0, 0, 0
assert_page_counts @page2, 0, 0, 0
end
-
+
should "work with ids and modifier hash" do
@page_class.decrement(@page.id, @page2.id, :day_count => 1, :week_count => 2, :month_count => 3)
-
+
assert_page_counts @page, 0, 0, 0
assert_page_counts @page2, 0, 0, 0
end
-
+
should "decrement with positive or negative numbers" do
@page_class.decrement(@page.id, @page2.id, :day_count => -1, :week_count => 2, :month_count => -3)
-
+
assert_page_counts @page, 0, 0, 0
assert_page_counts @page2, 0, 0, 0
end
end
-
+
context "set" do
setup do
@page = @page_class.create(:title => 'Home')
@page2 = @page_class.create(:title => 'Home')
end
-
+
should "work with criteria and modifier hashes" do
@page_class.set({:title => 'Home'}, :title => 'Home Revised')
-
+
@page.reload
@page.title.should == 'Home Revised'
-
+
@page2.reload
@page2.title.should == 'Home Revised'
end
-
+
should "work with ids and modifier hash" do
@page_class.set(@page.id, @page2.id, :title => 'Home Revised')
-
+
@page.reload
@page.title.should == 'Home Revised'
-
+
@page2.reload
@page2.title.should == 'Home Revised'
end
-
+
should "typecast values before querying" do
@page_class.key :tags, Set
-
+
assert_nothing_raised do
@page_class.set(@page.id, :tags => ['foo', 'bar'].to_set)
@page.reload
@page.tags.should == Set.new(['foo', 'bar'])
end
end
-
+
should "not typecast keys that are not defined in document" do
assert_raises(BSON::InvalidDocument) do
@page_class.set(@page.id, :colors => ['red', 'green'].to_set)
end
end
-
+
should "set keys that are not defined in document" do
@page_class.set(@page.id, :colors => %w[red green])
@page.reload
@page[:colors].should == %w[red green]
end
end
-
+
context "push" do
setup do
@page = @page_class.create(:title => 'Home')
@page2 = @page_class.create(:title => 'Home')
end
-
+
should "work with criteria and modifier hashes" do
@page_class.push({:title => 'Home'}, :tags => 'foo')
-
+
@page.reload
@page.tags.should == %w(foo)
-
+
@page2.reload
@page2.tags.should == %w(foo)
end
-
+
should "work with ids and modifier hash" do
@page_class.push(@page.id, @page2.id, :tags => 'foo')
-
+
@page.reload
@page.tags.should == %w(foo)
-
+
@page2.reload
@page2.tags.should == %w(foo)
end
end
-
+
context "push_all" do
setup do
@page = @page_class.create(:title => 'Home')
@page2 = @page_class.create(:title => 'Home')
@tags = %w(foo bar)
end
-
+
should "work with criteria and modifier hashes" do
@page_class.push_all({:title => 'Home'}, :tags => @tags)
-
+
@page.reload
@page.tags.should == @tags
-
+
@page2.reload
@page2.tags.should == @tags
end
-
+
should "work with ids and modifier hash" do
@page_class.push_all(@page.id, @page2.id, :tags => @tags)
-
+
@page.reload
@page.tags.should == @tags
-
+
@page2.reload
@page2.tags.should == @tags
end
end
-
+
context "pull" do
setup do
@page = @page_class.create(:title => 'Home', :tags => %w(foo bar))
@page2 = @page_class.create(:title => 'Home', :tags => %w(foo bar))
end
-
+
should "work with criteria and modifier hashes" do
@page_class.pull({:title => 'Home'}, :tags => 'foo')
-
+
@page.reload
@page.tags.should == %w(bar)
-
+
@page2.reload
@page2.tags.should == %w(bar)
end
-
+
should "be able to pull with ids and modifier hash" do
@page_class.pull(@page.id, @page2.id, :tags => 'foo')
-
+
@page.reload
@page.tags.should == %w(bar)
-
+
@page2.reload
@page2.tags.should == %w(bar)
end
end
-
+
context "pull_all" do
setup do
@page = @page_class.create(:title => 'Home', :tags => %w(foo bar baz))
@page2 = @page_class.create(:title => 'Home', :tags => %w(foo bar baz))
end
-
+
should "work with criteria and modifier hashes" do
@page_class.pull_all({:title => 'Home'}, :tags => %w(foo bar))
-
+
@page.reload
@page.tags.should == %w(baz)
-
+
@page2.reload
@page2.tags.should == %w(baz)
end
-
+
should "work with ids and modifier hash" do
@page_class.pull_all(@page.id, @page2.id, :tags => %w(foo bar))
-
+
@page.reload
@page.tags.should == %w(baz)
-
+
@page2.reload
@page2.tags.should == %w(baz)
end
end
-
+
context "add_to_set" do
setup do
@page = @page_class.create(:title => 'Home', :tags => 'foo')
@page2 = @page_class.create(:title => 'Home')
end
-
+
should "be able to add to set with criteria and modifier hash" do
@page_class.add_to_set({:title => 'Home'}, :tags => 'foo')
-
+
@page.reload
@page.tags.should == %w(foo)
-
+
@page2.reload
@page2.tags.should == %w(foo)
end
-
+
should "be able to add to set with ids and modifier hash" do
@page_class.add_to_set(@page.id, @page2.id, :tags => 'foo')
-
+
@page.reload
@page.tags.should == %w(foo)
-
+
@page2.reload
@page2.tags.should == %w(foo)
end
end
-
+
context "push_uniq" do
setup do
@page = @page_class.create(:title => 'Home', :tags => 'foo')
@page2 = @page_class.create(:title => 'Home')
end
-
+
should "be able to push uniq with criteria and modifier hash" do
@page_class.push_uniq({:title => 'Home'}, :tags => 'foo')
-
+
@page.reload
@page.tags.should == %w(foo)
-
+
@page2.reload
@page2.tags.should == %w(foo)
end
-
+
should "be able to push uniq with ids and modifier hash" do
@page_class.push_uniq(@page.id, @page2.id, :tags => 'foo')
-
+
@page.reload
@page.tags.should == %w(foo)
-
+
@page2.reload
@page2.tags.should == %w(foo)
end
end
-
+
context "pop" do
setup do
@page = @page_class.create(:title => 'Home', :tags => %w(foo bar))
end
-
+
should "be able to remove the last element the array" do
@page_class.pop(@page.id, :tags => 1)
@page.reload
@page.tags.should == %w(foo)
end
-
+
should "be able to remove the first element of the array" do
@page_class.pop(@page.id, :tags => -1)
@page.reload
@page.tags.should == %w(bar)
end
end
+
+ context "additional options (upsert & safe)" do
+ should "be able to pass upsert option" do
+ new_key_value = DateTime.now.to_s
+ @page_class.increment({:title => new_key_value}, {:day_count => 1}, {:upsert => true})
+ @page_class.count(:title => new_key_value).should == 1
+ @page_class.first(:title => new_key_value).day_count.should == 1
+ end
+
+ should "be able to pass safe option" do
+ @page_class.create(:title => "Better Be Safe than Sorry")
+
+ # We are trying to increment a key of type string here which should fail
+ assert_raises(Mongo::OperationFailure) do
+ @page_class.increment({:title => "Better Be Safe than Sorry"}, {:title => 1}, {:safe => true})
+ end
+ end
+
+ should "be able to pass both safe and upsert options" do
+ new_key_value = DateTime.now.to_s
+ @page_class.increment({:title => new_key_value}, {:day_count => 1}, {:upsert => true, :safe => true})
+ @page_class.count(:title => new_key_value).should == 1
+ @page_class.first(:title => new_key_value).day_count.should == 1
+ end
+ end
end
context "instance methods" do
@@ -331,102 +356,128 @@ def assert_keys_removed(page, *keys)
page.unset(:title, :tags)
assert_keys_removed page, :title, :tags
end
-
+
should "be able to increment with modifier hashes" do
page = @page_class.create
page.increment(:day_count => 1, :week_count => 2, :month_count => 3)
-
+
assert_page_counts page, 1, 2, 3
end
-
+
should "be able to decrement with modifier hashes" do
page = @page_class.create(:day_count => 1, :week_count => 2, :month_count => 3)
page.decrement(:day_count => 1, :week_count => 2, :month_count => 3)
-
+
assert_page_counts page, 0, 0, 0
end
-
+
should "always decrement when decrement is called whether number is positive or negative" do
page = @page_class.create(:day_count => 1, :week_count => 2, :month_count => 3)
page.decrement(:day_count => -1, :week_count => 2, :month_count => -3)
-
+
assert_page_counts page, 0, 0, 0
end
-
+
should "be able to set with modifier hashes" do
page = @page_class.create(:title => 'Home')
page.set(:title => 'Home Revised')
-
+
page.reload
page.title.should == 'Home Revised'
end
-
+
should "be able to push with modifier hashes" do
page = @page_class.create
page.push(:tags => 'foo')
-
+
page.reload
page.tags.should == %w(foo)
end
-
+
should "be able to push_all with modifier hashes" do
page = @page_class.create
page.push_all(:tags => %w(foo bar))
-
+
page.reload
page.tags.should == %w(foo bar)
end
-
+
should "be able to pull with criteria and modifier hashes" do
page = @page_class.create(:tags => %w(foo bar))
page.pull(:tags => 'foo')
-
+
page.reload
page.tags.should == %w(bar)
end
-
+
should "be able to pull_all with criteria and modifier hashes" do
page = @page_class.create(:tags => %w(foo bar baz))
page.pull_all(:tags => %w(foo bar))
-
+
page.reload
page.tags.should == %w(baz)
end
-
+
should "be able to add_to_set with criteria and modifier hash" do
page = @page_class.create(:tags => 'foo')
page2 = @page_class.create
-
+
page.add_to_set(:tags => 'foo')
page2.add_to_set(:tags => 'foo')
-
+
page.reload
page.tags.should == %w(foo)
-
+
page2.reload
page2.tags.should == %w(foo)
end
-
+
should "be able to push uniq with criteria and modifier hash" do
page = @page_class.create(:tags => 'foo')
page2 = @page_class.create
-
+
page.push_uniq(:tags => 'foo')
page2.push_uniq(:tags => 'foo')
-
+
page.reload
page.tags.should == %w(foo)
-
+
page2.reload
page2.tags.should == %w(foo)
end
-
+
should "be able to pop with modifier hashes" do
page = @page_class.create(:tags => %w(foo bar))
page.pop(:tags => 1)
-
+
page.reload
page.tags.should == %w(foo)
end
+
+ should "be able to pass upsert option" do
+ page = @page_class.create(:title => "Upsert Page")
+ page.increment({:new_count => 1}, {:upsert => true})
+
+ page.reload
+ page.new_count.should == 1
+ end
+
+ should "be able to pass safe option" do
+ page = @page_class.create(:title => "Safe Page")
+
+ # We are trying to increment a key of type string here which should fail
+ assert_raises(Mongo::OperationFailure) do
+ page.increment({:title => 1}, {:safe => true})
+ end
+ end
+
+ should "be able to pass upsert and safe options" do
+ page = @page_class.create(:title => "Upsert and Safe Page")
+ page.increment({:another_count => 1}, {:upsert => true, :safe => true})
+
+ page.reload
+ page.another_count.should == 1
+ end
+
end
end
Something went wrong with that request. Please try again.