Skip to content

Commit

Permalink
Implement $sum aggregation standard operator (#3063)
Browse files Browse the repository at this point in the history
Closes #2680.
  • Loading branch information
chilagrow authored Jul 19, 2023
1 parent 9ac03af commit 4d534d6
Show file tree
Hide file tree
Showing 9 changed files with 582 additions and 101 deletions.
195 changes: 195 additions & 0 deletions integration/aggregate_documents_compat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,22 @@ func TestAggregateCompatGroup(t *testing.T) {
}}}},
skip: "https://github.com/FerretDB/FerretDB/issues/2679",
},
"IDSum": {
pipeline: bson.A{
bson.D{{"$sort", bson.D{{"_id", 1}}}},
bson.D{{"$group", bson.D{{"_id", bson.D{{"$sum", "$v"}}}}}},
bson.D{{"$sort", bson.D{{"_id", 1}}}},
},
skip: "https://github.com/FerretDB/FerretDB/issues/2694",
},
"IDSumNonExistentField": {
pipeline: bson.A{
bson.D{{"$sort", bson.D{{"_id", 1}}}},
bson.D{{"$group", bson.D{{"_id", bson.D{{"$sum", "$non-existent"}}}}}},
bson.D{{"$sort", bson.D{{"_id", 1}}}},
},
skip: "https://github.com/FerretDB/FerretDB/issues/2694",
},
}

testAggregateStagesCompat(t, testCases)
Expand Down Expand Up @@ -1130,6 +1146,12 @@ func TestAggregateCompatMatch(t *testing.T) {
pipeline: bson.A{bson.D{{"$match", 1}}},
resultType: emptyResult,
},
"SumValue": {
pipeline: bson.A{
bson.D{{"$match", bson.D{{"$expr", bson.D{{"$sum", "$v"}}}}}},
},
skip: "https://github.com/FerretDB/FerretDB/issues/414",
},
}

testAggregateStagesCompatWithProviders(t, providers, testCases)
Expand Down Expand Up @@ -1756,6 +1778,165 @@ func TestAggregateCompatProject(t *testing.T) {
},
resultType: emptyResult,
},
"SumValue": {
pipeline: bson.A{
bson.D{{"$project", bson.D{
{"sum", bson.D{{"$sum", "$v"}}},
}}},
},
},
}

testAggregateStagesCompat(t, testCases)
}

func TestAggregateCompatProjectSum(t *testing.T) {
t.Parallel()

testCases := map[string]aggregateStagesCompatTestCase{
"Value": {
pipeline: bson.A{
bson.D{{"$project", bson.D{
{"sum", bson.D{{"$sum", "$v"}}},
}}},
},
},
"DotNotation": {
pipeline: bson.A{
bson.D{{"$project", bson.D{
{"sum", bson.D{{"$sum", "$v.foo"}}},
}}},
},
},
"ArrayDotNotation": {
pipeline: bson.A{
bson.D{{"$project", bson.D{
{"sum", bson.D{{"$sum", "$v.0.foo"}}},
}}},
},
},
"Int": {
pipeline: bson.A{
bson.D{{"$project", bson.D{
{"sum", bson.D{{"$sum", int32(2)}}},
}}},
},
},
"Long": {
pipeline: bson.A{
bson.D{{"$project", bson.D{
{"sum", bson.D{{"$sum", int64(3)}}},
}}},
},
},
"Double": {
pipeline: bson.A{
bson.D{{"$project", bson.D{
{"sum", bson.D{{"$sum", float64(4)}}},
}}},
},
},
"EmptyString": {
pipeline: bson.A{
bson.D{{"$project", bson.D{
{"sum", bson.D{{"$sum", ""}}},
}}},
},
},
"ArrayEmptyVariable": {
pipeline: bson.A{
bson.D{{"$project", bson.D{
{"sum", bson.D{{"$sum", bson.A{"$"}}}},
}}},
},
resultType: emptyResult,
},
"ArrayValue": {
pipeline: bson.A{
bson.D{{"$project", bson.D{
{"sum", bson.D{{"$sum", bson.A{"$v"}}}},
}}},
},
},
"ArrayTwoValues": {
pipeline: bson.A{
bson.D{{"$project", bson.D{
{"sum", bson.D{{"$sum", bson.A{"$v", "$v"}}}},
}}},
},
},
"ArrayValueInt": {
pipeline: bson.A{
bson.D{{"$project", bson.D{
{"sum", bson.D{{"$sum", bson.A{"$v", int32(1)}}}},
}}},
},
},
"ArrayIntLongDoubleStringBool": {
pipeline: bson.A{
bson.D{{"$project", bson.D{
{"sum", bson.D{{"$sum", bson.A{int32(2), int64(3), float64(4), "not-expression", true}}}},
}}},
},
},
"RecursiveValue": {
pipeline: bson.A{
bson.D{{"$project", bson.D{
{"sumsum", bson.D{{"$sum", bson.D{{"$sum", "$v"}}}}},
}}},
},
},
"RecursiveArrayValue": {
pipeline: bson.A{
bson.D{{"$project", bson.D{
{"sumsum", bson.D{{"$sum", bson.D{{"$sum", bson.A{"$v"}}}}}},
}}},
},
},
"ArrayValueRecursiveInt": {
pipeline: bson.A{
bson.D{{"$project", bson.D{
{"sum", bson.D{{"$sum", bson.A{"$v", bson.D{{"$sum", int32(2)}}}}}},
}}},
},
},
"ArrayValueAndRecursiveValue": {
pipeline: bson.A{
bson.D{{"$project", bson.D{
{"sum", bson.D{{"$sum", bson.A{"$v", bson.D{{"$sum", "$v"}}}}}},
}}},
},
},
"ArrayValueAndRecursiveArray": {
pipeline: bson.A{
bson.D{{"$project", bson.D{
{"sum", bson.D{{"$sum", bson.A{"$v", bson.D{{"$sum", bson.A{"$v"}}}}}}},
}}},
},
},
"Type": {
pipeline: bson.A{
bson.D{{"$project", bson.D{
{"sumtype", bson.D{{"$sum", bson.D{{"$type", "$v"}}}}},
}}},
},
},
"RecursiveEmptyVariable": {
pipeline: bson.A{
bson.D{{"$project", bson.D{
{"sum", bson.D{{"$sum", bson.D{{"$sum", "$$$"}}}}},
}}},
},
resultType: emptyResult,
},
"MultipleRecursiveEmptyVariable": {
pipeline: bson.A{
bson.D{{"$project", bson.D{
{"sum", bson.D{{"$sum", bson.D{{"$sum", bson.D{{"$sum", "$$$"}}}}}}},
}}},
},
resultType: emptyResult,
},
}

testAggregateStagesCompat(t, testCases)
Expand Down Expand Up @@ -1986,6 +2167,13 @@ func TestAggregateCompatAddFields(t *testing.T) {
bson.D{{"$addFields", bson.D{{"type", bson.D{{"$type", true}}}}}},
},
},
"SumValue": {
pipeline: bson.A{
bson.D{{"$addFields", bson.D{
{"sum", bson.D{{"$sum", "$v"}}},
}}},
},
},
}

