mongoid 9.1.0 → 9.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/lib/config/locales/en.yml +49 -0
  3. data/lib/mongoid/association/accessors.rb +9 -1
  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 +57 -100
  14. data/lib/mongoid/association/embedded/embeds_many/proxy.rb +6 -1
  15. data/lib/mongoid/association/embedded/embeds_one.rb +1 -0
  16. data/lib/mongoid/association/macros.rb +13 -1
  17. data/lib/mongoid/association/nested/many.rb +34 -5
  18. data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
  19. data/lib/mongoid/association/nested/one.rb +6 -1
  20. data/lib/mongoid/association/options.rb +17 -0
  21. data/lib/mongoid/association/referenced/belongs_to/eager.rb +6 -1
  22. data/lib/mongoid/association/referenced/belongs_to.rb +1 -0
  23. data/lib/mongoid/association/referenced/counter_cache.rb +3 -3
  24. data/lib/mongoid/association/referenced/has_and_belongs_to_many.rb +5 -0
  25. data/lib/mongoid/association/referenced/has_many/enumerable.rb +3 -1
  26. data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
  27. data/lib/mongoid/association/referenced/has_many_through/eager.rb +99 -0
  28. data/lib/mongoid/association/referenced/has_many_through/proxy.rb +68 -0
  29. data/lib/mongoid/association/referenced/has_many_through.rb +201 -0
  30. data/lib/mongoid/association/referenced/has_one.rb +1 -0
  31. data/lib/mongoid/association/referenced/has_one_through/eager.rb +65 -0
  32. data/lib/mongoid/association/referenced/has_one_through/proxy.rb +30 -0
  33. data/lib/mongoid/association/referenced/has_one_through.rb +167 -0
  34. data/lib/mongoid/association/referenced.rb +2 -0
  35. data/lib/mongoid/association/relatable.rb +40 -0
  36. data/lib/mongoid/association.rb +7 -0
  37. data/lib/mongoid/clients/factory.rb +7 -13
  38. data/lib/mongoid/collection_configurable.rb +8 -0
  39. data/lib/mongoid/config/defaults.rb +0 -1
  40. data/lib/mongoid/config/encryption.rb +36 -19
  41. data/lib/mongoid/config.rb +56 -5
  42. data/lib/mongoid/contextual/aggregable/memory.rb +6 -2
  43. data/lib/mongoid/contextual/memory.rb +18 -7
  44. data/lib/mongoid/criteria/includable.rb +35 -11
  45. data/lib/mongoid/criteria/queryable/mergeable.rb +4 -0
  46. data/lib/mongoid/criteria/queryable/selectable.rb +123 -7
  47. data/lib/mongoid/deprecable.rb +45 -2
  48. data/lib/mongoid/encryptable.rb +46 -0
  49. data/lib/mongoid/errors/config_redactor.rb +43 -0
  50. data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
  51. data/lib/mongoid/errors/mixed_client_configuration.rb +1 -1
  52. data/lib/mongoid/errors/no_client_database.rb +1 -1
  53. data/lib/mongoid/errors/no_client_hosts.rb +1 -1
  54. data/lib/mongoid/errors/no_encryption_schema.rb +28 -0
  55. data/lib/mongoid/errors/readonly_association.rb +29 -0
  56. data/lib/mongoid/errors.rb +4 -0
  57. data/lib/mongoid/extensions/vector.rb +64 -0
  58. data/lib/mongoid/extensions.rb +1 -0
  59. data/lib/mongoid/field_readable.rb +69 -0
  60. data/lib/mongoid/indexable.rb +38 -26
  61. data/lib/mongoid/matchable.rb +6 -1
  62. data/lib/mongoid/matcher/eq_impl_with_regexp.rb +2 -4
  63. data/lib/mongoid/matcher/regex.rb +11 -12
  64. data/lib/mongoid/matcher/regexp_budget.rb +372 -0
  65. data/lib/mongoid/matcher.rb +1 -0
  66. data/lib/mongoid/persistable/creatable.rb +1 -1
  67. data/lib/mongoid/persistable/updatable.rb +9 -11
  68. data/lib/mongoid/persistence_context.rb +35 -0
  69. data/lib/mongoid/search_indexable.rb +64 -23
  70. data/lib/mongoid/serializable.rb +12 -4
  71. data/lib/mongoid/tasks/database.rb +23 -9
  72. data/lib/mongoid/threaded.rb +36 -0
  73. data/lib/mongoid/timestamps/timeless.rb +84 -10
  74. data/lib/mongoid/traversable.rb +2 -2
  75. data/lib/mongoid/validatable/associated.rb +18 -2
  76. data/lib/mongoid/version.rb +1 -1
  77. data/spec/integration/app_spec.rb +7 -1
  78. data/spec/integration/associations/belongs_to_spec.rb +13 -0
  79. data/spec/integration/associations/has_and_belongs_to_many_spec.rb +17 -2
  80. data/spec/integration/dots_and_dollars_spec.rb +12 -2
  81. data/spec/integration/encryption_spec.rb +151 -0
  82. data/spec/integration/matcher_operator_data/regex.yml +21 -0
  83. data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
  84. data/spec/integration/query_operator_guard_spec.rb +89 -0
  85. data/spec/mongoid/association/eager_loadable_spec.rb +47 -0
  86. data/spec/mongoid/association/eager_spec.rb +2 -2
  87. data/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb +33 -0
  88. data/spec/mongoid/association/fallback_spec.rb +425 -0
  89. data/spec/mongoid/association/macros_spec.rb +26 -0
  90. data/spec/mongoid/association/referenced/belongs_to/eager_spec.rb +17 -0
  91. data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
  92. data/spec/mongoid/association/referenced/has_and_belongs_to_many_spec.rb +50 -0
  93. data/spec/mongoid/association/referenced/has_many/eager_spec.rb +33 -0
  94. data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
  95. data/spec/mongoid/association/referenced/has_many_through/eager_spec.rb +58 -0
  96. data/spec/mongoid/association/referenced/has_many_through/proxy_spec.rb +62 -0
  97. data/spec/mongoid/association/referenced/has_many_through_spec.rb +399 -0
  98. data/spec/mongoid/association/referenced/has_one_through/eager_spec.rb +58 -0
  99. data/spec/mongoid/association/referenced/has_one_through/proxy_spec.rb +49 -0
  100. data/spec/mongoid/association/referenced/has_one_through_spec.rb +211 -0
  101. data/spec/mongoid/attributes/nested_spec.rb +249 -8
  102. data/spec/mongoid/config/defaults_spec.rb +33 -0
  103. data/spec/mongoid/config/encryption_spec.rb +228 -0
  104. data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
  105. data/spec/mongoid/contextual/memory_spec.rb +177 -0
  106. data/spec/mongoid/criteria/includable_spec.rb +875 -0
  107. data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +15 -1
  108. data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +201 -0
  109. data/spec/mongoid/criteria_spec.rb +3 -0
  110. data/spec/mongoid/encryptable_spec.rb +61 -0
  111. data/spec/mongoid/errors/config_redactor_spec.rb +112 -0
  112. data/spec/mongoid/errors/mixed_client_configuration_spec.rb +31 -0
  113. data/spec/mongoid/errors/no_client_database_spec.rb +29 -0
  114. data/spec/mongoid/errors/no_client_hosts_spec.rb +26 -0
  115. data/spec/mongoid/errors/readonly_association_spec.rb +34 -0
  116. data/spec/mongoid/extensions/vector_spec.rb +175 -0
  117. data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
  118. data/spec/mongoid/search_indexable_spec.rb +295 -9
  119. data/spec/mongoid/tasks/database_spec.rb +18 -0
  120. data/spec/mongoid/timestamps/timeless_spec.rb +197 -5
  121. data/spec/mongoid/touchable_spec.rb +25 -0
  122. data/spec/mongoid/touchable_spec_models.rb +10 -0
  123. data/spec/mongoid/validatable/associated_spec.rb +19 -0
  124. data/spec/spec_helper.rb +5 -1
  125. data/spec/support/crypt/models.rb +157 -0
  126. metadata +52 -2
