mongoid 9.0.2 → 9.0.3

Sign up to get free protection for your applications and to get access to all the features.
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