testAggregateStagesCompat(t, testCases)
Expand Down Expand Up @@ -2088,6 +2276,13 @@ func TestAggregateCompatSet(t *testing.T) {
resultType: emptyResult,
skip: "https://github.com/FerretDB/FerretDB/issues/1413",
},
"SumValue": {
pipeline: bson.A{
bson.D{{"$set", bson.D{
{"sum", bson.D{{"$sum", "$v"}}},
}}},
},
},
}
testAggregateStagesCompat(t, testCases)
}
Expand Down
36 changes: 36 additions & 0 deletions integration/aggregate_documents_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,42 @@ func TestAggregateProjectErrors(t *testing.T) {
Message: "Invalid $project :: caused by :: Unrecognized expression '$non-existent'",
},
},
"SumEmptyExpression": {
pipeline: bson.A{
bson.D{{"$project", bson.D{
{"sum", bson.D{{"$sum", "$"}}},
}}},
},
err: &mongo.CommandError{
Code: 16872,
Name: "Location16872",
Message: "Invalid $project :: caused by :: '$' by itself is not a valid FieldPath",
},
},
"SumEmptyVariable": {
pipeline: bson.A{
bson.D{{"$project", bson.D{
{"sum", bson.D{{"$sum", "$$"}}},
}}},
},
err: &mongo.CommandError{
Code: 9,
Name: "FailedToParse",
Message: "Invalid $project :: caused by :: empty variable names are not allowed",
},
},
"SumDollarVariable": {
pipeline: bson.A{
bson.D{{"$project", bson.D{
{"sum", bson.D{{"$sum", "$$$"}}},
}}},
},
err: &mongo.CommandError{
Code: 9,
Name: "FailedToParse",
Message: "Invalid $project :: caused by :: '$' starts with an invalid character for a user variable name",
},
},
} {
name, tc := name, tc
t.Run(name, func(t *testing.T) {
Expand Down
7 changes: 7 additions & 0 deletions integration/query_projection_compat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,13 @@ func TestQueryProjectionPositionalOperatorCompat(t *testing.T) {
projection: bson.D{{"type", bson.D{{"$type", "$v"}}}},
skip: "https://github.com/FerretDB/FerretDB/issues/2679",
},
"SumOperatorValue": {
filter: bson.D{},
projection: bson.D{
{"sum", bson.D{{"$sum", "$v"}}},
},
skip: "https://github.com/FerretDB/FerretDB/issues/835",
},
}

testQueryCompatWithProviders(t, providers, testCases)
Expand Down
71 changes: 71 additions & 0 deletions internal/handlers/common/aggregations/number.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright 2021 FerretDB Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package aggregations

import (
"math"
"math/big"
)

// SumNumbers accumulate numbers and returns the result of summation.
// The result has the same type as the input, except when the result
// cannot be presented accurately. Then int32 is converted to int64,
// and int64 is converted to float64. It ignores non-number values.
// For empty `vs`, it returns int32(0).
// This should only be used for aggregation, aggregation does not return
// error on overflow.
func SumNumbers(vs ...any) any {
// use big.Int to accumulate values larger than math.MaxInt64.
intSum := big.NewInt(0)

// TODO: handle accumulation of doubles close to max precision.
// https://github.com/FerretDB/FerretDB/issues/2300
var floatSum float64

var hasFloat64, hasInt64 bool

for _, v := range vs {
switch v := v.(type) {
case float64:
hasFloat64 = true

floatSum = floatSum + v
case int32:
intSum.Add(intSum, big.NewInt(int64(v)))
case int64:
hasInt64 = true

intSum.Add(intSum, big.NewInt(v))
default:
// ignore non-number
}
}

if hasFloat64 || !intSum.IsInt64() {
// ignore accuracy because there is no rounding from int64.
intAsFloat, _ := new(big.Float).SetInt(intSum).Float64()

return intAsFloat + floatSum
}

integer := intSum.Int64()

if !hasInt64 && integer <= math.MaxInt32 && integer >= math.MinInt32 {
// convert to int32 if input has no int64 and can be represented in int32.
return int32(integer)
}

return integer
}
Loading

0 comments on commit 4d534d6

Please sign in to comment.