mongoid 9.0.2 → 9.0.3

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: 558b30b0a39cd36cd9a3e54695f4617a9fcc0d67300637d1299b487b7d911bda
4
- data.tar.gz: eccf9c55bdde17fd38592427e0dd6b5545d3e605b94a4308a422ba332e20c183
3
+ metadata.gz: 421d53636b0e90abcfa52bf637229588d434ff73f14574b1d316b72009f7236b
4
+ data.tar.gz: fafa07a7cc9b17de0abbb140ca3d021e3041bfe00b67a424c81f6c15018e607d
5
5
  SHA512:
6
- metadata.gz: 420ac9d23abeecb04c19794df124fc80d4f99883e1156d4e3be42e79b4b2e5e5a558c6b650746784024f0477a800def3a2ded537b848d3e9b97fb5e5e220b1f0
7
- data.tar.gz: 40bfdcaf862e3ae7d3d046d84d5f9f5eea4752b5559c869eda16e1bd00cbc9aaae3eef05a7e4bec497915619fb162a248ac8f4be3525068e81f28ae876af1a29
6
+ metadata.gz: d4f2011d1f85178a8693d653518f06e7ad55c1363958e3577f80d040e3986b595bdcaade0e71696ff762e008c195ee23b4579458708bb9e916f7ddfa8e3b9352
7
+ data.tar.gz: 8b981e1bc4e904085ecedfe459d31c91d06ed353ba0ceef3d67865ea195f31a36539284246b54537fc5af8b54eafcf1a60415eb88c4850d7cdf3af378d449206
@@ -86,7 +86,7 @@ module Mongoid
86
86
  else
87
87
  PersistenceContext.get(self) ||
88
88
  PersistenceContext.get(self.class) ||
89
- PersistenceContext.new(self.class, storage_options)
89
+ PersistenceContext.new(self.class, default_storage_options)
90
90
  end
91
91
  end
92
92
 
@@ -112,6 +112,19 @@ module Mongoid
112
112
 
113
113
  private
114
114
 
115
+ def default_storage_options
116
+ # Nothing is overridden, we use either the default storage_options
117
+ # or storage_options defined for the document class.
118
+ return storage_options if Threaded.client_override.nil? && Threaded.database_override.nil?
119
+
120
+ storage_options.tap do |opts|
121
+ # Globally overridden client replaces client defined for the document class.
122
+ opts[:client] = Threaded.client_override unless Threaded.client_override.nil?
123
+ # Globally overridden database replaces database defined for the document class.
124
+ opts[:database] = Threaded.database_override unless Threaded.database_override.nil?
125
+ end
126
+ end
127
+
115
128
  def set_persistence_context(options_or_context)
116
129
  PersistenceContext.set(self, options_or_context)
117
130
  end
@@ -62,6 +62,7 @@ module Mongoid
62
62
  rescue *transactions_not_supported_exceptions
63
63
  raise Mongoid::Errors::TransactionsNotSupported
64
64
  ensure
65
+ Threaded.clear_modified_documents(session)
65
66
  Threaded.clear_session(client: persistence_context.client)
66
67
  end
67
68
 
@@ -39,11 +39,25 @@ module Mongoid
39
39
  # from and behaves identically to association traversal for the purposes
40
40
  # of, for example, subsequent array element retrieval.
41
41
  #
42
- # @param [ Document | Hash ] document The document to extract from.
42
+ # @param [ Document | Hash | String ] document The document to extract from.
43
43
  # @param [ String ] key The key path to extract.
44
44
  #
45
45
  # @return [ Object | Array ] Field value or values.
46
46
  module_function def extract_attribute(document, key)
47
+ # The matcher system will wind up sending atomic values to this as well,
48
+ # when attepting to match more complex types. If anything other than a
49
+ # Document or a Hash is given, we'll short-circuit the logic and just
50
+ # return an empty array.
51
+ return [] unless document.is_a?(Hash) || document.is_a?(Document)
52
+
53
+ # Performance optimization; if the key does not include a '.' character,
54
+ # it must reference an immediate attribute of the document.
55
+ unless key.include?('.')
56
+ hash = document.respond_to?(:attributes) ? document.attributes : document
57
+ key = find_exact_key(hash, key)
58
+ return key ? [ hash[key] ] : []
59
+ end
60
+
47
61
  if document.respond_to?(:as_attributes, true)
48
62
  # If a document has hash fields, as_attributes would keep those fields
49
63
  # as Hash instances which do not offer indifferent access.
@@ -117,12 +117,15 @@ module Mongoid
117
117
  def client
118
118
  @client ||= begin
119
119
  client = Clients.with_name(client_name)
120
+ options = client_options
121
+
120
122
  if database_name_option
121
123
  client = client.use(database_name)
124
+ options = options.except(:database, 'database')
122
125
  end
123
- unless client_options.empty?
124
- client = client.with(client_options)
125
- end
126
+
127
+ client = client.with(options) unless options.empty?
128
+
126
129
  client
127
130
  end
128
131
  end
@@ -282,6 +285,10 @@ module Mongoid
282
285
  # @api private
283
286
  PERSISTENCE_CONTEXT_KEY = :"[mongoid]:persistence_context"
284
287
 
288
+ def context_store
289
+ Threaded.get(PERSISTENCE_CONTEXT_KEY) { {} }
290
+ end
291
+
285
292
  # Get the persistence context for a given object from the thread local
286
293
  # storage.
287
294
  #
@@ -292,8 +299,7 @@ module Mongoid
292
299
  #
293
300
  # @api private
294
301
  def get_context(object)
295
- Thread.current[PERSISTENCE_CONTEXT_KEY] ||= {}
296
- Thread.current[PERSISTENCE_CONTEXT_KEY][object.object_id]
302
+ context_store[object.object_id]
297
303
  end
