mongoid 6.3.0 → 6.4.5

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.
Files changed (87) hide show
  1. checksums.yaml +5 -5
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/Rakefile +12 -0
  5. data/lib/config/locales/en.yml +21 -0
  6. data/lib/mongoid.rb +2 -2
  7. data/lib/mongoid/clients.rb +2 -0
  8. data/lib/mongoid/clients/sessions.rb +113 -0
  9. data/lib/mongoid/clients/storage_options.rb +1 -0
  10. data/lib/mongoid/contextual/aggregable/mongo.rb +1 -1
  11. data/lib/mongoid/contextual/map_reduce.rb +7 -3
  12. data/lib/mongoid/contextual/memory.rb +7 -2
  13. data/lib/mongoid/contextual/mongo.rb +11 -2
  14. data/lib/mongoid/criteria.rb +1 -0
  15. data/lib/mongoid/criteria/modifiable.rb +12 -2
  16. data/lib/mongoid/criteria/queryable/mergeable.rb +3 -1
  17. data/lib/mongoid/criteria/queryable/selectable.rb +34 -7
  18. data/lib/mongoid/document.rb +4 -4
  19. data/lib/mongoid/errors.rb +1 -0
  20. data/lib/mongoid/errors/invalid_session_use.rb +24 -0
  21. data/lib/mongoid/extensions/big_decimal.rb +1 -1
  22. data/lib/mongoid/extensions/regexp.rb +1 -0
  23. data/lib/mongoid/extensions/string.rb +3 -1
  24. data/lib/mongoid/indexable.rb +4 -4
  25. data/lib/mongoid/matchable.rb +3 -0
  26. data/lib/mongoid/matchable/nor.rb +37 -0
  27. data/lib/mongoid/persistable.rb +1 -1
  28. data/lib/mongoid/persistable/creatable.rb +4 -2
  29. data/lib/mongoid/persistable/deletable.rb +4 -2
  30. data/lib/mongoid/persistable/destroyable.rb +1 -5
  31. data/lib/mongoid/persistable/settable.rb +5 -5
  32. data/lib/mongoid/persistable/updatable.rb +2 -2
  33. data/lib/mongoid/persistable/upsertable.rb +2 -1
  34. data/lib/mongoid/persistence_context.rb +4 -0
  35. data/lib/mongoid/railtie.rb +17 -0
  36. data/lib/mongoid/railties/controller_runtime.rb +86 -0
  37. data/lib/mongoid/relations/embedded/batchable.rb +10 -4
  38. data/lib/mongoid/relations/embedded/many.rb +23 -0
  39. data/lib/mongoid/relations/many.rb +4 -0
  40. data/lib/mongoid/relations/referenced/many.rb +1 -1
  41. data/lib/mongoid/relations/touchable.rb +1 -1
  42. data/lib/mongoid/reloadable.rb +1 -1
  43. data/lib/mongoid/scopable.rb +3 -3
  44. data/lib/mongoid/tasks/database.rb +3 -2
  45. data/lib/mongoid/threaded.rb +74 -0
  46. data/lib/mongoid/version.rb +1 -1
  47. data/lib/rails/generators/mongoid/config/templates/mongoid.yml +4 -0
  48. data/spec/app/models/array_field.rb +7 -0
  49. data/spec/app/models/delegating_patient.rb +16 -0
  50. data/spec/integration/document_spec.rb +22 -0
  51. data/spec/mongoid/attributes/nested_spec.rb +4 -0
  52. data/spec/mongoid/clients/factory_spec.rb +52 -28
  53. data/spec/mongoid/clients/options_spec.rb +30 -15
  54. data/spec/mongoid/clients/sessions_spec.rb +334 -0
  55. data/spec/mongoid/contextual/geo_near_spec.rb +1 -0
  56. data/spec/mongoid/contextual/mongo_spec.rb +40 -2
  57. data/spec/mongoid/criteria/modifiable_spec.rb +59 -10
  58. data/spec/mongoid/criteria/queryable/extensions/big_decimal_spec.rb +3 -3
  59. data/spec/mongoid/criteria/queryable/selectable_spec.rb +74 -6
  60. data/spec/mongoid/criteria/queryable/selector_spec.rb +2 -2
  61. data/spec/mongoid/criteria/scopable_spec.rb +81 -0
  62. data/spec/mongoid/criteria_spec.rb +4 -1
  63. data/spec/mongoid/document_spec.rb +54 -0
  64. data/spec/mongoid/extensions/big_decimal_spec.rb +9 -9
  65. data/spec/mongoid/extensions/regexp_spec.rb +23 -0
  66. data/spec/mongoid/extensions/string_spec.rb +35 -7
  67. data/spec/mongoid/fields_spec.rb +1 -1
  68. data/spec/mongoid/findable_spec.rb +1 -1
  69. data/spec/mongoid/interceptable_spec.rb +1 -1
  70. data/spec/mongoid/matchable/nor_spec.rb +209 -0
  71. data/spec/mongoid/matchable_spec.rb +26 -1
  72. data/spec/mongoid/persistable/deletable_spec.rb +19 -0
  73. data/spec/mongoid/persistable/destroyable_spec.rb +19 -0
  74. data/spec/mongoid/persistable/incrementable_spec.rb +6 -6
  75. data/spec/mongoid/persistable/settable_spec.rb +35 -1
  76. data/spec/mongoid/persistable_spec.rb +16 -16
  77. data/spec/mongoid/relations/embedded/many_spec.rb +246 -16
  78. data/spec/mongoid/scopable_spec.rb +13 -0
  79. data/spec/mongoid/threaded_spec.rb +68 -0
  80. data/spec/rails/controller_extension/controller_runtime_spec.rb +110 -0
  81. data/spec/spec_helper.rb +79 -0
  82. data/spec/support/cluster_config.rb +158 -0
  83. data/spec/support/constraints.rb +101 -0
  84. data/spec/support/macros.rb +20 -0
  85. data/spec/support/spec_config.rb +42 -0
  86. metadata +471 -443
  87. metadata.gz.sig +0 -0
