mongoid 8.0.10 → 8.1.0

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 (212) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +3 -3
  4. data/README.md +3 -3
  5. data/Rakefile +18 -67
  6. data/lib/config/locales/en.yml +46 -14
  7. data/lib/mongoid/association/accessors.rb +3 -7
  8. data/lib/mongoid/association/builders.rb +1 -1
  9. data/lib/mongoid/association/eager_loadable.rb +0 -3
  10. data/lib/mongoid/association/embedded/batchable.rb +2 -2
  11. data/lib/mongoid/association/embedded/embedded_in/buildable.rb +2 -2
  12. data/lib/mongoid/association/embedded/embedded_in/proxy.rb +2 -1
  13. data/lib/mongoid/association/embedded/embeds_many/buildable.rb +3 -2
  14. data/lib/mongoid/association/embedded/embeds_many/proxy.rb +6 -6
  15. data/lib/mongoid/association/embedded/embeds_one/buildable.rb +1 -1
  16. data/lib/mongoid/association/embedded/embeds_one/proxy.rb +1 -1
  17. data/lib/mongoid/association/macros.rb +0 -6
  18. data/lib/mongoid/association/nested/one.rb +40 -2
  19. data/lib/mongoid/association/proxy.rb +1 -1
  20. data/lib/mongoid/association/referenced/counter_cache.rb +2 -2
  21. data/lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb +1 -1
  22. data/lib/mongoid/association/referenced/has_many/enumerable.rb +6 -23
  23. data/lib/mongoid/association/referenced/has_many/proxy.rb +3 -3
  24. data/lib/mongoid/association/reflections.rb +2 -2
  25. data/lib/mongoid/atomic.rb +7 -16
  26. data/lib/mongoid/attributes/dynamic.rb +1 -1
  27. data/lib/mongoid/attributes/nested.rb +2 -2
  28. data/lib/mongoid/attributes/processing.rb +5 -29
  29. data/lib/mongoid/attributes/projector.rb +1 -1
  30. data/lib/mongoid/attributes/readonly.rb +1 -1
  31. data/lib/mongoid/attributes.rb +8 -2
  32. data/lib/mongoid/changeable.rb +107 -5
  33. data/lib/mongoid/clients/storage_options.rb +2 -5
  34. data/lib/mongoid/clients/validators/storage.rb +1 -13
  35. data/lib/mongoid/collection_configurable.rb +58 -0
  36. data/lib/mongoid/composable.rb +2 -0
  37. data/lib/mongoid/config/defaults.rb +60 -0
  38. data/lib/mongoid/config/options.rb +0 -3
  39. data/lib/mongoid/config/validators/async_query_executor.rb +24 -0
  40. data/lib/mongoid/config/validators.rb +1 -0
  41. data/lib/mongoid/config.rb +88 -27
  42. data/lib/mongoid/contextual/atomic.rb +1 -1
  43. data/lib/mongoid/contextual/memory.rb +233 -33
  44. data/lib/mongoid/contextual/mongo/documents_loader.rb +177 -0
  45. data/lib/mongoid/contextual/mongo.rb +370 -133
  46. data/lib/mongoid/contextual/none.rb +162 -7
  47. data/lib/mongoid/contextual.rb +12 -0
  48. data/lib/mongoid/criteria/findable.rb +2 -2
  49. data/lib/mongoid/criteria/includable.rb +4 -3
  50. data/lib/mongoid/criteria/queryable/extensions/numeric.rb +1 -15
  51. data/lib/mongoid/criteria/queryable/key.rb +1 -1
  52. data/lib/mongoid/criteria/queryable/mergeable.rb +1 -1
  53. data/lib/mongoid/criteria/queryable/optional.rb +8 -8
  54. data/lib/mongoid/criteria/queryable/selectable.rb +43 -12
  55. data/lib/mongoid/criteria/queryable/selector.rb +1 -1
  56. data/lib/mongoid/criteria/queryable/storable.rb +1 -1
  57. data/lib/mongoid/criteria.rb +6 -5
  58. data/lib/mongoid/deprecable.rb +1 -2
  59. data/lib/mongoid/deprecation.rb +3 -3
  60. data/lib/mongoid/document.rb +1 -8
  61. data/lib/mongoid/errors/create_collection_failure.rb +33 -0
  62. data/lib/mongoid/errors/drop_collection_failure.rb +27 -0
  63. data/lib/mongoid/errors/immutable_attribute.rb +26 -0
  64. data/lib/mongoid/errors/invalid_async_query_executor.rb +25 -0
  65. data/lib/mongoid/errors/invalid_global_executor_concurrency.rb +22 -0
  66. data/lib/mongoid/errors/invalid_storage_parent.rb +2 -0
  67. data/lib/mongoid/errors.rb +4 -1
  68. data/lib/mongoid/extensions/hash.rb +2 -24
  69. data/lib/mongoid/extensions/object.rb +2 -2
  70. data/lib/mongoid/extensions/time.rb +2 -0
  71. data/lib/mongoid/fields/localized.rb +10 -0
  72. data/lib/mongoid/fields/standard.rb +10 -0
  73. data/lib/mongoid/fields.rb +59 -35
  74. data/lib/mongoid/findable.rb +27 -3
  75. data/lib/mongoid/interceptable.rb +6 -116
  76. data/lib/mongoid/matcher/eq_impl.rb +1 -1
  77. data/lib/mongoid/matcher/type.rb +1 -1
  78. data/lib/mongoid/persistable/creatable.rb +1 -0
  79. data/lib/mongoid/persistable/deletable.rb +1 -1
  80. data/lib/mongoid/persistable/savable.rb +13 -1
  81. data/lib/mongoid/persistable/unsettable.rb +2 -2
  82. data/lib/mongoid/persistable/updatable.rb +51 -1
  83. data/lib/mongoid/persistable/upsertable.rb +20 -1
  84. data/lib/mongoid/persistable.rb +3 -0
  85. data/lib/mongoid/query_cache.rb +5 -1
  86. data/lib/mongoid/railties/database.rake +7 -2
  87. data/lib/mongoid/reloadable.rb +5 -3
  88. data/lib/mongoid/stateful.rb +22 -1
  89. data/lib/mongoid/tasks/database.rake +12 -0
  90. data/lib/mongoid/tasks/database.rb +20 -0
  91. data/lib/mongoid/timestamps/created.rb +1 -8
  92. data/lib/mongoid/traversable.rb +0 -12
  93. data/lib/mongoid/utils.rb +22 -0
  94. data/lib/mongoid/validatable/associated.rb +17 -98
  95. data/lib/mongoid/validatable/macros.rb +5 -5
  96. data/lib/mongoid/validatable.rb +4 -9
  97. data/lib/mongoid/version.rb +1 -1
  98. data/lib/mongoid/warnings.rb +17 -1
  99. data/lib/mongoid.rb +16 -3
  100. data/spec/integration/app_spec.rb +2 -6
  101. data/spec/integration/associations/has_and_belongs_to_many_spec.rb +0 -40
  102. data/spec/integration/callbacks_spec.rb +99 -12
  103. data/spec/integration/discriminator_key_spec.rb +4 -5
  104. data/spec/integration/i18n_fallbacks_spec.rb +3 -2
  105. data/spec/mongoid/association/eager_spec.rb +2 -24
  106. data/spec/mongoid/association/embedded/embedded_in/proxy_spec.rb +27 -0
  107. data/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb +20 -25
  108. data/spec/mongoid/association/embedded/embeds_many_models.rb +1 -0
  109. data/spec/mongoid/association/embedded/embeds_many_query_spec.rb +0 -4
  110. data/spec/mongoid/association/embedded/embeds_one/proxy_spec.rb +15 -2
  111. data/spec/mongoid/association/referenced/belongs_to_spec.rb +2 -18
  112. data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +42 -55
  113. data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +9 -50
  114. data/spec/mongoid/association/syncable_spec.rb +1 -1
  115. data/spec/mongoid/association_spec.rb +0 -60
  116. data/spec/mongoid/attributes_spec.rb +3 -33
  117. data/spec/mongoid/changeable_spec.rb +299 -24
  118. data/spec/mongoid/clients_spec.rb +122 -13
  119. data/spec/mongoid/collection_configurable_spec.rb +158 -0
  120. data/spec/mongoid/config/defaults_spec.rb +160 -0
  121. data/spec/mongoid/config_spec.rb +154 -27
  122. data/spec/mongoid/contextual/memory_spec.rb +332 -76
  123. data/spec/mongoid/contextual/mongo/documents_loader_spec.rb +187 -0
  124. data/spec/mongoid/contextual/mongo_spec.rb +1009 -125
  125. data/spec/mongoid/contextual/none_spec.rb +49 -2
  126. data/spec/mongoid/copyable_spec.rb +2 -10
  127. data/spec/mongoid/criteria/queryable/extensions/string_spec.rb +4 -10
  128. data/spec/mongoid/criteria/queryable/options_spec.rb +1 -1
  129. data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +419 -0
  130. data/spec/mongoid/criteria/queryable/selectable_spec.rb +1 -1
  131. data/spec/mongoid/criteria/queryable/selector_spec.rb +3 -76
  132. data/spec/mongoid/criteria/queryable/storable_spec.rb +0 -72
  133. data/spec/mongoid/criteria_projection_spec.rb +1 -4
  134. data/spec/mongoid/criteria_spec.rb +5 -9
  135. data/spec/mongoid/document_spec.rb +0 -27
  136. data/spec/mongoid/errors/readonly_document_spec.rb +2 -2
  137. data/spec/mongoid/extensions/hash_spec.rb +3 -3
  138. data/spec/mongoid/extensions/time_spec.rb +8 -43
  139. data/spec/mongoid/extensions/time_with_zone_spec.rb +7 -52
  140. data/spec/mongoid/fields/localized_spec.rb +46 -28
  141. data/spec/mongoid/fields_spec.rb +136 -77
  142. data/spec/mongoid/findable_spec.rb +391 -34
  143. data/spec/mongoid/indexable_spec.rb +16 -10
  144. data/spec/mongoid/interceptable_spec.rb +153 -442
  145. data/spec/mongoid/interceptable_spec_models.rb +111 -51
  146. data/spec/mongoid/persistable/deletable_spec.rb +26 -6
  147. data/spec/mongoid/persistable/destroyable_spec.rb +26 -6
  148. data/spec/mongoid/persistable/incrementable_spec.rb +37 -0
  149. data/spec/mongoid/persistable/logical_spec.rb +37 -0
  150. data/spec/mongoid/persistable/poppable_spec.rb +36 -0
  151. data/spec/mongoid/persistable/pullable_spec.rb +72 -0
  152. data/spec/mongoid/persistable/pushable_spec.rb +72 -0
  153. data/spec/mongoid/persistable/renamable_spec.rb +36 -0
  154. data/spec/mongoid/persistable/savable_spec.rb +96 -0
  155. data/spec/mongoid/persistable/settable_spec.rb +37 -0
  156. data/spec/mongoid/persistable/unsettable_spec.rb +36 -0
  157. data/spec/mongoid/persistable/updatable_spec.rb +20 -28
  158. data/spec/mongoid/persistable/upsertable_spec.rb +80 -6
  159. data/spec/mongoid/persistence_context_spec.rb +7 -57
  160. data/spec/mongoid/query_cache_spec.rb +56 -61
  161. data/spec/mongoid/reloadable_spec.rb +24 -28
  162. data/spec/mongoid/scopable_spec.rb +70 -0
  163. data/spec/mongoid/serializable_spec.rb +23 -44
  164. data/spec/mongoid/stateful_spec.rb +122 -8
  165. data/spec/mongoid/tasks/database_rake_spec.rb +74 -0
  166. data/spec/mongoid/tasks/database_spec.rb +127 -0
  167. data/spec/mongoid/timestamps/created_spec.rb +0 -23
  168. data/spec/mongoid/timestamps_spec.rb +9 -11
  169. data/spec/mongoid/touchable_spec.rb +277 -5
  170. data/spec/mongoid/touchable_spec_models.rb +3 -1
  171. data/spec/mongoid/traversable_spec.rb +9 -24
  172. data/spec/mongoid/validatable/associated_spec.rb +34 -27
  173. data/spec/mongoid/validatable/uniqueness_spec.rb +2 -3
  174. data/spec/mongoid_spec.rb +36 -10
  175. data/spec/shared/LICENSE +20 -0
  176. data/spec/shared/bin/get-mongodb-download-url +17 -0
  177. data/spec/shared/bin/s3-copy +45 -0
  178. data/spec/shared/bin/s3-upload +69 -0
  179. data/spec/shared/lib/mrss/child_process_helper.rb +80 -0
  180. data/spec/shared/lib/mrss/cluster_config.rb +231 -0
  181. data/spec/shared/lib/mrss/constraints.rb +378 -0
  182. data/spec/shared/lib/mrss/docker_runner.rb +298 -0
  183. data/spec/shared/lib/mrss/eg_config_utils.rb +51 -0
  184. data/spec/shared/lib/mrss/event_subscriber.rb +210 -0
  185. data/spec/shared/lib/mrss/lite_constraints.rb +238 -0
  186. data/spec/shared/lib/mrss/server_version_registry.rb +113 -0
  187. data/spec/shared/lib/mrss/session_registry.rb +69 -0
  188. data/spec/shared/lib/mrss/session_registry_legacy.rb +60 -0
  189. data/spec/shared/lib/mrss/spec_organizer.rb +179 -0
  190. data/spec/shared/lib/mrss/utils.rb +37 -0
  191. data/spec/shared/share/Dockerfile.erb +321 -0
  192. data/spec/shared/share/haproxy-1.conf +16 -0
  193. data/spec/shared/share/haproxy-2.conf +17 -0
  194. data/spec/shared/shlib/config.sh +27 -0
  195. data/spec/shared/shlib/distro.sh +74 -0
  196. data/spec/shared/shlib/server.sh +416 -0
  197. data/spec/shared/shlib/set_env.sh +169 -0
  198. data/spec/spec_helper.rb +5 -0
  199. data/spec/support/immutable_ids.rb +118 -0
  200. data/spec/support/macros.rb +47 -15
  201. data/spec/support/models/artist.rb +0 -1
  202. data/spec/support/models/band.rb +1 -0
  203. data/spec/support/models/building.rb +2 -0
  204. data/spec/support/models/name.rb +0 -10
  205. data/spec/support/models/person.rb +0 -1
  206. data/spec/support/models/product.rb +1 -0
  207. data.tar.gz.sig +0 -0
  208. metadata +745 -637
  209. metadata.gz.sig +2 -0
  210. data/spec/mongoid/criteria/queryable/extensions/bignum_spec.rb +0 -60
  211. data/spec/mongoid/criteria/queryable/extensions/fixnum_spec.rb +0 -60
  212. data/spec/support/models/purse.rb +0 -9
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "mongoid/config/defaults"
3
4
  require "mongoid/config/environment"
