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.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/lib/config/locales/en.yml +69 -0
  3. data/lib/mongoid/association/accessors.rb +27 -5
  4. data/lib/mongoid/association/depending.rb +14 -10
  5. data/lib/mongoid/association/eager.rb +9 -0
  6. data/lib/mongoid/association/eager_load/discriminated_inclusion.rb +88 -0
  7. data/lib/mongoid/association/eager_load/embedded_distributor.rb +151 -0
  8. data/lib/mongoid/association/eager_load/inclusion.rb +173 -0
  9. data/lib/mongoid/association/eager_load/inclusion_tree.rb +96 -0
  10. data/lib/mongoid/association/eager_load/lookup_pipeline.rb +121 -0
  11. data/lib/mongoid/association/eager_load/polymorphic_preloader.rb +61 -0
  12. data/lib/mongoid/association/eager_load/polymorphic_targets.rb +151 -0
  13. data/lib/mongoid/association/eager_loadable.rb +65 -101
  14. data/lib/mongoid/association/embedded/batchable.rb +17 -1
  15. data/lib/mongoid/association/embedded/embeds_many/proxy.rb +6 -1
  16. data/lib/mongoid/association/embedded/embeds_one.rb +1 -0
  17. data/lib/mongoid/association/macros.rb +13 -1
  18. data/lib/mongoid/association/nested/many.rb +34 -5
  19. data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
  20. data/lib/mongoid/association/nested/one.rb +6 -1
  21. data/lib/mongoid/association/options.rb +17 -0
  22. data/lib/mongoid/association/referenced/belongs_to/eager.rb +6 -1
  23. data/lib/mongoid/association/referenced/belongs_to.rb +1 -0
  24. data/lib/mongoid/association/referenced/counter_cache.rb +3 -3
  25. data/lib/mongoid/association/referenced/has_and_belongs_to_many/buildable.rb +6 -0
  26. data/lib/mongoid/association/referenced/has_and_belongs_to_many.rb +5 -0
  27. data/lib/mongoid/association/referenced/has_many/enumerable.rb +3 -1
  28. data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
  29. data/lib/mongoid/association/referenced/has_many_through/eager.rb +99 -0
  30. data/lib/mongoid/association/referenced/has_many_through/proxy.rb +68 -0
  31. data/lib/mongoid/association/referenced/has_many_through.rb +201 -0
  32. data/lib/mongoid/association/referenced/has_one.rb +1 -0
  33. data/lib/mongoid/association/referenced/has_one_through/eager.rb +65 -0
  34. data/lib/mongoid/association/referenced/has_one_through/proxy.rb +30 -0
  35. data/lib/mongoid/association/referenced/has_one_through.rb +167 -0
  36. data/lib/mongoid/association/referenced.rb +2 -0
  37. data/lib/mongoid/association/relatable.rb +58 -0
  38. data/lib/mongoid/association.rb +15 -0
  39. data/lib/mongoid/atomic/modifiers.rb +100 -16
  40. data/lib/mongoid/clients/factory.rb +9 -13
  41. data/lib/mongoid/collection_configurable.rb +8 -0
  42. data/lib/mongoid/config/defaults.rb +0 -1
  43. data/lib/mongoid/config/encryption.rb +170 -22
  44. data/lib/mongoid/config.rb +56 -5
  45. data/lib/mongoid/contextual/aggregable/memory.rb +6 -2
  46. data/lib/mongoid/contextual/memory.rb +18 -7
  47. data/lib/mongoid/criteria/includable.rb +35 -11
  48. data/lib/mongoid/criteria/queryable/mergeable.rb +4 -0
  49. data/lib/mongoid/criteria/queryable/selectable.rb +123 -7
  50. data/lib/mongoid/deprecable.rb +45 -2
  51. data/lib/mongoid/document.rb +3 -0
  52. data/lib/mongoid/encryptable.rb +73 -0
  53. data/lib/mongoid/errors/config_redactor.rb +43 -0
  54. data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
  55. data/lib/mongoid/errors/invalid_encryption_schema.rb +27 -0
  56. data/lib/mongoid/errors/mixed_client_configuration.rb +1 -1
  57. data/lib/mongoid/errors/no_client_database.rb +1 -1
  58. data/lib/mongoid/errors/no_client_hosts.rb +1 -1
  59. data/lib/mongoid/errors/no_encryption_schema.rb +30 -0
  60. data/lib/mongoid/errors/readonly_association.rb +29 -0
  61. data/lib/mongoid/errors.rb +5 -0
  62. data/lib/mongoid/extensions/vector.rb +64 -0
  63. data/lib/mongoid/extensions.rb +1 -0
  64. data/lib/mongoid/field_readable.rb +69 -0
  65. data/lib/mongoid/fields.rb +7 -0
  66. data/lib/mongoid/indexable.rb +38 -26
  67. data/lib/mongoid/matchable.rb +6 -1
  68. data/lib/mongoid/matcher/eq_impl_with_regexp.rb +2 -4
  69. data/lib/mongoid/matcher/regex.rb +11 -12
  70. data/lib/mongoid/matcher/regexp_budget.rb +372 -0
  71. data/lib/mongoid/matcher.rb +1 -0
  72. data/lib/mongoid/persistable/creatable.rb +120 -14
  73. data/lib/mongoid/persistable/updatable.rb +9 -11
  74. data/lib/mongoid/persistable.rb +15 -15
  75. data/lib/mongoid/persistence_context.rb +77 -0
  76. data/lib/mongoid/search_indexable.rb +69 -24
  77. data/lib/mongoid/serializable.rb +12 -4
  78. data/lib/mongoid/tasks/database.rb +33 -16
  79. data/lib/mongoid/threaded.rb +36 -0
  80. data/lib/mongoid/timestamps/timeless.rb +84 -10
  81. data/lib/mongoid/touchable.rb +28 -1
  82. data/lib/mongoid/traversable.rb +2 -2
  83. data/lib/mongoid/validatable/associated.rb +18 -2
  84. data/lib/mongoid/version.rb +1 -1
  85. data/lib/mongoid/warnings.rb +4 -0
  86. data/spec/integration/app_spec.rb +7 -1
  87. data/spec/integration/associations/belongs_to_spec.rb +13 -0
  88. data/spec/integration/associations/embedded_delayed_atomic_sets_spec.rb +216 -0
  89. data/spec/integration/associations/has_and_belongs_to_many_spec.rb +31 -2
  90. data/spec/integration/atomic/modifiers_spec.rb +283 -0
  91. data/spec/integration/dots_and_dollars_spec.rb +12 -2
  92. data/spec/integration/encryption_spec.rb +300 -0
  93. data/spec/integration/matcher_operator_data/regex.yml +21 -0
  94. data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
  95. data/spec/integration/query_operator_guard_spec.rb +89 -0
  96. data/spec/mongoid/association/accessors_spec.rb +35 -0
  97. data/spec/mongoid/association/eager_loadable_spec.rb +47 -0
  98. data/spec/mongoid/association/eager_spec.rb +2 -2
  99. data/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb +33 -0
  100. data/spec/mongoid/association/fallback_spec.rb +425 -0
  101. data/spec/mongoid/association/macros_spec.rb +26 -0
  102. data/spec/mongoid/association/referenced/belongs_to/eager_spec.rb +17 -0
  103. data/spec/mongoid/association/referenced/has_and_belongs_to_many/buildable_spec.rb +11 -0
  104. data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
  105. data/spec/mongoid/association/referenced/has_and_belongs_to_many_spec.rb +50 -0
  106. data/spec/mongoid/association/referenced/has_many/eager_spec.rb +33 -0
  107. data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
  108. data/spec/mongoid/association/referenced/has_many_through/eager_spec.rb +58 -0
  109. data/spec/mongoid/association/referenced/has_many_through/proxy_spec.rb +62 -0
  110. data/spec/mongoid/association/referenced/has_many_through_spec.rb +399 -0
  111. data/spec/mongoid/association/referenced/has_one_through/eager_spec.rb +58 -0
  112. data/spec/mongoid/association/referenced/has_one_through/proxy_spec.rb +49 -0
  113. data/spec/mongoid/association/referenced/has_one_through_spec.rb +211 -0
  114. data/spec/mongoid/atomic/modifiers_spec.rb +347 -0
  115. data/spec/mongoid/attributes/nested_spec.rb +278 -8
  116. data/spec/mongoid/clients/factory_spec.rb +36 -0
  117. data/spec/mongoid/config/defaults_spec.rb +33 -0
  118. data/spec/mongoid/config/encryption_spec.rb +451 -0
  119. data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
  120. data/spec/mongoid/contextual/memory_spec.rb +177 -0
  121. data/spec/mongoid/criteria/includable_spec.rb +1221 -0
  122. data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +15 -1
  123. data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +201 -0
  124. data/spec/mongoid/criteria_spec.rb +3 -0
  125. data/spec/mongoid/encryptable_spec.rb +115 -0
  126. data/spec/mongoid/errors/config_redactor_spec.rb +112 -0
  127. data/spec/mongoid/errors/mixed_client_configuration_spec.rb +31 -0
  128. data/spec/mongoid/errors/no_client_database_spec.rb +29 -0
  129. data/spec/mongoid/errors/no_client_hosts_spec.rb +26 -0
  130. data/spec/mongoid/errors/readonly_association_spec.rb +34 -0
  131. data/spec/mongoid/extensions/vector_spec.rb +175 -0
  132. data/spec/mongoid/fields_spec.rb +8 -0
  133. data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
  134. data/spec/mongoid/search_indexable_spec.rb +321 -9
  135. data/spec/mongoid/tasks/database_spec.rb +31 -0
  136. data/spec/mongoid/timestamps/timeless_spec.rb +197 -5
  137. data/spec/mongoid/touchable_spec.rb +242 -0
  138. data/spec/mongoid/touchable_spec_models.rb +47 -0
  139. data/spec/mongoid/validatable/associated_spec.rb +19 -0
  140. data/spec/spec_helper.rb +5 -1
  141. data/spec/support/crypt/models.rb +253 -0
  142. data/spec/support/models/modifier_models.rb +65 -0
  143. metadata +55 -2
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mongoid/association/referenced/has_many_through/proxy'
4
+ require 'mongoid/association/referenced/has_many_through/eager'
5
+
6
+ module Mongoid
7
+ module Association
8
+ module Referenced
9
+ # Metadata class for has_many :through associations.
10
+ class HasManyThrough
11
+ include Relatable
12
+
13
+ # The options available for this type of association, in addition to the
14
+ # common ones.
15
+ #
16
+ # @return [ Array<Symbol> ] The extra valid options.
17
+ ASSOCIATION_OPTIONS = %i[order source through].freeze
18
+
19
+ # The complete list of valid options for this association, including
20
+ # the shared ones.
21
+ #
22
+ # @return [ Array<Symbol> ] The valid options.
23
+ VALID_OPTIONS = (ASSOCIATION_OPTIONS + SHARED_OPTIONS).freeze
24
+
25
+ # The list of association complements.
26
+ #
27
+ # @return [ Array ]
28
+ def relation_complements
29
+ [].freeze
30
+ end
31
+
32
+ # Setup instance methods on the owner class.
33
+ #
34
+ # @return [ self ]
35
+ def setup!
36
+ setup_instance_methods!
37
+ self
38
+ end
39
+
40
+ # Is this association embedded?
41
+ #
42
+ # @return [ false ]
43
+ def embedded?
44
+ false
45
+ end
46
+
47
+ # The proxy class for this association type.
48
+ #
49
+ # @return [ Class ]
50
+ def relation
51
+ Proxy
52
+ end
53
+
54
+ # Through associations never store a foreign key on the owner document.
55
+ #
56
+ # @return [ false ]
57
+ def stores_foreign_key?
58
+ false
59
+ end
60
+
61
+ # The intermediate association metadata on the owner class.
62
+ # Resolved lazily to allow forward references.
63
+ #
64
+ # @return [ Mongoid::Association::Relatable ]
65
+ def through_association
66
+ @through_association ||= begin
67
+ assoc = @owner_class.relations[@options[:through].to_s] ||
68
+ raise(
69
+ Errors::InvalidRelationOption.new(
70
+ @owner_class, name, :through, @options[:through]
71
+ )
72
+ )
73
+ if assoc.embedded?
74
+ raise(
75
+ Errors::InvalidRelationOption.new(
76
+ @owner_class, name, :through,
77
+ 'through association must be a referenced association, not embedded'
78
+ )
79
+ )
80
+ end
81
+ assoc
82
+ end
83
+ end
84
+
85
+ # The source association metadata on the intermediate class.
86
+ # Resolved lazily to allow forward references.
87
+ #
88
+ # @return [ Mongoid::Association::Relatable ]
89
+ def source_association
90
+ @source_association ||= begin
91
+ source_name = (@options[:source] || name.to_s.singularize).to_s
92
+ assoc = through_association.klass.relations[source_name] ||
93
+ raise(
94
+ Errors::InvalidRelationOption.new(
95
+ @owner_class, name, :source, source_name
96
+ )
97
+ )
98
+ if assoc.is_a?(Referenced::HasAndBelongsToMany)
99
+ raise(
100
+ Errors::InvalidRelationOption.new(
101
+ @owner_class, name, :source,
102
+ 'has_and_belongs_to_many is not supported as a :through source'
103
+ )
104
+ )
105
+ end
106
+ assoc
107
+ end
108
+ end
109
+
110
+ # Return a Criteria scoped to the target documents reachable from base
111
+ # via the through association. Performs two queries: one against the
112
+ # intermediate collection, one against the source collection.
113
+ #
114
+ # @param [ Document ] base The owner document.
115
+ #
116
+ # @return [ Mongoid::Criteria ]
117
+ def criteria(base)
118
+ through_crit = through_association.criteria(base)
119
+
120
+ crit = if source_association.stores_foreign_key?
121
+ # FK is on the intermediate (e.g. appointment.patient_id -> belongs_to :patient)
122
+ target_pk = source_association.primary_key # '_id' on Patient
123
+ source_fk = source_association.foreign_key # 'patient_id' on Appointment
124
+ source_association.klass.where(
125
+ target_pk => { '$in' => key_values(through_crit, source_fk) }
126
+ )
127
+ else
128
+ # FK is on the source (e.g. reader.book_id -> has_many :readers on Book)
129
+ source_pk = source_association.primary_key # '_id' on intermediate (Book)
130
+ source_fk = source_association.foreign_key # 'book_id' on Reader
131
+ source_association.klass.where(
132
+ source_fk => { '$in' => key_values(through_crit, source_pk) }
133
+ )
134
+ end
135
+ order ? crit.order_by(order) : crit
136
+ end
137
+
138
+ # The default for validating the association object.
139
+ #
140
+ # @return [ false ]
141
+ def validation_default
142
+ false
143
+ end
144
+
145
+ private
146
+
147
+ # Pluck the given key from the criteria, dropping intermediates that
148
+ # have no value for it. A nil in an $in array matches every document
149
+ # whose queried field is null or absent, which would return documents
150
+ # outside the association. An empty result correctly matches nothing.
151
+ # Mirrors what the eager loader does.
152
+ #
153
+ # @param [ Mongoid::Criteria ] criteria The intermediate criteria.
154
+ # @param [ String ] key The key to pluck.
155
+ #
156
+ # @return [ Array<Object> ] The usable key values.
157
+ def key_values(criteria, key)
158
+ criteria.pluck(key).compact.uniq
159
+ end
160
+
161
+ def setup_instance_methods!
162
+ define_through_getter!
163
+ define_through_ids_getter!
164
+ define_readonly_setter!
165
+ define_existence_check!
166
+ self
167
+ end
168
+
169
+ def define_through_getter!
170
+ assoc = self
171
+ assoc_name = name
172
+ @owner_class.re_define_method(assoc_name) do |reload = false|
173
+ if reload || !instance_variable_defined?("@_#{assoc_name}")
174
+ set_relation(assoc_name, HasManyThrough::Proxy.new(self, assoc))
175
+ end
176
+ instance_variable_get("@_#{assoc_name}")
177
+ end
178
+ end
179
+
180
+ def define_through_ids_getter!
181
+ assoc_name = name
182
+ ids_method = :"#{assoc_name.to_s.singularize}_ids"
183
+ @owner_class.re_define_method(ids_method) do
184
+ send(assoc_name).pluck(:_id)
185
+ end
186
+ end
187
+
188
+ def define_readonly_setter!
189
+ assoc = self
190
+ @owner_class.re_define_method(:"#{name}=") do |_object|
191
+ raise Mongoid::Errors::ReadonlyAssociation.new(self.class, assoc)
192
+ end
193
+ end
194
+
195
+ def default_primary_key
196
+ PRIMARY_KEY_DEFAULT
197
+ end
198
+ end
199
+ end
200
+ end
201
+ end
@@ -22,6 +22,7 @@ module Mongoid
22
22
  autobuild
