diff --git a/docs/reference/queries.txt b/docs/reference/queries.txt index 7cef79b94d..978c89d0c7 100644 --- a/docs/reference/queries.txt +++ b/docs/reference/queries.txt @@ -1522,6 +1522,41 @@ Mongoid also has some helpful methods on criteria. Band.all.pluck(:name, :likes) #=> [ ["Daft Punk", 342], ["Aphex Twin", 98], ["Ween", 227] ] + * - ``Criteria#pluck_each`` + + *This method returns an Enumerator for the results of ``pluck``. + A block may optionally be given, which will be called once for + each result.* + + *Similar to the ``each`` method, this method will use the + `MongoDB getMore command + `_ + to load results in batches. This is useful for working with + large query results.* + + *The method arguments and field normalization behavior are + otherwise identical to ``pluck``.* + + - + .. code-block:: ruby + + Band.all.pluck_each(:name) + #=> # + + Band.all.pluck_each(:name, 'address.city', :founded) do |name, city, founded| + puts "#{name} from #{city} started in #{founded}" + end + # => + # The Rolling Stones from London started in 1962 + # The Beatles from Liverpool started in 1960 + # The Monkees from Los Angeles started in 1966 + #=> [ ["Berry Gordy", "Tommy Mottola"], [], ["Quincy Jones"] ] + + # Accepts multiple field arguments, in which case + # the result will be returned as an Array of Arrays. + Band.all.pluck(:name, :likes) + #=> [ ["Daft Punk", 342], ["Aphex Twin", 98], ["Ween", 227] ] + * - ``Criteria#read`` *Sets the read preference for the criteria.* diff --git a/docs/release-notes/mongoid-9.0.txt b/docs/release-notes/mongoid-9.0.txt index 653eb4dc20..0b395ebe21 100644 --- a/docs/release-notes/mongoid-9.0.txt +++ b/docs/release-notes/mongoid-9.0.txt @@ -386,6 +386,35 @@ defaults to ``true``. When set to false, the older, inconsistent behavior is restored. +``Criteria#pluck_each`` Method Added +---------------------------------------- + +The newly introduced ``Criteria#pluck_each`` method returns +an Enumerator for the results of ``pluck``, or if a block is given, +calls the block once for each pluck result in a progressively-loaded +fashion. + +Previously, calling ``criteria.pluck(:name).each`` would load the +entire result set into Ruby's memory before iterating over the results. +In contrast, ``criteria.pluck_each(:name)`` uses the `MongoDB getMore command +`_ +to load results in batches, similar to how ``criteria.each`` behaves. +This is useful for working with large query results. + +The method arguments and behavior of ``pluck_each`` are otherwise +identical to ``pluck``. + +.. code-block:: ruby + + Band.all.pluck_each(:name, 'address.city', :founded) do |name, city, founded| + puts "#{name} from #{city} started in #{founded}" + end + # => + # The Rolling Stones from London started in 1962 + # The Beatles from Liverpool started in 1960 + # The Monkees from Los Angeles started in 1966 + + Support Field Aliases on Index Options -------------------------------------- diff --git a/lib/mongoid/contextual/memory.rb b/lib/mongoid/contextual/memory.rb index 6f9d2e8083..c9046b5968 100644 --- a/lib/mongoid/contextual/memory.rb +++ b/lib/mongoid/contextual/memory.rb @@ -263,6 +263,22 @@ def pluck(*fields) end end + # Iterate through plucked field values in memory. + # + # @example Iterate through the values for null context. + # context.pluck_each(:name) { |name| puts name } + # + # @param [ [ String | Symbol ]... ] *fields Field(s) to pluck. + # @param [ Proc ] &block The block to call once for each plucked + # result. + # + # @return [ Enumerator | Memory ] An enumerator, or the context + # if a block was given. + def pluck_each(*fields, &block) + enum = pluck(*fields).each(&block) + block_given? ? self : enum + end + # Pick the field values in memory. # # @example Get the values in memory. diff --git a/lib/mongoid/contextual/mongo.rb b/lib/mongoid/contextual/mongo.rb index 3fceba9dfc..b1f6ddfe57 100644 --- a/lib/mongoid/contextual/mongo.rb +++ b/lib/mongoid/contextual/mongo.rb @@ -8,6 +8,7 @@ require "mongoid/contextual/command" require "mongoid/contextual/geo_near" require "mongoid/contextual/map_reduce" +require "mongoid/contextual/mongo/pluck_enumerator" require "mongoid/association/eager_loadable" module Mongoid @@ -357,23 +358,27 @@ 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 + pluck_each(*fields).to_a + 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 + # Iterate through plucked field value(s) from the database + # for the context. Yields result values progressively as they are + # read from the database. The yielded results are normalized + # according to their Mongoid field types. + # + # @example Iterate through the plucked values from the database. + # context.pluck_each(:name) { |name| puts name } + # + # @param [ [ String | Symbol ]... ] *fields Field(s) to pluck, + # which may include nested fields using dot-notation. + # @param [ Proc ] &block The block to call once for each plucked + # result. + # + # @return [ Enumerator | Mongoid::Contextual::Mongo ] The enumerator, + # or the context if a block was given. + def pluck_each(*fields, &block) + enum = PluckEnumerator.new(klass, view, fields).each(&block) + block_given? ? self : enum end # Pick the single field values from the database. @@ -919,78 +924,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/contextual/mongo/pluck_enumerator.rb b/lib/mongoid/contextual/mongo/pluck_enumerator.rb new file mode 100644 index 0000000000..f65db34669 --- /dev/null +++ b/lib/mongoid/contextual/mongo/pluck_enumerator.rb @@ -0,0 +1,147 @@ +# frozen_string_literal: true + +module Mongoid + module Contextual + class Mongo + + # Utility class to add enumerable behavior for Criteria#pluck_each. + # + # @api private + class PluckEnumerator + include Enumerable + + # Create the new PluckEnumerator. + # + # @api private + # + # @example Initialize a PluckEnumerator. + # PluckEnumerator.new(klass, view, fields) + # + # @param [ Class ] klass The base of the binding. + # @param [ Mongo::Collection::View ] view The Mongo view context. + # @param [ String | Symbol ] *fields Field(s) to pluck, + # which may include nested fields using dot-notation. + def initialize(klass, view, fields) + @klass = klass + @view = view + @fields = fields + end + + # Iterate through plucked field value(s) from the database + # for the view context. Yields result values progressively as + # they are read from the database. The yielded results are + # normalized according to their Mongoid field types. + # + # @api private + # + # @example Iterate through the plucked values from the database. + # context.pluck_each(:name) { |name| puts name } + # + # @param [ Proc ] &block The block to call once for each plucked + # result. + # + # @return [ Enumerator | PluckEnumerator ] The enumerator, or + # self if a block was given. + def each(&block) + return to_enum unless block_given? + + @view.projection(normalized_field_names.index_with(true)).each do |doc| + yield_result(doc, &block) + end + + self + end + + private + + def database_field_names + @database_field_names ||= @fields.map {|f| @klass.database_field_name(f) } + end + + def normalized_field_names + @normalized_field_names ||= @fields.map {|f| @klass.cleanse_localized_field_names(f) } + end + + def yield_result(doc) + values = database_field_names.map {|n| extract_value(doc, n) } + yield(values.size == 1 ? values.first : values) + 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. + # + # @return [ Object ] The value for the given field name + # + # @api private + 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 + end + end + end +end diff --git a/lib/mongoid/contextual/none.rb b/lib/mongoid/contextual/none.rb index 9f1fbce8c2..d9a6a9f3ec 100644 --- a/lib/mongoid/contextual/none.rb +++ b/lib/mongoid/contextual/none.rb @@ -87,6 +87,22 @@ def pluck(*_fields) [] end + # Iterate through plucked field values in null context. + # + # @example Iterate through the values for null context. + # context.pluck_each(:name) { |name| puts name } + # + # @param [ [ String | Symbol ]... ] *_fields Field(s) to pluck. + # @param [ Proc ] &block The block which will not be called + # due to null context. + # + # @return [ Enumerator | None ] An enumerator, or the context + # if a block was given. + def pluck_each(*_fields, &block) + enum = pluck(*_fields).each(&block) + block_given? ? self : enum + end + # Pick the field values in null context. # # @example Get the value for null context. diff --git a/lib/mongoid/findable.rb b/lib/mongoid/findable.rb index fccfe2b1cd..01dd97746a 100644 --- a/lib/mongoid/findable.rb +++ b/lib/mongoid/findable.rb @@ -47,6 +47,7 @@ module Findable :none, :pick, :pluck, + :pluck_each, :read, :second, :second!, diff --git a/spec/mongoid/contextual/memory_spec.rb b/spec/mongoid/contextual/memory_spec.rb index d45038f502..bee2096539 100644 --- a/spec/mongoid/contextual/memory_spec.rb +++ b/spec/mongoid/contextual/memory_spec.rb @@ -1828,6 +1828,126 @@ end end + describe "#pluck_each" do + + let(:hobrecht) do + Address.new(street: "hobrecht", number: 213) + end + + let(:friedel) do + Address.new(street: "friedel", number: 11) + end + + let(:criteria) do + Address.all.tap do |crit| + crit.documents = [ hobrecht, friedel ] + end + end + + let(:context) do + described_class.new(criteria) + end + + context "when block given" do + + let!(:plucked_values) { [] } + + let!(:plucked) do + context.pluck_each(:street) { |value| plucked_values << value } + end + + it "returns the context" do + expect(plucked).to eq context + end + + it "yields values to the block" do + expect(plucked_values).to eq([ "hobrecht", "friedel" ]) + end + end + + context "when block not given" do + + let!(:plucked) do + context.pluck_each(:street) + end + + it "returns an Enumerator" do + expect(plucked).to be_an Enumerator + end + + it "can yield the values" do + expect(plucked.map { |value| value }).to eq([ "hobrecht", "friedel" ]) + end + end + + context "when plucking multiple fields" do + + let!(:plucked_values) { [] } + + let!(:plucked) do + context.pluck_each(:street, :number) { |value| plucked_values << value } + end + + it "returns the context" do + expect(plucked).to eq context + end + + it "yields values to the block" do + expect(plucked_values).to eq([ ["hobrecht", 213], ["friedel", 11] ]) + end + end + + context "when plucking a field that doesnt exist" do + + let!(:plucked_values) { [] } + + let!(:plucked) do + context.pluck_each(*pluck_args) { |value| plucked_values << value } + end + + context "when plucking one field" do + + let(:pluck_args) { [:foo] } + + it "returns the context" do + expect(plucked).to eq context + end + + it "yields the plucked values" do + expect(plucked_values).to eq([nil, nil]) + end + end + + context "when plucking multiple fields" do + + let(:pluck_args) { [:foo, :bar] } + + it "returns the context" do + expect(plucked).to eq context + end + + it "yields the plucked values" do + expect(plucked_values).to eq([[nil, nil], [nil, nil]]) + end + end + end + + context 'when there is a collation on the criteria' do + + let(:criteria) do + Address.all.tap do |crit| + crit.documents = [ hobrecht, friedel ] + end.collation(locale: 'en_US', strength: 2) + end + + it "raises an exception" do + expect { + context.pluck(:foo, :bar) + }.to raise_exception(Mongoid::Errors::InMemoryCollationNotSupported) + end + end + end + describe "#pick" do let(:depeche) do diff --git a/spec/mongoid/contextual/none_spec.rb b/spec/mongoid/contextual/none_spec.rb index fa26045cb6..9ca82b750d 100644 --- a/spec/mongoid/contextual/none_spec.rb +++ b/spec/mongoid/contextual/none_spec.rb @@ -69,6 +69,41 @@ end end + describe "#pluck_each" do + + context "when block given" do + + let!(:plucked_values) { [] } + + let!(:plucked) do + context.pluck_each(:street) { |value| plucked_values << value } + end + + it "returns the context" do + expect(plucked).to eq context + end + + it "yields no values to the block" do + expect(plucked_values).to eq([]) + end + end + + context "when block not given" do + + let!(:plucked) do + context.pluck_each(:street) + end + + it "returns an Enumerator" do + expect(plucked).to be_an Enumerator + end + + it "does not yield any values" do + expect(plucked.map { |value| value }).to eq([]) + end + end + end + describe "#pick" do it "returns an empty array" do expect(context.pick(:id)).to eq(nil) diff --git a/spec/mongoid/criteria_spec.rb b/spec/mongoid/criteria_spec.rb index 65d1ca8092..c1de0fe7af 100644 --- a/spec/mongoid/criteria_spec.rb +++ b/spec/mongoid/criteria_spec.rb @@ -2094,6 +2094,409 @@ end end + describe "#pluck_each" do + + let!(:depeche) do + Band.create!(name: "Depeche Mode", likes: 3) + end + + let!(:tool) do + Band.create!(name: "Tool", likes: 3) + end + + let!(:photek) do + Band.create!(name: "Photek", likes: 1) + end + + let(:maniacs) do + Band.create!(name: "10,000 Maniacs", likes: 1, sales: "1E2") + end + + context "when block given" do + + let!(:plucked_values) { [] } + + let!(:plucked) do + Band.pluck_each(:name) { |value| plucked_values << value } + end + + it "returns the context" do + expect(plucked).to be_a Mongoid::Contextual::Mongo + end + + it "yields values to the block" do + expect(plucked_values).to eq([ "Depeche Mode", "Tool", "Photek" ]) + end + end + + context "when block not given" do + + let!(:plucked) do + Band.pluck_each(:name) + end + + it "returns an Enumerator" do + expect(plucked).to be_an Enumerator + end + + it "can yield the values" do + expect(plucked.map { |value| value }).to eq([ "Depeche Mode", "Tool", "Photek" ]) + end + end + + context "when the field is aliased" do + + let!(:expensive) do + Product.create!(price: 100000) + end + + let!(:cheap) do + Product.create!(price: 1) + end + + context "when using alias_attribute" do + + let!(:plucked) { [] } + let!(:pluck_each) { Product.pluck_each(:price) { |v| plucked << v } } + + it "uses the aliases" do + expect(plucked).to eq([ 100000, 1 ]) + end + end + end + + context "when the criteria matches" do + + context "when there are no duplicate values" do + + let(:criteria) do + Band.where(:name.exists => true) + end + + let!(:plucked) { [] } + let!(:pluck_each) { criteria.pluck_each(:name) { |v| plucked << v } } + + it "returns the values" do + expect(plucked).to contain_exactly("Depeche Mode", "Tool", "Photek") + end + + context "when subsequently executing the criteria without a pluck" do + it "does not limit the fields" do + expect(criteria.first.likes).to eq(3) + end + end + + context 'when the field is a subdocument' do + + let(:criteria) do + Band.where(name: 'FKA Twigs') + end + + context 'when a top-level field and a subdocument field are plucked' do + before do + Band.create!(name: 'FKA Twigs') + Band.create!(name: 'FKA Twigs', records: [ Record.new(name: 'LP1') ]) + end + + let(:expected) do + [ + ["FKA Twigs", nil], + ['FKA Twigs', ["LP1"]] + ] + end + + it 'returns the list of top-level field and subdocument values' do + plucked = [] + criteria.pluck_each(:name, 'records.name') { |v| plucked << v } + expect(plucked).to eq(expected) + end + end + + context 'when only a subdocument field is plucked' do + + before do + Band.create!(name: 'FKA Twigs') + Band.create!(name: 'FKA Twigs', records: [ Record.new(name: 'LP1') ]) + end + + let(:expected) do + [ + nil, + ["LP1"] + ] + end + + it 'returns the list of subdocument values' do + plucked = [] + criteria.pluck_each('records.name') { |v| plucked << v } + expect(plucked).to eq(expected) + end + end + end + end + + context "when plucking multi-fields" do + + let!(:plucked) { [] } + let!(:pluck_each) { Band.where(:name.exists => true).pluck_each(:name, :likes) { |v| plucked << v } } + + it "returns the values" do + expect(plucked).to contain_exactly(["Depeche Mode", 3], ["Tool", 3], ["Photek", 1]) + end + end + + context "when there are duplicate values" do + + let!(:plucked) { [] } + let!(:pluck_each) { Band.where(:name.exists => true).pluck_each(:likes) { |v| plucked << v } } + + it "returns the duplicates" do + expect(plucked).to contain_exactly(3, 3, 1) + end + end + end + + context "when the criteria does not match" do + + let!(:plucked) { [] } + let!(:pluck_each) { Band.where(name: "New Order").pluck_each(:_id) { |v| plucked << v } } + + it "returns an empty array" do + expect(plucked).to be_empty + end + end + + context "when plucking an aliased field" do + + let!(:plucked) { [] } + let!(:pluck_each) { Band.all.pluck_each(:id) { |v| plucked << v } } + + it "returns the field values" do + expect(plucked).to eq([ depeche.id, tool.id, photek.id ]) + end + end + + context "when plucking existent and non-existent fields" do + + let!(:plucked) { [] } + let!(:pluck_each) { Band.all.pluck_each(:id, :fooz) { |v| plucked << v } } + + it "returns nil for the field that doesnt exist" do + expect(plucked).to eq([[depeche.id, nil], [tool.id, nil], [photek.id, nil] ]) + end + end + + context "when plucking a field that doesnt exist" do + + context "when pluck one field" do + + let!(:plucked) { [] } + let!(:pluck_each) { Band.all.pluck_each(:foo) { |v| plucked << v } } + + it "returns a array with nil values" do + expect(plucked).to eq([nil, nil, nil]) + end + end + + context "when pluck multiple fields" do + + let!(:plucked) { [] } + let!(:pluck_each) { Band.all.pluck_each(:foo, :bar) { |v| plucked << v } } + + it "returns a nil arrays" do + expect(plucked).to eq([[nil, nil], [nil, nil], [nil, nil]]) + end + end + end + + context 'when plucking a localized field' do + + before do + I18n.locale = :en + d = Dictionary.create!(description: 'english-text') + I18n.locale = :de + d.description = 'deutsch-text' + d.save! + end + + after do + I18n.locale = :en + end + + context 'when plucking the entire field' do + + let!(:plucked) { [] } + let!(:plucked_translations) { [] } + let!(:plucked_translations_both) { [] } + + let!(:pluck_each) do + Dictionary.all.pluck_each(:description) { |v| plucked << v } + end + + let!(:pluck_each_translations) do + Dictionary.all.pluck_each(:description_translations) { |v| plucked_translations << v } + end + + let!(:pluck_each_translations_both) do + Dictionary.all.pluck_each(:description_translations, :description) { |v| plucked_translations_both << v } + 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 + Dictionary.all.pluck_each(:'description.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 + Dictionary.all.pluck_each(:'description_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 + + around(:all) do |example| + prev_fallbacks = I18n.fallbacks.dup + I18n.fallbacks[:he] = [ :en ] + example.run + I18n.fallbacks = prev_fallbacks + end + + let(:plucked) do + Dictionary.all.pluck_each(:description).first + end + + it "correctly uses the fallback" do + I18n.locale = :en + d = Dictionary.create!(description: 'english-text') + I18n.locale = :he + plucked.should == "english-text" + end + end + + context "when the localized field is embedded" do + before do + p = Passport.new + I18n.locale = :en + p.name = "Neil" + I18n.locale = :he + p.name = "Nissim" + + Person.create!(passport: p, employer_id: 12345) + end + + let(:plucked) do + Person.where(employer_id: 12345).pluck_each("pass.name").first + end + + let(:plucked_translations) do + Person.where(employer_id: 12345).pluck_each("pass.name_translations").first + end + + let(:plucked_translations_field) do + Person.where(employer_id: 12345).pluck_each("pass.name_translations.en").first + end + + it "returns the translation for the current locale" do + expect(plucked).to eq("Nissim") + end + + it "returns the full _translation hash" do + expect(plucked_translations).to eq({ "en" => "Neil", "he" => "Nissim" }) + end + + it "returns the translation for the requested locale" do + expect(plucked_translations_field).to eq("Neil") + end + end + end + + context 'when plucking a field to be demongoized' do + + let(:plucked) do + Band.where(name: maniacs.name).pluck_each(:sales) + end + + context 'when value is stored as string' do + config_override :map_big_decimal_to_decimal128, false + + it "demongoizes the field" do + expect(plucked.first).to be_a(BigDecimal) + expect(plucked.first).to eq(BigDecimal("1E2")) + end + end + + context 'when value is stored as decimal128' do + config_override :map_big_decimal_to_decimal128, true + + it "demongoizes the field" do + expect(plucked.first).to be_a(BigDecimal) + expect(plucked.first).to eq(BigDecimal("1E2")) + end + end + end + + context "when plucking an embedded field" do + let(:label) { Label.new(sales: "1E2") } + let!(:band) { Band.create!(label: label) } + + let!(:plucked) { [] } + let!(:pluck_each) { Band.where(_id: band.id).pluck_each("label.sales") { |v| plucked << v } } + + it "demongoizes the field" do + expect(plucked.first).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) { [] } + let!(:pluck_each) { Band.where(_id: band.id).pluck_each("labels.sales") { |v| plucked << v } } + + 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) { [] } + let!(:pluck_each) { Band.where(_id: band.id).pluck_each("label.qwerty") { |v| plucked << v } } + + it "returns nil" do + expect(plucked.first).to eq(nil) + end + end + end + describe "#pick" do let!(:depeche) do