mongoid 7.1.0 → 7.1.1

Sign up to get free protection for your applications and to get access to all the features.
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