@@ -190,11 +190,11 @@ module Mongoid
190
190
  #
191
191
  # @since 5.1.0
192
192
  def as_json(options = nil)
193
- if options && (options[:compact] == true)
194
- super(options).reject! { |k,v| v.nil? }
195
- else
196
- super(options)
193
+ rv = super
194
+ if options && options[:compact]
195
+ rv = rv.compact
197
196
  end
197
+ rv
198
198
  end
199
199
 
200
200
  # Returns an instance of the specified class with the attributes,
@@ -18,6 +18,7 @@ require "mongoid/errors/invalid_path"
18
18
  require "mongoid/errors/invalid_persistence_option"
19
19
  require "mongoid/errors/invalid_relation"
20
20
  require "mongoid/errors/invalid_scope"
21
+ require "mongoid/errors/invalid_session_use"
21
22
  require "mongoid/errors/invalid_set_polymorphic_relation"
22
23
  require "mongoid/errors/invalid_storage_options"
23
24
  require "mongoid/errors/invalid_storage_parent"
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+ module Mongoid
3
+ module Errors
4
+
5
+ # This error is raised when a session is attempted to be used with a model whose client cannot use it, if
6
+ # sessions are nested, or if the mongodb deployment doesn't support sessions.
7
+ #
8
+ # @since 6.4.0
9
+ class InvalidSessionUse < MongoidError
10
+
11
+ # Create the error.
12
+ #
13
+ # @example Create the error.
14
+ # InvalidSessionUse.new(:invalid_session_use)
15
+ #
16
+ # @param [ :invalid_sesion_use, :invalid_session_nesting ] error_type The type of session misuse.
17
+ #
18
+ # @since 6.4.0
19
+ def initialize(error_type)
20
+ super(compose_message(error_type.to_s))
21
+ end
22
+ end
23
+ end
24
+ end
@@ -53,7 +53,7 @@ module Mongoid
53
53
  #
