mongoid 9.1.0 → 9.1.1
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 +49 -0
- data/lib/mongoid/association/accessors.rb +9 -1
- 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 +57 -100
- 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.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 +40 -0
- data/lib/mongoid/association.rb +7 -0
- data/lib/mongoid/clients/factory.rb +7 -13
- data/lib/mongoid/collection_configurable.rb +8 -0
- data/lib/mongoid/config/defaults.rb +0 -1
- data/lib/mongoid/config/encryption.rb +36 -19
- 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/encryptable.rb +46 -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/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 +28 -0
- data/lib/mongoid/errors/readonly_association.rb +29 -0
- data/lib/mongoid/errors.rb +4 -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/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 +1 -1
- data/lib/mongoid/persistable/updatable.rb +9 -11
- data/lib/mongoid/persistence_context.rb +35 -0
- data/lib/mongoid/search_indexable.rb +64 -23
- data/lib/mongoid/serializable.rb +12 -4
- data/lib/mongoid/tasks/database.rb +23 -9
- data/lib/mongoid/threaded.rb +36 -0
- data/lib/mongoid/timestamps/timeless.rb +84 -10
- data/lib/mongoid/traversable.rb +2 -2
- data/lib/mongoid/validatable/associated.rb +18 -2
- data/lib/mongoid/version.rb +1 -1
- data/spec/integration/app_spec.rb +7 -1
- data/spec/integration/associations/belongs_to_spec.rb +13 -0
- data/spec/integration/associations/has_and_belongs_to_many_spec.rb +17 -2
- data/spec/integration/dots_and_dollars_spec.rb +12 -2
- data/spec/integration/encryption_spec.rb +151 -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/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/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/attributes/nested_spec.rb +249 -8
- data/spec/mongoid/config/defaults_spec.rb +33 -0
- data/spec/mongoid/config/encryption_spec.rb +228 -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 +875 -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 +61 -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/matcher/regexp_budget_spec.rb +570 -0
- data/spec/mongoid/search_indexable_spec.rb +295 -9
- data/spec/mongoid/tasks/database_spec.rb +18 -0
- data/spec/mongoid/timestamps/timeless_spec.rb +197 -5
- data/spec/mongoid/touchable_spec.rb +25 -0
- data/spec/mongoid/touchable_spec_models.rb +10 -0
- data/spec/mongoid/validatable/associated_spec.rb +19 -0
- data/spec/spec_helper.rb +5 -1
- data/spec/support/crypt/models.rb +157 -0
- metadata +52 -2
|
@@ -179,7 +179,7 @@ module Mongoid
|
|
|
179
179
|
# @param [ Hash ] attrs The single document attributes to process.
|
|
180
180
|
def update_nested_relation(parent, id, attrs)
|
|
181
181
|
first = existing.first
|
|
182
|
-
converted = first ?
|
|
182
|
+
converted = convert_id(first ? first.class : association.klass, id)
|
|
183
183
|
|
|
184
184
|
if existing.where(_id: converted).exists?
|
|
185
185
|
# document exists in association
|
|
@@ -191,19 +191,48 @@ module Mongoid
|
|
|
191
191
|
end
|
|
192
192
|
elsif association.embedded?
|
|
193
193
|
raise Errors::DocumentNotFound.new(association.klass, id)
|
|
194
|
-
elsif
|
|
195
|
-
|
|
194
|
+
elsif destroyable?(attrs)
|
|
195
|
+
# A destroy of a document that is not in the association is
|
|
196
|
+
# ignored, rather than reaching for it outside the association.
|
|
197
|
+
nil
|
|
198
|
+
elsif Mongoid.allow_reparenting_via_nested_attributes?
|
|
199
|
+
Mongoid::Warnings.warn_reparenting_via_nested_attributes
|
|
196
200
|
|
|
197
201
|
# push existing document to association
|
|
198
202
|
doc = association.klass.unscoped.find(converted)
|
|
203
|
+
# find returns nil instead of raising when
|
|
204
|
+
# Mongoid.raise_not_found_error is false; a missing document
|
|
205
|
+
# must still be an error here, consistent with the other
|
|
206
|
+
# not-found branches.
|
|
207
|
+
raise Errors::DocumentNotFound.new(association.klass, id) if doc.nil?
|
|
208
|
+
|
|
199
209
|
update_document(doc, attrs)
|
|
200
|
-
existing.push(doc)
|
|
210
|
+
existing.push(doc)
|
|
201
211
|
else
|
|
202
|
-
raise Errors::DocumentNotFound.new(association.klass,
|
|
212
|
+
raise Errors::DocumentNotFound.new(association.klass, not_found_params(parent, id))
|
|
203
213
|
end
|
|
204
214
|
|
|
205
215
|
parent.children_may_have_changed!
|
|
206
216
|
end
|
|
217
|
+
|
|
218
|
+
# The params to report when an id in the nested attributes could not
|
|
219
|
+
# be resolved within the association.
|
|
220
|
+
#
|
|
221
|
+
# @api private
|
|
222
|
+
#
|
|
223
|
+
# @param [ Document ] parent The parent document.
|
|
224
|
+
# @param [ String | BSON::ObjectId ] id of the related document.
|
|
225
|
+
#
|
|
226
|
+
# @return [ Hash | Object ] The params for the not found error.
|
|
227
|
+
def not_found_params(parent, id)
|
|
228
|
+
if association.is_a?(Association::Referenced::HasAndBelongsToMany)
|
|
229
|
+
# A many-to-many association has no foreign key on the child, so
|
|
230
|
+
# the id is only ever resolved within the association itself.
|
|
231
|
+
id
|
|
232
|
+
else
|
|
233
|
+
{ _id: id, association.foreign_key => parent.id }
|
|
234
|
+
end
|
|
235
|
+
end
|
|
207
236
|
end
|
|
208
237
|
end
|
|
209
238
|
end
|
|
@@ -55,12 +55,26 @@ module Mongoid
|
|
|
55
55
|
# @example Convert the id.
|
|
56
56
|
# builder.convert_id(Person, "4d371b444835d98b8b000010")
|
|
57
57
|
#
|
|
58
|
+
# Ids arriving from a form are always scalars. A Hash or an Array
|
|
59
|
+
# here means the parameters were crafted, and letting one through
|
|
60
|
+
# would turn the id into a query operator, so they are rejected.
|
|
61
|
+
#
|
|
58
62
|
# @param [ Class ] klass The class we're trying to convert for.
|
|
59
63
|
# @param [ String ] id The id, usually coming from the form.
|
|
60
64
|
#
|
|
61
65
|
# @return [ BSON::ObjectId | String | Object ] The converted id.
|
|
66
|
+
#
|
|
67
|
+
# @raise [ Errors::DocumentNotFound ] if the id is not a scalar, or
|
|
68
|
+
# cannot be converted to the type the class uses for its ids.
|
|
69
|
+
# The BSON::Error rescue is defensive: a value that reaches
|
|
70
|
+
# BSON::ObjectId.mongoize and raises there must not surface as an
|
|
71
|
+
# unhandled BSON error in the caller.
|
|
62
72
|
def convert_id(klass, id)
|
|
73
|
+
raise Errors::DocumentNotFound.new(klass, id) if id.is_a?(::Hash) || id.is_a?(::Array)
|
|
74
|
+
|
|
63
75
|
klass.using_object_ids? ? BSON::ObjectId.mongoize(id) : id
|
|
76
|
+
rescue BSON::Error
|
|
77
|
+
raise Errors::DocumentNotFound.new(klass, id)
|
|
64
78
|
end
|
|
65
79
|
|
|
66
80
|
private
|
|
@@ -26,7 +26,12 @@ module Mongoid
|
|
|
26
26
|
def build(parent)
|
|
27
27
|
return if reject?(parent, attributes)
|
|
28
28
|
|
|
29
|
-
@existing =
|
|
29
|
+
@existing =
|
|
30
|
+
if association.fallback?
|
|
31
|
+
parent.send(:without_autobuild) { parent.send(association.name) }
|
|
32
|
+
else
|
|
33
|
+
parent.send(association.name)
|
|
34
|
+
end
|
|
30
35
|
if update?
|
|
31
36
|
delete_id(attributes)
|
|
32
37
|
existing.assign_attributes(attributes)
|
|
@@ -39,6 +39,23 @@ module Mongoid
|
|
|
39
39
|
!!@options[:autobuild]
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# Invokes the :fallback Proc and returns the null object that stands in
|
|
43
|
+
# for the association when its actual value is nil. The Proc is invoked
|
|
44
|
+
# on every access; identity is the user's responsibility (return a fresh
|
|
45
|
+
# instance, a shared constant, or whatever the Proc chooses).
|
|
46
|
+
#
|
|
47
|
+
# @return [ Object | nil ] The Proc's return value, or nil if not set.
|
|
48
|
+
def fallback
|
|
49
|
+
@options[:fallback]&.call
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Whether the association has a :fallback (null object) option set.
|
|
53
|
+
#
|
|
54
|
+
# @return [ true | false ]
|
|
55
|
+
def fallback?
|
|
56
|
+
!@options[:fallback].nil?
|
|
57
|
+
end
|
|
58
|
+
|
|
42
59
|
# Is the association cyclic.
|
|
43
60
|
#
|
|
44
61
|
# @return [ true | false ] Whether the association is cyclic.
|
|
@@ -24,10 +24,15 @@ module Mongoid
|
|
|
24
24
|
# association is not polymorphic, all documents are retrieved in
|
|
25
25
|
# a single query. If the association is polymorphic, one query is
|
|
26
26
|
# issued per association target class.
|
|
27
|
+
#
|
|
28
|
+
# The polymorphic *_type field is resolved through the
|
|
29
|
+
# association's resolver to keep this path consistent with the
|
|
30
|
+
# non-eager accessor and to avoid loading arbitrary constants
|
|
31
|
+
# named in user-controlled BSON data.
|
|
27
32
|
def each_loaded_document(&block)
|
|
28
33
|
if @association.polymorphic?
|
|
29
34
|
keys_by_type_from_docs.each do |type, keys|
|
|
30
|
-
each_loaded_document_of_class(
|
|
35
|
+
each_loaded_document_of_class(@association.resolver.model_for(type), keys, &block)
|
|
31
36
|
end
|
|
32
37
|
else
|
|
33
38
|
super
|
|
@@ -110,7 +110,7 @@ module Mongoid
|
|
|
110
110
|
end
|
|
111
111
|
end
|
|
112
112
|
|
|
113
|
-
if (record = __send__(name)) && !current.nil?
|
|
113
|
+
if (record = without_autobuild { __send__(name) }) && !current.nil?
|
|
114
114
|
record[cache_column] = (record[cache_column] || 0) + 1
|
|
115
115
|
record.class.with(record.persistence_context) do |_class|
|
|
116
116
|
_class.increment_counter(cache_column, current) if record.persisted?
|
|
@@ -120,7 +120,7 @@ module Mongoid
|
|
|
120
120
|
end
|
|
121
121
|
|
|
122
122
|
klass.after_create do
|
|
123
|
-
if record = __send__(name)
|
|
123
|
+
if record = without_autobuild { __send__(name) }
|
|
124
124
|
record[cache_column] = (record[cache_column] || 0) + 1
|
|
125
125
|
|
|
126
126
|
if record.persisted?
|
|
@@ -133,7 +133,7 @@ module Mongoid
|
|
|
133
133
|
end
|
|
134
134
|
|
|
135
135
|
klass.before_destroy do
|
|
136
|
-
if record = __send__(name)
|
|
136
|
+
if record = without_autobuild { __send__(name) }
|
|
137
137
|
record[cache_column] = (record[cache_column] || 0) - 1 unless record.frozen?
|
|
138
138
|
|
|
139
139
|
if record.persisted?
|
|
@@ -271,6 +271,11 @@ module Mongoid
|
|
|
271
271
|
crit = relation_class.criteria
|
|
272
272
|
crit = if id_list
|
|
273
273
|
crit = crit.apply_scope(scope)
|
|
274
|
+
# A nil in the stored key array would match every document
|
|
275
|
+
# whose primary key is null or absent, returning documents
|
|
276
|
+
# that were never associated. Associating a target whose
|
|
277
|
+
# custom primary key is unset stores such a nil.
|
|
278
|
+
id_list = id_list.compact if id_list.is_a?(Array)
|
|
274
279
|
crit.all_of(primary_key => { '$in' => id_list })
|
|
275
280
|
else
|
|
276
281
|
crit.none
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'mongoid/field_readable'
|
|
3
4
|
require 'mongoid/pluckable'
|
|
4
5
|
|
|
5
6
|
module Mongoid
|
|
@@ -12,6 +13,7 @@ module Mongoid
|
|
|
12
13
|
class Enumerable
|
|
13
14
|
extend Forwardable
|
|
14
15
|
include ::Enumerable
|
|
16
|
+
include FieldReadable
|
|
15
17
|
include Pluckable
|
|
16
18
|
|
|
17
19
|
# The three main instance variables are collections of documents.
|
|
@@ -590,7 +592,7 @@ module Mongoid
|
|
|
590
592
|
#
|
|
591
593
|
# @return [ Array<Numeric> ] The non-nil field values.
|
|
592
594
|
def field_values_for(field)
|
|
593
|
-
map { |doc| doc
|
|
595
|
+
map { |doc| read_field_value(doc, field) }.compact
|
|
594
596
|
end
|
|
595
597
|
|
|
596
598
|
def set_base(document)
|
|
@@ -538,12 +538,111 @@ module Mongoid
|
|
|
538
538
|
# @return [ Integer ] The number of documents deleted.
|
|
539
539
|
def remove_all(conditions = nil, method = :delete_all)
|
|
540
540
|
selector = conditions || {}
|
|
541
|
-
|
|
541
|
+
selector.merge!(criteria.selector)
|
|
542
|
+
|
|
543
|
+
# Scanning before the delete means scanning only what is already in
|
|
544
|
+
# memory, so it is only done where there is a pattern whose cost has
|
|
545
|
+
# to be bounded. Everywhere else the delete comes first and the scan
|
|
546
|
+
# sees the survivors, which is what this association has always
|
|
547
|
+
# done.
|
|
548
|
+
#
|
|
549
|
+
# The limit is read once and carried into whichever branch is taken.
|
|
550
|
+
# Letting the branch below ask again would read it a second time,
|
|
551
|
+
# across a server round trip, and a limit that had become positive
|
|
552
|
+
# in the meantime would put a deadline on the delete_if -- a query,
|
|
553
|
+
# and an unbind of every match -- which is the arrangement this
|
|
554
|
+
# branch exists to avoid.
|
|
555
|
+
limit = Matcher::RegexpBudget.limit_for(selector)
|
|
556
|
+
return remove_all_bounded(selector, method, limit) if limit
|
|
557
|
+
|
|
558
|
+
removed = klass.send(method, selector)
|
|
559
|
+
|
|
560
|
+
# No scope around this. The scan runs after the delete, so the
|
|
561
|
+
# association this loads comes back holding only what the delete did
|
|
562
|
+
# not match, and there is no pattern here to bound in any case.
|
|
563
|
+
# Opening a scope would save _matches? from deciding once per
|
|
564
|
+
# document -- 0.48us against 1.74us for the match itself -- but a
|
|
565
|
+
# scope with nothing to bound suppresses that decision for
|
|
566
|
+
# everything nested inside it, and loading the association runs
|
|
567
|
+
# find callbacks. A pattern in a selector one of those evaluates
|
|
568
|
+
# would go unguarded, which costs more than the saving is worth on a
|
|
569
|
+
# path that has just made two round trips.
|
|
542
570
|
_target.delete_if do |doc|
|
|
543
571
|
doc._matches?(selector).tap do |b|
|
|
544
572
|
unbind_one(doc) if b
|
|
545
573
|
end
|
|
546
574
|
end
|
|
575
|
+
|
|
576
|
+
removed
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
# Deletes all related documents matching a selector that carries a
|
|
580
|
+
# regular expression, with the whole scan under one regexp budget.
|
|
581
|
+
#
|
|
582
|
+
# The scan runs before anything is deleted, so a budget that runs out
|
|
583
|
+
# leaves both the database and the association untouched rather than
|
|
584
|
+
# reporting a failure for a delete that has already happened. It also
|
|
585
|
+
# means the budget is closed by the time the association is mutated,
|
|
586
|
+
# so where the budget is enforced with Timeout there is no window for
|
|
587
|
+
# the exception to land in the middle of an unbind.
|
|
588
|
+
#
|
|
589
|
+
# @param [ Hash ] selector The selector to delete with.
|
|
590
|
+
# @param [ Symbol ] method The deletion method to call.
|
|
591
|
+
# @param [ Float ] limit The seconds the scan may spend, as read by
|
|
592
|
+
# the caller when it chose this path.
|
|
593
|
+
#
|
|
594
|
+
# @return [ Integer ] The number of documents deleted.
|
|
595
|
+
def remove_all_bounded(selector, method, limit)
|
|
596
|
+
# Only what the association already holds. Iterating it instead
|
|
597
|
+
# would load the whole association before the delete, and for a
|
|
598
|
+
# broad pattern that is every document the association has: /.*/ is
|
|
599
|
+
# both the cheapest pattern an attacker can supply and the one that
|
|
600
|
+
# matches everything, so bounding the cost of the matching would
|
|
601
|
+
# have been paid for with an unbounded amount of memory. Nothing is
|
|
602
|
+
# missed by not loading, because the server evaluates the same
|
|
603
|
+
# selector for the delete itself.
|
|
604
|
+
#
|
|
605
|
+
# It also means the scan runs no query, so no deadline of ours can
|
|
606
|
+
# land on one. Where the budget is enforced with Timeout, covering
|
|
607
|
+
# a query would report a slow network as a regexp timeout and could
|
|
608
|
+
# deliver the asynchronous exception inside the driver's socket
|
|
609
|
+
# read.
|
|
610
|
+
documents = _target.in_memory
|
|
611
|
+
|
|
612
|
+
matching = Matcher::RegexpBudget.open_with(limit) do
|
|
613
|
+
documents.select { |doc| doc._matches?(selector) }
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
removed = klass.send(method, selector)
|
|
617
|
+
|
|
618
|
+
# The ids are already known to be in memory, so the loaded and added
|
|
619
|
+
# documents can be dropped directly. Enumerable#delete would look
|
|
620
|
+
# each one up by id and stop at the first hash that has it, which
|
|
621
|
+
# removes the wrong instance when both hashes hold one for the same
|
|
622
|
+
# id. Enumerable#delete_if drops it from both, as this does, but
|
|
623
|
+
# only after load_all!, which is the load this scan exists to avoid.
|
|
624
|
+
#
|
|
625
|
+
# Which documents get unbound has always depended on which ones
|
|
626
|
+
# happened to be in memory, and scanning only what is in memory
|
|
627
|
+
# keeps that. It matters for has_and_belongs_to_many, whose
|
|
628
|
+
# unbind_one pulls the id out of the foreign key array on _base and
|
|
629
|
+
# marks it dirty; the scan used to run after the delete, against an
|
|
630
|
+
# association that a cold proxy had just reloaded as holding only
|
|
631
|
+
# the survivors, so nothing matched and nothing was unbound.
|
|
632
|
+
#
|
|
633
|
+
# TODO: unbinding should not depend on what was in memory. Every
|
|
634
|
+
# matched document leaves the association, so every one of them
|
|
635
|
+
# should be unbound, which would also stop _base's foreign key
|
|
636
|
+
# array from keeping ids that no longer resolve. That is a
|
|
637
|
+
# behaviour change for has_and_belongs_to_many and belongs in a
|
|
638
|
+
# ticket of its own, with a release note; it should not ride along
|
|
639
|
+
# on this one.
|
|
640
|
+
matching.each do |doc|
|
|
641
|
+
unbind_one(doc)
|
|
642
|
+
_target._loaded.delete(doc._id)
|
|
643
|
+
_target._added.delete(doc._id)
|
|
644
|
+
end
|
|
645
|
+
|
|
547
646
|
removed
|
|
548
647
|
end
|
|
549
648
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mongoid
|
|
4
|
+
module Association
|
|
5
|
+
module Referenced
|
|
6
|
+
class HasManyThrough
|
|
7
|
+
# Two-query eager preloader for has_many :through associations.
|
|
8
|
+
class Eager < Association::Eager
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def preload
|
|
12
|
+
@docs.each { |d| set_relation(d, []) }
|
|
13
|
+
|
|
14
|
+
through_assoc = @association.through_association
|
|
15
|
+
source_assoc = @association.source_association
|
|
16
|
+
|
|
17
|
+
owner_pk = through_assoc.primary_key
|
|
18
|
+
through_fk = through_assoc.foreign_key
|
|
19
|
+
|
|
20
|
+
owner_ids = @docs.filter_map { |d| d.public_send(owner_pk) }.uniq
|
|
21
|
+
return if owner_ids.empty?
|
|
22
|
+
|
|
23
|
+
# Step 1: load all intermediate records
|
|
24
|
+
intermediates = through_assoc.klass
|
|
25
|
+
.where(through_fk => { '$in' => owner_ids })
|
|
26
|
+
.to_a
|
|
27
|
+
|
|
28
|
+
# Step 2: map owner FK values to arrays of target docs
|
|
29
|
+
targets_by_owner_fk = build_targets_map(intermediates, through_fk, source_assoc)
|
|
30
|
+
|
|
31
|
+
# Step 3: set relation on each owner doc
|
|
32
|
+
@docs.each do |doc|
|
|
33
|
+
key_val = doc.public_send(owner_pk)
|
|
34
|
+
set_relation(doc, targets_by_owner_fk[key_val] || [])
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Build a Hash mapping each owner FK value to an array of target docs.
|
|
39
|
+
# Uses two different strategies depending on where the FK lives.
|
|
40
|
+
def build_targets_map(intermediates, through_fk, source_assoc)
|
|
41
|
+
if source_assoc.stores_foreign_key?
|
|
42
|
+
fk_on_intermediate_targets_map(intermediates, through_fk, source_assoc)
|
|
43
|
+
else
|
|
44
|
+
fk_on_source_targets_map(intermediates, through_fk, source_assoc)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# FK is on the intermediate (e.g. appointment.patient_id -> belongs_to :patient).
|
|
49
|
+
def fk_on_intermediate_targets_map(intermediates, through_fk, source_assoc)
|
|
50
|
+
source_fk_vals = intermediates.filter_map { |i| i.public_send(source_assoc.foreign_key) }.uniq
|
|
51
|
+
targets = source_assoc.klass.where(
|
|
52
|
+
source_assoc.primary_key => { '$in' => source_fk_vals }
|
|
53
|
+
).to_a
|
|
54
|
+
targets_by_pk = targets.group_by { |t| t.public_send(source_assoc.primary_key) }
|
|
55
|
+
|
|
56
|
+
result = Hash.new { |h, k| h[k] = [] }
|
|
57
|
+
intermediates.each do |i|
|
|
58
|
+
owner_fk_val = i.public_send(through_fk)
|
|
59
|
+
matched = targets_by_pk[i.public_send(source_assoc.foreign_key)] || []
|
|
60
|
+
result[owner_fk_val].concat(matched)
|
|
61
|
+
end
|
|
62
|
+
result
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# FK is on the source (e.g. reader.book_id -> has_many :readers on Book).
|
|
66
|
+
def fk_on_source_targets_map(intermediates, through_fk, source_assoc)
|
|
67
|
+
source_pk = source_assoc.primary_key
|
|
68
|
+
intermediate_pks = intermediates.filter_map { |i| i.public_send(source_pk) }.uniq
|
|
69
|
+
targets = source_assoc.klass.where(
|
|
70
|
+
source_assoc.foreign_key => { '$in' => intermediate_pks }
|
|
71
|
+
).to_a
|
|
72
|
+
targets_by_source_fk = targets.group_by { |t| t.public_send(source_assoc.foreign_key) }
|
|
73
|
+
|
|
74
|
+
result = Hash.new { |h, k| h[k] = [] }
|
|
75
|
+
intermediates.each do |i|
|
|
76
|
+
owner_fk_val = i.public_send(through_fk)
|
|
77
|
+
matched = targets_by_source_fk[i.public_send(source_pk)] || []
|
|
78
|
+
result[owner_fk_val].concat(matched)
|
|
79
|
+
end
|
|
80
|
+
result
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def set_relation(doc, element)
|
|
84
|
+
return if doc.blank?
|
|
85
|
+
|
|
86
|
+
proxy = HasManyThrough::Proxy.new(doc, @association, preloaded: element)
|
|
87
|
+
doc.set_relation(@association.name, proxy)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Required by base class contract. Not called from preload since this
|
|
91
|
+
# class manages its own two-query traversal directly.
|
|
92
|
+
def group_by_key
|
|
93
|
+
@association.through_association.primary_key
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mongoid
|
|
4
|
+
module Association
|
|
5
|
+
module Referenced
|
|
6
|
+
class HasManyThrough
|
|
7
|
+
# Read-only proxy for has_many :through associations. Wraps the lazy
|
|
8
|
+
# Criteria returned by the association and raises ReadonlyAssociation on
|
|
9
|
+
# any write attempt.
|
|
10
|
+
class Proxy
|
|
11
|
+
extend Forwardable
|
|
12
|
+
include Enumerable
|
|
13
|
+
|
|
14
|
+
module ClassMethods
|
|
15
|
+
def eager_loader(association, docs)
|
|
16
|
+
Eager.new(association, docs)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def embedded?
|
|
20
|
+
false
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
extend ClassMethods
|
|
25
|
+
|
|
26
|
+
# Enumerable methods use the preloaded array when available; query
|
|
27
|
+
# methods always delegate to criteria so callers get a Criteria back.
|
|
28
|
+
def_delegators :_source,
|
|
29
|
+
:each, :to_a, :first, :last, :count, :size, :length,
|
|
30
|
+
:exists?, :any?, :none?, :empty?
|
|
31
|
+
|
|
32
|
+
def_delegators :criteria,
|
|
33
|
+
:where, :pluck, :limit, :skip, :order_by, :only, :without,
|
|
34
|
+
:sum, :avg, :min, :max
|
|
35
|
+
|
|
36
|
+
READONLY_METHODS = %i[
|
|
37
|
+
<< push concat substitute build new create create!
|
|
38
|
+
delete delete_one delete_all destroy_all clear nullify
|
|
39
|
+
].freeze
|
|
40
|
+
|
|
41
|
+
def initialize(base, association, preloaded: nil)
|
|
42
|
+
@_base = base
|
|
43
|
+
@_association = association
|
|
44
|
+
@preloaded = preloaded
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def criteria
|
|
48
|
+
@criteria ||= @_association.criteria(@_base)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def _source
|
|
54
|
+
@preloaded || criteria
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
public
|
|
58
|
+
|
|
59
|
+
READONLY_METHODS.each do |meth|
|
|
60
|
+
define_method(meth) do |*|
|
|
61
|
+
raise Mongoid::Errors::ReadonlyAssociation.new(@_base.class, @_association)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|