mongoid 7.0.1 → 7.0.2

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 (39) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/lib/mongoid/association.rb +0 -1
  5. data/lib/mongoid/association/depending.rb +22 -1
  6. data/lib/mongoid/association/embedded/embeds_one/proxy.rb +3 -3
  7. data/lib/mongoid/association/referenced/has_many/proxy.rb +1 -1
  8. data/lib/mongoid/association/relatable.rb +16 -2
  9. data/lib/mongoid/attributes/nested.rb +14 -3
  10. data/lib/mongoid/contextual/map_reduce.rb +1 -1
  11. data/lib/mongoid/copyable.rb +3 -2
  12. data/lib/mongoid/document.rb +2 -0
  13. data/lib/mongoid/matchable.rb +3 -0
  14. data/lib/mongoid/matchable/nor.rb +37 -0
  15. data/lib/mongoid/persistable/settable.rb +58 -13
  16. data/lib/mongoid/persistence_context.rb +5 -1
  17. data/lib/mongoid/touchable.rb +102 -0
  18. data/lib/mongoid/version.rb +1 -1
  19. data/spec/app/models/array_field.rb +7 -0
  20. data/spec/app/models/updatable.rb +7 -0
  21. data/spec/mongoid/association/accessors_spec.rb +39 -0
  22. data/spec/mongoid/association/depending_spec.rb +253 -0
  23. data/spec/mongoid/association/polymorphic_spec.rb +59 -0
  24. data/spec/mongoid/association/referenced/belongs_to_spec.rb +3 -3
  25. data/spec/mongoid/association/referenced/has_one_spec.rb +59 -0
  26. data/spec/mongoid/attributes/nested_spec.rb +18 -2
  27. data/spec/mongoid/clients/factory_spec.rb +3 -3
  28. data/spec/mongoid/clients/options_spec.rb +28 -13
  29. data/spec/mongoid/clients/sessions_spec.rb +3 -3
  30. data/spec/mongoid/clients/transactions_spec.rb +369 -0
  31. data/spec/mongoid/copyable_spec.rb +16 -0
  32. data/spec/mongoid/matchable/nor_spec.rb +209 -0
  33. data/spec/mongoid/matchable_spec.rb +26 -1
  34. data/spec/mongoid/persistable/settable_spec.rb +128 -9
  35. data/spec/mongoid/{association/touchable_spec.rb → touchable_spec.rb} +28 -7
  36. data/spec/spec_helper.rb +8 -0
  37. metadata +457 -448
  38. metadata.gz.sig +0 -0
  39. data/lib/mongoid/association/touchable.rb +0 -97
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 24a3c4aba84097db1be0810c29d175dfa6e69f6346efa857ac124aeb77bec1b1
4
- data.tar.gz: b181a4840f966322895bf4dfe0b64cb136513412d8a3ca89b9aa28a342d27054
3
+ metadata.gz: 0e3c2f9aa40e2f20c7620a2ae7684da4ebfc1699e32357dd4aa663d93b7ade33
4
+ data.tar.gz: 9f0d2145026c3db222638b7d00d842a690294cc6101a582a14a75607c44474a1
5
5
  SHA512:
6
- metadata.gz: 12979183bf4d077b673beb7a57632fc73b7f1672be666401c68969ae675164b0d0641881cdcb8fb43bec283c4b4ca4d946f63a0c988bb58ac4065e677a5278c6
7
- data.tar.gz: 3ae9210672c5c5dc3f8536983e814840fc823668373c9594d9a7cb834b533e945eca850c5840dbf0f90e7f707318c0a16823c0a54599d29b5f3a6308f5bf2dd1
6
+ metadata.gz: 0f9b11ae060df18208b48b911ee39c2954a7855ac7ffc19b073aa21a707e51805bb465b73b7634abb425d983b3d57fbbab044a202853e532c0fc8331141b9734
7
+ data.tar.gz: 24d37b3c3b77d110d45ec6df6209e8cf4a7821fb787c4d369cd87f01bcf66db0292e101928f8ab2a102f5020c6bb4d7b4b19b13fc2c7765096f5f409251825ff
Binary file
data.tar.gz.sig CHANGED
Binary file
@@ -3,7 +3,6 @@ require 'mongoid/association/builders'
3
3
  require 'mongoid/association/bindable'