23
23
  autosave
24
24
  dependent
25
+ fallback
25
26
  foreign_key
26
27
  primary_key
27
28
  scope
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongoid
4
+ module Association
5
+ module Referenced
6
+ class HasOneThrough
7
+ # Two-query eager preloader for has_one :through associations.
8
+ class Eager < Association::Eager
9
+ private
10
+
11
+ def preload
12
+ @docs.each { |d| set_relation(d, nil) }
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
+ intermediates = through_assoc.klass.where(through_fk => { '$in' => owner_ids }).to_a
24
+ intermediate_to_target = build_intermediate_to_target(intermediates, through_fk, source_assoc)
25
+
26
+ @docs.each do |doc|
27
+ owner_key_val = doc.public_send(owner_pk)
28
+ target = intermediate_to_target[owner_key_val]
29
+ proxy = target ? HasOneThrough::Proxy.new(doc, target, @association) : nil
30
+ doc.set_relation(@association.name, proxy) unless doc.blank?
31
+ end
32
+ end
33
+
34
+ def build_intermediate_to_target(intermediates, through_fk, source_assoc)
35
+ if source_assoc.stores_foreign_key?
36
+ # FK is on the intermediate (e.g. belongs_to :store => intermediate.store_id)
37
+ source_fk_values = intermediates.filter_map { |i| i.public_send(source_assoc.foreign_key) }.uniq
38
+ targets = source_assoc.klass.where(source_assoc.primary_key => { '$in' => source_fk_values }).to_a
39
+ targets_by_key = targets.index_by { |t| t.public_send(source_assoc.primary_key) }
40
+ intermediates.to_h do |i|
41
+ [ i.public_send(through_fk), targets_by_key[i.public_send(source_assoc.foreign_key)] ]
42
+ end
43
+ else
44
+ # FK is on the source (e.g. has_one :store => store.franchise_id)
45
+ source_pk = source_assoc.primary_key
46
+ intermediate_pks = intermediates.filter_map { |i| i.public_send(source_pk) }.uniq
47
+ targets = source_assoc.klass.where(source_assoc.foreign_key => { '$in' => intermediate_pks }).to_a
48
+ targets_by_fk = targets.index_by { |t| t.public_send(source_assoc.foreign_key) }
49
+ intermediates.index_by { |i| i.public_send(through_fk) }.transform_values do |i|
50
+ targets_by_fk[i.public_send(source_pk)]
51
+ end
52
+ end
53
+ end
54
+
55
+ # Required by the base class contract. Not called by this preloader
56
+ # because preload manages document traversal directly without using
57
+ # the grouped_docs / keys_from_docs machinery from the base class.
58
+ def group_by_key
59
+ @association.through_association.primary_key
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongoid
4
+ module Association
5
+ module Referenced
6
+ class HasOneThrough
7
+ # Read-only proxy for has_one :through associations.
8
+ # Instances are returned by the association getter. Write attempts raise
9
+ # ReadonlyAssociation.
10
+ class Proxy < Association::One
11
+ module ClassMethods
12
+ def eager_loader(association, docs)
13
+ Eager.new(association, docs)
14
+ end
15
+
16
+ # Returns true if the association is an embedded one. In this case
17
+ # always false.
18
+ #
19
+ # @return [ false ] Always false.
20
+ def embedded?
21
+ false
22
+ end
23
+ end
24
+
25
+ extend ClassMethods
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mongoid/association/referenced/has_one_through/proxy'
4
+ require 'mongoid/association/referenced/has_one_through/eager'
5
+
6
+ module Mongoid
7
+ module Association
8
+ module Referenced
9
+ # Metadata class for has_one :through associations.
10
+ class HasOneThrough
11
+ include Relatable
12
+
13
+ # The options available for this type of association, in addition to the
14
+ # common ones.
15
+ #
16
+ # @return [ Array<Symbol> ] The extra valid options.
17
+ ASSOCIATION_OPTIONS = %i[source through].freeze
18
+
19
+ # The complete list of valid options for this association, including
20
+ # the shared ones.
21
+ #
22
+ # @return [ Array<Symbol> ] The valid options.
23
+ VALID_OPTIONS = (ASSOCIATION_OPTIONS + SHARED_OPTIONS).freeze
24
+
25
+ # The list of association complements.
26
+ #
27
+ # @return [ Array ]
28
+ def relation_complements
29
+ [].freeze
30
+ end
31
+
32
+ # Setup instance methods on the owner class.
33
+ #
34
+ # @return [ self ]
35
+ def setup!
36
+ setup_instance_methods!
37
+ self
38
+ end
39
+
40
+ # Is this association embedded?
41
+ #
42
+ # @return [ false ]
43
+ def embedded?
44
+ false
45
+ end
46
+
47
+ # The proxy class for this association type.
48
+ #
49
+ # @return [ Class ]
50
+ def relation
51
+ Proxy
52
+ end
53
+
54
+ # The intermediate association metadata on the owner class.
55
+ # Resolved lazily to allow forward references.
56
+ #
57
+ # @return [ Mongoid::Association::Relatable ]
58
+ def through_association
59
+ @through_association ||= begin
60
+ assoc = @owner_class.relations[@options[:through].to_s] ||
61
+ raise(
62
+ Errors::InvalidRelationOption.new(
63
+ @owner_class, name, :through, @options[:through]
64
+ )
65
+ )
66
+ if assoc.embedded?
67
+ raise(
68
+ Errors::InvalidRelationOption.new(
69
+ @owner_class, name, :through,
70
+ 'through association must be a referenced association, not embedded'
71
+ )
72
+ )
73
+ end
74
+ assoc
75
+ end
76
+ end
77
+
78
+ # The source association metadata on the intermediate class.
79
+ # Resolved lazily to allow forward references.
80
+ #
81
+ # @return [ Mongoid::Association::Relatable ]
82
+ def source_association
83
+ @source_association ||= begin
84
+ source_name = (@options[:source] || name).to_s
85
+ assoc = through_association.klass.relations[source_name] ||
86
+ raise(
87
+ Errors::InvalidRelationOption.new(
88
+ @owner_class, name, :source, source_name
89
+ )
90
+ )
91
+ if assoc.is_a?(Referenced::HasAndBelongsToMany)
92
+ raise(
93
+ Errors::InvalidRelationOption.new(
94
+ @owner_class, name, :source,
95
+ 'has_and_belongs_to_many is not supported as a :through source'
96
+ )
97
+ )
98
+ end
99
+ assoc
100
+ end
101
+ end
102
+
103
+ # Through associations never store a foreign key on the owner document.
104
+ #
105
+ # @return [ false ]
106
+ def stores_foreign_key?
107
+ false
108
+ end
109
+
110
+ # Resolve the target by delegating through the intermediate proxy.
111
+ # Unlike other association types, this returns a document (or nil)
112
+ # directly rather than a Mongoid::Criteria, because the two-hop
113
+ # traversal is performed eagerly via the existing association proxy.
114
+ #
115
+ # @param [ Document ] base The owner document.
116
+ #
117
+ # @return [ Document | nil ]
118
+ def criteria(base)
119
+ through_target = base.public_send(through_association.name)
120
+ return nil if through_target.nil?
121
+
122
+ through_target.public_send(source_association.name)
123
+ end
124
+
125
+ # The default for validating the association object.
126
+ #
127
+ # @return [ false ]
128
+ def validation_default
129
+ false
130
+ end
131
+
132
+ private
133
+
134
+ def setup_instance_methods!
135
+ define_through_getter!
136
+ define_readonly_setter!
137
+ define_existence_check!
138
+ self
139
+ end
140
+
141
+ def define_through_getter!
142
+ assoc = self
143
+ assoc_name = name
144
+ @owner_class.re_define_method(assoc_name) do |reload = false|
145
+ if reload || !instance_variable_defined?("@_#{assoc_name}")
146
+ doc = assoc.criteria(self)
147
+ proxy = doc ? HasOneThrough::Proxy.new(self, doc, assoc) : nil
148
+ set_relation(assoc_name, proxy)
149
+ end
150
+ instance_variable_get("@_#{assoc_name}")
151
+ end
152
+ end
153
+
154
+ def define_readonly_setter!
155
+ assoc = self
156
+ @owner_class.re_define_method("#{name}=") do |_object|
157
+ raise Mongoid::Errors::ReadonlyAssociation.new(self.class, assoc)
158
+ end
159
+ end
160
+
161
+ def default_primary_key
162
+ PRIMARY_KEY_DEFAULT
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -7,3 +7,5 @@ require 'mongoid/association/referenced/belongs_to'
7
7
  require 'mongoid/association/referenced/has_many'