4
5
  require "mongoid/config/options"
5
6
  require "mongoid/config/validators"
@@ -11,6 +12,7 @@ module Mongoid
11
12
  module Config
12
13
  extend Forwardable
13
14
  extend Options
15
+ extend Defaults
14
16
  extend self
15
17
 
16
18
  def_delegators ::Mongoid, :logger, :logger=
@@ -70,6 +72,7 @@ module Mongoid
70
72
 
71
73
  # Use ActiveSupport's time zone in time operations instead of the
72
74
  # Ruby default time zone.
75
+ # @deprecated
73
76
  option :use_activesupport_time_zone, default: true
74
77
 
75
78
  # Return stored times as UTC.
@@ -125,22 +128,35 @@ module Mongoid
125
128
  # always return a Hash.
126
129
  option :legacy_attributes, default: false
127
130
 
128
- # Allow BSON::Decimal128 to be parsed and returned directly in
129
- # field values. When BSON 5 is present and the this option is set to false
130
- # (the default), BSON::Decimal128 values in the database will be returned
131
- # as BigDecimal.
132
- #
133
- # @note this option only has effect when BSON 5+ is present. Otherwise,
134
- # the setting is ignored.
135
- option :allow_bson5_decimal128, default: false, on_change: -> (allow) do
136
- if BSON::VERSION >= '5.0.0'
137
- if allow
138
- BSON::Registry.register(BSON::Decimal128::BSON_TYPE, BSON::Decimal128)
139
- else
140
- BSON::Registry.register(BSON::Decimal128::BSON_TYPE, BigDecimal)
141
- end
142
- end
143
- end
131
+ # Sets the async_query_executor for the application. By default the thread pool executor
132
+ # is set to `:immediate. Options are:
133
+ #
134
+ # - :immediate - Initializes a single +Concurrent::ImmediateExecutor+
135
+ # - :global_thread_pool - Initializes a single +Concurrent::ThreadPoolExecutor+
136
+ # that uses the +async_query_concurrency+ for the +max_threads+ value.
137
+ option :async_query_executor, default: :immediate
138
+
139
+ # Defines how many asynchronous queries can be executed concurrently.
140
+ # This option should be set only if `async_query_executor` is set
141
+ # to `:global_thread_pool`.
142
+ option :global_executor_concurrency, default: nil
143
+
144
+ # When this flag is false, a document will become read-only only once the
145
+ # #readonly! method is called, and an error will be raised on attempting
146
+ # to save or update such documents, instead of just on delete. When this
147
+ # flag is true, a document is only read-only if it has been projected
148
+ # using #only or #without, and read-only documents will not be
149
+ # deletable/destroyable, but they will be savable/updatable.
150
+ # When this feature flag is turned on, the read-only state will be reset on
151
+ # reload, but when it is turned off, it won't be.
152
+ option :legacy_readonly, default: true
153
+
154
+ # When this flag is true, any attempt to change the _id of a persisted
155
+ # document will raise an exception (`Errors::ImmutableAttribute`).
156
+ # This will be the default in 9.0. When this flag is false (the default
157
+ # in 8.x), changing the _id of a persisted document might be ignored,
158
+ # or it might work, depending on the situation.
159
+ option :immutable_ids, default: false
144
160
 