4
4
  require 'mongoid/association/depending'
5
5
  require 'mongoid/association/proxy'
6
- require 'mongoid/association/touchable'
7
6
 
8
7
  require 'mongoid/association/many'
9
8
  require 'mongoid/association/one'
@@ -8,7 +8,23 @@ module Mongoid
8
8
 
9
9
  included do
10
10
  class_attribute :dependents
11
+
12
+ # @api private
13
+ class_attribute :dependents_owner
14
+
11
15
  self.dependents = []
16
+ self.dependents_owner = self
17
+ end
18
+
19
+ class_methods do
20
+ # @api private
21
+ def _all_dependents
22
+ superclass_dependents = superclass.respond_to?(:_all_dependents) ? superclass._all_dependents : []
23
+ dependents + superclass_dependents.reject do |new_dep|
24
+ dependents.any? do |old_dep| old_dep.name == new_dep.name
25
+ end
26
+ end
27
+ end
12
28
  end
13
29
 
14
30
  # The valid dependent strategies.
@@ -41,6 +57,11 @@ module Mongoid
41
57
  def self.define_dependency!(association)
42
58
  validate!(association)
43
59
  association.inverse_class.tap do |klass|
60
+ if klass.dependents_owner != klass
61
+ klass.dependents = []
62
+ klass.dependents_owner = klass
63
+ end
64
+
44
65
  if association.dependent && !klass.dependents.include?(association)
45
66
  klass.dependents.push(association)
46
67
  end
@@ -63,7 +84,7 @@ module Mongoid
63
84
  #
64
85
  # @since 2.0.0.rc.1
65
86
  def apply_delete_dependencies!
66
- dependents.each do |association|
87
+ self.class._all_dependents.each do |association|
67
88
  if association.try(:dependent)
68
89
  send("_dependent_#{association.dependent}!", association)
69
90
  end
@@ -52,9 +52,9 @@ module Mongoid
52
52
  if _assigning?
53
53
  _base.add_atomic_unset(_target) unless replacement
54
54
  else
55
- # The associated object will be replaced by the below update, so only
56
- # run the callbacks and state-changing code by passing persist: false.
57
- _target.destroy(persist: false) if persistable?
55
+ # The associated object will be replaced by the below update if non-nil, so only
56
+ # run the callbacks and state-changing code by passing persist: false in that case.
57
+ _target.destroy(persist: !replacement) if persistable?
58
58
  end
59
59
  unbind_one
60
60
  return nil unless replacement
@@ -459,7 +459,7 @@ module Mongoid
459
459
  collection.insert_many(inserts, session: _session)
460
460
  docs.each do |doc|
461
461
  doc.new_record = false
462
- doc.run_after_callbacks(:create, :save)
462
+ doc.run_after_callbacks(:create, :save) unless _association.autosave?
463
463
  doc.post_persist
464
464
  end
465
465
  end
@@ -57,6 +57,10 @@ module Mongoid
57
57
  @name = name
58
58
  @options = opts
59
59
  @extension = nil
60
+
61
+ @module_path = _class.name ? _class.name.split('::')[0..-2].join('::') : ''
62
+ @module_path << '::' unless @module_path.empty?
63
+
60
64
  create_extension!(&block)
61
65
  validate!
62
66
  end
@@ -147,7 +151,7 @@ module Mongoid
147
151
  #
148
152
  # @since 7.0
149
153
  def relation_class_name
150
- @class_name ||= @options[:class_name] || ActiveSupport::Inflector.classify(name)
154
+ @class_name ||= @options[:class_name] || ActiveSupport::Inflector.classify(name_with_module)
151
155
  end
152
156
  alias :class_name :relation_class_name