8
8
  require 'mongoid/association/referenced/has_and_belongs_to_many'
9
9
  require 'mongoid/association/referenced/has_one'
10
+ require 'mongoid/association/referenced/has_one_through'
11
+ require 'mongoid/association/referenced/has_many_through'
@@ -177,6 +177,23 @@ module Mongoid
177
177
  end
178
178
  alias klass relation_class
179
179
 
180
+ # The class of the association target, or nil when the named class is
181
+ # not defined.
182
+ #
183
+ # An association may name a class that never gets defined. The
184
+ # association is then unusable, but its owner still has to be. Callers
185
+ # that walk every association of every model, rather than following the
186
+ # one the application asked for, use this instead of relation_class.
187
+ #
188
+ # @return [ Class | nil ] The association objects' class.
189
+ #
190
+ # @api private
191
+ def try_relation_class
192
+ relation_class
193
+ rescue NameError
194
+ nil
195
+ end
196
+
180
197
  # The class name of the object owning this association.
181
198
  #
182
199
  # @return [ String ] The owning objects' class name.
@@ -200,6 +217,24 @@ module Mongoid
200
217
  stores_foreign_key? ? foreign_key : primary_key
201
218
  end
202
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
+
203
238
  # The name of the setter on this object for assigning an associated object.
204
239
  #
