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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5bfb9894822f364261e1579143e0a4860a27dfe42437243938ecb2bc186a0819
|
|
4
|
+
data.tar.gz: f66924e8684458b2318f82bc7614f01823938e8c7cb3f19bf5246dd1098dd5b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 16d064d36b9a6a425d2727afdb879da7a28dd25a5889de7dd6242120d69441fb666db16dec584b0e00c77aed32fcd0a5d900c7fc3ed8ddfe0fe2006b0053dbc2
|
|
7
|
+
data.tar.gz: 2d8c367c96724961cdd78354a8bf6d10cef2fa209cec4103741e26d43389286a173a8df301116121b2e9ad89495c7c5e268b4458dd8b22e6a2ddb4fbdd835eb0
|
data/lib/config/locales/en.yml
CHANGED
|
@@ -140,6 +140,32 @@ en:
|
|
|
140
140
|
A collation option is only supported if the query is executed on a MongoDB server
|
|
141
141
|
with version >= 3.4."
|
|
142
142
|
resolution: "Remove the collation option from the query."
|
|
143
|
+
in_memory_regexp_timeout:
|
|
144
|
+
message: "Evaluating this query in memory exceeded the %{limit} second
|
|
145
|
+
limit that applies to conditions containing a regular expression."
|
|
146
|
+
summary: "Mongoid evaluates some conditions in the calling thread
|
|
147
|
+
rather than sending them to the server: queries against an embedded
|
|
148
|
+
association, removing documents from an association by condition,
|
|
149
|
+
and Document#_matches?. A regular expression in such a condition
|
|
150
|
+
runs locally. Mongoid limits the total time one in-memory
|
|
151
|
+
evaluation may spend matching, because both the cost of a single
|
|
152
|
+
match and the number of matches performed grow with the condition,
|
|
153
|
+
and a pattern built from end-user input can be made to consume an
|
|
154
|
+
unbounded amount of CPU. The limit is cumulative over the whole
|
|
155
|
+
evaluation rather than per match. Where the Ruby in use offers no
|
|
156
|
+
per-Regexp timeout the limit is enforced as wall clock over the
|
|
157
|
+
whole in-memory evaluation, so there it can be exceeded by a large
|
|
158
|
+
scan even when the pattern itself is cheap."
|
|
159
|
+
resolution: "Check whether the pattern in this query comes from user
|
|
160
|
+
input; if it does, validate it before querying. If the query is
|
|
161
|
+
legitimate and simply large, raise
|
|
162
|
+
Mongoid::Config.in_memory_regexp_time_limit, or set it to nil to
|
|
163
|
+
remove the limit entirely. The limit in force is the smaller of
|
|
164
|
+
that setting and any global Regexp.timeout the application has set,
|
|
165
|
+
so check both. On JRuby, and on MRI before 3.2, there is no
|
|
166
|
+
per-Regexp timeout, so what the limit bounds there is the elapsed
|
|
167
|
+
time of the whole in-memory evaluation rather than the time spent
|
|
168
|
+
matching."
|
|
143
169
|
invalid_around_callback:
|
|
144
170
|
message: "An around callback must contain a yield in its definition."
|
|
145
171
|
summary: "The block needs to be yielded to for around callbacks to function as intended."
|
|
@@ -515,6 +541,25 @@ en:
|
|
|
515
541
|
environment, Mongoid cannot load its configuration."
|
|
516
542
|
resolution: "Please ensure an environment is set in one of the
|
|
517
543
|
listed locations. The environment must be explicitly set."
|
|
544
|
+
no_encryption_schema:
|
|
545
|
+
message: "The encryption schema of client %{client} does not cover
|
|
546
|
+
%{namespace}."
|
|
547
|
+
summary: "%{klass} declares encrypted fields, but the namespace it
|
|
548
|
+
resolved to, %{namespace}, is not part of the automatic encryption
|
|
549
|
+
schema of client %{client}. The encryption schema is built once,
|
|
550
|
+
when the client is created, and is keyed by namespace, so a
|
|
551
|
+
database name that is only known later - a callable :database
|
|
552
|
+
option, Model.with(database:), Mongoid.override_database - is not
|
|
553
|
+
in it. Routing the model to a client that has no
|
|
554
|
+
auto_encryption_options has the same effect. Mongoid refuses the
|
|
555
|
+
operation because the driver would store the declared fields in
|
|
556
|
+
plaintext without reporting an error."
|
|
557
|
+
resolution: "Give the model a namespace that is known when the
|
|
558
|
+
client is created, and a client configured with
|
|
559
|
+
auto_encryption_options. If the model has to live in a database
|
|
560
|
+
chosen at runtime, configure one client per database, each with
|
|
561
|
+
its own auto_encryption_options, and select between them with
|
|
562
|
+
Model.with(client:) instead of switching the database."
|
|
518
563
|
no_map_reduce_output:
|
|
519
564
|
message: "No output location was specified for the map/reduce
|
|
520
565
|
operation."
|
|
@@ -618,6 +663,10 @@ en:
|
|
|
618
663
|
can only have values set when the document is a new record."
|
|
619
664
|
resolution: "Don't define '%{name}' as readonly, or do not attempt
|
|
620
665
|
to update its value after the document is persisted."
|
|
666
|
+
readonly_association:
|
|
667
|
+
message: "Attempted to modify the read-only association '%{name}' on '%{klass}'."
|
|
668
|
+
summary: "The :%{name} association is defined with :through and is read-only."
|
|
669
|
+
resolution: "To modify the association, update :%{through} directly."
|
|
621
670
|
readonly_document:
|
|
622
671
|
message: "Attempted to persist a readonly document of class '%{klass}'."
|
|
623
672
|
summary: "Documents that are marked readonly cannot be persisted."
|
|
@@ -301,7 +301,10 @@ module Mongoid
|
|
|
301
301
|
association.inverse_class.tap do |klass|
|
|
302
302
|
klass.re_define_method(name) do |reload = false|
|
|
303
303
|
value = get_relation(name, association, nil, reload)
|
|
304
|
-
value
|
|
304
|
+
next value unless value.nil? && !without_autobuild?
|
|
305
|
+
next association.fallback if association.fallback?
|
|
306
|
+
next send("build_#{name}") if association.autobuilding?
|
|
307
|
+
|
|
305
308
|
value
|
|
306
309
|
end
|
|
307
310
|
end
|
|
@@ -341,6 +344,11 @@ module Mongoid
|
|
|
341
344
|
association.inverse_class.tap do |klass|
|
|
342
345
|
klass.re_define_method("#{name}=") do |object|
|
|
343
346
|
without_autobuild do
|
|
347
|
+
if association.fallback?
|
|
348
|
+
klass = association.polymorphic? ? Mongoid::Document : association.relation_class
|
|
349
|
+
object = nil unless object.is_a?(klass)
|
|
350
|
+
end
|
|
351
|
+
|
|
344
352
|
if value = get_relation(name, association, object)
|
|
345
353
|
value = __build__(name, value, association) unless value.respond_to?(:substitute)
|
|
346
354
|
|
|
@@ -10,11 +10,15 @@ module Mongoid
|
|
|
10
10
|
included do
|
|
11
11
|
class_attribute :dependents
|
|
12
12
|
|
|
13
|
+
# Note the leading underscore: without it, ActiveSupport's
|
|
14
|
+
# class_attribute would generate a helper method for this attribute
|
|
15
|
+
# that collides with one it generates for :dependents.
|
|
16
|
+
#
|
|
13
17
|
# @api private
|
|
14
|
-
class_attribute :
|
|
18
|
+
class_attribute :_dependents_owner
|
|
15
19
|
|
|
16
20
|
self.dependents = []
|
|
17
|
-
self.
|
|
21
|
+
self._dependents_owner = self
|
|
18
22
|
end
|
|
19
23
|
|
|
20
24
|
class_methods do
|
|
@@ -55,9 +59,9 @@ module Mongoid
|
|
|
55
59
|
def self.define_dependency!(association)
|
|
56
60
|
validate!(association)
|
|
57
61
|
association.inverse_class.tap do |klass|
|
|
58
|
-
if klass.
|
|
62
|
+
if klass._dependents_owner != klass
|
|
59
63
|
klass.dependents = []
|
|
60
|
-
klass.
|
|
64
|
+
klass._dependents_owner = klass
|
|
61
65
|
end
|
|
62
66
|
|
|
63
67
|
klass.dependents.push(association) if association.dependent && !klass.dependents.include?(association)
|
|
@@ -96,17 +100,17 @@ module Mongoid
|
|
|
96
100
|
private
|
|
97
101
|
|
|
98
102
|
def _dependent_delete_all!(association)
|
|
99
|
-
return unless relation = send(association.name)
|
|
103
|
+
return unless relation = without_autobuild { send(association.name) }
|
|
100
104
|
|
|
101
105
|
if relation.respond_to?(:dependents) && relation.dependents.blank?
|
|
102
106
|
relation.clear
|
|
103
107
|
else
|
|
104
|
-
::Array.wrap(
|
|
108
|
+
::Array.wrap(relation).each { |rel| rel.delete }
|
|
105
109
|
end
|
|
106
110
|
end
|
|
107
111
|
|
|
108
112
|
def _dependent_destroy!(association)
|
|
109
|
-
return unless relation = send(association.name)
|
|
113
|
+
return unless relation = without_autobuild { send(association.name) }
|
|
110
114
|
|
|
111
115
|
if relation.is_a?(Enumerable)
|
|
112
116
|
relation.entries
|
|
@@ -117,19 +121,19 @@ module Mongoid
|
|
|
117
121
|
end
|
|
118
122
|
|
|
119
123
|
def _dependent_nullify!(association)
|
|
120
|
-
return unless relation = send(association.name)
|
|
124
|
+
return unless relation = without_autobuild { send(association.name) }
|
|
121
125
|
|
|
122
126
|
relation.nullify
|
|
123
127
|
end
|
|
124
128
|
|
|
125
129
|
def _dependent_restrict_with_exception!(association)
|
|
126
|
-
if (relation = send(association.name)) && !relation.blank?
|
|
130
|
+
if (relation = without_autobuild { send(association.name) }) && !relation.blank?
|
|
127
131
|
raise Errors::DeleteRestriction.new(relation, association.name)
|
|
128
132
|
end
|
|
129
133
|
end
|
|
130
134
|
|
|
131
135
|
def _dependent_restrict_with_error!(association)
|
|
132
|
-
return unless (relation = send(association.name)) && !relation.blank?
|
|
136
|
+
return unless (relation = without_autobuild { send(association.name) }) && !relation.blank?
|
|
133
137
|
|
|
134
138
|
errors.add(association.name, :destroy_restrict_with_error_dependencies_exist)
|
|
135
139
|
throw(:abort, false)
|
|
@@ -4,6 +4,15 @@ module Mongoid
|
|
|
4
4
|
module Association
|
|
5
5
|
# Base class for eager load preload functions.
|
|
6
6
|
class Eager
|
|
7
|
+
# Build a preloader for the given arguments and run it.
|
|
8
|
+
#
|
|
9
|
+
# @param (see #initialize)
|
|
10
|
+
#
|
|
11
|
+
# @return [ Array ] The list of documents given.
|
|
12
|
+
def self.run(associations, docs, use_lookup = false, pipeline = [])
|
|
13
|
+
new(associations, docs, use_lookup, pipeline).run
|
|
14
|
+
end
|
|
15
|
+
|
|
7
16
|
# Instantiate the eager load class.
|
|
8
17
|
#
|
|
9
18
|
# @example Create the new belongs to eager load preloader.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'mongoid/association/eager_load/inclusion'
|
|
4
|
+
|
|
5
|
+
module Mongoid
|
|
6
|
+
module Association
|
|
7
|
+
module EagerLoad
|
|
8
|
+
# Loads an inclusion that more than one subclass defines under the same name
|
|
9
|
+
# but pointing at different targets. A single $lookup can't serve them: they
|
|
10
|
+
# would all write to the same field and overwrite one another. So each
|
|
11
|
+
# subclass's inclusion is contributed into its own temporary field, carrying
|
|
12
|
+
# its own nested children, and a $set then routes every document to the field
|
|
13
|
+
# for its own type, by the discriminator.
|
|
14
|
+
#
|
|
15
|
+
# For Machine.eager_load(:widgets), where Lathe#widgets => Cog and
|
|
16
|
+
# Press#widgets => Belt, it emits:
|
|
17
|
+
#
|
|
18
|
+
# { '$lookup' => { 'from' => 'cogs', ..., 'as' => '__eager_load_widgets_Lathe' } },
|
|
19
|
+
# { '$lookup' => { 'from' => 'belts', ..., 'as' => '__eager_load_widgets_Press' } },
|
|
20
|
+
# { '$set' => {
|
|
21
|
+
# 'widgets' => { '$switch' => { 'branches' => [ # route each document to its
|
|
22
|
+
# { 'case' => { '$eq' => [ '$_type', 'Lathe' ] }, 'then' => '$__eager_load_widgets_Lathe' }, # own type's matches
|
|
23
|
+
# { 'case' => { '$eq' => [ '$_type', 'Press' ] }, 'then' => '$__eager_load_widgets_Press' }
|
|
24
|
+
# ], 'default' => [] } }
|
|
25
|
+
# } },
|
|
26
|
+
# { '$unset' => [ '__eager_load_widgets_Lathe', '__eager_load_widgets_Press' ] }
|
|
27
|
+
#
|
|
28
|
+
# @api private
|
|
29
|
+
class DiscriminatedInclusion < Inclusion
|
|
30
|
+
def initialize(nodes)
|
|
31
|
+
super()
|
|
32
|
+
@nodes = nodes
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Append each subclass's lookup (into its own temporary field), the routing
|
|
36
|
+
# $set, and the cleanup $unset.
|
|
37
|
+
#
|
|
38
|
+
# @param [ Array<Hash> ] destination The pipeline the stages are appended to.
|
|
39
|
+
def contribute(destination, _chain)
|
|
40
|
+
fields = @nodes.map { |node| [ node, contribute_into_temporary(destination, node) ] }
|
|
41
|
+
destination << route_by_type(fields)
|
|
42
|
+
destination << { '$unset' => fields.map { |_node, field| field } }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# Let the node build its own $lookup (with its nested children) and redirect
|
|
48
|
+
# it to write into a temporary field instead of the shared association name.
|
|
49
|
+
def contribute_into_temporary(destination, node)
|
|
50
|
+
field = temporary_field(node)
|
|
51
|
+
captured = []
|
|
52
|
+
node.contribute(captured, [])
|
|
53
|
+
captured.first['$lookup']['as'] = field
|
|
54
|
+
destination.concat(captured)
|
|
55
|
+
field
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def temporary_field(node)
|
|
59
|
+
"__eager_load_#{node.association.name}_#{owner(node).discriminator_value}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The $set that fills the association on each document from the temporary
|
|
63
|
+
# field matching its own type.
|
|
64
|
+
def route_by_type(fields)
|
|
65
|
+
branches = fields.map do |node, field|
|
|
66
|
+
{
|
|
67
|
+
'case' => { '$eq' => [ "$#{discriminator_key}", owner(node).discriminator_value ] },
|
|
68
|
+
'then' => "$#{field}"
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
{ '$set' => { name => { '$switch' => { 'branches' => branches, 'default' => [] } } } }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def owner(node)
|
|
75
|
+
node.association.inverse_class
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def name
|
|
79
|
+
@nodes.first.association.name.to_s
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def discriminator_key
|
|
83
|
+
owner(@nodes.first).discriminator_key
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mongoid
|
|
4
|
+
module Association
|
|
5
|
+
# Objects that carry out the aggregation-based eager load triggered by
|
|
6
|
+
# Criteria#eager_load.
|
|
7
|
+
module EagerLoad
|
|
8
|
+
# Distributes the results of a $lookup onto the embedded documents they
|
|
9
|
+
# belong to.
|
|
10
|
+
#
|
|
11
|
+
# A $lookup overwrites the single field it writes to, and it can't distribute
|
|
12
|
+
# its matches across the elements of an embedded array. So when the reference
|
|
13
|
+
# being eager-loaded lives on an embedded document, the matches are first
|
|
14
|
+
# collected in a temporary top-level field and then distributed down the
|
|
15
|
+
# embedded path onto each embedded document, merging into it (so the rest of
|
|
16
|
+
# the document is kept) and correlating by key. The temporary field is then
|
|
17
|
+
# dropped.
|
|
18
|
+
#
|
|
19
|
+
# For Computer.eager_load(port: :device) (Port belongs_to :device) it emits:
|
|
20
|
+
#
|
|
21
|
+
# { '$lookup' => { # devices can't be written into
|
|
22
|
+
# 'from' => 'devices', # the embedded port, so they are
|
|
23
|
+
# 'localField' => 'port.device_id', # collected in a temp top-level
|
|
24
|
+
# 'foreignField' => '_id', # field instead
|
|
25
|
+
# 'as' => '__eager_load_port_device'
|
|
26
|
+
# } },
|
|
27
|
+
# { '$set' => {
|
|
28
|
+
# 'port' => { '$mergeObjects' => [ # merge a 'device' key onto the port
|
|
29
|
+
# '$port',
|
|
30
|
+
# { 'device' => { '$filter' => { ... } } } # this port's matches
|
|
31
|
+
# ] }
|
|
32
|
+
# } },
|
|
33
|
+
# { '$unset' => '__eager_load_port_device' } # drop the temp field
|
|
34
|
+
#
|
|
35
|
+
# @api private
|
|
36
|
+
class EmbeddedDistributor
|
|
37
|
+
# @param [ Mongoid::Association::Relatable ] association The referenced
|
|
38
|
+
# inclusion being eager-loaded from within an embedded document.
|
|
39
|
+
# @param [ Array<Mongoid::Association::Relatable> ] chain The embedded
|
|
40
|
+
# ancestors, from the root document inward, down to the association's owner.
|
|
41
|
+
# @param [ Hash ] lookup_stage The $lookup stage built for the association.
|
|
42
|
+
#
|
|
43
|
+
# @return [ EmbeddedDistributor ] The distributor.
|
|
44
|
+
class << self
|
|
45
|
+
def for(association:, chain:, lookup_stage:)
|
|
46
|
+
lookup = lookup_stage['$lookup']
|
|
47
|
+
new(association, chain, lookup_stage, lookup['localField'], lookup['foreignField'])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private :new
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def initialize(association, chain, lookup_stage, local_field, foreign_field)
|
|
54
|
+
@association = association
|
|
55
|
+
@chain = chain
|
|
56
|
+
@lookup_stage = lookup_stage
|
|
57
|
+
@local_field = local_field
|
|
58
|
+
@foreign_field = foreign_field
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The stages that run the $lookup into a temporary field and then
|
|
62
|
+
# distribute its matches onto the embedded documents along the path.
|
|
63
|
+
#
|
|
64
|
+
# @return [ Array<Hash> ] The stages to append to the pipeline.
|
|
65
|
+
def stages
|
|
66
|
+
redirect_lookup_to_temporary_field
|
|
67
|
+
[
|
|
68
|
+
@lookup_stage,
|
|
69
|
+
{ '$set' => {
|
|
70
|
+
root => distributed_value(@chain, "$#{root}")
|
|
71
|
+
} },
|
|
72
|
+
{ '$unset' => temporary_field }
|
|
73
|
+
]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# The $lookup runs at the top level, so it reads the local field by its
|
|
79
|
+
# full embedded path and writes the matches into the temporary field.
|
|
80
|
+
def redirect_lookup_to_temporary_field
|
|
81
|
+
lookup = @lookup_stage['$lookup']
|
|
82
|
+
lookup['localField'] = "#{path}.#{@local_field}"
|
|
83
|
+
lookup['as'] = temporary_field
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def path
|
|
87
|
+
@chain.map(&:store_as).join('.')
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def root
|
|
91
|
+
@chain.first.store_as
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def temporary_field
|
|
95
|
+
"__eager_load_#{path.tr('.', '_')}_#{@association.name}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# An embedded collection (embeds_many) is rebuilt with $map so each element
|
|
99
|
+
# keeps its own matches instead of collapsing onto the first; a single
|
|
100
|
+
# embedded document (embeds_one) receives its matches in place.
|
|
101
|
+
def distributed_value(chain, node)
|
|
102
|
+
head, *rest = chain
|
|
103
|
+
many = head.many?
|
|
104
|
+
element = many ? "$$#{head.store_as}" : node
|
|
105
|
+
child =
|
|
106
|
+
if rest.empty?
|
|
107
|
+
{ @association.name.to_s => correlated_matches(element) }
|
|
108
|
+
else
|
|
109
|
+
segment = rest.first.store_as
|
|
110
|
+
{ segment => distributed_value(rest, "#{element}.#{segment}") }
|
|
111
|
+
end
|
|
112
|
+
merged = { '$mergeObjects' => [ element, child ] }
|
|
113
|
+
return merge_into_present(node, merged) unless many
|
|
114
|
+
|
|
115
|
+
{ '$map' => {
|
|
116
|
+
'input' => node,
|
|
117
|
+
'as' => head.store_as,
|
|
118
|
+
'in' => merged
|
|
119
|
+
} }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Merge the matches into a single embedded document only when it exists, so
|
|
123
|
+
# an absent embeds_one stays absent instead of being synthesized from its
|
|
124
|
+
# matches alone.
|
|
125
|
+
def merge_into_present(node, merged)
|
|
126
|
+
{ '$cond' => {
|
|
127
|
+
'if' => { '$ifNull' => [ node, false ] },
|
|
128
|
+
'then' => merged,
|
|
129
|
+
'else' => node
|
|
130
|
+
} }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# The matches that belong to a single embedded element.
|
|
134
|
+
def correlated_matches(element)
|
|
135
|
+
{ '$filter' => {
|
|
136
|
+
'input' => "$#{temporary_field}",
|
|
137
|
+
'as' => 'match',
|
|
138
|
+
'cond' => { match_operator => [ "$$match.#{@foreign_field}", "#{element}.#{@local_field}" ] }
|
|
139
|
+
} }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# A has_and_belongs_to_many holds an array of foreign keys, so a match
|
|
143
|
+
# belongs when its key is among them ($in); every other association points
|
|
144
|
+
# at a single key ($eq).
|
|
145
|
+
def match_operator
|
|
146
|
+
@association.many_to_many? ? '$in' : '$eq'
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mongoid
|
|
4
|
+
module Association
|
|
5
|
+
module EagerLoad
|
|
6
|
+
# Something an eager load contributes to the pipeline, in the role it plays
|
|
7
|
+
# while the pipeline is built. A root is asked to contribute and the whole
|
|
8
|
+
# tree follows by recursion. AssociationInclusion stands for a single
|
|
9
|
+
# association; DiscriminatedInclusion stands for a name several subclasses
|
|
10
|
+
# share.
|
|
11
|
+
#
|
|
12
|
+
# @api private
|
|
13
|
+
class Inclusion
|
|
14
|
+
# Add this inclusion's stages to the destination.
|
|
15
|
+
#
|
|
16
|
+
# @param [ Array<Hash> ] destination The pipeline (or sub-pipeline) the
|
|
17
|
+
# stages are appended to.
|
|
18
|
+
# @param [ Array<Mongoid::Association::Relatable> ] chain The embedded path
|
|
19
|
+
# accumulated from the ancestors above this inclusion (empty at the top).
|
|
20
|
+
def contribute(destination, chain)
|
|
21
|
+
raise NotImplementedError
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# An inclusion that stands for a single association. The LookupPipeline holds
|
|
26
|
+
# the stage-building helpers the kinds lean on, and a node carries its own
|
|
27
|
+
# children, so the pipeline is built by recursion from the roots downward.
|
|
28
|
+
#
|
|
29
|
+
# @api private
|
|
30
|
+
class AssociationInclusion < Inclusion
|
|
31
|
+
class << self
|
|
32
|
+
# Builds the right kind of inclusion for the association. Each subclass
|
|
33
|
+
# decides whether it handles it (.for?); exactly one does.
|
|
34
|
+
#
|
|
35
|
+
# @param [ Mongoid::Association::Relatable ] association The inclusion.
|
|
36
|
+
# @param [ LookupPipeline ] pipeline The pipeline being built.
|
|
37
|
+
# @param [ Array<Inclusion> ] children The inclusions nested under it.
|
|
38
|
+
#
|
|
39
|
+
# @return [ AssociationInclusion ] The matching kind of inclusion.
|
|
40
|
+
def for(association, pipeline, children)
|
|
41
|
+
subclasses.find { |kind| kind.for?(association) }.new(association, pipeline, children)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Whether this kind handles the given association.
|
|
45
|
+
#
|
|
46
|
+
# @param [ Mongoid::Association::Relatable ] association The inclusion.
|
|
47
|
+
#
|
|
48
|
+
# @return [ true | false ] Whether it handles it.
|
|
49
|
+
def for?(association)
|
|
50
|
+
raise NotImplementedError
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [ Mongoid::Association::Relatable ] The association this stands for.
|
|
55
|
+
attr_reader :association
|
|
56
|
+
|
|
57
|
+
def initialize(association, pipeline, children)
|
|
58
|
+
super()
|
|
59
|
+
@association = association
|
|
60
|
+
@pipeline = pipeline
|
|
61
|
+
@children = children
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# A referenced inclusion: contributes a $lookup whose sub-pipeline holds its
|
|
66
|
+
# own children. When it lives inside an embedded document (a non-empty
|
|
67
|
+
# chain), the $lookup is distributed onto that embedded path instead of
|
|
68
|
+
# standing at the top level.
|
|
69
|
+
#
|
|
70
|
+
# For a has_many :albums it contributes:
|
|
71
|
+
#
|
|
72
|
+
# { '$lookup' => {
|
|
73
|
+
# 'from' => 'albums',
|
|
74
|
+
# 'localField' => '_id', # the band's _id...
|
|
75
|
+
# 'foreignField' => 'band_id', # ...matched against each album's band_id
|
|
76
|
+
# 'as' => 'albums', # matches are written to this field
|
|
77
|
+
# 'pipeline' => [
|
|
78
|
+
# { '$sort' => {
|
|
79
|
+
# '_id' => 1
|
|
80
|
+
# } },
|
|
81
|
+
# <children>
|
|
82
|
+
# ]
|
|
83
|
+
# } }
|
|
84
|
+
#
|
|
85
|
+
# @api private
|
|
86
|
+
class JoinedInclusion < AssociationInclusion
|
|
87
|
+
class << self
|
|
88
|
+
# The default kind: a referenced, non-polymorphic association, i.e. the
|
|
89
|
+
# one no sibling kind claims.
|
|
90
|
+
#
|
|
91
|
+
# @param [ Mongoid::Association::Relatable ] association The inclusion.
|
|
92
|
+
#
|
|
93
|
+
# @return [ true | false ] Whether it handles it.
|
|
94
|
+
def for?(association)
|
|
95
|
+
(superclass.subclasses - [ self ]).none? { |kind| kind.for?(association) }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Append the $lookup, with the children in its sub-pipeline, to the
|
|
100
|
+
# destination; or distribute it onto the embedded path when nested in one.
|
|
101
|
+
#
|
|
102
|
+
# @param [ Array<Hash> ] destination The pipeline (or sub-pipeline) the
|
|
103
|
+
# stages are appended to.
|
|
104
|
+
# @param [ Array<Mongoid::Association::Relatable> ] chain The embedded path
|
|
105
|
+
# accumulated from the ancestors above this inclusion (empty at the top).
|
|
106
|
+
def contribute(destination, chain)
|
|
107
|
+
stage = @pipeline.lookup_stage_for(@association)
|
|
108
|
+
@children.each { |child| child.contribute(stage['$lookup']['pipeline'], []) }
|
|
109
|
+
|
|
110
|
+
if chain.empty?
|
|
111
|
+
destination << stage
|
|
112
|
+
else
|
|
113
|
+
destination.concat(@pipeline.distribute(@association, chain, stage))
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# An embedded inclusion: it rides inside its own document, so it adds no
|
|
119
|
+
# stage of its own. Its children contribute to the same destination, with
|
|
120
|
+
# this document appended to their embedded path.
|
|
121
|
+
#
|
|
122
|
+
# For Computer.eager_load(port: :device) the :port inclusion emits nothing;
|
|
123
|
+
# it hands the path [ :port ] to :device, which EmbeddedDistributor then
|
|
124
|
+
# turns into stages.
|
|
125
|
+
#
|
|
126
|
+
# @api private
|
|
127
|
+
class EmbeddedInclusion < AssociationInclusion
|
|
128
|
+
class << self
|
|
129
|
+
# @param [ Mongoid::Association::Relatable ] association The inclusion.
|
|
130
|
+
#
|
|
131
|
+
# @return [ true | false ] Whether the association is embedded.
|
|
132
|
+
def for?(association)
|
|
133
|
+
association.embedded?
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Add no stage of its own; hand this document down the embedded path so the
|
|
138
|
+
# children distribute onto it.
|
|
139
|
+
#
|
|
140
|
+
# @param [ Array<Hash> ] destination The pipeline (or sub-pipeline) the
|
|
141
|
+
# stages are appended to.
|
|
142
|
+
# @param [ Array<Mongoid::Association::Relatable> ] chain The embedded path
|
|
143
|
+
# accumulated from the ancestors above this inclusion (empty at the top).
|
|
144
|
+
def contribute(destination, chain)
|
|
145
|
+
@children.each { |child| child.contribute(destination, chain + [ @association ]) }
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# A polymorphic inclusion: its target collection varies per document, so it
|
|
150
|
+
# can't be a $lookup. It adds nothing here; PolymorphicPreloader resolves it
|
|
151
|
+
# after the roots are materialized.
|
|
152
|
+
#
|
|
153
|
+
# @api private
|
|
154
|
+
class DeferredInclusion < AssociationInclusion
|
|
155
|
+
class << self
|
|
156
|
+
# @param [ Mongoid::Association::Relatable ] association The inclusion.
|
|
157
|
+
#
|
|
158
|
+
# @return [ true | false ] Whether the association is polymorphic.
|
|
159
|
+
def for?(association)
|
|
160
|
+
association.polymorphic?
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Add nothing; PolymorphicPreloader resolves the association after the
|
|
165
|
+
# roots are materialized.
|
|
166
|
+
#
|
|
167
|
+
# @param [ Array<Hash> ] destination The pipeline (unused).
|
|
168
|
+
# @param [ Array<Mongoid::Association::Relatable> ] chain The embedded path (unused).
|
|
169
|
+
def contribute(destination, chain); end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|