153
157
 
@@ -321,13 +325,23 @@ module Mongoid
321
325
 
322
326
  private
323
327
 
328
+ def name_with_module
329
+ @module_path + name.to_s.capitalize
330
+ end
331
+
332
+ # Gets the model classes with inverse associations of this model. This is used to determine
333
+ # the classes on the other end of polymorphic relations with models.
334
+ def inverse_association_classes
335
+ Mongoid::Config.models.map { |m| inverse_association(m) }.compact.map(&:inverse_class)
336
+ end
337
+
324
338
  def setup_index!
325
339
  @owner_class.index(index_spec, background: true) if indexed?
326
340
  end
327
341
 
328
342
  def define_touchable!
329
343
  if touchable?
330
- Association::Touchable.define_touchable!(self)
344
+ Touchable.define_touchable!(self)
331
345
  end
332
346
  end
333
347
 
@@ -42,15 +42,21 @@ module Mongoid
42
42
  # to a class method to reject documents with.
43
43
  # @option *args [ Integer ] :limit The max number to create.
44
44
  # @option *args [ true, false ] :update_only Only update existing docs.
45
+ # @options *args [ true, false ] :autosave Whether autosave should be enabled on the
46
+ # association. Note that since the default is true, setting autosave to nil will still
47
+ # enable it.
45
48
  def accepts_nested_attributes_for(*args)
46
- options = args.extract_options!
49
+ options = args.extract_options!.dup
50
+ options[:autosave] = true if options[:autosave].nil?
51
+
47
52
  options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
48
53
  args.each do |name|
49
54
  meth = "#{name}_attributes="
50
55
  self.nested_attributes["#{name}_attributes"] = meth
51
56
  association = relations[name.to_s]
52
57
  raise Errors::NestedAttributesMetadataNotFound.new(self, name) unless association
53
- autosave_nested_attributes(association)
58
+ autosave_nested_attributes(association) if options[:autosave]
59
+
54
60
  re_define_method(meth) do |attrs|
55
61
  _assigning do
56
62
  if association.polymorphic? and association.inverse_type
@@ -75,7 +81,12 @@ module Mongoid
75
81
  #
76
82
  # @since 3.1.4
77
83
  def autosave_nested_attributes(association)
78
- if association.autosave?
84
+ # In order for the autosave functionality to work properly, the association needs to be
85
+ # marked as autosave despite the fact that the option isn't present. Because the method
86
+ # Association#autosave? is implemented by checking the autosave option, this is the most
87
+ # straightforward way to mark it.
88
+ if association.autosave? || (association.options[:autosave].nil? && !association.embedded?)
89
+ association.options[:autosave] = true
79
90
  Association::Referenced::AutoSave.define_autosave!(association)
80
91
  end
81
92
  end
@@ -165,7 +165,7 @@ module Mongoid
165
165
  def raw
166
166
  validate_out!
167
167
  cmd = command
168
- opts = { read: cmd.delete(:read).options } if cmd[:read]
168
+ opts = { read: cmd.delete(:read) } if cmd[:read]
169
169
  @map_reduce.database.command(cmd, (opts || {}).merge(session: _session)).first
170
170
  end
171
171
  alias :results :raw
@@ -73,8 +73,9 @@ module Mongoid
73
73
  next unless attrs.present? && attrs[association.key].present?
74
74
 
75
75
  if association.is_a?(Association::Embedded::EmbedsMany)
76
- attrs[association.name.to_s].each_with_index do |attr, index|
77
- process_localized_attributes(send(association.name)[index].class, attr)
76
+ attrs[association.name.to_s].each do |attr|
77
+ embedded_klass = attr.fetch('_type', association.class_name).constantize
78
+ process_localized_attributes(embedded_klass, attr)
78
79
  end
79
80
  else
80
81
  process_localized_attributes(association.klass, attrs[association.key])
@@ -15,6 +15,7 @@ require "mongoid/fields"
15
15
  require "mongoid/timestamps"
16
16
  require "mongoid/association"
