Skip to content

Commit

Permalink
add avg operator and integration test
Browse files Browse the repository at this point in the history
Signed-off-by: sayedppqq <sayedmmnn0@gmail.com>
  • Loading branch information
sayedppqq committed Nov 1, 2024
1 parent e090e53 commit e6db2e9
Show file tree
Hide file tree
Showing 12 changed files with 409 additions and 1 deletion.
8 changes: 8 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions .idea/FerretDB.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 40 additions & 0 deletions integration/aggregate_documents_compat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,46 @@ func TestAggregateCompatGroup(t *testing.T) {
},
resultType: emptyResult,
},
"IDAvg": {
pipeline: bson.A{
bson.D{{"$sort", bson.D{{"_id", 1}}}},
bson.D{{"$group", bson.D{{"_id", bson.D{{"$avg", "$v"}}}}}},
bson.D{{"$sort", bson.D{{"_id", 1}}}},
},
},
"IDFieldAvg": {
pipeline: bson.A{
bson.D{{"$sort", bson.D{{"_id", 1}}}},
bson.D{{"$group", bson.D{{"_id", bson.D{{"avg", bson.D{{"$avg", "$v"}}}}}}}},
bson.D{{"$sort", bson.D{{"_id", 1}}}},
},
},
"IDNestedFieldAvg": {
pipeline: bson.A{
bson.D{{"$sort", bson.D{{"_id", 1}}}},
bson.D{{"$group", bson.D{{"_id", bson.D{{"nested", bson.D{{"avg", bson.D{{"$avg", "$v"}}}}}}}}}},
bson.D{{"$sort", bson.D{{"_id", 1}}}},
},
},
"IDAvgNonExistentField": {
pipeline: bson.A{
bson.D{{"$sort", bson.D{{"_id", 1}}}},
bson.D{{"$group", bson.D{{"_id", bson.D{{"$avg", "$non-existent"}}}}}},
bson.D{{"$sort", bson.D{{"_id", 1}}}},
},
},
"IDAvgInvalid": {
pipeline: bson.A{
bson.D{{"$group", bson.D{{"_id", bson.D{{"$avg", "$"}}}}}},
},
resultType: emptyResult,
},
"IDAvgRecursiveInvalid": {
pipeline: bson.A{
bson.D{{"$group", bson.D{{"_id", bson.D{{"$avg", bson.D{{"$avg", "$"}}}}}}}},
},
resultType: emptyResult,
},
}

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 @@ -587,6 +587,42 @@ func TestAggregateProjectErrors(t *testing.T) {
Message: "Invalid $project :: caused by :: '$' starts with an invalid character for a user variable name",
},
},
"AvgEmptyExpression": {
pipeline: bson.A{
bson.D{{"$project", bson.D{
{"avg", bson.D{{"$avg", "$"}}},
}}},
},
err: &mongo.CommandError{
Code: 16872,
Name: "Location16872",
Message: "Invalid $project :: caused by :: '$' by itself is not a valid FieldPath",
},
},
"AvgEmptyVariable": {
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",
},
},
"AvgDollarVariable": {
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",
},
},
} {
t.Run(name, func(t *testing.T) {
if tc.skip != "" {
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 @@ -349,6 +349,13 @@ func TestQueryProjectionPositionalOperatorCompat(t *testing.T) {
},
skip: "https://github.com/FerretDB/FerretDB/issues/835",
},
"AvgOperatorValue": {
filter: bson.D{},
projection: bson.D{
{"avg", bson.D{{"$avg", "$v"}}},
},
skip: "https://github.com/FerretDB/FerretDB/issues/835",
},
}

testQueryCompatWithProviders(t, providers, testCases)
Expand Down
34 changes: 34 additions & 0 deletions internal/handler/common/aggregations/number.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,37 @@ func SumNumbers(vs ...any) any {

return integer
}

// AvgNumbers accumulate numbers and returns the average result.
// It will calculate sum value and return by dividing according to its type
// For empty `vs`, it returns int32(0).
// This should only be used for aggregation, aggregation does not return
// error on overflow.
func AvgNumbers(vs ...any) any {
if len(vs) == 0 {
return int32(0)
}

Check warning on line 81 in internal/handler/common/aggregations/number.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/common/aggregations/number.go#L78-L81

Added lines #L78 - L81 were not covered by tests

numCount := float64(0)
for _, v := range vs {
switch v.(type) {
case float64, int32, int64:
numCount++
default:

Check warning on line 88 in internal/handler/common/aggregations/number.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/common/aggregations/number.go#L83-L88

Added lines #L83 - L88 were not covered by tests
// ignore non-number
}
}

sum := SumNumbers(vs...)
switch v := sum.(type) {
case int32:
return float64(v) / numCount
case int64:
return float64(v) / numCount
case float64:
return v / numCount
default:

Check warning on line 101 in internal/handler/common/aggregations/number.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/common/aggregations/number.go#L93-L101

Added lines #L93 - L101 were not covered by tests
// SumNumbers should only return an int64 or float64
}
return 0

Check warning on line 104 in internal/handler/common/aggregations/number.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/common/aggregations/number.go#L104

Added line #L104 was not covered by tests
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ func NewAccumulator(stage, key string, value any) (Accumulator, error) {
// Accumulators maps all aggregation accumulators.
var Accumulators = map[string]newAccumulatorFunc{
// sorted alphabetically
"$avg": newAvg,
"$count": newCount,
"$sum": newSum,
// please keep sorted alphabetically
Expand Down
126 changes: 126 additions & 0 deletions internal/handler/common/aggregations/operators/accumulators/avg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package accumulators

import (
"errors"

"github.com/FerretDB/FerretDB/internal/handler/common/aggregations"
"github.com/FerretDB/FerretDB/internal/handler/common/aggregations/operators"
"github.com/FerretDB/FerretDB/internal/handler/handlererrors"
"github.com/FerretDB/FerretDB/internal/types"
"github.com/FerretDB/FerretDB/internal/util/iterator"
"github.com/FerretDB/FerretDB/internal/util/lazyerrors"
)

// avg represents $avg aggregation operator.
type avg struct {
expression *aggregations.Expression
operator operators.Operator
number any
}

// newAvg creates a new $avg aggregation operator.
func newAvg(args ...any) (Accumulator, error) {
accumulator := new(avg)

if len(args) != 1 {
return nil, handlererrors.NewCommandErrorMsgWithArgument(
handlererrors.ErrStageGroupUnaryOperator,
"The $avg accumulator is a unary operator",
"$avg (accumulator)",
)
}

Check warning on line 31 in internal/handler/common/aggregations/operators/accumulators/avg.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/common/aggregations/operators/accumulators/avg.go#L22-L31

Added lines #L22 - L31 were not covered by tests

for _, arg := range args {
switch arg := arg.(type) {
case *types.Document:
if !operators.IsOperator(arg) {
accumulator.number = int32(0)
break

Check warning on line 38 in internal/handler/common/aggregations/operators/accumulators/avg.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/common/aggregations/operators/accumulators/avg.go#L33-L38

Added lines #L33 - L38 were not covered by tests
}

op, err := operators.NewOperator(arg)
if err != nil {
var opErr operators.OperatorError
if !errors.As(err, &opErr) {
return nil, lazyerrors.Error(err)
}

Check warning on line 46 in internal/handler/common/aggregations/operators/accumulators/avg.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/common/aggregations/operators/accumulators/avg.go#L41-L46

Added lines #L41 - L46 were not covered by tests

return nil, opErr

Check warning on line 48 in internal/handler/common/aggregations/operators/accumulators/avg.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/common/aggregations/operators/accumulators/avg.go#L48

Added line #L48 was not covered by tests
}

accumulator.operator = op
case float64:
accumulator.number = arg
case string:
var err error
if accumulator.expression, err = aggregations.NewExpression(arg, nil); err != nil {
// $avg returns 0 on non-existent field.
accumulator.number = int32(0)
}
case int32, int64:
accumulator.number = arg
default:
accumulator.number = int32(0)

Check warning on line 63 in internal/handler/common/aggregations/operators/accumulators/avg.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/common/aggregations/operators/accumulators/avg.go#L51-L63

Added lines #L51 - L63 were not covered by tests
// $avg returns 0 on non-numeric field
}
}

return accumulator, nil

Check warning on line 68 in internal/handler/common/aggregations/operators/accumulators/avg.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/common/aggregations/operators/accumulators/avg.go#L68

Added line #L68 was not covered by tests
}

// Accumulate implements Accumulator interface.
func (s *avg) Accumulate(iter types.DocumentsIterator) (any, error) {
var numbers []any

for {
_, doc, err := iter.Next()

if errors.Is(err, iterator.ErrIteratorDone) {
break

Check warning on line 79 in internal/handler/common/aggregations/operators/accumulators/avg.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/common/aggregations/operators/accumulators/avg.go#L72-L79

Added lines #L72 - L79 were not covered by tests
}

if err != nil {
return nil, lazyerrors.Error(err)
}

Check warning on line 84 in internal/handler/common/aggregations/operators/accumulators/avg.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/common/aggregations/operators/accumulators/avg.go#L82-L84

Added lines #L82 - L84 were not covered by tests

switch {
case s.operator != nil:
v, err := s.operator.Process(doc)
if err != nil {
return nil, err
}

Check warning on line 91 in internal/handler/common/aggregations/operators/accumulators/avg.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/common/aggregations/operators/accumulators/avg.go#L86-L91

Added lines #L86 - L91 were not covered by tests

numbers = append(numbers, v)

continue

Check warning on line 95 in internal/handler/common/aggregations/operators/accumulators/avg.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/common/aggregations/operators/accumulators/avg.go#L93-L95

Added lines #L93 - L95 were not covered by tests

case s.expression != nil:
value, err := s.expression.Evaluate(doc)

// avg fields that exist
if err == nil {
numbers = append(numbers, value)
}

Check warning on line 103 in internal/handler/common/aggregations/operators/accumulators/avg.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/common/aggregations/operators/accumulators/avg.go#L97-L103

Added lines #L97 - L103 were not covered by tests

continue

Check warning on line 105 in internal/handler/common/aggregations/operators/accumulators/avg.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/common/aggregations/operators/accumulators/avg.go#L105

Added line #L105 was not covered by tests
}

switch number := s.number.(type) {
case float64, int32, int64:
// For number types, the result is equivalent of iterator len*number,
// with conversion handled upon overflow of int32 and int64.
// For example, { $avg: 1 } is equivalent of { $count: { } }.
numbers = append(numbers, number)
default:
// $avg returns 0 on non-existent and non-numeric field.
return int32(0), nil

Check warning on line 116 in internal/handler/common/aggregations/operators/accumulators/avg.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/common/aggregations/operators/accumulators/avg.go#L108-L116

Added lines #L108 - L116 were not covered by tests
}
}

return aggregations.AvgNumbers(numbers...), nil

Check warning on line 120 in internal/handler/common/aggregations/operators/accumulators/avg.go

View check run for this annotation

Codecov / codecov/patch

internal/handler/common/aggregations/operators/accumulators/avg.go#L120

Added line #L120 was not covered by tests
}

// check interfaces
var (
_ Accumulator = (*avg)(nil)
)
Loading

0 comments on commit e6db2e9

Please sign in to comment.