mongoid 7.1.0 → 7.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 (37) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/CHANGELOG.md +6 -6
  5. data/README.md +1 -1
  6. data/lib/config/locales/en.yml +4 -4
  7. data/lib/mongoid/association/referenced/belongs_to/eager.rb +38 -2
  8. data/lib/mongoid/association/referenced/eager.rb +29 -9
  9. data/lib/mongoid/config.rb +39 -9
  10. data/lib/mongoid/criteria.rb +16 -3
  11. data/lib/mongoid/criteria/queryable/pipeline.rb +3 -2
  12. data/lib/mongoid/criteria/queryable/selectable.rb +94 -7
  13. data/lib/mongoid/criteria/queryable/storable.rb +104 -99
  14. data/lib/mongoid/errors/eager_load.rb +2 -0
  15. data/lib/mongoid/persistable/pushable.rb +7 -1
  16. data/lib/mongoid/serializable.rb +9 -3
  17. data/lib/mongoid/version.rb +1 -1
  18. data/lib/rails/generators/mongoid/config/templates/mongoid.yml +32 -23
  19. data/spec/app/models/coding.rb +4 -0
  20. data/spec/app/models/coding/pull_request.rb +12 -0
  21. data/spec/app/models/delegating_patient.rb +16 -0
  22. data/spec/app/models/publication.rb +5 -0
  23. data/spec/app/models/publication/encyclopedia.rb +12 -0
  24. data/spec/app/models/publication/review.rb +14 -0
  25. data/spec/integration/document_spec.rb +22 -0
  26. data/spec/mongoid/association/referenced/belongs_to/eager_spec.rb +193 -10
  27. data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +504 -127
  28. data/spec/mongoid/criteria/queryable/selectable_spec.rb +52 -0
  29. data/spec/mongoid/criteria/queryable/storable_spec.rb +80 -2
  30. data/spec/mongoid/criteria_spec.rb +32 -0
  31. data/spec/mongoid/persistable/pushable_spec.rb +55 -1
  32. data/spec/mongoid/serializable_spec.rb +129 -18
  33. data/spec/spec_helper.rb +2 -0
  34. data/spec/support/expectations.rb +3 -1
  35. data/spec/support/helpers.rb +11 -0
  36. metadata +504 -490
  37. metadata.gz.sig +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8352346c7b47e2323d4afee8968a5581b091735a11d3aacaf9a1c73197d511db
4
- data.tar.gz: 4780a1d8ec316330301e933a145957e48f4927c9fbcbe582404f062179280947
3
+ metadata.gz: b90b3442fbb22055d692fca89eb33e90868e45588812f1975926dee5a85d8b10
4
+ data.tar.gz: d9c2a5eaf30d5ca34a365be11b648efc8932a7cda1b29fd8982847cf9cb528cd
5
5
  SHA512:
6
- metadata.gz: 870e9d5aff9bd06217cea69dd7b86b15ea9ec89464a8e6f579e24e58789a1e5612256026927e6cdd0e17cb39340cf4ef7758bc75fc1b5176939f8b11968bd76c
7
- data.tar.gz: fc095139739ed83771b35d9f2c91b2a0e33bb99125df7622fe0f8f24bf971c6d0cabcd5ac507d4bb72e8a178bc988661197258f20f0193b7e72c8d7a219d2459
6
+ metadata.gz: ca1480e9cbd438a8905ef4d8f1e9ac59e66ee6436b0017f40c350de6421dd5f99a8a96c1bfa31bb0291738ad97bdad5ce18ee2ccc60a0cea2d91f1795abd5a8c
7
+ data.tar.gz: a3f7da9b676f022dc9bf48199eeb34f68fc14e2c076b539a81149401a6b1b81914f96ba21daca7a392b40a5bf5ed8ef9b76d7499033b786b530d161a1e3bb29b
Binary file
data.tar.gz.sig CHANGED
Binary file
@@ -1848,18 +1848,18 @@ child elements.
1848
1848
  set a child on a relation without the proper inverse_of definitions
1849
1849
  due to Mongoid not being able to determine it.
1850
1850
 
1851
- class Lush
1851
+ class Car
1852
1852
  include Mongoid::Document
