Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate scale param for dbStats and collStats correctly #2418

Merged
merged 35 commits into from
Apr 18, 2023
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c828017
wip
rumyantseva Apr 12, 2023
a76b6d0
wip
rumyantseva Apr 12, 2023
e9db5a5
Merge branch 'main' into issue-1346-stats
Apr 12, 2023
06df4cb
wip
rumyantseva Apr 12, 2023
092cb9d
Merge branch 'issue-1346-stats' of https://github.com/rumyantseva/Fer…
rumyantseva Apr 12, 2023
0939552
wip
rumyantseva Apr 12, 2023
17f3727
Merge branch 'main' into issue-1346-stats
rumyantseva Apr 12, 2023
a02f874
wip
rumyantseva Apr 12, 2023
065fc0f
wip
rumyantseva Apr 12, 2023
83a80e9
linting things
rumyantseva Apr 12, 2023
5ab050d
wip
rumyantseva Apr 12, 2023
324cb14
wip
rumyantseva Apr 12, 2023
124eb09
wip
rumyantseva Apr 12, 2023
eabaaa0
Merge branch 'main' into issue-1346-stats
Apr 12, 2023
7abc624
wip
rumyantseva Apr 13, 2023
2656559
Merge branch 'issue-1346-stats' of https://github.com/rumyantseva/Fer…
rumyantseva Apr 13, 2023
c0f4adc
wip
rumyantseva Apr 13, 2023
9dfb2b1
Merge branch 'main' into issue-1346-stats
Apr 13, 2023
50738d5
diff
rumyantseva Apr 13, 2023
91bf238
wip
rumyantseva Apr 13, 2023
dcc9efe
Merge branch 'main' into issue-1346-stats
Apr 13, 2023
a13133b
Merge branch 'main' into issue-1346-stats
mergify[bot] Apr 14, 2023
69bafab
Merge branch 'main' into issue-1346-stats
mergify[bot] Apr 14, 2023
aeb71ef
wip
rumyantseva Apr 14, 2023
d70a4e6
Merge branch 'main' into issue-1346-stats
Apr 14, 2023
318cd6e
Merge branch 'main' into issue-1346-stats
AlekSi Apr 17, 2023
a5c8c9d
Merge branch 'main' into issue-1346-stats
Apr 17, 2023
56e1dc5
wip
rumyantseva Apr 17, 2023
8a7adda
wip
rumyantseva Apr 17, 2023
f0b5a46
wip
rumyantseva Apr 17, 2023
c302ef8
wip
rumyantseva Apr 17, 2023
ff5708a
wip
rumyantseva Apr 17, 2023
8c5e55c
wip
rumyantseva Apr 17, 2023
0ac1cd5
Merge branch 'main' into issue-1346-stats
Apr 17, 2023
3abcf2e
Merge branch 'main' into issue-1346-stats
mergify[bot] Apr 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions integration/aggregate_documents_compat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -835,6 +835,13 @@ func TestAggregateCompatLimit(t *testing.T) {
bson.D{{"$limit", math.MaxInt64}},
},
},
"MinInt64": {
pipeline: bson.A{
bson.D{{"$sort", bson.D{{"_id", -1}}}},
bson.D{{"$limit", math.MinInt64}},
},
resultType: emptyResult,
},
"Negative": {
pipeline: bson.A{
bson.D{{"$sort", bson.D{{"_id", -1}}}},
Expand Down Expand Up @@ -1367,6 +1374,13 @@ func TestAggregateCompatSkip(t *testing.T) {
},
resultType: emptyResult,
},
"MinInt64": {
pipeline: bson.A{
bson.D{{"$sort", bson.D{{"_id", -1}}}},
bson.D{{"$skip", math.MinInt64}},
},
resultType: emptyResult,
},
"Int64Overflow": {
pipeline: bson.A{
bson.D{{"$sort", bson.D{{"_id", -1}}}},
Expand Down
11 changes: 11 additions & 0 deletions integration/aggregate_stats_compat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ func TestAggregateCompatCollStats(t *testing.T) {
"StorageStatsWithScale": {
collStats: bson.D{{"storageStats", bson.D{{"scale", 1000}}}},
},
"StorageStatsNegativeScale": {
collStats: bson.D{{"storageStats", bson.D{{"scale", -1000}}}},
resultType: emptyResult,
},
"StorageStatsFloatScale": {
collStats: bson.D{{"storageStats", bson.D{{"scale", 42.42}}}},
},
"StorageStatsInvalidScale": {
collStats: bson.D{{"storageStats", bson.D{{"scale", "invalid"}}}},
resultType: emptyResult,
},
"CountAndStorageStats": {
collStats: bson.D{{"count", bson.D{}}, {"storageStats", bson.D{}}},
},
Expand Down
178 changes: 178 additions & 0 deletions integration/commands_administration_compat_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// 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 integration

import (
"errors"
"math"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"

"github.com/FerretDB/FerretDB/integration/setup"
"github.com/FerretDB/FerretDB/integration/shareddata"
"github.com/FerretDB/FerretDB/internal/util/must"
)

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

s := setup.SetupCompatWithOpts(t, &setup.SetupCompatOpts{
Providers: []shareddata.Provider{shareddata.DocumentsDocuments},
AddNonExistentCollection: true,
})

ctx, targetCollection, compatCollection := s.Ctx, s.TargetCollections[0], s.CompatCollections[0]

for name, tc := range map[string]struct { //nolint:vet // for readability
scale any
resultType compatTestCaseResultType
altMessage string
}{
"scaleOne": {scale: int32(1)},
"scaleBig": {scale: int64(1000)},
"scaleMaxInt": {scale: math.MaxInt64},
"scaleZero": {scale: int32(0), resultType: emptyResult},
"scaleNegative": {scale: int32(-100), resultType: emptyResult},
"scaleFloat": {scale: 2.8},
"scaleFloatNegative": {scale: -2.8, resultType: emptyResult},
"scaleMinFloat": {scale: -math.MaxFloat64, resultType: emptyResult},
"scaleMaxFloat": {scale: math.MaxFloat64},
"scaleString": {
scale: "1",
resultType: emptyResult,
altMessage: `BSON field 'collStats.scale' is the wrong type 'string', expected types '[long, int, decimal, double]'`,
},
"scaleObject": {
scale: bson.D{{"a", 1}},
resultType: emptyResult,
altMessage: `BSON field 'collStats.scale' is the wrong type 'object', expected types '[long, int, decimal, double]'`,
},
"scaleNull": {scale: nil},
} {
name, tc := name, tc

t.Run(name, func(t *testing.T) {
t.Helper()

t.Parallel()

var targetRes bson.D
targetCommand := bson.D{{"collStats", targetCollection.Name()}, {"scale", tc.scale}}
targetErr := targetCollection.Database().RunCommand(ctx, targetCommand).Decode(&targetRes)

var compatRes bson.D
compatCommand := bson.D{{"collStats", compatCollection.Name()}, {"scale", tc.scale}}
compatErr := compatCollection.Database().RunCommand(ctx, compatCommand).Decode(&compatRes)

if tc.resultType == emptyResult {
require.Error(t, compatErr)

targetErr = UnsetRaw(t, targetErr)
compatErr = UnsetRaw(t, compatErr)

if tc.altMessage != "" {
var expectedErr mongo.CommandError
require.True(t, errors.As(compatErr, &expectedErr))
AssertEqualAltError(t, expectedErr, tc.altMessage, targetErr)
} else {
assert.Equal(t, compatErr, targetErr)
}

return
}

require.NoError(t, compatErr)
require.NoError(t, targetErr)

targetDoc := ConvertDocument(t, targetRes)
compatDoc := ConvertDocument(t, compatRes)

targetFactor := must.NotFail(targetDoc.Get("scaleFactor"))
compatFactor := must.NotFail(compatDoc.Get("scaleFactor"))

assert.Equal(t, compatFactor, targetFactor)
})
}
}

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

s := setup.SetupCompatWithOpts(t, &setup.SetupCompatOpts{
Providers: []shareddata.Provider{shareddata.DocumentsDocuments},
AddNonExistentCollection: true,
})

ctx, targetCollection, compatCollection := s.Ctx, s.TargetCollections[0], s.CompatCollections[0]

for name, tc := range map[string]struct { //nolint:vet // for readability
scale any
resultType compatTestCaseResultType
altMessage string
}{
"scaleOne": {scale: int32(1)},
"scaleBig": {scale: int64(1000)},
"scaleFloat": {scale: 2.8},
"scaleNull": {scale: nil},
} {
name, tc := name, tc

t.Run(name, func(t *testing.T) {
t.Helper()

t.Parallel()

var targetRes bson.D
targetCommand := bson.D{{"dbStats", int32(1)}, {"scale", tc.scale}}
targetErr := targetCollection.Database().RunCommand(ctx, targetCommand).Decode(&targetRes)

var compatRes bson.D
compatCommand := bson.D{{"dbStats", int32(1)}, {"scale", tc.scale}}
compatErr := compatCollection.Database().RunCommand(ctx, compatCommand).Decode(&compatRes)

if tc.resultType == emptyResult {
require.Error(t, compatErr)

targetErr = UnsetRaw(t, targetErr)
compatErr = UnsetRaw(t, compatErr)

if tc.altMessage != "" {
var expectedErr mongo.CommandError
require.True(t, errors.As(compatErr, &expectedErr))
AssertEqualAltError(t, expectedErr, tc.altMessage, targetErr)
} else {
assert.Equal(t, compatErr, targetErr)
}

return
}

require.NoError(t, compatErr)
require.NoError(t, targetErr)

targetDoc := ConvertDocument(t, targetRes)
compatDoc := ConvertDocument(t, compatRes)

targetFactor := must.NotFail(targetDoc.Get("scaleFactor"))
compatFactor := must.NotFail(compatDoc.Get("scaleFactor"))

assert.Equal(t, compatFactor, targetFactor)
})
}
}
6 changes: 6 additions & 0 deletions integration/count_compat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package integration

import (
"errors"
"math"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -210,6 +211,11 @@ func TestCountCompat(t *testing.T) {
optSkip: -1.888,
resultType: emptyResult,
},
"SkipMinFloat": {
filter: bson.D{},
optSkip: -math.MaxFloat64,
resultType: emptyResult,
},
"SkipNull": {
filter: bson.D{},
},
Expand Down
5 changes: 5 additions & 0 deletions integration/query_compat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,11 @@ func TestQueryCompatSkip(t *testing.T) {
optSkip: pointer.ToInt64(math.MaxInt64),
resultType: emptyResult,
},
"MinInt64": {
filter: bson.D{},
optSkip: pointer.ToInt64(math.MinInt64),
resultType: emptyResult,
},
}

testQueryCompat(t, testCases)
Expand Down
14 changes: 7 additions & 7 deletions internal/handlers/common/aggregations/collstats.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ func newCollStats(stage *types.Document) (Stage, error) {
if fields.Has("storageStats") {
cs.storageStats = new(storageStats)

// TODO Add proper support for scale: https://github.com/FerretDB/FerretDB/issues/1346
cs.storageStats.scale, err = common.GetOptionalPositiveNumber(
must.NotFail(fields.Get("storageStats")).(*types.Document),
"scale",
)
if err != nil || cs.storageStats.scale == 0 {
cs.storageStats.scale = 1
storageStatsFields := must.NotFail(fields.Get("storageStats")).(*types.Document)

var s any
if s, err = storageStatsFields.Get("scale"); err == nil {
if cs.storageStats.scale, err = common.GetScaleParam("$collStats.storageStats", s); err != nil {
return nil, err
}
}
}

Expand Down
35 changes: 17 additions & 18 deletions internal/handlers/common/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ var (
errNotBinaryMask = fmt.Errorf("not a binary mask")
errUnexpectedLeftOpType = fmt.Errorf("unexpected left operand type")
errUnexpectedRightOpType = fmt.Errorf("unexpected right operand type")
errLongExceeded = fmt.Errorf("long exceeded")
errLongExceededPositive = fmt.Errorf("long exceeded - positive value")
errLongExceededNegative = fmt.Errorf("long exceeded - negative value")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added/updated comments and checked rendering.

Before submitting a pull request, please make sure that:
[…]
2. Comments are added or updated for all new or changed code.
Please add missing comments for all (both exported and unexported)
new and changed top-level declarations (functions, types, etc).

Both points were ignored.

Updated possible error results are also not documented.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AlekSi could you please leave more specific feedback? What exactly needs to be documented here?
Sometimes when I leave comments (e.g. for structure fields) you say that they are obvious and not needed. What needs to be documented here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fields are not top-level declarations. Global variables are.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I document what each error in this list means?
Or should I document each function/method where these errors could be returned?
Or both? Or something else?

errIntExceeded = fmt.Errorf("int exceeded")
errInfinity = fmt.Errorf("infinity")
)
Expand All @@ -132,16 +133,14 @@ func GetWholeNumberParam(value any) (int64, error) {
switch value := value.(type) {
// TODO: add string support https://github.com/FerretDB/FerretDB/issues/1089
case float64:
if math.IsInf(value, 1) {
switch {
case math.IsInf(value, 1):
return 0, errInfinity
}

if value > float64(math.MaxInt64) ||
value < float64(math.MinInt64) {
return 0, errLongExceeded
}

if value != math.Trunc(value) {
case value > float64(math.MaxInt64):
return 0, errLongExceededPositive
case value < float64(math.MinInt64):
return 0, errLongExceededNegative
case value != math.Trunc(value):
return 0, errNotWholeNumber
}

Expand Down Expand Up @@ -174,7 +173,7 @@ func GetLimitStageParam(value any) (int64, error) {
fmt.Sprintf("invalid argument to $limit stage: Expected an integer: $limit: %#v", value),
"$limit (stage)",
)
case errors.Is(err, errLongExceeded):
case errors.Is(err, errLongExceededPositive), errors.Is(err, errLongExceededNegative):
return 0, commonerrors.NewCommandErrorMsgWithArgument(
commonerrors.ErrStageLimitInvalidArg,
fmt.Sprintf("invalid argument to $limit stage: Cannot represent as a 64-bit integer: $limit: %#v", value),
Expand Down Expand Up @@ -215,7 +214,7 @@ func GetSkipStageParam(value any) (int64, error) {
fmt.Sprintf("invalid argument to $skip stage: Expected an integer: $skip: %#v", value),
"$skip (stage)",
)
case errors.Is(err, errLongExceeded):
case errors.Is(err, errLongExceededPositive), errors.Is(err, errLongExceededNegative):
return 0, commonerrors.NewCommandErrorMsgWithArgument(
commonerrors.ErrStageSkipBadValue,
fmt.Sprintf("invalid argument to $skip stage: Cannot represent as a 64-bit integer: $skip: %#v", value),
Expand Down Expand Up @@ -363,11 +362,11 @@ func addNumbers(v1, v2 any) (any, error) {
case int64:
if v2 > 0 {
if int64(v1) > math.MaxInt64-v2 {
return nil, errLongExceeded
return nil, errLongExceededPositive
}
} else {
if int64(v1) < math.MinInt64-v2 {
return nil, errLongExceeded
return nil, errLongExceededNegative
}
}

Expand All @@ -394,11 +393,11 @@ func addNumbers(v1, v2 any) (any, error) {
case int64:
if v2 > 0 {
if v1 > math.MaxInt64-v2 {
return nil, errLongExceeded
return nil, errLongExceededPositive
}
} else {
if v1 < math.MinInt64-v2 {
return nil, errLongExceeded
return nil, errLongExceededNegative
}
}

Expand Down Expand Up @@ -483,12 +482,12 @@ func multiplyLongSafely(v1, v2 int64) (int64, error) {
// This check is necessary only for MinInt64, as multiplying MinInt64 by -1
// results in overflow with the MinInt64 as result.
case v1 == math.MinInt64 || v2 == math.MinInt64:
return 0, errLongExceeded
return 0, errLongExceededNegative
}

res := v1 * v2
if res/v2 != v1 {
return 0, errLongExceeded
return 0, errLongExceededPositive
}

return res, nil
Expand Down
Loading