@@ -0,0 +1,26 @@
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
@@ -16,7 +16,7 @@ module Mongoid
16
16
  super(
17
17
  compose_message(
18
18
  'mixed_client_configuration',
19
- { name: name, config: config }
19
+ { name: name, config: ConfigRedactor.redact(config) }
20
20
  )
21
21
  )
22
22
  end
@@ -15,7 +15,7 @@ module Mongoid
15
15
  super(
16
16
  compose_message(
17
17
  'no_client_database',
18
- { name: name, config: config }
18
+ { name: name, config: ConfigRedactor.redact(config) }
19
19
  )
20
20
  )
21
21
  end
@@ -15,7 +15,7 @@ module Mongoid
15
15
  super(
16
16
  compose_message(
17
17
  'no_client_hosts',
18
- { name: name, config: config }
18
+ { name: name, config: ConfigRedactor.redact(config) }
19
19
  )
20
20
  )
21
21
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongoid
4
+ module Errors
5
+ # This error is raised when a model declares encrypted fields, but the
6
+ # namespace it is about to be persisted to is not covered by the automatic
7
+ # encryption schema of the client in use. Without a schema the driver has
8
+ # nothing to encrypt with, and the fields would be stored in plaintext.
9
+ class NoEncryptionSchema < MongoidError
10
+ # Create the new error.
11
+ #
12
+ # @example Create the error.
13
+ # NoEncryptionSchema.new(Band, 'music.bands', :default)
14
+ #
15
+ # @param [ Class ] klass The model class.
16
+ # @param [ String ] namespace The namespace the model resolved to.
17
+ # @param [ String | Symbol ] client The name of the client in use.
18
+ def initialize(klass, namespace, client)
19
+ super(
20
+ compose_message(
21
+ 'no_encryption_schema',
22
+ { klass: klass, namespace: namespace, client: client }
23
+ )
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongoid
4
+ module Errors
5
+ # Raised when attempting to write to a :through association, which is
6
+ # read-only.
7
+ class ReadonlyAssociation < MongoidError
8
+ # Instantiate the exception.
9
+ #
10
+ # @example Create the error.
11
+ # ReadonlyAssociation.new(Physician, association)
12
+ #
13
+ # @param [ Class ] klass The owner class.
14
+ # @param [ Mongoid::Association::Relatable ] association The through association.
15
+ def initialize(klass, association)
16
+ super(
17
+ compose_message(
18
+ 'readonly_association',
19
+ {
20
+ klass: klass,
21
+ name: association.name,
22
+ through: association.options[:through]
23
+ }
24
+ )
25
+ )
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'mongoid/errors/mongoid_error'
4
+ require 'mongoid/errors/config_redactor'
4
5
  require 'mongoid/errors/ambiguous_relationship'
5
6
  require 'mongoid/errors/attribute_not_loaded'
6
7
  require 'mongoid/errors/callback'
@@ -11,6 +12,7 @@ require 'mongoid/errors/document_not_found'
11
12
  require 'mongoid/errors/empty_config_file'
12
13
  require 'mongoid/errors/immutable_attribute'
13
14
  require 'mongoid/errors/in_memory_collation_not_supported'
15
+ require 'mongoid/errors/in_memory_regexp_timeout'
14
16
  require 'mongoid/errors/invalid_auto_encryption_configuration'
15
17
  require 'mongoid/errors/invalid_around_callback'
16
18
  require 'mongoid/errors/invalid_async_query_executor'
@@ -51,6 +53,7 @@ require 'mongoid/errors/mixed_client_configuration'
51
53
  require 'mongoid/errors/nested_attributes_metadata_not_found'
52
54
  require 'mongoid/errors/no_default_client'
53
55
  require 'mongoid/errors/no_environment'
56
+ require 'mongoid/errors/no_encryption_schema'
54
57
  require 'mongoid/errors/no_map_reduce_output'
55
58
  require 'mongoid/errors/no_metadata'
56
59
  require 'mongoid/errors/no_parent'
@@ -60,6 +63,7 @@ require 'mongoid/errors/no_client_database'
60
63
  require 'mongoid/errors/no_client_hosts'
61
64
  require 'mongoid/errors/readonly_attribute'
62
65
  require 'mongoid/errors/readonly_document'
66
+ require 'mongoid/errors/readonly_association'
63
67
  require 'mongoid/errors/rollback'
64
68
  require 'mongoid/errors/sessions_not_supported'
65
69
  require 'mongoid/errors/scope_overwrite'
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongoid
4
+ module Extensions
5
+ # Adds type-casting behavior to BSON::Vector class so that a field
6
+ # declared with +type: BSON::Vector+ is stored as a BSON binary of the
7
+ # vector subtype and read back as a BSON::Vector.
8
+ #
9
+ # Querying by an exact vector value (e.g. +where(embedding: vector)+) is
10
+ # not supported: BSON::Vector subclasses Array, so the criteria selector
11
+ # treats it as a list of elements rather than a scalar. Use Atlas Vector
12
+ # Search ($vectorSearch) for similarity queries on vector fields.
13
+ module Vector
14
+ # Turn the object from the ruby type we deal with to a Mongo friendly
15
+ # type.
16
+ #
17
+ # @example Mongoize the object.
18
+ # object.mongoize
19
+ #
20
+ # @return [ BSON::Binary | nil ] The object as a vector binary.
21
+ def mongoize
22
+ BSON::Vector.mongoize(self)
23
+ end
24
+
25
+ module ClassMethods
26
+ # Mongoize an object of any type to how it's stored in the db.
27
+ #
28
+ # @example Mongoize the object.
29
+ # BSON::Vector.mongoize(vector)
30
+ #
31
+ # @param [ Object ] object The object to Mongoize.
32
+ #
33
+ # @return [ BSON::Binary | nil ] A vector binary or nil.
34
+ def mongoize(object)
35
+ case object
36
+ when BSON::Vector then BSON::Binary.from_vector(object)
37
+ when BSON::Binary then object
38
+ end
39
+ end
40
+
41
+ # Convert the object from its mongo friendly ruby type back to a
42
+ # BSON::Vector.
43
+ #
44
+ # @example Demongoize the object.
45
+ # BSON::Vector.demongoize(binary)
46
+ #
47
+ # @param [ Object ] object The object to demongoize.
48
+ #
49
+ # @return [ BSON::Vector | nil ] The vector or nil.
50
+ def demongoize(object)
51
+ case object
52
+ when BSON::Binary then (object.type == :vector) ? object.as_vector : nil
53
+ when BSON::Vector then object
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ if defined?(BSON::Vector)
62
+ BSON::Vector.include Mongoid::Extensions::Vector
63
+ BSON::Vector.extend(Mongoid::Extensions::Vector::ClassMethods)
64
+ end
@@ -26,3 +26,4 @@ require 'mongoid/extensions/symbol'
26
26
  require 'mongoid/extensions/time'
27
27
  require 'mongoid/extensions/time_with_zone'
28
28
  require 'mongoid/extensions/true_class'
29
+ require 'mongoid/extensions/vector'
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongoid
4
+ # Reads the value of a field name from a document without dispatching the
5
+ # name as an arbitrary method.
6
+ #
7
+ # Field names may come from application input, where a name like +destroy+
8
+ # or +attributes+ would delete the document or disclose its contents.
9
+ # Declared field and association names are validated when they are declared
10
+ # (see Mongoid.destructive_fields), so a name that resolves to one of them
11
+ # is safe to send to a document. Any other name is read from the attributes
12
+ # hash, which is what the database-backed query contexts do.
13
+ #
14
+ # @api private
15
+ module FieldReadable
16
+ private
17
+
18
+ # Read the value of the given field name from the given document.
19
+ #
20
+ # @param [ Document ] document The document to read from.
21
+ # @param [ String | Symbol ] name The name of the field.
22
+ #
23
+ # @return [ Object | nil ] The value of the field, or nil when the name
24
+ # is neither a declared field nor present in the attributes.
25
+ def read_field_value(document, name)
26
+ name = name.to_s
27
+ # A blank name cannot name a field. Return nil, which is what reading
28
+ # one has always done.
29
+ return nil if name.blank?
30
+
31
+ if (meth = readable_method_for(document.class, name))
32
+ document.public_send(meth)
33
+ else
34
+ document.attributes[document.class.database_field_name(name)]
35
+ end
36
+ end
37
+
38
+ # Resolve the given name to a method that is declared by the given class,
39
+ # and therefore safe to send to one of its instances.
40
+ #
41
+ # @param [ Class ] klass The document class.
42
+ # @param [ String ] name The name of the field.
43
+ #
44
+ # @return [ String | nil ] The method to send, or nil when the name is
45
+ # not declared by the class.
46
+ def readable_method_for(klass, name)
47
+ # Fields, associations, and field aliases each define a reader of their
48
+ # own name. Note that associations must be resolved before aliases: a
49
+ # belongs_to aliases its own name to its foreign key, and reading
50
+ # `band` must give the document, not the id.
51
+ return name if klass.relations.key?(name) ||
52
+ klass.fields.key?(name) ||
53
+ klass.aliased_fields.key?(name)
54
+
55
+ # An association may also be named by its `store_as`, which has no
56
+ # reader of its own, or by its ids accessor, which does.
57
+ if (assoc = klass.relations[klass.aliased_associations[name]])
58
+ return (assoc.store_as == name) ? assoc.name.to_s : name
59
+ end
60
+
61
+ # Localized fields also get a _translations reader, which does not
62
+ # appear in the fields hash.
63
+ base = name.delete_suffix(Fields::TRANSLATIONS_SFX)
64
+ return nil if base == name
65
+
66
+ name if klass.fields[klass.database_field_name(base)]&.localized?
67
+ end
68
+ end
69
+ end
@@ -24,18 +24,9 @@ module Mongoid
24
24
  def create_indexes
25
25
  return unless index_specifications
26
26
 
27
- default_options = { background: Config.background_indexing }
28
-
29
- index_specifications.each do |spec|
30
- key, options = spec.key, default_options.merge(spec.options)
31
- if database = options[:database]
32
- with(database: database) do |klass|
33
- klass.collection.indexes(session: _session).create_one(key, options.except(:database))
34
- end
35
- else
36
- collection.indexes(session: _session).create_one(key, options)
37
- end
38
- end and true
27
+ Threaded.with_collection_management do
28
+ perform_create_indexes
29
+ end
39
30
  end
40
31
 
41
32
  # Send the actual index removal comments to the MongoDB driver,
@@ -46,20 +37,9 @@ module Mongoid
46
37
  #
47
38
  # @return [ true ] If the operation succeeded.
48
39
  def remove_indexes
49
- indexed_database_names.each do |database|
50
- with(database: database) do |klass|
51
- klass.collection.indexes(session: _session).each do |spec|
52
- next if spec['name'] == '_id_'
53
-
54
- klass.collection.indexes(session: _session).drop_one(spec['key'])
55
- logger.info(
56
- "MONGOID: Removed index '#{spec['name']}' on collection " +
57
- "'#{klass.collection.name}' in database '#{database}'."
58
- )
59
- end
60
- rescue Mongo::Error::OperationFailure
61
- end
62
- end and true
40
+ Threaded.with_collection_management do
41
+ perform_remove_indexes
42
+ end
63
43
  end
64
44
 
65
45
  # Add the default indexes to the root document if they do not already
@@ -119,6 +99,38 @@ module Mongoid
119
99
 
120
100
  private
121
101
 
102
+ def perform_create_indexes
103
+ default_options = { background: Config.background_indexing }
104
+
105
+ index_specifications.each do |spec|
106
+ key, options = spec.key, default_options.merge(spec.options)
107
+ if database = options[:database]
108
+ with(database: database) do |klass|
109
+ klass.collection.indexes(session: _session).create_one(key, options.except(:database))
110
+ end
111
+ else
112
+ collection.indexes(session: _session).create_one(key, options)
113
+ end
114
+ end and true
115
+ end
116
+
117
+ def perform_remove_indexes
118
+ indexed_database_names.each do |database|
119
+ with(database: database) do |klass|
120
+ klass.collection.indexes(session: _session).each do |spec|
121
+ next if spec['name'] == '_id_'
122
+
123
+ klass.collection.indexes(session: _session).drop_one(spec['key'])
124
+ logger.info(
125
+ "MONGOID: Removed index '#{spec['name']}' on collection " +
126
+ "'#{klass.collection.name}' in database '#{database}'."
127
+ )
128
+ end
129
+ rescue Mongo::Error::OperationFailure
130
+ end
131
+ end and true
132
+ end
133
+
122
134
  # Get the names of all databases for this model that have index
123
135
  # definitions.
124
136
  #
@@ -16,7 +16,12 @@ module Mongoid
16
16
  #
17
17
  # @return [ true | false ] True if matches, false if not.
18
18
  def _matches?(selector)
19
- Matcher::Expression.matches?(self, selector)
19
+ # Opens a regexp budget for this document only. Callers that match many
20
+ # documents against one selector open a budget of their own first, and
21
+ # this one joins it rather than giving every document a fresh limit.
22
+ Matcher::RegexpBudget.open(selector) do
23
+ Matcher::Expression.matches?(self, selector)
24
+ end
20
25
  end
21
26
  end
22
27
  end
@@ -18,10 +18,8 @@ module Mongoid
18
18
  # @api private
19
19
  module_function def matches?(_original_operator, value, condition)
20
20
  case condition
21
- when Regexp
22
- value.respond_to?(:=~) && value =~ condition
23
- when ::BSON::Regexp::Raw
24
- value.respond_to?(:=~) && value =~ condition.compile
21
+ when Regexp, ::BSON::Regexp::Raw
22
+ value.respond_to?(:=~) && RegexpBudget.match?(value, condition)
25
23
  else
26
24
  if value.is_a?(Time) && condition.is_a?(Time)
27
25
  EqImpl.time_eq?(value, condition)
@@ -16,24 +16,23 @@ module Mongoid
16
16
  #
17
17
  # @api private
18
18
  module_function def matches?(_exists, value, condition)
19
- condition = case condition
20
- when Regexp
21
- condition
22
- when BSON::Regexp::Raw
23
- condition.compile
24
- else
25
- # Note that strings must have been converted to a regular expression
26
- # instance already (with $options taken into account, if provided).
27
- raise Errors::InvalidQuery, "$regex requires a regular expression argument: #{Errors::InvalidQuery.truncate_expr(condition)}"
28
- end
19
+ unless condition.is_a?(Regexp) || condition.is_a?(BSON::Regexp::Raw)
20
+ # Note that strings must have been converted to a regular expression
21
+ # instance already (with $options taken into account, if provided).
22
+ raise Errors::InvalidQuery, "$regex requires a regular expression argument: #{Errors::InvalidQuery.truncate_expr(condition)}"
23
+ end
29
24
 
25
+ # The condition is compiled by RegexpBudget rather than here, so that
26
+ # the budget's timeout can be baked into the pattern.
30
27
  case value
31
28
  when Array
29
+ # Object#=~ is gone as of Ruby 3.2, so an element that cannot be
30
+ # matched against has to be rejected rather than passed to =~.
32
31
  value.any? do |v|
33
- v =~ condition
32
+ v.respond_to?(:=~) && RegexpBudget.match?(v, condition)
34
33
  end
35
34
  when String
36
- value =~ condition
35
+ RegexpBudget.match?(value, condition)
37
36
  else
38
37
  false
39
38
  end