mongoid 9.1.0 → 9.1.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.
- checksums.yaml +4 -4
- data/lib/config/locales/en.yml +69 -0
- data/lib/mongoid/association/accessors.rb +27 -5
- data/lib/mongoid/association/depending.rb +14 -10
- data/lib/mongoid/association/eager.rb +9 -0
- data/lib/mongoid/association/eager_load/discriminated_inclusion.rb +88 -0
- data/lib/mongoid/association/eager_load/embedded_distributor.rb +151 -0
- data/lib/mongoid/association/eager_load/inclusion.rb +173 -0
- data/lib/mongoid/association/eager_load/inclusion_tree.rb +96 -0
- data/lib/mongoid/association/eager_load/lookup_pipeline.rb +121 -0
- data/lib/mongoid/association/eager_load/polymorphic_preloader.rb +61 -0
- data/lib/mongoid/association/eager_load/polymorphic_targets.rb +151 -0
- data/lib/mongoid/association/eager_loadable.rb +65 -101
- data/lib/mongoid/association/embedded/batchable.rb +17 -1
- data/lib/mongoid/association/embedded/embeds_many/proxy.rb +6 -1
- data/lib/mongoid/association/embedded/embeds_one.rb +1 -0
- data/lib/mongoid/association/macros.rb +13 -1
- data/lib/mongoid/association/nested/many.rb +34 -5
- data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
- data/lib/mongoid/association/nested/one.rb +6 -1
- data/lib/mongoid/association/options.rb +17 -0
- data/lib/mongoid/association/referenced/belongs_to/eager.rb +6 -1
- data/lib/mongoid/association/referenced/belongs_to.rb +1 -0
- data/lib/mongoid/association/referenced/counter_cache.rb +3 -3
- data/lib/mongoid/association/referenced/has_and_belongs_to_many/buildable.rb +6 -0
- data/lib/mongoid/association/referenced/has_and_belongs_to_many.rb +5 -0
- data/lib/mongoid/association/referenced/has_many/enumerable.rb +3 -1
- data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
- data/lib/mongoid/association/referenced/has_many_through/eager.rb +99 -0
- data/lib/mongoid/association/referenced/has_many_through/proxy.rb +68 -0
- data/lib/mongoid/association/referenced/has_many_through.rb +201 -0
- data/lib/mongoid/association/referenced/has_one.rb +1 -0
- data/lib/mongoid/association/referenced/has_one_through/eager.rb +65 -0
- data/lib/mongoid/association/referenced/has_one_through/proxy.rb +30 -0
- data/lib/mongoid/association/referenced/has_one_through.rb +167 -0
- data/lib/mongoid/association/referenced.rb +2 -0
- data/lib/mongoid/association/relatable.rb +58 -0
- data/lib/mongoid/association.rb +15 -0
- data/lib/mongoid/atomic/modifiers.rb +100 -16
- data/lib/mongoid/clients/factory.rb +9 -13
- data/lib/mongoid/collection_configurable.rb +8 -0
- data/lib/mongoid/config/defaults.rb +0 -1
- data/lib/mongoid/config/encryption.rb +170 -22
- data/lib/mongoid/config.rb +56 -5
- data/lib/mongoid/contextual/aggregable/memory.rb +6 -2
- data/lib/mongoid/contextual/memory.rb +18 -7
- data/lib/mongoid/criteria/includable.rb +35 -11
- data/lib/mongoid/criteria/queryable/mergeable.rb +4 -0
- data/lib/mongoid/criteria/queryable/selectable.rb +123 -7
- data/lib/mongoid/deprecable.rb +45 -2
- data/lib/mongoid/document.rb +3 -0
- data/lib/mongoid/encryptable.rb +73 -0
- data/lib/mongoid/errors/config_redactor.rb +43 -0
- data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
- data/lib/mongoid/errors/invalid_encryption_schema.rb +27 -0
- data/lib/mongoid/errors/mixed_client_configuration.rb +1 -1
- data/lib/mongoid/errors/no_client_database.rb +1 -1
- data/lib/mongoid/errors/no_client_hosts.rb +1 -1
- data/lib/mongoid/errors/no_encryption_schema.rb +30 -0
- data/lib/mongoid/errors/readonly_association.rb +29 -0
- data/lib/mongoid/errors.rb +5 -0
- data/lib/mongoid/extensions/vector.rb +64 -0
- data/lib/mongoid/extensions.rb +1 -0
- data/lib/mongoid/field_readable.rb +69 -0
- data/lib/mongoid/fields.rb +7 -0
- data/lib/mongoid/indexable.rb +38 -26
- data/lib/mongoid/matchable.rb +6 -1
- data/lib/mongoid/matcher/eq_impl_with_regexp.rb +2 -4
- data/lib/mongoid/matcher/regex.rb +11 -12
- data/lib/mongoid/matcher/regexp_budget.rb +372 -0
- data/lib/mongoid/matcher.rb +1 -0
- data/lib/mongoid/persistable/creatable.rb +120 -14
- data/lib/mongoid/persistable/updatable.rb +9 -11
- data/lib/mongoid/persistable.rb +15 -15
- data/lib/mongoid/persistence_context.rb +77 -0
- data/lib/mongoid/search_indexable.rb +69 -24
- data/lib/mongoid/serializable.rb +12 -4
- data/lib/mongoid/tasks/database.rb +33 -16
- data/lib/mongoid/threaded.rb +36 -0
- data/lib/mongoid/timestamps/timeless.rb +84 -10
- data/lib/mongoid/touchable.rb +28 -1
- data/lib/mongoid/traversable.rb +2 -2
- data/lib/mongoid/validatable/associated.rb +18 -2
- data/lib/mongoid/version.rb +1 -1
- data/lib/mongoid/warnings.rb +4 -0
- data/spec/integration/app_spec.rb +7 -1
- data/spec/integration/associations/belongs_to_spec.rb +13 -0
- data/spec/integration/associations/embedded_delayed_atomic_sets_spec.rb +216 -0
- data/spec/integration/associations/has_and_belongs_to_many_spec.rb +31 -2
- data/spec/integration/atomic/modifiers_spec.rb +283 -0
- data/spec/integration/dots_and_dollars_spec.rb +12 -2
- data/spec/integration/encryption_spec.rb +300 -0
- data/spec/integration/matcher_operator_data/regex.yml +21 -0
- data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
- data/spec/integration/query_operator_guard_spec.rb +89 -0
- data/spec/mongoid/association/accessors_spec.rb +35 -0
- data/spec/mongoid/association/eager_loadable_spec.rb +47 -0
- data/spec/mongoid/association/eager_spec.rb +2 -2
- data/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb +33 -0
- data/spec/mongoid/association/fallback_spec.rb +425 -0
- data/spec/mongoid/association/macros_spec.rb +26 -0
- data/spec/mongoid/association/referenced/belongs_to/eager_spec.rb +17 -0
- data/spec/mongoid/association/referenced/has_and_belongs_to_many/buildable_spec.rb +11 -0
- data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
- data/spec/mongoid/association/referenced/has_and_belongs_to_many_spec.rb +50 -0
- data/spec/mongoid/association/referenced/has_many/eager_spec.rb +33 -0
- data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
- data/spec/mongoid/association/referenced/has_many_through/eager_spec.rb +58 -0
- data/spec/mongoid/association/referenced/has_many_through/proxy_spec.rb +62 -0
- data/spec/mongoid/association/referenced/has_many_through_spec.rb +399 -0
- data/spec/mongoid/association/referenced/has_one_through/eager_spec.rb +58 -0
- data/spec/mongoid/association/referenced/has_one_through/proxy_spec.rb +49 -0
- data/spec/mongoid/association/referenced/has_one_through_spec.rb +211 -0
- data/spec/mongoid/atomic/modifiers_spec.rb +347 -0
- data/spec/mongoid/attributes/nested_spec.rb +278 -8
- data/spec/mongoid/clients/factory_spec.rb +36 -0
- data/spec/mongoid/config/defaults_spec.rb +33 -0
- data/spec/mongoid/config/encryption_spec.rb +451 -0
- data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
- data/spec/mongoid/contextual/memory_spec.rb +177 -0
- data/spec/mongoid/criteria/includable_spec.rb +1221 -0
- data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +15 -1
- data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +201 -0
- data/spec/mongoid/criteria_spec.rb +3 -0
- data/spec/mongoid/encryptable_spec.rb +115 -0
- data/spec/mongoid/errors/config_redactor_spec.rb +112 -0
- data/spec/mongoid/errors/mixed_client_configuration_spec.rb +31 -0
- data/spec/mongoid/errors/no_client_database_spec.rb +29 -0
- data/spec/mongoid/errors/no_client_hosts_spec.rb +26 -0
- data/spec/mongoid/errors/readonly_association_spec.rb +34 -0
- data/spec/mongoid/extensions/vector_spec.rb +175 -0
- data/spec/mongoid/fields_spec.rb +8 -0
- data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
- data/spec/mongoid/search_indexable_spec.rb +321 -9
- data/spec/mongoid/tasks/database_spec.rb +31 -0
- data/spec/mongoid/timestamps/timeless_spec.rb +197 -5
- data/spec/mongoid/touchable_spec.rb +242 -0
- data/spec/mongoid/touchable_spec_models.rb +47 -0
- data/spec/mongoid/validatable/associated_spec.rb +19 -0
- data/spec/spec_helper.rb +5 -1
- data/spec/support/crypt/models.rb +253 -0
- data/spec/support/models/modifier_models.rb +65 -0
- metadata +55 -2
data/lib/mongoid/threaded.rb
CHANGED
|
@@ -32,6 +32,9 @@ module Mongoid
|
|
|
32
32
|
hash[key] = "#{key}-stack"
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# The name of the stack tracking collection and index management.
|
|
36
|
+
COLLECTION_MANAGEMENT = :collection_management
|
|
37
|
+
|
|
35
38
|
# The key for the current thread's sessions.
|
|
36
39
|
SESSIONS_KEY = 'sessions'
|
|
37
40
|
|
|
@@ -42,6 +45,9 @@ module Mongoid
|
|
|
42
45
|
# executed on documents.
|
|
43
46
|
EXECUTE_CALLBACKS = 'execute-callbacks'
|
|
44
47
|
|
|
48
|
+
# The key for the time left in the current in-memory regexp budget.
|
|
49
|
+
REGEXP_BUDGET_KEY = 'regexp-budget'
|
|
50
|
+
|
|
45
51
|
extend self
|
|
46
52
|
|
|
47
53
|
# Resets the current thread- or fiber-local storage to its initial state.
|
|
@@ -165,6 +171,36 @@ module Mongoid
|
|
|
165
171
|
!stack(name).empty?
|
|
166
172
|
end
|
|
167
173
|
|
|
174
|
+
# Execute the block as collection or index management.
|
|
175
|
+
#
|
|
176
|
+
# Creating, dropping and inspecting collections and indexes sends no
|
|
177
|
+
# document data, so these operations are exempt from the encryption schema
|
|
178
|
+
# check that PersistenceContext applies to reads and writes. Without the
|
|
179
|
+
# exemption, tasks such as db:mongoid:create_collections would need an
|
|
180
|
+
# encryption-capable client to run.
|
|
181
|
+
#
|
|
182
|
+
# @example Create a collection.
|
|
183
|
+
# Threaded.with_collection_management { model.create_collection }
|
|
184
|
+
#
|
|
185
|
+
# @return [ Object ] The result of the block.
|
|
186
|
+
def with_collection_management
|
|
187
|
+
begin_execution(COLLECTION_MANAGEMENT)
|
|
188
|
+
yield
|
|
189
|
+
ensure
|
|
190
|
+
exit_execution(COLLECTION_MANAGEMENT)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Is collection or index management being executed?
|
|
194
|
+
#
|
|
195
|
+
# @example Is a collection being managed?
|
|
196
|
+
# Threaded.managing_collection?
|
|
197
|
+
#
|
|
198
|
+
# @return [ true | false ] Whether collection or index management is in
|
|
199
|
+
# progress on the current thread.
|
|
200
|
+
def managing_collection?
|
|
201
|
+
executing?(COLLECTION_MANAGEMENT)
|
|
202
|
+
end
|
|
203
|
+
|
|
168
204
|
# Exit from a named thread local stack.
|
|
169
205
|
#
|
|
170
206
|
# @example Exit from the stack.
|
|
@@ -22,13 +22,20 @@ module Mongoid
|
|
|
22
22
|
true
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
#
|
|
25
|
+
# Skip timestamping for the duration of the given block, or (in the
|
|
26
|
+
# deprecated, block-less form) for the next persistence operation.
|
|
26
27
|
#
|
|
27
|
-
# @example Save a document but don't timestamp.
|
|
28
|
+
# @example Save a document but don't timestamp (block form).
|
|
29
|
+
# person.timeless { person.save }
|
|
30
|
+
#
|
|
31
|
+
# @example Save a document but don't timestamp (deprecated chained form).
|
|
28
32
|
# person.timeless.save
|
|
29
33
|
#
|
|
30
|
-
# @return [ Document ] The
|
|
31
|
-
|
|
34
|
+
# @return [ Object | Document ] The return value of the block, or (in the
|
|
35
|
+
# block-less form) the document this was called on.
|
|
36
|
+
def timeless(&block)
|
|
37
|
+
return Timeless.with_timeless(&block) if block
|
|
38
|
+
|
|
32
39
|
self.class.timeless
|
|
33
40
|
self
|
|
34
41
|
end
|
|
@@ -47,6 +54,9 @@ module Mongoid
|
|
|
47
54
|
# The key to use to store the timeless table
|
|
48
55
|
TIMELESS_TABLE_KEY = '[mongoid]:timeless'
|
|
49
56
|
|
|
57
|
+
# The key to use to store the block-based timeless flag.
|
|
58
|
+
TIMELESS_FLAG_KEY = '[mongoid]:timeless-flag'
|
|
59
|
+
|
|
50
60
|
# Returns the in-memory thread cache of classes
|
|
51
61
|
# for which to skip timestamping.
|
|
52
62
|
#
|
|
@@ -58,16 +68,78 @@ module Mongoid
|
|
|
58
68
|
end
|
|
59
69
|
|
|
60
70
|
def_delegators :timeless_table, :[]=, :[]
|
|
71
|
+
|
|
72
|
+
# Skip timestamping for the duration of the given block, on the
|
|
73
|
+
# current thread or fiber. This applies to every document persisted
|
|
74
|
+
# while the block is executing, regardless of class, including
|
|
75
|
+
# cascaded embedded children at any nesting depth.
|
|
76
|
+
#
|
|
77
|
+
# @example Skip timestamping for a block.
|
|
78
|
+
# Mongoid::Timestamps::Timeless.with_timeless do
|
|
79
|
+
# person.save
|
|
80
|
+
# end
|
|
81
|
+
#
|
|
82
|
+
# @return [ Object ] The return value of the block.
|
|
83
|
+
def with_timeless
|
|
84
|
+
# Only the outermost block owns the flag: if we are already inside a
|
|
85
|
+
# timeless scope, we leave the suppression in place when this block
|
|
86
|
+
# ends. This avoids tracking a nesting depth that could drift out of
|
|
87
|
+
# sync.
|
|
88
|
+
already_timeless = suppressing_timestamps?
|
|
89
|
+
set_suppressing_timestamps(true) unless already_timeless
|
|
90
|
+
yield
|
|
91
|
+
ensure
|
|
92
|
+
set_suppressing_timestamps(false) unless already_timeless
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Whether a block-based timeless scope is currently active on this
|
|
96
|
+
# thread/fiber.
|
|
97
|
+
#
|
|
98
|
+
# @return [ true | false ] Whether timestamps are being suppressed.
|
|
99
|
+
#
|
|
100
|
+
# @api private
|
|
101
|
+
def suppressing_timestamps?
|
|
102
|
+
!!Threaded.get(TIMELESS_FLAG_KEY) { false }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Set whether a block-based timeless scope is active on this
|
|
106
|
+
# thread/fiber.
|
|
107
|
+
#
|
|
108
|
+
# @param [ true | false ] value Whether to suppress timestamps.
|
|
109
|
+
#
|
|
110
|
+
# @api private
|
|
111
|
+
def set_suppressing_timestamps(value)
|
|
112
|
+
Threaded.set(TIMELESS_FLAG_KEY, value)
|
|
113
|
+
end
|
|
61
114
|
end
|
|
62
115
|
|
|
63
116
|
module ClassMethods
|
|
64
|
-
#
|
|
117
|
+
# Skip timestamping for the duration of the given block, or (in the
|
|
118
|
+
# deprecated, block-less form) for the next persistence operation.
|
|
119
|
+
#
|
|
120
|
+
# @example Create a document but don't timestamp (block form).
|
|
121
|
+
# Person.timeless { Person.create(title: "Sir") }
|
|
65
122
|
#
|
|
66
|
-
# @example Create a document but don't timestamp.
|
|
123
|
+
# @example Create a document but don't timestamp (deprecated form).
|
|
67
124
|
# Person.timeless.create(:title => "Sir")
|
|
68
125
|
#
|
|
69
|
-
# @return [ Class ] The
|
|
70
|
-
|
|
126
|
+
# @return [ Object | Class ] The return value of the block, or (in the
|
|
127
|
+
# block-less form) the class this was called on.
|
|
128
|
+
def timeless(&block)
|
|
129
|
+
return Timeless.with_timeless(&block) if block
|
|
130
|
+
|
|
131
|
+
locations = caller_locations
|
|
132
|
+
|
|
133
|
+
# if this is called from the #timeless instance method, look up one
|
|
134
|
+
# level higher for the call site
|
|
135
|
+
locations.shift if locations[0].path == __FILE__
|
|
136
|
+
|
|
137
|
+
Mongoid.deprecation_warning(
|
|
138
|
+
:timeless_sans_block,
|
|
139
|
+
'Calling #timeless without a block is deprecated; pass a block ' \
|
|
140
|
+
'instead, e.g. `record.timeless { record.save }`.',
|
|
141
|
+
locations
|
|
142
|
+
)
|
|
71
143
|
counter = 0
|
|
72
144
|
counter += 1 if self < Mongoid::Timestamps::Created
|
|
73
145
|
counter += 1 if self < Mongoid::Timestamps::Updated
|
|
@@ -109,12 +181,14 @@ module Mongoid
|
|
|
109
181
|
Timeless[name] = (counter == 0) ? nil : counter
|
|
110
182
|
end
|
|
111
183
|
|
|
112
|
-
# Returns whether the current class should skip timestamping.
|
|
184
|
+
# Returns whether the current class should skip timestamping. This is
|
|
185
|
+
# true when either a block-based timeless scope is active on the
|
|
186
|
+
# current thread/fiber, or the deprecated per-class counter is set.
|
|
113
187
|
#
|
|
114
188
|
# @return [ true | false ] Whether the current class should
|
|
115
189
|
# skip timestamping.
|
|
116
190
|
def timeless?
|
|
117
|
-
!!Timeless[name]
|
|
191
|
+
Timeless.suppressing_timestamps? || !!Timeless[name]
|
|
118
192
|
end
|
|
119
193
|
end
|
|
120
194
|
end
|
data/lib/mongoid/touchable.rb
CHANGED
|
@@ -49,7 +49,7 @@ module Mongoid
|
|
|
49
49
|
|
|
50
50
|
begin
|
|
51
51
|
touches = _gather_touch_updates(Time.current, field)
|
|
52
|
-
_root.
|
|
52
|
+
_root.persist_atomic_operations('$set' => touches) if touches.present?
|
|
53
53
|
_run_touch_callbacks_from_root
|
|
54
54
|
ensure
|
|
55
55
|
_clear_touch_updates(field)
|
|
@@ -104,6 +104,33 @@ module Mongoid
|
|
|
104
104
|
run_callbacks(:touch)
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
+
# The root-relative paths that a touch of this document's parent chain
|
|
108
|
+
# writes: the (possibly aliased) +updated_at+ field of each touchable
|
|
109
|
+
# node in the chain, plus this document's custom touch field, if any.
|
|
110
|
+
#
|
|
111
|
+
# @api private
|
|
112
|
+
#
|
|
113
|
+
# @param [ Symbol, String, nil ] field The custom touch field.
|
|
114
|
+
#
|
|
115
|
+
# @return [ Array<String> ] The touchable paths.
|
|
116
|
+
def _touchable_chain_paths(field = nil)
|
|
117
|
+
field = database_field_name(field) if field
|
|
118
|
+
|
|
119
|
+
node = self
|
|
120
|
+
paths = []
|
|
121
|
+
loop do
|
|
122
|
+
if node.respond_to?(:updated_at=)
|
|
123
|
+
paths << node.atomic_attribute_name(node.database_field_name(:updated_at))
|
|
124
|
+
end
|
|
125
|
+
paths << node.atomic_attribute_name(field) if node.equal?(self) && field
|
|
126
|
+
|
|
127
|
+
break unless node._touchable_parent?
|
|
128
|
+
|
|
129
|
+
node = node._parent
|
|
130
|
+
end
|
|
131
|
+
paths
|
|
132
|
+
end
|
|
133
|
+
|
|
107
134
|
# Indicates whether the parent exists and is touchable.
|
|
108
135
|
#
|
|
109
136
|
# @api private
|
data/lib/mongoid/traversable.rb
CHANGED
|
@@ -211,7 +211,7 @@ module Mongoid
|
|
|
211
211
|
# @api private
|
|
212
212
|
def self.add_discriminator_mapping(value, klass = self)
|
|
213
213
|
self.discriminator_mapping ||= {}
|
|
214
|
-
|
|
214
|
+
discriminator_mapping[value] = klass
|
|
215
215
|
superclass.add_discriminator_mapping(value, klass) if hereditary?
|
|
216
216
|
end
|
|
217
217
|
|
|
@@ -225,7 +225,7 @@ module Mongoid
|
|
|
225
225
|
#
|
|
226
226
|
# @api private
|
|
227
227
|
def self.get_discriminator_mapping(value)
|
|
228
|
-
|
|
228
|
+
discriminator_mapping[value] if discriminator_mapping
|
|
229
229
|
end
|
|
230
230
|
end
|
|
231
231
|
|
|
@@ -67,12 +67,13 @@ module Mongoid
|
|
|
67
67
|
valid = document.validating do
|
|
68
68
|
# Now, treating the target as an array, look at each element
|
|
69
69
|
# and see if it is valid, but only if it has already been
|
|
70
|
-
# persisted, or changed, and hasn't been flagged for destroy
|
|
70
|
+
# persisted, or changed, and hasn't been flagged for destroy
|
|
71
|
+
# or already destroyed.
|
|
71
72
|
#
|
|
72
73
|
# use map.all? instead of just all?, because all? will do short-circuit
|
|
73
74
|
# evaluation and terminate on the first failed validation.
|
|
74
75
|
list.map do |value|
|
|
75
|
-
if
|
|
76
|
+
if needs_validation?(value)
|
|
76
77
|
value.validated? || value.valid?
|
|
77
78
|
else
|
|
78
79
|
true
|
|
@@ -120,6 +121,21 @@ module Mongoid
|
|
|
120
121
|
def get_target_documents_for_other(target)
|
|
121
122
|
Array.wrap(target)
|
|
122
123
|
end
|
|
124
|
+
|
|
125
|
+
# Returns true if the given value should be validated as part of
|
|
126
|
+
# an associated validation. Destroyed and flagged-for-destroy
|
|
127
|
+
# documents are skipped, as are persisted documents that haven't
|
|
128
|
+
# changed.
|
|
129
|
+
#
|
|
130
|
+
# @param [ Mongoid::Document | nil ] value The value to check.
|
|
131
|
+
#
|
|
132
|
+
# @return [ true | false ] Whether the value needs validation.
|
|
133
|
+
def needs_validation?(value)
|
|
134
|
+
value &&
|
|
135
|
+
!value.flagged_for_destroy? &&
|
|
136
|
+
!value.destroyed? &&
|
|
137
|
+
(!value.persisted? || value.changed?)
|
|
138
|
+
end
|
|
123
139
|
end
|
|
124
140
|
end
|
|
125
141
|
end
|
data/lib/mongoid/version.rb
CHANGED
data/lib/mongoid/warnings.rb
CHANGED
|
@@ -36,5 +36,9 @@ module Mongoid
|
|
|
36
36
|
warning :autosave_saves_unchanged_documents, "Autosave associations are currently configured to save documents even if they haven't changed. " \
|
|
37
37
|
'This legacy behavior is deprecated. Set Mongoid.autosave_saves_unchanged_documents to false to ' \
|
|
38
38
|
'skip saving unchanged documents in autosave associations.'
|
|
39
|
+
warning :bypass_auto_encryption,
|
|
40
|
+
'auto_encryption_options includes bypass_auto_encryption: true. The driver ' \
|
|
41
|
+
'will not encrypt anything for this client: fields declared with encrypt: true ' \
|
|
42
|
+
'are written to the database unencrypted.'
|
|
39
43
|
end
|
|
40
44
|
end
|
|
@@ -435,7 +435,13 @@ describe 'Mongoid application tests' do
|
|
|
435
435
|
end
|
|
436
436
|
gemfile_lines << "gem 'mongoid', path: '#{File.expand_path(BASE)}'\n"
|
|
437
437
|
|
|
438
|
-
#
|
|
438
|
+
# json 3.0 removed the quirks_mode and max_nesting keywords that
|
|
439
|
+
# ActiveSupport::JSON (every 7.x) still passes to JSON.generate and
|
|
440
|
+
# JSON.parse, so a freshly resolved bundle picks a 3.x json and the app
|
|
441
|
+
# fails to encode any response with ArgumentError: unknown keyword:
|
|
442
|
+
# quirks_mode. Cap json at 2.x, which still accepts those keywords.
|
|
443
|
+
gemfile_lines << "gem 'json', '< 3'\n" # Rails 6.0 and 6.1 need logger gem on Ruby 2.7+ due to stdlib changes
|
|
444
|
+
|
|
439
445
|
gemfile_lines << "gem 'logger'\n" if rails_version && rails_version.to_f < 7.0 && RUBY_VERSION >= '2.7'
|
|
440
446
|
|
|
441
447
|
if rails_version
|
|
@@ -99,6 +99,19 @@ describe 'belongs_to associations' do
|
|
|
99
99
|
expect(team_manager.reload.unit_type).to eq 'group'
|
|
100
100
|
expect(team_manager.unit).to eq team
|
|
101
101
|
end
|
|
102
|
+
|
|
103
|
+
it 'eager-loads the corresponding unit when unit_type is a different alias' do
|
|
104
|
+
dept_manager.update unit_type: 'sandbox_dept'
|
|
105
|
+
team_manager.update unit_type: 'group'
|
|
106
|
+
|
|
107
|
+
managers = SandboxManager.where(:_id.in => [ dept_manager.id, team_manager.id ])
|
|
108
|
+
.includes(:unit)
|
|
109
|
+
.to_a
|
|
110
|
+
units_by_id = managers.to_h { |m| [ m.id, m.ivar(:unit) ] }
|
|
111
|
+
|
|
112
|
+
expect(units_by_id[dept_manager.id]).to eq department
|
|
113
|
+
expect(units_by_id[team_manager.id]).to eq team
|
|
114
|
+
end
|
|
102
115
|
end
|
|
103
116
|
|
|
104
117
|
context 'when the association uses the default resolver' do
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
module EmbeddedDelayedAtomicSetsSpec
|
|
6
|
+
class Section
|
|
7
|
+
include Mongoid::Document
|
|
8
|
+
|
|
9
|
+
field :content
|
|
10
|
+
|
|
11
|
+
embedded_in :parent, polymorphic: true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class Group
|
|
15
|
+
include Mongoid::Document
|
|
16
|
+
|
|
17
|
+
embedded_in :root
|
|
18
|
+
embeds_many :sections, class_name: 'EmbeddedDelayedAtomicSetsSpec::Section'
|
|
19
|
+
accepts_nested_attributes_for :sections
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class Root
|
|
23
|
+
include Mongoid::Document
|
|
24
|
+
|
|
25
|
+
embeds_many :sections, class_name: 'EmbeddedDelayedAtomicSetsSpec::Section'
|
|
26
|
+
embeds_one :group, class_name: 'EmbeddedDelayedAtomicSetsSpec::Group'
|
|
27
|
+
accepts_nested_attributes_for :sections
|
|
28
|
+
accepts_nested_attributes_for :group
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module NewEmbeddedChildSpec
|
|
33
|
+
class Member
|
|
34
|
+
include Mongoid::Document
|
|
35
|
+
|
|
36
|
+
field :name
|
|
37
|
+
|
|
38
|
+
embedded_in :team
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class Team
|
|
42
|
+
include Mongoid::Document
|
|
43
|
+
|
|
44
|
+
embedded_in :org
|
|
45
|
+
embeds_many :members, class_name: 'NewEmbeddedChildSpec::Member'
|
|
46
|
+
accepts_nested_attributes_for :members
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class Org
|
|
50
|
+
include Mongoid::Document
|
|
51
|
+
|
|
52
|
+
field :title
|
|
53
|
+
|
|
54
|
+
embeds_one :team, class_name: 'NewEmbeddedChildSpec::Team'
|
|
55
|
+
accepts_nested_attributes_for :team
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
RSpec.describe 'embedded delayed atomic sets' do
|
|
60
|
+
context 'with the same stored name' do
|
|
61
|
+
let!(:root) do
|
|
62
|
+
root = EmbeddedDelayedAtomicSetsSpec::Root.new
|
|
63
|
+
root.sections = [ EmbeddedDelayedAtomicSetsSpec::Section.new(content: 'root-original') ]
|
|
64
|
+
root.save!
|
|
65
|
+
root
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def stored_document
|
|
69
|
+
EmbeddedDelayedAtomicSetsSpec::Root.collection.find(_id: root.id).first
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def stored_contents(sections)
|
|
73
|
+
sections.to_a.map { |section| section['content'] }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def document_contents(sections)
|
|
77
|
+
sections.map(&:content)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The persisted root has no group, so attributes= builds the new nested group
|
|
81
|
+
# while the root sections update is still pending.
|
|
82
|
+
it 'does not merge a new child into the parent association' do
|
|
83
|
+
reloaded = EmbeddedDelayedAtomicSetsSpec::Root.find(root.id)
|
|
84
|
+
reloaded.attributes = {
|
|
85
|
+
sections: [ { _id: reloaded.sections.first.id, content: 'root-updated' } ],
|
|
86
|
+
group: { sections: [ { content: 'group-new' } ] }
|
|
87
|
+
}
|
|
88
|
+
reloaded.save!
|
|
89
|
+
|
|
90
|
+
stored = stored_document
|
|
91
|
+
expect(stored_contents(stored['sections'])).to eq [ 'root-updated' ]
|
|
92
|
+
expect(stored_contents(stored.dig('group', 'sections'))).to eq [ 'group-new' ]
|
|
93
|
+
expect(document_contents(reloaded.sections)).to eq [ 'root-updated' ]
|
|
94
|
+
expect(document_contents(reloaded.group.sections)).to eq [ 'group-new' ]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'does not copy a new child into an empty parent association' do
|
|
98
|
+
reloaded = EmbeddedDelayedAtomicSetsSpec::Root.find(root.id)
|
|
99
|
+
reloaded.attributes = {
|
|
100
|
+
sections: [],
|
|
101
|
+
group: { sections: [ { content: 'group-new' } ] }
|
|
102
|
+
}
|
|
103
|
+
reloaded.save!
|
|
104
|
+
|
|
105
|
+
stored = stored_document
|
|
106
|
+
expect(stored).not_to have_key('sections')
|
|
107
|
+
expect(stored_contents(stored.dig('group', 'sections'))).to eq [ 'group-new' ]
|
|
108
|
+
expect(reloaded.sections).to be_empty
|
|
109
|
+
expect(document_contents(reloaded.group.sections)).to eq [ 'group-new' ]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'does not merge a new child when the parent association is unchanged' do
|
|
113
|
+
reloaded = EmbeddedDelayedAtomicSetsSpec::Root.find(root.id)
|
|
114
|
+
reloaded.attributes = { group: { sections: [ { content: 'group-new' } ] } }
|
|
115
|
+
reloaded.save!
|
|
116
|
+
|
|
117
|
+
stored = stored_document
|
|
118
|
+
expect(stored_contents(stored['sections'])).to eq [ 'root-original' ]
|
|
119
|
+
expect(stored_contents(stored.dig('group', 'sections'))).to eq [ 'group-new' ]
|
|
120
|
+
expect(document_contents(reloaded.sections)).to eq [ 'root-original' ]
|
|
121
|
+
expect(document_contents(reloaded.group.sections)).to eq [ 'group-new' ]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'updates an existing group and isolates a subsequently added group' do
|
|
125
|
+
root.group = EmbeddedDelayedAtomicSetsSpec::Group.new(
|
|
126
|
+
sections: [ EmbeddedDelayedAtomicSetsSpec::Section.new(content: 'group-original') ]
|
|
127
|
+
)
|
|
128
|
+
root.save!
|
|
129
|
+
|
|
130
|
+
reloaded = EmbeddedDelayedAtomicSetsSpec::Root.find(root.id)
|
|
131
|
+
reloaded.attributes = {
|
|
132
|
+
group: {
|
|
133
|
+
_id: reloaded.group.id,
|
|
134
|
+
sections: [ { _id: reloaded.group.sections.first.id, content: 'group-updated' } ]
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
reloaded.save!
|
|
138
|
+
|
|
139
|
+
stored = stored_document
|
|
140
|
+
expect(stored_contents(stored['sections'])).to eq [ 'root-original' ]
|
|
141
|
+
expect(stored_contents(stored.dig('group', 'sections'))).to eq [ 'group-updated' ]
|
|
142
|
+
expect(document_contents(reloaded.sections)).to eq [ 'root-original' ]
|
|
143
|
+
expect(document_contents(reloaded.group.sections)).to eq [ 'group-updated' ]
|
|
144
|
+
|
|
145
|
+
reloaded.group = nil
|
|
146
|
+
reloaded.save!
|
|
147
|
+
reloaded = EmbeddedDelayedAtomicSetsSpec::Root.find(root.id)
|
|
148
|
+
reloaded.attributes = {
|
|
149
|
+
sections: [ { _id: reloaded.sections.first.id, content: 'root-updated' } ],
|
|
150
|
+
group: { sections: [ { content: 'group-new' } ] }
|
|
151
|
+
}
|
|
152
|
+
reloaded.save!
|
|
153
|
+
|
|
154
|
+
stored = stored_document
|
|
155
|
+
expect(stored_contents(stored['sections'])).to eq [ 'root-updated' ]
|
|
156
|
+
expect(stored_contents(stored.dig('group', 'sections'))).to eq [ 'group-new' ]
|
|
157
|
+
expect(document_contents(reloaded.sections)).to eq [ 'root-updated' ]
|
|
158
|
+
expect(document_contents(reloaded.group.sections)).to eq [ 'group-new' ]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it 'does not merge a new child when updating the parent through nested attributes' do
|
|
162
|
+
reloaded = EmbeddedDelayedAtomicSetsSpec::Root.find(root.id)
|
|
163
|
+
reloaded.attributes = {
|
|
164
|
+
sections_attributes: {
|
|
165
|
+
'0' => { _id: reloaded.sections.first.id, content: 'root-updated' }
|
|
166
|
+
},
|
|
167
|
+
group: { sections: [ { content: 'group-new' } ] }
|
|
168
|
+
}
|
|
169
|
+
reloaded.save!
|
|
170
|
+
|
|
171
|
+
stored = stored_document
|
|
172
|
+
expect(stored_contents(stored['sections'])).to eq [ 'root-updated' ]
|
|
173
|
+
expect(stored_contents(stored.dig('group', 'sections'))).to eq [ 'group-new' ]
|
|
174
|
+
expect(document_contents(reloaded.sections)).to eq [ 'root-updated' ]
|
|
175
|
+
expect(document_contents(reloaded.group.sections)).to eq [ 'group-new' ]
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
context 'when assigning a new embeds_one child with nested embeds_many' do
|
|
180
|
+
let!(:org) { NewEmbeddedChildSpec::Org.create!(title: 't0') }
|
|
181
|
+
|
|
182
|
+
it 'does not write the nested array as a root-level field' do
|
|
183
|
+
reloaded = NewEmbeddedChildSpec::Org.find(org.id)
|
|
184
|
+
reloaded.attributes = { title: 't1', team: { members: [ { name: 'm1' } ] } }
|
|
185
|
+
reloaded.save!
|
|
186
|
+
|
|
187
|
+
stored = NewEmbeddedChildSpec::Org.collection.find(_id: org.id).first
|
|
188
|
+
expect(stored.keys).to match_array(%w[_id title team])
|
|
189
|
+
expect(stored.dig('team', 'members').map { |member| member['name'] }).to eq [ 'm1' ]
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it 'does not write nested attributes at the root level' do
|
|
193
|
+
reloaded = NewEmbeddedChildSpec::Org.find(org.id)
|
|
194
|
+
reloaded.attributes = {
|
|
195
|
+
team_attributes: { members: [ { name: 'm1' } ] }
|
|
196
|
+
}
|
|
197
|
+
reloaded.save!
|
|
198
|
+
|
|
199
|
+
stored = NewEmbeddedChildSpec::Org.collection.find(_id: org.id).first
|
|
200
|
+
expect(stored.keys).to match_array(%w[_id title team])
|
|
201
|
+
expect(stored.dig('team', 'members').map { |member| member['name'] }).to eq [ 'm1' ]
|
|
202
|
+
expect(reloaded.team.members.map(&:name)).to eq [ 'm1' ]
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
it 'does not conflict when assigning to a newly built child' do
|
|
206
|
+
reloaded = NewEmbeddedChildSpec::Org.find(org.id)
|
|
207
|
+
reloaded.build_team
|
|
208
|
+
reloaded.team.attributes = { members: [ { name: 'm1' } ] }
|
|
209
|
+
reloaded.save!
|
|
210
|
+
|
|
211
|
+
stored = NewEmbeddedChildSpec::Org.collection.find(_id: org.id).first
|
|
212
|
+
expect(stored.dig('team', 'members').map { |member| member['name'] }).to eq [ 'm1' ]
|
|
213
|
+
expect(reloaded.team.members.map(&:name)).to eq [ 'm1' ]
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
@@ -60,6 +60,20 @@ module HabtmSpec
|
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
describe 'has_and_belongs_to_many associations' do
|
|
63
|
+
context 'when the stored foreign-key array is empty' do
|
|
64
|
+
it 'can reset and enumerate the association proxy' do
|
|
65
|
+
item = HabtmSpec::Item.create!(color_ids: [])
|
|
66
|
+
colors = item.reload.colors
|
|
67
|
+
|
|
68
|
+
expect_query(0) do
|
|
69
|
+
colors.reset
|
|
70
|
+
expect(colors.empty?).to be true
|
|
71
|
+
expect(colors.size).to be 0
|
|
72
|
+
expect(colors.to_a).to eq []
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
63
77
|
context 'when an anonymous class defines a has_and_belongs_to_many association' do
|
|
64
78
|
let(:klass) do
|
|
65
79
|
Class.new do
|
|
@@ -90,8 +104,23 @@ describe 'has_and_belongs_to_many associations' do
|
|
|
90
104
|
})
|
|
91
105
|
end
|
|
92
106
|
|
|
93
|
-
|
|
94
|
-
|
|
107
|
+
# The nested attributes are processed before the attachment_ids
|
|
108
|
+
# assignment is applied, so at that point the id is not yet in the
|
|
109
|
+
# association and resolving it requires a collection-wide lookup.
|
|
110
|
+
context 'when allow_reparenting_via_nested_attributes is false' do
|
|
111
|
+
config_override :allow_reparenting_via_nested_attributes, false
|
|
112
|
+
|
|
113
|
+
it 'raises a document not found error' do
|
|
114
|
+
expect { image_block.save! }.to raise_error(Mongoid::Errors::DocumentNotFound)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
context 'when allow_reparenting_via_nested_attributes is true' do
|
|
119
|
+
config_override :allow_reparenting_via_nested_attributes, true
|
|
120
|
+
|
|
121
|
+
it 'does not raise on save' do
|
|
122
|
+
expect { image_block.save! }.not_to raise_error
|
|
123
|
+
end
|
|
95
124
|
end
|
|
96
125
|
end
|
|
97
126
|
|