145
161
  # When this flag is true, callbacks for every embedded document will be
146
162
  # called only once, even if the embedded document is embedded in multiple
@@ -152,18 +168,12 @@ module Mongoid
152
168
  # See https://jira.mongodb.org/browse/MONGOID-5542
153
169
  option :prevent_multiple_calls_of_embedded_callbacks, default: false
154
170
 
155
- # When this flag is true, callbacks for embedded documents will not be
156
- # called. This is the default in 8.x, but will be changed to false in 9.0.
157
- #
158
- # Setting this flag to true (as it is in 8.x) may lead to stack
159
- # overflow errors if there are more than cicrca 1000 embedded
160
- # documents in the root document's dependencies graph.
171
+ # Returns the Config singleton, for use in the configure DSL.
161
172
  #
162
- # It is strongly recommended to set this flag to false in 8.x, if you
163
- # are not using around callbacks for embedded documents.
164
- #
165
- # See https://jira.mongodb.org/browse/MONGOID-5658 for more details.
166
- option :around_callbacks_for_embeds, default: true
173
+ # @return [ self ] The Config singleton.
174
+ def config
175
+ self
176
+ end
167
177
 
168
178
  # Has Mongoid been configured? This is checking that at least a valid
169
179
  # client config exists.
@@ -246,6 +256,17 @@ module Mongoid
246
256
  end