205
240
  # @return [ String ] The setter name.
@@ -342,6 +377,15 @@ module Mongoid
342
377
  [ Referenced::BelongsTo, Embedded::EmbeddedIn ].any? { |a| is_a?(a) }
343
378
  end
344
379
 
380
+ # Is this association a many-to-many association? Such an association stores
381
+ # its foreign keys as an array on the document itself.
382
+ #
383
+ # @return [ true | false ] true if it is a has_and_belongs_to_many
384
+ # association, false if not.
385
+ def many_to_many?
386
+ is_a?(Referenced::HasAndBelongsToMany)
387
+ end
388
+
345
389
  private
346
390
 
347
391
  # Gets the model classes with inverse associations of this model. This is used to determine
@@ -413,6 +457,20 @@ module Mongoid
413
457
  end
414
458
  end
415
459
 
460
+ if @options.key?(:fallback)
461
+ unless @options[:fallback].respond_to?(:call)
462
+ raise ArgumentError,
463
+ "The :fallback option for association '#{name}' on " \
464
+ "#{@owner_class} must be a Proc or lambda."
465
+ end
466
+
467
+ if @options.key?(:autobuild)
468
+ raise ArgumentError,
469
+ "The :fallback option for association '#{name}' on " \
470
+ "#{@owner_class} cannot be combined with :autobuild."
471
+ end
472
+ end
473
+
416
474
  [ name, :"#{name}?", :"#{name}=" ].each do |n|
417
475
  raise Errors::InvalidRelation.new(@owner_class, n) if Mongoid.destructive_fields.include?(n)
418
476
  end
@@ -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.
@@ -46,6 +54,13 @@ module Mongoid
46
54
  belongs_to: Association::Referenced::BelongsTo
47
55
  }.freeze
48
56
 
57
+ # Internal mapping used when :through option is present. Not exposed as
58
+ # callable macros.
59
+ THROUGH_MACRO_MAPPING = {
60
+ has_one: Association::Referenced::HasOneThrough,
61
+ has_many: Association::Referenced::HasManyThrough
62
+ }.freeze
63
+
49
64
  attr_accessor :_association
50
65
 
51
66
  included do