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