298
304
 
299
305
  # Store persistence context for a given object in the thread local
@@ -305,10 +311,9 @@ module Mongoid
305
311
  # @api private
306
312
  def store_context(object, context)
307
313
  if context.nil?
308
- Thread.current[PERSISTENCE_CONTEXT_KEY]&.delete(object.object_id)
314
+ context_store.delete(object.object_id)
309
315
  else
310
- Thread.current[PERSISTENCE_CONTEXT_KEY] ||= {}
311
- Thread.current[PERSISTENCE_CONTEXT_KEY][object.object_id] = context
316
+ context_store[object.object_id] = context
312
317
  end
313
318
  end
314
319
  end
@@ -78,7 +78,7 @@ module Mongoid
78
78
  #
79
79
  # @return [ Integer ] The runtime value.
80
80
  def self.runtime
81
- Thread.current[VARIABLE_NAME] ||= 0
81
+ Threaded.get(VARIABLE_NAME) { 0 }
82
82
  end
83
83
 
84
84
  # Set the runtime value on the current thread.
@@ -87,7 +87,7 @@ module Mongoid
87
87
  #
88
88
  # @return [ Integer ] The runtime value.
89
89
  def self.runtime= value
90
- Thread.current[VARIABLE_NAME] = value
90
+ Threaded.set(VARIABLE_NAME, value)
91
91
  end
92
92
 
93
93
  # Reset the runtime value to zero the current thread.
@@ -13,13 +13,13 @@ module Mongoid
13
13
  included do
14
14
 
15
15
  class << self
16
- # Note that this intentionally only delegates :include_root_in_json
17
- # and not :include_root_in_json? - delegating the latter produces
18
- # wrong behavior.
19
- # Also note that this intentionally uses the ActiveSupport delegation
20
- # functionality and not the Ruby standard library one.
21
- # See https://jira.mongodb.org/browse/MONGOID-4849.
22
- delegate :include_root_in_json, to: ::Mongoid
16
+ def include_root_in_json
17
+ @include_root_in_json.nil? ? ::Mongoid.include_root_in_json : @include_root_in_json
18
+ end
19
+
20
+ def include_root_in_json=(new_value)
21
+ @include_root_in_json = new_value
22
+ end
23
23
  end
24
24
  end
25
25
 
@@ -18,6 +18,7 @@ module Mongoid
18
18
  CURRENT_SCOPE_KEY = '[mongoid]:current-scope'
19
19
 
20
20
  AUTOSAVES_KEY = '[mongoid]:autosaves'
21
+
21
22
  VALIDATIONS_KEY = '[mongoid]:validations'
22
23
 
23
24
  STACK_KEYS = Hash.new do |hash, key|
@@ -36,6 +37,75 @@ module Mongoid
36
37
 
37
38
  extend self
38
39
 
40
+ # Queries the thread-local variable with the given name. If a block is
41
+ # given, and the variable does not already exist, the return value of the
42
+ # block will be set as the value of the variable before returning it.
43
+ #
44
+ # It is very important that applications (and espcially Mongoid)
45
+ # use this method instead of Thread#[], since Thread#[] is actually for
46
+ # fiber-local variables, and Mongoid uses Fibers as an implementation
47
+ # detail in some callbacks. Putting thread-local state in a fiber-local
48
+ # store will result in the state being invisible when relevant callbacks are
49
+ # run in a different fiber.
50
+ #
51
+ # Affected callbacks are cascading callbacks on embedded children.
52
+ #
53
+ # @param [ String | Symbol ] key the name of the variable to query
54
+ # @param [ Proc ] default an optional block that must return the default
55
+ # (initial) value of this variable.
56
+ #
57
+ # @return [ Object | nil ] the value of the queried variable, or nil if
58
+ # it is not set and no default was given.
59
+ def get(key, &default)
60
+ result = Thread.current.thread_variable_get(key)
61
+
62
+ if result.nil? && default
63
+ result = yield
64
+ set(key, result)
65
+ end
66
+
67
+ result
68
+ end
69
+
70
+ # Sets a thread-local variable with the given name to the given value.
71
+ # See #get for a discussion of why this method is necessary, and why
72
+ # Thread#[]= should be avoided in cascading callbacks on embedded children.
73
+ #
74
+ # @param [ String | Symbol ] key the name of the variable to set.
75
+ # @param [ Object | nil ] value the value of the variable to set (or `nil`
76
+ # if you wish to unset the variable)
77
+ def set(key, value)
78
+ Thread.current.thread_variable_set(key, value)
79
+ end
80
+
81
+ # Removes the named variable from thread-local storage.
82
+ #
83
+ # @param [ String | Symbol ] key the name of the variable to remove.
84
+ def delete(key)
85
+ set(key, nil)
86
+ end
87
+
88
+ # Queries the presence of a named variable in thread-local storage.
89
+ #
90
+ # @param [ String | Symbol ] key the name of the variable to query.
91
+ #
92
+ # @return [ true | false ] whether the given variable is present or not.
93
+ def has?(key)
94
+ # Here we have a classic example of JRuby not behaving like MRI. In
95
+ # MRI, if you set a thread variable to nil, it removes it from the list
96
+ # and subsequent calls to thread_variable?(key) will return false. Not
97
+ # so with JRuby. Once set, you cannot unset the thread variable.
98
+ #
99
+ # However, because setting a variable to nil is supposed to remove it,
100
+ # we can assume a nil-valued variable doesn't actually exist.
101
+
102
+ # So, instead of this:
103
+ # Thread.current.thread_variable?(key)
104
+
105
+ # We have to do this:
106
+ !get(key).nil?
107
+ end
108
+
39
109
  # Begin entry into a named thread local stack.