247
257
  end
248
258
 
259
+ # Deregister a model in the application with Mongoid.
260
+ #
261
+ # @param [ Class ] klass The model to deregister.
262
+ #
263
+ # @api private
264
+ def deregister_model(klass)
265
+ LOCK.synchronize do
266
+ models.delete(klass)
267
+ end
268
+ end
269
+
249
270
  # From a hash of settings, load all the configuration.
250
271
  #
251
272
  # @example Load the configuration.
@@ -318,6 +339,7 @@ module Mongoid
318
339
  # @param [ Hash ] options The configuration options.
319
340
  def options=(options)
320
341
  if options
342
+ Validators::AsyncQueryExecutor.validate(options)
321
343
  options.each_pair do |option, value|
322
344
  Validators::Option.validate(option)
323
345
  send("#{option}=", value)
@@ -385,5 +407,44 @@ module Mongoid
385
407
  client
386
408
  end
387
409
  end
410
+
411
+ module DeprecatedOptions
412
+ OPTIONS = %i[ use_activesupport_time_zone
413
+ broken_aggregables
414
+ broken_alias_handling
415
+ broken_and
416
+ broken_scoping
417
+ broken_updates
418
+ compare_time_by_ms
419
+ legacy_attributes
420
+ legacy_pluck_distinct
421
+ legacy_triple_equals
422
+ object_id_as_json_oid
423
+ overwrite_chained_operators ]
424
+
425
+ if RUBY_VERSION < '3.0'
426
+ def self.prepended(klass)
427
+ klass.class_eval do
428
+ OPTIONS.each do |option|
429
+ alias_method :"#{option}_without_deprecation=", :"#{option}="
430
+
431
+ define_method(:"#{option}=") do |value|
432
+ Mongoid::Warnings.send(:"warn_#{option}_deprecated")
433
+ send(:"#{option}_without_deprecation=", value)
434
+ end
435
+ end
436
+ end
437
+ end
438
+ else
439
+ OPTIONS.each do |option|
440
+ define_method(:"#{option}=") do |value|
441
+ Mongoid::Warnings.send(:"warn_#{option}_deprecated")
442
+ super(value)
443
+ end
444
+ end
445
+ end
446
+ end
447
+
448
+ prepend DeprecatedOptions
388
449
  end