54
54
  # @since 3.0.0
55
55
  def demongoize(object)
56
- object && object.numeric? ? ::BigDecimal.new(object.to_s) : nil
56
+ object && object.numeric? ? BigDecimal(object.to_s) : nil
57
57
  end
58
58
 
59
59
  # Mongoize an object of any type to how it's stored in the db as a String.
@@ -17,6 +17,7 @@ module Mongoid
17
17
  #
18
18
  # @since 3.0.0
19
19
  def mongoize(object)
20
+ return nil if object.nil?
20
21
  ::Regexp.new(object)
21
22
  end
22
23
  end
@@ -82,7 +82,9 @@ module Mongoid
82
82
  #
83
83
  # @since 3.0.0
84
84
  def numeric?
85
- true if Float(self) rescue (self =~ /^NaN|\-?Infinity$/)
85
+ !!Float(self)
86
+ rescue ArgumentError
87
+ (self =~ /\A(?:NaN|-?Infinity)\z/) == 0
86
88
  end
87
89
 
88
90
  # Get the string as a getter string.
@@ -32,10 +32,10 @@ module Mongoid
32
32
  key, options = spec.key, spec.options
33
33
  if database = options[:database]
34
34
  with(database: database) do |klass|
35
- klass.collection.indexes.create_one(key, options.except(:database))
35
+ klass.collection.indexes(session: _session).create_one(key, options.except(:database))
36
36
  end
37
37
  else
38
- collection.indexes.create_one(key, options)
38
+ collection.indexes(session: _session).create_one(key, options)
39
39
  end
40
40
  end and true
41
41
  end
@@ -53,9 +53,9 @@ module Mongoid
53
53
  indexed_database_names.each do |database|
54
54
  with(database: database) do |klass|
55
55
  begin
56
- klass.collection.indexes.each do |spec|
56
+ klass.collection.indexes(session: _session).each do |spec|
57
57
  unless spec["name"] == "_id_"
58
- klass.collection.indexes.drop_one(spec["key"])
58
+ klass.collection.indexes(session: _session).drop_one(spec["key"])
59
59
  logger.info(
60
60
  "MONGOID: Removed index '#{spec["name"]}' on collection " +
61
61
  "'#{klass.collection.name}' in database '#{database}'."
@@ -11,6 +11,7 @@ require "mongoid/matchable/lte"
11
11
  require "mongoid/matchable/ne"
12
12
  require "mongoid/matchable/nin"
13
13
  require "mongoid/matchable/or"
14
+ require "mongoid/matchable/nor"
14
15
  require "mongoid/matchable/size"
15
16
  require "mongoid/matchable/elem_match"
16
17
  require "mongoid/matchable/regexp"
@@ -40,6 +41,7 @@ module Mongoid
40
41
  "$ne" => Ne,
41
42
  "$nin" => Nin,
42
43
  "$or" => Or,
44
+ "$nor" => Nor,
43
45
  "$size" => Size
44
46
  }.with_indifferent_access.freeze
45
47
 
@@ -124,6 +126,7 @@ module Mongoid
124
126
  case key.to_s
125
127
  when "$or" then Or.new(value, document)
126
128
  when "$and" then And.new(value, document)
129
+ when "$nor" then Nor.new(value, document)
127
130
  else Default.new(extract_attribute(document, key))
128
131
  end
129
132
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+ # encoding: utf-8
3
+ module Mongoid
4
+ module Matchable
5
+
6
+ # Defines behavior for handling $nor expressions in embedded documents.
7
+ class Nor < Default
8
+
9
+ # Does the supplied query match the attribute?
10
+ #
11
+ # Note: an empty array as an argument to $nor is prohibited by
12
+ # MongoDB server. Mongoid does allow this and returns false in this case.
13
+ #
14
+ # @example Does this match?
15
+ # matcher._matches?("$nor" => [ { field => value } ])
16
+ #
17
+ # @param [ Array ] conditions The or expression.
18
+ #
19
+ # @return [ true, false ] True if matches, false if not.
20
+ #
21
+ # @since 6.4.2/7.0.2/7.1.0
22
+ def _matches?(conditions)
23
+ if conditions.length == 0
24
+ # MongoDB does not allow $nor array to be empty, but
25
+ # Mongoid accepts an empty array for $or which MongoDB also
26
+ # prohibits
27
+ return false
28
+ end
29
+ conditions.none? do |condition|
30
+ condition.all? do |key, value|
31
+ document._matches?(key => value)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -204,7 +204,7 @@ module Mongoid
204
204
  def persist_atomic_operations(operations)
