Files
FerretDB/integration/aggregate_stats_test.go
T

314 lines
11 KiB
Go

// 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 (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"github.com/FerretDB/FerretDB/internal/types"
"github.com/FerretDB/FerretDB/internal/util/must"
"github.com/FerretDB/FerretDB/internal/util/testutil"
"github.com/FerretDB/FerretDB/integration/setup"
"github.com/FerretDB/FerretDB/integration/shareddata"
)
func TestAggregateCollStats(t *testing.T) {
t.Parallel()
ctx, collection := setup.Setup(t, shareddata.DocumentsStrings)
// call validate to updated statistics
err := collection.Database().RunCommand(ctx, bson.D{{"validate", collection.Name()}}).Err()
require.NoError(t, err)
pipeline := bson.A{bson.D{{"$collStats", bson.D{{"storageStats", bson.D{}}}}}}
cursor, err := collection.Aggregate(ctx, pipeline)
require.NoError(t, err)
res := FetchAll(t, ctx, cursor)
require.Len(t, res, 1)
doc := ConvertDocument(t, res[0])
assert.Equal(t, collection.Database().Name()+"."+collection.Name(), must.NotFail(doc.Get("ns")))
v, _ := doc.Get("storageStats")
require.NotNil(t, v)
storageStats, ok := v.(*types.Document)
require.True(t, ok)
assert.NotZero(t, must.NotFail(storageStats.Get("size")))
assert.NotZero(t, must.NotFail(storageStats.Get("count")))
assert.NotZero(t, must.NotFail(storageStats.Get("avgObjSize")))
assert.NotZero(t, must.NotFail(storageStats.Get("storageSize")))
assert.Zero(t, must.NotFail(storageStats.Get("freeStorageSize")))
assert.Equal(t, false, must.NotFail(storageStats.Get("capped")))
assert.NotZero(t, must.NotFail(storageStats.Get("nindexes")))
assert.NotZero(t, must.NotFail(storageStats.Get("totalIndexSize")))
assert.NotZero(t, must.NotFail(storageStats.Get("totalSize")))
assert.NotZero(t, must.NotFail(storageStats.Get("indexSizes")))
assert.Equal(t, int32(1), must.NotFail(storageStats.Get("scaleFactor")))
cappedCollectionName := testutil.CollectionName(t) + "capped"
opts := options.CreateCollection().SetCapped(true).SetSizeInBytes(1000).SetMaxDocuments(10)
err = collection.Database().CreateCollection(ctx, cappedCollectionName, opts)
require.NoError(t, err)
cappedCollection := collection.Database().Collection(cappedCollectionName)
cursor, err = cappedCollection.Aggregate(ctx, pipeline)
require.NoError(t, err)
res = FetchAll(t, ctx, cursor)
require.Len(t, res, 1)
v, _ = ConvertDocument(t, res[0]).Get("storageStats")
require.NotNil(t, v)
storageStats, ok = v.(*types.Document)
require.True(t, ok)
assert.Equal(t, true, must.NotFail(storageStats.Get("capped")))
}
func TestAggregateCollStatsCommandErrors(t *testing.T) {
t.Parallel()
ctx, collection := setup.Setup(t)
for name, tc := range map[string]struct { //nolint:vet // used for test only
command bson.D // required, command to run
database *mongo.Database // defaults to collection.Database()
err *mongo.CommandError // required
altMessage string // optional, alternative error message
skip string // optional, skip test with a specified reason
}{
"NonExistentDatabase": {
database: collection.Database().Client().Database("non-existent"),
command: bson.D{
{"aggregate", collection.Name()},
{"pipeline", bson.A{bson.D{{"$collStats", bson.D{{"storageStats", bson.D{}}}}}}},
{"cursor", bson.D{}},
},
err: &mongo.CommandError{
Code: 26,
Name: "NamespaceNotFound",
Message: `PlanExecutor error during aggregation :: caused by :: ` +
`Unable to retrieve storageStats in $collStats stage :: caused by :: ` +
`Collection [non-existent.TestAggregateCollStatsCommandErrors] not found.`,
},
altMessage: "ns not found: non-existent.TestAggregateCollStatsCommandErrors",
},
"NonExistentCollection": {
command: bson.D{
{"aggregate", "non-existent"},
{"pipeline", bson.A{bson.D{{"$collStats", bson.D{{"storageStats", bson.D{}}}}}}},
{"cursor", bson.D{}},
},
err: &mongo.CommandError{
Code: 26,
Name: "NamespaceNotFound",
Message: `PlanExecutor error during aggregation :: caused by :: ` +
`Unable to retrieve storageStats in $collStats stage :: caused by :: ` +
`Collection [TestAggregateCollStatsCommandErrors.non-existent] not found.`,
},
altMessage: "ns not found: TestAggregateCollStatsCommandErrors.non-existent",
},
"NilCollStats": {
command: bson.D{
{"aggregate", collection.Name()},
{"pipeline", bson.A{bson.D{{"$collStats", nil}}}},
{"cursor", bson.D{}},
},
err: &mongo.CommandError{
Code: 5447000,
Name: "Location5447000",
Message: `$collStats must take a nested object but found: $collStats: null`,
},
altMessage: `$collStats must take a nested object but found: { $collStats: null }`,
},
"StorageStatsNegativeScale": {
command: bson.D{
{"aggregate", collection.Name()},
{"pipeline", bson.A{bson.D{{"$collStats", bson.D{{"storageStats", bson.D{{"scale", -1000}}}}}}}},
{"cursor", bson.D{}},
},
err: &mongo.CommandError{
Code: 51024,
Name: "Location51024",
Message: `BSON field 'scale' value must be >= 1, actual value '-1000'`,
},
},
"StorageStatsInvalidScale": {
command: bson.D{
{"aggregate", collection.Name()},
{"pipeline", bson.A{bson.D{{"$collStats", bson.D{{"storageStats", bson.D{{"scale", "invalid"}}}}}}}},
{"cursor", bson.D{}},
},
err: &mongo.CommandError{
Code: 14,
Name: "TypeMismatch",
Message: `BSON field '$collStats.storageStats.scale' is the wrong type 'string', expected types '[long, int, decimal, double']`,
},
altMessage: `BSON field '$collStats.storageStats.scale' is the wrong type 'string', expected types '[long, int, decimal, double]'`,
},
"CountCollStatsCount": {
command: bson.D{
{"aggregate", collection.Name()},
{"pipeline", bson.A{
bson.D{{"$count", "before"}},
bson.D{{"$collStats", bson.D{{"count", bson.D{}}, {"storageStats", bson.D{}}}}},
bson.D{{"$count", "after"}},
}},
{"cursor", bson.D{}},
},
err: &mongo.CommandError{
Code: 40602,
Name: "Location40602",
Message: `$collStats is only valid as the first stage in a pipeline`,
},
},
} {
t.Run(name, func(t *testing.T) {
if tc.skip != "" {
t.Skip(tc.skip)
}
require.NotNil(t, tc.err, "err must not be nil")
db := tc.database
if db == nil {
db = collection.Database()
}
var res bson.D
err := db.RunCommand(ctx, tc.command).Decode(&res)
AssertEqualAltCommandError(t, *tc.err, tc.altMessage, err)
require.Nil(t, res)
})
}
}
func TestAggregateCollStatsCommandIndexSizes(t *testing.T) {
t.Parallel()
ctx, collection := setup.Setup(t, shareddata.DocumentsStrings)
cursorNoScale, err := collection.Aggregate(ctx, bson.A{
bson.D{{"$collStats", bson.D{{"storageStats", bson.D{}}}}},
})
require.NoError(t, err)
defer cursorNoScale.Close(ctx)
scale := int32(1000)
cursor, err := collection.Aggregate(ctx, bson.A{
bson.D{{"$collStats", bson.D{{"storageStats", bson.D{{"scale", scale}}}}}},
})
require.NoError(t, err)
defer cursor.Close(ctx)
resNoScale := FetchAll(t, ctx, cursorNoScale)
require.Equal(t, 1, len(resNoScale))
res := FetchAll(t, ctx, cursor)
require.Equal(t, 1, len(res))
docNoScale := ConvertDocument(t, resNoScale[0])
doc := ConvertDocument(t, res[0])
storageStatsNoScale := must.NotFail(docNoScale.Get("storageStats")).(*types.Document)
storageStats := must.NotFail(doc.Get("storageStats")).(*types.Document)
size := must.NotFail(storageStats.Get("size"))
switch sizeNoScale := must.NotFail(storageStatsNoScale.Get("size")).(type) {
case int32:
require.EqualValues(t, sizeNoScale/scale, size)
case int64:
require.EqualValues(t, sizeNoScale/int64(scale), size)
default:
t.Fatalf("unknown type %v", sizeNoScale)
}
avgObjSizeNoScale := must.NotFail(storageStatsNoScale.Get("avgObjSize"))
avgObjSize := must.NotFail(storageStats.Get("avgObjSize"))
require.EqualValues(t, avgObjSizeNoScale, avgObjSize)
storageSize := must.NotFail(storageStats.Get("storageSize"))
switch sizeNoScale := must.NotFail(storageStatsNoScale.Get("storageSize")).(type) {
case int32:
require.EqualValues(t, sizeNoScale/scale, storageSize)
case int64:
require.EqualValues(t, sizeNoScale/int64(scale), storageSize)
default:
t.Fatalf("unknown type %v", sizeNoScale)
}
freeStorageSize := must.NotFail(storageStats.Get("freeStorageSize"))
switch sizeNoScale := must.NotFail(storageStatsNoScale.Get("freeStorageSize")).(type) {
case int32:
require.EqualValues(t, sizeNoScale/scale, freeStorageSize)
case int64:
require.EqualValues(t, sizeNoScale/int64(scale), freeStorageSize)
default:
t.Fatalf("unknown type %v", sizeNoScale)
}
totalIndexSize := must.NotFail(storageStats.Get("totalIndexSize"))
switch sizeNoScale := must.NotFail(storageStatsNoScale.Get("totalIndexSize")).(type) {
case int32:
require.EqualValues(t, sizeNoScale/scale, totalIndexSize)
case int64:
require.EqualValues(t, sizeNoScale/int64(scale), totalIndexSize)
default:
t.Fatalf("unknown type %v", sizeNoScale)
}
totalSize := must.NotFail(storageStats.Get("totalSize"))
switch sizeNoScale := must.NotFail(storageStatsNoScale.Get("totalSize")).(type) {
case int32:
require.EqualValues(t, sizeNoScale/scale, totalSize)
case int64:
require.EqualValues(t, sizeNoScale/int64(scale), totalSize)
default:
t.Fatalf("unknown type %v", sizeNoScale)
}
indexSizesNoScale := must.NotFail(storageStatsNoScale.Get("indexSizes")).(*types.Document)
indexSizes := must.NotFail(storageStats.Get("indexSizes")).(*types.Document)
require.Equal(t, []string{"_id_"}, indexSizesNoScale.Keys())
require.Equal(t, []string{"_id_"}, indexSizes.Keys())
for _, index := range indexSizesNoScale.Keys() {
size := must.NotFail(indexSizes.Get(index))
switch sizeNoScale := must.NotFail(indexSizesNoScale.Get(index)).(type) {
case int32:
require.EqualValues(t, sizeNoScale/scale, size)
case int64:
require.EqualValues(t, sizeNoScale/int64(scale), size)
default:
t.Fatalf("unknown type %v", sizeNoScale)
}
}
}