diff --git a/.rubocop.yml b/.rubocop.yml index a4343f419e..f7a6156fdc 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -72,6 +72,15 @@ Metrics/ModuleLength: Metrics/MethodLength: Max: 20 +Metrics/PerceivedComplexity: + Max: 10 + +Metrics/CyclomaticComplexity: + Max: 10 + +Metrics/AbcSize: + Max: 20 + RSpec/BeforeAfterAll: Enabled: false diff --git a/lib/mongoid/association/embedded/embeds_many/proxy.rb b/lib/mongoid/association/embedded/embeds_many/proxy.rb index 75170cb2e0..f794aa3962 100644 --- a/lib/mongoid/association/embedded/embeds_many/proxy.rb +++ b/lib/mongoid/association/embedded/embeds_many/proxy.rb @@ -14,6 +14,7 @@ class EmbedsMany # the array of child documents. class Proxy < Association::Many include Batchable + extend Forwardable # Class-level methods for the Proxy class. module ClassMethods @@ -54,6 +55,8 @@ def foreign_key_suffix extend ClassMethods + def_delegators :criteria, :find, :pluck + # Instantiate a new embeds_many association. # # @example Create the new association. @@ -312,35 +315,6 @@ def exists?(id_or_conditions = :none) end end - # Finds a document in this association through several different - # methods. - # - # This method delegates to +Mongoid::Criteria#find+. If this method is - # not given a block, it returns one or many documents for the provided - # _id values. - # - # If this method is given a block, it returns the first document - # of those found by the current Criteria object for which the block - # returns a truthy value. - # - # @example Find a document by its id. - # person.addresses.find(BSON::ObjectId.new) - # - # @example Find documents for multiple ids. - # person.addresses.find([ BSON::ObjectId.new, BSON::ObjectId.new ]) - # - # @example Finds the first matching document using a block. - # person.addresses.find { |addr| addr.state == 'CA' } - # - # @param [ Object... ] *args Various arguments. - # @param &block Optional block to pass. - # @yield [ Object ] Yields each enumerable element to the block. - # - # @return [ Document | Array | nil ] A document or matching documents. - def find(...) - criteria.find(...) - end - # Get all the documents in the association that are loaded into memory. # # @example Get the in memory documents. diff --git a/lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb b/lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb index 8046f38fe4..c9b07fdab1 100644 --- a/lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb +++ b/lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb @@ -53,8 +53,6 @@ def embedded? # @param [ Document... ] *args Any number of documents. # # @return [ Array ] The loaded docs. - # - # rubocop:disable Metrics/AbcSize def <<(*args) docs = args.flatten return concat(docs) if docs.size > 1 @@ -89,7 +87,6 @@ def <<(*args) end unsynced(_base, foreign_key) and self end - # rubocop:enable Metrics/AbcSize alias push << @@ -360,8 +357,6 @@ def clear_target_for_nullify # in bulk # @param [ Array ] inserts the list of Hashes of attributes that will # be inserted (corresponding to the ``docs`` list) - # - # rubocop:disable Metrics/AbcSize def append_document(doc, ids, docs, inserts) return unless doc @@ -379,7 +374,6 @@ def append_document(doc, ids, docs, inserts) unsynced(_base, foreign_key) end end - # rubocop:enable Metrics/AbcSize end end end diff --git a/lib/mongoid/association/referenced/has_many/enumerable.rb b/lib/mongoid/association/referenced/has_many/enumerable.rb index 1fb8d13389..b2f9ef7c2f 100644 --- a/lib/mongoid/association/referenced/has_many/enumerable.rb +++ b/lib/mongoid/association/referenced/has_many/enumerable.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true # rubocop:todo all +require 'mongoid/pluckable' + module Mongoid module Association module Referenced @@ -12,6 +14,7 @@ class HasMany class Enumerable extend Forwardable include ::Enumerable + include Pluckable # The three main instance variables are collections of documents. # @@ -374,6 +377,43 @@ def marshal_load(data) @_added, @_loaded, @_unloaded, @executed = data end + # Plucks the given field names from the documents in the target. + # If the collection has been loaded, it plucks from the loaded + # documents; otherwise, it plucks from the unloaded criteria. + # Regardless, it also plucks from any added documents. + # + # @param [ Symbol... ] *fields The field names to pluck. + # + # @return [ Array | Array ] The array of field values. If + # multiple fields are given, an array of arrays is returned. + def pluck(*keys) + [].tap do |results| + if _loaded? || _added.any? + klass = @_association.klass + prepared = prepare_pluck(keys, document_class: klass) + end + + if _loaded? + docs = _loaded.values.map { |v| BSON::Document.new(v.attributes) } + results.concat pluck_from_documents(docs, prepared[:field_names], document_class: klass) + elsif _unloaded + criteria = if _added.any? + ids_to_exclude = _added.keys + _unloaded.not(:_id.in => ids_to_exclude) + else + _unloaded + end + + results.concat criteria.pluck(*keys) + end + + if _added.any? + docs = _added.values.map { |v| BSON::Document.new(v.attributes) } + results.concat pluck_from_documents(docs, prepared[:field_names], document_class: klass) + end + end + end + # Reset the enumerable back to its persisted state. # # @example Reset the enumerable. diff --git a/lib/mongoid/association/referenced/has_many/proxy.rb b/lib/mongoid/association/referenced/has_many/proxy.rb index a512351bfe..bf29253a79 100644 --- a/lib/mongoid/association/referenced/has_many/proxy.rb +++ b/lib/mongoid/association/referenced/has_many/proxy.rb @@ -33,7 +33,7 @@ def embedded? extend ClassMethods - def_delegator :criteria, :count + def_delegators :criteria, :count def_delegators :_target, :first, :in_memory, :last, :reset, :uniq # Instantiate a new references_many association. Will set the foreign key @@ -281,6 +281,22 @@ def nullify alias nullify_all nullify + # Plucks the given field names from the documents in the + # association. It is safe to use whether the association is + # loaded or not, and whether there are unsaved documents in the + # association or not. + # + # @example Pluck the titles of all posts. + # person.posts.pluck(:title) + # + # @param [ Symbol... ] *fields The field names to pluck. + # + # @return [ Array | Array ] The array of field values. If + # multiple fields are given, an array of arrays is returned. + def pluck(*fields) + _target.pluck(*fields) + end + # Clear the association. Will delete the documents from the db if they are # already persisted. # diff --git a/lib/mongoid/contextual/mongo.rb b/lib/mongoid/contextual/mongo.rb index 6d78f4084d..68b3877dfd 100644 --- a/lib/mongoid/contextual/mongo.rb +++ b/lib/mongoid/contextual/mongo.rb @@ -2,6 +2,7 @@ # rubocop:todo all require 'mongoid/atomic_update_preparer' +require 'mongoid/pluckable' require "mongoid/contextual/mongo/documents_loader" require "mongoid/contextual/atomic" require "mongoid/contextual/aggregable/mongo" @@ -22,6 +23,7 @@ class Mongo include Atomic include Association::EagerLoadable include Queryable + include Pluckable # Options constant. OPTIONS = [ :hint, @@ -331,23 +333,12 @@ def map_reduce(map, reduce) # in the array will be a single value. Otherwise, each # result in the array will be an array of values. def pluck(*fields) - # Multiple fields can map to the same field name. For example, plucking - # a field and its _translations field map to the same field in the database. - # because of this, we need to keep track of the fields requested. - normalized_field_names = [] - normalized_select = fields.inject({}) do |hash, f| - db_fn = klass.database_field_name(f) - normalized_field_names.push(db_fn) - hash[klass.cleanse_localized_field_names(f)] = true - hash - end - - view.projection(normalized_select).reduce([]) do |plucked, doc| - values = normalized_field_names.map do |n| - extract_value(doc, n) - end - plucked << (values.size == 1 ? values.first : values) - end + # Multiple fields can map to the same field name. For example, + # plucking a field and its _translations field map to the same + # field in the database. because of this, we need to prepare the + # projection specifically. + prep = prepare_pluck(fields, prepare_projection: true) + pluck_from_documents(view.projection(prep[:projection]), prep[:field_names]) end # Pick the single field values from the database. @@ -903,78 +894,6 @@ def acknowledged_write? collection.write_concern.nil? || collection.write_concern.acknowledged? end - # Fetch the element from the given hash and demongoize it using the - # given field. If the obj is an array, map over it and call this method - # on all of its elements. - # - # @param [ Hash | Array ] obj The hash or array of hashes to fetch from. - # @param [ String ] meth The key to fetch from the hash. - # @param [ Field ] field The field to use for demongoization. - # - # @return [ Object ] The demongoized value. - # - # @api private - def fetch_and_demongoize(obj, meth, field) - if obj.is_a?(Array) - obj.map { |doc| fetch_and_demongoize(doc, meth, field) } - else - res = obj.try(:fetch, meth, nil) - field ? field.demongoize(res) : res.class.demongoize(res) - end - end - - # Extracts the value for the given field name from the given attribute - # hash. - # - # @param [ Hash ] attrs The attributes hash. - # @param [ String ] field_name The name of the field to extract. - # - # @param [ Object ] The value for the given field name - def extract_value(attrs, field_name) - i = 1 - num_meths = field_name.count('.') + 1 - curr = attrs.dup - - klass.traverse_association_tree(field_name) do |meth, obj, is_field| - field = obj if is_field - is_translation = false - # If no association or field was found, check if the meth is an - # _translations field. - if obj.nil? & tr = meth.match(/(.*)_translations\z/)&.captures&.first - is_translation = true - meth = tr - end - - # 1. If curr is an array fetch from all elements in the array. - # 2. If the field is localized, and is not an _translations field - # (_translations fields don't show up in the fields hash). - # - If this is the end of the methods, return the translation for - # the current locale. - # - Otherwise, return the whole translations hash so the next method - # can select the language it wants. - # 3. If the meth is an _translations field, do not demongoize the - # value so the full hash is returned. - # 4. Otherwise, fetch and demongoize the value for the key meth. - curr = if curr.is_a? Array - res = fetch_and_demongoize(curr, meth, field) - res.empty? ? nil : res - elsif !is_translation && field&.localized? - if i < num_meths - curr.try(:fetch, meth, nil) - else - fetch_and_demongoize(curr, meth, field) - end - elsif is_translation - curr.try(:fetch, meth, nil) - else - fetch_and_demongoize(curr, meth, field) - end - - i += 1 - end - curr - end - # Recursively demongoize the given value. This method recursively traverses # the class tree to find the correct field to use to demongoize the value. # diff --git a/lib/mongoid/pluckable.rb b/lib/mongoid/pluckable.rb new file mode 100644 index 0000000000..609d581ae7 --- /dev/null +++ b/lib/mongoid/pluckable.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +module Mongoid + # Provides shared behavior for any document with "pluck" functionality. + # + # @api private + module Pluckable + extend ActiveSupport::Concern + + private + + # Prepares the field names for plucking by normalizing them to their + # database field names. Also prepares a projection hash if requested. + def prepare_pluck(field_names, document_class: klass, prepare_projection: false) + normalized_field_names = [] + projection = {} + + field_names.each do |f| + db_fn = document_class.database_field_name(f) + normalized_field_names.push(db_fn) + + next unless prepare_projection + + cleaned_name = document_class.cleanse_localized_field_names(f) + canonical_name = document_class.database_field_name(cleaned_name) + projection[canonical_name] = true + end + + { field_names: normalized_field_names, projection: projection } + end + + # Plucks the given field names from the given documents. + def pluck_from_documents(documents, field_names, document_class: klass) + documents.reduce([]) do |plucked, doc| + values = field_names.map { |name| extract_value(doc, name.to_s, document_class) } + plucked << ((values.size == 1) ? values.first : values) + end + end + + # Fetch the element from the given hash and demongoize it using the + # given field. If the obj is an array, map over it and call this method + # on all of its elements. + # + # @param [ Hash | Array ] obj The hash or array of hashes to fetch from. + # @param [ String ] key The key to fetch from the hash. + # @param [ Field ] field The field to use for demongoization. + # + # @return [ Object ] The demongoized value. + def fetch_and_demongoize(obj, key, field) + if obj.is_a?(Array) + obj.map { |doc| fetch_and_demongoize(doc, key, field) } + else + value = obj.try(:fetch, key, nil) + field ? field.demongoize(value) : value.class.demongoize(value) + end + end + + # Extracts the value for the given field name from the given attribute + # hash. + # + # @param [ Hash ] attrs The attributes hash. + # @param [ String ] field_name The name of the field to extract. + # + # @return [ Object ] The value for the given field name + def extract_value(attrs, field_name, document_class) + i = 1 + num_meths = field_name.count('.') + 1 + curr = attrs.dup + + document_class.traverse_association_tree(field_name) do |meth, obj, is_field| + field = obj if is_field + + # use the correct document class to check for localized fields on + # embedded documents. + document_class = obj.klass if obj.respond_to?(:klass) + + is_translation = false + # If no association or field was found, check if the meth is an + # _translations field. + if obj.nil? && (tr = meth.match(/(.*)_translations\z/)&.captures&.first) + is_translation = true + meth = document_class.database_field_name(tr) + end + + curr = descend(i, curr, meth, field, num_meths, is_translation) + + i += 1 + end + curr + end + + # Descend one level in the attribute hash. + # + # @param [ Integer ] part The current part index. + # @param [ Hash | Array ] current The current level in the attribute hash. + # @param [ String ] method_name The method name to descend to. + # @param [ Field|nil ] field The field to use for demongoization. + # @param [ Boolean ] is_translation Whether the method is an _translations field. + # @param [ Integer ] part_count The total number of parts in the field name. + # + # @return [ Object ] The value at the next level. + # + # rubocop:disable Metrics/ParameterLists + def descend(part, current, method_name, field, part_count, is_translation) + # 1. If curr is an array fetch from all elements in the array. + # 2. If the field is localized, and is not an _translations field + # (_translations fields don't show up in the fields hash). + # - If this is the end of the methods, return the translation for + # the current locale. + # - Otherwise, return the whole translations hash so the next method + # can select the language it wants. + # 3. If the meth is an _translations field, do not demongoize the + # value so the full hash is returned. + # 4. Otherwise, fetch and demongoize the value for the key meth. + if current.is_a? Array + res = fetch_and_demongoize(current, method_name, field) + res.empty? ? nil : res + elsif !is_translation && field&.localized? + if part < part_count + current.try(:fetch, method_name, nil) + else + fetch_and_demongoize(current, method_name, field) + end + elsif is_translation + current.try(:fetch, method_name, nil) + else + fetch_and_demongoize(current, method_name, field) + end + end + # rubocop:enable Metrics/ParameterLists + end +end diff --git a/lib/mongoid/traversable.rb b/lib/mongoid/traversable.rb index 584340de19..f495ee3cdf 100644 --- a/lib/mongoid/traversable.rb +++ b/lib/mongoid/traversable.rb @@ -131,7 +131,6 @@ module DiscriminatorAssignment # @param [ String ] value The discriminator key to set. # # @api private - # rubocop:disable Metrics/AbcSize def discriminator_key=(value) raise Errors::InvalidDiscriminatorKeyTarget.new(self, superclass) if hereditary? @@ -159,7 +158,6 @@ class << self default_proc = -> { self.class.discriminator_value } field(discriminator_key, default: default_proc, type: String) end - # rubocop:enable Metrics/AbcSize # Returns the discriminator key. # diff --git a/spec/mongoid/association/referenced/has_many/enumerable_spec.rb b/spec/mongoid/association/referenced/has_many/enumerable_spec.rb index 5d49883d30..98bcb433df 100644 --- a/spec/mongoid/association/referenced/has_many/enumerable_spec.rb +++ b/spec/mongoid/association/referenced/has_many/enumerable_spec.rb @@ -1819,6 +1819,400 @@ end end + describe '#pluck' do + let(:person) do + Person.create! + end + + let!(:post) do + Post.create!(person_id: person.id, title: 'Test Title') + end + + let(:base) { Person } + let(:association) { Person.relations[:posts] } + + let(:criteria) do + Post.where(person_id: person.id) + end + + context 'when the enumerable is not loaded' do + let!(:enumerable) do + described_class.new(criteria, base, association) + end + + context 'when the criteria is present' do + it 'delegates to the criteria pluck method' do + result = enumerable.pluck(:title) + expect(result).to eq(['Test Title']) + end + + context 'when added docs are present' do + it 'combines the results from the criteria and the added docs' do + added_post = Post.new(title: 'Added Title', person_id: person.id) + enumerable << added_post + + expect(criteria).to receive(:pluck).with(:title).and_return(['Test Title']) + result = enumerable.pluck(:title) + expect(result).to eq(['Test Title', 'Added Title']) + end + end + end + + context 'when the criteria is not present' do + let(:enumerable) { described_class.new([], base, association) } + + it 'returns nothing' do + result = enumerable.pluck(:title) + expect(result).to eq([]) + end + + context 'when added docs are present' do + it 'returns the values from the added docs' do + added_post = Post.new(title: 'Added Title', person_id: person.id) + enumerable << added_post + + result = enumerable.pluck(:title) + expect(result).to eq(['Added Title']) + end + end + end + end + + context 'when the enumerable is loaded' do + let(:enumerable) { described_class.new([post], base, association) } + + it 'returns the values from the loaded documents' do + result = enumerable.pluck(:title) + expect(result).to eq(['Test Title']) + end + + context 'when added docs are present' do + it 'returns the values from both loaded and added docs' do + added_post = Post.new(title: 'Added Title', person_id: person.id) + enumerable << added_post + + result = enumerable.pluck(:title) + expect(result).to eq(['Test Title', 'Added Title']) + end + end + end + end + + describe '#pluck with aliases' do + let!(:parent) do + Company.create! + end + + context 'when the field is aliased' do + let!(:expensive) do + parent.products.create!(price: 100000) + end + + let!(:cheap) do + parent.products.create!(price: 1) + end + + context 'when using alias_attribute' do + + let(:plucked) do + parent.products.pluck(:price) + end + + it 'uses the aliases' do + expect(plucked).to eq([ 100000, 1 ]) + end + end + end + + context 'when plucking a localized field' do + with_default_i18n_configs + + before do + I18n.locale = :en + p = parent.products.create!(name: 'english-text') + I18n.locale = :de + p.name = 'deutsch-text' + p.save! + end + + context 'when plucking the entire field' do + let(:plucked) do + parent.products.all.pluck(:name) + end + + let(:plucked_translations) do + parent.products.all.pluck(:name_translations) + end + + let(:plucked_translations_both) do + parent.products.all.pluck(:name_translations, :name) + end + + it 'returns the demongoized translations' do + expect(plucked.first).to eq('deutsch-text') + end + + it 'returns the full translations hash to _translations' do + expect(plucked_translations.first).to eq({'de'=>'deutsch-text', 'en'=>'english-text'}) + end + + it 'returns both' do + expect(plucked_translations_both.first).to eq([{'de'=>'deutsch-text', 'en'=>'english-text'}, 'deutsch-text']) + end + end + + context 'when plucking a specific locale' do + + let(:plucked) do + parent.products.all.pluck(:'name.de') + end + + it 'returns the specific translations' do + expect(plucked.first).to eq('deutsch-text') + end + end + + context 'when plucking a specific locale from _translations field' do + + let(:plucked) do + parent.products.all.pluck(:'name_translations.de') + end + + it 'returns the specific translations' do + expect(plucked.first).to eq('deutsch-text') + end + end + + context 'when fallbacks are enabled with a locale list' do + require_fallbacks + + before do + I18n.fallbacks[:he] = [ :en ] + end + + let(:plucked) do + parent.products.all.pluck(:name).first + end + + it 'correctly uses the fallback' do + I18n.locale = :en + parent.products.create!(name: 'english-text') + I18n.locale = :he + expect(plucked).to eq 'english-text' + end + end + + context 'when the localized field is aliased' do + before do + I18n.locale = :en + parent.products.delete_all + p = parent.products.create!(name: 'ACME Rocket Skates', tagline: 'english-text') + I18n.locale = :de + p.tagline = 'deutsch-text' + p.save! + end + + context 'when plucking the entire field' do + let(:plucked) do + parent.products.all.pluck(:tagline) + end + + let(:plucked_unaliased) do + parent.products.all.pluck(:tl) + end + + let(:plucked_translations) do + parent.products.all.pluck(:tagline_translations) + end + + let(:plucked_translations_both) do + parent.products.all.pluck(:tagline_translations, :tagline) + end + + it 'returns the demongoized translations' do + expect(plucked.first).to eq('deutsch-text') + end + + it 'returns the demongoized translations when unaliased' do + expect(plucked_unaliased.first).to eq('deutsch-text') + end + + it 'returns the full translations hash to _translations' do + expect(plucked_translations.first).to eq({ 'de' => 'deutsch-text', 'en' => 'english-text' }) + end + + it 'returns both' do + expect(plucked_translations_both.first).to eq([{ 'de' => 'deutsch-text', 'en' => 'english-text' }, 'deutsch-text']) + end + end + + context 'when plucking a specific locale' do + + let(:plucked) do + parent.products.all.pluck(:'tagline.de') + end + + it 'returns the specific translations' do + expect(plucked.first).to eq('deutsch-text') + end + end + + context 'when plucking a specific locale from _translations field' do + + let(:plucked) do + parent.products.all.pluck(:'tagline_translations.de') + end + + it 'returns the specific translations' do + expect(plucked.first).to eq('deutsch-text') + end + end + + context 'when fallbacks are enabled with a locale list' do + require_fallbacks + + before do + I18n.fallbacks[:he] = [:en] + end + + let(:plucked) do + parent.products.all.pluck(:tagline).first + end + + it 'correctly uses the fallback' do + I18n.locale = :en + parent.products.create!(tagline: 'english-text') + I18n.locale = :he + expect(plucked).to eq 'english-text' + end + end + end + + context 'when the localized field is embedded' do + with_default_i18n_configs + + before do + s = Seo.new + I18n.locale = :en + s.name = 'english-text' + I18n.locale = :de + s.name = 'deutsch-text' + + parent.products.delete_all + parent.products.create!(name: 'ACME Tunnel Paint', seo: s) + end + + let(:plucked) do + parent.products.pluck('seo.name').first + end + + let(:plucked_translations) do + parent.products.pluck('seo.name_translations').first + end + + let(:plucked_translations_field) do + parent.products.pluck('seo.name_translations.en').first + end + + it 'returns the translation for the current locale' do + expect(plucked).to eq('deutsch-text') + end + + it 'returns the full _translation hash' do + expect(plucked_translations).to eq({ 'en' => 'english-text', 'de' => 'deutsch-text' }) + end + + it 'returns the translation for the requested locale' do + expect(plucked_translations_field).to eq('english-text') + end + end + end + + context 'when the localized field is embedded and aliased' do + with_default_i18n_configs + + before do + s = Seo.new + I18n.locale = :en + s.description = 'english-text' + I18n.locale = :de + s.description = 'deutsch-text' + + parent.products.delete_all + parent.products.create!(name: 'ACME Tunnel Paint', seo: s) + end + + let(:plucked) do + parent.products.pluck('seo.description').first + end + + let(:plucked_unaliased) do + parent.products.pluck('seo.desc').first + end + + let(:plucked_translations) do + parent.products.pluck('seo.description_translations').first + end + + let(:plucked_translations_field) do + parent.products.pluck('seo.description_translations.en').first + end + + it 'returns the translation for the current locale' do + I18n.with_locale(:en) do + expect(plucked).to eq('english-text') + end + end + + it 'returns the translation for the current locale when unaliased' do + I18n.with_locale(:en) do + expect(plucked_unaliased).to eq('english-text') + end + end + + it 'returns the full _translation hash' do + expect(plucked_translations).to eq({ 'en' => 'english-text', 'de' => 'deutsch-text' }) + end + + it 'returns the translation for the requested locale' do + expect(plucked_translations_field).to eq('english-text') + end + end + + context 'when plucking an embedded field' do + let(:label) { Label.new(sales: '1E2') } + let!(:band) { Band.create!(label: label) } + + let(:plucked) { Band.where(_id: band.id).pluck('label.sales') } + + it 'demongoizes the field' do + expect(plucked).to eq([ BigDecimal('1E2') ]) + end + end + + context 'when plucking an embeds_many field' do + let(:label) { Label.new(sales: '1E2') } + let!(:band) { Band.create!(labels: [label]) } + + let(:plucked) { Band.where(_id: band.id).pluck('labels.sales') } + + it 'demongoizes the field' do + expect(plucked.first).to eq([ BigDecimal('1E2') ]) + end + end + + context 'when plucking a nonexistent embedded field' do + let(:label) { Label.new(sales: '1E2') } + let!(:band) { Band.create!(label: label) } + + let(:plucked) { Band.where(_id: band.id).pluck('label.qwerty') } + + it 'returns nil' do + expect(plucked.first).to eq(nil) + end + end + end + describe "#reset" do let(:person) do diff --git a/spec/support/models/company.rb b/spec/support/models/company.rb index 5838e28f07..6d08f25b9a 100644 --- a/spec/support/models/company.rb +++ b/spec/support/models/company.rb @@ -5,4 +5,6 @@ class Company include Mongoid::Document embeds_many :staffs + + has_many :products end diff --git a/spec/support/models/passport.rb b/spec/support/models/passport.rb index f1c11ae6d9..78c88f4995 100644 --- a/spec/support/models/passport.rb +++ b/spec/support/models/passport.rb @@ -8,6 +8,7 @@ class Passport field :country, type: String field :exp, as: :expiration_date, type: Date field :name, localize: true + field :bp, as: :birthplace, localize: true field :localized_translations, localize: true embedded_in :person, autobuild: true diff --git a/spec/support/models/product.rb b/spec/support/models/product.rb index 9d9c96576b..30f2808009 100644 --- a/spec/support/models/product.rb +++ b/spec/support/models/product.rb @@ -18,4 +18,6 @@ class Product validates :website, format: { with: URI.regexp, allow_blank: true } embeds_one :seo, as: :seo_tags, cascade_callbacks: true, autobuild: true + + belongs_to :company end diff --git a/spec/support/models/seo.rb b/spec/support/models/seo.rb index 51ba402cdc..e6d8b23540 100644 --- a/spec/support/models/seo.rb +++ b/spec/support/models/seo.rb @@ -5,6 +5,8 @@ class Seo include Mongoid::Document include Mongoid::Timestamps field :title, type: String + field :name, type: String, localize: true + field :desc, as: :description, type: String, localize: true embedded_in :seo_tags, polymorphic: true end