mongoid 9.0.8 → 9.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '032258516f15736f4121de82ee4cdcdb625515f653f85a66b0df613665236e4d'
4
- data.tar.gz: 443ec24d9a54bb10c87023cd6304081aa48bf92bf5654044cc935f79ca954b9c
3
+ metadata.gz: c5dca0df68ed7086b6c641b1021e556812e23872d5bd52a9777093e2d8cf1e64
4
+ data.tar.gz: 24630d19104369a922b3a5a152012ae8eba3719576ef3c9e5f97304b6896a1c4
5
5
  SHA512:
6
- metadata.gz: 3c875bf631d019da208fa59221a74e3468931a802664e43ce2e8ed669e990722a499d8976a8acafb8078dbeff8c02a76df96b9d76f8e33c0c979658f40748fd9
7
- data.tar.gz: 46943ea7b081db4784159bacc2056b75b6511f8c8892910dc2c2bfc7ee1229a6ddf0d9141f52137a12a04eea841c24e16b4eae7fb8fc15bd26e2dec7d0273ecc
6
+ metadata.gz: 03013a0c28cda90cfc3b99c26d22e7672e8ce1c6449e599d7ae39a42d19ba742f37dea7510411ca004c7d5d5f25af80e9230bac63fef8ce43515711d7f669343
7
+ data.tar.gz: 8bd5b0c4faa4f5f16fcc56ff612bae5a14fd40fa01513fda545469d225c6cd4909022c066719950147dc116dc1a7bdb79b69d48ad40454aabdb45481994dd69b
@@ -14,6 +14,7 @@ module Mongoid
14
14
  # the array of child documents.
15
15
  class Proxy < Association::Many
16
16
  include Batchable
17
+ extend Forwardable
17
18
 
18
19
  # Class-level methods for the Proxy class.
19
20
  module ClassMethods
@@ -54,6 +55,8 @@ module Mongoid
54
55
 
55
56
  extend ClassMethods
56
57
 
58
+ def_delegators :criteria, :find, :pluck
59
+
57
60
  # Instantiate a new embeds_many association.
58
61
  #
59
62
  # @example Create the new association.
@@ -312,35 +315,6 @@ module Mongoid
312
315
  end
313
316
  end
314
317
 
315
- # Finds a document in this association through several different
316
- # methods.
317
- #
318
- # This method delegates to +Mongoid::Criteria#find+. If this method is
319
- # not given a block, it returns one or many documents for the provided
320
- # _id values.
321
- #
322
- # If this method is given a block, it returns the first document
323
- # of those found by the current Criteria object for which the block
324
- # returns a truthy value.
325
- #
326
- # @example Find a document by its id.
327
- # person.addresses.find(BSON::ObjectId.new)
328
- #
329
- # @example Find documents for multiple ids.
330
- # person.addresses.find([ BSON::ObjectId.new, BSON::ObjectId.new ])
331
- #
332
- # @example Finds the first matching document using a block.
333
- # person.addresses.find { |addr| addr.state == 'CA' }
334
- #
335
- # @param [ Object... ] *args Various arguments.
336
- # @param &block Optional block to pass.
337
- # @yield [ Object ] Yields each enumerable element to the block.
338
- #
339
- # @return [ Document | Array<Document> | nil ] A document or matching documents.
340
- def find(...)
341
- criteria.find(...)
342
- end
343
-
344
318
  # Get all the documents in the association that are loaded into memory.
345
319
  #
346
320
  # @example Get the in memory documents.
@@ -53,8 +53,6 @@ module Mongoid
53
53
  # @param [ Document... ] *args Any number of documents.
54
54
  #
55
55
  # @return [ Array<Document> ] The loaded docs.
56
- #
57
- # rubocop:disable Metrics/AbcSize
58
56
  def <<(*args)
59
57
  docs = args.flatten
60
58
  return concat(docs) if docs.size > 1
@@ -89,7 +87,6 @@ module Mongoid
89
87
  end
90
88
  unsynced(_base, foreign_key) and self
91
89
  end
92
- # rubocop:enable Metrics/AbcSize
93
90
 
94
91
  alias push <<
95
92
 
@@ -360,8 +357,6 @@ module Mongoid
360
357
  # in bulk
361
358
  # @param [ Array ] inserts the list of Hashes of attributes that will
362
359
  # be inserted (corresponding to the ``docs`` list)
363
- #
364
- # rubocop:disable Metrics/AbcSize
365
360
  def append_document(doc, ids, docs, inserts)
366
361
  return unless doc
367
362
 
@@ -379,7 +374,6 @@ module Mongoid
379
374
  unsynced(_base, foreign_key)
380
375
  end
381
376
  end
382
- # rubocop:enable Metrics/AbcSize
383
377
  end
384
378
  end
385
379
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
  # rubocop:todo all
3
3
 
4
+ require 'mongoid/pluckable'
5
+
4
6
  module Mongoid
5
7
  module Association
6
8
  module Referenced
@@ -12,6 +14,7 @@ module Mongoid
12
14
  class Enumerable
13
15
  extend Forwardable
14
16
  include ::Enumerable
17
+ include Pluckable
15
18
 
16
19
  # The three main instance variables are collections of documents.
17
20
  #
@@ -374,6 +377,43 @@ module Mongoid
374
377
  @_added, @_loaded, @_unloaded, @executed = data
375
378
  end
376
379
 