205
205
  if persisted? && operations
206
206
  selector = atomic_selector
207
- _root.collection.find(selector).update_one(positionally(selector, operations))
207
+ _root.collection.find(selector).update_one(positionally(selector, operations), session: _session)
208
208
  end
209
209
  end
210
210
  end
@@ -61,7 +61,9 @@ module Mongoid
61
61
  _parent.insert
62
62
  else
63
63
  selector = _parent.atomic_selector
64
- _root.collection.find(selector).update_one(positionally(selector, atomic_inserts))
64
+ _root.collection.find(selector).update_one(
65
+ positionally(selector, atomic_inserts),
66
+ session: _session)
65
67
  end
66
68
  end
67
69
 
@@ -76,7 +78,7 @@ module Mongoid
76
78
  #
77
79
  # @since 4.0.0
78
80
  def insert_as_root
79
- collection.insert_one(as_attributes)
81
+ collection.insert_one(as_attributes, session: _session)
80
82
  end
81
83
 
82
84
  # Post process an insert, which sets the new record attribute to false
@@ -62,7 +62,9 @@ module Mongoid
62
62
  _parent.remove_child(self) if notifying_parent?(options)
63
63
  if _parent.persisted?
64
64
  selector = _parent.atomic_selector
65
- _root.collection.find(selector).update_one(positionally(selector, atomic_deletes))
65
+ _root.collection.find(selector).update_one(
66
+ positionally(selector, atomic_deletes),
67
+ session: _session)
66
68
  end
67
69
  true
68
70
  end
@@ -78,7 +80,7 @@ module Mongoid
78
80
  #
79
81
  # @since 4.0.0
80
82
  def delete_as_root
81
- collection.find(atomic_selector).delete_one
83
+ collection.find(atomic_selector).delete_one(session: _session)
82
84
  true
83
85
  end
84
86
 
@@ -48,11 +48,7 @@ module Mongoid
48
48
  #
49
49
  # @since 1.0.0
50
50
  def destroy_all(conditions = nil)
51
- selector = conditions || {}
52
- documents = where(selector)
53
- destroyed = documents.count
54
- documents.each { |doc| doc.destroy }
55
- destroyed
51
+ where(conditions || {}).destroy
56
52
  end
57
53
  end
58
54
  end
@@ -25,15 +25,15 @@ module Mongoid
25
25
 
26
26
  field_and_value_hash = hasherizer(field.split('.'), value)
27
27
  field = field_and_value_hash.keys.first.to_s
28
+ value = field_and_value_hash[field]
28
29
 
29
- if fields[field] && fields[field].type == Hash && attributes.key?(field)
30
+ if fields[field] && fields[field].type == Hash && attributes.key?(field) && Hash === value && !value.empty?
30
31
  merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
31
- value = (attributes[field] || {}).merge(field_and_value_hash[field], &merger)
32
- process_attribute(field.to_s, value)
33
- else
34
- process_attribute(field.to_s, field_and_value_hash[field])
32
+ value = (attributes[field] || {}).merge(value, &merger)
35
33
  end
36
34
 
35
+ process_attribute(field.to_s, value)
36
+
37
37
  unless relations.include?(field.to_s)
38
38
  ops[atomic_attribute_name(field)] = attributes[field]
39
39
  end
@@ -134,9 +134,9 @@ module Mongoid
134
134
  unless updates.empty?