17
17
  require "mongoid/composable"
18
+ require "mongoid/touchable"
18
19
 
19
20
  module Mongoid
20
21
 
@@ -23,6 +24,7 @@ module Mongoid
23
24
  module Document
24
25
  extend ActiveSupport::Concern
25
26
  include Composable
27
+ include Mongoid::Touchable::InstanceMethods
26
28
 
27
29
  attr_accessor :__selected_fields
28
30
  attr_reader :new_record
@@ -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
@@ -14,6 +14,34 @@ module Mongoid
14
14
  # @example Set the values.
15
15
  # document.set(title: "sir", dob: Date.new(1970, 1, 1))
16
16
  #
17
+ # The key can be a dotted sequence of keys, in which case the
18
+ # top level field is treated as a nested hash and any missing keys
19
+ # are created automatically:
20
+ #
21
+ # @example Set the values using nested hash semantics.
22
+ # document.set('author.title' => 'Sir')
23
+ # # => document.author == {'title' => 'Sir'}
24
+ #
25
+ # Performing a nested set like this merges values of intermediate keys:
26
+ #
27
+ # @example Nested hash value merging.
28
+ # document.set('author.title' => 'Sir')
29
+ # document.set('author.name' => 'Linus Torvalds')
30
+ # # => document.author == {'title' => 'Sir', 'name' => 'Linus Torvalds'}
31
+ #
32
+ # If the top level field was not a hash, its original value is discarded
33
+ # and the field is replaced with a hash.
34
+ #
35
+ # @example Nested hash overwriting a non-hash value.
36
+ # document.set('author' => 'John Doe')
37
+ # document.set('author.title' => 'Sir')
38
+ # # => document.author == {'title' => 'Sir'}
39
+ #
40
+ # Note that unlike MongoDB's $set, Mongoid's set writes out the entire
41
+ # field even when setting a subset of the field via the nested hash
42
+ # semantics. This means performing a $set with nested hash semantics
43
+ # can overwrite other hash keys within the top level field in the database.
44
+ #
17
45
  # @param [ Hash ] setters The field/value pairs to set.
18
46
  #
19
47
  # @return [ Document ] The document.
@@ -23,17 +51,39 @@ module Mongoid
23
51
  prepare_atomic_operation do |ops|
24
52
  process_atomic_operations(setters) do |field, value|
25
53
 
26
- field_and_value_hash = hasherizer(field.split('.'), value)
27
- field = field_and_value_hash.keys.first.to_s
54
+ field_seq = field.to_s.split('.')
55
+ field = field_seq.shift
56
+ if field_seq.length > 0
57
+ # nested hash path
58
+ old_value = attributes[field]
28
59
 
29
- if fields[field] && fields[field].type == Hash && attributes.key?(field)
30
- 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])
60
+ # if the old value is not a hash, clobber it
61
+ unless Hash === old_value
62
+ old_value = {}
63
+ end
64
+
65
+ # descend into the hash, creating intermediate keys as needed
66
+ cur_value = old_value
67
+ while field_seq.length > 1
68
+ cur_key = field_seq.shift
69
+ # clobber on each level if type is not a hash
70
+ unless Hash === cur_value[cur_key]
71
+ cur_value[cur_key] = {}
72
+ end
73
+ cur_value = cur_value[cur_key]
74
+ end
75
+
76
+ # now we are on the leaf level, perform the set
77
+ # and overwrite whatever was on this level before
78
+ cur_value[field_seq.shift] = value
79
+
80
+ # and set value to the value of the top level field
81
+ # because this is what we pass to $set
82
+ value = old_value
35
83
  end
36
84
 
85
+ process_attribute(field, value)
86
+
37
87
  unless relations.include?(field.to_s)
38
88
  ops[atomic_attribute_name(field)] = attributes[field]
39
89
  end
@@ -42,10 +92,5 @@ module Mongoid
42
92
  end
43
93
  end
44
94
  end