389
450
  end
@@ -150,7 +150,7 @@ module Mongoid
150
150
  # @example Unset the field on the matches.
151
151
  # context.unset(:name)
152
152
  #
153
- # @param [ String | Symbol | Array<String | Symbol> | Hash ] args
153
+ # @param [ [ String | Symbol | Array<String | Symbol> | Hash ]... ] *args
154
154
  # The name(s) of the field(s) to unset.
155
155
  # If a Hash is specified, its keys will be used irrespective of what
156
156
  # each key's value is, even if the value is nil or false.
@@ -110,9 +110,24 @@ module Mongoid
110
110
  # @example Do any documents exist for the context.
111
111
  # context.exists?
112
112
  #
113
+ # @example Do any documents exist for given _id.
114
+ # context.exists?(BSON::ObjectId(...))
115
+ #
116
+ # @example Do any documents exist for given conditions.
117
+ # context.exists?(name: "...")
118
+ #
119
+ # @param [ Hash | Object | false ] id_or_conditions an _id to
120
+ # search for, a hash of conditions, nil or false.
121
+ #
113
122
  # @return [ true | false ] If the count is more than zero.
114
- def exists?
115
- any?
123
+ # Always false if passed nil or false.
124
+ def exists?(id_or_conditions = :none)
125
+ case id_or_conditions
126
+ when :none then any?
127
+ when nil, false then false
128
+ when Hash then Memory.new(criteria.where(id_or_conditions)).exists?
129
+ else Memory.new(criteria.where(_id: id_or_conditions)).exists?
130
+ end
116
131
  end
117
132
 
118
133
  # Get the first document in the database for the criteria's selector.
@@ -133,6 +148,20 @@ module Mongoid
133
148
  alias :one :first
134
149
  alias :find_first :first
135
150
 
151
+ # Get the first document in the database for the criteria's selector or
152
+ # raise an error if none is found.
153
+ #
154
+ # @example Get the first document.
155
+ # context.first!
156
+ #
157
+ # @return [ Document ] The first document.
158
+ #
159
+ # @raises [ Mongoid::Errors::DocumentNotFound ] raises when there are no
160
+ # documents to take.
161
+ def first!
162
+ first || raise_document_not_found_error
163
+ end
164
+
136
165
  # Create the new in memory context.
137
166
  #
138
167
  # @example Create the new context.
@@ -180,37 +209,18 @@ module Mongoid
180
209
  end
181
210
  end
182
211
 
183
- # Take the given number of documents from the database.
184
- #
185
- # @example Take a document.
186
- # context.take
187
- #
188
- # @param [ Integer | nil ] limit The number of documents to take or nil.
212
+ # Get the last document in the database for the criteria's selector or
213
+ # raise an error if none is found.
189
214
  #
190
- # @return [ Document ] The document.
191
- def take(limit = nil)
192
- if limit
193
- eager_load(documents.take(limit))
194
- else
195
- eager_load([documents.first]).first
196
- end
197
- end
198
-
199
- # Take the given number of documents from the database.
200
- #
201
- # @example Take a document.
202
- # context.take
215
+ # @example Get the last document.
216
+ # context.last!
203
217
  #
204
- # @return [ Document ] The document.
218
+ # @return [ Document ] The last document.
205
219
  #
206
220
  # @raises [ Mongoid::Errors::DocumentNotFound ] raises when there are no
207
221
  # documents to take.
208
- def take!
209
- if documents.empty?
210
- raise Errors::DocumentNotFound.new(klass, nil, nil)
211
- else
212
- eager_load([documents.first]).first
213
- end
222
+ def last!
223
+ last || raise_document_not_found_error
214
224
  end
