mongoid 7.0.1 → 7.0.2

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