mongoid 8.0.13 → 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 (225) 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 +17 -66
  6. data/lib/config/locales/en.yml +46 -40
  7. data/lib/mongoid/association/accessors.rb +3 -7
  8. data/lib/mongoid/association/builders.rb +1 -1
  9. data/lib/mongoid/association/depending.rb +4 -8
  10. data/lib/mongoid/association/eager_loadable.rb +0 -3
  11. data/lib/mongoid/association/embedded/batchable.rb +2 -2
  12. data/lib/mongoid/association/embedded/embedded_in/buildable.rb +2 -2
  13. data/lib/mongoid/association/embedded/embedded_in/proxy.rb +2 -1
  14. data/lib/mongoid/association/embedded/embeds_many/buildable.rb +3 -2
  15. data/lib/mongoid/association/embedded/embeds_many/proxy.rb +6 -6
  16. data/lib/mongoid/association/embedded/embeds_one/buildable.rb +1 -1
  17. data/lib/mongoid/association/embedded/embeds_one/proxy.rb +2 -2
  18. data/lib/mongoid/association/macros.rb +0 -6
  19. data/lib/mongoid/association/nested/many.rb +3 -38
  20. data/lib/mongoid/association/nested/nested_buildable.rb +0 -14
  21. data/lib/mongoid/association/nested/one.rb +40 -2
  22. data/lib/mongoid/association/proxy.rb +1 -1
  23. data/lib/mongoid/association/referenced/counter_cache.rb +2 -2
  24. data/lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb +1 -1
  25. data/lib/mongoid/association/referenced/has_many/enumerable.rb +6 -23
  26. data/lib/mongoid/association/referenced/has_many/proxy.rb +4 -103
  27. data/lib/mongoid/association/reflections.rb +2 -2
  28. data/lib/mongoid/atomic.rb +7 -16
  29. data/lib/mongoid/attributes/dynamic.rb +1 -1
  30. data/lib/mongoid/attributes/nested.rb +2 -2
  31. data/lib/mongoid/attributes/processing.rb +5 -29
  32. data/lib/mongoid/attributes/projector.rb +1 -1
  33. data/lib/mongoid/attributes/readonly.rb +1 -1
  34. data/lib/mongoid/attributes.rb +8 -2
  35. data/lib/mongoid/changeable.rb +107 -5
  36. data/lib/mongoid/clients/factory.rb +0 -4
  37. data/lib/mongoid/clients/storage_options.rb +2 -5
  38. data/lib/mongoid/clients/validators/storage.rb +1 -13
  39. data/lib/mongoid/collection_configurable.rb +58 -0
  40. data/lib/mongoid/composable.rb +2 -0
  41. data/lib/mongoid/config/defaults.rb +60 -0
  42. data/lib/mongoid/config/options.rb +0 -3
  43. data/lib/mongoid/config/validators/async_query_executor.rb +24 -0
  44. data/lib/mongoid/config/validators.rb +1 -0
  45. data/lib/mongoid/config.rb +88 -89
  46. data/lib/mongoid/contextual/aggregable/memory.rb +2 -5
  47. data/lib/mongoid/contextual/atomic.rb +1 -1
  48. data/lib/mongoid/contextual/memory.rb +240 -51
  49. data/lib/mongoid/contextual/mongo/documents_loader.rb +177 -0
  50. data/lib/mongoid/contextual/mongo.rb +370 -133
  51. data/lib/mongoid/contextual/none.rb +162 -7
  52. data/lib/mongoid/contextual.rb +12 -0
  53. data/lib/mongoid/criteria/findable.rb +2 -2
  54. data/lib/mongoid/criteria/includable.rb +4 -3
  55. data/lib/mongoid/criteria/queryable/extensions/numeric.rb +1 -15
  56. data/lib/mongoid/criteria/queryable/key.rb +1 -1
  57. data/lib/mongoid/criteria/queryable/mergeable.rb +1 -13
  58. data/lib/mongoid/criteria/queryable/optional.rb +8 -8
  59. data/lib/mongoid/criteria/queryable/selectable.rb +43 -121
  60. data/lib/mongoid/criteria/queryable/selector.rb +1 -1
  61. data/lib/mongoid/criteria/queryable/storable.rb +1 -1
  62. data/lib/mongoid/criteria.rb +6 -5
  63. data/lib/mongoid/deprecable.rb +1 -2
  64. data/lib/mongoid/deprecation.rb +3 -3
  65. data/lib/mongoid/document.rb +1 -8
  66. data/lib/mongoid/errors/create_collection_failure.rb +33 -0
  67. data/lib/mongoid/errors/drop_collection_failure.rb +27 -0
  68. data/lib/mongoid/errors/immutable_attribute.rb +26 -0
  69. data/lib/mongoid/errors/invalid_async_query_executor.rb +25 -0
  70. data/lib/mongoid/errors/invalid_global_executor_concurrency.rb +22 -0
  71. data/lib/mongoid/errors/invalid_storage_parent.rb +2 -0
  72. data/lib/mongoid/errors.rb +4 -2
  73. data/lib/mongoid/extensions/hash.rb +3 -51
  74. data/lib/mongoid/extensions/object.rb +2 -2
  75. data/lib/mongoid/extensions/time.rb +2 -0
  76. data/lib/mongoid/fields/localized.rb +10 -0
  77. data/lib/mongoid/fields/standard.rb +10 -0
  78. data/lib/mongoid/fields.rb +59 -35
  79. data/lib/mongoid/findable.rb +27 -3
  80. data/lib/mongoid/interceptable.rb +6 -116
  81. data/lib/mongoid/matchable.rb +1 -6
  82. data/lib/mongoid/matcher/eq_impl.rb +1 -1
  83. data/lib/mongoid/matcher/eq_impl_with_regexp.rb +5 -3
  84. data/lib/mongoid/matcher/regex.rb +9 -8
  85. data/lib/mongoid/matcher/type.rb +1 -1
  86. data/lib/mongoid/matcher.rb +0 -1
  87. data/lib/mongoid/persistable/creatable.rb +1 -0
  88. data/lib/mongoid/persistable/deletable.rb +1 -1
  89. data/lib/mongoid/persistable/savable.rb +13 -1
  90. data/lib/mongoid/persistable/unsettable.rb +2 -2
  91. data/lib/mongoid/persistable/updatable.rb +51 -1
  92. data/lib/mongoid/persistable/upsertable.rb +20 -1
  93. data/lib/mongoid/persistable.rb +3 -0
  94. data/lib/mongoid/query_cache.rb +5 -1
  95. data/lib/mongoid/railties/database.rake +7 -2
  96. data/lib/mongoid/reloadable.rb +5 -3
  97. data/lib/mongoid/stateful.rb +22 -1
  98. data/lib/mongoid/tasks/database.rake +12 -0
  99. data/lib/mongoid/tasks/database.rb +20 -0
  100. data/lib/mongoid/threaded.rb +0 -3
  101. data/lib/mongoid/timestamps/created.rb +1 -8
  102. data/lib/mongoid/traversable.rb +0 -12
  103. data/lib/mongoid/utils.rb +22 -0
  104. data/lib/mongoid/validatable/associated.rb +17 -98
  105. data/lib/mongoid/validatable/macros.rb +5 -20
  106. data/lib/mongoid/validatable.rb +4 -10
  107. data/lib/mongoid/version.rb +1 -5
  108. data/lib/mongoid/warnings.rb +17 -2
  109. data/lib/mongoid.rb +16 -3
  110. data/spec/integration/app_spec.rb +2 -20
  111. data/spec/integration/associations/embeds_one_spec.rb +5 -25
  112. data/spec/integration/associations/has_and_belongs_to_many_spec.rb +0 -55
  113. data/spec/integration/callbacks_spec.rb +99 -12
  114. data/spec/integration/discriminator_key_spec.rb +4 -5
  115. data/spec/integration/dots_and_dollars_spec.rb +2 -12
  116. data/spec/integration/i18n_fallbacks_spec.rb +3 -2
  117. data/spec/integration/matcher_operator_data/regex.yml +0 -21
  118. data/spec/mongoid/association/eager_spec.rb +2 -24
  119. data/spec/mongoid/association/embedded/embedded_in/proxy_spec.rb +27 -0
  120. data/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb +20 -25
  121. data/spec/mongoid/association/embedded/embeds_many_models.rb +1 -0
  122. data/spec/mongoid/association/embedded/embeds_many_query_spec.rb +0 -4
  123. data/spec/mongoid/association/embedded/embeds_one/proxy_spec.rb +15 -2
  124. data/spec/mongoid/association/referenced/belongs_to_spec.rb +2 -18
  125. data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +42 -103
  126. data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +9 -195
  127. data/spec/mongoid/association/syncable_spec.rb +1 -1
  128. data/spec/mongoid/attributes/nested_spec.rb +10 -204
  129. data/spec/mongoid/attributes_spec.rb +3 -33
  130. data/spec/mongoid/changeable_spec.rb +299 -24
  131. data/spec/mongoid/clients/factory_spec.rb +0 -30
  132. data/spec/mongoid/clients_spec.rb +122 -13
  133. data/spec/mongoid/collection_configurable_spec.rb +158 -0
  134. data/spec/mongoid/config/defaults_spec.rb +160 -0
  135. data/spec/mongoid/config_spec.rb +154 -27
  136. data/spec/mongoid/contextual/aggregable/memory_spec.rb +0 -91
  137. data/spec/mongoid/contextual/memory_spec.rb +332 -253
  138. data/spec/mongoid/contextual/mongo/documents_loader_spec.rb +187 -0
  139. data/spec/mongoid/contextual/mongo_spec.rb +1009 -125
  140. data/spec/mongoid/contextual/none_spec.rb +49 -2
  141. data/spec/mongoid/copyable_spec.rb +2 -10
  142. data/spec/mongoid/criteria/queryable/extensions/string_spec.rb +4 -10
  143. data/spec/mongoid/criteria/queryable/options_spec.rb +1 -1
  144. data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +419 -2
  145. data/spec/mongoid/criteria/queryable/selectable_spec.rb +1 -1
  146. data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +0 -235
  147. data/spec/mongoid/criteria/queryable/selector_spec.rb +3 -76
  148. data/spec/mongoid/criteria/queryable/storable_spec.rb +0 -72
  149. data/spec/mongoid/criteria_projection_spec.rb +1 -4
  150. data/spec/mongoid/criteria_spec.rb +5 -11
  151. data/spec/mongoid/document_spec.rb +0 -27
  152. data/spec/mongoid/errors/readonly_document_spec.rb +2 -2
  153. data/spec/mongoid/extensions/hash_spec.rb +3 -239
  154. data/spec/mongoid/extensions/time_spec.rb +8 -43
  155. data/spec/mongoid/extensions/time_with_zone_spec.rb +7 -52
  156. data/spec/mongoid/fields/localized_spec.rb +46 -28
  157. data/spec/mongoid/fields_spec.rb +136 -77
  158. data/spec/mongoid/findable_spec.rb +391 -34
  159. data/spec/mongoid/indexable_spec.rb +16 -10
  160. data/spec/mongoid/interceptable_spec.rb +153 -442
  161. data/spec/mongoid/interceptable_spec_models.rb +111 -51
  162. data/spec/mongoid/persistable/deletable_spec.rb +26 -6
  163. data/spec/mongoid/persistable/destroyable_spec.rb +26 -6
  164. data/spec/mongoid/persistable/incrementable_spec.rb +37 -0
  165. data/spec/mongoid/persistable/logical_spec.rb +37 -0
  166. data/spec/mongoid/persistable/poppable_spec.rb +36 -0
  167. data/spec/mongoid/persistable/pullable_spec.rb +72 -0
  168. data/spec/mongoid/persistable/pushable_spec.rb +72 -0
  169. data/spec/mongoid/persistable/renamable_spec.rb +36 -0
  170. data/spec/mongoid/persistable/savable_spec.rb +96 -0
  171. data/spec/mongoid/persistable/settable_spec.rb +37 -0
  172. data/spec/mongoid/persistable/unsettable_spec.rb +36 -0
  173. data/spec/mongoid/persistable/updatable_spec.rb +20 -28
  174. data/spec/mongoid/persistable/upsertable_spec.rb +80 -6
  175. data/spec/mongoid/persistence_context_spec.rb +7 -57
  176. data/spec/mongoid/query_cache_spec.rb +56 -61
  177. data/spec/mongoid/reloadable_spec.rb +24 -28
  178. data/spec/mongoid/scopable_spec.rb +70 -0
  179. data/spec/mongoid/serializable_spec.rb +23 -44
  180. data/spec/mongoid/stateful_spec.rb +122 -8
  181. data/spec/mongoid/tasks/database_rake_spec.rb +74 -0
  182. data/spec/mongoid/tasks/database_spec.rb +127 -0
  183. data/spec/mongoid/timestamps/created_spec.rb +0 -23
  184. data/spec/mongoid/timestamps_spec.rb +9 -11
  185. data/spec/mongoid/touchable_spec.rb +277 -5
  186. data/spec/mongoid/touchable_spec_models.rb +3 -1
  187. data/spec/mongoid/traversable_spec.rb +9 -24
  188. data/spec/mongoid/validatable/associated_spec.rb +34 -27
  189. data/spec/mongoid/validatable/numericality_spec.rb +0 -16
  190. data/spec/mongoid/validatable/uniqueness_spec.rb +2 -3
  191. data/spec/mongoid_spec.rb +36 -10
  192. data/spec/shared/lib/mrss/docker_runner.rb +1 -1
  193. data/spec/shared/lib/mrss/server_version_registry.rb +1 -1
  194. data/spec/shared/lib/mrss/spec_organizer.rb +3 -32
  195. data/spec/shared/share/Dockerfile.erb +85 -15
  196. data/spec/shared/shlib/distro.sh +0 -10
  197. data/spec/shared/shlib/server.sh +27 -34
  198. data/spec/shared/shlib/set_env.sh +68 -9
  199. data/spec/spec_helper.rb +5 -0
  200. data/spec/support/expectations.rb +17 -20
  201. data/spec/support/immutable_ids.rb +118 -0
  202. data/spec/support/macros.rb +47 -15
  203. data/spec/support/models/artist.rb +0 -1
  204. data/spec/support/models/band.rb +1 -0
  205. data/spec/support/models/building.rb +2 -0
  206. data/spec/support/models/name.rb +0 -10
  207. data/spec/support/models/person.rb +0 -1
  208. data/spec/support/models/product.rb +1 -0
  209. data.tar.gz.sig +0 -0
  210. metadata +715 -671
  211. metadata.gz.sig +2 -0
  212. data/lib/mongoid/errors/in_memory_regexp_timeout.rb +0 -26
  213. data/lib/mongoid/field_readable.rb +0 -70
  214. data/lib/mongoid/matcher/regexp_budget.rb +0 -389
  215. data/lib/mongoid/validatable/numericality.rb +0 -19
  216. data/spec/integration/matcher_regexp_timeout_spec.rb +0 -215
  217. data/spec/integration/query_operator_guard_spec.rb +0 -89
  218. data/spec/mongoid/criteria/queryable/extensions/bignum_spec.rb +0 -61
  219. data/spec/mongoid/criteria/queryable/extensions/fixnum_spec.rb +0 -61
  220. data/spec/mongoid/matcher/regexp_budget_spec.rb +0 -570
  221. data/spec/shared/CANDIDATE.md +0 -28
  222. data/spec/shared/lib/mrss/release/candidate.rb +0 -281
  223. data/spec/shared/lib/mrss/release/product_data.rb +0 -144
  224. data/spec/shared/lib/tasks/candidate.rake +0 -64
  225. data/spec/support/models/purse.rb +0 -9