1853
- embeds_one :whiskey, class_name: "Drink"
1853
+ embeds_one :engine, class_name: "Motor"
1854
1854
  end
1855
1855
 
1856
- class Drink
1856
+ class Motor
1857
1857
  include Mongoid::Document
1858
- embedded_in :alcoholic, class_name: "Lush"
1858
+ embedded_in :machine, class_name: "Car"
1859
1859
  end
1860
1860
 
1861
- lush = Lush.new
1862
- lush.whiskey = Drink.new # raises an InverseNotFound error.
1861
+ car = Car.new
1862
+ car.engine = Motor.new # raises an InverseNotFound error.
1863
1863
 
1864
1864
  * \#1680 Polymorphic relations now use `*_type` keys in lookup queries.
1865
1865
 
data/README.md CHANGED
@@ -31,7 +31,7 @@ Support
31
31
  -------
32
32
 
33
33
  * [Stack Overflow](http://stackoverflow.com/questions/tagged/mongoid)
34
- * [Mongoid Google Group](http://groups.google.com/group/mongoid)
34
+ * [MongoDB Community Forum](https://developer.mongodb.com/community/forums/tags/c/drivers-odms-connectors/7/mongoid-odm)
35
35
  * [#mongoid](http://webchat.freenode.net/?channels=mongoid) on Freenode IRC
36
36
 
37
37
  License
@@ -282,13 +282,13 @@ en:
282
282
  you will need to explicitly tell Mongoid on the association what
283
283
  the inverse is.\n\n
284
284
  Example:\n
285
- \_\_class Lush\n
285
+ \_\_class Car\n
286
286
  \_\_\_\_include Mongoid::Document\n
287
- \_\_\_\_has_one :whiskey, class_name: \"Drink\", inverse_of: :alcoholic\n
287
+ \_\_\_\_has_one :engine, class_name: \"Motor\", inverse_of: :machine\n
288
288
  \_\_end\n\n
289
- \_\_class Drink\n
289
+ \_\_class Motor\n
290
290
  \_\_\_\_include Mongoid::Document\n
291
- \_\_\_\_belongs_to :alcoholic, class_name: \"Lush\", inverse_of: :whiskey\n
291
+ \_\_\_\_belongs_to :machine, class_name: \"Car\", inverse_of: :engine\n
292
292
  \_\_end"
293
293
  invalid_set_polymorphic_relation:
294
294
  message: "The %{name} attribute can't be set to an instance of
@@ -12,8 +12,6 @@ module Mongoid
12
12
  private
13
13
 
14
14
  def preload
15
- raise Errors::EagerLoad.new(@association.name) if @association.polymorphic?
16
-
17
15
  @docs.each do |d|
18
16
  set_relation(d, nil)
19
17
  end
@@ -24,6 +22,44 @@ module Mongoid
24
22
  end
25
23
  end
26
24
 
25
+ # Retrieves the documents referenced by the association, and
26
+ # yields each one sequentially to the provided block. If the
27
+ # association is not polymorphic, all documents are retrieved in
28
+ # a single query. If the association is polymorphic, one query is
29
+ # issued per association target class.
30
+ def each_loaded_document(&block)
31
+ if @association.polymorphic?
32
+ keys_by_type_from_docs.each do |type, keys|
33
+ each_loaded_document_of_class(Object.const_get(type), keys, &block)
34
+ end
35
+ else
36
+ super
37
+ end
38
+ end
39
+
40
+ # Returns a map from association target class name to foreign key
41
+ # values for the documents of that association target class,
42
+ # as referenced by this association.
43
+ def keys_by_type_from_docs
44
+ inverse_type_field = @association.inverse_type
45
+
46
+ @docs.each_with_object({}) do |doc, keys_by_type|
47
+ next unless doc.respond_to?(inverse_type_field) && doc.respond_to?(group_by_key)
48
+ inverse_type_name = doc.send(inverse_type_field)
49
+ # If a particular document does not have a value for this
50
+ # association, inverse_type_name will be nil.
51
+ next if inverse_type_name.nil?
52
+
53
+ key_value = doc.send(group_by_key)
54
+ # If a document has the *_type field set but the corresponding
55
+ # *_id field not set, the key value here will be nil.
56
+ next unless key_value
57
+
58
+ keys_by_type[inverse_type_name] ||= []
59
+ keys_by_type[inverse_type_name].push(key_value)
60
+ end
61
+ end
62
+
27
63
  def group_by_key
28
64
  @association.foreign_key
29
65
  end
@@ -59,17 +59,29 @@ module Mongoid
59
59
  raise NotImplementedError
60
60
  end
61
61
 
62
- # Run the preloader.
63
- #
64
- # @example Iterate over the documents loaded for the current association
65
- # loader.each_loaded_document { |doc| }
62
+ # Retrieves the documents referenced by the association, and
63
+ # yields each one sequentially to the provided block. If the
64
+ # association is not polymorphic, all documents are retrieved in
65
+ # a single query. If the association is polymorphic, one query is
66
+ # issued per association target class.
66
67
  #
67
68
  # @since 4.0.0
68
- def each_loaded_document
69
- doc_keys = keys_from_docs
70
- return @association.klass.none if doc_keys.all?(&:nil?)
69
+ def each_loaded_document(&block)
70
+ each_loaded_document_of_class(@association.klass, keys_from_docs, &block)
71
+ end
71
72
 
72
- criteria = @association.klass.any_in(key => doc_keys)
73
+ # Retrieves the documents of the specified class, that have the
74
+ # foreign key included in the specified list of keys.
75
+ #
76
+ # When the documents are retrieved, the set of inclusions applied
77
+ # is the set of inclusions applied to the host document minus the
78
+ # association that is being eagerly loaded.
79
+ private def each_loaded_document_of_class(cls, keys)
80
+ # Note: keys should not include nil elements.
81
+ # Upstream code is responsible for eliminating nils from keys.
82
+ return cls.none if keys.empty?
83
+
84
+ criteria = cls.any_in(key => keys)
73
85
  criteria.inclusions = criteria.inclusions - [@association]
74
86
  criteria.each do |doc|
75
87
  yield doc
@@ -93,6 +105,9 @@ module Mongoid
93
105
 
94
106
  # Return a hash with the current documents grouped by key.
95
107
  #
108
+ # Documents that do not have a value for the association being loaded
109
+ # are not returned.
110
+ #
96
111
  # @example Return a hash with the current documents grouped by key.
97
112
  # loader.grouped_docs
98
113
  #
@@ -102,10 +117,15 @@ module Mongoid
102
117
  def grouped_docs
103
118
  @grouped_docs[@association.name] ||= @docs.group_by do |doc|
104
119
  doc.send(group_by_key) if doc.respond_to?(group_by_key)
120
+ end.reject do |k, v|
121
+ k.nil?
105
122
  end
106
123
  end
107
124
 
108
- # Group the documents and return the keys
125
+ # Group the documents and return the keys.
126
+ #
127
+ # This method omits nil keys (i.e. keys from documents that do not
128
+ # have a value for the association being loaded).
109
129
  #
110
130
  # @example
111
131
  # loader.keys_from_docs
@@ -18,14 +18,31 @@ module Mongoid
18
18
 
19
19
  LOCK = Mutex.new
20
20
 
21
+ # Application name that is printed to the mongodb logs upon establishing
22
+ # a connection in server versions >= 3.4. Note that the name cannot
23
+ # exceed 128 bytes. It is also used as the database name if the
24
+ # database name is not explicitly defined.
25
+ option :app_name, default: nil
26
+
27
+ # Create indexes in background by default.
28
+ option :background_indexing, default: false
29
+
30
+ # Mark belongs_to associations as required by default, so that saving a
31
+ # model with a missing belongs_to association will trigger a validation
32
+ # error.
33
+ option :belongs_to_required_by_default, default: true
34
+
35
+ # Raise an exception when a field is redefined.
36
+ option :duplicate_fields_exception, default: false
37
+
38
+ # Include the root model name in json serialization.
21
39
  option :include_root_in_json, default: false
40
+
41
+ # # Include the _type field in serialization.
22
42
  option :include_type_for_serialization, default: false
23
- option :preload_models, default: false
24
- option :raise_not_found_error, default: true
25
- option :scope_overwrite_exception, default: false
26
- option :duplicate_fields_exception, default: false
27
- option :use_activesupport_time_zone, default: true
28
- option :use_utc, default: false
43
+
44
+ # Whether to join nested persistence contexts for atomic operations
45
+ # to parent contexts by default.
29
46
  option :join_contexts, default: false
30
47
 
31
48
  # The log level.
@@ -38,9 +55,22 @@ module Mongoid
38
55
  # configuration file is the log level given by this option honored.
39
56
  option :log_level, default: :info
40
57
 
41
- option :belongs_to_required_by_default, default: true
42
- option :app_name, default: nil
43
- option :background_indexing, default: false
58
+ # Preload all models in development, needed when models use inheritance.
59
+ option :preload_models, default: false
60
+
61
+ # Raise an error when performing a #find and the document is not found.
62
+ option :raise_not_found_error, default: true
63
+
64
+ # Raise an error when defining a scope with the same name as an
65
+ # existing method.
66
+ option :scope_overwrite_exception, default: false
67
+
68
+ # Use ActiveSupport's time zone in time operations instead of the
69
+ # Ruby default time zone.
70
+ option :use_activesupport_time_zone, default: true
71
+
72
+ # Return stored times as UTC.
73
+ option :use_utc, default: false
44
74
 
45
75
  # Has Mongoid been configured? This is checking that at least a valid
46
76
  # client config exists.
@@ -400,9 +400,22 @@ module Mongoid
400
400
  # @return [ Criteria ] The cloned selectable.
401
401
  #
402
402
  # @since 1.0.0
403
- def where(expression)
404
- if expression.is_a?(::String) && embedded?
405
- raise Errors::UnsupportedJavascript.new(klass, expression)
403
+ def where(*args)
404
+ # Historically this method required exactly one argument.
405
+ # As of https://jira.mongodb.org/browse/MONGOID-4804 it also accepts
406
+ # zero arguments.
407
+ # The underlying where implemetation that super invokes supports
408
+ # any number of arguments, but we don't presently allow mutiple
409
+ # arguments through this method. This API can be reconsidered in the
410
+ # future.
411
+ if args.length > 1
412
+ raise ArgumentError, "Criteria#where requires zero or one arguments (given #{args.length})"
413
+ end
414
+ if args.length == 1
415
+ expression = args.first
416
+ if expression.is_a?(::String) && embedded?
417
+ raise Errors::UnsupportedJavascript.new(klass, expression)
418
+ end
406
419
  end
407
420
  super
408
421
  end
@@ -33,7 +33,7 @@ module Mongoid
33
33
  # Add a group operation to the aggregation pipeline.
34
34
  #
35
35
  # @example Add a group operation.
36
- # pipeline.group(:count.sum => 1, :max.max => "likes")
36
+ # pipeline.group(:_id => "foo", :count.sum => 1, :max.max => "likes")
37
37
  #
38
38
  # @param [ Hash ] entry The group entry.
39
39
  #
@@ -76,7 +76,8 @@ module Mongoid
76
76
  # pipeline.unwind(:field)
77
77
  # pipeline.unwind(document)
78
78
  #
79
- # @param [ String, Symbol, Hash ] field_or_doc The name of the field or a document.
79
+ # @param [ String, Symbol, Hash ] field_or_doc A field name or a
80
+ # document.
80
81
  #
81
82
  # @return [ Pipeline ] The pipeline.
82
83
  #
@@ -92,8 +92,8 @@ module Mongoid
92
92
  normalized = _mongoid_normalize_expr(new_s)
93
93
  normalized.each do |k, v|
94
94
  k = k.to_s
95
- if c.selector[k] || k[0] == ?$
96
- c = c.send(:__multi__, [{k => v}], '$and')
95
+ if c.selector[k]
96
+ c = c.send(:__multi__, [k => v], '$and')
97
97
  else
98
98
  c.selector.store(k, v)
99
99
  end
@@ -586,13 +586,34 @@ module Mongoid
586
586
  end
587
587
  key :not, :override, "$not"
588
588
 
589
- # Adds $or selection to the selectable.
589
+ # Creates a disjunction using $or from the existing criteria in the
590
+ # receiver and the provided arguments.
590
591
  #
591
- # @example Add the $or selection.
592
+ # This behavior (receiver becoming one of the disjunction operands)
593
+ # matches ActiveRecord's +or+ behavior.
594
+ #
595
+ # Use +any_of+ to add a disjunction of the arguments as an additional
596
+ # constraint to the criteria already existing in the receiver.
597
+ #
598
+ # Each argument can be a Hash, a Criteria object, an array of
599
+ # Hash or Criteria objects, or a nested array. Nested arrays will be
600
+ # flattened and can be of any depth. Passing arrays is deprecated.
601
+ #
602
+ # @example Add the $or selection where both fields must have the specified values.
592
603
  # selectable.or(field: 1, field: 2)
593
604
  #
594
- # @param [ Array<Hash | Criteria> ] criteria Multiple key/value pair
595
- # matches or Criteria objects.
605
+ # @example Add the $or selection where either value match is sufficient.
606
+ # selectable.or({field: 1}, {field: 2})
607
+ #
608
+ # @example Same as previous example but using the deprecated array wrap.
609
+ # selectable.or([{field: 1}, {field: 2}])
610
+ #
611
+ # @example Same as previous example, also deprecated.
612
+ # selectable.or([{field: 1}], [{field: 2}])
613
+ #
614
+ # @param [ Hash | Criteria | Array<Hash | Criteria>, ... ] criteria
615
+ # Multiple key/value pair matches or Criteria objects, or arrays
616
+ # thereof. Passing arrays is deprecated.
596
617
  #
597
618
  # @return [ Selectable ] The new selectable.
598
619
  #
@@ -600,7 +621,73 @@ module Mongoid
600
621
  def or(*criteria)
601
622
  _mongoid_add_top_level_operation('$or', criteria)
602
623
  end
603
- alias :any_of :or
624
+
625
+ # Adds a disjunction of the arguments as an additional constraint
626
+ # to the criteria already existing in the receiver.
627
+ #
628
+ # Use +or+ to make the receiver one of the disjunction operands.
629
+ #
630
+ # Each argument can be a Hash, a Criteria object, an array of
631
+ # Hash or Criteria objects, or a nested array. Nested arrays will be
632
+ # flattened and can be of any depth. Passing arrays is deprecated.
633
+ #
634
+ # @example Add the $or selection where both fields must have the specified values.
635
+ # selectable.any_of(field: 1, field: 2)
636
+ #
637
+ # @example Add the $or selection where either value match is sufficient.
638
+ # selectable.any_of({field: 1}, {field: 2})
639
+ #
640
+ # @example Same as previous example but using the deprecated array wrap.
641
+ # selectable.any_of([{field: 1}, {field: 2}])
642
+ #
643
+ # @example Same as previous example, also deprecated.
644
+ # selectable.any_of([{field: 1}], [{field: 2}])
645
+ #
646
+ # @param [ Hash | Criteria | Array<Hash | Criteria>, ... ] criteria
647
+ # Multiple key/value pair matches or Criteria objects, or arrays
648
+ # thereof. Passing arrays is deprecated.
649
+ #
650
+ # @return [ Selectable ] The new selectable.
651
+ #
652
+ # @since 1.0.0
653
+ def any_of(*criteria)
654
+ criteria = _mongoid_flatten_arrays(criteria)
655
+ case criteria.length
656
+ when 0
657
+ clone
658
+ when 1
659
+ # When we have a single criteria, any_of behaves like and.
660
+ # Note: criteria can be a Query object, which #where method does
661
+ # not support.
662
+ self.and(*criteria)
663
+ else
664
+ # When we have multiple criteria, combine them all with $or
665
+ # and add the result to self.
666
+ exprs = criteria.map do |criterion|
667
+ if criterion.is_a?(Selectable)
668
+ _mongoid_normalize_expr(criterion.selector)
669
+ else
670
+ Hash[criterion.map do |k, v|
671
+ if k.is_a?(Symbol)
672
+ [k.to_s, v]
673
+ else
674
+ [k, v]
675
+ end
676
+ end]
677
+ end
678
+ end
679
+ # Should be able to do:
680
+ #where('$or' => exprs)
681
+ # But since that is broken do instead:
682
+ clone.tap do |query|
683
+ if query.selector['$or']
684
+ query.selector.store('$or', query.selector['$or'] + exprs)
685
+ else
686
+ query.selector.store('$or', exprs)
687
+ end
688
+ end
689
+ end
690
+ end
604
691
 
605
692
  # Add a $size selection for array fields.
606
693
  #
@@ -17,67 +17,49 @@ module Mongoid
17
17
  # @api private
18
18
  module Storable
19
19
 
20
- # Adds an operator expression to the selector.
21
- #
22
- # This method takes the operator and the operator value expression
23
- # separately for callers' convenience. It can be considered to
24
- # handle storing the hash `{operator => op_expr}`.
25
- #
26
- # If the selector already has the specified operator in it (on the
27
- # top level), the new condition given in op_expr is added to the
28
- # existing conditions for the specified operator. This is
29
- # straightforward for $and; for other logical operators, the behavior
30
- # of this method is to add the new conditions to the existing operator.
31
- # For example, if the selector is currently:
32
- #
33
- # {'foo' => 'bar', '$or' => [{'hello' => 'world'}]}
34
- #
35
- # ... and operator is '$or' and op_expr is `{'test' => 123'}`,
36
- # the resulting selector will be:
37
- #
38
- # {'foo' => 'bar', '$or' => [{'hello' => 'world'}, {'test' => 123}]}
20
+ # Adds a field expression to the query.
39
21
  #
40
- # This does not implement an OR between the existing selector and the
41
- # new operator expression - handling this is the job of upstream
42
- # methods. This method simply stores op_expr into the selector on the
43
- # assumption that the existing selector is the correct left hand side
44
- # of the operation already.
22
+ # +field+ must be a field name, and it must be a string. The upstream
23
+ # code must have converted other field/key types to the simple string
24
+ # form by the time this method is invoked.
45
25
  #
46
- # For non-logical query-level operators like $where and $text, if
47
- # there already is a top-level operator with the same name, the
48
- # op_expr is added to the selector via a top-level $and operator,
49
- # thus producing a selector having both operator values.
26
+ # +value+ can be of any type, it is written into the selector unchanged.
50
27
  #
51
- # This method does not simplify values (i.e. if the selector is
52
- # currently empty and operator is $and, op_expr is written to the
53
- # selector with $and even if the $and can in principle be elided).
28
+ # This method performs no processing on the provided field value.
54
29
  #
55
- # This method mutates the receiver.
30
+ # Mutates the receiver.
56
31
  #
57
- # @param [ String ] operator The operator to add.
58
- # @param [ Hash ] op_expr Operator value to add.
32
+ # @param [ String ] field The field name.
33
+ # @param [ Object ] value The field value.
59
34
  #
60
35
  # @return [ Storable ] self.
61
- def add_operator_expression(operator, op_expr)
62
- unless operator.is_a?(String)
63
- raise ArgumentError, "Operator must be a string: #{operator}"
64
- end
65
-
66
- unless operator[0] == ?$
67
- raise ArgumentError, "Operator must begin with $: #{operator}"
36
+ def add_field_expression(field, value)
37
+ unless field.is_a?(String)
38
+ raise ArgumentError, "Field must be a string: #{field}"
68
39
  end
69
40
 
70
- if %w($and $nor $or).include?(operator)
71
- return add_logical_operator_expression(operator, op_expr)
41
+ if field[0] == ?$
42
+ raise ArgumentError, "Field cannot be an operator (i.e. begin with $): #{field}"
72
43
  end
73
44
 
74
- # For other operators, if the operator already exists in the
75
- # query, add the new condition with $and, otherwise add the
76
- # new condition to the top level.
77
- if selector[operator]
78
- add_logical_operator_expression('$and', [{operator => op_expr}])
45
+ if selector[field]
46
+ # We already have a restriction by the field we are trying
47
+ # to restrict, combine the restrictions.
48
+ if value.is_a?(Hash) && selector[field].is_a?(Hash) &&
49
+ value.keys.all? { |key|
50
+ key_s = key.to_s
51
+ key_s[0] == ?$ && !selector[field].key?(key_s)
52
+ }
53
+ then
54
+ # Multiple operators can be combined on the same field by
55
+ # adding them to the existing hash.
56
+ new_value = selector[field].merge(value)
57
+ selector.store(field, new_value)
58
+ else
59
+ add_operator_expression('$and', [{field => value}])
60
+ end
79
61
  else
80
- selector.store(operator, op_expr)
62
+ selector.store(field, value)
81
63
  end
82
64
 
83
65
  self
@@ -87,41 +69,40 @@ module Mongoid
87
69
  #
88
70
  # This method only handles logical operators ($and, $nor and $or).
89
71
  # It raises ArgumentError if called with another operator. Note that
90
- # in MongoDB, $not is a field-level operator and not a query-level one
91
- # $not is not handled by this method as a result.
72
+ # in MQL, $not is a field-level operator and not a query-level one,
73
+ # and therefore $not is not handled by this method.
92
74
  #
93
75
  # This method takes the operator and the operator value expression
94
76
  # separately for callers' convenience. It can be considered to
95
77
  # handle storing the hash `{operator => op_expr}`.
96
78
  #
97
- # If the selector already has the specified operator in it (on the
98
- # top level), the new condition given in op_expr is added to the
99
- # existing conditions for the specified operator. This is
100
- # straightforward for $and; for other logical operators, the behavior
101
- # of this method is to add the new conditions to the existing operator.
79
+ # If the selector consists of a single condition which is the specified
80
+ # operator (on the top level), the new condition given in op_expr is
81
+ # added to the existing conditions for the specified operator.
102
82
  # For example, if the selector is currently:
103
83
  #
104
- # {'foo' => 'bar', '$or' => [{'hello' => 'world'}]}
84
+ # {'$or' => [{'hello' => 'world'}]}
105
85
  #
106
- # ... and operator is '$or' and op_expr is `{'test' => 123'}`,
86
+ # ... and operator is '$or' and op_expr is `[{'test' => 123'}]`,
107
87
  # the resulting selector will be:
108
88
  #
109
- # {'foo' => 'bar', '$or' => [{'hello' => 'world'}, {'test' => 123}]}
89
+ # {'$or' => [{'hello' => 'world'}, {'test' => 123}]}
110
90
  #
111
- # This does not implement an OR between the existing selector and the
112
- # new operator expression - handling this is the job of upstream
113
- # methods. This method simply stores op_expr into the selector on the
114
- # assumption that the existing selector is the correct left hand side
115
- # of the operation already.
91
+ # This method always adds the new conditions as additional requirements;
92
+ # in other words, it does not implement the ActiveRecord or/nor behavior
93
+ # where the receiver becomes one of the operands. It is expected that
94
+ # code upstream of this method implements such behavior.
116
95
  #
117
96
  # This method does not simplify values (i.e. if the selector is
118
97
  # currently empty and operator is $and, op_expr is written to the
119
98
  # selector with $and even if the $and can in principle be elided).
99
+ # Such simplification is also expected to have already been performed
100
+ # by the upstream code.
120
101
  #
121
102
  # This method mutates the receiver.
122
103
  #
123
104
  # @param [ String ] operator The operator to add.
124
- # @param [ Hash ] op_expr Operator value to add.
105
+ # @param [ Array<Hash> ] op_expr Operator value to add.
125
106
  #
126
107
  # @return [ Storable ] self.
127
108
  def add_logical_operator_expression(operator, op_expr)
@@ -145,56 +126,80 @@ module Mongoid
145
126
  # with whatever other conditions exist.
146
127
  selector.store(operator, op_expr)
147
128
  else
148
- # Other operators need to operate explicitly on the previous
149
- # conditions and the new condition.
150
- new_value = [selector.to_hash.dup] + op_expr
151
- selector.replace(operator => new_value)
129
+ # Other operators need to be added separately
130
+ if selector[operator]
131
+ add_logical_operator_expression('$and', [operator => op_expr])
132
+ else
133
+ selector.store(operator, op_expr)
134
+ end
152
135
  end
153
136
 
154
137
  self
155
138
  end
156
139
 
157
- # Adds a field expression to the query.
140
+ # Adds an operator expression to the selector.
158
141
  #
159
- # field must be a field name, and it must be a string. The upstream
160
- # code must have converted other field/key types to the simple string
161
- # form by the time this method is invoked.
142
+ # This method takes the operator and the operator value expression
143
+ # separately for callers' convenience. It can be considered to
144
+ # handle storing the hash `{operator => op_expr}`.
162
145
  #
163
- # This method performs no processing on the provided field value.
146
+ # The operator value can be of any type.
164
147
  #
165
- # Mutates the receiver.
148
+ # If the selector already has the specified operator in it (on the
149
+ # top level), the new condition given in op_expr is added to the
150
+ # existing conditions for the specified operator. This is
151
+ # straightforward for $and; for other logical operators, the behavior
152
+ # of this method is to add the new conditions to the existing operator.
153
+ # For example, if the selector is currently:
166
154
  #
167
- # @param [ String ] field The field name.
168
- # @param [ Object ] value The field value.
155
+ # {'foo' => 'bar', '$or' => [{'hello' => 'world'}]}
156
+ #
157
+ # ... and operator is '$or' and op_expr is `{'test' => 123'}`,
158
+ # the resulting selector will be:
159
+ #
160
+ # {'foo' => 'bar', '$or' => [{'hello' => 'world'}, {'test' => 123}]}
161
+ #
162
+ # This does not implement an OR between the existing selector and the
163
+ # new operator expression - handling this is the job of upstream
164
+ # methods. This method simply stores op_expr into the selector on the
165
+ # assumption that the existing selector is the correct left hand side
166
+ # of the operation already.
167
+ #
168
+ # For non-logical query-level operators like $where and $text, if
169
+ # there already is a top-level operator with the same name, the
170
+ # op_expr is added to the selector via a top-level $and operator,
171
+ # thus producing a selector having both operator values.
172
+ #
173
+ # This method does not simplify values (i.e. if the selector is
174
+ # currently empty and operator is $and, op_expr is written to the
175
+ # selector with $and even if the $and can in principle be elided).
176
+ #
177
+ # This method mutates the receiver.
178
+ #
179
+ # @param [ String ] operator The operator to add.
180
+ # @param [ Object ] op_expr Operator value to add.
169
181
  #
170
182
  # @return [ Storable ] self.
171
- def add_field_expression(field, value)
172
- unless field.is_a?(String)
173
- raise ArgumentError, "Field must be a string: #{field}"
183
+ def add_operator_expression(operator, op_expr)
184
+ unless operator.is_a?(String)
185
+ raise ArgumentError, "Operator must be a string: #{operator}"
174
186
  end
175
187
 
176
- if field[0] == ?$
177
- raise ArgumentError, "Field cannot be an operator (i.e. begin with $): #{field}"
188
+ unless operator[0] == ?$
189
+ raise ArgumentError, "Operator must begin with $: #{operator}"
178
190
  end
179
191
 
180
- if selector[field]
181
- # We already have a restriction by the field we are trying
182
- # to restrict, combine the restrictions.
183
- if value.is_a?(Hash) && selector[field].is_a?(Hash) &&
184
- value.keys.all? { |key|
185
- key_s = key.to_s
186
- key_s[0] == ?$ && !selector[field].key?(key_s)
187
- }
188
- then
189
- # Multiple operators can be combined on the same field by
190
- # adding them to the existing hash.
191
- new_value = selector[field].merge(value)
192
- selector.store(field, new_value)
193
- else
194
- add_operator_expression('$and', [{field => value}])
195
- end
192
+ if %w($and $nor $or).include?(operator)
193
+ return add_logical_operator_expression(operator, op_expr)
194
+ end
195
+
196
+ # For other operators, if the operator already exists in the
197
+ # query, add the new condition with $and, otherwise add the
198
+ # new condition to the top level.
199
+ if selector[operator]
200
+ add_logical_operator_expression('$and', [{operator => op_expr}])
196
201
  else
197
- selector.store(field, value)
202
+ selector.store(operator, op_expr)
198
203
  end
199
204
 
200
205
  self