40
110
  #
41
111
  # @example Begin entry into the stack.
@@ -55,7 +125,7 @@ module Mongoid
55
125
  #
56
126
  # @return [ String | Symbol ] The override.
57
127
  def database_override
58
- Thread.current[DATABASE_OVERRIDE_KEY]
128
+ get(DATABASE_OVERRIDE_KEY)
59
129
  end
60
130
 
61
131
  # Set the global database override.
@@ -67,7 +137,7 @@ module Mongoid
67
137
  #
68
138
  # @return [ String | Symbol ] The override.
69
139
  def database_override=(name)
70
- Thread.current[DATABASE_OVERRIDE_KEY] = name
140
+ set(DATABASE_OVERRIDE_KEY, name)
71
141
  end
72
142
 
73
143
  # Are in the middle of executing the named stack
@@ -103,7 +173,7 @@ module Mongoid
103
173
  #
104
174
  # @return [ Array ] The stack.
105
175
  def stack(name)
106
- Thread.current[STACK_KEYS[name]] ||= []
176
+ get(STACK_KEYS[name]) { [] }
107
177
  end
108
178
 
109
179
  # Begin autosaving a document on the current thread.
@@ -177,7 +247,7 @@ module Mongoid
177
247
  #
178
248
  # @return [ String | Symbol ] The override.
179
249
  def client_override
180
- Thread.current[CLIENT_OVERRIDE_KEY]
250
+ get(CLIENT_OVERRIDE_KEY)
181
251
  end
182
252
 
183
253
  # Set the global client override.
@@ -189,7 +259,7 @@ module Mongoid
189
259
  #
190
260
  # @return [ String | Symbol ] The override.
191
261
  def client_override=(name)
192
- Thread.current[CLIENT_OVERRIDE_KEY] = name
262
+ set(CLIENT_OVERRIDE_KEY, name)
193
263
  end
194
264
 
195
265
  # Get the current Mongoid scope.
@@ -202,12 +272,12 @@ module Mongoid
202
272
  #
203
273
  # @return [ Criteria ] The scope.
204
274
  def current_scope(klass = nil)
205
- if klass && Thread.current[CURRENT_SCOPE_KEY].respond_to?(:keys)
206
- Thread.current[CURRENT_SCOPE_KEY][
207
- Thread.current[CURRENT_SCOPE_KEY].keys.find { |k| k <= klass }
208
- ]
275
+ current_scope = get(CURRENT_SCOPE_KEY)
276
+
277
+ if klass && current_scope.respond_to?(:keys)
278
+ current_scope[current_scope.keys.find { |k| k <= klass }]
209
279
  else
210
- Thread.current[CURRENT_SCOPE_KEY]
280
+ current_scope
211
281
  end
212
282
  end
213
283
 
@@ -220,7 +290,7 @@ module Mongoid
220
290
  #
221
291
  # @return [ Criteria ] The scope.
222
292
  def current_scope=(scope)
223
- Thread.current[CURRENT_SCOPE_KEY] = scope
293
+ set(CURRENT_SCOPE_KEY, scope)
224
294
  end
225
295
 
226
296
  # Set the current Mongoid scope. Safe for multi-model scope chaining.
@@ -236,8 +306,8 @@ module Mongoid
236
306
  if scope.nil?
237
307
  unset_current_scope(klass)
238
308
  else
239
- Thread.current[CURRENT_SCOPE_KEY] ||= {}
240
- Thread.current[CURRENT_SCOPE_KEY][klass] = scope
309
+ current_scope = get(CURRENT_SCOPE_KEY) { {} }
310
+ current_scope[klass] = scope
241
311
  end
242
312
  end
243
313
 
@@ -284,7 +354,7 @@ module Mongoid
284
354
  #
285
355
  # @return [ Hash ] The current autosaves.
286
356
  def autosaves
287
- Thread.current[AUTOSAVES_KEY] ||= {}
357
+ get(AUTOSAVES_KEY) { {} }
288
358
  end
289
359
 
290
360
  # Get all validations on the current thread.
@@ -294,7 +364,7 @@ module Mongoid
294
364
  #
295
365
  # @return [ Hash ] The current validations.
296
366
  def validations
297
- Thread.current[VALIDATIONS_KEY] ||= {}
367
+ get(VALIDATIONS_KEY) { {} }
298
368
  end
299
369
 
300
370
  # Get all autosaves on the current thread for the class.
@@ -376,9 +446,7 @@ module Mongoid
376
446
  # @return [ Set<Mongoid::Document> ] Collection of modified documents before
377
447
  # it was cleared.
378
448
  def clear_modified_documents(session)
379
- modified_documents[session].dup
380
- ensure
381
- modified_documents[session].clear
449
+ modified_documents.delete(session) || []
382
450
  end
383
451
 
384
452
  # Queries whether document callbacks should be executed by default for the
@@ -390,8 +458,8 @@ module Mongoid
390
458
  # @return [ true | false ] Whether or not document callbacks should be
391
459
  # executed by default.
392
460
  def execute_callbacks?
393
- if Thread.current.key?(EXECUTE_CALLBACKS)
394
- Thread.current[EXECUTE_CALLBACKS]
461
+ if has?(EXECUTE_CALLBACKS)
462
+ get(EXECUTE_CALLBACKS)
395
463
  else
396
464
  true
397
465
  end
@@ -404,7 +472,7 @@ module Mongoid
404
472
  # @param flag [ true | false ] Whether or not document callbacks should be
405
473
  # executed by default.
406
474
  def execute_callbacks=(flag)
407
- Thread.current[EXECUTE_CALLBACKS] = flag
475
+ set(EXECUTE_CALLBACKS, flag)
408
476
  end
