diff --git a/integration/commands_administration_compat_test.go b/integration/commands_administration_compat_test.go index c32cb63e4446..e8c15cdf67d0 100644 --- a/integration/commands_administration_compat_test.go +++ b/integration/commands_administration_compat_test.go @@ -100,8 +100,6 @@ func TestCommandsAdministrationCompatCollStatsWithScale(t *testing.T) { } func TestCommandsAdministrationCompatCollStatsCappedCollection(t *testing.T) { - t.Skip("https://github.com/FerretDB/FerretDB/issues/2447") - t.Parallel() s := setup.SetupCompatWithOpts(t, &setup.SetupCompatOpts{ @@ -109,18 +107,27 @@ func TestCommandsAdministrationCompatCollStatsCappedCollection(t *testing.T) { AddNonExistentCollection: true, }) - ctx, targetCollection, compatCollection := s.Ctx, s.TargetCollections[0], s.CompatCollections[0] + targetDB := s.TargetCollections[0].Database() + compatDB := s.CompatCollections[0].Database() - for name, tc := range map[string]struct { //nolint:vet // for readability - sizeInBytes int64 // also sets capped true if it is greater than zero - maxDocuments int64 // maxDocuments is set if sizeInBytes is greater than zero + for name, tc := range map[string]struct { + sizeInBytes int64 + maxDocuments int64 + + expectedSize int64 }{ "Size": { - sizeInBytes: 1000, + sizeInBytes: 256, + expectedSize: 256, }, - "MaxDocuments": { + "SizeRounded": { sizeInBytes: 1000, + expectedSize: 1024, + }, + "MaxDocuments": { + sizeInBytes: 1, maxDocuments: 10, + expectedSize: 256, }, } { name, tc := name, tc @@ -128,40 +135,37 @@ func TestCommandsAdministrationCompatCollStatsCappedCollection(t *testing.T) { t.Parallel() cName := testutil.CollectionName(t) + name - opts := options.CreateCollection() - - if tc.sizeInBytes > 0 { - opts.SetCapped(true) - opts.SetSizeInBytes(tc.sizeInBytes) + opts := options.CreateCollection().SetCapped(true).SetSizeInBytes(tc.sizeInBytes) - if tc.maxDocuments > 0 { - opts.SetMaxDocuments(tc.maxDocuments) - } + if tc.maxDocuments > 0 { + opts.SetMaxDocuments(tc.maxDocuments) } - targetErr := targetCollection.Database().CreateCollection(ctx, cName, opts) + targetErr := targetDB.CreateCollection(s.Ctx, cName, opts) require.NoError(t, targetErr) - compatErr := compatCollection.Database().CreateCollection(ctx, cName, opts) + compatErr := compatDB.CreateCollection(s.Ctx, cName, opts) require.NoError(t, compatErr) - require.Equal(t, compatCollection.Name(), targetCollection.Name()) - command := bson.D{{"collStats", targetCollection.Name()}} + command := bson.D{{"collStats", cName}} var targetRes bson.D - targetErr = targetCollection.Database().RunCommand(ctx, command).Decode(&targetRes) + targetErr = targetDB.RunCommand(s.Ctx, command).Decode(&targetRes) require.NoError(t, targetErr) var compatRes bson.D - targetErr = compatCollection.Database().RunCommand(ctx, command).Decode(&targetRes) - require.NoError(t, targetErr) + compatErr = compatDB.RunCommand(s.Ctx, command).Decode(&compatRes) + require.NoError(t, compatErr) targetDoc := ConvertDocument(t, targetRes) compatDoc := ConvertDocument(t, compatRes) assert.Equal(t, must.NotFail(compatDoc.Get("capped")), must.NotFail(targetDoc.Get("capped"))) - assert.Equal(t, must.NotFail(compatDoc.Get("max")), must.NotFail(targetDoc.Get("max"))) - assert.Equal(t, must.NotFail(compatDoc.Get("maxSize")), must.NotFail(targetDoc.Get("maxSize"))) + + // TODO https://github.com/FerretDB/FerretDB/issues/3582 + assert.EqualValues(t, tc.expectedSize, must.NotFail(targetDoc.Get("maxSize"))) + assert.EqualValues(t, must.NotFail(compatDoc.Get("maxSize")), must.NotFail(targetDoc.Get("maxSize"))) + assert.EqualValues(t, must.NotFail(compatDoc.Get("max")), must.NotFail(targetDoc.Get("max"))) }) } } diff --git a/integration/create_test.go b/integration/create_test.go index 5a3aededdb94..b77030db5cfc 100644 --- a/integration/create_test.go +++ b/integration/create_test.go @@ -241,3 +241,116 @@ func TestCreateStressSameCollection(t *testing.T) { require.Equal(t, int32(1), created.Load(), "Only one attempt to create a collection should succeed") } + +// TestCreateCappedCommandInvalidSpec checks that invalid create capped collection commands are handled correctly. +// For valid test cases see collStats for capped collections tests. +func TestCreateCappedCommandInvalidSpec(t *testing.T) { + t.Parallel() + + for name, tc := range map[string]struct { //nolint:vet // used for testing only + capped any + size any + max any + unsetSize bool // optional, if true size field is unset + + err *mongo.CommandError // required, expected error from MongoDB + altMessage string // optional, alternative error message for FerretDB, ignored if empty + }{ + "ZeroSize": { + capped: true, + size: 0, + err: &mongo.CommandError{ + Code: 51024, + Name: "Location51024", + Message: "BSON field 'size' value must be >= 1, actual value '0'", + }, + }, + "EmptySize": { + capped: true, + err: &mongo.CommandError{ + Code: 72, + Name: "InvalidOptions", + Message: "the 'size' field is required when 'capped' is true", + }, + }, + "MissingSizeField": { + capped: true, + unsetSize: true, + err: &mongo.CommandError{ + Code: 72, + Name: "InvalidOptions", + Message: "the 'size' field is required when 'capped' is true", + }, + }, + "EmptySizeWithMax": { + capped: true, + max: 500, + err: &mongo.CommandError{ + Code: 72, + Name: "InvalidOptions", + Message: "the 'size' field is required when 'capped' is true", + }, + }, + "WrongSizeType": { + capped: true, + size: "foo", + err: &mongo.CommandError{ + Code: 14, + Name: "TypeMismatch", + Message: "BSON field 'create.size' is the wrong type 'string', expected types '[long, int, decimal, double']", + }, + altMessage: "BSON field 'create.size' is the wrong type 'string', expected types '[long, int, decimal, double]'", + }, + "WrongMaxType": { + capped: true, + size: 500, + max: "foo", + err: &mongo.CommandError{ + Code: 14, + Name: "TypeMismatch", + Message: "BSON field 'create.max' is the wrong type 'string', expected types '[long, int, decimal, double']", + }, + altMessage: "BSON field 'create.max' is the wrong type 'string', expected types '[long, int, decimal, double]'", + }, + "WrongCappedType": { + capped: "foo", + err: &mongo.CommandError{ + Code: 14, + Name: "TypeMismatch", + Message: "BSON field 'create.capped' is the wrong type 'string', expected types '[bool, long, int, decimal, double']", + }, + altMessage: "BSON field 'capped' is the wrong type 'string', expected types '[bool, long, int, decimal, double]'", + }, + "NegativeSize": { + capped: true, + size: -500, + err: &mongo.CommandError{ + Code: 51024, + Name: "Location51024", + Message: "BSON field 'size' value must be >= 1, actual value '-500'", + }, + }, + } { + tc, name := tc, name + t.Run(name, func(t *testing.T) { + t.Parallel() + + ctx, collection := setup.Setup(t) + + command := bson.D{ + {"create", collection.Name()}, + {"capped", tc.capped}, + {"max", tc.max}, + } + + if !tc.unsetSize { + command = append(command, bson.E{Key: "size", Value: tc.size}) + } + + var res bson.D + err := collection.Database().RunCommand(ctx, command).Decode(&res) + AssertEqualAltCommandError(t, *tc.err, tc.altMessage, err) + require.Nil(t, res) + }) + } +} diff --git a/integration/indexes_command_test.go b/integration/indexes_command_test.go index eb67957c1fff..55de7efc1aed 100644 --- a/integration/indexes_command_test.go +++ b/integration/indexes_command_test.go @@ -509,8 +509,7 @@ func TestCreateIndexesCommandInvalidCollection(t *testing.T) { t.Parallel() - provider := shareddata.ArrayDocuments // one provider is enough to check for errors - ctx, collection := setup.Setup(t, provider) + ctx, collection := setup.Setup(t) command := bson.D{ {"createIndexes", tc.collectionName}, @@ -583,8 +582,7 @@ func TestDropIndexesCommandInvalidCollection(t *testing.T) { t.Parallel() - provider := shareddata.ArrayDocuments // one provider is enough to check for errors - ctx, collection := setup.Setup(t, provider) + ctx, collection := setup.Setup(t) command := bson.D{ {"dropIndexes", tc.collectionName}, diff --git a/integration/setup/listener.go b/integration/setup/listener.go index 334449ac2104..b5de84f097d2 100644 --- a/integration/setup/listener.go +++ b/integration/setup/listener.go @@ -228,7 +228,7 @@ func setupListener(tb testtb.TB, ctx context.Context, logger *zap.Logger) string TestOpts: registry.TestOpts{ DisableFilterPushdown: *disableFilterPushdownF, EnableSortPushdown: *enableSortPushdownF, - EnableOplog: *enableOplogF, + EnableOplog: true, UseNewHana: *useNewHanaF, }, diff --git a/integration/setup/setup.go b/integration/setup/setup.go index ae1b2ce32537..c2a45837f2b3 100644 --- a/integration/setup/setup.go +++ b/integration/setup/setup.go @@ -61,7 +61,6 @@ var ( disableFilterPushdownF = flag.Bool("disable-filter-pushdown", false, "disable filter pushdown") enableSortPushdownF = flag.Bool("enable-sort-pushdown", false, "enable sort pushdown") - enableOplogF = flag.Bool("enable-oplog", false, "enable OpLog") useNewHanaF = flag.Bool("use-new-hana", false, "use new SAP HANA backend") ) diff --git a/internal/backends/database.go b/internal/backends/database.go index 806282f97f6e..d030490242f4 100644 --- a/internal/backends/database.go +++ b/internal/backends/database.go @@ -87,8 +87,8 @@ type ListCollectionsResult struct { // CollectionInfo represents information about a single collection. type CollectionInfo struct { Name string - CappedSize int64 // TODO https://github.com/FerretDB/FerretDB/issues/3458 - CappedDocuments int64 // TODO https://github.com/FerretDB/FerretDB/issues/3458 + CappedSize int64 + CappedDocuments int64 } // Capped returns true if collection is capped. @@ -119,8 +119,8 @@ func (dbc *databaseContract) ListCollections(ctx context.Context, params *ListCo // CreateCollectionParams represents the parameters of Database.CreateCollection method. type CreateCollectionParams struct { Name string - CappedSize int64 // TODO https://github.com/FerretDB/FerretDB/issues/3458 - CappedDocuments int64 // TODO https://github.com/FerretDB/FerretDB/issues/3458 + CappedSize int64 + CappedDocuments int64 } // Capped returns true if capped collection creation is requested. diff --git a/internal/backends/postgresql/collection.go b/internal/backends/postgresql/collection.go index 1e730fe080c3..53aa1036bc1f 100644 --- a/internal/backends/postgresql/collection.go +++ b/internal/backends/postgresql/collection.go @@ -119,7 +119,10 @@ func (c *collection) Query(ctx context.Context, params *backends.QueryParams) (* // InsertAll implements backends.Collection interface. func (c *collection) InsertAll(ctx context.Context, params *backends.InsertAllParams) (*backends.InsertAllResult, error) { - if _, err := c.r.CollectionCreate(ctx, c.dbName, c.name); err != nil { + if _, err := c.r.CollectionCreate(ctx, &metadata.CollectionCreateParams{ + DBName: c.dbName, + Name: c.name, + }); err != nil { return nil, lazyerrors.Error(err) } diff --git a/internal/backends/postgresql/database.go b/internal/backends/postgresql/database.go index 859ad087cade..68d5a37a653a 100644 --- a/internal/backends/postgresql/database.go +++ b/internal/backends/postgresql/database.go @@ -53,7 +53,9 @@ func (db *database) ListCollections(ctx context.Context, params *backends.ListCo res := make([]backends.CollectionInfo, len(list)) for i, c := range list { res[i] = backends.CollectionInfo{ - Name: c.Name, + Name: c.Name, + CappedSize: c.CappedSize, + CappedDocuments: c.CappedDocuments, } } @@ -64,7 +66,12 @@ func (db *database) ListCollections(ctx context.Context, params *backends.ListCo // CreateCollection implements backends.Database interface. func (db *database) CreateCollection(ctx context.Context, params *backends.CreateCollectionParams) error { - created, err := db.r.CollectionCreate(ctx, db.name, params.Name) + created, err := db.r.CollectionCreate(ctx, &metadata.CollectionCreateParams{ + DBName: db.name, + Name: params.Name, + CappedSize: params.CappedSize, + CappedDocuments: params.CappedDocuments, + }) if err != nil { return lazyerrors.Error(err) } diff --git a/internal/backends/postgresql/metadata/metadata.go b/internal/backends/postgresql/metadata/metadata.go index e7e5ca6240db..0c5c273c5d63 100644 --- a/internal/backends/postgresql/metadata/metadata.go +++ b/internal/backends/postgresql/metadata/metadata.go @@ -19,6 +19,7 @@ import ( "database/sql" "database/sql/driver" + "github.com/FerretDB/FerretDB/internal/backends" "github.com/FerretDB/FerretDB/internal/handlers/sjson" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" @@ -31,6 +32,9 @@ const ( // IDColumn is a PostgreSQL path expression for _id field. IDColumn = DefaultColumn + "->'_id'" + + // RecordIDColumn is a name for RecordID column to store capped collection record id. + RecordIDColumn = backends.ReservedPrefix + "record_id" ) // Collection represents collection metadata. @@ -38,9 +42,11 @@ const ( // Collection value should be immutable to avoid data races. // Use [deepCopy] to replace the whole value instead of modifying fields of existing value. type Collection struct { - Name string - TableName string - Indexes Indexes + Name string + TableName string + Indexes Indexes + CappedSize int64 + CappedDocuments int64 } // deepCopy returns a deep copy. @@ -50,9 +56,11 @@ func (c *Collection) deepCopy() *Collection { } return &Collection{ - Name: c.Name, - TableName: c.TableName, - Indexes: c.Indexes.deepCopy(), + Name: c.Name, + TableName: c.TableName, + Indexes: c.Indexes.deepCopy(), + CappedSize: c.CappedSize, + CappedDocuments: c.CappedDocuments, } } @@ -100,6 +108,8 @@ func (c *Collection) marshal() *types.Document { "_id", c.Name, "table", c.TableName, "indexes", c.Indexes.marshal(), + "cappedSize", c.CappedSize, + "cappedDocs", c.CappedDocuments, )) } @@ -130,6 +140,16 @@ func (c *Collection) unmarshal(doc *types.Document) error { return lazyerrors.Error(err) } + // For compatibility with older FerretDB versions where cappedSize didn't exist. + if v, _ := doc.Get("cappedSize"); v != nil { + c.CappedSize = v.(int64) + } + + // For compatibility with older FerretDB versions where cappedDocs didn't exist. + if v, _ := doc.Get("cappedDocs"); v != nil { + c.CappedDocuments = v.(int64) + } + return nil } diff --git a/internal/backends/postgresql/metadata/registry.go b/internal/backends/postgresql/metadata/registry.go index 1a723baded99..9d546e6817bb 100644 --- a/internal/backends/postgresql/metadata/registry.go +++ b/internal/backends/postgresql/metadata/registry.go @@ -438,6 +438,14 @@ func (r *Registry) CollectionList(ctx context.Context, dbName string) ([]*Collec return res, nil } +// CollectionCreateParams contains parameters for CollectionCreate. +type CollectionCreateParams struct { + DBName string + Name string + CappedSize int64 + CappedDocuments int64 +} + // CollectionCreate creates a collection in the database. // Database will be created automatically if needed. // @@ -445,7 +453,7 @@ func (r *Registry) CollectionList(ctx context.Context, dbName string) ([]*Collec // If collection already exists, (false, nil) is returned. // // If the user is not authenticated, it returns error. -func (r *Registry) CollectionCreate(ctx context.Context, dbName, collectionName string) (bool, error) { +func (r *Registry) CollectionCreate(ctx context.Context, params *CollectionCreateParams) (bool, error) { defer observability.FuncCall(ctx)() p, err := r.getPool(ctx) @@ -456,7 +464,7 @@ func (r *Registry) CollectionCreate(ctx context.Context, dbName, collectionName r.rw.Lock() defer r.rw.Unlock() - return r.collectionCreate(ctx, p, dbName, collectionName) + return r.collectionCreate(ctx, p, params) } // collectionCreate creates a collection in the database. @@ -466,9 +474,11 @@ func (r *Registry) CollectionCreate(ctx context.Context, dbName, collectionName // If collection already exists, (false, nil) is returned. // // It does not hold the lock. -func (r *Registry) collectionCreate(ctx context.Context, p *pgxpool.Pool, dbName, collectionName string) (bool, error) { +func (r *Registry) collectionCreate(ctx context.Context, p *pgxpool.Pool, params *CollectionCreateParams) (bool, error) { defer observability.FuncCall(ctx)() + dbName, collectionName := params.DBName, params.Name + _, err := r.databaseGetOrCreate(ctx, p, dbName) if err != nil { return false, lazyerrors.Error(err) @@ -505,15 +515,20 @@ func (r *Registry) collectionCreate(ctx context.Context, p *pgxpool.Pool, dbName } c := &Collection{ - Name: collectionName, - TableName: tableName, + Name: collectionName, + TableName: tableName, + CappedSize: params.CappedSize, + CappedDocuments: params.CappedDocuments, } - q := fmt.Sprintf( - `CREATE TABLE %s (%s jsonb)`, - pgx.Identifier{dbName, tableName}.Sanitize(), - DefaultColumn, - ) + q := fmt.Sprintf(`CREATE TABLE %s (`, pgx.Identifier{dbName, tableName}.Sanitize()) + + if params.CappedSize > 0 { + q += fmt.Sprintf(`%s bigint PRIMARY KEY, `, RecordIDColumn) + } + + q += fmt.Sprintf(`%s jsonb)`, DefaultColumn) + if _, err = p.Exec(ctx, q); err != nil { return false, lazyerrors.Error(err) } @@ -737,7 +752,7 @@ func (r *Registry) IndexesCreate(ctx context.Context, dbName, collectionName str func (r *Registry) indexesCreate(ctx context.Context, p *pgxpool.Pool, dbName, collectionName string, indexes []IndexInfo) error { defer observability.FuncCall(ctx)() - _, err := r.collectionCreate(ctx, p, dbName, collectionName) + _, err := r.collectionCreate(ctx, p, &CollectionCreateParams{DBName: dbName, Name: collectionName}) if err != nil { return lazyerrors.Error(err) } diff --git a/internal/backends/postgresql/metadata/registry_test.go b/internal/backends/postgresql/metadata/registry_test.go index 920b513a0b1f..b3838572b1f6 100644 --- a/internal/backends/postgresql/metadata/registry_test.go +++ b/internal/backends/postgresql/metadata/registry_test.go @@ -41,11 +41,11 @@ func testCollection(t *testing.T, ctx context.Context, r *Registry, db *pgxpool. require.NoError(t, err) require.Nil(t, c) - created, err := r.CollectionCreate(ctx, dbName, collectionName) + created, err := r.CollectionCreate(ctx, &CollectionCreateParams{DBName: dbName, Name: collectionName}) require.NoError(t, err) require.True(t, created) - created, err = r.CollectionCreate(ctx, dbName, collectionName) + created, err = r.CollectionCreate(ctx, &CollectionCreateParams{DBName: dbName, Name: collectionName}) require.NoError(t, err) require.False(t, created) @@ -196,13 +196,13 @@ func TestCreateSameStress(t *testing.T) { ready <- struct{}{} <-start - created, err := r.CollectionCreate(ctx, dbName, collectionName) + created, err := r.CollectionCreate(ctx, &CollectionCreateParams{DBName: dbName, Name: collectionName}) require.NoError(t, err) if created { createdTotal.Add(1) } - created, err = r.CollectionCreate(ctx, dbName, collectionName) + created, err = r.CollectionCreate(ctx, &CollectionCreateParams{DBName: dbName, Name: collectionName}) require.NoError(t, err) require.False(t, created) @@ -238,7 +238,7 @@ func TestDropSameStress(t *testing.T) { r, _, dbName := createDatabase(t, ctx) collectionName := testutil.CollectionName(t) - created, err := r.CollectionCreate(ctx, dbName, collectionName) + created, err := r.CollectionCreate(ctx, &CollectionCreateParams{DBName: dbName, Name: collectionName}) require.NoError(t, err) require.True(t, created) @@ -277,7 +277,7 @@ func TestCreateDropSameStress(t *testing.T) { <-start if id%2 == 0 { - created, err := r.CollectionCreate(ctx, dbName, collectionName) + created, err := r.CollectionCreate(ctx, &CollectionCreateParams{DBName: dbName, Name: collectionName}) require.NoError(t, err) if created { createdTotal.Add(1) @@ -320,7 +320,7 @@ func TestCheckDatabaseUpdated(t *testing.T) { }) collectionName := testutil.CollectionName(t) - created, err := r.CollectionCreate(ctx, dbName, collectionName) + created, err := r.CollectionCreate(ctx, &CollectionCreateParams{DBName: dbName, Name: collectionName}) require.NoError(t, err) require.True(t, created) @@ -379,7 +379,7 @@ func TestRenameCollection(t *testing.T) { oldCollectionName := testutil.CollectionName(t) newCollectionName := "new" - created, err := r.CollectionCreate(ctx, dbName, oldCollectionName) + created, err := r.CollectionCreate(ctx, &CollectionCreateParams{DBName: dbName, Name: oldCollectionName}) require.NoError(t, err) require.True(t, created) diff --git a/internal/backends/sqlite/collection.go b/internal/backends/sqlite/collection.go index 60fad4d3669a..e899a7f76370 100644 --- a/internal/backends/sqlite/collection.go +++ b/internal/backends/sqlite/collection.go @@ -104,7 +104,7 @@ func (c *collection) Query(ctx context.Context, params *backends.QueryParams) (* // InsertAll implements backends.Collection interface. func (c *collection) InsertAll(ctx context.Context, params *backends.InsertAllParams) (*backends.InsertAllResult, error) { - if _, err := c.r.CollectionCreate(ctx, c.dbName, c.name); err != nil { + if _, err := c.r.CollectionCreate(ctx, &metadata.CollectionCreateParams{DBName: c.dbName, Name: c.name}); err != nil { return nil, lazyerrors.Error(err) } diff --git a/internal/backends/sqlite/database.go b/internal/backends/sqlite/database.go index f07b7608de64..f1d42867fd98 100644 --- a/internal/backends/sqlite/database.go +++ b/internal/backends/sqlite/database.go @@ -53,7 +53,9 @@ func (db *database) ListCollections(ctx context.Context, params *backends.ListCo res := make([]backends.CollectionInfo, len(list)) for i, c := range list { res[i] = backends.CollectionInfo{ - Name: c.Name, + Name: c.Name, + CappedSize: c.Settings.CappedSize, + CappedDocuments: c.Settings.CappedDocuments, } } @@ -64,7 +66,12 @@ func (db *database) ListCollections(ctx context.Context, params *backends.ListCo // CreateCollection implements backends.Database interface. func (db *database) CreateCollection(ctx context.Context, params *backends.CreateCollectionParams) error { - created, err := db.r.CollectionCreate(ctx, db.name, params.Name) + created, err := db.r.CollectionCreate(ctx, &metadata.CollectionCreateParams{ + DBName: db.name, + Name: params.Name, + CappedSize: params.CappedSize, + CappedDocuments: params.CappedDocuments, + }) if err != nil { return lazyerrors.Error(err) } diff --git a/internal/backends/sqlite/metadata/metadata.go b/internal/backends/sqlite/metadata/metadata.go index 27527153cdb8..625ceb800d86 100644 --- a/internal/backends/sqlite/metadata/metadata.go +++ b/internal/backends/sqlite/metadata/metadata.go @@ -28,6 +28,9 @@ const ( // IDColumn is a SQLite path expression for _id field. IDColumn = DefaultColumn + "->'$._id'" + + // RecordIDColumn is a name for RecordID column to store capped collection record id. + RecordIDColumn = backends.ReservedPrefix + "record_id" ) // Collection represents collection metadata. diff --git a/internal/backends/sqlite/metadata/registry.go b/internal/backends/sqlite/metadata/registry.go index a34360af29bb..b9d8b43f3480 100644 --- a/internal/backends/sqlite/metadata/registry.go +++ b/internal/backends/sqlite/metadata/registry.go @@ -229,18 +229,26 @@ func (r *Registry) CollectionList(ctx context.Context, dbName string) ([]*Collec return res, nil } +// CollectionCreateParams contains parameters for CollectionCreate. +type CollectionCreateParams struct { + DBName string + Name string + CappedSize int64 + CappedDocuments int64 +} + // CollectionCreate creates a collection in the database. // Database will be created automatically if needed. // // Returned boolean value indicates whether the collection was created. // If collection already exists, (false, nil) is returned. -func (r *Registry) CollectionCreate(ctx context.Context, dbName, collectionName string) (bool, error) { +func (r *Registry) CollectionCreate(ctx context.Context, params *CollectionCreateParams) (bool, error) { defer observability.FuncCall(ctx)() r.rw.Lock() defer r.rw.Unlock() - return r.collectionCreate(ctx, dbName, collectionName) + return r.collectionCreate(ctx, params) } // collectionCreate creates a collection in the database. @@ -250,9 +258,11 @@ func (r *Registry) CollectionCreate(ctx context.Context, dbName, collectionName // If collection already exists, (false, nil) is returned. // // It does not hold the lock. -func (r *Registry) collectionCreate(ctx context.Context, dbName, collectionName string) (bool, error) { +func (r *Registry) collectionCreate(ctx context.Context, params *CollectionCreateParams) (bool, error) { defer observability.FuncCall(ctx)() + dbName, collectionName := params.DBName, params.Name + db, err := r.databaseGetOrCreate(ctx, dbName) if err != nil { return false, lazyerrors.Error(err) @@ -284,7 +294,14 @@ func (r *Registry) collectionCreate(ctx context.Context, dbName, collectionName s++ } - q := fmt.Sprintf("CREATE TABLE %[1]q (%[2]s TEXT NOT NULL CHECK(%[2]s != '')) STRICT", tableName, DefaultColumn) + q := fmt.Sprintf("CREATE TABLE %q (", tableName) + + if params.CappedSize > 0 { + q += fmt.Sprintf("%s INTEGER PRIMARY KEY, ", RecordIDColumn) + } + + q += fmt.Sprintf("%[1]s TEXT NOT NULL CHECK(%[1]s != '')) STRICT", DefaultColumn) + if _, err = db.ExecContext(ctx, q); err != nil { return false, lazyerrors.Error(err) } @@ -301,6 +318,10 @@ func (r *Registry) collectionCreate(ctx context.Context, dbName, collectionName r.colls[dbName][collectionName] = &Collection{ Name: collectionName, TableName: tableName, + Settings: Settings{ + CappedSize: params.CappedSize, + CappedDocuments: params.CappedDocuments, + }, } err = r.indexesCreate(ctx, dbName, collectionName, []IndexInfo{{ @@ -445,7 +466,7 @@ func (r *Registry) IndexesCreate(ctx context.Context, dbName, collectionName str func (r *Registry) indexesCreate(ctx context.Context, dbName, collectionName string, indexes []IndexInfo) error { defer observability.FuncCall(ctx)() - _, err := r.collectionCreate(ctx, dbName, collectionName) + _, err := r.collectionCreate(ctx, &CollectionCreateParams{DBName: dbName, Name: collectionName}) if err != nil { return lazyerrors.Error(err) } diff --git a/internal/backends/sqlite/metadata/registry_test.go b/internal/backends/sqlite/metadata/registry_test.go index 6e7ffdf7b904..e8cb826dc76c 100644 --- a/internal/backends/sqlite/metadata/registry_test.go +++ b/internal/backends/sqlite/metadata/registry_test.go @@ -35,11 +35,11 @@ func testCollection(t *testing.T, ctx context.Context, r *Registry, db *fsql.DB, c := r.CollectionGet(ctx, dbName, collectionName) require.Nil(t, c) - created, err := r.CollectionCreate(ctx, dbName, collectionName) + created, err := r.CollectionCreate(ctx, &CollectionCreateParams{DBName: dbName, Name: collectionName}) require.NoError(t, err) require.True(t, created) - created, err = r.CollectionCreate(ctx, dbName, collectionName) + created, err = r.CollectionCreate(ctx, &CollectionCreateParams{DBName: dbName, Name: collectionName}) require.NoError(t, err) require.False(t, created) @@ -176,13 +176,13 @@ func TestCreateSameStress(t *testing.T) { ready <- struct{}{} <-start - created, err := r.CollectionCreate(ctx, dbName, collectionName) + created, err := r.CollectionCreate(ctx, &CollectionCreateParams{DBName: dbName, Name: collectionName}) require.NoError(t, err) if created { createdTotal.Add(1) } - created, err = r.CollectionCreate(ctx, dbName, collectionName) + created, err = r.CollectionCreate(ctx, &CollectionCreateParams{DBName: dbName, Name: collectionName}) require.NoError(t, err) require.False(t, created) @@ -236,7 +236,7 @@ func TestDropSameStress(t *testing.T) { collectionName := "collection" - created, err := r.CollectionCreate(ctx, dbName, collectionName) + created, err := r.CollectionCreate(ctx, &CollectionCreateParams{DBName: dbName, Name: collectionName}) require.NoError(t, err) require.True(t, created) @@ -297,7 +297,7 @@ func TestCreateDropSameStress(t *testing.T) { <-start if id%2 == 0 { - created, err := r.CollectionCreate(ctx, dbName, collectionName) + created, err := r.CollectionCreate(ctx, &CollectionCreateParams{DBName: dbName, Name: collectionName}) require.NoError(t, err) if created { createdTotal.Add(1) diff --git a/internal/backends/sqlite/metadata/settings.go b/internal/backends/sqlite/metadata/settings.go index 59b4d8a41574..a5751c7b85be 100644 --- a/internal/backends/sqlite/metadata/settings.go +++ b/internal/backends/sqlite/metadata/settings.go @@ -25,7 +25,9 @@ import ( // Settings represents collection settings. type Settings struct { - Indexes []IndexInfo `json:"indexes"` + Indexes []IndexInfo `json:"indexes"` + CappedSize int64 `json:"cappedSize"` + CappedDocuments int64 `json:"cappedDocuments"` } // IndexInfo represents information about a single index. @@ -54,7 +56,9 @@ func (s Settings) deepCopy() Settings { } return Settings{ - Indexes: indexes, + Indexes: indexes, + CappedSize: s.CappedSize, + CappedDocuments: s.CappedDocuments, } } diff --git a/internal/backends/sqlite/sqlite_test.go b/internal/backends/sqlite/sqlite_test.go index d91287c55542..8bb0a0556d47 100644 --- a/internal/backends/sqlite/sqlite_test.go +++ b/internal/backends/sqlite/sqlite_test.go @@ -43,7 +43,7 @@ func TestCollectionsStats(t *testing.T) { colls := make([]*metadata.Collection, len(cNames)) for i, cName := range cNames { - _, err = r.CollectionCreate(ctx, dbName, cName) + _, err = r.CollectionCreate(ctx, &metadata.CollectionCreateParams{DBName: dbName, Name: cName}) require.NoError(t, err) colls[i] = r.CollectionGet(ctx, dbName, cName) } diff --git a/internal/handlers/pg/msg_aggregate.go b/internal/handlers/pg/msg_aggregate.go index c5dcab0f9ce1..c285c523c048 100644 --- a/internal/handlers/pg/msg_aggregate.go +++ b/internal/handlers/pg/msg_aggregate.go @@ -436,15 +436,15 @@ func processStagesStats(ctx context.Context, closer *iterator.MultiCloser, p *st "count", collStats.CountObjects, "avgObjSize", avgObjSize, "storageSize", collStats.SizeCollection, - "freeStorageSize", int64(0), // TODO https://github.com/FerretDB/FerretDB/issues/2342 - "capped", false, // TODO https://github.com/FerretDB/FerretDB/issues/2342 - "wiredTiger", must.NotFail(types.NewDocument()), // TODO https://github.com/FerretDB/FerretDB/issues/2342 + "freeStorageSize", int64(0), + "capped", false, + "wiredTiger", must.NotFail(types.NewDocument()), "nindexes", collStats.CountIndexes, - "indexDetails", must.NotFail(types.NewDocument()), // TODO https://github.com/FerretDB/FerretDB/issues/2342 - "indexBuilds", must.NotFail(types.NewDocument()), // TODO https://github.com/FerretDB/FerretDB/issues/2342 + "indexDetails", must.NotFail(types.NewDocument()), + "indexBuilds", must.NotFail(types.NewDocument()), "totalIndexSize", collStats.SizeIndexes, "totalSize", collStats.SizeTotal, - "indexSizes", must.NotFail(types.NewDocument()), // TODO https://github.com/FerretDB/FerretDB/issues/2342 + "indexSizes", must.NotFail(types.NewDocument()), )), ) } diff --git a/internal/handlers/pg/msg_serverstatus.go b/internal/handlers/pg/msg_serverstatus.go index d2802e9ae693..3b287a5b1316 100644 --- a/internal/handlers/pg/msg_serverstatus.go +++ b/internal/handlers/pg/msg_serverstatus.go @@ -50,7 +50,7 @@ func (h *Handler) MsgServerStatus(ctx context.Context, msg *wire.OpMsg) (*wire.O res.Set("catalogStats", must.NotFail(types.NewDocument( "collections", stats.CountCollections, - "capped", int32(0), // TODO https://github.com/FerretDB/FerretDB/issues/2342 + "capped", int32(0), "clustered", int32(0), "timeseries", int32(0), "views", int32(0), diff --git a/internal/handlers/sqlite/msg_create.go b/internal/handlers/sqlite/msg_create.go index 3383cd14cbea..004d7cfb08d2 100644 --- a/internal/handlers/sqlite/msg_create.go +++ b/internal/handlers/sqlite/msg_create.go @@ -21,6 +21,7 @@ import ( "github.com/FerretDB/FerretDB/internal/backends" "github.com/FerretDB/FerretDB/internal/handlers/common" "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" + "github.com/FerretDB/FerretDB/internal/handlers/commonparams" "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" "github.com/FerretDB/FerretDB/internal/util/must" @@ -37,8 +38,6 @@ func (h *Handler) MsgCreate(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, unimplementedFields := []string{ "timeseries", "expireAfterSeconds", - "size", // TODO https://github.com/FerretDB/FerretDB/issues/3458 - "max", // TODO https://github.com/FerretDB/FerretDB/issues/3458 "validator", "validationLevel", "validationAction", @@ -50,14 +49,6 @@ func (h *Handler) MsgCreate(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, return nil, err } - // TODO https://github.com/FerretDB/FerretDB/issues/3458 - if err = common.UnimplementedNonDefault(document, "capped", func(v any) bool { - b, ok := v.(bool) - return ok && !b - }); err != nil { - return nil, err - } - ignoredFields := []string{ "autoIndexId", "storageEngine", @@ -79,6 +70,49 @@ func (h *Handler) MsgCreate(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, return nil, err } + params := backends.CreateCollectionParams{ + Name: collectionName, + } + + var capped bool + if v, _ := document.Get("capped"); v != nil { + capped, err = commonparams.GetBoolOptionalParam("capped", v) + if err != nil { + return nil, err + } + } + + if h.EnableOplog && capped { + var size any + + size, _ = document.Get("size") + if size == nil { + msg := "the 'size' field is required when 'capped' is true" + return nil, commonerrors.NewCommandErrorMsgWithArgument(commonerrors.ErrInvalidOptions, msg, "create") + } + + if _, ok := size.(types.NullType); ok { + msg := "the 'size' field is required when 'capped' is true" + return nil, commonerrors.NewCommandErrorMsgWithArgument(commonerrors.ErrInvalidOptions, msg, "create") + } + + params.CappedSize, err = commonparams.GetValidatedNumberParamWithMinValue(document.Command(), "size", size, 1) + if err != nil { + return nil, err + } + + if params.CappedSize%256 != 0 { + params.CappedSize = (params.CappedSize/256 + 1) * 256 + } + + if max, _ := document.Get("max"); max != nil { + params.CappedDocuments, err = commonparams.GetValidatedNumberParamWithMinValue(document.Command(), "max", max, 0) + if err != nil { + return nil, err + } + } + } + db, err := h.b.Database(dbName) if err != nil { if backends.ErrorCodeIs(err, backends.ErrorCodeDatabaseNameIsInvalid) { @@ -89,9 +123,7 @@ func (h *Handler) MsgCreate(ctx context.Context, msg *wire.OpMsg) (*wire.OpMsg, return nil, lazyerrors.Error(err) } - err = db.CreateCollection(ctx, &backends.CreateCollectionParams{ - Name: collectionName, - }) + err = db.CreateCollection(ctx, ¶ms) switch { case err == nil: diff --git a/website/docs/reference/supported-commands.md b/website/docs/reference/supported-commands.md index bc12b4683bb5..67827b08bd13 100644 --- a/website/docs/reference/supported-commands.md +++ b/website/docs/reference/supported-commands.md @@ -559,7 +559,7 @@ Related [issue](https://github.com/FerretDB/FerretDB/issues/1917). | | `writeConcern` | | ⚠️ | | | | `comment` | | ⚠️ | | | `create` | | | ✅ | | -| | `capped` | | ⚠️ | [Issue](https://github.com/FerretDB/FerretDB/issues/3458) | +| | `capped` | | ✅️ | | | | `timeseries` | | ⚠️ | [Issue](https://github.com/FerretDB/FerretDB/issues/177) | | | | `timeField` | ⚠️ | | | | | `metaField` | ⚠️ | | @@ -568,8 +568,8 @@ Related [issue](https://github.com/FerretDB/FerretDB/issues/1917). | | `clusteredIndex` | | ⚠️ | | | | `changeStreamPreAndPostImages` | | ⚠️ | | | | `autoIndexId` | | ⚠️ | Ignored | -| | `size` | | ⚠️ | [Issue](https://github.com/FerretDB/FerretDB/issues/3458) | -| | `max` | | ⚠️ | [Issue](https://github.com/FerretDB/FerretDB/issues/3458) | +| | `size` | | ✅️ | | +| | `max` | | ✅ | | | | `storageEngine` | | ⚠️ | Ignored | | | `validator` | | ⚠️ | Not implemented in PostgreSQL | | | `validationLevel` | | ⚠️ | Unimplemented |