Skip to content

Commit

Permalink
Array foreign key fields perform $addToSet ops via dirty tracking. Fixes
Browse files Browse the repository at this point in the history
 #1530.
  • Loading branch information
durran committed Dec 30, 2011
1 parent d1d410b commit 9e561d0
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 9 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Expand Up @@ -65,6 +65,9 @@ For instructions on upgrading to newer versions, visit
* Calling `Document#as_document` on a frozen document on Rubinius returns the
attributes instead of nil.

* \#1530 Don't duplicate added values to arrays via dirty tracking if the
array is a foreign key field.

* \#1523 Allow disabling of observers via `disable`. (Jonas Schneider)

* \#1522 Fixed create indexes rake task for Rails 3.2. (Gray Manley)
Expand Down
14 changes: 14 additions & 0 deletions lib/mongoid/atomic.rb
Expand Up @@ -12,6 +12,7 @@ module Atomic
UPDATES = [
:atomic_array_pushes,
:atomic_array_pulls,
:atomic_array_add_to_sets,
:atomic_pulls,
:atomic_unsets,
:delayed_atomic_sets,
Expand Down Expand Up @@ -62,6 +63,18 @@ def atomic_array_pulls
@atomic_array_pulls ||= {}
end

# For array fields these are the unique adds that need to happen.
#
# @example Get the array unique adds.
# person.atomic_array_add_to_sets
#
# @return [ Hash ] The array add_to_sets.
#
# @since 2.4.0
def atomic_array_add_to_sets
@atomic_array_add_to_sets ||= {}
end

# Get all the atomic updates that need to happen for the current
# +Document+. This includes all changes that need to happen in the
# entire hierarchy that exists below where the save call was made.
Expand Down Expand Up @@ -259,6 +272,7 @@ def generate_atomic_updates(mods, doc)
mods.set(doc.delayed_atomic_sets)
mods.push(doc.atomic_pushes)
mods.push(doc.atomic_array_pushes)
mods.add_to_set(doc.atomic_array_add_to_sets)
mods.pull(doc.atomic_array_pulls)
end
end
Expand Down
34 changes: 33 additions & 1 deletion lib/mongoid/atomic/modifiers.rb
Expand Up @@ -6,6 +6,26 @@ module Atomic #:nodoc:
# database.
class Modifiers < Hash

# Add the atomic $addToSet modifiers to the hash.
#
# @example Add the $addToSet modifiers.
# modifiers.add_to_set({ "preference_ids" => [ "one" ] })
#
# @param [ Hash ] modifications The add to set modifiers.
#
# @since 2.4.0
def add_to_set(modifications)
modifications.each_pair do |field, value|
if add_to_sets.has_key?(field)
value.each do |val|
add_to_sets[field]["$each"].push(val)
end
else
add_to_sets[field] = { "$each" => value }
end
end
end

# Adds pull modifiers to the modifiers hash.
#
# @example Add pull operations.
Expand Down Expand Up @@ -90,6 +110,18 @@ def add_operation(mods, field, value)
end
end

# Get the $addToSet operations or intialize a new one.
#
# @example Get the $addToSet operations.
# modifiers.add_to_sets
#
# @return [ Hash ] The $addToSet operations.
#
# @since 2.4.0
def add_to_sets
self["$addToSet"] ||= {}
end

# Is the operation going to be a conflict for a $set?
#
# @example Is this a conflict for a set?
Expand Down Expand Up @@ -194,7 +226,7 @@ def set_fields
# Get the $pullAll operations or intialize a new one.
#
# @example Get the $pullAll operations.
# modifiers.pulles
# modifiers.pulls
#
# @return [ Hash ] The $pullAll operations.
#
Expand Down
8 changes: 1 addition & 7 deletions lib/mongoid/dirty.rb
Expand Up @@ -118,13 +118,7 @@ def setters
key = embedded? ? "#{atomic_position}.#{name}" : name
if field && field.resizable?
pushes, pulls = new - (old || []), (old || []) - new
if pushes.any? && pulls.any?
modifications[key] = new
elsif pushes.any?
atomic_array_pushes[key] = pushes
else
atomic_array_pulls[key] = pulls
end
field.add_atomic_changes(self, key, modifications, pushes, pulls)
else
modifications[key] = new
end
Expand Down
22 changes: 22 additions & 0 deletions lib/mongoid/fields/internal/array.rb
Expand Up @@ -7,6 +7,28 @@ module Internal #:nodoc:
class Array
include Serializable

# Adds the atomic changes for this type of resizable field.
#
# @example Add the atomic changes.
# field.add_atomic_changes(doc, "key", {}, [], [])
#
# @param [ Document ] document The document to add to.
# @param [ String ] key The name of the field.
# @param [ Hash ] mods The current modifications.
# @param [ Array ] new The new elements to add.
# @param [ Array ] old The old elements getting removed.
#
# @since 2.4.0
def add_atomic_changes(document, key, mods, new, old)
if new.any? && old.any?
mods[key] = new
elsif new.any?
document.atomic_array_pushes[key] = new
elsif old.any?
document.atomic_array_pulls[key] = old
end
end

# Array fields are resizable.
#
# @example Is this field resizable?
Expand Down
24 changes: 23 additions & 1 deletion lib/mongoid/fields/internal/foreign_keys/array.rb
Expand Up @@ -4,10 +4,32 @@ module Fields #:nodoc:
module Internal #:nodoc:
module ForeignKeys #:nodoc:

# Defines the behaviour for array fields.
# Defines the behaviour for array foreign key fields.
class Array
include Serializable

# Adds the atomic changes for this type of resizable field.
#
# @example Add the atomic changes.
# field.add_atomic_changes(doc, "key", {}, [], [])
#
# @param [ Document ] document The document to add to.
# @param [ String ] key The name of the field.
# @param [ Hash ] mods The current modifications.
# @param [ Array ] new The new elements to add.
# @param [ Array ] old The old elements getting removed.
#
# @since 2.4.0
def add_atomic_changes(document, key, mods, new, old)
if new.any? && old.any?
mods[key] = new
elsif new.any?
document.atomic_array_add_to_sets[key] = new
elsif old.any?
document.atomic_array_pulls[key] = old
end
end

# Is the field a BSON::ObjectId?
#
# @example Is the field a BSON::ObjectId?
Expand Down
55 changes: 55 additions & 0 deletions spec/unit/mongoid/atomic/modifiers_spec.rb
Expand Up @@ -6,6 +6,61 @@
described_class.new
end

describe "#add_to_set" do

context "when the unique adds are empty" do

before do
modifiers.add_to_set({})
end

it "does not contain any operations" do
modifiers.should eq({})
end
end

context "when the adds are not empty" do

let(:adds) do
{ "preference_ids" => [ "one", "two" ] }
end

context "when adding a single field" do

before do
modifiers.add_to_set(adds)
end

it "adds the add to set with each modifiers" do
modifiers.should eq({
"$addToSet" => { "preference_ids" => { "$each" => [ "one", "two" ] }}
})
end
end

context "when adding to an existing field" do

let(:adds_two) do
{ "preference_ids" => [ "three" ] }
end

before do
modifiers.add_to_set(adds)
modifiers.add_to_set(adds_two)
end

it "adds the add to set with each modifiers" do
modifiers.should eq({
"$addToSet" =>
{ "preference_ids" =>
{ "$each" => [ "one", "two", "three" ] }
}
})
end
end
end
end

describe "#pull" do

context "when the pulls are empty" do
Expand Down

0 comments on commit 9e561d0

Please sign in to comment.