45
-
46
- def hasherizer(keys, value)
47
- return value if keys.empty?
48
- {}.tap { |hash| hash[keys.shift] = hasherizer(keys, value) }
49
- end
50
95
  end
51
96
  end
@@ -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))
@@ -208,7 +212,7 @@ module Mongoid
208
212
  if context = get(object)
209
213
  context.client.close unless (context.cluster.equal?(cluster) || cluster.nil?)
210
214
  end
211
- ensure
215
+ ensure
212
216
  Thread.current["[mongoid][#{object.object_id}]:context"] = nil
213
217
  end
214
218
  end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+ # encoding: utf-8
3
+ module Mongoid
4
+ module Touchable
5
+
6
+ module InstanceMethods
7
+
8
+ # Touch the document, in effect updating its updated_at timestamp and
9
+ # optionally the provided field to the current time. If any belongs_to
10
+ # relations exist with a touch option, they will be updated as well.
11
+ #
12
+ # @example Update the updated_at timestamp.
13
+ # document.touch
14
+ #
15
+ # @example Update the updated_at and provided timestamps.
16
+ # document.touch(:audited)
17
+ #
18
+ # @note This will not autobuild relations if those options are set.
19
+ #
20
+ # @param [ Symbol ] field The name of an additional field to update.
21
+ #
22
+ # @return [ true/false ] false if record is new_record otherwise true.
23
+ #
24
+ # @since 3.0.0
25
+ def touch(field = nil)
26
+ return false if _root.new_record?
27
+ current = Time.now
28
+ field = database_field_name(field)
29
+ write_attribute(:updated_at, current) if respond_to?("updated_at=")
30
+ write_attribute(field, current) if field
31
+
32
+ touches = touch_atomic_updates(field)
33
+ unless touches["$set"].blank?
34
+ selector = atomic_selector
35
+ _root.collection.find(selector).update_one(positionally(selector, touches), session: _session)
36
+ end
37
+ run_callbacks(:touch)
38
+ true
39
+ end
40
+ end
41
+
42
+ extend self
43
+
44
+ # Add the association to the touchable relations if the touch option was
45
+ # provided.
46
+ #
47
+ # @example Add the touchable.
48
+ # Model.define_touchable!(assoc)
49
+ #
50
+ # @param [ Association ] association The association metadata.
51
+ #
52
+ # @return [ Class ] The model class.
53
+ #
54
+ # @since 3.0.0
55
+ def define_touchable!(association)
56
+ name = association.name
57
+ method_name = define_relation_touch_method(name, association)
58
+ association.inverse_class.tap do |klass|
59
+ klass.after_save method_name
60
+ klass.after_destroy method_name
61
+ klass.after_touch method_name
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ # Define the method that will get called for touching belongs_to
68
+ # relations.
69
+ #
70
+ # @api private
71
+ #
72
+ # @example Define the touch relation.
73
+ # Model.define_relation_touch_method(:band)
74
+ # Model.define_relation_touch_method(:band, :band_updated_at)
75
+ #
76
+ # @param [ Symbol ] name The name of the relation.
77
+ # @param [ Association ] association The association metadata.
78
+ #
79
+ # @since 3.1.0
80
+ #
81
+ # @return [ Symbol ] The method name.
82
+ def define_relation_touch_method(name, association)
83
+ relation_classes = if association.polymorphic?
84
+ association.send(:inverse_association_classes)
85
+ else
86
+ [ association.relation_class ]
87
+ end
88
+
89
+ relation_classes.each { |c| c.send(:include, InstanceMethods) }
90
+ method_name = "touch_#{name}_after_create_or_destroy"
91
+ association.inverse_class.class_eval <<-TOUCH, __FILE__, __LINE__ + 1
92
+ def #{method_name}
93
+ without_autobuild do
94
+ relation = __send__(:#{name})
95
+ relation.touch #{":#{association.touch_field}" if association.touch_field} if relation
96
+ end
97
+ end
98
+ TOUCH
99
+ method_name.to_sym
100
+ end
101
+ end
102
+ end