mongoid 9.1.1 → 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 +31 -11
- data/lib/mongoid/association/accessors.rb +18 -4
- data/lib/mongoid/association/eager_load/discriminated_inclusion.rb +10 -10
- data/lib/mongoid/association/eager_load/embedded_distributor.rb +6 -6
- data/lib/mongoid/association/eager_load/lookup_pipeline.rb +1 -1
- data/lib/mongoid/association/eager_loadable.rb +8 -1
- data/lib/mongoid/association/embedded/batchable.rb +17 -1
- data/lib/mongoid/association/nested/many.rb +1 -1
- data/lib/mongoid/association/referenced/has_and_belongs_to_many/buildable.rb +6 -0
- data/lib/mongoid/association/relatable.rb +18 -0
- data/lib/mongoid/association.rb +8 -0
- data/lib/mongoid/atomic/modifiers.rb +100 -16
- data/lib/mongoid/clients/factory.rb +2 -0
- data/lib/mongoid/config/encryption.rb +148 -17
- data/lib/mongoid/document.rb +3 -0
- data/lib/mongoid/encryptable.rb +42 -15
- data/lib/mongoid/errors/invalid_encryption_schema.rb +27 -0
- data/lib/mongoid/errors/no_encryption_schema.rb +5 -3
- data/lib/mongoid/errors.rb +1 -0
- data/lib/mongoid/fields.rb +7 -0
- data/lib/mongoid/persistable/creatable.rb +120 -14
- data/lib/mongoid/persistable.rb +15 -15
- data/lib/mongoid/persistence_context.rb +46 -4
- data/lib/mongoid/search_indexable.rb +5 -1
- data/lib/mongoid/tasks/database.rb +10 -7
- data/lib/mongoid/touchable.rb +28 -1
- data/lib/mongoid/version.rb +1 -1
- data/lib/mongoid/warnings.rb +4 -0
- data/spec/integration/associations/embedded_delayed_atomic_sets_spec.rb +216 -0
- data/spec/integration/associations/has_and_belongs_to_many_spec.rb +14 -0
- data/spec/integration/atomic/modifiers_spec.rb +283 -0
- data/spec/integration/encryption_spec.rb +149 -0
- data/spec/mongoid/association/accessors_spec.rb +35 -0
- data/spec/mongoid/association/referenced/has_and_belongs_to_many/buildable_spec.rb +11 -0
- data/spec/mongoid/atomic/modifiers_spec.rb +347 -0
- data/spec/mongoid/attributes/nested_spec.rb +29 -0
- data/spec/mongoid/clients/factory_spec.rb +36 -0
- data/spec/mongoid/config/encryption_spec.rb +223 -0
- data/spec/mongoid/criteria/includable_spec.rb +346 -0
- data/spec/mongoid/encryptable_spec.rb +54 -0
- data/spec/mongoid/fields_spec.rb +8 -0
- data/spec/mongoid/search_indexable_spec.rb +26 -0
- data/spec/mongoid/tasks/database_spec.rb +13 -0
- data/spec/mongoid/touchable_spec.rb +217 -0
- data/spec/mongoid/touchable_spec_models.rb +37 -0
- data/spec/support/crypt/models.rb +96 -0
- data/spec/support/models/modifier_models.rb +65 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0f94dec89da2023d081f5c08eed8a5ee4f61fd511b7f8eaea3dd8afde665829d
|
|
4
|
+
data.tar.gz: cb6e13ef1c7e708620d747a31f4cfff15088bd451c41259c2ade7b9bf4b26bdb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e4695df34902c08d6c0bb8f95f9e128f0a154b5a859b3e97181646cb6cd9f24a2d138be0721c2bba761e1db5327d7b207ce104c8d6edad1b9336f6dc62aa2e39
|
|
7
|
+
data.tar.gz: 99cb2aa72fcdf82ecd8f43d2a5bae06113a1af687d1e01d7a54f12e6b996c517c817b7b9b72c0418ddbd8e09af6e692159852cf8acafa3166166db5174acf34d
|
data/lib/config/locales/en.yml
CHANGED
|
@@ -239,6 +239,13 @@ en:
|
|
|
239
239
|
Mongoid does not support."
|
|
240
240
|
resolution: "Valid field operators are: %{valid_operators}.
|
|
241
241
|
Ensure you are using one of these operators."
|
|
242
|
+
invalid_encryption_schema:
|
|
243
|
+
message: "The encryption schema for %{namespace} is invalid."
|
|
244
|
+
summary: "Two models writing to %{namespace} describe %{path}
|
|
245
|
+
differently. The encryption schema has one entry per namespace,
|
|
246
|
+
so the two descriptions cannot both be used."
|
|
247
|
+
resolution: "Make the models agree on the encryption options of the
|
|
248
|
+
conflicting field, or store them in different collections."
|
|
242
249
|
invalid_estimated_count_criteria:
|
|
243
250
|
message: "Invalid criteria for estimated_count."
|
|
244
251
|
summary: "Estimated count is strictly a collection-level operation and cannot be called
|
|
@@ -544,22 +551,35 @@ en:
|
|
|
544
551
|
no_encryption_schema:
|
|
545
552
|
message: "The encryption schema of client %{client} does not cover
|
|
546
553
|
%{namespace}."
|
|
547
|
-
summary: "%{klass} declares encrypted fields,
|
|
548
|
-
resolved to, %{namespace},
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
554
|
+
summary: "%{klass} declares encrypted fields, directly or through
|
|
555
|
+
an embedded model, but the namespace it resolved to, %{namespace},
|
|
556
|
+
is not part of the automatic encryption schema of client
|
|
557
|
+
%{client}. The encryption schema is built once, when the client
|
|
558
|
+
is created, and is keyed by namespace, so a database name that is
|
|
559
|
+
only known later - a callable :database option,
|
|
560
|
+
Model.with(database:), Mongoid.override_database - is not in it.
|
|
561
|
+
The same happens when an embedded model with encrypted fields was
|
|
562
|
+
not yet loaded when the client was created. Routing the model to a
|
|
563
|
+
client that has no auto_encryption_options has the same effect.
|
|
564
|
+
Mongoid refuses the operation because the driver would store the
|
|
565
|
+
declared fields in plaintext without reporting an error."
|
|
557
566
|
resolution: "Give the model a namespace that is known when the
|
|
558
567
|
client is created, and a client configured with
|
|
559
|
-
auto_encryption_options.
|
|
568
|
+
auto_encryption_options. Load all models, including embedded
|
|
569
|
+
ones, before the first client is created, for example by
|
|
570
|
+
preloading them at boot. If the model has to live in a database
|
|
560
571
|
chosen at runtime, configure one client per database, each with
|
|
561
572
|
its own auto_encryption_options, and select between them with
|
|
562
573
|
Model.with(client:) instead of switching the database."
|
|
574
|
+
no_encryption_schema_field:
|
|
575
|
+
message: "The encryption schema of client %{client} does not cover
|
|
576
|
+
the field %{field} of %{namespace}."
|
|
577
|
+
summary: "The namespace %{namespace} is in the automatic encryption
|
|
578
|
+
schema of client %{client}, but its entry does not encrypt
|
|
579
|
+
%{field}, which %{klass} declares as encrypted. Mongoid refuses
|
|
580
|
+
the operation rather than storing the field in plaintext."
|
|
581
|
+
resolution: "Regenerate the schema map of the client, or add
|
|
582
|
+
%{field} to the schema entry for %{namespace}."
|
|
563
583
|
no_map_reduce_output:
|
|
564
584
|
message: "No output location was specified for the map/reduce
|
|
565
585
|
operation."
|
|
@@ -86,6 +86,24 @@ module Mongoid
|
|
|
86
86
|
instance_variable_set("@_#{name}", relation)
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
+
# Loads the documents the query preloaded for this one, kept apart from the
|
|
90
|
+
# attributes so they are not part of the document. A name this class
|
|
91
|
+
# doesn't define is dropped: a $lookup writes its field on every document
|
|
92
|
+
# it returns, including those of a sibling subclass that has no such
|
|
93
|
+
# association.
|
|
94
|
+
#
|
|
95
|
+
# @param [ Hash ] preloaded The preloaded documents by the field they
|
|
96
|
+
# travelled in.
|
|
97
|
+
#
|
|
98
|
+
# @api private
|
|
99
|
+
def load_preloaded_associations(preloaded)
|
|
100
|
+
preloaded.each do |field, documents|
|
|
101
|
+
name = field.delete_prefix(PRELOADED_PREFIX)
|
|
102
|
+
association = relations[name]
|
|
103
|
+
__build__(name, documents, association) if association
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
89
107
|
private
|
|
90
108
|
|
|
91
109
|
# Get the association. Extracted out from the getter method to avoid
|
|
@@ -127,10 +145,6 @@ module Mongoid
|
|
|
127
145
|
_loading do
|
|
128
146
|
if object && needs_no_database_query?(object, association)
|
|
129
147
|
__build__(name, object, association)
|
|
130
|
-
# Check if data was loaded via $lookup aggregation
|
|
131
|
-
elsif !association.embedded? && attributes.key?(name.to_s)
|
|
132
|
-
# Use the pre-loaded association data from $lookup
|
|
133
|
-
__build__(name, attributes[name.to_s], association)
|
|
134
148
|
else
|
|
135
149
|
selected_fields = _mongoid_filter_selected_fields(association.key)
|
|
136
150
|
__build__(name, attributes[association.key], association, selected_fields)
|
|
@@ -15,15 +15,15 @@ module Mongoid
|
|
|
15
15
|
# For Machine.eager_load(:widgets), where Lathe#widgets => Cog and
|
|
16
16
|
# Press#widgets => Belt, it emits:
|
|
17
17
|
#
|
|
18
|
-
# { '$lookup' => { 'from' => 'cogs', ..., 'as' => '
|
|
19
|
-
# { '$lookup' => { 'from' => 'belts', ..., 'as' => '
|
|
18
|
+
# { '$lookup' => { 'from' => 'cogs', ..., 'as' => '__mongoid_eager_loaded_widgets_Lathe' } },
|
|
19
|
+
# { '$lookup' => { 'from' => 'belts', ..., 'as' => '__mongoid_eager_loaded_widgets_Press' } },
|
|
20
20
|
# { '$set' => {
|
|
21
|
-
# '
|
|
22
|
-
# { 'case' => { '$eq' => [ '$_type', 'Lathe' ] }, 'then' => '$
|
|
23
|
-
# { 'case' => { '$eq' => [ '$_type', 'Press' ] }, 'then' => '$
|
|
21
|
+
# '__mongoid_eager_loaded_widgets' => { '$switch' => { 'branches' => [ # route each document to its
|
|
22
|
+
# { 'case' => { '$eq' => [ '$_type', 'Lathe' ] }, 'then' => '$__mongoid_eager_loaded_widgets_Lathe' }, # own type's matches
|
|
23
|
+
# { 'case' => { '$eq' => [ '$_type', 'Press' ] }, 'then' => '$__mongoid_eager_loaded_widgets_Press' }
|
|
24
24
|
# ], 'default' => [] } }
|
|
25
25
|
# } },
|
|
26
|
-
# { '$unset' => [ '
|
|
26
|
+
# { '$unset' => [ '__mongoid_eager_loaded_widgets_Lathe', '__mongoid_eager_loaded_widgets_Press' ] }
|
|
27
27
|
#
|
|
28
28
|
# @api private
|
|
29
29
|
class DiscriminatedInclusion < Inclusion
|
|
@@ -56,7 +56,7 @@ module Mongoid
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
def temporary_field(node)
|
|
59
|
-
|
|
59
|
+
node.association.temporary_field(owner(node).discriminator_value)
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
# The $set that fills the association on each document from the temporary
|
|
@@ -68,15 +68,15 @@ module Mongoid
|
|
|
68
68
|
'then' => "$#{field}"
|
|
69
69
|
}
|
|
70
70
|
end
|
|
71
|
-
{ '$set' => {
|
|
71
|
+
{ '$set' => { preloaded_field => { '$switch' => { 'branches' => branches, 'default' => [] } } } }
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
def owner(node)
|
|
75
75
|
node.association.inverse_class
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
def
|
|
79
|
-
@nodes.first.association.
|
|
78
|
+
def preloaded_field
|
|
79
|
+
@nodes.first.association.preloaded_field
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
def discriminator_key
|
|
@@ -22,15 +22,15 @@ module Mongoid
|
|
|
22
22
|
# 'from' => 'devices', # the embedded port, so they are
|
|
23
23
|
# 'localField' => 'port.device_id', # collected in a temp top-level
|
|
24
24
|
# 'foreignField' => '_id', # field instead
|
|
25
|
-
# 'as' => '
|
|
25
|
+
# 'as' => '__mongoid_eager_loaded_device_port'
|
|
26
26
|
# } },
|
|
27
27
|
# { '$set' => {
|
|
28
|
-
# 'port' => { '$mergeObjects' => [ # merge
|
|
28
|
+
# 'port' => { '$mergeObjects' => [ # merge the devices onto the port
|
|
29
29
|
# '$port',
|
|
30
|
-
# { '
|
|
30
|
+
# { '__mongoid_eager_loaded_device' => { '$filter' => { ... } } } # this port's matches
|
|
31
31
|
# ] }
|
|
32
32
|
# } },
|
|
33
|
-
# { '$unset' => '
|
|
33
|
+
# { '$unset' => '__mongoid_eager_loaded_device_port' } # drop the temp field
|
|
34
34
|
#
|
|
35
35
|
# @api private
|
|
36
36
|
class EmbeddedDistributor
|
|
@@ -92,7 +92,7 @@ module Mongoid
|
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
def temporary_field
|
|
95
|
-
|
|
95
|
+
@association.temporary_field(path.tr('.', '_'))
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
# An embedded collection (embeds_many) is rebuilt with $map so each element
|
|
@@ -104,7 +104,7 @@ module Mongoid
|
|
|
104
104
|
element = many ? "$$#{head.store_as}" : node
|
|
105
105
|
child =
|
|
106
106
|
if rest.empty?
|
|
107
|
-
{ @association.
|
|
107
|
+
{ @association.preloaded_field => correlated_matches(element) }
|
|
108
108
|
else
|
|
109
109
|
segment = rest.first.store_as
|
|
110
110
|
{ segment => distributed_value(rest, "#{element}.#{segment}") }
|
|
@@ -94,7 +94,14 @@ module Mongoid
|
|
|
94
94
|
next unless as = assoc_map.delete(klass)
|
|
95
95
|
|
|
96
96
|
as.each do |assoc|
|
|
97
|
-
queue
|
|
97
|
+
# The queue is matched against the assoc_map keys, which are
|
|
98
|
+
# inverse_class_name values (resolved class names). Push the
|
|
99
|
+
# resolved class name so a class_name such as "::Photo" or a
|
|
100
|
+
# module-relative "Item" still matches its nested associations.
|
|
101
|
+
# A truly polymorphic association (a polymorphic belongs_to) has
|
|
102
|
+
# no single resolvable target class, so fall back to the raw name.
|
|
103
|
+
target = assoc.try_relation_class
|
|
104
|
+
queue << (target ? target.name : assoc.class_name)
|
|
98
105
|
|
|
99
106
|
# If this class is nested in the inclusion tree, only load documents
|
|
100
107
|
# for the association above it. If there is no parent association,
|
|
@@ -130,13 +130,29 @@ module Mongoid
|
|
|
130
130
|
# batchable.add_atomic_sets([{ field: value }])
|
|
131
131
|
#
|
|
132
132
|
# @param [ Array<Hash> ] sets The atomic sets.
|
|
133
|
+
#
|
|
134
|
+
# @return [ Array<Hash> | nil ] The atomic sets, or nil if the base
|
|
135
|
+
# document is not persisted or is not being assigned.
|
|
133
136
|
def add_atomic_sets(sets)
|
|
134
|
-
|
|
137
|
+
# New embedded documents are persisted whole by their parent with a
|
|
138
|
+
# $set or $push. A delayed update for one of their associations is
|
|
139
|
+
# redundant whether or not the document is attached to its parent.
|
|
140
|
+
# Before attachment, its path is relative to the document instead
|
|
141
|
+
# of the root.
|
|
142
|
+
return unless _assigning? && _base.persisted?
|
|
135
143
|
|
|
136
144
|
_base.delayed_atomic_sets[path].try(:clear)
|
|
137
145
|
_base.collect_children.each do |child|
|
|
138
146
|
child.delayed_atomic_sets.clear
|
|
139
147
|
end
|
|
148
|
+
|
|
149
|
+
# If the base document is itself inserted wholesale via a $set of
|
|
150
|
+
# its atomic path, that $set already contains these documents, and
|
|
151
|
+
# a nested $set under the same path would conflict with it. The
|
|
152
|
+
# cleanup above has already cleared the delayed sets the assigned
|
|
153
|
+
# children recorded, so skipping the recording here is safe.
|
|
154
|
+
return if _base.settable?
|
|
155
|
+
|
|
140
156
|
_base.delayed_atomic_sets[path] = sets
|
|
141
157
|
end
|
|
142
158
|
|
|
@@ -230,7 +230,7 @@ module Mongoid
|
|
|
230
230
|
# the id is only ever resolved within the association itself.
|
|
231
231
|
id
|
|
232
232
|
else
|
|
233
|
-
{ _id: id, association.foreign_key => parent.
|
|
233
|
+
{ _id: id, association.foreign_key => parent.send(association.primary_key) }
|
|
234
234
|
end
|
|
235
235
|
end
|
|
236
236
|
end
|
|
@@ -20,6 +20,12 @@ module Mongoid
|
|
|
20
20
|
# @return [ Array<Document> ] The documents.
|
|
21
21
|
def build(_base, object, _type = nil, selected_fields = nil)
|
|
22
22
|
if query?(object)
|
|
23
|
+
if object.is_a?(Array) && object.empty?
|
|
24
|
+
# An empty stored key array is not $lookup output. Build a none
|
|
25
|
+
# criteria so reset, empty?, and size do not query the database.
|
|
26
|
+
return query_criteria(nil)
|
|
27
|
+
end
|
|
28
|
+
|
|
23
29
|
# Handle array of hashes from $lookup aggregation
|
|
24
30
|
if object.is_a?(Array) && object.all? { |o| o.is_a?(Hash) }
|
|
25
31
|
return object.map do |attrs|
|
|
@@ -217,6 +217,24 @@ module Mongoid
|
|
|
217
217
|
stores_foreign_key? ? foreign_key : primary_key
|
|
218
218
|
end
|
|
219
219
|
|
|
220
|
+
# The field a $lookup writes this association's preloaded documents into.
|
|
221
|
+
#
|
|
222
|
+
# @return [ String ] The field.
|
|
223
|
+
def preloaded_field
|
|
224
|
+
"#{PRELOADED_PREFIX}#{name}"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# A field where the aggregation collects this association's documents
|
|
228
|
+
# before moving them to where they belong. The qualifier tells it apart
|
|
229
|
+
# from the other temporary fields of the same association.
|
|
230
|
+
#
|
|
231
|
+
# @param [ String ] qualifier What tells this temporary field apart.
|
|
232
|
+
#
|
|
233
|
+
# @return [ String ] The field.
|
|
234
|
+
def temporary_field(qualifier)
|
|
235
|
+
"#{preloaded_field}_#{qualifier}"
|
|
236
|
+
end
|
|
237
|
+
|
|
220
238
|
# The name of the setter on this object for assigning an associated object.
|
|
221
239
|
#
|
|
222
240
|
# @return [ String ] The setter name.
|
data/lib/mongoid/association.rb
CHANGED
|
@@ -33,6 +33,14 @@ module Mongoid
|
|
|
33
33
|
include Macros
|
|
34
34
|
include Reflections
|
|
35
35
|
|
|
36
|
+
# Prefixes the field a $lookup writes an association's preloaded documents
|
|
37
|
+
# into. They travel apart from the document's own fields, so the hydration
|
|
38
|
+
# can keep them out of the attributes instead of leaving them on the
|
|
39
|
+
# document, where a later save would persist them.
|
|
40
|
+
#
|
|
41
|
+
# @api private
|
|
42
|
+
PRELOADED_PREFIX = '__mongoid_eager_loaded_'
|
|
43
|
+
|
|
36
44
|
# Map the macros to their corresponding Association classes.
|
|
37
45
|
#
|
|
38
46
|
# @return [ Hash ] The mapping from macros to their Association class.
|
|
@@ -13,13 +13,9 @@ module Mongoid
|
|
|
13
13
|
# @param [ Hash ] modifications The add to set modifiers.
|
|
14
14
|
def add_to_set(modifications)
|
|
15
15
|
modifications.each_pair do |field, value|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
end
|
|
20
|
-
else
|
|
21
|
-
add_to_sets[field] = { '$each' => value }
|
|
22
|
-
end
|
|
16
|
+
mods = add_to_set_conflict?(field) ? conflicting_add_to_sets : add_to_sets
|
|
17
|
+
add_each_operation(mods, field, value)
|
|
18
|
+
add_to_set_fields[field.split('.', 2)[0]] = field
|
|
23
19
|
end
|
|
24
20
|
end
|
|
25
21
|
|
|
@@ -31,7 +27,8 @@ module Mongoid
|
|
|
31
27
|
# @param [ Hash ] modifications The pull all modifiers.
|
|
32
28
|
def pull_all(modifications)
|
|
33
29
|
modifications.each_pair do |field, value|
|
|
34
|
-
|
|
30
|
+
mods = pull_all_conflict?(field) ? conflicting_pull_alls : pull_alls
|
|
31
|
+
add_operation(mods, field, value)
|
|
35
32
|
pull_fields[field.split('.', 2)[0]] = field
|
|
36
33
|
end
|
|
37
34
|
end
|
|
@@ -41,6 +38,14 @@ module Mongoid
|
|
|
41
38
|
# @example Add pull all operations.
|
|
42
39
|
# modifiers.pull({ "addresses" => { "_id" => { "$in" => [ 1, 2, 3 ]}}})
|
|
43
40
|
#
|
|
41
|
+
# A $pull is never split out into a separate write. It shifts the
|
|
42
|
+
# positions of the remaining array elements, and the paths of the other
|
|
43
|
+
# operations on that array are built for the positions after the $pull.
|
|
44
|
+
# In a separate write the $pull could run after them, so they would hit
|
|
45
|
+
# a different element. Kept in the main update, an operation that
|
|
46
|
+
# conflicts with the $pull is rejected by MongoDB (code 40) without
|
|
47
|
+
# changing stored data.
|
|
48
|
+
#
|
|
44
49
|
# @param [ Hash ] modifications The pull all modifiers.
|
|
45
50
|
def pull(modifications)
|
|
46
51
|
modifications.each_pair do |field, value|
|
|
@@ -99,6 +104,7 @@ module Mongoid
|
|
|
99
104
|
else
|
|
100
105
|
unsets.update(field => true)
|
|
101
106
|
end
|
|
107
|
+
unset_fields[field.split('.', 2)[0]] = field
|
|
102
108
|
end
|
|
103
109
|
end
|
|
104
110
|
|
|
@@ -166,17 +172,28 @@ module Mongoid
|
|
|
166
172
|
# @return [ true | false ] If this field is a conflict.
|
|
167
173
|
def set_conflict?(field)
|
|
168
174
|
name = field.split('.', 2)[0]
|
|
169
|
-
pull_fields.has_key?(name) || push_fields.has_key?(name)
|
|
175
|
+
pull_fields.has_key?(name) || push_fields.has_key?(name) ||
|
|
176
|
+
add_to_set_fields.has_key?(name)
|
|
170
177
|
end
|
|
171
178
|
|
|
172
179
|
# Is the operation going to be a conflict for an $unset?
|
|
173
180
|
#
|
|
181
|
+
# A $push or $addToSet does not shift existing array element positions,
|
|
182
|
+
# so a conflicting $unset can be safely applied in a separate write
|
|
183
|
+
# afterwards. A $pull does shift positions, so the $unset is not split
|
|
184
|
+
# out in that case; the operations stay in the same update and MongoDB
|
|
185
|
+
# rejects the update (code 40) without changing stored data, rather
|
|
186
|
+
# than risking the $unset hitting a different element.
|
|
187
|
+
#
|
|
174
188
|
# @param [ String ] field The field.
|
|
175
189
|
#
|
|
176
190
|
# @return [ true | false ] If this field is a conflict.
|
|
177
191
|
def unset_conflict?(field)
|
|
178
192
|
key = field.split('.', 2)[0]
|
|
179
|
-
set_fields.has_key?(key)
|
|
193
|
+
return true if set_fields.has_key?(key)
|
|
194
|
+
return false if pull_fields.has_key?(key)
|
|
195
|
+
|
|
196
|
+
push_fields.has_key?(key) || add_to_set_fields.has_key?(key)
|
|
180
197
|
end
|
|
181
198
|
|
|
182
199
|
# Is the operation going to be a conflict for a $push?
|
|
@@ -190,9 +207,36 @@ module Mongoid
|
|
|
190
207
|
def push_conflict?(field)
|
|
191
208
|
name = field.split('.', 2)[0]
|
|
192
209
|
set_fields.has_key?(name) || pull_fields.has_key?(name) ||
|
|
210
|
+
add_to_set_fields.has_key?(name) || unset_fields.has_key?(name) ||
|
|
193
211
|
(push_fields.keys.count { |item| item.split('.', 2).first == name } > 1)
|
|
194
212
|
end
|
|
195
213
|
|
|
214
|
+
# Is the operation going to be a conflict for an $addToSet?
|
|
215
|
+
#
|
|
216
|
+
# As with $push, an $addToSet is only split out when the other
|
|
217
|
+
# operation does not shift array element positions. A conflicting
|
|
218
|
+
# $pull is left as-is so MongoDB rejects the update rather than the
|
|
219
|
+
# $addToSet possibly hitting a different element in a separate write.
|
|
220
|
+
#
|
|
221
|
+
# @param [ String ] field The field.
|
|
222
|
+
#
|
|
223
|
+
# @return [ true | false ] If this field is a conflict.
|
|
224
|
+
def add_to_set_conflict?(field)
|
|
225
|
+
name = field.split('.', 2)[0]
|
|
226
|
+
set_fields.has_key?(name) || push_fields.has_key?(name)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Is the operation going to be a conflict for a $pullAll?
|
|
230
|
+
#
|
|
231
|
+
# @param [ String ] field The field.
|
|
232
|
+
#
|
|
233
|
+
# @return [ true | false ] If this field is a conflict.
|
|
234
|
+
def pull_all_conflict?(field)
|
|
235
|
+
name = field.split('.', 2)[0]
|
|
236
|
+
set_fields.has_key?(name) || push_fields.has_key?(name) ||
|
|
237
|
+
add_to_set_fields.has_key?(name) || pull_root?(name)
|
|
238
|
+
end
|
|
239
|
+
|
|
196
240
|
# Returns true if the $unset is made redundant by a $set that covers
|
|
197
241
|
# the entire root-level field. When $set "children" is issued, the
|
|
198
242
|
# current (live) state of the array already includes all pending changes
|
|
@@ -202,16 +246,26 @@ module Mongoid
|
|
|
202
246
|
# @param [ String ] field the field
|
|
203
247
|
def unset_superseded_by_set?(field)
|
|
204
248
|
key = field.split('.', 2).first
|
|
205
|
-
sets(initialize: false)
|
|
249
|
+
sets(initialize: false)&.has_key?(key)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Get the conflicting $addToSet operations.
|
|
253
|
+
#
|
|
254
|
+
# @example Get the conflicting add to sets.
|
|
255
|
+
# modifiers.conflicting_add_to_sets
|
|
256
|
+
#
|
|
257
|
+
# @return [ Hash ] The conflicting add to set operations.
|
|
258
|
+
def conflicting_add_to_sets
|
|
259
|
+
conflicts['$addToSet'] ||= {}
|
|
206
260
|
end
|
|
207
261
|
|
|
208
|
-
# Get the conflicting pull modifications.
|
|
262
|
+
# Get the conflicting pull all modifications.
|
|
209
263
|
#
|
|
210
|
-
# @example Get the conflicting
|
|
211
|
-
# modifiers.
|
|
264
|
+
# @example Get the conflicting pull alls.
|
|
265
|
+
# modifiers.conflicting_pull_alls
|
|
212
266
|
#
|
|
213
|
-
# @return [ Hash ] The conflicting pull operations.
|
|
214
|
-
def
|
|
267
|
+
# @return [ Hash ] The conflicting pull all operations.
|
|
268
|
+
def conflicting_pull_alls
|
|
215
269
|
conflicts['$pullAll'] ||= {}
|
|
216
270
|
end
|
|
217
271
|
|
|
@@ -282,6 +336,26 @@ module Mongoid
|
|
|
282
336
|
@set_fields ||= {}
|
|
283
337
|
end
|
|
284
338
|
|
|
339
|
+
# Get the names of the fields that need to be added to sets.
|
|
340
|
+
#
|
|
341
|
+
# @example Get the add to set fields.
|
|
342
|
+
# modifiers.add_to_set_fields
|
|
343
|
+
#
|
|
344
|
+
# @return [ Array<String> ] The add to set fields.
|
|
345
|
+
def add_to_set_fields
|
|
346
|
+
@add_to_set_fields ||= {}
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Get the names of the fields that need to be unset.
|
|
350
|
+
#
|
|
351
|
+
# @example Get the unset fields.
|
|
352
|
+
# modifiers.unset_fields
|
|
353
|
+
#
|
|
354
|
+
# @return [ Array<String> ] The unset fields.
|
|
355
|
+
def unset_fields
|
|
356
|
+
@unset_fields ||= {}
|
|
357
|
+
end
|
|
358
|
+
|
|
285
359
|
# Get the $pullAll operations or initialize a new one.
|
|
286
360
|
#
|
|
287
361
|
# @example Get the $pullAll operations.
|
|
@@ -333,6 +407,16 @@ module Mongoid
|
|
|
333
407
|
def unsets
|
|
334
408
|
self['$unset'] ||= {}
|
|
335
409
|
end
|
|
410
|
+
|
|
411
|
+
# Is there a $pull on the given root-level field? Does not initialize
|
|
412
|
+
# the $pull operations.
|
|
413
|
+
#
|
|
414
|
+
# @param [ String ] name The root-level field name.
|
|
415
|
+
#
|
|
416
|
+
# @return [ true | false ] If a $pull touches this field.
|
|
417
|
+
def pull_root?(name)
|
|
418
|
+
(self['$pull'] || {}).keys.any? { |key| key.split('.', 2)[0] == name }
|
|
419
|
+
end
|
|
336
420
|
end
|
|
337
421
|
end
|
|
338
422
|
end
|
|
@@ -90,6 +90,8 @@ module Mongoid
|
|
|
90
90
|
return nil unless opts[:auto_encryption_options]
|
|
91
91
|
|
|
92
92
|
opts[:auto_encryption_options].dup.tap do |auto_encryption_options|
|
|
93
|
+
Mongoid::Warnings.warn_bypass_auto_encryption if auto_encryption_options[:bypass_auto_encryption]
|
|
94
|
+
|
|
93
95
|
if auto_encryption_options.key?(:schema_map)
|
|
94
96
|
default_logger.warn(
|
|
95
97
|
'The :schema_map is configured in the :auto_encryption_options for the client;' +
|