metadata.gz.sig ADDED
@@ -0,0 +1,2 @@
1
+ W�5�8ZK�zg�����פ=�ղ"p��_NJ���Ga�*�ֳ�n�)�$��r ��'ĒSl��� ����%����OD)ʍ�6�������l]�nc�=�[E�u�f�$F̌sBP�������h7�H�m�2^©��//}�[Ƶ=�.�����Y���ղ���m.��g���9��S�&e%6&��-s-Oƥ�@+V�ku���`o��!3ڗ����hY;Q;N���� �6�44��KgS9 �S9���G �U���S��+h6 ���(1\BvcԓT%��hUm.J��Z��d��x?���3Xm/fG8.�EP OX��
2
+ ��q�i�=[�o�� !�b�8��#?EoT"�'YS0" u�A��>�1�K'���
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Mongoid
4
- module Errors
5
- # This error is raised when evaluating a query in memory exceeds
6
- # Mongoid::Config.in_memory_regexp_time_limit.
7
- #
8
- # What the limit bounds depends on the Ruby in use. Where per-Regexp
9
- # timeouts are available it is the time spent executing regular
10
- # expressions; elsewhere it is the elapsed time of the whole in-memory
11
- # evaluation. The message is worded to hold either way.
12
- class InMemoryRegexpTimeout < MongoidError
13
- # Create the new error.
14
- #
15
- # @example Create the new in-memory regexp timeout error.
16
- # InMemoryRegexpTimeout.new(5.0)
17
- #
18
- # @param [ Float ] limit The limit that was exceeded, in seconds. Not
19
- # always the configured one: a global Regexp.timeout stricter than the
20
- # configuration takes its place.
21
- def initialize(limit)
22
- super(compose_message('in_memory_regexp_timeout', limit: limit))
23
- end
24
- end
25
- end
26
- end
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
- # rubocop:todo all
3
-
4
- module Mongoid
5
- # Reads the value of a field name from a document without dispatching the
6
- # name as an arbitrary method.
7
- #
8
- # Field names may come from application input, where a name like +destroy+
9
- # or +attributes+ would delete the document or disclose its contents.
10
- # Declared field and association names are validated when they are declared
11
- # (see Mongoid.destructive_fields), so a name that resolves to one of them
12
- # is safe to send to a document. Any other name is read from the attributes
13
- # hash, which is what the database-backed query contexts do.
14
- #
15
- # @api private
16
- module FieldReadable
17
- private
18
-
19
- # Read the value of the given field name from the given document.
20
- #
21
- # @param [ Document ] document The document to read from.
22
- # @param [ String | Symbol ] name The name of the field.
23
- #
24
- # @return [ Object | nil ] The value of the field, or nil when the name
25
- # is neither a declared field nor present in the attributes.
26
- def read_field_value(document, name)
27
- name = name.to_s
28
- # A blank name cannot name a field. Return nil, which is what reading
29
- # one has always done.
30
- return nil if name.blank?
31
-
32
- if (meth = readable_method_for(document.class, name))
33
- document.public_send(meth)
34
- else
35
- document.attributes[document.class.database_field_name(name)]
36
- end
37
- end
38
-
39
- # Resolve the given name to a method that is declared by the given class,
40
- # and therefore safe to send to one of its instances.
41
- #
42
- # @param [ Class ] klass The document class.
43
- # @param [ String ] name The name of the field.
44
- #
45
- # @return [ String | nil ] The method to send, or nil when the name is
46
- # not declared by the class.
47
- def readable_method_for(klass, name)
48
- # Fields, associations, and field aliases each define a reader of their
49
- # own name. Note that associations must be resolved before aliases: a
50
- # belongs_to aliases its own name to its foreign key, and reading
51
- # `band` must give the document, not the id.
52
- return name if klass.relations.key?(name) ||
53
- klass.fields.key?(name) ||
54
- klass.aliased_fields.key?(name)
55
-
56
- # An association may also be named by its `store_as`, which has no
57
- # reader of its own, or by its ids accessor, which does.
58
- if (assoc = klass.relations[klass.aliased_associations[name]])
59
- return (assoc.store_as == name) ? assoc.name.to_s : name
60
- end
61
-
62
- # Localized fields also get a _translations reader, which does not
63
- # appear in the fields hash.
64
- base = name.delete_suffix(Fields::TRANSLATIONS_SFX)
65
- return nil if base == name
66
-
67
- name if klass.fields[klass.database_field_name(base)]&.localized?
68
- end
69
- end
70
- end
@@ -1,389 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'timeout'
4
-
5
- module Mongoid
6
- module Matcher
7
- # Bounds the time spent executing regular expressions while evaluating a
8
- # single in-memory match operation.
9
- #
10
- # A query condition can carry an application-supplied pattern, and the
11
- # in-memory matcher compiles and runs that pattern in the caller's thread.
12
- # Both the cost of one match and the number of matches performed are under
13
- # the control of whoever supplied the condition, so the limit is cumulative
14
- # over an entire operation rather than per match.
15
- #
16
- # The budget is held in thread- or fiber-local storage, so concurrent
17
- # queries are accounted for independently.
18
- #
19
- # @api private
20
- module RegexpBudget
21
- # Whether a per-Regexp timeout can be relied on to reach Regexp.new.
22
- #
23
- # MRI added them in 3.2. JRuby 10.0.6 defines Regexp::TimeoutError,
24
- # reports Ruby 3.4, and does honour a timeout that reaches it, but its
25
- # Regexp.new accepts the keyword only for the first couple of calls
26
- # through a given call site and raises ArgumentError from then on.
27
- # Because that breakage is per call site, no load-time probe can predict
28
- # it: a probe at its own call site reports a capability that the call in
29
- # Budget#compile does not have. So non-MRI engines are excluded outright
30
- # and use the Timeout fallback, which does interrupt a Joni match already
31
- # under way. Worth revisiting if JRuby fixes the keyword handling.
32
- PER_REGEXP_TIMEOUT =
33
- if RUBY_ENGINE == 'ruby' && defined?(::Regexp::TimeoutError)
34
- true
35
- else
36
- false
37
- end
38
-
39
- # The exception raised by a per-Regexp timeout. Tied to the constant
40
- # rather than to the probe, so that a timeout set some other way (an
41
- # application assigning Regexp.timeout, say) is still translated. On
42
- # Rubies with no such constant, a class that is never raised stands in.
43
- TIMEOUT_ERROR = defined?(::Regexp::TimeoutError) ? ::Regexp::TimeoutError : Class.new(StandardError)
44
-
45
- # Raised by Timeout on Rubies without per-Regexp timeouts, and converted
46
- # immediately. It is private to this module so that an application's own
47
- # Timeout, firing inside our block, is never mistaken for ours.
48
- class TimedOut < StandardError; end
49
-
50
- # The marker stored in thread-local storage when a scope is open but has
51
- # no budget to enforce. A nil-valued thread variable reads as absent
52
- # (Thread.current[:key] returns nil for a missing key), so an
53
- # open-but-unbounded scope has to store a real marker; a nested call
54
- # checks for the key and joins the enclosing scope rather than deciding
55
- # again for itself.
56
- NO_BUDGET = Object.new
57
-
58
- # The state of one open budget: what is left of the limit, and the
59
- # patterns compiled under it.
60
- #
61
- # @api private
62
- class Budget
63
- # @return [ Float ] The limit this budget started with.
64
- attr_reader :limit
65
-
66
- # @return [ Float ] The seconds left before the budget is spent.
67
- attr_reader :remaining
68
-
69
- # @param [ Float ] limit The seconds this budget may spend.
70
- def initialize(limit)
71
- @limit = limit
72
- @remaining = limit
73
- @cache = {}
74
- # A per-Regexp timeout bounds one match; the cumulative budget bounds
75
- # the operation. So the timeout is fixed for the life of the scope
76
- # rather than following the drawdown, which is what lets a pattern be
77
- # compiled once instead of once per match. It means a match that
78
- # starts with almost nothing left can still run for a whole limit, so
79
- # an operation can overshoot by at most one limit -- bounded, which is
80
- # the point, and far cheaper than recompiling.
81
- #
82
- # An application that has set a stricter global Regexp.timeout keeps
83
- # it: baking in a larger value would leave it less protected than it
84
- # asked to be. A timeout set on one individual pattern is a different
85
- # matter, and is not preserved -- Budget#compile rebuilds the pattern
86
- # from its source and flags, neither of which carries one, so this
87
- # value takes its place. Only trusted code can supply such a pattern:
88
- # a condition decoded from JSON or BSON arrives as a string or a
89
- # BSON::Regexp::Raw, with no timeout of its own.
90
- @timeout = [ limit, ::Regexp.timeout ].compact.min if PER_REGEXP_TIMEOUT
91
- end
92
-
93
- # Draws the elapsed time down from the budget.
94
- #
95
- # @param [ Float ] elapsed The seconds to charge.
96
- def charge(elapsed)
97
- @remaining -= elapsed
98
- end
99
-
100
- # @return [ true | false ] Whether the budget is spent.
101
- def exhausted?
102
- @remaining <= 0
103
- end
104
-
105
- # Returns the condition as a Regexp which, where the Ruby in use
106
- # supports it, gives up once its timeout is spent.
107
- #
108
- # The condition is taken uncompiled so that the cache can answer before
109
- # any compiling happens. A BSON::Regexp::Raw memoizes its own compile,
110
- # but FieldExpression builds a fresh one for every $regex it evaluates,
111
- # so that memo is worth nothing across documents and the source would
112
- # otherwise be compiled once per document.
113
- #
114
- # @param [ Regexp | BSON::Regexp::Raw ] condition The condition.
115
- #
116
- # @return [ Regexp ] The compiled pattern.
117
- def compile(condition)
118
- @cache[cache_key(condition)] ||= bake(RegexpBudget.coerce(condition))
119
- end
120
-
121
- private
122
-
123
- # BSON::Regexp::Raw aliases eql? to == but leaves hash alone, so two
124
- # equal instances hash differently and cannot key the cache. What they
125
- # are equal by can. Anything else keys on itself and is left to coerce
126
- # to reject.
127
- def cache_key(condition)
128
- case condition
129
- when BSON::Regexp::Raw then [ condition.pattern, condition.options ]
130
- else condition
131
- end
132
- end
133
-
134
- # Rebuilds the pattern with the budget's timeout, where the Ruby in use
135
- # has them.
136
- def bake(regexp)
137
- return regexp unless PER_REGEXP_TIMEOUT
138
-
139
- ::Regexp.new(regexp.source, regexp.options, timeout: @timeout)
140
- end
141
- end
142
-
143
- class << self
144
- # Opens a budget scope for the duration of the block.
145
- #
146
- # Nested calls join the enclosing budget instead of starting a new one,
147
- # which is what lets a scan over many documents share a single limit.
148
- # It also keeps the recursion in Expression.matches? (through
149
- # $elemMatch, $and, $or and $nor) from resetting the budget.
150
- #
151
- # No budget is opened for a selector that carries no regular
152
- # expression. There would be nothing for it to bound, and on the
153
- # Timeout path it would put a deadline on in-memory work that has
154
- # nothing to do with regular expressions.
155
- #
156
- # The scope covers everything nested inside the block, a selector other
157
- # than this one included: a nested call joins the scope rather than
158
- # deciding for itself, which is what keeps a scan from walking the
159
- # selector once per document. Where the scope has nothing to bound, that
160
- # means nested selectors are not bounded either -- so do not open one
161
- # around work that can run application code. Loading documents runs find
162
- # callbacks, and a query in one of those brings its own selector.
163
- #
164
- # Where a selector does carry one, the Timeout path still measures the
165
- # whole scope rather than the matching alone, so a long scan can trip
166
- # the limit with a cheap pattern. That imprecision is accepted: the
167
- # alternative is a Timeout around each individual match, which was
168
- # measured at about nine seconds per million matches, and the limit
169
- # exists to bound a scan of exactly that size. The error message is
170
- # worded to hold either way, and Rubies with per-Regexp timeouts --
171
- # every supported MRI from 3.2 on -- do not take this path at all.
172
- #
173
- # Code inside the block that mutates state should be wrapped in
174
- # .protect, since on Rubies without a per-Regexp timeout the budget is
175
- # enforced with an asynchronous exception that can land anywhere.
176
- #
177
- # @param [ Hash ] selector The selector about to be evaluated.
178
- #
179
- # @return [ Object ] The value of the block.
180
- def open(selector, &block)
181
- # The key is present (holding NO_BUDGET) where an enclosing scope
182
- # found nothing to bound, so that a nested call does not scan the
183
- # selector again. Deciding before this check, in a default argument
184
- # say, would walk the selector once per document on a scan. The
185
- # storage is Thread.current directly, matching this branch's Threaded
186
- # conventions; only the key is shared with Threaded.
187
- return yield if Thread.current[Threaded::REGEXP_BUDGET_KEY]
188
-
189
- open_with(limit_for(selector), &block)
190
- end
191
-
192
- # Opens a budget scope for a limit the caller has already decided on.
193
- #
194
- # A caller that rearranges its work around the decision -- loading
195
- # documents up front so that nothing is mutated before the scan
196
- # finishes, say -- has to make it before it can act on it, and must not
197
- # then make it a second time. Asking .limit_for and letting .open ask
198
- # again reads the configured limit twice, and the two reads can differ:
199
- # a limit that becomes positive in between would establish a budget in
200
- # the branch that was chosen for not needing one, and on the Timeout
201
- # path that arms a deadline over work the branch never made
202
- # interruptible.
203
- #
204
- # See .open for what the scope does and does not bound, and for the
205
- # note about mutating state inside it.
206
- #
207
- # @param [ Float | nil ] limit The seconds the scope may spend, or nil
208
- # for a scope with nothing to bound.
209
- #
210
- # @return [ Object ] The value of the block.
211
- def open_with(limit, &block)
212
- return yield if Thread.current[Threaded::REGEXP_BUDGET_KEY]
213
-
214
- budget = Budget.new(limit) if limit&.positive?
215
-
216
- begin
217
- # Set inside the begin so that an asynchronous exception from an
218
- # enclosing timeout cannot leave the key behind on a pooled thread.
219
- Thread.current[Threaded::REGEXP_BUDGET_KEY] = budget || NO_BUDGET
220
-
221
- if budget.nil? || PER_REGEXP_TIMEOUT
222
- yield
223
- else
224
- begin
225
- Timeout.timeout(budget.limit, TimedOut, &block)
226
- rescue TimedOut
227
- raise timeout_error(budget)
228
- end
229
- end
230
- ensure
231
- # Setting nil on the way out removes the key on MRI, and reading a
232
- # nil value back is treated as absent regardless, so a lingering
233
- # nil on JRuby is harmless too.
234
- Thread.current[Threaded::REGEXP_BUDGET_KEY] = nil
235
- end
236
- end
237
-
238
- # The limit a scope evaluating this selector would be bounded by.
239
- #
240
- # A caller that has to rearrange its work to make the scan
241
- # interruptible -- loading documents up front so that nothing is
242
- # mutated before the scan finishes, say -- can ask this first and skip
243
- # the rearrangement, and whatever it costs, when there is no pattern to
244
- # bound. It then passes what it got to .open_with, so that the decision
245
- # it acted on is the one the scope is opened with. Callers with nothing
246
- # to rearrange should just call .open, which asks this itself.
247
- #
248
- # @param [ Hash ] selector The selector about to be evaluated.
249
- #
250
- # @return [ Float | nil ] The limit, or nil where there is nothing to
251
- # bound.
252
- def limit_for(selector)
253
- # nil.to_f is 0.0, so an unset limit and a limit of zero or less are
254
- # the same thing here: no limit. Zero is a common way to spell
255
- # "disabled", and taking it literally would mean a budget that is
256
- # spent before the first match and a query that can never run.
257
- limit = Mongoid::Config.in_memory_regexp_time_limit.to_f
258
- return nil unless limit.positive?
259
-
260
- limit if contains_regexp?(selector)
261
- end
262
-
263
- # Matches a value against a regular expression condition, charging the
264
- # time it takes against the open budget.
265
- #
266
- # @param [ Object ] value The value to match.
267
- # @param [ Regexp | BSON::Regexp::Raw ] condition The condition.
268
- #
269
- # @raise [ Errors::InMemoryRegexpTimeout ] if the budget is exhausted.
270
- #
271
- # @return [ Integer | nil ] The offset of the match, or nil.
272
- def match?(value, condition)
273
- budget = current
274
- return value =~ coerce(condition) unless budget
275
-
276
- started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
277
- pattern = nil
278
- begin
279
- raise timeout_error(budget) if budget.exhausted?
280
-
281
- # Compiling is charged too. It is not free, and for a pattern with
282
- # very many branches it costs far more than running the pattern
283
- # does, so leaving it out would leave a way to spend unbounded time
284
- # without the budget ever noticing.
285
- pattern = budget.compile(condition)
286
- value =~ pattern
287
- rescue TIMEOUT_ERROR
288
- # Name the limit that actually fired, which is not always the
289
- # budget's. A baked pattern carries it: the smaller of the budget's
290
- # limit and any global Regexp.timeout the application has set.
291
- # Where nothing was baked -- an engine that raises this error but
292
- # will not take a per-Regexp timeout, which is JRuby -- the global
293
- # is the only thing that can have fired, and the pattern reports
294
- # nil. Naming the budget's limit in either case would state a time
295
- # that was never spent and send the reader after a setting that is
296
- # not the one in the way.
297
- #
298
- # Both readers arrived together with Regexp::TimeoutError, so every
299
- # engine that can reach this rescue at all has them.
300
- raise timeout_error(budget, pattern&.timeout || ::Regexp.timeout || budget.limit)
301
- ensure
302
- budget.charge(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started)
303
- end
304
- end
305
-
306
- # Runs the block without letting a scope timeout tear it in half.
307
- #
308
- # Where the budget is enforced with Timeout, the exception is raised
309
- # asynchronously and can arrive at any point. Wrapping a mutation in
310
- # this holds the exception back until the block has finished, so the
311
- # interruption is deferred rather than given up.
312
- #
313
- # @return [ Object ] The value of the block.
314
- def protect(&block)
315
- Thread.handle_interrupt(TimedOut => :never, &block)
316
- end
317
-
318
- # The time left in the open budget, or nil when no budget is open.
319
- #
320
- # @return [ Float | nil ] The remaining seconds.
321
- def remaining
322
- current&.remaining
323
- end
324
-
325
- # Returns the condition as a Regexp, without a timeout.
326
- #
327
- # @param [ Regexp | BSON::Regexp::Raw ] condition The condition.
328
- #
329
- # @return [ Regexp ] The pattern.
330
- def coerce(condition)
331
- case condition
332
- when ::Regexp then condition
333
- when BSON::Regexp::Raw then condition.compile
334
- else raise ArgumentError, "Not a regular expression: #{condition.inspect}"
335
- end
336
- end
337
-
338
- private
339
-
340
- # The budget for the open scope, if there is one.
341
- #
342
- # A scope with nothing to bound leaves NO_BUDGET in storage, which reads
343
- # as no budget.
344
- #
345
- # @return [ Budget | nil ] The open budget.
346
- def current
347
- budget = Thread.current[Threaded::REGEXP_BUDGET_KEY]
348
- budget unless budget.equal?(NO_BUDGET)
349
- end
350
-
351
- # Whether evaluating the selector could run a regular expression.
352
- #
353
- # A string under $regex counts: FieldExpression turns it into a pattern
354
- # at match time.
355
- def contains_regexp?(object)
356
- case object
357
- when ::Regexp, BSON::Regexp::Raw
358
- true
359
- when Hash
360
- object.any? do |k, v|
361
- k.to_s == '$regex' || contains_regexp?(v)
362
- end
363
- when Array
364
- object.any? { |v| contains_regexp?(v) }
365
- else
366
- false
367
- end
368
- end
369
-
370
- # Builds the error with the scope timeout held back.
371
- #
372
- # Composing the message goes through I18n, which reads locale files the
373
- # first time it runs. An asynchronous TimedOut landing in the middle of
374
- # that is caught by I18n and reraised as a locale-loading failure, so
375
- # the real error never surfaces.
376
- #
377
- # @param [ Budget ] budget The open budget.
378
- # @param [ Float ] limit The limit that was exceeded. Defaults to the
379
- # budget's own, which is the right one to name everywhere the budget
380
- # itself ran out.
381
- def timeout_error(budget, limit = budget.limit)
382
- protect do
383
- Errors::InMemoryRegexpTimeout.new(limit)
384
- end
385
- end
386
- end
387
- end
388
- end
389
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Mongoid
4
- module Validatable
5
- # A specialization of the ActiveModel numericality validator, which adds
6
- # logic to recognize and accept BSON::Decimal128 as a number.
7
- class NumericalityValidator < ActiveModel::Validations::NumericalityValidator
8
- private
9
-
10
- # Ensure that BSON::Decimal128 is treated as a BigDecimal during the
11
- # validation step.
12
- def prepare_value_for_validation(value, record, attr_name)
13
- result = super
14
-
15
- result.is_a?(BSON::Decimal128) ? result.to_big_decimal : result
16
- end
17
- end
18
- end
19
- end