135
135
  coll = collection(_root)
136
136
  selector = atomic_selector
137
- coll.find(selector).update_one(positionally(selector, updates))
137
+ coll.find(selector).update_one(positionally(selector, updates), session: _session)
138
138
  conflicts.each_pair do |key, value|
139
- coll.find(selector).update_one(positionally(selector, { key => value }))
139
+ coll.find(selector).update_one(positionally(selector, { key => value }), session: _session)
140
140
  end
141
141
  end
142
142
  end
@@ -21,7 +21,8 @@ module Mongoid
21
21
  # @since 3.0.0
22
22
  def upsert(options = {})
23
23
  prepare_upsert(options) do
24
- collection.find(atomic_selector).update_one(as_attributes, upsert: true)
24
+ collection.find(atomic_selector).update_one(
25
+ as_attributes, upsert: true, session: _session)
25
26
  end
26
27
  end
27
28
 
@@ -107,6 +107,10 @@ module Mongoid
107
107
  #
108
108
  # @since 6.0.0
109
109
  def client
110
+ client_options = send(:client_options)
111
+ if client_options[:read].is_a?(Symbol)
112
+ client_options = client_options.merge(read: {mode: client_options[:read]})
113
+ end
110
114
  @client ||= (client = Clients.with_name(client_name)
111
115
  client = client.use(database_name) if database_name_option
112
116
  client.with(client_options))
@@ -102,6 +102,23 @@ module Rails
102
102
  puts "There is a configuration error with the current mongoid.yml."
103
103
  puts e.message
104
104
  end
105
+
106
+ # Include Controller extension that measures Mongoid runtime
107
+ # during request processing. The value then appears in Rails'
108
+ # instrumentation event `process_action.action_controller`.
109
+ #
110
+ # The measurement is made via internal Mongo monitoring subscription
111
+ initializer "mongoid.runtime-metric" do
112
+ require "mongoid/railties/controller_runtime"
113
+
114
+ ActiveSupport.on_load :action_controller do
115
+ include ::Mongoid::Railties::ControllerRuntime::ControllerExtension
116
+ end
117
+
118
+ Mongo::Monitoring::Global.subscribe Mongo::Monitoring::COMMAND,
119
+ ::Mongoid::Railties::ControllerRuntime::Collector.new
120
+ end
121
+
105
122
  end
106
123
  end
107
124
  end
@@ -0,0 +1,86 @@
1
+ module Mongoid
2
+ module Railties
3
+ module ControllerRuntime
4
+
5
+ # This extension mimics the Rails' internal method to
6
+ # measure ActiveRecord runtime during request processing.
7
+ # It appends MongoDB runtime value (`mongoid_runtime`) into payload
8
+ # of instrumentation event `process_action.action_controller`.
9
+ module ControllerExtension
10
+ extend ActiveSupport::Concern
11
+
12
+ protected
13
+
14
+ attr_internal :mongoid_runtime
15
+
16
+ # Reset the runtime before each action.
17
+ def process_action(action, *args)
18
+ Collector.reset_runtime
19
+ super
20
+ end
21
+
22
+ # Override to collect the measurements.
23
+ def cleanup_view_runtime
24
+ mongo_rt_before_render = Collector.reset_runtime
25
+ runtime = super
26
+ mongo_rt_after_render = Collector.reset_runtime
27
+ self.mongoid_runtime = mongo_rt_before_render + mongo_rt_after_render
28
+ runtime - mongo_rt_after_render
29
+ end
30
+
31
+ # Add the measurement to the instrumentation event payload.
32
+ def append_info_to_payload(payload)
33
+ super
34
+ payload[:mongoid_runtime] = (mongoid_runtime || 0) + Collector.reset_runtime
35
+ end
36
+
37
+ module ClassMethods
38
+
39
+ # Append MongoDB runtime information to ActionController runtime
40
+ # log message.
41
+ def log_process_action(payload)
42
+ messages = super
43
+ mongoid_runtime = payload[:mongoid_runtime]
44
+ messages << ("MongoDB: %.1fms" % mongoid_runtime.to_f) if mongoid_runtime
45
+ messages
46
+ end
47
+
48
+ end
49
+
50
+ end
51
+
52
+ # The Collector of MongoDB runtime metric, that subscribes to Mongo
53
+ # driver command monitoring. Stores the value within a thread-local
54
+ # variable to provide correct accounting when an application issues
55
+ # MongoDB operations from background threads.
56
+ class Collector
57
+
58
+ VARIABLE_NAME = "Mongoid.controller_runtime".freeze
59
+
60
+ def started _; end
61
+
62
+ def _completed e
63
+ Collector.runtime += e.duration
64
+ end
65
+ alias :succeeded :_completed
66
+ alias :failed :_completed
67
+
68
+ def self.runtime
69
+ Thread.current[VARIABLE_NAME] ||= 0
70
+ end
71
+
72
+ def self.runtime= value
73
+ Thread.current[VARIABLE_NAME] = value
74
+ end
75
+
76
+ def self.reset_runtime
77
+ to_now = runtime
78
+ self.runtime = 0
79
+ to_now
80
+ end
81
+
82
+ end
83
+
84
+ end
85
+ end
86
+ end
@@ -38,7 +38,8 @@ module Mongoid
38
38
  pre_process_batch_remove(docs, :delete)