409
477
 
410
478
  # Returns the thread store of sessions.
@@ -413,7 +481,7 @@ module Mongoid
413
481
  #
414
482
  # @api private
415
483
  def sessions
416
- Thread.current[SESSIONS_KEY] ||= {}.compare_by_identity
484
+ get(SESSIONS_KEY) { {}.compare_by_identity }
417
485
  end
418
486
 
419
487
  # Returns the thread store of modified documents.
@@ -423,9 +491,7 @@ module Mongoid
423
491
  #
424
492
  # @api private
425
493
  def modified_documents
426
- Thread.current[MODIFIED_DOCUMENTS_KEY] ||= Hash.new do |h, k|
427
- h[k] = Set.new
428
- end
494
+ get(MODIFIED_DOCUMENTS_KEY) { Hash.new { |h, k| h[k] = Set.new } }
429
495
  end
430
496
 
431
497
  private
@@ -435,10 +501,12 @@ module Mongoid
435
501
  #
436
502
  # @param klass [ Class ] the class to remove from the current scope.
437
503
  def unset_current_scope(klass)
438
- return unless Thread.current[CURRENT_SCOPE_KEY]
504
+ return unless has?(CURRENT_SCOPE_KEY)
505
+
506
+ scope = get(CURRENT_SCOPE_KEY)
507
+ scope.delete(klass)
439
508
 
440
- Thread.current[CURRENT_SCOPE_KEY].delete(klass)
441
- Thread.current[CURRENT_SCOPE_KEY] = nil if Thread.current[CURRENT_SCOPE_KEY].empty?
509
+ delete(CURRENT_SCOPE_KEY) if scope.empty?
442
510
  end
443
511
  end
444
512
  end
@@ -46,6 +46,9 @@ module Mongoid
46
46
  class << self
47
47
  extend Forwardable
48
48
 
49
+ # The key to use to store the timeless table
50
+ TIMELESS_TABLE_KEY = '[mongoid]:timeless'
51
+
49
52
  # Returns the in-memory thread cache of classes
50
53
  # for which to skip timestamping.
51
54
  #
@@ -53,7 +56,7 @@ module Mongoid
53
56
  #
54
57
  # @api private
55
58
  def timeless_table
56
- Thread.current['[mongoid]:timeless'] ||= Hash.new
59
+ Threaded.get(TIMELESS_TABLE_KEY) { Hash.new }
57
60
  end
58
61
 
59
62
  def_delegators :timeless_table, :[]=, :[]
@@ -195,7 +195,7 @@ module Mongoid
195
195
  # @return [ Hash ] The hash that contains touch callback suppression
196
196
  # statuses
197
197
  def touch_callback_statuses
198
- Thread.current[SUPPRESS_TOUCH_CALLBACKS_KEY] ||= {}
198
+ Threaded.get(SUPPRESS_TOUCH_CALLBACKS_KEY) { {} }
199
199
  end
200
200
 
201
201
  # Define the method that will get called for touching belongs_to
@@ -105,7 +105,11 @@ module Mongoid
105
105
  if value
106
106
  Mongoid::Fields::Validators::Macro.validate_field_name(self, value)
107
107
  value = value.to_s
108
- super
108
+ if defined?(::ActiveSupport::ClassAttribute)
109
+ ::ActiveSupport::ClassAttribute.redefine(self, 'discriminator_key', value)
110
+ else
111
+ super
112
+ end
109
113
  else
110
114
  # When discriminator key is set to nil, replace the class's definition
111
115
  # of the discriminator key reader (provided by class_attribute earlier)
@@ -119,7 +123,7 @@ module Mongoid
119
123
  # an existing field.
120
124
  # This condition also checks if the class has any descendants, because
121
125
  # if it doesn't then it doesn't need a discriminator key.
122
- return unless !fields.key?(discriminator_key) && !descendants.empty?
126
+ return if fields.key?(discriminator_key) || descendants.empty?
123
127
 
124
128
  default_proc = -> { self.class.discriminator_value }
125
129
  field(discriminator_key, default: default_proc, type: String)
@@ -70,13 +70,16 @@ module Mongoid
70
70
  # Now, treating the target as an array, look at each element
71
71
  # and see if it is valid, but only if it has already been
72
72
  # persisted, or changed, and hasn't been flagged for destroy.
73
- list.all? do |value|
73
+ #
74
+ # use map.all? instead of just all?, because all? will do short-circuit
75
+ # evaluation and terminate on the first failed validation.
76
+ list.map do |value|
74
77
  if value && !value.flagged_for_destroy? && (!value.persisted? || value.changed?)
75
78
  value.validated? ? true : value.valid?
76
79
  else
77
80
  true
78
81
  end
79
- end
82
+ end.all?
80
83
  end
81
84
 
82
85
  document.errors.add(attribute, :invalid) unless valid
@@ -2,5 +2,5 @@
2
2
  # rubocop:todo all
3
3
 
4
4
  module Mongoid
5
- VERSION = "9.0.2"
5
+ VERSION = "9.0.3"
6
6
  end
@@ -2,31 +2,35 @@
2
2
  # rubocop:todo all
3
3
 
4
4
  require 'spec_helper'
5
- require 'active_job'
6
- require 'mongoid/railties/bson_object_id_serializer'
5
+ begin
6
+ require 'active_job'
7
+ require 'mongoid/railties/bson_object_id_serializer'
7
8
 
8
- describe 'ActiveJob Serialization' do
9
- skip unless defined?(ActiveJob)
9
+ describe 'ActiveJob Serialization' do
10
+ skip unless defined?(ActiveJob)
10
11
 
