lib/mongoid/criteria/queryable/mergeable.rb in mongoid-7.1.1 vs lib/mongoid/criteria/queryable/mergeable.rb in mongoid-7.1.2

- old
+ new

@@ -161,11 +161,11 @@ next unless expr result_criteria = sel[operator] || [] if expr.is_a?(Selectable) expr = expr.selector end - normalized = _mongoid_normalize_expr(expr) + normalized = _mongoid_expand_keys(expr) sel.store(operator, result_criteria.push(normalized)) end end end @@ -188,13 +188,13 @@ # flatten if needed. clone.tap do |query| sel = query.selector _mongoid_flatten_arrays(criteria).each do |criterion| if criterion.is_a?(Selectable) - expr = _mongoid_normalize_expr(criterion.selector) + expr = _mongoid_expand_keys(criterion.selector) else - expr = criterion + expr = _mongoid_expand_keys(criterion) end if sel.empty? sel.store(operator, [expr]) elsif sel.keys == [operator] sel.store(operator, sel[operator] + [expr]) @@ -210,11 +210,11 @@ # Calling .flatten on an array which includes a Criteria instance # evaluates the criteria, which we do not want. Hence this method # explicitly only expands Array objects and Array subclasses. private def _mongoid_flatten_arrays(array) out = [] - pending = array + pending = array.dup until pending.empty? item = pending.shift if item.nil? # skip elsif item.is_a?(Array) @@ -224,14 +224,81 @@ end end out end - # @api private - private def _mongoid_normalize_expr(expr) - expr.inject({}) do |hash, (field, value)| - hash.merge!(field.__expr_part__(value.__expand_complex__)) + # Takes a criteria hash and expands Key objects into hashes containing + # MQL corresponding to said key objects. + # + # Ruby does not permit multiple symbol operators. For example, + # {:foo.gt => 1, :foo.gt => 2} is collapsed to {:foo.gt => 2} by the + # language. Therefore this method never has to deal with multiple + # identical operators. + # + # Similarly, this method should never need to expand a literal value + # and an operator at the same time. + # + # @param [ Hash ] Criteria including Key instances. + # + # @return [ Hash ] Expanded criteria. + private def _mongoid_expand_keys(expr) + unless expr.is_a?(Hash) + raise ArgumentError, 'Argument must be a Hash' end + + result = {} + expr.each do |field, value| + field.__expr_part__(value.__expand_complex__).each do |k, v| + if result[k] + if result[k].is_a?(Hash) + # Existing value is an operator. + # If new value is also an operator, ensure there are no + # conflicts and add + if v.is_a?(Hash) + # The new value is also an operator. + # If there are no conflicts, combine the hashes, otherwise + # add new conditions to top level with $and. + if (v.keys & result[k].keys).empty? + result[k].update(v) + else + raise NotImplementedError, 'Ruby does not allow same symbol operator with different values' + result['$and'] ||= [] + result['$and'] << {k => v} + end + else + # The new value is a simple value. + # If there isn't an $eq operator already in the query, + # transform the new value into an $eq operator and add it + # to the existing hash. Otherwise add the new condition + # with $and to the top level. + if result[k].key?('$eq') + raise NotImplementedError, 'Ruby does not allow same symbol operator with different values' + result['$and'] ||= [] + result['$and'] << {k => v} + else + result[k].update('$eq' => v) + end + end + else + # Existing value is a simple value. + # If we are adding an operator, and the operator is not $eq, + # convert existing value into $eq and add the new operator + # to the same hash. Otherwise add the new condition with $and + # to the top level. + if v.is_a?(Hash) && !v.key?('$eq') + result[k] = {'$eq' => result[k]}.update(v) + else + raise NotImplementedError, 'Ruby does not allow same symbol operator with different values' + result['$and'] ||= [] + result['$and'] << {k => v} + end + end + else + result[k] = v + end + end + end + result end # Adds the criterion to the existing selection. # # @api private