39
39
  unless docs.empty?
40
40
  collection.find(selector).update_one(
41
- positionally(selector, "$unset" => { path => true })
41
+ positionally(selector, "$unset" => { path => true }),
42
+ session: _session
42
43
  )
43
44
  post_process_batch_remove(docs, :delete)
44
45
  end
@@ -58,7 +59,8 @@ module Mongoid
58
59
  removals = pre_process_batch_remove(docs, method)
59
60
  if !docs.empty?
60
61
  collection.find(selector).update_one(
61
- positionally(selector, "$pullAll" => { path => removals })
62
+ positionally(selector, "$pullAll" => { path => removals }),
63
+ session: _session
62
64
  )
63
65
  post_process_batch_remove(docs, method)
64
66
  end
@@ -133,7 +135,9 @@ module Mongoid
133
135
  inserts = pre_process_batch_insert(docs)
134
136
  if insertable?
135
137
  collection.find(selector).update_one(
136
- positionally(selector, '$set' => { path => inserts }))
138
+ positionally(selector, '$set' => { path => inserts }),
139
+ session: _session
140
+ )
137
141
  post_process_batch_insert(docs)
138
142
  end
139
143
  inserts
@@ -156,7 +160,9 @@ module Mongoid
156
160
  pushes = pre_process_batch_insert(docs)
157
161
  if insertable?
158
162
  collection.find(selector).update_one(
159
- positionally(selector, '$push' => { path => { '$each' => pushes } }))
163
+ positionally(selector, '$push' => { path => { '$each' => pushes } }),
164
+ session: _session
165
+ )
160
166
  post_process_batch_insert(docs)
161
167
  end
162
168
  pushes
@@ -294,6 +294,29 @@ module Mongoid
294
294
  end
295
295
  end
296
296
 
297
+ # Shift documents off the relation. This can be a single document or
298
+ # multiples, and will automatically persist the changes.
299
+ #
300
+ # @example Shift a single document.
301
+ # relation.shift
302
+ #
303
+ # @example Shift multiple documents.
304
+ # relation.shift(3)
305
+ #
306
+ # @param [ Integer ] count The number of documents to shift, or 1 if not
307
+ # provided.
308
+ #
309
+ # @return [ Document, Array<Document> ] The shifted document(s).
310
+ def shift(count = nil)
311
+ if count
312
+ if target.size > 0 && docs = target[0, count]
313
+ docs.each { |doc| delete(doc) }
314
+ end
315
+ else
316
+ delete(target[0])
317
+ end
318
+ end
319
+
297
320
  # Substitutes the supplied target documents for the existing documents
298
321
  # in the relation.
299
322
  #