11
- class TestBsonObjectIdSerializerJob < ActiveJob::Base
12
- def perform(*args)
13
- args
12
+ class TestBsonObjectIdSerializerJob < ActiveJob::Base
13
+ def perform(*args)
14
+ args
15
+ end
14
16
  end
15
- end
16
17
 
17
- let(:band) do
18
- Band.create!
19
- end
18
+ let(:band) do
19
+ Band.create!
20
+ end
20
21
 
21
- before do
22
- ActiveJob::Serializers.add_serializers(
23
- [::Mongoid::Railties::ActiveJobSerializers::BsonObjectIdSerializer]
24
- )
25
- end
22
+ before do
23
+ ActiveJob::Serializers.add_serializers(
24
+ [::Mongoid::Railties::ActiveJobSerializers::BsonObjectIdSerializer]
25
+ )
26
+ end
26
27
 
27
- it 'serializes and deserializes BSON::ObjectId' do
28
- expect do
29
- TestBsonObjectIdSerializerJob.perform_later(band.id)
30
- end.not_to raise_error
28
+ it 'serializes and deserializes BSON::ObjectId' do
29
+ expect do
30
+ TestBsonObjectIdSerializerJob.perform_later(band.id)
31
+ end.not_to raise_error
32
+ end
31
33
  end
34
+ rescue LoadError
35
+ RSpec.context.skip 'This test requires active_job'
32
36
  end
@@ -36,6 +36,8 @@ describe 'Mongoid application tests' do
36
36
  context 'demo application' do
37
37
  context 'sinatra' do
38
38
  it 'runs' do
