diff --git a/lib/mongoid/attributes.rb b/lib/mongoid/attributes.rb index 73f734fa4a..25f4d8dda8 100644 --- a/lib/mongoid/attributes.rb +++ b/lib/mongoid/attributes.rb @@ -176,7 +176,8 @@ def write_attribute(name, value) attribute_will_change!(access) end if localized - (attributes[access] ||= {}).merge!(typed_value) + attributes[access] ||= {} + attributes[access].merge!(typed_value) else attributes[access] = typed_value end diff --git a/lib/mongoid/attributes/processing.rb b/lib/mongoid/attributes/processing.rb index e70b9f7662..401d76850f 100644 --- a/lib/mongoid/attributes/processing.rb +++ b/lib/mongoid/attributes/processing.rb @@ -92,6 +92,9 @@ def pending_nested # # @since 2.0.0.rc.7 def process_attribute(name, value) + if !respond_to?("#{name}=", true) && store_as = aliased_fields.invert[name.to_s] + name = store_as + end responds = respond_to?("#{name}=") raise Errors::UnknownAttribute.new(self.class, name) unless responds send("#{name}=", value) diff --git a/lib/mongoid/contextual/memory.rb b/lib/mongoid/contextual/memory.rb index 835152cdc2..023faae7ce 100644 --- a/lib/mongoid/contextual/memory.rb +++ b/lib/mongoid/contextual/memory.rb @@ -9,7 +9,6 @@ class Memory include Aggregable::Memory include Relations::Eager include Queryable - include Positional # @attribute [r] root The root document. # @attribute [r] path The atomic path. @@ -47,9 +46,7 @@ def delete doc.as_document end unless removed.empty? - collection.find(selector).update( - positionally(selector, "$pullAll" => { path => removed }) - ) + collection.find(selector).update("$pullAll" => { path => removed }) end deleted end diff --git a/lib/mongoid/copyable.rb b/lib/mongoid/copyable.rb index abab1b3430..ee0228358d 100644 --- a/lib/mongoid/copyable.rb +++ b/lib/mongoid/copyable.rb @@ -22,7 +22,21 @@ def clone # _id and id field in the document would cause problems with Mongoid # elsewhere. attrs = clone_document.except("_id", "id") - self.class.new(attrs) + dynamic_attrs = {} + attrs.reject! do |attr_name, value| + dynamic_attrs.merge!(attr_name => value) unless self.attribute_names.include?(attr_name) + end + self.class.new(attrs).tap do |object| + dynamic_attrs.each do |attr_name, value| + if object.respond_to?("#{attr_name}=") + object.send("#{attr_name}=", value) + elsif (store_as = aliased_fields.invert[attr_name.to_s]) + object.send("#{store_as}=", value) + else + object.attributes[attr_name] = value + end + end + end end alias :dup :clone @@ -40,7 +54,7 @@ def clone # @since 3.0.22 def clone_document attrs = as_document.__deep_copy__ - process_localized_attributes(attrs) + process_localized_attributes(self, attrs) attrs end @@ -55,12 +69,24 @@ def clone_document # @param [ Hash ] attrs The attributes. # # @since 3.0.20 - def process_localized_attributes(attrs) - localized_fields.keys.each do |name| + def process_localized_attributes(klass, attrs) + klass.localized_fields.keys.each do |name| if value = attrs.delete(name) attrs["#{name}_translations"] = value end end + klass.embedded_relations.each do |_, metadata| + next unless attrs.present? && attrs[metadata.key].present? + + if metadata.macro == :embeds_many + attrs[metadata.key].each do |attr| + embedded_klass = attr.fetch('_type', metadata.class_name).constantize + process_localized_attributes(embedded_klass, attr) + end + else + process_localized_attributes(metadata.klass, attrs[metadata.key]) + end + end end end end diff --git a/lib/mongoid/document.rb b/lib/mongoid/document.rb index 09d4b0db2b..e7a6a0d327 100644 --- a/lib/mongoid/document.rb +++ b/lib/mongoid/document.rb @@ -1,5 +1,4 @@ # encoding: utf-8 -require "mongoid/positional" require "mongoid/evolvable" require "mongoid/extensions" require "mongoid/errors" diff --git a/lib/mongoid/persistable.rb b/lib/mongoid/persistable.rb index a846b87025..adfa651a20 100644 --- a/lib/mongoid/persistable.rb +++ b/lib/mongoid/persistable.rb @@ -27,7 +27,6 @@ module Persistable include Incrementable include Logical include Poppable - include Positional include Pullable include Pushable include Renamable @@ -209,7 +208,7 @@ def persist_or_delay_atomic_operation(operation) def persist_atomic_operations(operations) if persisted? selector = atomic_selector - _root.collection.find(selector).update(positionally(selector, operations)) + _root.collection.find(selector).update(operations) end end end diff --git a/lib/mongoid/persistable/creatable.rb b/lib/mongoid/persistable/creatable.rb index 1d5501753e..f142eb65a1 100644 --- a/lib/mongoid/persistable/creatable.rb +++ b/lib/mongoid/persistable/creatable.rb @@ -61,7 +61,7 @@ def insert_as_embedded _parent.insert else selector = _parent.atomic_selector - _root.collection.find(selector).update(positionally(selector, atomic_inserts)) + _root.collection.find(selector).update(atomic_inserts) end end diff --git a/lib/mongoid/persistable/deletable.rb b/lib/mongoid/persistable/deletable.rb index 1f58ca3d0f..bd1aa630d1 100644 --- a/lib/mongoid/persistable/deletable.rb +++ b/lib/mongoid/persistable/deletable.rb @@ -62,7 +62,7 @@ def delete_as_embedded(options = {}) _parent.remove_child(self) if notifying_parent?(options) if _parent.persisted? selector = _parent.atomic_selector - _root.collection.find(selector).update(positionally(selector, atomic_deletes)) + _root.collection.find(selector).update(atomic_deletes) end true end diff --git a/lib/mongoid/persistable/updatable.rb b/lib/mongoid/persistable/updatable.rb index 1633c1cc24..d5a44e036f 100644 --- a/lib/mongoid/persistable/updatable.rb +++ b/lib/mongoid/persistable/updatable.rb @@ -141,9 +141,9 @@ def update_document(options = {}) unless updates.empty? coll = _root.collection selector = atomic_selector - coll.find(selector).update(positionally(selector, updates)) + coll.find(selector).update(updates) conflicts.each_pair do |key, value| - coll.find(selector).update(positionally(selector, { key => value })) + coll.find(selector).update({ key => value }) end end end diff --git a/lib/mongoid/positional.rb b/lib/mongoid/positional.rb deleted file mode 100644 index 00ae9ffa99..0000000000 --- a/lib/mongoid/positional.rb +++ /dev/null @@ -1,71 +0,0 @@ -# encoding: utf-8 -module Mongoid - - # This module is responsible for taking update selectors and switching out - # the indexes for the $ positional operator where appropriate. - # - # @since 4.0.0 - module Positional - - # Takes the provided selector and atomic operations and replaces the - # indexes of the embedded documents with the positional operator when - # needed. - # - # @note The only time we can accurately know when to use the positional - # operator is at the exact time we are going to persist something. So - # we can tell by the selector that we are sending if it is actually - # possible to use the positional operator at all. For example, if the - # selector is: { "_id" => 1 }, then we could not use the positional - # operator for updating embedded documents since there would never be a - # match - we base whether we can based on the number of levels deep the - # selector goes, and if the id values are not nil. - # - # @example Process the operations. - # positionally( - # { "_id" => 1, "addresses._id" => 2 }, - # { "$set" => { "addresses.0.street" => "hobrecht" }} - # ) - # - # @param [ Hash ] selector The selector. - # @param [ Hash ] operations The update operations. - # @param [ Hash ] processed The processed update operations. - # - # @return [ Hash ] The new operations. - # - # @since 3.1.0 - def positionally(selector, operations, processed = {}) - if selector.size == 1 || selector.values.any? { |val| val.nil? } - return operations - end - keys = selector.keys.map{ |m| m.sub('._id','') } - ['_id'] - keys = keys.sort_by { |s| s.length*-1 } - process_operations(keys, operations, processed) - end - - private - - def process_operations(keys, operations, processed) - operations.each_pair do |operation, update| - processed[operation] = process_updates(keys, update) - end - processed - end - - def process_updates(keys, update, updates = {}) - update.each_pair do |position, value| - updates[replace_index(keys, position)] = value - end - updates - end - - def replace_index(keys, position) - # replace to $ only if that key is on the selector - keys.each do |kk| - if position =~ /^#{kk}\.\d+\.(.*)/ - return "#{kk}.$.#{$1}" - end - end - position - end - end -end diff --git a/lib/mongoid/relations/embedded/batchable.rb b/lib/mongoid/relations/embedded/batchable.rb index 09749cd0f9..8cb3478254 100644 --- a/lib/mongoid/relations/embedded/batchable.rb +++ b/lib/mongoid/relations/embedded/batchable.rb @@ -6,7 +6,6 @@ module Embedded # Contains behaviour for executing operations in batch on embedded # documents. module Batchable - include Positional # Insert new documents as a batch push ($pushAll). This ensures that # all callbacks are run at the appropriate time and only 1 request is @@ -37,9 +36,7 @@ def batch_insert(docs) def batch_clear(docs) pre_process_batch_remove(docs, :delete) unless docs.empty? - collection.find(selector).update( - positionally(selector, "$unset" => { path => true }) - ) + collection.find(selector).update("$unset" => { path => true }) post_process_batch_remove(docs, :delete) end _unscoped.clear @@ -57,9 +54,7 @@ def batch_clear(docs) def batch_remove(docs, method = :delete) removals = pre_process_batch_remove(docs, method) if !docs.empty? - collection.find(selector).update( - positionally(selector, "$pullAll" => { path => removals }) - ) + collection.find(selector).update("$pullAll" => { path => removals }) post_process_batch_remove(docs, method) end reindex @@ -130,9 +125,7 @@ def execute_batch_insert(docs, operation) self.inserts_valid = true inserts = pre_process_batch_insert(docs) if insertable? - collection.find(selector).update( - positionally(selector, operation => { path => inserts }) - ) + collection.find(selector).update(operation => { path => inserts }) post_process_batch_insert(docs) end inserts diff --git a/lib/mongoid/relations/touchable.rb b/lib/mongoid/relations/touchable.rb index f6f473fa6a..4726a4b46a 100644 --- a/lib/mongoid/relations/touchable.rb +++ b/lib/mongoid/relations/touchable.rb @@ -31,7 +31,7 @@ def touch(field = nil) touches = touch_atomic_updates(field) unless touches.empty? selector = atomic_selector - _root.collection.where(selector).update(positionally(selector, touches)) + _root.collection.find(selector).update(touches) end run_callbacks(:touch) true diff --git a/spec/app/models/address_customized.rb b/spec/app/models/address_customized.rb new file mode 100644 index 0000000000..490fb49c3f --- /dev/null +++ b/spec/app/models/address_customized.rb @@ -0,0 +1,3 @@ +class AddressCustomized < Address + field :alternative_name, localize: true +end diff --git a/spec/app/models/courier_job.rb b/spec/app/models/courier_job.rb new file mode 100644 index 0000000000..a751e8c083 --- /dev/null +++ b/spec/app/models/courier_job.rb @@ -0,0 +1,4 @@ +class CourierJob + include Mongoid::Document + embeds_one :drop_address, as: :addressable, autobuild: true, class_name: "ShipmentAddress" +end diff --git a/spec/app/models/shipment_address.rb b/spec/app/models/shipment_address.rb new file mode 100644 index 0000000000..efddd8d9e8 --- /dev/null +++ b/spec/app/models/shipment_address.rb @@ -0,0 +1,2 @@ +class ShipmentAddress < Address +end diff --git a/spec/app/models/store_as_dup_test1.rb b/spec/app/models/store_as_dup_test1.rb new file mode 100644 index 0000000000..c18c038c93 --- /dev/null +++ b/spec/app/models/store_as_dup_test1.rb @@ -0,0 +1,5 @@ +class StoreAsDupTest1 + include Mongoid::Document + embeds_one :store_as_dup_test2, :store_as => :t + field :name +end diff --git a/spec/app/models/store_as_dup_test2.rb b/spec/app/models/store_as_dup_test2.rb new file mode 100644 index 0000000000..175da72426 --- /dev/null +++ b/spec/app/models/store_as_dup_test2.rb @@ -0,0 +1,5 @@ +class StoreAsDupTest2 + include Mongoid::Document + embedded_in :store_as_dup_test1 + field :name +end diff --git a/spec/app/models/store_as_dup_test3.rb b/spec/app/models/store_as_dup_test3.rb new file mode 100644 index 0000000000..ddb20b21da --- /dev/null +++ b/spec/app/models/store_as_dup_test3.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class StoreAsDupTest3 + include Mongoid::Document + embeds_many :store_as_dup_test4s, :store_as => :t + field :name +end diff --git a/spec/app/models/store_as_dup_test4.rb b/spec/app/models/store_as_dup_test4.rb new file mode 100644 index 0000000000..bcd8a0c01f --- /dev/null +++ b/spec/app/models/store_as_dup_test4.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class StoreAsDupTest4 + include Mongoid::Document + embedded_in :store_as_dup_test3 + field :name +end diff --git a/spec/mongoid/attributes_spec.rb b/spec/mongoid/attributes_spec.rb index 3fe0fc2fd7..d77184ff96 100644 --- a/spec/mongoid/attributes_spec.rb +++ b/spec/mongoid/attributes_spec.rb @@ -1207,6 +1207,19 @@ }.to raise_error(Mongoid::Errors::InvalidValue) end end + + context "when attribute is localized and #attributes is a BSON::Document" do + let(:dictionary) { Dictionary.new } + + before do + allow(dictionary).to receive(:attributes).and_return(BSON::Document.new) + end + + it "sets the value for the current locale" do + dictionary.write_attribute(:description, 'foo') + expect(dictionary.description).to eq('foo') + end + end end describe "#typed_value_for" do diff --git a/spec/mongoid/copyable_spec.rb b/spec/mongoid/copyable_spec.rb index 19f770d209..1673b86aec 100644 --- a/spec/mongoid/copyable_spec.rb +++ b/spec/mongoid/copyable_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # -*- coding: utf-8 -*- require "spec_helper" @@ -20,7 +21,7 @@ end let!(:address) do - person.addresses.build(street: "Bond") + person.addresses.build(street: "Bond", name: "Bond") end let!(:name) do @@ -35,6 +36,10 @@ person.build_game(name: "Tron") end + let!(:name_translations) do + person.name.translations.build(language: 'en') + end + context "when the document has an id field in the database" do let!(:band) do @@ -54,11 +59,85 @@ end end + context "when a document has fields from a legacy schema" do + + let!(:actor) do + Actor.create(name: "test") + end + + before do + legacy_fields = { "this_is_not_a_field" => 1, "this_legacy_field_is_nil" => nil } + Actor.collection.find(_id: actor.id).update_all("$set" => legacy_fields) + end + + let(:cloned) do + actor.reload.send(method) + end + + it "sets the legacy attribute" do + expect(cloned.attributes['this_is_not_a_field']).to eq(1) + end + + it "contains legacy attribute that are nil" do + expect(cloned.attributes.key?('this_legacy_field_is_nil')).to eq(true) + end + + it "copies the known attriutes" do + expect(cloned.name).to eq('test') + end + end + + context "when using store_as" do + + context "and dynamic attributes are not set" do + + context 'embeds_one' do + + it "clones" do + t = StoreAsDupTest1.new(:name => "hi") + t.build_store_as_dup_test2(:name => "there") + t.save + copy = t.send(method) + expect(copy.object_id).not_to eq(t.object_id) + expect(copy.store_as_dup_test2.name).to eq(t.store_as_dup_test2.name) + end + end + + context 'embeds_many' do + + it "clones" do + t = StoreAsDupTest3.new(:name => "hi") + t.store_as_dup_test4s << StoreAsDupTest4.new + t.save + copy = t.send(method) + expect(copy.object_id).not_to eq(t.object_id) + expect(copy.store_as_dup_test4s).not_to be_empty + expect(copy.store_as_dup_test4s.first.object_id).not_to eq(t.store_as_dup_test4s.first.object_id) + end + end + + context 'embeds_many' do + + it "clones" do + t = StoreAsDupTest3.new(:name => "hi") + t.store_as_dup_test4s << StoreAsDupTest4.new + t.save + copy = t.send(method) + expect(copy.object_id).not_to eq(t.object_id) + expect(copy.store_as_dup_test4s).not_to be_empty + expect(copy.store_as_dup_test4s.first.object_id).not_to eq(t.store_as_dup_test4s.first.object_id) + end + end + end + end + context "when cloning a document with multiple languages field" do before do + I18n.enforce_available_locales = false I18n.locale = 'pt_BR' person.desc = "descrição" + person.addresses.first.name = "descrição" person.save end @@ -88,6 +167,55 @@ I18n.locale = :fr expect(copy.desc).to be_nil end + + it 'sets embedded translations' do + I18n.locale = 'pt_BR' + expect(copy.addresses.first.name).to eq("descrição") + end + + it 'sets embedded english version' do + I18n.locale = :en + expect(copy.addresses.first.name).to eq("Bond") + end + end + + context "when cloning a document with polymorphic embedded documents with multiple language field" do + + let!(:address_customized) do + person.addresses.build({ street: "Bond", name: "Bond" }, AddressCustomized) + end + + before do + I18n.enforce_available_locales = false + I18n.locale = 'pt_BR' + person.addresses.each { |address| address.name = "descrição" } + person.addresses.type(AddressCustomized).first.alternative_name = "alternativa" + person.save + end + + after do + I18n.locale = :en + end + + let!(:from_db) do + Person.find(person.id) + end + + let(:copy) do + from_db.send(method) + end + + it 'sets embedded translations' do + I18n.locale = 'pt_BR' + copy.addresses.each do |address| + expect(address.name).to eq("descrição") + end + + copy.addresses.type(AddressCustomized).each do |address| + expect(address.alternative_name).to eq("alternativa") + end + end + end context "when cloning a loaded document" do @@ -157,14 +285,26 @@ expect(copy.addresses).to eq(person.addresses) end + it "copys deep embeds many documents" do + expect(copy.name.translations).to eq(person.name.translations) + end + it "sets the embedded many documents as new" do expect(copy.addresses.first).to be_new_record end + it "sets the deep embedded many documents as new" do + expect(copy.name.translations.first).to be_new_record + end + it "creates new embeds many instances" do expect(copy.addresses).to_not equal(person.addresses) end + it "creates new deep embeds many instances" do + expect(copy.name.translations).to_not equal(person.name.translations) + end + it "copys embeds one documents" do expect(copy.name).to eq(person.name) end diff --git a/spec/mongoid/positional_spec.rb b/spec/mongoid/positional_spec.rb deleted file mode 100644 index 49f75d62b6..0000000000 --- a/spec/mongoid/positional_spec.rb +++ /dev/null @@ -1,222 +0,0 @@ -require "spec_helper" - -describe Mongoid::Positional do - - describe "#positionally" do - - let(:positionable) do - Class.new do - include Mongoid::Positional - end.new - end - - let(:updates) do - { - "$set" => { - "field" => "value", - "children.0.field" => "value", - "children.0.children.1.children.3.field" => "value" - }, - "$pushAll" => { - "children.0.children.1.children.3.fields" => [ "value", "value" ] - } - } - end - - context "when a child has an embeds many under an embeds one" do - - context "when selector does not include the embeds one" do - - let(:selector) do - { "_id" => 1, "child._id" => 2 } - end - - let(:ops) do - { - "$set" => { - "field" => "value", - "child.children.1.children.3.field" => "value", - } - } - end - - let(:processed) do - positionable.positionally(selector, ops) - end - - it "does not do any replacement" do - expect(processed).to eq(ops) - end - end - - context "when selector includes the embeds one" do - - let(:selector) do - { "_id" => 1, "child._id" => 2, "child.children._id" => 3 } - end - - let(:ops) do - { - "$set" => { - "field" => "value", - "child.children.1.children.3.field" => "value", - } - } - end - - let(:expected) do - { - "$set" => { - "field" => "value", - "child.children.$.children.3.field" => "value", - } - } - end - - - let(:processed) do - positionable.positionally(selector, ops) - end - - it "does not do any replacement" do - expect(processed).to eq(expected) - end - end - end - - context "when the selector has only 1 pair" do - - let(:selector) do - { "_id" => 1 } - end - - let(:processed) do - positionable.positionally(selector, updates) - end - - it "does not do any replacement" do - expect(processed).to eq(updates) - end - end - - context "when the selector has 2 pairs" do - - context "when the second pair has an id" do - - let(:selector) do - { "_id" => 1, "children._id" => 2 } - end - - let(:expected) do - { - "$set" => { - "field" => "value", - "children.$.field" => "value", - "children.$.children.1.children.3.field" => "value" - }, - "$pushAll" => { - "children.$.children.1.children.3.fields" => [ "value", "value" ] - } - } - end - - let(:processed) do - positionable.positionally(selector, updates) - end - - it "replaces the first index with the positional operator" do - expect(processed).to eq(expected) - end - end - - context "when the second pair has no id" do - - let(:selector) do - { "_id" => 1, "children._id" => nil } - end - - let(:expected) do - { - "$set" => { - "field" => "value", - "children.0.field" => "value", - "children.0.children.1.children.3.field" => "value" - }, - "$pushAll" => { - "children.0.children.1.children.3.fields" => [ "value", "value" ] - } - } - end - - let(:processed) do - positionable.positionally(selector, updates) - end - - it "replaces the first index with the positional operator" do - expect(processed).to eq(expected) - end - end - end - - context "when the selector has 3 pairs" do - - let(:selector) do - { "_id" => 1, "children._id" => 2, "children.0.children._id" => 3 } - end - - let(:expected) do - { - "$set" => { - "field" => "value", - "children.$.field" => "value", - "children.0.children.$.children.3.field" => "value" - }, - "$pushAll" => { - "children.0.children.$.children.3.fields" => [ "value", "value" ] - } - } - end - - let(:processed) do - positionable.positionally(selector, updates) - end - - it "replaces the first index with the positional operator" do - expect(processed).to eq(expected) - end - end - - context "when the selector has 4 pairs" do - - let(:selector) do - { - "_id" => 1, - "children._id" => 2, - "children.0.children._id" => 3, - "children.0.children.1.children._id" => 4 - } - end - - let(:expected) do - { - "$set" => { - "field" => "value", - "children.$.field" => "value", - "children.0.children.1.children.$.field" => "value" - }, - "$pushAll" => { - "children.0.children.1.children.$.fields" => [ "value", "value" ] - } - } - end - - let(:processed) do - positionable.positionally(selector, updates) - end - - it "replaces the first index with the positional operator" do - expect(processed).to eq(expected) - end - end - end -end diff --git a/spec/mongoid/relations/embedded/one_spec.rb b/spec/mongoid/relations/embedded/one_spec.rb index 55051a0027..da7f14dc9f 100644 --- a/spec/mongoid/relations/embedded/one_spec.rb +++ b/spec/mongoid/relations/embedded/one_spec.rb @@ -768,6 +768,37 @@ class << person end end + describe "when the relationship is polymorphic" do + + context "when updating an aliased embedded document" do + + context "when the embedded document inherits its relationship" do + + let(:courier_job) do + CourierJob.create + end + + let(:old_child) do + ShipmentAddress.new + end + + let(:new_child) do + ShipmentAddress.new + end + + before do + courier_job.drop_address = old_child + courier_job.update_attribute(:drop_address, new_child) + courier_job.reload + end + + it "the child is embedded correctly" do + expect(courier_job.drop_address).to eq(new_child) + end + end + end + end + describe ".embedded?" do it "returns true" do @@ -963,7 +994,7 @@ class << person end before do - address_two.code = code + person.reload.addresses.first.code = code end it "reloads the correct number" do