diff --git a/lib/bson/types/code.rb b/lib/bson/types/code.rb index 60d6a99..f67b5cb 100644 --- a/lib/bson/types/code.rb +++ b/lib/bson/types/code.rb @@ -47,5 +47,9 @@ module BSON "" end + def to_bson_code + self + end + end end diff --git a/lib/mongo/collection.rb b/lib/mongo/collection.rb index 98c02f1..e3fde8e 100644 --- a/lib/mongo/collection.rb +++ b/lib/mongo/collection.rb @@ -556,17 +556,28 @@ module Mongo # Perform a group aggregation. # - # @param [Array, String, BSON::Code, Nil] :key either 1) an array of fields to group by, - # 2) a javascript function to generate the key object, or 3) nil. - # @param [Hash] condition an optional document specifying a query to limit the documents over which group is run. - # @param [Hash] initial initial value of the aggregation counter object - # @param [String, BSON::Code] reduce aggregation function, in JavaScript - # @param [String, BSON::Code] finalize :: optional. a JavaScript function that receives and modifies - # each of the resultant grouped objects. Available only when group is run - # with command set to true. + # @param [Hash] opts the options for this group operation. The minimum required are :initial + # and :reduce. # - # @return [Array] the grouped items. - def group(key, condition, initial, reduce, finalize=nil) + # @option opts [Array, String, Symbol] :key (nil) Either the name of a field or a list of fields to group by (optional). + # @option opts [String, BSON::Code] :keyf (nil) A JavaScript function to be used to generate the grouping keys (optional). + # @option opts [String, BSON::Code] :cond ({}) A document specifying a query for filtering the documents over + # which the aggregation is run (optional). + # @option opts [Hash] :initial the initial value of the aggregation counter object (required). + # @option opts [String, BSON::Code] :reduce (nil) a JavaScript aggregation function (required). + # @option opts [String, BSON::Code] :finalize (nil) a JavaScript function that receives and modifies + # each of the resultant grouped objects. Available only when group is run with command + # set to true. + # + # @return [Array] the command response consisting of grouped items. + def group(key, condition={}, initial={}, reduce=nil, finalize=nil) + if key.is_a?(Hash) + return new_group(key) + else + warn "Collection#group no longer take a list of paramters. This usage is deprecated." + + "Check out the new API at http://api.mongodb.org/ruby/current/Mongo/Collection.html#group-instance_method" + end + reduce = BSON::Code.new(reduce) unless reduce.is_a?(BSON::Code) group_command = { @@ -578,6 +589,11 @@ module Mongo } } + if key.is_a?(Symbol) + raise MongoArgumentError, "Group takes either an array of fields to group by or a JavaScript function" + + "in the form of a String or BSON::Code." + end + unless key.nil? if key.is_a? Array key_type = "key" @@ -605,6 +621,48 @@ module Mongo end end + private + + def new_group(opts={}) + reduce = opts[:reduce] + finalize = opts[:finalize] + cond = opts.fetch(:cond, {}) + initial = opts[:initial] + + if !(reduce && initial) + raise MongoArgumentError, "Group requires at minimum values for initial and reduce." + end + + cmd = { + "group" => { + "ns" => @name, + "$reduce" => reduce.to_bson_code, + "cond" => cond, + "initial" => initial + } + } + + if finalize + cmd['group']['finalize'] = finalize.to_bson_code + end + + if key = opts[:key] + if key.is_a?(String) || key.is_a?(Symbol) + key = [key] + end + key_value = {} + key.each { |k| key_value[k] = 1 } + cmd["group"]["key"] = key_value + elsif keyf = opts[:keyf] + cmd["group"]["$keyf"] = keyf.to_bson_code + end + + result = @db.command(cmd) + result["retval"] + end + + public + # Return a list of distinct values for +key+ across all # documents in the collection. The key may use dot notation # to reach into an embedded object. diff --git a/lib/mongo/util/core_ext.rb b/lib/mongo/util/core_ext.rb index bc034e5..741126f 100644 --- a/lib/mongo/util/core_ext.rb +++ b/lib/mongo/util/core_ext.rb @@ -48,3 +48,13 @@ class Hash end end + +#:nodoc: +class String + + #:nodoc: + def to_bson_code + BSON::Code.new(self) + end + +end diff --git a/test/collection_test.rb b/test/collection_test.rb index 4ac5558..f137679 100644 --- a/test/collection_test.rb +++ b/test/collection_test.rb @@ -626,15 +626,25 @@ class TestCollection < Test::Unit::TestCase @reduce_function = "function (obj, prev) { prev.count += inc_value; }" end + should "fail if missing required options" do + assert_raise MongoArgumentError do + @@test.group(:initial => {}) + end + + assert_raise MongoArgumentError do + @@test.group(:reduce => "foo") + end + 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"] + assert_equal 1, @@test.group(:initial => @initial, :reduce => Code.new(@reduce_function, {"inc_value" => 0.5}))[0]["count"] + assert_equal 2, @@test.group(:initial => @initial, :reduce => Code.new(@reduce_function, {"inc_value" => 1}))[0]["count"] + assert_equal 4, @@test.group(:initial => @initial, :reduce => Code.new(@reduce_function, {"inc_value" => 2}))[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}), @finalize)[0]["f"] + assert_equal 202, @@test.group(:initial => @initial, :reduce => Code.new(@reduce_function, {"inc_value" => 1}), :finalize => @finalize)[0]["f"] end end @@ -650,7 +660,7 @@ class TestCollection < Test::Unit::TestCase end should "group" do - result = @@test.group([:a], {}, @initial, @reduce_function, nil) + result = @@test.group(:key => :a, :initial => @initial, :reduce => @reduce_function) assert result.all? { |r| r['count'] == 200 } end end @@ -669,10 +679,17 @@ class TestCollection < Test::Unit::TestCase end should "group results" do - results = @@test.group(@keyf, {}, @initial, @reduce).sort {|a, b| a['count'] <=> b['count']} + results = @@test.group(:keyf => @keyf, :initial => @initial, :reduce => @reduce).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 "group filtered results" do + results = @@test.group(:keyf => @keyf, :cond => {:a => {'$ne' => 2}}, + :initial => @initial, :reduce => @reduce).sort {|a, b| a['count'] <=> b['count']} + assert results[0]['even'] && results[0]['count'] == 1.0 + assert results[1]['odd'] && results[1]['count'] == 3.0 + end end context "A collection with two records" do