39
+ skip 'https://jira.mongodb.org/browse/MONGOID-5826'
40
+
39
41
  clone_application(
40
42
  'https://github.com/mongoid/mongoid-demo',
41
43
  subdir: 'sinatra-minimal',
@@ -55,6 +57,8 @@ describe 'Mongoid application tests' do
55
57
 
56
58
  context 'rails-api' do
57
59
  it 'runs' do
60
+ skip 'https://jira.mongodb.org/browse/MONGOID-5826'
61
+
58
62
  clone_application(
59
63
  'https://github.com/mongoid/mongoid-demo',
60
64
  subdir: 'rails-api',
@@ -172,7 +176,7 @@ describe 'Mongoid application tests' do
172
176
  if (rails_version = SpecConfig.instance.rails_version) == 'master'
173
177
  else
174
178
  check_call(%w(gem list))
175
- check_call(%w(gem install rails --no-document -v) + ["~> #{rails_version}.0"])
179
+ check_call(%w(gem install rails --no-document --force -v) + ["~> #{rails_version}.0"])
176
180
  end
177
181
  end
178
182
 
@@ -319,6 +323,10 @@ describe 'Mongoid application tests' do
319
323
  end
320
324
 
321
325
  def adjust_rails_defaults(rails_version: SpecConfig.instance.rails_version)
326
+ if !rails_version.match?(/^\d+\.\d+$/)
327
+ # This must be pre-release version, we trim it
328
+ rails_version = rails_version.split('.')[0..1].join('.')
329
+ end
322
330
  if File.exist?('config/application.rb')
323
331
  lines = IO.readlines('config/application.rb')
324
332
  lines.each do |line|
@@ -751,6 +751,10 @@ describe Mongoid::Association::Referenced::BelongsTo::Proxy do
751
751
  person.save!
752
752
  end
753
753
 
754
+ # NOTE: there as a bad interdependency here, with the auto_save_spec.rb
755
+ # file. If auto_save_spec.rb runs before this, the following specs fail
756
+ # with "undefined method `nullify' for an instance of Person".
757
+
754
758
  context "when parent exists" do
755
759
 
756
760
  context "when child is destroyed" do
@@ -27,7 +27,7 @@ describe Mongoid::Clients::Options, retry: 3 do
27
27
  let(:options) { { database: 'other' } }
28
28
 
29
29
  it 'sets the options on the client' do
30
- expect(persistence_context.client.options['database']).to eq(options[:database])
30
+ expect(persistence_context.client.options['database'].to_s).to eq(options[:database].to_s)
31
31
  end
32
32
 
33
33
  it 'does not set the options on class level' do
@@ -319,7 +319,7 @@ describe Mongoid::Clients::Options, retry: 3 do
319
319
  end
320
320
 
321
321
  it 'sets the options on the client' do
322
- expect(persistence_context.client.options['database']).to eq(options[:database])
322
+ expect(persistence_context.client.options['database'].to_s).to eq(options[:database].to_s)
323
323
  end
324
324
 
325
325
  it 'does not set the options on instance level' do
@@ -522,4 +522,129 @@ describe Mongoid::Clients::Options, retry: 3 do
522
522
  end
523
523
  end
524
524
  end
525
+
526
+ context 'with global overrides' do
527
+ let(:default_subscriber) do
528
+ Mrss::EventSubscriber.new
529
+ end
530
+
531
+ let(:override_subscriber) do
532
+ Mrss::EventSubscriber.new
533
+ end
534
+
535
+ context 'when global client is overridden' do
536
+ before do
537
+ Mongoid.clients['override_client'] = { hosts: SpecConfig.instance.addresses, database: 'default_override_database' }
538
+ Mongoid.override_client('override_client')
539
+ Mongoid.client(:default).subscribe(Mongo::Monitoring::COMMAND, default_subscriber)
540
+ Mongoid.client('override_client').subscribe(Mongo::Monitoring::COMMAND, override_subscriber)
541
+ end
542
+
543
+ after do
544
+ Mongoid.client(:default).unsubscribe(Mongo::Monitoring::COMMAND, default_subscriber)
545
+ Mongoid.client('override_client').unsubscribe(Mongo::Monitoring::COMMAND, override_subscriber)
546
+ Mongoid.override_client(nil)
547
+ Mongoid.clients['override_client'] = nil
548
+ end
549
+
550
+ it 'uses the overridden client for create' do
551
+ Minim.create!
552
+
553
+ expect(override_subscriber.single_command_started_event('insert').database_name).to eq('default_override_database')
554
+ expect(default_subscriber.command_started_events('insert')).to be_empty
555
+ end
556
+
557
+ it 'uses the overridden client for queries' do
558
+ Minim.where(name: 'Dmitry').to_a
559
+
560
+ expect(override_subscriber.single_command_started_event('find').database_name).to eq('default_override_database')
561
+ expect(default_subscriber.command_started_events('find')).to be_empty
562
+ end
563
+
564
+ context 'when the client is set on the model level' do
565
+ let(:model_level_subscriber) do
566
+ Mrss::EventSubscriber.new
567
+ end
568
+
569
+ around(:example) do |example|
570
+ opts = Minim.storage_options
571
+ Minim.storage_options = Minim.storage_options.merge( { client: 'model_level_client' } )
572
+ Mongoid.clients['model_level_client'] = { hosts: SpecConfig.instance.addresses, database: 'model_level_database' }
573
+ Mongoid.client('model_level_client').subscribe(Mongo::Monitoring::COMMAND, override_subscriber)
574
+ example.run
575
+ Mongoid.client('model_level_client').unsubscribe(Mongo::Monitoring::COMMAND, override_subscriber)
576
+ Mongoid.clients['model_level_client'] = nil
577
+ Minim.storage_options = opts
578
+ end
579
+
580
+ # This behaviour is consistent with 8.x
581
+ it 'uses the overridden client for create' do
582
+ Minim.create!
583
+
584
+ expect(override_subscriber.single_command_started_event('insert').database_name).to eq('default_override_database')
585
+ expect(default_subscriber.command_started_events('insert')).to be_empty
586
+ expect(model_level_subscriber.command_started_events('insert')).to be_empty
587
+ end
588
+
589
+ # This behaviour is consistent with 8.x
590
+ it 'uses the overridden client for queries' do
591
+ Minim.where(name: 'Dmitry').to_a
592
+
593
+ expect(override_subscriber.single_command_started_event('find').database_name).to eq('default_override_database')
594
+ expect(default_subscriber.command_started_events('find')).to be_empty
595
+ expect(model_level_subscriber.command_started_events('find')).to be_empty
596
+ end
597
+ end
598
+ end
599
+
600
+ context 'when global database is overridden' do
601
+ before do
602
+ Mongoid.override_database('override_database')
603
+ Mongoid.client(:default).subscribe(Mongo::Monitoring::COMMAND, default_subscriber)
604
+ end
605
+
606
+ after do
607
+ Mongoid.client(:default).unsubscribe(Mongo::Monitoring::COMMAND, default_subscriber)
608
+ Mongoid.override_database(nil)
609
+ end
610
+
611
+ it 'uses the overridden database for create' do
612
+ Minim.create!
613
+
614
+ expect(default_subscriber.single_command_started_event('insert').database_name).to eq('override_database')
615
+ end
616
+
617
+ it 'uses the overridden database for queries' do
618
+ Minim.where(name: 'Dmitry').to_a
619
+
620
+ expect(default_subscriber.single_command_started_event('find').database_name).to eq('override_database')
621
+ end
622
+
623
+ context 'when the database is set on the model level' do
624
+ around(:example) do |example|
625
+ opts = Minim.storage_options
626
+ Minim.storage_options = Minim.storage_options.merge( { database: 'model_level_database' } )
627
+ Mongoid.clients['model_level_client'] = { hosts: SpecConfig.instance.addresses, database: 'model_level_database' }
628
+ Mongoid.client(:default).subscribe(Mongo::Monitoring::COMMAND, default_subscriber)
629
+ example.run
630
+ Mongoid.client(:default).unsubscribe(Mongo::Monitoring::COMMAND, default_subscriber)
631
+ Mongoid.clients['model_level_client'] = nil
632
+ Minim.storage_options = opts
633
+ end
634
+
635
+ # This behaviour is consistent with 8.x
636
+ it 'uses the overridden database for create' do
637
+ Minim.create!
638
+
639
+ expect(default_subscriber.single_command_started_event('insert').database_name).to eq('override_database')
640
+ end
641
+
642
+ it 'uses the overridden database for queries' do
643
+ Minim.where(name: 'Dmitry').to_a
644
+
645
+ expect(default_subscriber.single_command_started_event('find').database_name).to eq('override_database')
646
+ end
647
+ end
648
+ end
649
+ end
525
650
  end
@@ -1789,6 +1789,12 @@ describe Mongoid::Interceptable do
1789
1789
  context 'with around callbacks' do
1790
1790
  config_override :around_callbacks_for_embeds, true
1791
1791
 
1792
+ after do
1793
+ Mongoid::Threaded.stack('interceptable').clear
1794
+ end
1795
+
1796
+ let(:stack) { Mongoid::Threaded.stack('interceptable') }
1797
+
1792
1798
  let(:expected) do
1793
1799
  [
1794
1800
  [InterceptableSpec::CbCascadedChild, :before_validation],
@@ -1824,6 +1830,12 @@ describe Mongoid::Interceptable do
1824
1830
  parent.save!
1825
1831
  expect(registry.calls).to eq expected
1826
1832
  end
1833
+
1834
+ it 'shows that cascaded callbacks can access Mongoid state' do
1835
+ expect(stack).to be_empty
1836
+ parent.save!
1837
+ expect(stack).not_to be_empty
1838
+ end
1827
1839
  end
1828
1840
 
1829
1841
  context 'without around callbacks' do
@@ -224,7 +224,19 @@ module InterceptableSpec
224
224
 
225
225
  attr_accessor :callback_registry
226
226
 
227
+ before_save :test_mongoid_state
228
+
227
229
  include CallbackTracking
230
+
231
+ private
232
+
233
+ # Helps test that cascading child callbacks have access to the Mongoid
234
+ # state objects; if the implementation uses fiber-local (instead of truly
235
+ # thread-local) variables, the related tests will fail because the
236
+ # cascading child callbacks use fibers to linearize the recursion.
237
+ def test_mongoid_state
238
+ Mongoid::Threaded.stack('interceptable').push(self)
239
+ end
228
240
  end
229
241
  end
230
242
 
@@ -536,6 +536,20 @@ describe Mongoid::PersistenceContext do
536
536
  end
537
537
  end
538
538
  end
539
+
540
+ context 'when the database is specified as a proc' do
541
+ let(:options) { { database: ->{ 'other' } } }
542
+
543
+ after { persistence_context.client.close }
544
+
545
+ it 'evaluates the proc' do
546
+ expect(persistence_context.database_name).to eq(:other)
547
+ end
548
+
549
+ it 'does not pass the proc to the client' do
550
+ expect(persistence_context.client.database.name).to eq('other')
551
+ end
552
+ end
539
553
  end
540
554
 
541
555
  describe '#client' do
@@ -608,6 +622,23 @@ describe Mongoid::PersistenceContext do
608
622
  end
609
623
  end
610
624
 
625
+ context 'when the client is set as a proc in the storage options' do
626
+ let(:options) { {} }
627
+
628
+ before do
629
+ Band.store_in client: ->{ :alternative }
630
+ end
631
+
632
+ after do
633
+ persistence_context.client.close
634
+ Band.store_in client: nil
635
+ end
636
+
637
+ it 'uses the client option' do
638
+ expect(persistence_context.client).to eq(Mongoid::Clients.with_name(:alternative))
639
+ end
640
+ end
641
+
611
642
  context 'when there is no client option set' do
612
643
 
613
644
  let(:options) do
@@ -2,23 +2,29 @@
2
2
  # rubocop:todo all
3
3
 
4
4
  require 'spec_helper'
5
- require 'active_job'
6
- require 'mongoid/railties/bson_object_id_serializer'
7
5
 
8
- describe 'Mongoid::Railties::ActiveJobSerializers::BsonObjectIdSerializer' do
6
+ begin
7
+ require 'active_job'
8
+ require 'mongoid/railties/bson_object_id_serializer'
9
9
 
10
- let(:serializer) { Mongoid::Railties::ActiveJobSerializers::BsonObjectIdSerializer.instance }
11
- let(:object_id) { BSON::ObjectId.new }
12
10
 
13
- describe '#serialize' do
14
- it 'serializes BSON::ObjectId' do
15
- expect(serializer.serialize(object_id)).to be_a(String)
11
+ describe 'Mongoid::Railties::ActiveJobSerializers::BsonObjectIdSerializer' do
12
+
13
+ let(:serializer) { Mongoid::Railties::ActiveJobSerializers::BsonObjectIdSerializer.instance }
14
+ let(:object_id) { BSON::ObjectId.new }
15
+
16
+ describe '#serialize' do
17
+ it 'serializes BSON::ObjectId' do
18
+ expect(serializer.serialize(object_id)).to be_a(String)
19
+ end
16
20
  end
17
- end
18
21
 
19
- describe '#deserialize' do
20
- it 'deserializes BSON::ObjectId' do
21
- expect(serializer.deserialize(serializer.serialize(object_id))).to eq(object_id)
22
+ describe '#deserialize' do
23
+ it 'deserializes BSON::ObjectId' do
24
+ expect(serializer.deserialize(serializer.serialize(object_id))).to eq(object_id)
25
+ end
22
26
  end
23
27
  end
28
+ rescue LoadError
29
+ RSpec.context.skip 'This test requires active_job'
24
30
  end
@@ -36,11 +36,11 @@ describe Mongoid::Threaded do
36
36
  context "when the stack has elements" do
37
37
 
38
38
  before do
39
- Thread.current["[mongoid]:load-stack"] = [ true ]
39
+ described_class.stack('load').push(true)
40
40
  end
41
41
 
42
42
  after do
43
- Thread.current["[mongoid]:load-stack"] = []
43
+ described_class.stack('load').clear
44
44
  end
45
45
 
46
46
  it "returns true" do
@@ -51,7 +51,7 @@ describe Mongoid::Threaded do
51
51
  context "when the stack has no elements" do
52
52
 
53
53
  before do
54
- Thread.current["[mongoid]:load-stack"] = []
54
+ described_class.stack('load').clear
55
55
  end
56
56
 
57
57
  it "returns false" do
@@ -76,7 +76,7 @@ describe Mongoid::Threaded do
76
76
  context "when a stack has been initialized" do
77
77
 
78
78
  before do
79
- Thread.current["[mongoid]:load-stack"] = [ true ]
79
+ described_class.stack('load').push(true)
80
80
  end
81
81
 
82
82
  let(:loading) do
@@ -84,7 +84,7 @@ describe Mongoid::Threaded do
84
84
  end
85
85
 
86
86
  after do
87
- Thread.current["[mongoid]:load-stack"] = []
87
+ described_class.stack('load').clear
88
88
  end
89
89
 
90
90
  it "returns the stack" do
@@ -341,4 +341,23 @@ describe Mongoid::Threaded do
341
341
  end
342
342
  end
343
343
  end
344
+
345
+ describe '#clear_modified_documents' do
346
+ let(:session) do
347
+ double(Mongo::Session).tap do |session|
348
+ allow(session).to receive(:in_transaction?).and_return(true)
349
+ end
350
+ end
351
+
352
+ context 'when there are modified documents' do
353
+ before do
354
+ described_class.add_modified_document(session, Minim.new)
355
+ described_class.clear_modified_documents(session)
356
+ end
357
+
358
+ it 'removes the documents and keys' do
359
+ expect(described_class.modified_documents).to be_empty
360
+ end
361
+ end
362
+ end
344
363
  end
@@ -38,12 +38,18 @@ describe Mongoid::Validatable::AssociatedValidator do
38
38
  User.new(name: "test")
39
39
  end
40
40
 
41
- let(:description) do
41
+ let(:description1) do
42
+ Description.new
43
+ end
44
+
45
+ let(:description2) do
42
46
  Description.new
43
47
  end
44
48
 
45
49
  before do
46
- user.descriptions << description
50
+ user.descriptions << description1
51
+ user.descriptions << description2
52
+ user.valid?
47
53
  end
48
54
 
49
55
  it "only validates the parent once" do
@@ -51,12 +57,16 @@ describe Mongoid::Validatable::AssociatedValidator do
51
57
  end
52
58
 
53
59
  it "adds the errors from the relation" do
54
- user.valid?
55
60
  expect(user.errors[:descriptions]).to_not be_nil
56
61
  end
57
62
 
63
+ it 'reports all failed validations' do
64
+ errors = user.descriptions.flat_map { |d| d.errors[:details] }
65
+ expect(errors.length).to be == 2
66
+ end
67
+
58
68
  it "only validates the child once" do
59
- expect(description).to_not be_valid
69
+ expect(description1).to_not be_valid
60
70
  end
61
71
  end
62
72
 
@@ -5,11 +5,11 @@ require "spec_helper"
5
5
  require "mongoid/railties/controller_runtime"
6
6
 
7
7
  describe "Mongoid::Railties::ControllerRuntime" do
8
- controller_runtime = Mongoid::Railties::ControllerRuntime
9
- collector = controller_runtime::Collector
8
+ CONTROLLER_RUNTIME = Mongoid::Railties::ControllerRuntime
9
+ COLLECTOR = CONTROLLER_RUNTIME::Collector
10
10
 
11
- def set_metric value
12
- Thread.current["Mongoid.controller_runtime"] = value
11
+ def set_metric(value)
12
+ Mongoid::Threaded.set(COLLECTOR::VARIABLE_NAME, value)
13
13
  end
14
14
 
15
15
  def clear_metric!
@@ -20,30 +20,30 @@ describe "Mongoid::Railties::ControllerRuntime" do
20
20
 
21
21
  it "stores the metric in thread-safe manner" do
22
22
  clear_metric!
23
- expect(collector.runtime).to eq(0)
23
+ expect(COLLECTOR.runtime).to eq(0)
24
24
  set_metric 42
25
- expect(collector.runtime).to eq(42)
25
+ expect(COLLECTOR.runtime).to eq(42)
26
26
  end
27
27
 
28
28
  it "sets metric on both succeeded and failed" do
29
- instance = collector.new
29
+ instance = COLLECTOR.new
30
30
  event_payload = OpenStruct.new duration: 42
31
31
 
32
32
  clear_metric!
33
33
  instance.succeeded event_payload
34
- expect(collector.runtime).to eq(42000)
34
+ expect(COLLECTOR.runtime).to eq(42000)
35
35
 
36
36
  clear_metric!
37
37
  instance.failed event_payload
38
- expect(collector.runtime).to eq(42000)
38
+ expect(COLLECTOR.runtime).to eq(42000)
39
39
  end
40
40
 
41
41
  it "resets the metric and returns the value" do
42
42
  clear_metric!
43
- expect(collector.reset_runtime).to eq(0)
43
+ expect(COLLECTOR.reset_runtime).to eq(0)
44
44
  set_metric 42
45
- expect(collector.reset_runtime).to eq(42)
46
- expect(collector.runtime).to eq(0)
45
+ expect(COLLECTOR.reset_runtime).to eq(42)
46
+ expect(COLLECTOR.runtime).to eq(0)
47
47
  end
48
48
 
49
49
  end
@@ -67,7 +67,7 @@ describe "Mongoid::Railties::ControllerRuntime" do
67
67
  end
68
68
 
69
69
  controller_class = Class.new reference_controller_class do
70
- include controller_runtime::ControllerExtension
70
+ include CONTROLLER_RUNTIME::ControllerExtension
71
71
  end
72
72
 
73
73
  let(:controller){ controller_class.new }
@@ -75,7 +75,7 @@ describe "Mongoid::Railties::ControllerRuntime" do
75
75
  it "resets the metric before each action" do
76
76
  set_metric 42
77
77
  controller.send(:process_action, 'foo')
78
- expect(collector.runtime).to be(0)
78
+ expect(COLLECTOR.runtime).to be(0)
79
79
  expect(controller.instance_variable_get "@process_action").to be(true)
80
80
  end
81
81
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongoid
3
3
  version: !ruby/object:Gem::Version
4
- version: 9.0.2
4
+ version: 9.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - The MongoDB Ruby Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-09-20 00:00:00.000000000 Z
11
+ date: 2024-11-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -19,7 +19,7 @@ dependencies:
19
19
  version: '5.1'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '7.3'
22
+ version: '8.1'
23
23
  - - "!="
24
24
  - !ruby/object:Gem::Version
25
25
  version: 7.0.0
@@ -32,7 +32,7 @@ dependencies:
32
32
  version: '5.1'
33
33
  - - "<"
34
34
  - !ruby/object:Gem::Version
35
- version: '7.3'
35
+ version: '8.1'
36
36
  - - "!="
37
37
  - !ruby/object:Gem::Version
38
38
  version: 7.0.0