215
225
 
216
226
  # Get the length of matching documents in the context.
@@ -242,7 +252,7 @@ module Mongoid
242
252
  # @example Get the values in memory.
243
253
  # context.pluck(:name)
244
254
  #
245
- # @param [ String | Symbol ] *fields Field(s) to pluck.
255
+ # @param [ [ String | Symbol ]... ] *fields Field(s) to pluck.
246
256
  #
247
257
  # @return [ Array<Object> | Array<Array<Object>> ] The plucked values.
248
258
  def pluck(*fields)
@@ -260,9 +270,9 @@ module Mongoid
260
270
  # @example Get the values in memory.
261
271
  # context.pick(:name)
262
272
  #
263
- # @param [ String | Symbol ] *fields Field(s) to pick.
273
+ # @param [ [ String | Symbol ]... ] *fields Field(s) to pick.
264
274
  #
265
- # @return [ Object, Array<Object> ] The picked values.
275
+ # @return [ Object | Array<Object> ] The picked values.
266
276
  def pick(*fields)
267
277
  if doc = documents.first
268
278
  pluck_from_doc(doc, *fields)
@@ -285,6 +295,36 @@ module Mongoid
285
295
  end
286
296
  end
287
297
 
298
+ # Take the given number of documents from the database.
299
+ #
300
+ # @example Take a document.
301
+ # context.take
302
+ #
303
+ # @param [ Integer | nil ] limit The number of documents to take or nil.
304
+ #
305
+ # @return [ Document ] The document.
306
+ def take(limit = nil)
307
+ if limit
308
+ eager_load(documents.take(limit))
309
+ else
310
+ eager_load([documents.first]).first
311
+ end
312
+ end
313
+
314
+ # Take the given number of documents from the database or raise an error
315
+ # if none are found.
316
+ #
317
+ # @example Take a document.
318
+ # context.take
319
+ #
320
+ # @return [ Document ] The document.
321
+ #
322
+ # @raises [ Mongoid::Errors::DocumentNotFound ] raises when there are no
323
+ # documents to take.
324
+ def take!
325
+ take || raise_document_not_found_error
326
+ end
327
+
288
328
  # Skips the provided number of documents.
289
329
  #
290
330
  # @example Skip the documents.
@@ -335,6 +375,162 @@ module Mongoid
335
375
  update_documents(attributes, entries)
336
376
  end
337
377
 
378
+ # Get the second document in the database for the criteria's selector.
379
+ #
380
+ # @example Get the second document.
381
+ # context.second
382
+ #
383
+ # @param [ Integer ] limit The number of documents to return.
384
+ #
385
+ # @return [ Document ] The second document.
386
+ def second
387
+ eager_load([documents.second]).first
388
+ end
389
+
390
+ # Get the second document in the database for the criteria's selector or
391
+ # raise an error if none is found.
392
+ #
393
+ # @example Get the second document.
394
+ # context.second!
395
+ #
396
+ # @return [ Document ] The second document.
397
+ #
398
+ # @raises [ Mongoid::Errors::DocumentNotFound ] raises when there are no
399
+ # documents to take.
400
+ def second!
401
+ second || raise_document_not_found_error
402
+ end
403
+
404
+ # Get the third document in the database for the criteria's selector.
405
+ #
406
+ # @example Get the third document.
407
+ # context.third
408
+ #
409
+ # @param [ Integer ] limit The number of documents to return.
410
+ #
411
+ # @return [ Document ] The third document.
412
+ def third
413
+ eager_load([documents.third]).first
414
+ end
415
+
416
+ # Get the third document in the database for the criteria's selector or
417
+ # raise an error if none is found.
418
+ #
419
+ # @example Get the third document.
420
+ # context.third!
421
+ #
422
+ # @return [ Document ] The third document.
423
+ #
424
+ # @raises [ Mongoid::Errors::DocumentNotFound ] raises when there are no
425
+ # documents to take.
426
+ def third!
427
+ third || raise_document_not_found_error
428
+ end
429
+
430
+ # Get the fourth document in the database for the criteria's selector.
431
+ #
432
+ # @example Get the fourth document.
433
+ # context.fourth
434
+ #
435
+ # @param [ Integer ] limit The number of documents to return.
436
+ #
437
+ # @return [ Document ] The fourth document.
438
+ def fourth
439
+ eager_load([documents.fourth]).first
440
+ end
441
+
442
+ # Get the fourth document in the database for the criteria's selector or
443
+ # raise an error if none is found.
444
+ #
445
+ # @example Get the fourth document.
446
+ # context.fourth!
447
+ #
448
+ # @return [ Document ] The fourth document.
449
+ #
450
+ # @raises [ Mongoid::Errors::DocumentNotFound ] raises when there are no
451
+ # documents to take.
452
+ def fourth!
453
+ fourth || raise_document_not_found_error
454
+ end
455
+
456
+ # Get the fifth document in the database for the criteria's selector.
457
+ #
458
+ # @example Get the fifth document.
459
+ # context.fifth
460
+ #
461
+ # @param [ Integer ] limit The number of documents to return.
462
+ #
463
+ # @return [ Document ] The fifth document.
464
+ def fifth
465
+ eager_load([documents.fifth]).first
466
+ end
467
+
468
+ # Get the fifth document in the database for the criteria's selector or
469
+ # raise an error if none is found.
470
+ #
471
+ # @example Get the fifth document.
472
+ # context.fifth!
473
+ #
474
+ # @return [ Document ] The fifth document.
475
+ #
476
+ # @raises [ Mongoid::Errors::DocumentNotFound ] raises when there are no
477
+ # documents to take.
478
+ def fifth!
479
+ fifth || raise_document_not_found_error
480
+ end
481
+
482
+ # Get the second to last document in the database for the criteria's selector.
483
+ #
484
+ # @example Get the second to last document.
485
+ # context.second_to_last
486
+ #
487
+ # @param [ Integer ] limit The number of documents to return.
488
+ #
489
+ # @return [ Document ] The second to last document.
490
+ def second_to_last
491
+ eager_load([documents.second_to_last]).first
492
+ end
493
+
494
+ # Get the second to last document in the database for the criteria's selector or
495
+ # raise an error if none is found.
496
+ #
497
+ # @example Get the second to last document.
498
+ # context.second_to_last!
499
+ #
500
+ # @return [ Document ] The second to last document.
501
+ #
502
+ # @raises [ Mongoid::Errors::DocumentNotFound ] raises when there are no
503
+ # documents to take.
504
+ def second_to_last!
505
+ second_to_last || raise_document_not_found_error
506
+ end
507
+
508
+ # Get the third to last document in the database for the criteria's selector.
509
+ #
510
+ # @example Get the third to last document.
511
+ # context.third_to_last
512
+ #
513
+ # @param [ Integer ] limit The number of documents to return.
514
+ #
515
+ # @return [ Document ] The third to last document.
516
+ def third_to_last
517
+ eager_load([documents.third_to_last]).first
518
+ end
519
+
520
+ # Get the third to last document in the database for the criteria's selector or
521
+ # raise an error if none is found.
522
+ #
523
+ # @example Get the third to last document.
524
+ # context.third_to_last!
525
+ #
526
+ # @return [ Document ] The third to last document.
527
+ #
528
+ # @raises [ Mongoid::Errors::DocumentNotFound ] raises when there are no
529
+ # documents to take.
530
+ def third_to_last!
531
+ third_to_last || raise_document_not_found_error
532
+ end
533
+
338
534
  private