380
+ # Plucks the given field names from the documents in the target.
381
+ # If the collection has been loaded, it plucks from the loaded
382
+ # documents; otherwise, it plucks from the unloaded criteria.
383
+ # Regardless, it also plucks from any added documents.
384
+ #
385
+ # @param [ Symbol... ] *fields The field names to pluck.
386
+ #
387
+ # @return [ Array | Array<Array> ] The array of field values. If
388
+ # multiple fields are given, an array of arrays is returned.
389
+ def pluck(*keys)
390
+ [].tap do |results|
391
+ if _loaded? || _added.any?
392
+ klass = @_association.klass
393
+ prepared = prepare_pluck(keys, document_class: klass)
394
+ end
395
+
396
+ if _loaded?
397
+ docs = _loaded.values.map { |v| BSON::Document.new(v.attributes) }
398
+ results.concat pluck_from_documents(docs, prepared[:field_names], document_class: klass)
399
+ elsif _unloaded
400
+ criteria = if _added.any?
401
+ ids_to_exclude = _added.keys
402
+ _unloaded.not(:_id.in => ids_to_exclude)
403
+ else
404
+ _unloaded
405
+ end
406
+
407
+ results.concat criteria.pluck(*keys)
408
+ end
409
+
410
+ if _added.any?
411
+ docs = _added.values.map { |v| BSON::Document.new(v.attributes) }
412
+ results.concat pluck_from_documents(docs, prepared[:field_names], document_class: klass)
413
+ end
414
+ end
415
+ end
416
+
377
417
  # Reset the enumerable back to its persisted state.
378
418
  #
379
419
  # @example Reset the enumerable.
@@ -33,7 +33,7 @@ module Mongoid
33
33
 
34
34
  extend ClassMethods
35
35
 
36
- def_delegator :criteria, :count
36
+ def_delegators :criteria, :count
37
37
  def_delegators :_target, :first, :in_memory, :last, :reset, :uniq
38
38
 
39
39
  # Instantiate a new references_many association. Will set the foreign key
@@ -281,6 +281,22 @@ module Mongoid
281
281
 
282
282
  alias nullify_all nullify
283
283
 
284
+ # Plucks the given field names from the documents in the
285
+ # association. It is safe to use whether the association is
286
+ # loaded or not, and whether there are unsaved documents in the
287
+ # association or not.
288
+ #
289
+ # @example Pluck the titles of all posts.
290
+ # person.posts.pluck(:title)
291
+ #
292
+ # @param [ Symbol... ] *fields The field names to pluck.
293
+ #
294
+ # @return [ Array | Array<Array> ] The array of field values. If
295
+ # multiple fields are given, an array of arrays is returned.
296
+ def pluck(*fields)
297
+ _target.pluck(*fields)
298
+ end
299
+
284
300
  # Clear the association. Will delete the documents from the db if they are
285
301
  # already persisted.
286
302
  #
@@ -175,7 +175,7 @@ module Mongoid
175
175
  localized = fields[field_name].try(:localized?)
176
176
  attributes_before_type_cast[name.to_s] = value
177
177
  typed_value = typed_value_for(field_name, value)
178
- unless attributes[field_name] == typed_value || attribute_changed?(field_name)
178
+ unless attribute_will_not_change?(field_name, typed_value) || attribute_changed?(field_name)
179
179
  attribute_will_change!(field_name)
180
180
  end
181
181
  if localized
@@ -366,5 +366,23 @@ module Mongoid
366
366
  end
367
367
  value.present?
368
368
  end
369
+
370
+ # If `value` is a `BSON::Decimal128`, convert it to a `BigDecimal` for
371
+ # comparison purposes. This is necessary because `BSON::Decimal128` does
372
+ # not implement `#==` in a way that is compatible with `BigDecimal`.
373
+ def normalize_value(value)
374
+ value.is_a?(BSON::Decimal128) ? BigDecimal(value.to_s) : value
375
+ end
376
+
377
+ # Determine if the attribute will not change, by comparing the current
378
+ # value with the new value. The values are normalized to account for
379
+ # types that do not implement `#==` in a way that is compatible with
380
+ # each other, such as `BSON::Decimal128` and `BigDecimal`.
381
+ def attribute_will_not_change?(field_name, typed_value)
382
+ normalized_attribute = normalize_value(attributes[field_name])
383
+ normalized_typed_value = normalize_value(typed_value)
384
+
385
+ normalized_attribute == normalized_typed_value
386
+ end
369
387
  end
370
388
  end
@@ -2,6 +2,7 @@
2
2
  # rubocop:todo all
3
3
 
4
4
  require 'mongoid/atomic_update_preparer'
5
+ require 'mongoid/pluckable'
5
6
  require "mongoid/contextual/mongo/documents_loader"
6
7
  require "mongoid/contextual/atomic"
7
8
  require "mongoid/contextual/aggregable/mongo"
@@ -22,6 +23,7 @@ module Mongoid
22
23
  include Atomic
23
24
  include Association::EagerLoadable
24
25
  include Queryable
26
+ include Pluckable
25
27
 
26
28
  # Options constant.
