From 670ef44b4e1b2f2bdcefb3cdc4dae0853de00906 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Fri, 12 May 2023 11:39:46 +0400 Subject: [PATCH 01/11] Add common backend interface prototype --- internal/backend/backend.go | 17 +++++++ internal/backend/collection.go | 17 +++++++ internal/backend/database.go | 51 +++++++++++++++++++ internal/backend/error.go | 75 ++++++++++++++++++++++++++++ internal/backend/errorcode_string.go | 25 ++++++++++ 5 files changed, 185 insertions(+) create mode 100644 internal/backend/backend.go create mode 100644 internal/backend/collection.go create mode 100644 internal/backend/database.go create mode 100644 internal/backend/error.go create mode 100644 internal/backend/errorcode_string.go diff --git a/internal/backend/backend.go b/internal/backend/backend.go new file mode 100644 index 000000000000..926f0a0c7a91 --- /dev/null +++ b/internal/backend/backend.go @@ -0,0 +1,17 @@ +// 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 backend + +type Backend interface{} diff --git a/internal/backend/collection.go b/internal/backend/collection.go new file mode 100644 index 000000000000..e0424ed405a0 --- /dev/null +++ b/internal/backend/collection.go @@ -0,0 +1,17 @@ +// 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 backend + +type Collection interface{} diff --git a/internal/backend/database.go b/internal/backend/database.go new file mode 100644 index 000000000000..fa4267c9db9c --- /dev/null +++ b/internal/backend/database.go @@ -0,0 +1,51 @@ +// 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 backend + +type Database interface { + CreateCollection(params *CreateCollectionParams) error + DropCollection(params *DropCollectionParams) error +} + +func DatabaseContract(db Database) Database { + return &databaseContract{ + db: db, + } +} + +type databaseContract struct { + db Database +} + +type CreateCollectionParams struct{} + +func (db *databaseContract) CreateCollection(params *CreateCollectionParams) (err error) { + defer checkError(err, ErrCollectionAlreadyExists) + err = db.db.CreateCollection(params) + return +} + +type DropCollectionParams struct{} + +func (db *databaseContract) DropCollection(params *DropCollectionParams) (err error) { + defer checkError(err, ErrCollectionAlreadyExists) + err = db.db.DropCollection(params) + return +} + +// check interfaces +var ( + _ Database = (*databaseContract)(nil) +) diff --git a/internal/backend/error.go b/internal/backend/error.go new file mode 100644 index 000000000000..b5c0c710fc5d --- /dev/null +++ b/internal/backend/error.go @@ -0,0 +1,75 @@ +// 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 backend + +import ( + "errors" + + "golang.org/x/exp/slices" + + "github.com/FerretDB/FerretDB/internal/util/debugbuild" +) + +//go:generate ../../bin/stringer -linecomment -type ErrorCode + +type ErrorCode int + +const ( + _ ErrorCode = iota + + ErrCollectionDoesNotExist // collection does not exist + ErrCollectionAlreadyExists // collection already exists +) + +type Error struct { + Code ErrorCode + err error +} + +func (err *Error) Error() string { + return err.err.Error() +} + +func (err *Error) Unwrap() error { + return err.err +} + +func checkError(err error, codes ...ErrorCode) { + if !debugbuild.Enabled { + return + } + + if err == nil { + return + } + + var e *Error + if !errors.As(err, &e) { + panic(err) + } + + if e.Code == 0 { + panic(err) + } + + if !slices.Contains(codes, e.Code) { + panic(err) + } +} + +// check interfaces +var ( + _ error = (*Error)(nil) +) diff --git a/internal/backend/errorcode_string.go b/internal/backend/errorcode_string.go new file mode 100644 index 000000000000..2311cc8f1c45 --- /dev/null +++ b/internal/backend/errorcode_string.go @@ -0,0 +1,25 @@ +// Code generated by "stringer -linecomment -type ErrorCode"; DO NOT EDIT. + +package backend + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ErrCollectionDoesNotExist-1] + _ = x[ErrCollectionAlreadyExists-2] +} + +const _ErrorCode_name = "collection does not existcollection already exists" + +var _ErrorCode_index = [...]uint8{0, 25, 50} + +func (i ErrorCode) String() string { + i -= 1 + if i < 0 || i >= ErrorCode(len(_ErrorCode_index)-1) { + return "ErrorCode(" + strconv.FormatInt(int64(i+1), 10) + ")" + } + return _ErrorCode_name[_ErrorCode_index[i]:_ErrorCode_index[i+1]] +} From 7b40fddedadf21c383e4932253281a30f382d943 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Fri, 12 May 2023 13:44:10 +0400 Subject: [PATCH 02/11] More work --- internal/backend/backend.go | 25 +++++++++++++++++++++++- internal/backend/collection.go | 32 ++++++++++++++++++++++++++++++- internal/backend/database.go | 26 +++++++++++++++++++++---- internal/backend/sqlite/sqlite.go | 15 +++++++++++++++ 4 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 internal/backend/sqlite/sqlite.go diff --git a/internal/backend/backend.go b/internal/backend/backend.go index 926f0a0c7a91..11c9fad37e07 100644 --- a/internal/backend/backend.go +++ b/internal/backend/backend.go @@ -14,4 +14,27 @@ package backend -type Backend interface{} +type Backend interface { + Database(params *DatabaseParams) Database +} + +func BackendContract(b Backend) Backend { + return &backendContract{ + b: b, + } +} + +type backendContract struct { + b Backend +} + +type DatabaseParams struct{} + +func (bc *backendContract) Database(params *DatabaseParams) Database { + return bc.b.Database(params) +} + +// check interfaces +var ( + _ Backend = (*backendContract)(nil) +) diff --git a/internal/backend/collection.go b/internal/backend/collection.go index e0424ed405a0..9d8dea1102d6 100644 --- a/internal/backend/collection.go +++ b/internal/backend/collection.go @@ -14,4 +14,34 @@ package backend -type Collection interface{} +import "github.com/FerretDB/FerretDB/internal/types" + +type Collection interface { + Insert(params *InsertParams) error +} + +func CollectionContract(c Collection) Collection { + return &collectionContract{ + c: c, + } +} + +type collectionContract struct { + c Collection +} + +type InsertParams struct { + Docs types.DocumentsIterator + Ordered bool +} + +func (cc *collectionContract) Insert(params *InsertParams) (err error) { + // defer checkError(err, ErrCollectionDoesNotExist) + err = cc.c.Insert(params) + return +} + +// check interfaces +var ( + _ Collection = (*collectionContract)(nil) +) diff --git a/internal/backend/database.go b/internal/backend/database.go index fa4267c9db9c..310ad52528f5 100644 --- a/internal/backend/database.go +++ b/internal/backend/database.go @@ -15,6 +15,8 @@ package backend type Database interface { + Collection(params *CollectionParams) Collection + ListCollections(params *ListCollectionsParams) ([]CollectionInfo, error) CreateCollection(params *CreateCollectionParams) error DropCollection(params *DropCollectionParams) error } @@ -29,19 +31,35 @@ type databaseContract struct { db Database } +type CollectionParams struct{} + +func (dbc *databaseContract) Collection(params *CollectionParams) Collection { + return dbc.db.Collection(params) +} + +type ListCollectionsParams struct{} + +type CollectionInfo struct{} + +func (dbc *databaseContract) ListCollections(params *ListCollectionsParams) (res []CollectionInfo, err error) { + // defer checkError(err, ErrCollectionDoesNotExist) + res, err = dbc.db.ListCollections(params) + return +} + type CreateCollectionParams struct{} -func (db *databaseContract) CreateCollection(params *CreateCollectionParams) (err error) { +func (dbc *databaseContract) CreateCollection(params *CreateCollectionParams) (err error) { defer checkError(err, ErrCollectionAlreadyExists) - err = db.db.CreateCollection(params) + err = dbc.db.CreateCollection(params) return } type DropCollectionParams struct{} -func (db *databaseContract) DropCollection(params *DropCollectionParams) (err error) { +func (dbc *databaseContract) DropCollection(params *DropCollectionParams) (err error) { defer checkError(err, ErrCollectionAlreadyExists) - err = db.db.DropCollection(params) + err = dbc.db.DropCollection(params) return } diff --git a/internal/backend/sqlite/sqlite.go b/internal/backend/sqlite/sqlite.go new file mode 100644 index 000000000000..d0a3ca049dd8 --- /dev/null +++ b/internal/backend/sqlite/sqlite.go @@ -0,0 +1,15 @@ +// 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 sqlite From eb198a55d17c6cdfb9830f3a5cce10f6262dce50 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Mon, 15 May 2023 10:57:34 +0400 Subject: [PATCH 03/11] More --- CONTRIBUTING.md | 4 +- internal/backend/database.go | 69 ----------- internal/backends/backend.go | 73 +++++++++++ internal/{backend => backends}/collection.go | 16 ++- internal/backends/database.go | 114 ++++++++++++++++++ .../sqlite/sqlite.go => backends/doc.go} | 11 +- internal/{backend => backends}/error.go | 37 +++++- .../{backend => backends}/errorcode_string.go | 7 +- internal/backends/sqlite/backend.go | 40 ++++++ .../sqlite/collection.go} | 31 ++--- internal/backends/sqlite/database.go | 57 +++++++++ 11 files changed, 360 insertions(+), 99 deletions(-) delete mode 100644 internal/backend/database.go create mode 100644 internal/backends/backend.go rename internal/{backend => backends}/collection.go (74%) create mode 100644 internal/backends/database.go rename internal/{backend/sqlite/sqlite.go => backends/doc.go} (54%) rename internal/{backend => backends}/error.go (67%) rename internal/{backend => backends}/errorcode_string.go (82%) create mode 100644 internal/backends/sqlite/backend.go rename internal/{backend/backend.go => backends/sqlite/collection.go} (55%) create mode 100644 internal/backends/sqlite/database.go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 91f95494c5f7..cc5393b2af0c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -270,7 +270,9 @@ Before submitting a pull request, please make sure that: so there is **no need** to squash them manually, amend them, and/or do force pushes. But notice that the autogenerated GitHub's squash commit's body **should be** manually replaced by "Closes #{issue_number}.". -5. Please don't forget to click "re-request review" buttons once PR is ready for re-review. +5. Please don't forget to click + ["re-request review" buttons](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/requesting-a-pull-request-review) + once PR is ready for re-review. If you have interest in becoming or are a long-term contributor, please read [PROCESS.md](.github/PROCESS.md) for more details. diff --git a/internal/backend/database.go b/internal/backend/database.go deleted file mode 100644 index 310ad52528f5..000000000000 --- a/internal/backend/database.go +++ /dev/null @@ -1,69 +0,0 @@ -// 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 backend - -type Database interface { - Collection(params *CollectionParams) Collection - ListCollections(params *ListCollectionsParams) ([]CollectionInfo, error) - CreateCollection(params *CreateCollectionParams) error - DropCollection(params *DropCollectionParams) error -} - -func DatabaseContract(db Database) Database { - return &databaseContract{ - db: db, - } -} - -type databaseContract struct { - db Database -} - -type CollectionParams struct{} - -func (dbc *databaseContract) Collection(params *CollectionParams) Collection { - return dbc.db.Collection(params) -} - -type ListCollectionsParams struct{} - -type CollectionInfo struct{} - -func (dbc *databaseContract) ListCollections(params *ListCollectionsParams) (res []CollectionInfo, err error) { - // defer checkError(err, ErrCollectionDoesNotExist) - res, err = dbc.db.ListCollections(params) - return -} - -type CreateCollectionParams struct{} - -func (dbc *databaseContract) CreateCollection(params *CreateCollectionParams) (err error) { - defer checkError(err, ErrCollectionAlreadyExists) - err = dbc.db.CreateCollection(params) - return -} - -type DropCollectionParams struct{} - -func (dbc *databaseContract) DropCollection(params *DropCollectionParams) (err error) { - defer checkError(err, ErrCollectionAlreadyExists) - err = dbc.db.DropCollection(params) - return -} - -// check interfaces -var ( - _ Database = (*databaseContract)(nil) -) diff --git a/internal/backends/backend.go b/internal/backends/backend.go new file mode 100644 index 000000000000..75bf349dba50 --- /dev/null +++ b/internal/backends/backend.go @@ -0,0 +1,73 @@ +// 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 backends + +// Backends is a generic interface for all backends for accessing them. +// +// Backend object is expected to be stateful and wrap database connection(s). +// Handler can create one Backend or multiple Backends with different authentication credentials. +// +// Backend(s) methods can be called by multiple client connections / command handlers concurrently. +// They should be thread-safe. +// +// See backendContract and its methods for additional details. +type Backend interface { + Database(*DatabaseParams) Database + ListDatabases(*ListDatabasesParams) (*ListDatabasesResult, error) +} + +// BackendContract wraps Backend and enforces its contract. +// +// All backend implementations should use that function when they create new Backend instances. +// The handler should not use that function. +// +// See backendContract and its methods for additional details. +func BackendContract(b Backend) Backend { + return &backendContract{ + b: b, + } +} + +// backendContract implements Backend interface. +type backendContract struct { + b Backend +} + +// DatabaseParams represents the parameters of Backend.Database method. +type DatabaseParams struct { + Name string +} + +// Database returns a Database instance for given parameters. +// +// The database does not need to exist; even parameters like name could be invalid. +func (bc *backendContract) Database(params *DatabaseParams) Database { + return bc.b.Database(params) +} + +type ListDatabasesParams struct{} + +type ListDatabasesResult struct{} + +func (bc *backendContract) ListDatabases(params *ListDatabasesParams) (res *ListDatabasesResult, err error) { + defer checkError(err) + res, err = bc.b.ListDatabases(params) + return +} + +// check interfaces +var ( + _ Backend = (*backendContract)(nil) +) diff --git a/internal/backend/collection.go b/internal/backends/collection.go similarity index 74% rename from internal/backend/collection.go rename to internal/backends/collection.go index 9d8dea1102d6..2d075b1bf383 100644 --- a/internal/backend/collection.go +++ b/internal/backends/collection.go @@ -12,12 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. -package backend +package backends -import "github.com/FerretDB/FerretDB/internal/types" +import ( + "context" + + "github.com/FerretDB/FerretDB/internal/types" +) type Collection interface { - Insert(params *InsertParams) error + Insert(context.Context, *InsertParams) (*InsertResult, error) } func CollectionContract(c Collection) Collection { @@ -35,9 +39,11 @@ type InsertParams struct { Ordered bool } -func (cc *collectionContract) Insert(params *InsertParams) (err error) { +type InsertResult struct{} + +func (cc *collectionContract) Insert(ctx context.Context, params *InsertParams) (res *InsertResult, err error) { // defer checkError(err, ErrCollectionDoesNotExist) - err = cc.c.Insert(params) + res, err = cc.c.Insert(ctx, params) return } diff --git a/internal/backends/database.go b/internal/backends/database.go new file mode 100644 index 000000000000..592cf79f7665 --- /dev/null +++ b/internal/backends/database.go @@ -0,0 +1,114 @@ +// 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 backends + +import "context" + +// Database is a generic interface for all backends for accessing databases. +// +// Database object is expected to be stateless and temporary; +// all state should be in the Backend that created that Database instance. +// Handler can create and destroy Database objects on the fly. +// Creating a Database object does not imply the create of the database itself. +// +// Database methods should be thread-safe. +// +// See databaseContract and its methods for additional details. +type Database interface { + Collection(*CollectionParams) Collection + ListCollections(context.Context, *ListCollectionsParams) (*ListCollectionsResult, error) + CreateCollection(context.Context, *CreateCollectionParams) error + DropCollection(context.Context, *DropCollectionParams) error +} + +// DatabaseContract wraps Database and enforces its contract. +// +// All backend implementations should use that function when they create new Database instances. +// The handler should not use that function. +// +// See databaseContract and its methods for additional details. +func DatabaseContract(db Database) Database { + return &databaseContract{ + db: db, + } +} + +// databaseContract implements Database interface. +type databaseContract struct { + db Database +} + +// CollectionParams represents the parameters of Database.Collection method. +type CollectionParams struct{} + +// Collection returns a Collection instance for given parameters. +// +// The collection (or database) does not need to exist; even parameters like name could be invalid. +func (dbc *databaseContract) Collection(params *CollectionParams) Collection { + return dbc.db.Collection(params) +} + +// ListCollectionsParams represents the parameters of Database.ListCollections method. +type ListCollectionsParams struct{} + +// ListCollectionsResult represents the results of Database.ListCollections method. +type ListCollectionsResult struct { + Collections []CollectionInfo +} + +// CollectionInfo represents information about a single collection. +type CollectionInfo struct{} + +// ListCollections returns information about collections in the database. +// +// Database doesn't have to exist; that's not an error. +// +//nolint:lll // for readability +func (dbc *databaseContract) ListCollections(ctx context.Context, params *ListCollectionsParams) (res *ListCollectionsResult, err error) { + defer checkError(err) + res, err = dbc.db.ListCollections(ctx, params) + return +} + +// CreateCollectionParams represents the parameters of Database.CreateCollection method. +type CreateCollectionParams struct { + Name string +} + +// CreateCollection creates a new collection in the database; it should not already exist. +// +// Database may or may not exist; it should be created automatically if needed. +func (dbc *databaseContract) CreateCollection(ctx context.Context, params *CreateCollectionParams) (err error) { + defer checkError(err, ErrCollectionAlreadyExists, ErrCollectionNameIsInvalid) + err = dbc.db.CreateCollection(ctx, params) + return +} + +// DropCollectionParams represents the parameters of Database.DropCollection method. +type DropCollectionParams struct { + Name string +} + +// DropCollection drops existing collection in the database. +func (dbc *databaseContract) DropCollection(ctx context.Context, params *DropCollectionParams) (err error) { + defer checkError(err, ErrCollectionDoesNotExist) + err = dbc.db.DropCollection(ctx, params) + return +} + +// check interfaces +var ( + _ Database = (*databaseContract)(nil) +) diff --git a/internal/backend/sqlite/sqlite.go b/internal/backends/doc.go similarity index 54% rename from internal/backend/sqlite/sqlite.go rename to internal/backends/doc.go index d0a3ca049dd8..c0988a550978 100644 --- a/internal/backend/sqlite/sqlite.go +++ b/internal/backends/doc.go @@ -12,4 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -package sqlite +// Package backends provides common interfaces and code for all backend implementations. +// +// # Design principles. +// +// 1. Contexts are per-operation and should not be stored. +// 2. Backend is stateful. Database and Collection are stateless. +// 3. Returned errors could be nil, *Error or any other error type. +// *Error codes are enforced by contracts; +// they are not documented in the code comments, but are visible in the contract's code (to avoid duplication). +package backends diff --git a/internal/backend/error.go b/internal/backends/error.go similarity index 67% rename from internal/backend/error.go rename to internal/backends/error.go index b5c0c710fc5d..ba862aa59634 100644 --- a/internal/backend/error.go +++ b/internal/backends/error.go @@ -12,10 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package backend +package backends import ( "errors" + "fmt" "golang.org/x/exp/slices" @@ -31,15 +32,37 @@ const ( ErrCollectionDoesNotExist // collection does not exist ErrCollectionAlreadyExists // collection already exists + ErrCollectionNameIsInvalid // collection name is invalid ) type Error struct { - Code ErrorCode + code ErrorCode err error } +// NewError creates a new backend error wrapping another error. +func NewError(code ErrorCode, err error) *Error { + if code == 0 { + panic("backends.NewError: code must not be 0") + } + + // TODO we might allow nil error if needed + if err == nil { + panic("backends.NewError: err must not be nil") + } + + return &Error{ + code: code, + err: err, + } +} + +func (err *Error) Code() ErrorCode { + return err.code +} + func (err *Error) Error() string { - return err.err.Error() + return fmt.Sprintf("backends.Error: %v", err.err) } func (err *Error) Unwrap() error { @@ -57,14 +80,18 @@ func checkError(err error, codes ...ErrorCode) { var e *Error if !errors.As(err, &e) { + return + } + + if e.code == 0 { panic(err) } - if e.Code == 0 { + if len(codes) == 0 { panic(err) } - if !slices.Contains(codes, e.Code) { + if !slices.Contains(codes, e.code) { panic(err) } } diff --git a/internal/backend/errorcode_string.go b/internal/backends/errorcode_string.go similarity index 82% rename from internal/backend/errorcode_string.go rename to internal/backends/errorcode_string.go index 2311cc8f1c45..6498e896431b 100644 --- a/internal/backend/errorcode_string.go +++ b/internal/backends/errorcode_string.go @@ -1,6 +1,6 @@ // Code generated by "stringer -linecomment -type ErrorCode"; DO NOT EDIT. -package backend +package backends import "strconv" @@ -10,11 +10,12 @@ func _() { var x [1]struct{} _ = x[ErrCollectionDoesNotExist-1] _ = x[ErrCollectionAlreadyExists-2] + _ = x[ErrCollectionNameIsInvalid-3] } -const _ErrorCode_name = "collection does not existcollection already exists" +const _ErrorCode_name = "collection does not existcollection already existscollection name is invalid" -var _ErrorCode_index = [...]uint8{0, 25, 50} +var _ErrorCode_index = [...]uint8{0, 25, 50, 76} func (i ErrorCode) String() string { i -= 1 diff --git a/internal/backends/sqlite/backend.go b/internal/backends/sqlite/backend.go new file mode 100644 index 000000000000..a984744a9958 --- /dev/null +++ b/internal/backends/sqlite/backend.go @@ -0,0 +1,40 @@ +// 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 sqlite + +import ( + "github.com/FerretDB/FerretDB/internal/backends" +) + +type backend struct{} + +func NewBackend() backends.Backend { + return backends.BackendContract(&backend{}) +} + +// Database implements backends.Backend interface. +func (b *backend) Database(params *backends.DatabaseParams) backends.Database { + return newDatabase(b) +} + +// ListDatabases implements backends.Backend interface. +func (b *backend) ListDatabases(params *backends.ListDatabasesParams) (res *backends.ListDatabasesResult, err error) { + panic("not implemented") // TODO: Implement +} + +// check interfaces +var ( + _ backends.Backend = (*backend)(nil) +) diff --git a/internal/backend/backend.go b/internal/backends/sqlite/collection.go similarity index 55% rename from internal/backend/backend.go rename to internal/backends/sqlite/collection.go index 11c9fad37e07..2db631540ab0 100644 --- a/internal/backend/backend.go +++ b/internal/backends/sqlite/collection.go @@ -12,29 +12,30 @@ // See the License for the specific language governing permissions and // limitations under the License. -package backend +package sqlite -type Backend interface { - Database(params *DatabaseParams) Database -} +import ( + "context" -func BackendContract(b Backend) Backend { - return &backendContract{ - b: b, - } -} + "github.com/FerretDB/FerretDB/internal/backends" +) -type backendContract struct { - b Backend +type collection struct { + db *database } -type DatabaseParams struct{} +func newCollection(db *database) backends.Collection { + return backends.CollectionContract(&collection{ + db: db, + }) +} -func (bc *backendContract) Database(params *DatabaseParams) Database { - return bc.b.Database(params) +// Insert implements backends.Collection interface. +func (c *collection) Insert(ctx context.Context, params *backends.InsertParams) (*backends.InsertResult, error) { + panic("not implemented") // TODO: Implement } // check interfaces var ( - _ Backend = (*backendContract)(nil) + _ backends.Collection = (*collection)(nil) ) diff --git a/internal/backends/sqlite/database.go b/internal/backends/sqlite/database.go new file mode 100644 index 000000000000..00276b4eeaf9 --- /dev/null +++ b/internal/backends/sqlite/database.go @@ -0,0 +1,57 @@ +// 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 sqlite + +import ( + "context" + + "github.com/FerretDB/FerretDB/internal/backends" +) + +type database struct { + b *backend +} + +func newDatabase(b *backend) backends.Database { + return backends.DatabaseContract(&database{ + b: b, + }) +} + +func (db *database) Collection(params *backends.CollectionParams) backends.Collection { + return newCollection(db) +} + +// ListCollections implements backends.Database interface. +// +//nolint:lll // for readability +func (db *database) ListCollections(ctx context.Context, params *backends.ListCollectionsParams) (*backends.ListCollectionsResult, error) { + panic("not implemented") // TODO: Implement +} + +// CreateCollection implements backends.Database interface. +func (db *database) CreateCollection(ctx context.Context, params *backends.CreateCollectionParams) error { + panic("not implemented") // TODO: Implement +} + +// DropCollection implements backends.Database interface. +func (db *database) DropCollection(ctx context.Context, params *backends.DropCollectionParams) error { + panic("not implemented") // TODO: Implement +} + +// check interfaces +var ( + _ backends.Database = (*database)(nil) +) From 3c62db0a560c65d7cd7a4cdcf07bc91506e90237 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Mon, 15 May 2023 19:08:48 +0400 Subject: [PATCH 04/11] More methods --- internal/backends/backend.go | 18 ++++++++++++++++++ internal/backends/sqlite/backend.go | 10 +++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/internal/backends/backend.go b/internal/backends/backend.go index 75bf349dba50..9818d5945fc5 100644 --- a/internal/backends/backend.go +++ b/internal/backends/backend.go @@ -26,6 +26,8 @@ package backends type Backend interface { Database(*DatabaseParams) Database ListDatabases(*ListDatabasesParams) (*ListDatabasesResult, error) + CreateDatabase(*CreateDatabaseParams) error + DropDatabase(*DropDatabaseParams) error } // BackendContract wraps Backend and enforces its contract. @@ -67,6 +69,22 @@ func (bc *backendContract) ListDatabases(params *ListDatabasesParams) (res *List return } +type CreateDatabaseParams struct{} + +func (bc *backendContract) CreateDatabase(params *CreateDatabaseParams) (err error) { + defer checkError(err) + err = bc.b.CreateDatabase(params) + return +} + +type DropDatabaseParams struct{} + +func (bc *backendContract) DropDatabase(params *DropDatabaseParams) (err error) { + defer checkError(err) + err = bc.b.DropDatabase(params) + return +} + // check interfaces var ( _ Backend = (*backendContract)(nil) diff --git a/internal/backends/sqlite/backend.go b/internal/backends/sqlite/backend.go index a984744a9958..1be135089f37 100644 --- a/internal/backends/sqlite/backend.go +++ b/internal/backends/sqlite/backend.go @@ -30,7 +30,15 @@ func (b *backend) Database(params *backends.DatabaseParams) backends.Database { } // ListDatabases implements backends.Backend interface. -func (b *backend) ListDatabases(params *backends.ListDatabasesParams) (res *backends.ListDatabasesResult, err error) { +func (b *backend) ListDatabases(params *backends.ListDatabasesParams) (*backends.ListDatabasesResult, error) { + panic("not implemented") // TODO: Implement +} + +func (b *backend) CreateDatabase(params *backends.CreateDatabaseParams) error { + panic("not implemented") // TODO: Implement +} + +func (b *backend) DropDatabase(params *backends.DropDatabaseParams) error { panic("not implemented") // TODO: Implement } From 1db5589853c6d89f1bf362c1f0c594fd1e225224 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Tue, 16 May 2023 16:29:53 +0400 Subject: [PATCH 05/11] Tweak formatting --- internal/handlers/sjson/sjson.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/internal/handlers/sjson/sjson.go b/internal/handlers/sjson/sjson.go index 67fbabd76b4f..36b793822d67 100644 --- a/internal/handlers/sjson/sjson.go +++ b/internal/handlers/sjson/sjson.go @@ -37,28 +37,28 @@ // // Composite types // -// Alias types package sjson package sjson schema JSON representation +// Alias types package sjson package sjson schema JSON representation // // object *types.Document *sjson.documentType {"t":"object", "$s": {"$k":[], "p":{}} JSON object -// array *types.Array *sjson.arrayType {"t":"array", "i": [, ]} JSON array +// array *types.Array *sjson.arrayType {"t":"array", "i": [, ]} JSON array // // Scalar types // -// Alias types package sjson package sjson schema JSON representation +// Alias types package sjson package sjson schema JSON representation // -// double float64 *sjson.doubleType {"t":"double"} JSON number -// string string *sjson.stringType {"t":"string"} JSON string -// binData types.Binary *sjson.binaryType {"t":"binData", -// "s":} "" -// objectId types.ObjectID *sjson.objectIDType {"t":"objectId"} "" -// bool bool *sjson.boolType {"t":"bool"} JSON true / false values -// date time.Time *sjson.dateTimeType {"t":"date"} milliseconds since epoch as JSON number -// null types.NullType *sjson.nullType {"t":"null"} JSON null -// regex types.Regex *sjson.regexType {"t":"regex", -// "o": ""} "" -// int int32 *sjson.int32Type {"t":"int"} JSON number -// timestamp types.Timestamp *sjson.timestampType {"t":"timestamp"} JSON number -// long int64 *sjson.int64Type {"t":"long"} JSON number +// double float64 *sjson.doubleType {"t":"double"} JSON number +// string string *sjson.stringType {"t":"string"} JSON string +// binData types.Binary *sjson.binaryType {"t":"binData", +// "s":} "" +// objectId types.ObjectID *sjson.objectIDType {"t":"objectId"} "" +// bool bool *sjson.boolType {"t":"bool"} JSON true / false values +// date time.Time *sjson.dateTimeType {"t":"date"} milliseconds since epoch as JSON number +// null types.NullType *sjson.nullType {"t":"null"} JSON null +// regex types.Regex *sjson.regexType {"t":"regex", +// "o": ""} "" +// int int32 *sjson.int32Type {"t":"int"} JSON number +// timestamp types.Timestamp *sjson.timestampType {"t":"timestamp"} JSON number +// long int64 *sjson.int64Type {"t":"long"} JSON number // //nolint:lll // for readability //nolint:dupword // false positive From ffe7a0be1897df48173613742cf18618d1ae21ca Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Tue, 16 May 2023 17:15:21 +0400 Subject: [PATCH 06/11] Add dummy tests --- internal/backends/backends_test.go | 21 +++++++++++++++++++++ internal/backends/sqlite/sqlite_test.go | 21 +++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 internal/backends/backends_test.go create mode 100644 internal/backends/sqlite/sqlite_test.go diff --git a/internal/backends/backends_test.go b/internal/backends/backends_test.go new file mode 100644 index 000000000000..c809d01dc11e --- /dev/null +++ b/internal/backends/backends_test.go @@ -0,0 +1,21 @@ +// 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 backends + +import "testing" + +func TestDummy(t *testing.T) { + // we need at least one test per package to correctly calculate coverage +} diff --git a/internal/backends/sqlite/sqlite_test.go b/internal/backends/sqlite/sqlite_test.go new file mode 100644 index 000000000000..94c2c9d3af10 --- /dev/null +++ b/internal/backends/sqlite/sqlite_test.go @@ -0,0 +1,21 @@ +// 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 sqlite + +import "testing" + +func TestDummy(t *testing.T) { + // we need at least one test per package to correctly calculate coverage +} From 0c52498af8816fff403649a6a0e176370bd1bb12 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Tue, 16 May 2023 17:15:28 +0400 Subject: [PATCH 07/11] Update code owners --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 45a0cedbe89b..76b72a687917 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,6 +8,7 @@ /integration/ @FerretDB/leads /internal/ @FerretDB/leads +/internal/backends/ @AlekSi /internal/handlers/hana/ @AlekSi # @AlekSi for engineering, @ptrfarkas for marketing. From 5b9353ba197693fe3aca6fddafb7d916a6c67de3 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Tue, 16 May 2023 19:05:45 +0400 Subject: [PATCH 08/11] More --- internal/backends/backend.go | 47 ++++++++++++++++++----------- internal/backends/database.go | 4 ++- internal/backends/sqlite/backend.go | 12 +++----- 3 files changed, 37 insertions(+), 26 deletions(-) diff --git a/internal/backends/backend.go b/internal/backends/backend.go index 9818d5945fc5..1d5f321e8dee 100644 --- a/internal/backends/backend.go +++ b/internal/backends/backend.go @@ -14,6 +14,8 @@ package backends +import "context" + // Backends is a generic interface for all backends for accessing them. // // Backend object is expected to be stateful and wrap database connection(s). @@ -24,10 +26,11 @@ package backends // // See backendContract and its methods for additional details. type Backend interface { - Database(*DatabaseParams) Database - ListDatabases(*ListDatabasesParams) (*ListDatabasesResult, error) - CreateDatabase(*CreateDatabaseParams) error - DropDatabase(*DropDatabaseParams) error + Database(context.Context, *DatabaseParams) Database + ListDatabases(context.Context, *ListDatabasesParams) (*ListDatabasesResult, error) + DropDatabase(context.Context, *DropDatabaseParams) error + + // There is no interface method to create a database; see package documentation. } // BackendContract wraps Backend and enforces its contract. @@ -55,33 +58,41 @@ type DatabaseParams struct { // Database returns a Database instance for given parameters. // // The database does not need to exist; even parameters like name could be invalid. -func (bc *backendContract) Database(params *DatabaseParams) Database { - return bc.b.Database(params) +func (bc *backendContract) Database(ctx context.Context, params *DatabaseParams) Database { + return bc.b.Database(ctx, params) } +// ListDatabasesParams represents the parameters of Backend.ListDatabases method. type ListDatabasesParams struct{} -type ListDatabasesResult struct{} - -func (bc *backendContract) ListDatabases(params *ListDatabasesParams) (res *ListDatabasesResult, err error) { - defer checkError(err) - res, err = bc.b.ListDatabases(params) - return +// ListCollectionsResult represents the results of Database.ListCollections method. +type ListDatabasesResult struct { + Databases []DatabaseInfo } -type CreateDatabaseParams struct{} +// DatabaseInfo represents information about a single database. +type DatabaseInfo struct { + Name string +} -func (bc *backendContract) CreateDatabase(params *CreateDatabaseParams) (err error) { +// ListDatabases returns a Database instance for given parameters. +func (bc *backendContract) ListDatabases(ctx context.Context, params *ListDatabasesParams) (res *ListDatabasesResult, err error) { defer checkError(err) - err = bc.b.CreateDatabase(params) + res, err = bc.b.ListDatabases(ctx, params) return } -type DropDatabaseParams struct{} +// DropDatabaseParams represents the parameters of Backend.DropDatabase method. +type DropDatabaseParams struct { + Name string +} -func (bc *backendContract) DropDatabase(params *DropDatabaseParams) (err error) { +// DropDatabase drops database with given parameters. +// +// Database doesn't have to exist; that's not an error. +func (bc *backendContract) DropDatabase(ctx context.Context, params *DropDatabaseParams) (err error) { defer checkError(err) - err = bc.b.DropDatabase(params) + err = bc.b.DropDatabase(ctx, params) return } diff --git a/internal/backends/database.go b/internal/backends/database.go index 592cf79f7665..2a1a180e9c94 100644 --- a/internal/backends/database.go +++ b/internal/backends/database.go @@ -69,7 +69,9 @@ type ListCollectionsResult struct { } // CollectionInfo represents information about a single collection. -type CollectionInfo struct{} +type CollectionInfo struct { + Name string +} // ListCollections returns information about collections in the database. // diff --git a/internal/backends/sqlite/backend.go b/internal/backends/sqlite/backend.go index 1be135089f37..7270b6aa1792 100644 --- a/internal/backends/sqlite/backend.go +++ b/internal/backends/sqlite/backend.go @@ -15,6 +15,8 @@ package sqlite import ( + "context" + "github.com/FerretDB/FerretDB/internal/backends" ) @@ -25,20 +27,16 @@ func NewBackend() backends.Backend { } // Database implements backends.Backend interface. -func (b *backend) Database(params *backends.DatabaseParams) backends.Database { +func (b *backend) Database(ctx context.Context, params *backends.DatabaseParams) backends.Database { return newDatabase(b) } // ListDatabases implements backends.Backend interface. -func (b *backend) ListDatabases(params *backends.ListDatabasesParams) (*backends.ListDatabasesResult, error) { - panic("not implemented") // TODO: Implement -} - -func (b *backend) CreateDatabase(params *backends.CreateDatabaseParams) error { +func (b *backend) ListDatabases(ctx context.Context, params *backends.ListDatabasesParams) (*backends.ListDatabasesResult, error) { panic("not implemented") // TODO: Implement } -func (b *backend) DropDatabase(params *backends.DropDatabaseParams) error { +func (b *backend) DropDatabase(ctx context.Context, params *backends.DropDatabaseParams) error { panic("not implemented") // TODO: Implement } From a51cfa830e0dc2d09d06993bd582b9b845bac371 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Tue, 16 May 2023 20:51:09 +0400 Subject: [PATCH 09/11] More work --- build/docker/all-in-one.Dockerfile | 3 +- build/docker/development.Dockerfile | 3 +- build/docker/production.Dockerfile | 3 +- internal/backends/database.go | 4 +- internal/backends/error.go | 40 +++++++++++------ internal/backends/error_test.go | 44 +++++++++++++++++++ internal/backends/errorcode_string.go | 12 ++--- internal/backends/sqlite/backend.go | 2 + .../{backends_test.go => sqlite/sqlite.go} | 9 +--- internal/handlers/registry/sqlite.go | 2 - internal/handlers/sqlite/sqlite.go | 3 ++ 11 files changed, 91 insertions(+), 34 deletions(-) create mode 100644 internal/backends/error_test.go rename internal/backends/{backends_test.go => sqlite/sqlite.go} (80%) diff --git a/build/docker/all-in-one.Dockerfile b/build/docker/all-in-one.Dockerfile index f071ace57a76..325cc0635251 100644 --- a/build/docker/all-in-one.Dockerfile +++ b/build/docker/all-in-one.Dockerfile @@ -42,7 +42,8 @@ ENV GOCOVERDIR=cover ENV GORACE=halt_on_error=1,history_size=2 ENV GOARM=7 -# do not raise it without providing a v1 build because v2+ is problematic for some virtualization platforms +# do not raise it without providing a v1 build because v2+ is problematic +# for some virtualization platforms and older hardware ENV GOAMD64=v1 # TODO https://github.com/FerretDB/FerretDB/issues/2170 diff --git a/build/docker/development.Dockerfile b/build/docker/development.Dockerfile index fd71c62b638c..8576b604ff2f 100644 --- a/build/docker/development.Dockerfile +++ b/build/docker/development.Dockerfile @@ -42,7 +42,8 @@ ENV GOCOVERDIR=cover ENV GORACE=halt_on_error=1,history_size=2 ENV GOARM=7 -# do not raise it without providing a v1 build because v2+ is problematic for some virtualization platforms +# do not raise it without providing a v1 build because v2+ is problematic +# for some virtualization platforms and older hardware ENV GOAMD64=v1 # TODO https://github.com/FerretDB/FerretDB/issues/2170 diff --git a/build/docker/production.Dockerfile b/build/docker/production.Dockerfile index aafc29229e7b..2da92051e5ef 100644 --- a/build/docker/production.Dockerfile +++ b/build/docker/production.Dockerfile @@ -38,7 +38,8 @@ ENV GOPROXY https://proxy.golang.org ENV CGO_ENABLED=0 ENV GOARM=7 -# do not raise it without providing a v1 build because v2+ is problematic for some virtualization platforms +# do not raise it without providing a v1 build because v2+ is problematic +# for some virtualization platforms and older hardware ENV GOAMD64=v1 # TODO https://github.com/FerretDB/FerretDB/issues/2170 diff --git a/internal/backends/database.go b/internal/backends/database.go index 2a1a180e9c94..85436ff2e462 100644 --- a/internal/backends/database.go +++ b/internal/backends/database.go @@ -93,7 +93,7 @@ type CreateCollectionParams struct { // // Database may or may not exist; it should be created automatically if needed. func (dbc *databaseContract) CreateCollection(ctx context.Context, params *CreateCollectionParams) (err error) { - defer checkError(err, ErrCollectionAlreadyExists, ErrCollectionNameIsInvalid) + defer checkError(err, ErrorCodeCollectionAlreadyExists, ErrorCodeCollectionNameIsInvalid) err = dbc.db.CreateCollection(ctx, params) return } @@ -105,7 +105,7 @@ type DropCollectionParams struct { // DropCollection drops existing collection in the database. func (dbc *databaseContract) DropCollection(ctx context.Context, params *DropCollectionParams) (err error) { - defer checkError(err, ErrCollectionDoesNotExist) + defer checkError(err, ErrorCodeCollectionDoesNotExist) err = dbc.db.DropCollection(ctx, params) return } diff --git a/internal/backends/error.go b/internal/backends/error.go index ba862aa59634..09e662c309c7 100644 --- a/internal/backends/error.go +++ b/internal/backends/error.go @@ -23,24 +23,29 @@ import ( "github.com/FerretDB/FerretDB/internal/util/debugbuild" ) -//go:generate ../../bin/stringer -linecomment -type ErrorCode +//go:generate ../../bin/stringer -type ErrorCode +// ErrorCode represent a backend error code. type ErrorCode int +// Error codes. const ( _ ErrorCode = iota - ErrCollectionDoesNotExist // collection does not exist - ErrCollectionAlreadyExists // collection already exists - ErrCollectionNameIsInvalid // collection name is invalid + ErrorCodeCollectionDoesNotExist + ErrorCodeCollectionAlreadyExists + ErrorCodeCollectionNameIsInvalid ) +// Error represents a backend error returned by all Backend, Database and Collection methods. type Error struct { + // this internal error can't be accessed by the caller + err error + code ErrorCode - err error } -// NewError creates a new backend error wrapping another error. +// NewError creates a new backend error. func NewError(code ErrorCode, err error) *Error { if code == 0 { panic("backends.NewError: code must not be 0") @@ -57,18 +62,25 @@ func NewError(code ErrorCode, err error) *Error { } } +// Code returns the error code. func (err *Error) Code() ErrorCode { return err.code } -func (err *Error) Error() string { - return fmt.Sprintf("backends.Error: %v", err.err) -} +// There is intentionally no method to return the internal error. -func (err *Error) Unwrap() error { - return err.err +// Error implements error interface. +func (err *Error) Error() string { + return fmt.Sprintf("%s: %v", err.code, err.err) } +// checkError enforces backend interfaces contracts. +// +// Err must be nil, *Error, or some other opaque error. +// If err is *Error, it must have one of the given error codes. +// If that's not the case, checkError panics in debug builds. +// +// It does nothing in non-debug builds. func checkError(err error, codes ...ErrorCode) { if !debugbuild.Enabled { return @@ -84,15 +96,15 @@ func checkError(err error, codes ...ErrorCode) { } if e.code == 0 { - panic(err) + panic(fmt.Sprintf("error code is 0: %v", err)) } if len(codes) == 0 { - panic(err) + panic(fmt.Sprintf("no allowed error codes: %v", err)) } if !slices.Contains(codes, e.code) { - panic(err) + panic(fmt.Sprintf("error code is not in %v: %v", codes, err)) } } diff --git a/internal/backends/error_test.go b/internal/backends/error_test.go new file mode 100644 index 000000000000..4c9175674e96 --- /dev/null +++ b/internal/backends/error_test.go @@ -0,0 +1,44 @@ +// 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 backends + +import ( + "io" + "io/fs" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestError(t *testing.T) { + t.Parallel() + + pe := &fs.PathError{ + Op: "open", + Path: "database.db", + Err: io.EOF, + } + err := NewError(ErrorCodeCollectionDoesNotExist, pe) + + assert.NotErrorIs(t, err, pe, "internal error should be hidden") + assert.NotErrorIs(t, err, io.EOF, "internal error should be hidden") + + var e *Error + assert.ErrorAs(t, err, &e) + assert.Equal(t, ErrorCodeCollectionDoesNotExist, e.code) + assert.Equal(t, pe, e.err) + + assert.Equal(t, `ErrorCodeCollectionDoesNotExist: open database.db: EOF`, err.Error()) +} diff --git a/internal/backends/errorcode_string.go b/internal/backends/errorcode_string.go index 6498e896431b..625214e13986 100644 --- a/internal/backends/errorcode_string.go +++ b/internal/backends/errorcode_string.go @@ -1,4 +1,4 @@ -// Code generated by "stringer -linecomment -type ErrorCode"; DO NOT EDIT. +// Code generated by "stringer -type ErrorCode"; DO NOT EDIT. package backends @@ -8,14 +8,14 @@ func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} - _ = x[ErrCollectionDoesNotExist-1] - _ = x[ErrCollectionAlreadyExists-2] - _ = x[ErrCollectionNameIsInvalid-3] + _ = x[ErrorCodeCollectionDoesNotExist-1] + _ = x[ErrorCodeCollectionAlreadyExists-2] + _ = x[ErrorCodeCollectionNameIsInvalid-3] } -const _ErrorCode_name = "collection does not existcollection already existscollection name is invalid" +const _ErrorCode_name = "ErrorCodeCollectionDoesNotExistErrorCodeCollectionAlreadyExistsErrorCodeCollectionNameIsInvalid" -var _ErrorCode_index = [...]uint8{0, 25, 50, 76} +var _ErrorCode_index = [...]uint8{0, 31, 63, 95} func (i ErrorCode) String() string { i -= 1 diff --git a/internal/backends/sqlite/backend.go b/internal/backends/sqlite/backend.go index 7270b6aa1792..0ebc7e0a3a92 100644 --- a/internal/backends/sqlite/backend.go +++ b/internal/backends/sqlite/backend.go @@ -17,6 +17,8 @@ package sqlite import ( "context" + _ "modernc.org/sqlite" + "github.com/FerretDB/FerretDB/internal/backends" ) diff --git a/internal/backends/backends_test.go b/internal/backends/sqlite/sqlite.go similarity index 80% rename from internal/backends/backends_test.go rename to internal/backends/sqlite/sqlite.go index c809d01dc11e..30a0e41707c5 100644 --- a/internal/backends/backends_test.go +++ b/internal/backends/sqlite/sqlite.go @@ -12,10 +12,5 @@ // See the License for the specific language governing permissions and // limitations under the License. -package backends - -import "testing" - -func TestDummy(t *testing.T) { - // we need at least one test per package to correctly calculate coverage -} +// Package sqlite provides SQLite backend. +package sqlite diff --git a/internal/handlers/registry/sqlite.go b/internal/handlers/registry/sqlite.go index 4fbe65bbe0e3..6af5bdf7af82 100644 --- a/internal/handlers/registry/sqlite.go +++ b/internal/handlers/registry/sqlite.go @@ -15,8 +15,6 @@ package registry import ( - _ "modernc.org/sqlite" // TODO move this to backend package - "github.com/FerretDB/FerretDB/internal/handlers" "github.com/FerretDB/FerretDB/internal/handlers/sqlite" ) diff --git a/internal/handlers/sqlite/sqlite.go b/internal/handlers/sqlite/sqlite.go index b451dfada372..b7a7b0ee5dd5 100644 --- a/internal/handlers/sqlite/sqlite.go +++ b/internal/handlers/sqlite/sqlite.go @@ -20,6 +20,7 @@ package sqlite import ( "go.uber.org/zap" + "github.com/FerretDB/FerretDB/internal/backends/sqlite" "github.com/FerretDB/FerretDB/internal/handlers" "github.com/FerretDB/FerretDB/internal/handlers/commonerrors" ) @@ -49,6 +50,8 @@ type Handler struct { // New returns a new handler. func New(l *zap.Logger) (handlers.Interface, error) { + _ = sqlite.NewBackend() + return &Handler{ L: l, }, nil From 5406717e8b932a77934ddc41edf679041c5a2c42 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Tue, 16 May 2023 22:43:18 +0400 Subject: [PATCH 10/11] More --- internal/backends/backend.go | 6 ++++-- internal/backends/collection.go | 25 ++++++++++++++++++++++++- internal/backends/database.go | 7 +++++-- internal/backends/sqlite/backend.go | 6 +++++- internal/backends/sqlite/database.go | 1 + 5 files changed, 39 insertions(+), 6 deletions(-) diff --git a/internal/backends/backend.go b/internal/backends/backend.go index 1d5f321e8dee..60359d65e8c8 100644 --- a/internal/backends/backend.go +++ b/internal/backends/backend.go @@ -16,7 +16,7 @@ package backends import "context" -// Backends is a generic interface for all backends for accessing them. +// Backend is a generic interface for all backends for accessing them. // // Backend object is expected to be stateful and wrap database connection(s). // Handler can create one Backend or multiple Backends with different authentication credentials. @@ -65,7 +65,7 @@ func (bc *backendContract) Database(ctx context.Context, params *DatabaseParams) // ListDatabasesParams represents the parameters of Backend.ListDatabases method. type ListDatabasesParams struct{} -// ListCollectionsResult represents the results of Database.ListCollections method. +// ListDatabasesResult represents the results of Backend.ListDatabases method. type ListDatabasesResult struct { Databases []DatabaseInfo } @@ -79,6 +79,7 @@ type DatabaseInfo struct { func (bc *backendContract) ListDatabases(ctx context.Context, params *ListDatabasesParams) (res *ListDatabasesResult, err error) { defer checkError(err) res, err = bc.b.ListDatabases(ctx, params) + return } @@ -93,6 +94,7 @@ type DropDatabaseParams struct { func (bc *backendContract) DropDatabase(ctx context.Context, params *DropDatabaseParams) (err error) { defer checkError(err) err = bc.b.DropDatabase(ctx, params) + return } diff --git a/internal/backends/collection.go b/internal/backends/collection.go index 2d075b1bf383..99737bf989e6 100644 --- a/internal/backends/collection.go +++ b/internal/backends/collection.go @@ -20,30 +20,53 @@ import ( "github.com/FerretDB/FerretDB/internal/types" ) +// Collection is a generic interface for all backends for accessing collection. +// +// Collection object is expected to be stateless and temporary; +// all state should be in the Backend that created Database instance that created this Collection instance. +// Handler can create and destroy Collection objects on the fly. +// Creating a Collection object does not imply the creation of the database or collection. +// +// Collection methods should be thread-safe. +// +// See collectionContract and its methods for additional details. type Collection interface { Insert(context.Context, *InsertParams) (*InsertResult, error) } +// CollectionContract wraps Collection and enforces its contract. +// +// All backend implementations should use that function when they create new Collection instances. +// The handler should not use that function. +// +// See collectionContract and its methods for additional details. func CollectionContract(c Collection) Collection { return &collectionContract{ c: c, } } +// collectionContract implements Collection interface. type collectionContract struct { c Collection } +// InsertParams represents the parameters of Collection.Insert method. type InsertParams struct { Docs types.DocumentsIterator Ordered bool } +// InsertResult represents the results of Collection.Insert method. type InsertResult struct{} +// Insert inserts documents into the collection. +// +// Both database and collection may or may not exist; they should be created automatically if needed. func (cc *collectionContract) Insert(ctx context.Context, params *InsertParams) (res *InsertResult, err error) { - // defer checkError(err, ErrCollectionDoesNotExist) + defer checkError(err) res, err = cc.c.Insert(ctx, params) + return } diff --git a/internal/backends/database.go b/internal/backends/database.go index 85436ff2e462..cb5cd103acc2 100644 --- a/internal/backends/database.go +++ b/internal/backends/database.go @@ -19,9 +19,9 @@ import "context" // Database is a generic interface for all backends for accessing databases. // // Database object is expected to be stateless and temporary; -// all state should be in the Backend that created that Database instance. +// all state should be in the Backend that created this Database instance. // Handler can create and destroy Database objects on the fly. -// Creating a Database object does not imply the create of the database itself. +// Creating a Database object does not imply the creating of the database itself. // // Database methods should be thread-safe. // @@ -81,6 +81,7 @@ type CollectionInfo struct { func (dbc *databaseContract) ListCollections(ctx context.Context, params *ListCollectionsParams) (res *ListCollectionsResult, err error) { defer checkError(err) res, err = dbc.db.ListCollections(ctx, params) + return } @@ -95,6 +96,7 @@ type CreateCollectionParams struct { func (dbc *databaseContract) CreateCollection(ctx context.Context, params *CreateCollectionParams) (err error) { defer checkError(err, ErrorCodeCollectionAlreadyExists, ErrorCodeCollectionNameIsInvalid) err = dbc.db.CreateCollection(ctx, params) + return } @@ -107,6 +109,7 @@ type DropCollectionParams struct { func (dbc *databaseContract) DropCollection(ctx context.Context, params *DropCollectionParams) (err error) { defer checkError(err, ErrorCodeCollectionDoesNotExist) err = dbc.db.DropCollection(ctx, params) + return } diff --git a/internal/backends/sqlite/backend.go b/internal/backends/sqlite/backend.go index 0ebc7e0a3a92..71193e6f060b 100644 --- a/internal/backends/sqlite/backend.go +++ b/internal/backends/sqlite/backend.go @@ -24,8 +24,9 @@ import ( type backend struct{} +// NewBackend creates a new SQLite backend. func NewBackend() backends.Backend { - return backends.BackendContract(&backend{}) + return backends.BackendContract(new(backend)) } // Database implements backends.Backend interface. @@ -34,10 +35,13 @@ func (b *backend) Database(ctx context.Context, params *backends.DatabaseParams) } // ListDatabases implements backends.Backend interface. +// +//nolint:lll // for readability func (b *backend) ListDatabases(ctx context.Context, params *backends.ListDatabasesParams) (*backends.ListDatabasesResult, error) { panic("not implemented") // TODO: Implement } +// DropDatabase implements backends.Backend interface. func (b *backend) DropDatabase(ctx context.Context, params *backends.DropDatabaseParams) error { panic("not implemented") // TODO: Implement } diff --git a/internal/backends/sqlite/database.go b/internal/backends/sqlite/database.go index 00276b4eeaf9..d54b40f3b150 100644 --- a/internal/backends/sqlite/database.go +++ b/internal/backends/sqlite/database.go @@ -30,6 +30,7 @@ func newDatabase(b *backend) backends.Database { }) } +// Collection implements backends.Database interface. func (db *database) Collection(params *backends.CollectionParams) backends.Collection { return newCollection(db) } From 045759fd6954a5c0f18d57eb786fe3db6e000366 Mon Sep 17 00:00:00 2001 From: Alexey Palazhchenko Date: Tue, 16 May 2023 22:51:34 +0400 Subject: [PATCH 11/11] More --- internal/backends/doc.go | 19 ++++++++++++++----- internal/backends/sqlite/backend.go | 1 + internal/backends/sqlite/collection.go | 2 ++ internal/backends/sqlite/database.go | 2 ++ 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/internal/backends/doc.go b/internal/backends/doc.go index c0988a550978..a6506bd7e05c 100644 --- a/internal/backends/doc.go +++ b/internal/backends/doc.go @@ -16,9 +16,18 @@ // // # Design principles. // -// 1. Contexts are per-operation and should not be stored. -// 2. Backend is stateful. Database and Collection are stateless. -// 3. Returned errors could be nil, *Error or any other error type. -// *Error codes are enforced by contracts; -// they are not documented in the code comments, but are visible in the contract's code (to avoid duplication). +// 1. Interfaces are relatively high-level and "fat". +// We are generally doing one backend interface call per handler call. +// For example, `insert` command handler calls only +// `db.Database("database").Collection("collection").Insert(ctx, params)` method that would +// create a database if needed, create a collection if needed, and insert all documents with correct parameters. +// There is no method to insert one document into an existing collection. +// That shifts some complexity from a single handler into multiple backend implementations; +// for example, support for `insert` with `ordered: true` and `ordered: false` should be implemented multiple times. +// But that allows those implementations to be much more effective. +// 2. Backend objects are stateful. Database and Collection objects are stateless. +// 3. Contexts are per-operation and should not be stored. +// 4. Errors returned by methods could be nil, *Error, or some other opaque error type. +// Contracts enforce *Error codes; they are not documented in the code comments +// but are visible in the contract's code (to avoid duplication). package backends diff --git a/internal/backends/sqlite/backend.go b/internal/backends/sqlite/backend.go index 71193e6f060b..51fe21181e65 100644 --- a/internal/backends/sqlite/backend.go +++ b/internal/backends/sqlite/backend.go @@ -22,6 +22,7 @@ import ( "github.com/FerretDB/FerretDB/internal/backends" ) +// backend implements backends.Backend interface. type backend struct{} // NewBackend creates a new SQLite backend. diff --git a/internal/backends/sqlite/collection.go b/internal/backends/sqlite/collection.go index 2db631540ab0..1bc1af9eb3e8 100644 --- a/internal/backends/sqlite/collection.go +++ b/internal/backends/sqlite/collection.go @@ -20,10 +20,12 @@ import ( "github.com/FerretDB/FerretDB/internal/backends" ) +// collection implements backends.Collection interface. type collection struct { db *database } +// newDatabase creates a new Collection. func newCollection(db *database) backends.Collection { return backends.CollectionContract(&collection{ db: db, diff --git a/internal/backends/sqlite/database.go b/internal/backends/sqlite/database.go index d54b40f3b150..c31b6352042f 100644 --- a/internal/backends/sqlite/database.go +++ b/internal/backends/sqlite/database.go @@ -20,10 +20,12 @@ import ( "github.com/FerretDB/FerretDB/internal/backends" ) +// database implements backends.Database interface. type database struct { b *backend } +// newDatabase creates a new Database. func newDatabase(b *backend) backends.Database { return backends.DatabaseContract(&database{ b: b,