339
535
 
340
536
  # Get the documents the context should iterate. This follows 3 rules:
@@ -561,9 +757,9 @@ module Mongoid
561
757
  # Pluck the field values from the given document.
562
758
  #
563
759
  # @param [ Document ] doc The document to pluck from.
564
- # @param [ String | Symbol ] *fields Field(s) to pluck.
760
+ # @param [ [ String | Symbol ]... ] *fields Field(s) to pluck.
565
761
  #
566
- # @return [ Object, Array<Object> ] The plucked values.
762
+ # @return [ Object | Array<Object> ] The plucked values.
567
763
  def pluck_from_doc(doc, *fields)
568
764
  if fields.length == 1
569
765
  retrieve_value_at_path(doc, fields.first)
@@ -573,6 +769,10 @@ module Mongoid
573
769
  end
574
770
  end
575
771
  end
772
+
773
+ def raise_document_not_found_error
774
+ raise Errors::DocumentNotFound.new(klass, nil, nil)
775
+ end
576
776
  end
577
777
  end
578
778
  end
@@ -0,0 +1,177 @@
1
+ require "mongoid/association/eager_loadable"
2
+
3
+ module Mongoid
4
+ module Contextual
5
+ class Mongo
6
+ # Loads documents for the provided criteria.
7
+ #
8
+ # @api private
9
+ class DocumentsLoader
10
+ extend Forwardable
11
+ include Association::EagerLoadable
12
+
13
+ def_delegators :@future, :value!, :value, :wait!, :wait
14
+
15
+ # Returns synchronous executor to be used when async_query_executor config option
16
+ # is set to :immediate. This executor runs all operations on the current
17
+ # thread, blocking as necessary.
18
+ #
19
+ # @return [ Concurrent::ImmediateExecutor ] The executor
20
+ # to be used to execute document loading tasks.
21
+ def self.immediate_executor
22
+ @@immediate_executor ||= Concurrent::ImmediateExecutor.new
23
+ end
24
+
25
+ # Returns asynchronous executor to be used when async_query_executor config option
26
+ # is set to :global_thread_pool. This executor runs operations on background threads
27
+ # using a thread pool.
28
+ #
29
+ # @return [ Concurrent::ThreadPoolExecutor ] The executor
30
+ # to be used to execute document loading tasks.
31
+ def self.global_thread_pool_async_query_executor
32
+ create_pool = Proc.new do |concurrency|
33
+ Concurrent::ThreadPoolExecutor.new(
34
+ min_threads: 0,
35
+ max_threads: concurrency,
36
+ max_queue: concurrency * 4,
37
+ fallback_policy: :caller_runs
38
+ )
39
+ end
40
+ concurrency = Mongoid.global_executor_concurrency || 4
41
+ @@global_thread_pool_async_query_executor ||= create_pool.call(concurrency)
42
+ if @@global_thread_pool_async_query_executor.max_length != concurrency
43
+ old_pool = @@global_thread_pool_async_query_executor
44
+ @@global_thread_pool_async_query_executor = create_pool.call(concurrency)
45
+ old_pool.shutdown
46
+ end
47
+ @@global_thread_pool_async_query_executor
48
+ end
49
+
50
+ # Returns suitable executor according to Mongoid config options.
51
+ #
52
+ # @param [ String | Symbol] name The query executor name, can be either
53
+ # :immediate or :global_thread_pool. Defaulted to `async_query_executor`
54
+ # config option.
55
+ #
56
+ # @return [ Concurrent::ImmediateExecutor | Concurrent::ThreadPoolExecutor ] The executor
57
+ # to be used to execute document loading tasks.
58
+ #
59
+ # @raise [ Errors::InvalidQueryExecutor ] If an unknown name is provided.
60
+ def self.executor(name = Mongoid.async_query_executor)
61
+ case name.to_sym
62
+ when :immediate
63
+ immediate_executor
64
+ when :global_thread_pool
65
+ global_thread_pool_async_query_executor
66
+ else
67
+ raise Errors::InvalidQueryExecutor.new(name)
68
+ end
69
+ end
70
+
71
+ # @return [ Mongoid::Criteria ] Criteria that specifies which documents should
72
+ # be loaded. Exposed here because `eager_loadable?` method from
73
+ # `Association::EagerLoadable` expects this to be available.
74
+ attr_accessor :criteria
75
+
76
+ # Instantiates the document loader instance and immediately schedules
77
+ # its execution using the provided executor.
78
+ #
79
+ # @param [ Mongo::Collection::View ] view The collection view to get
80
+ # records from the database.
81
+ # @param [ Class ] klass Mongoid model class to instantiate documents.
82
+ # All records obtained from the database will be converted to an
83
+ # instance of this class, if possible.
84
+ # @param [ Mongoid::Criteria ] criteria. Criteria that specifies which
85
+ # documents should be loaded.
86
+ # @param [ Concurrent::AbstractExecutorService ] executor. Executor that
87
+ # is capable of running `Concurrent::Promises::Future` instances.
88
+ def initialize(view, klass, criteria, executor: self.class.executor)
89
+ @view = view
90
+ @klass = klass
91
+ @criteria = criteria
92
+ @mutex = Mutex.new
93
+ @state = :pending
94
+ @future = Concurrent::Promises.future_on(executor) do
95
+ start && execute
96
+ end
97
+ end
98
+
99
+ # Returns false or true whether the loader is in pending state.
100
+ #
101
+ # Pending state means that the loader execution has been scheduled,
102
+ # but has not been started yet.
103
+ #
104
+ # @return [ true | false ] true if the loader is in pending state,
105
+ # otherwise false.
106
+ def pending?
107
+ @mutex.synchronize do
108
+ @state == :pending
109
+ end
110
+ end
111
+
112
+ # Returns false or true whether the loader is in started state.
113
+ #
114
+ # Started state means that the loader execution has been started.
115
+ # Note that the loader stays in this state even after the execution
116
+ # completed (successfully or failed).
117
+ #
118
+ # @return [ true | false ] true if the loader is in started state,
119
+ # otherwise false.
120
+ def started?
121
+ @mutex.synchronize do
122
+ @state == :started
123
+ end
124
+ end
125
+
126
+ # Mark the loader as unscheduled.
127
+ #
128
+ # If the loader is marked unscheduled, it will not be executed. The only
129
+ # option to load the documents is to call `execute` method directly.
130
+ #
131
+ # Please note that if execution of a task has been already started,
132
+ # unscheduling does not have any effect.
133
+ def unschedule
134
+ @mutex.synchronize do
135
+ @state = :cancelled unless @state == :started
136
+ end
137
+ end
138
+
139
+ # Loads records specified by `@criteria` from the database, and convert
140
+ # them to Mongoid documents of `@klass` type.
141
+ #
142
+ # This method is called by the task (possibly asynchronous) scheduled
143
+ # when creating an instance of the loader. However, this method can be
144
+ # called directly, if it is desired to execute loading on the caller
145
+ # thread immediately.
146
+ #
147
+ # Calling this method does not change the state of the loader.
148
+ #
149
+ # @return [ Array<Mongoid::Document> ] Array of document loaded from
150
+ # the database.
151
+ def execute
152
+ documents = @view.map do |doc|
153
+ Factory.from_db(@klass, doc, @criteria)
154
+ end
155
+ eager_load(documents) if eager_loadable?
156
+ documents
157
+ end
158
+
159
+ private
160
+
161
+ # Mark the loader as started if possible.
162
+ #
163
+ # @return [ true | false ] Whether the state was changed to :started.
164
+ def start
165
+ @mutex.synchronize do
166
+ if @state == :pending
167
+ @state = :started
168
+ true
169
+ else
170
+ false
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end