diff --git a/lib/mongo/collection.rb b/lib/mongo/collection.rb index 7f1839b..09dbafc 100644 --- a/lib/mongo/collection.rb +++ b/lib/mongo/collection.rb @@ -339,9 +339,9 @@ module Mongo # Performs a group query, similar to the 'SQL GROUP BY' operation. # Returns an array of grouped items. # - # :keys :: an array of fields to group by - # :condition :: specification of rows to be considered (as a 'find' - # query specification) + # :key :: either 1) an array of fields to group by, 2) a javascript function to generate + # the key object, or 3) nil. + # :condition :: an optional document specifying a query to limit the documents over which group is run. # :initial :: initial value of the aggregation counter object # :reduce :: aggregation function as a JavaScript string # :finalize :: optional. a JavaScript function that receives and modifies @@ -350,26 +350,34 @@ module Mongo # :command :: if true, run the group as a command instead of in an # eval - it is likely that this option will eventually be # deprecated and all groups will be run as commands - def group(keys, condition, initial, reduce, command=false, finalize=nil) + def group(key, condition, initial, reduce, command=false, finalize=nil) if command - hash = {} - keys.each do |k| - hash[k] = 1 - end - reduce = Code.new(reduce) unless reduce.is_a?(Code) + reduce = Code.new(reduce) unless reduce.is_a?(Code) group_command = { "group" => { "ns" => @name, "$reduce" => reduce, - "key" => hash, "cond" => condition, "initial" => initial } } + unless key.nil? + if key.is_a? Array + key_type = "key" + key_value = {} + key.each { |k| key_value[k] = 1 } + else + key_type = "$keyf" + key_value = key.is_a?(Code) ? key : Code.new(key) + end + + group_command["group"][key_type] = key_value + end + # only add finalize if specified if finalize finalize = Code.new(finalize) unless finalize.is_a?(Code) @@ -383,22 +391,28 @@ module Mongo else raise OperationFailure, "group command failed: #{result['errmsg']}" end - end - raise OperationFailure, ":finalize can be specified only when " + - "group is run as a command (set command param to true)" if finalize - - case reduce - when Code - scope = reduce.scope else - scope = {} - end - scope.merge!({ - "ns" => @name, - "keys" => keys, - "condition" => condition, - "initial" => initial }) + + warn "Collection#group must now be run as a command; you can do this by passing 'true' as the command argument." + + raise OperationFailure, ":finalize can be specified only when " + + "group is run as a command (set command param to true)" if finalize + + raise OperationFailure, "key must be an array of fields to group by. If you want to pass a key function, + run group as a command by passing 'true' as the command argument." unless key.is_a? Array || key.nil? + + case reduce + when Code + scope = reduce.scope + else + scope = {} + end + scope.merge!({ + "ns" => @name, + "keys" => key, + "condition" => condition, + "initial" => initial }) group_function = < 1) - @@test.save("b" => 1) - - reduce_function = "function (obj, prev) { prev.count += inc_value; }" - - assert_equal 2, @@test.group([], {}, {"count" => 0}, - Code.new(reduce_function, - {"inc_value" => 1}))[0]["count"] - -# TODO enable these tests when SERVER-262 is fixed - -# assert_equal 2, @@test.group([], {}, {"count" => 0}, -# Code.new(reduce_function, -# {"inc_value" => 1}), true)[0]["count"] - - assert_equal 4, @@test.group([], {}, {"count" => 0}, - Code.new(reduce_function, - {"inc_value" => 2}))[0]["count"] -# assert_equal 4, @@test.group([], {}, {"count" => 0}, -# Code.new(reduce_function, -# {"inc_value" => 2}), true)[0]["count"] - - assert_equal 1, @@test.group([], {}, {"count" => 0}, - Code.new(reduce_function, - {"inc_value" => 0.5}))[0]["count"] -# assert_equal 1, @@test.group([], {}, {"count" => 0}, -# Code.new(reduce_function, -# {"inc_value" => 0.5}), true)[0]["count"] - - # test finalize - #assert_equal( 3, - # @@test.group( - # [], {}, {"count" => 0}, - # Code.new(reduce_function,{"inc_value" => 2}), true, - # Code.new("function (o) { o.final_count = o.count - 1; }") - # )[0]["final_count"] - #) - + context "Grouping" do + setup do + @@test.remove + @@test.save("a" => 1) + @@test.save("b" => 1) + @initial = {"count" => 0} + @reduce_function = "function (obj, prev) { prev.count += inc_value; }" end + should "group results using eval form" do + assert_equal 1, @@test.group([], {}, @initial, Code.new(@reduce_function, {"inc_value" => 0.5}))[0]["count"] + assert_equal 2, @@test.group([], {}, @initial, Code.new(@reduce_function, {"inc_value" => 1}))[0]["count"] + assert_equal 4, @@test.group([], {}, @initial, Code.new(@reduce_function, {"inc_value" => 2}))[0]["count"] + end + + should "group results using command form" do + assert_equal 1, @@test.group([], {}, @initial, Code.new(@reduce_function, {"inc_value" => 0.5}), true)[0]["count"] + assert_equal 2, @@test.group([], {}, @initial, Code.new(@reduce_function, {"inc_value" => 1}), true)[0]["count"] + assert_equal 4, @@test.group([], {}, @initial, Code.new(@reduce_function, {"inc_value" => 2}), true)[0]["count"] + end + + should "finalize grouped results" do + @finalize = "function(doc) {doc.f = doc.count + 200; }" + assert_equal 202, @@test.group([], {}, @initial, Code.new(@reduce_function, {"inc_value" => 1}), true, @finalize)[0]["f"] + end + end + + context "Grouping with a key function" do + setup do + @@test.remove + @@test.save("a" => 1) + @@test.save("a" => 2) + @@test.save("a" => 3) + @@test.save("a" => 4) + @@test.save("a" => 5) + @initial = {"count" => 0} + @keyf = "function (doc) { if(doc.a % 2 == 0) { return {even: true}; } else {return {odd: true}} };" + @reduce = "function (obj, prev) { prev.count += 1; }" + end + + should "group results" do + results = @@test.group(@keyf, {}, @initial, @reduce, true).sort {|a, b| a['count'] <=> b['count']} + assert results[0]['even'] && results[0]['count'] == 2.0 + assert results[1]['odd'] && results[1]['count'] == 3.0 + end + + should "raise an error if trying to use keyf as a group eval" do + assert_raise OperationFailure do + @@test.group(@keyf, {}, @initial, @reduce) + end + end + end + context "A collection with two records" do setup do @collection = @@db.collection('test-collection')