27
29
  OPTIONS = [ :hint,
@@ -331,23 +333,12 @@ module Mongoid
331
333
  # in the array will be a single value. Otherwise, each
332
334
  # result in the array will be an array of values.
333
335
  def pluck(*fields)
334
- # Multiple fields can map to the same field name. For example, plucking
335
- # a field and its _translations field map to the same field in the database.
336
- # because of this, we need to keep track of the fields requested.
337
- normalized_field_names = []
338
- normalized_select = fields.inject({}) do |hash, f|
339
- db_fn = klass.database_field_name(f)
340
- normalized_field_names.push(db_fn)
341
- hash[klass.cleanse_localized_field_names(f)] = true
342
- hash
343
- end
344
-
345
- view.projection(normalized_select).reduce([]) do |plucked, doc|
346
- values = normalized_field_names.map do |n|
347
- extract_value(doc, n)
348
- end
349
- plucked << (values.size == 1 ? values.first : values)
350
- end
336
+ # Multiple fields can map to the same field name. For example,
337
+ # plucking a field and its _translations field map to the same
338
+ # field in the database. because of this, we need to prepare the
339
+ # projection specifically.
340
+ prep = prepare_pluck(fields, prepare_projection: true)
341
+ pluck_from_documents(view.projection(prep[:projection]), prep[:field_names])
351
342
  end
352
343
 
353
344
  # Pick the single field values from the database.
@@ -893,78 +884,6 @@ module Mongoid
893
884
  collection.write_concern.nil? || collection.write_concern.acknowledged?
894
885
  end
895
886
 
896
- # Fetch the element from the given hash and demongoize it using the
897
- # given field. If the obj is an array, map over it and call this method
898
- # on all of its elements.
899
- #
900
- # @param [ Hash | Array<Hash> ] obj The hash or array of hashes to fetch from.
901
- # @param [ String ] meth The key to fetch from the hash.
902
- # @param [ Field ] field The field to use for demongoization.
903
- #
904
- # @return [ Object ] The demongoized value.
905
- #
906
- # @api private
907
- def fetch_and_demongoize(obj, meth, field)
908
- if obj.is_a?(Array)
909
- obj.map { |doc| fetch_and_demongoize(doc, meth, field) }
910
- else
911
- res = obj.try(:fetch, meth, nil)
912
- field ? field.demongoize(res) : res.class.demongoize(res)
913
- end
914
- end
915
-
916
- # Extracts the value for the given field name from the given attribute
917
- # hash.
918
- #
919
- # @param [ Hash ] attrs The attributes hash.
920
- # @param [ String ] field_name The name of the field to extract.
921
- #
922
- # @param [ Object ] The value for the given field name
923
- def extract_value(attrs, field_name)
924
- i = 1
925
- num_meths = field_name.count('.') + 1
926
- curr = attrs.dup
927
-
928
- klass.traverse_association_tree(field_name) do |meth, obj, is_field|
929
- field = obj if is_field
930
- is_translation = false
931
- # If no association or field was found, check if the meth is an
932
- # _translations field.
933
- if obj.nil? & tr = meth.match(/(.*)_translations\z/)&.captures&.first
934
- is_translation = true
935
- meth = tr
936
- end
937
-
938
- # 1. If curr is an array fetch from all elements in the array.
939
- # 2. If the field is localized, and is not an _translations field
940
- # (_translations fields don't show up in the fields hash).
941
- # - If this is the end of the methods, return the translation for
942
- # the current locale.
943
- # - Otherwise, return the whole translations hash so the next method
944
- # can select the language it wants.
945
- # 3. If the meth is an _translations field, do not demongoize the
946
- # value so the full hash is returned.
947
- # 4. Otherwise, fetch and demongoize the value for the key meth.
948
- curr = if curr.is_a? Array
949
- res = fetch_and_demongoize(curr, meth, field)
950
- res.empty? ? nil : res
951
- elsif !is_translation && field&.localized?
952
- if i < num_meths
953
- curr.try(:fetch, meth, nil)
954
- else
955
- fetch_and_demongoize(curr, meth, field)
956
- end
957
- elsif is_translation
958
- curr.try(:fetch, meth, nil)
959
- else
960
- fetch_and_demongoize(curr, meth, field)
961
- end
962
-
963
- i += 1
964
- end
965
- curr
966
- end
967
-
968
887
  # Recursively demongoize the given value. This method recursively traverses
969
888
  # the class tree to find the correct field to use to demongoize the value.
970
889
  #
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongoid
4
+ # Provides shared behavior for any document with "pluck" functionality.
5
+ #
6
+ # @api private
7
+ module Pluckable
8
+ extend ActiveSupport::Concern
9
+
10
+ private
11
+
12
+ # Prepares the field names for plucking by normalizing them to their
13
+ # database field names. Also prepares a projection hash if requested.
14
+ def prepare_pluck(field_names, document_class: klass, prepare_projection: false)
15
+ normalized_field_names = []
16
+ projection = {}
17
+
18
+ field_names.each do |f|
19
+ db_fn = document_class.database_field_name(f)
20
+ normalized_field_names.push(db_fn)
21
+
22
+ next unless prepare_projection
23
+
24
+ cleaned_name = document_class.cleanse_localized_field_names(f)
25
+ canonical_name = document_class.database_field_name(cleaned_name)
26
+ projection[canonical_name] = true
27
+ end
28
+
29
+ { field_names: normalized_field_names, projection: projection }
30
+ end
31
+
32
+ # Plucks the given field names from the given documents.
33
+ def pluck_from_documents(documents, field_names, document_class: klass)
34
+ documents.reduce([]) do |plucked, doc|
35
+ values = field_names.map { |name| extract_value(doc, name.to_s, document_class) }
36
+ plucked << ((values.size == 1) ? values.first : values)
37
+ end
38
+ end
39
+
40
+ # Fetch the element from the given hash and demongoize it using the
41
+ # given field. If the obj is an array, map over it and call this method
42
+ # on all of its elements.
43
+ #
44
+ # @param [ Hash | Array<Hash> ] obj The hash or array of hashes to fetch from.
45
+ # @param [ String ] key The key to fetch from the hash.
46
+ # @param [ Field ] field The field to use for demongoization.
47
+ #
48
+ # @return [ Object ] The demongoized value.
49
+ def fetch_and_demongoize(obj, key, field)
50
+ if obj.is_a?(Array)
51
+ obj.map { |doc| fetch_and_demongoize(doc, key, field) }
52
+ else
53
+ value = obj.try(:fetch, key, nil)
54
+ field ? field.demongoize(value) : value.class.demongoize(value)
55
+ end
56
+ end
57
+
58
+ # Extracts the value for the given field name from the given attribute
59
+ # hash.
60
+ #
61
+ # @param [ Hash ] attrs The attributes hash.
62
+ # @param [ String ] field_name The name of the field to extract.
63
+ #
64
+ # @return [ Object ] The value for the given field name
65
+ def extract_value(attrs, field_name, document_class)
66
+ i = 1
67
+ num_meths = field_name.count('.') + 1
68
+ curr = attrs.dup
69
+
70
+ document_class.traverse_association_tree(field_name) do |meth, obj, is_field|
71
+ field = obj if is_field
72
+
73
+ # use the correct document class to check for localized fields on
74
+ # embedded documents.
75
+ document_class = obj.klass if obj.respond_to?(:klass)
76
+
77
+ is_translation = false
78
+ # If no association or field was found, check if the meth is an
79
+ # _translations field.
80
+ if obj.nil? && (tr = meth.match(/(.*)_translations\z/)&.captures&.first)
81
+ is_translation = true
82
+ meth = document_class.database_field_name(tr)
83
+ end
84
+
85
+ curr = descend(i, curr, meth, field, num_meths, is_translation)
86
+
87
+ i += 1
88
+ end
89
+ curr
90
+ end
91
+
92
+ # Descend one level in the attribute hash.
93
+ #
94
+ # @param [ Integer ] part The current part index.
95
+ # @param [ Hash | Array<Hash> ] current The current level in the attribute hash.
96
+ # @param [ String ] method_name The method name to descend to.
97
+ # @param [ Field|nil ] field The field to use for demongoization.
98
+ # @param [ Boolean ] is_translation Whether the method is an _translations field.
99
+ # @param [ Integer ] part_count The total number of parts in the field name.
100
+ #
101
+ # @return [ Object ] The value at the next level.
102
+ #
103
+ # rubocop:disable Metrics/ParameterLists
104
+ def descend(part, current, method_name, field, part_count, is_translation)
105
+ # 1. If curr is an array fetch from all elements in the array.
106
+ # 2. If the field is localized, and is not an _translations field
107
+ # (_translations fields don't show up in the fields hash).
108
+ # - If this is the end of the methods, return the translation for
109
+ # the current locale.
110
+ # - Otherwise, return the whole translations hash so the next method
111
+ # can select the language it wants.
112
+ # 3. If the meth is an _translations field, do not demongoize the
113
+ # value so the full hash is returned.
114
+ # 4. Otherwise, fetch and demongoize the value for the key meth.
115
+ if current.is_a? Array
116
+ res = fetch_and_demongoize(current, method_name, field)
117
+ res.empty? ? nil : res
118
+ elsif !is_translation && field&.localized?
119
+ if part < part_count
120
+ current.try(:fetch, method_name, nil)
121
+ else
122
+ fetch_and_demongoize(current, method_name, field)
123
+ end
124
+ elsif is_translation
125
+ current.try(:fetch, method_name, nil)
126
+ else
127
+ fetch_and_demongoize(current, method_name, field)
128
+ end
129
+ end
130
+ # rubocop:enable Metrics/ParameterLists
131
+ end
132
+ end
@@ -131,7 +131,6 @@ module Mongoid
131
131
  # @param [ String ] value The discriminator key to set.
132
132
  #
133
133
  # @api private
134
- # rubocop:disable Metrics/AbcSize
135
134
  def discriminator_key=(value)
136
135
  raise Errors::InvalidDiscriminatorKeyTarget.new(self, superclass) if hereditary?
137
136
 
@@ -159,7 +158,6 @@ module Mongoid
159
158
  default_proc = -> { self.class.discriminator_value }
160
159
  field(discriminator_key, default: default_proc, type: String)
161
160
  end
162
- # rubocop:enable Metrics/AbcSize
163
161
 
164
162
  # Returns the discriminator key.
165
163
  #
@@ -5,5 +5,5 @@ module Mongoid
5
5
  #
6
6
  # Note that this file is automatically updated via `rake candidate:create`.
7
7
  # Manual changes to this file will be overwritten by that rake task.
8
- VERSION = '9.0.8'
8
+ VERSION = '9.0.9'
9
9
  end
@@ -1819,6 +1819,400 @@ describe Mongoid::Association::Referenced::HasMany::Enumerable do
1819
1819
  end
1820
1820
  end
1821
1821
 
1822
+ describe '#pluck' do
1823
+ let(:person) do
1824
+ Person.create!
1825
+ end
1826
+
1827
+ let!(:post) do
1828
+ Post.create!(person_id: person.id, title: 'Test Title')
1829
+ end
1830
+
1831
+ let(:base) { Person }
1832
+ let(:association) { Person.relations[:posts] }
1833
+
1834
+ let(:criteria) do
1835
+ Post.where(person_id: person.id)
1836
+ end
1837
+
1838
+ context 'when the enumerable is not loaded' do
1839
+ let!(:enumerable) do
1840
+ described_class.new(criteria, base, association)
1841
+ end
1842
+
1843
+ context 'when the criteria is present' do
1844
+ it 'delegates to the criteria pluck method' do
1845
+ result = enumerable.pluck(:title)
1846
+ expect(result).to eq(['Test Title'])
1847
+ end
1848
+
1849
+ context 'when added docs are present' do
1850
+ it 'combines the results from the criteria and the added docs' do
1851
+ added_post = Post.new(title: 'Added Title', person_id: person.id)
1852
+ enumerable << added_post
1853
+
1854
+ expect(criteria).to receive(:pluck).with(:title).and_return(['Test Title'])
1855
+ result = enumerable.pluck(:title)
1856
+ expect(result).to eq(['Test Title', 'Added Title'])
1857
+ end
1858
+ end
1859
+ end
1860
+
1861
+ context 'when the criteria is not present' do
1862
+ let(:enumerable) { described_class.new([], base, association) }
1863
+
1864
+ it 'returns nothing' do
1865
+ result = enumerable.pluck(:title)
1866
+ expect(result).to eq([])
1867
+ end
1868
+
1869
+ context 'when added docs are present' do
1870
+ it 'returns the values from the added docs' do
1871
+ added_post = Post.new(title: 'Added Title', person_id: person.id)
1872
+ enumerable << added_post
1873
+
1874
+ result = enumerable.pluck(:title)
1875
+ expect(result).to eq(['Added Title'])
1876
+ end
1877
+ end
1878
+ end
1879
+ end
1880
+
1881
+ context 'when the enumerable is loaded' do
1882
+ let(:enumerable) { described_class.new([post], base, association) }
1883
+
1884
+ it 'returns the values from the loaded documents' do
1885
+ result = enumerable.pluck(:title)
1886
+ expect(result).to eq(['Test Title'])
1887
+ end
1888
+
1889
+ context 'when added docs are present' do
1890
+ it 'returns the values from both loaded and added docs' do
1891
+ added_post = Post.new(title: 'Added Title', person_id: person.id)
1892
+ enumerable << added_post
1893
+
1894
+ result = enumerable.pluck(:title)
1895
+ expect(result).to eq(['Test Title', 'Added Title'])
1896
+ end
1897
+ end
1898
+ end
1899
+ end
1900
+
1901
+ describe '#pluck with aliases' do
1902
+ let!(:parent) do
1903
+ Company.create!
1904
+ end
1905
+
1906
+ context 'when the field is aliased' do
1907
+ let!(:expensive) do
1908
+ parent.products.create!(price: 100000)
1909
+ end
1910
+
1911
+ let!(:cheap) do
1912
+ parent.products.create!(price: 1)
1913
+ end
1914
+
1915
+ context 'when using alias_attribute' do
1916
+
1917
+ let(:plucked) do
1918
+ parent.products.pluck(:price)
1919
+ end
1920
+
1921
+ it 'uses the aliases' do
1922
+ expect(plucked).to eq([ 100000, 1 ])
1923
+ end
1924
+ end
1925
+ end
1926
+
1927
+ context 'when plucking a localized field' do
1928
+ with_default_i18n_configs
1929
+
1930
+ before do
1931
+ I18n.locale = :en
1932
+ p = parent.products.create!(name: 'english-text')
1933
+ I18n.locale = :de
1934
+ p.name = 'deutsch-text'
1935
+ p.save!
1936
+ end
1937
+
1938
+ context 'when plucking the entire field' do
1939
+ let(:plucked) do
1940
+ parent.products.all.pluck(:name)
1941
+ end
1942
+
1943
+ let(:plucked_translations) do
1944
+ parent.products.all.pluck(:name_translations)
1945
+ end
1946
+
1947
+ let(:plucked_translations_both) do
1948
+ parent.products.all.pluck(:name_translations, :name)
1949
+ end
1950
+
1951
+ it 'returns the demongoized translations' do
1952
+ expect(plucked.first).to eq('deutsch-text')
1953
+ end
1954
+
1955
+ it 'returns the full translations hash to _translations' do
1956
+ expect(plucked_translations.first).to eq({'de'=>'deutsch-text', 'en'=>'english-text'})
1957
+ end
1958
+
1959
+ it 'returns both' do
1960
+ expect(plucked_translations_both.first).to eq([{'de'=>'deutsch-text', 'en'=>'english-text'}, 'deutsch-text'])
1961
+ end
1962
+ end
1963
+
1964
+ context 'when plucking a specific locale' do
1965
+
1966
+ let(:plucked) do
1967
+ parent.products.all.pluck(:'name.de')
1968
+ end
1969
+
1970
+ it 'returns the specific translations' do
1971
+ expect(plucked.first).to eq('deutsch-text')
1972
+ end
1973
+ end
1974
+
1975
+ context 'when plucking a specific locale from _translations field' do
1976
+
1977
+ let(:plucked) do
1978
+ parent.products.all.pluck(:'name_translations.de')
1979
+ end
1980
+
1981
+ it 'returns the specific translations' do
1982
+ expect(plucked.first).to eq('deutsch-text')
1983
+ end
1984
+ end
1985
+
1986
+ context 'when fallbacks are enabled with a locale list' do
1987
+ require_fallbacks
1988
+
1989
+ before do
1990
+ I18n.fallbacks[:he] = [ :en ]
1991
+ end
1992
+
1993
+ let(:plucked) do
1994
+ parent.products.all.pluck(:name).first
1995
+ end
1996
+
1997
+ it 'correctly uses the fallback' do
1998
+ I18n.locale = :en
1999
+ parent.products.create!(name: 'english-text')
2000
+ I18n.locale = :he
2001
+ expect(plucked).to eq 'english-text'
2002
+ end
2003
+ end
2004
+
2005
+ context 'when the localized field is aliased' do
2006
+ before do
2007
+ I18n.locale = :en
2008
+ parent.products.delete_all
2009
+ p = parent.products.create!(name: 'ACME Rocket Skates', tagline: 'english-text')
2010
+ I18n.locale = :de
2011
+ p.tagline = 'deutsch-text'
2012
+ p.save!
2013
+ end
2014
+
2015
+ context 'when plucking the entire field' do
2016
+ let(:plucked) do
2017
+ parent.products.all.pluck(:tagline)
2018
+ end
2019
+
2020
+ let(:plucked_unaliased) do
2021
+ parent.products.all.pluck(:tl)
2022
+ end
2023
+
2024
+ let(:plucked_translations) do
2025
+ parent.products.all.pluck(:tagline_translations)
2026
+ end
2027
+
2028
+ let(:plucked_translations_both) do
2029
+ parent.products.all.pluck(:tagline_translations, :tagline)
2030
+ end
2031
+
2032
+ it 'returns the demongoized translations' do
2033
+ expect(plucked.first).to eq('deutsch-text')
2034
+ end
2035
+
2036
+ it 'returns the demongoized translations when unaliased' do
2037
+ expect(plucked_unaliased.first).to eq('deutsch-text')
2038
+ end
2039
+
2040
+ it 'returns the full translations hash to _translations' do
2041
+ expect(plucked_translations.first).to eq({ 'de' => 'deutsch-text', 'en' => 'english-text' })
2042
+ end
2043
+
2044
+ it 'returns both' do
2045
+ expect(plucked_translations_both.first).to eq([{ 'de' => 'deutsch-text', 'en' => 'english-text' }, 'deutsch-text'])
2046
+ end
2047
+ end
2048
+
2049
+ context 'when plucking a specific locale' do
2050
+
2051
+ let(:plucked) do
2052
+ parent.products.all.pluck(:'tagline.de')
2053
+ end
2054
+
2055
+ it 'returns the specific translations' do
2056
+ expect(plucked.first).to eq('deutsch-text')
2057
+ end
2058
+ end
2059
+
2060
+ context 'when plucking a specific locale from _translations field' do
2061
+
2062
+ let(:plucked) do
2063
+ parent.products.all.pluck(:'tagline_translations.de')
2064
+ end
2065
+
2066
+ it 'returns the specific translations' do
2067
+ expect(plucked.first).to eq('deutsch-text')
2068
+ end
2069
+ end
2070
+
2071
+ context 'when fallbacks are enabled with a locale list' do
2072
+ require_fallbacks
2073
+
2074
+ before do
2075
+ I18n.fallbacks[:he] = [:en]
2076
+ end
2077
+
2078
+ let(:plucked) do
2079
+ parent.products.all.pluck(:tagline).first
2080
+ end
2081
+
2082
+ it 'correctly uses the fallback' do
2083
+ I18n.locale = :en
2084
+ parent.products.create!(tagline: 'english-text')
2085
+ I18n.locale = :he
2086
+ expect(plucked).to eq 'english-text'
2087
+ end
2088
+ end
2089
+ end
2090
+
2091
+ context 'when the localized field is embedded' do
2092
+ with_default_i18n_configs
2093
+
2094
+ before do
2095
+ s = Seo.new
2096
+ I18n.locale = :en
2097
+ s.name = 'english-text'
2098
+ I18n.locale = :de
2099
+ s.name = 'deutsch-text'
2100
+
2101
+ parent.products.delete_all
2102
+ parent.products.create!(name: 'ACME Tunnel Paint', seo: s)
2103
+ end
2104
+
2105
+ let(:plucked) do
2106
+ parent.products.pluck('seo.name').first
2107
+ end
2108
+
2109
+ let(:plucked_translations) do
2110
+ parent.products.pluck('seo.name_translations').first
2111
+ end
2112
+
2113
+ let(:plucked_translations_field) do
2114
+ parent.products.pluck('seo.name_translations.en').first
2115
+ end
2116
+
2117
+ it 'returns the translation for the current locale' do
2118
+ expect(plucked).to eq('deutsch-text')
2119
+ end
2120
+
2121
+ it 'returns the full _translation hash' do
2122
+ expect(plucked_translations).to eq({ 'en' => 'english-text', 'de' => 'deutsch-text' })
2123
+ end
2124
+
2125
+ it 'returns the translation for the requested locale' do
2126
+ expect(plucked_translations_field).to eq('english-text')
2127
+ end
2128
+ end
2129
+ end
2130
+
2131
+ context 'when the localized field is embedded and aliased' do
2132
+ with_default_i18n_configs
2133
+
2134
+ before do
2135
+ s = Seo.new
2136
+ I18n.locale = :en
2137
+ s.description = 'english-text'
2138
+ I18n.locale = :de
2139
+ s.description = 'deutsch-text'
2140
+
2141
+ parent.products.delete_all
2142
+ parent.products.create!(name: 'ACME Tunnel Paint', seo: s)
2143
+ end
2144
+
2145
+ let(:plucked) do
2146
+ parent.products.pluck('seo.description').first
2147
+ end
2148
+
2149
+ let(:plucked_unaliased) do
2150
+ parent.products.pluck('seo.desc').first
2151
+ end
2152
+
2153
+ let(:plucked_translations) do
2154
+ parent.products.pluck('seo.description_translations').first
2155
+ end
2156
+
2157
+ let(:plucked_translations_field) do
2158
+ parent.products.pluck('seo.description_translations.en').first
2159
+ end
2160
+
2161
+ it 'returns the translation for the current locale' do
2162
+ I18n.with_locale(:en) do
2163
+ expect(plucked).to eq('english-text')
2164
+ end
2165
+ end
2166
+
2167
+ it 'returns the translation for the current locale when unaliased' do
2168
+ I18n.with_locale(:en) do
2169
+ expect(plucked_unaliased).to eq('english-text')
2170
+ end
2171
+ end
2172
+
2173
+ it 'returns the full _translation hash' do
2174
+ expect(plucked_translations).to eq({ 'en' => 'english-text', 'de' => 'deutsch-text' })
2175
+ end
2176
+
2177
+ it 'returns the translation for the requested locale' do
2178
+ expect(plucked_translations_field).to eq('english-text')
2179
+ end
2180
+ end
2181
+
2182
+ context 'when plucking an embedded field' do
2183
+ let(:label) { Label.new(sales: '1E2') }
2184
+ let!(:band) { Band.create!(label: label) }
2185
+
2186
+ let(:plucked) { Band.where(_id: band.id).pluck('label.sales') }
2187
+
2188
+ it 'demongoizes the field' do
2189
+ expect(plucked).to eq([ BigDecimal('1E2') ])
2190
+ end
2191
+ end
2192
+
2193
+ context 'when plucking an embeds_many field' do
2194
+ let(:label) { Label.new(sales: '1E2') }
2195
+ let!(:band) { Band.create!(labels: [label]) }
2196
+
2197
+ let(:plucked) { Band.where(_id: band.id).pluck('labels.sales') }
2198
+
2199
+ it 'demongoizes the field' do
2200
+ expect(plucked.first).to eq([ BigDecimal('1E2') ])
2201
+ end
2202
+ end
2203
+
2204
+ context 'when plucking a nonexistent embedded field' do
2205
+ let(:label) { Label.new(sales: '1E2') }
2206
+ let!(:band) { Band.create!(label: label) }
2207
+
2208
+ let(:plucked) { Band.where(_id: band.id).pluck('label.qwerty') }
2209
+
2210
+ it 'returns nil' do
2211
+ expect(plucked.first).to eq(nil)
2212
+ end
2213
+ end
2214
+ end
2215
+
1822
2216
  describe "#reset" do
1823
2217
 
1824
2218
  let(:person) do
@@ -1729,6 +1729,19 @@ describe Mongoid::Attributes do
1729
1729
  end
1730
1730
  end
1731
1731
  end
1732
+
1733
+ context 'when map_big_decimal_to_decimal128 is enabled' do
1734
+ config_override :map_big_decimal_to_decimal128, true
1735
+
1736
+ context 'when writing an identical number' do
1737
+ let(:band) { Band.create!(name: 'Nirvana', sales: 123456.78).reload }
1738
+
1739
+ it 'does not mark the document as changed' do
1740
+ band.sales = 123456.78
1741
+ expect(band.changed?).to be false
1742
+ end
1743
+ end
1744
+ end
1732
1745
  end
1733
1746
 
1734
1747
  describe "#typed_value_for" do
@@ -0,0 +1,28 @@
1
+ # Candidate Tasks
2
+
3
+ When using the `candidate` rake tasks, you must make sure:
4
+
5
+ 1. You are using at least `git` version 2.49.0.
6
+ 2. You have the `gh` CLI tool installed.
7
+ 3. You are logged into `gh` with an account that has collaborator access to the repository.
8
+ 4. You have run `gh repo set-default` from the root of your local checkout to set the default repository to the canonical MongoDB repo.
9
+ 5. The `origin` remote for your local checkout is set to your own fork.
10
+ 6. The `upstream` remote for your local checkout is set to the canonical
11
+ MongoDB repo.
12
+
13
+ Once configured, you can use the following commands:
14
+
15
+ 1. `rake candidate:prs` - This will list all pull requests that will be included in the next release. Any with `[?]` are unlabelled (or are not labelled with a recognized label). Otherwise, `[b]` means `bug`, `[f]` means `feature`, and `[x]` means `bcbreak`.
16
+ 2. `rake candidate:preview` - This will generate and display the release notes for the next release, based on the associated pull requests.
17
+ 3. `rake candidate:create` - This will create a new PR against the default repository, using the generated release notes as the description. The new PR will be given the `release-candidate` label.
18
+
19
+ Then, after the release candidate PR is approved and merged, the release process will automatically bundle, sign, and release the new version.
20
+
21
+ Once you've merged the PR, you can switch to the "Actions" tab for the repository on GitHub and look for the "Release" workflow (might be named differently), which should have triggered automatically. You can monitor the progress of the release there. If there are any problems, the workflow is generally safe to re-run after you've addressed them.
22
+
23
+ Things to do after the release succeeds:
24
+
25
+ 1. Copy the release notes from the PR and create a new release announcement on the forums (https://www.mongodb.com/community/forums/c/announcements/driver-releases/110).
26
+ 2. If the release was not automatically announced in #ruby, copy a link to the GitHub release or MongoDB forum post there.
27
+ 3. Close the release in Jira.
28
+
@@ -25,19 +25,19 @@ module Mrss
25
25
  end
26
26
 
27
27
  def initialize(root: nil, classifiers:, priority_order:,
28
- spec_root: nil, rspec_json_path: nil, rspec_all_json_path: nil,
29
- randomize: false
28
+ spec_root: nil, rspec_json_path: nil, rspec_all_json_path: nil, rspec_xml_path: nil, randomize: false
30
29
  )
31
30
  @spec_root = spec_root || File.join(root, 'spec')
32
31
  @classifiers = classifiers
33
32
  @priority_order = priority_order
34
33
  @rspec_json_path = rspec_json_path || File.join(root, 'tmp/rspec.json')
35
34
  @rspec_all_json_path = rspec_all_json_path || File.join(root, 'tmp/rspec-all.json')
35
+ @rspec_xml_path = rspec_xml_path || File.join(root, 'tmp/rspec.xml')
36
36
  @randomize = !!randomize
37
37
  end
38
38
 
39
39
  attr_reader :spec_root, :classifiers, :priority_order
40
- attr_reader :rspec_json_path, :rspec_all_json_path
40
+ attr_reader :rspec_json_path, :rspec_all_json_path, :rspec_xml_path
41
41
 
42
42
  def randomize?
43
43
  @randomize
@@ -47,6 +47,25 @@ module Mrss
47
47
  @seed ||= (rand * 100_000).to_i
48
48
  end
49
49
 
50
+ # Remove all XML files from tmp directory before running tests
51
+ def cleanup_xml_files
52
+ xml_pattern = File.join(File.dirname(rspec_xml_path), '*.xml')
53
+ Dir.glob(xml_pattern).each do |xml_file|
54
+ FileUtils.rm_f(xml_file)
55
+ end
56
+ end
57
+
58
+ # Move the XML file to a timestamped version for evergreen upload
59
+ def archive_xml_file(category)
60
+ return unless File.exist?(rspec_xml_path)
61
+
62
+ timestamp = Time.now.strftime('%Y%m%d_%H%M%S_%3N')
63
+ archived_path = rspec_xml_path.sub(/\.xml$/, "-#{category}-#{timestamp}.xml")
64
+
65
+ FileUtils.mv(rspec_xml_path, archived_path)
66
+ puts "Archived XML results to #{archived_path}"
67
+ end
68
+
50
69
  def buckets
51
70
  @buckets ||= {}.tap do |buckets|
52
71
  Find.find(spec_root) do |path|
@@ -96,6 +115,8 @@ module Mrss
96
115
 
97
116
  def run_buckets(*buckets)
98
117
  FileUtils.rm_f(rspec_all_json_path)
118
+ # Clean up all XML files before starting test runs
119
+ cleanup_xml_files
99
120
 
100
121
  buckets.each do |bucket|
101
122
  if bucket && !self.buckets[bucket]
@@ -131,7 +152,12 @@ module Mrss
131
152
  def run_files(category, paths)
132
153
  puts "Running #{category.to_s.gsub('_', ' ')} tests"
133
154
  FileUtils.rm_f(rspec_json_path)
155
+ FileUtils.rm_f(rspec_xml_path) # Clean up XML file before running this bucket
156
+
134
157
  cmd = %w(rspec) + paths
158
+ # Add junit formatter for XML output
159
+ cmd += ['--format', 'Rfc::Riff', '--format', 'RspecJunitFormatter', '--out', rspec_xml_path]
160
+
135
161
  if randomize?
136
162
  cmd += %W(--order rand:#{seed})
137
163
  end
@@ -147,6 +173,9 @@ module Mrss
147
173
  FileUtils.cp(rspec_json_path, rspec_all_json_path)
148
174
  end
149
175
  end
176
+
177
+ # Archive XML file after running this bucket
178
+ archive_xml_file(category)
150
179
  end
151
180
 
152
181
  true
@@ -78,7 +78,7 @@ install_mlaunch_venv() {
78
78
  # Debian11/Ubuntu2204 have venv installed, but it is nonfunctional unless
79
79
  # the python3-venv package is also installed (it lacks the ensurepip
80
80
  # module).
81
- sudo apt-get install --yes python3-venv
81
+ sudo apt-get update && sudo apt-get install --yes python3-venv
82
82
  fi
83
83
  if test "$USE_SYSTEM_PYTHON_PACKAGES" = 1 &&
84
84
  python3 -m pip list |grep mtools
@@ -5,4 +5,6 @@ class Company
5
5
  include Mongoid::Document
6
6
 
7
7
  embeds_many :staffs
8
+
9
+ has_many :products
8
10
  end
@@ -8,6 +8,7 @@ class Passport
8
8
  field :country, type: String
9
9
  field :exp, as: :expiration_date, type: Date
10
10
  field :name, localize: true
11
+ field :bp, as: :birthplace, localize: true
11
12
  field :localized_translations, localize: true
12
13
 
13
14
  embedded_in :person, autobuild: true
@@ -18,4 +18,6 @@ class Product
18
18
  validates :website, format: { with: URI.regexp, allow_blank: true }
19
19
 
20
20
  embeds_one :seo, as: :seo_tags, cascade_callbacks: true, autobuild: true
21
+
22
+ belongs_to :company
21
23
  end
@@ -5,6 +5,8 @@ class Seo
5
5
  include Mongoid::Document
6
6
  include Mongoid::Timestamps
7
7
  field :title, type: String
8
+ field :name, type: String, localize: true
9
+ field :desc, as: :description, type: String, localize: true
8
10
 
9
11
  embedded_in :seo_tags, polymorphic: true
10
12
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongoid
3
3
  version: !ruby/object:Gem::Version
4
- version: 9.0.8
4
+ version: 9.0.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - The MongoDB Ruby Team
@@ -18,7 +18,7 @@ dependencies:
18
18
  version: '5.1'
19
19
  - - "<"
20
20
  - !ruby/object:Gem::Version
21
- version: '8.1'
21
+ version: '8.2'
22
22
  - - "!="
23
23
  - !ruby/object:Gem::Version
24
24
  version: 7.0.0
@@ -31,7 +31,7 @@ dependencies:
31
31
  version: '5.1'
32
32
  - - "<"
33
33
  - !ruby/object:Gem::Version
34
- version: '8.1'
34
+ version: '8.2'
35
35
  - - "!="
36
36
  - !ruby/object:Gem::Version
37
37
  version: 7.0.0
@@ -437,6 +437,7 @@ files:
437
437
  - lib/mongoid/persistable/updatable.rb
438
438
  - lib/mongoid/persistable/upsertable.rb
439
439
  - lib/mongoid/persistence_context.rb
440
+ - lib/mongoid/pluckable.rb
440
441
  - lib/mongoid/positional.rb
441
442
  - lib/mongoid/railtie.rb
442
443
  - lib/mongoid/railties/bson_object_id_serializer.rb
@@ -895,6 +896,7 @@ files:
895
896
  - spec/mongoid_spec.rb
896
897
  - spec/rails/controller_extension/controller_runtime_spec.rb
897
898
  - spec/rails/mongoid_spec.rb
899
+ - spec/shared/CANDIDATE.md
898
900
  - spec/shared/LICENSE
899
901
  - spec/shared/bin/get-mongodb-download-url
900
902
  - spec/shared/bin/s3-copy
@@ -1229,7 +1231,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
1229
1231
  - !ruby/object:Gem::Version
1230
1232
  version: 1.3.6
1231
1233
  requirements: []
1232
- rubygems_version: 3.7.2
1234
+ rubygems_version: 4.0.2
1233
1235
  specification_version: 4
1234
1236
  summary: Elegant Persistence in Ruby for MongoDB.
1235
1237
  test_files:
@@ -1642,6 +1644,7 @@ test_files:
1642
1644
  - spec/mongoid_spec.rb
1643
1645
  - spec/rails/controller_extension/controller_runtime_spec.rb
1644
1646
  - spec/rails/mongoid_spec.rb
1647
+ - spec/shared/CANDIDATE.md
1645
1648
  - spec/shared/LICENSE
1646
1649
  - spec/shared/bin/get-mongodb-download-url
1647
1650
  - spec/shared/bin/s3-copy