diff --git a/internal/backends/doc.go b/internal/backends/backends.go similarity index 95% rename from internal/backends/doc.go rename to internal/backends/backends.go index 48560fcbbc6b..700dbacbc252 100644 --- a/internal/backends/doc.go +++ b/internal/backends/backends.go @@ -43,5 +43,8 @@ // But there are some common tests for all backends that check corner cases in contracts. // They also test that all backends adjusted to contract changes. // +// Some backends may have their own tests. +// +// Both kinds of tests could be removed over time as they are replaced by integration tests. // Prefer integration tests when possible. package backends diff --git a/internal/backends/postgresql/collection_test.go b/internal/backends/postgresql/collection_test.go deleted file mode 100644 index d9bb62671a20..000000000000 --- a/internal/backends/postgresql/collection_test.go +++ /dev/null @@ -1,67 +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 postgresql - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/FerretDB/FerretDB/internal/backends" - "github.com/FerretDB/FerretDB/internal/clientconn/conninfo" - "github.com/FerretDB/FerretDB/internal/types" - "github.com/FerretDB/FerretDB/internal/util/state" - "github.com/FerretDB/FerretDB/internal/util/testutil" -) - -func TestInsertAll(t *testing.T) { - if testing.Short() { - t.Skip("skipping in -short mode") - } - - sp, err := state.NewProvider("") - require.NoError(t, err) - - params := NewBackendParams{ - URI: "postgres://username:password@127.0.0.1:5432/ferretdb?pool_min_conns=1", - L: testutil.Logger(t), - P: sp, - } - b, err := NewBackend(¶ms) - require.NoError(t, err) - - t.Cleanup(b.Close) - - db, err := b.Database(testutil.DatabaseName(t)) - require.NoError(t, err) - - c, err := db.Collection(testutil.CollectionName(t)) - require.NoError(t, err) - - ctx := conninfo.Ctx(testutil.Ctx(t), conninfo.New()) - - doc, err := types.NewDocument("_id", types.NewObjectID()) - require.NoError(t, err) - - _, err = c.InsertAll(ctx, &backends.InsertAllParams{ - Docs: []*types.Document{doc}, - }) - require.NoError(t, err) - // TODO https://github.com/FerretDB/FerretDB/issues/3375 - //_, err = c.InsertAll(ctx, &backends.InsertAllParams{ - // Docs: []*types.Document{doc}, - //}) - //require.True(t, backends.ErrorCodeIs(err, backends.ErrorCodeInsertDuplicateID)) -} diff --git a/internal/backends/postgresql/database_test.go b/internal/backends/postgresql/database_test.go index 3df4aa5ed4f9..e54e19cc1ffc 100644 --- a/internal/backends/postgresql/database_test.go +++ b/internal/backends/postgresql/database_test.go @@ -72,7 +72,7 @@ func TestDatabaseStats(t *testing.T) { require.NotZero(t, res.SizeCollections) require.Zero(t, res.CountObjects) require.Zero(t, res.CountIndexes) - require.Zero(t, res.SizeIndexes) + require.NotZero(t, res.SizeIndexes) // includes metadata table's indexes }) t.Run("DatabaseWithCollections", func(t *testing.T) { diff --git a/internal/backends/postgresql/dummy_test.go b/internal/backends/postgresql/dummy_test.go new file mode 100644 index 000000000000..ab1be0278a87 --- /dev/null +++ b/internal/backends/postgresql/dummy_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 postgresql + +import "testing" + +func TestDummy(t *testing.T) { + // we need at least one test per package to correctly calculate coverage +} diff --git a/internal/backends/postgresql/metadata/indexes.go b/internal/backends/postgresql/metadata/indexes.go new file mode 100644 index 000000000000..246c3e10ce34 --- /dev/null +++ b/internal/backends/postgresql/metadata/indexes.go @@ -0,0 +1,139 @@ +// 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 metadata + +import ( + "errors" + + "golang.org/x/exp/slices" + + "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/iterator" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" + "github.com/FerretDB/FerretDB/internal/util/must" +) + +// Indexes represents information about all indexes in a collection. +type Indexes []IndexInfo + +// IndexInfo represents information about a single index. +type IndexInfo struct { + Name string + PgIndex string + Key []IndexKeyPair + Unique bool +} + +// IndexKeyPair consists of a field name and a sort order that are part of the index. +type IndexKeyPair struct { + Field string + Descending bool +} + +// deepCopy returns a deep copy. +func (indexes Indexes) deepCopy() Indexes { + res := make(Indexes, len(indexes)) + + for i, index := range indexes { + res[i] = IndexInfo{ + Name: index.Name, + PgIndex: index.PgIndex, + Key: slices.Clone(index.Key), + Unique: index.Unique, + } + } + + return res +} + +// marshal returns [*types.Array] for indexes. +func (indexes Indexes) marshal() *types.Array { + res := types.MakeArray(len(indexes)) + + for _, index := range indexes { + key := types.MakeDocument(len(index.Key)) + + for _, pair := range index.Key { + order := int32(1) + if pair.Descending { + order = int32(-1) + } + + key.Set(pair.Field, order) + } + + res.Append(must.NotFail(types.NewDocument( + "pgindex", index.PgIndex, + "name", index.Name, + "key", key, + "unique", index.Unique, + ))) + } + + return res +} + +// unmarshal sets indexes from [*types.Array]. +func (s *Indexes) unmarshal(a *types.Array) error { + res := make(Indexes, a.Len()) + + iter := a.Iterator() + defer iter.Close() + + for { + i, v, err := iter.Next() + if errors.Is(err, iterator.ErrIteratorDone) { + break + } + + if err != nil { + return lazyerrors.Error(err) + } + + index := v.(*types.Document) + + keyDoc := must.NotFail(index.Get("key")).(*types.Document) + fields := keyDoc.Keys() + orders := keyDoc.Values() + key := make([]IndexKeyPair, keyDoc.Len()) + + for j, f := range fields { + descending := false + if orders[j].(int32) == -1 { + descending = true + } + + key[j] = IndexKeyPair{ + Field: f, + Descending: descending, + } + } + + // it was possible for it to be null in pgdb + v, _ = index.Get("unique") + unique, _ := v.(bool) + + res[i] = IndexInfo{ + Name: must.NotFail(index.Get("name")).(string), + PgIndex: must.NotFail(index.Get("pgindex")).(string), + Key: key, + Unique: unique, + } + } + + *s = res + + return nil +} diff --git a/internal/backends/postgresql/metadata/metadata.go b/internal/backends/postgresql/metadata/metadata.go index 7ea34e63e558..e7e5ca6240db 100644 --- a/internal/backends/postgresql/metadata/metadata.go +++ b/internal/backends/postgresql/metadata/metadata.go @@ -16,7 +16,12 @@ package metadata import ( + "database/sql" + "database/sql/driver" + + "github.com/FerretDB/FerretDB/internal/handlers/sjson" "github.com/FerretDB/FerretDB/internal/types" + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" "github.com/FerretDB/FerretDB/internal/util/must" ) @@ -29,10 +34,13 @@ const ( ) // Collection represents collection metadata. +// +// 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 - // TODO https://github.com/FerretDB/FerretDB/issues/3375 + Indexes Indexes } // deepCopy returns a deep copy. @@ -44,21 +52,89 @@ func (c *Collection) deepCopy() *Collection { return &Collection{ Name: c.Name, TableName: c.TableName, + Indexes: c.Indexes.deepCopy(), + } +} + +// Value implements driver.Valuer interface. +func (c Collection) Value() (driver.Value, error) { + b, err := sjson.Marshal(c.marshal()) + if err != nil { + return nil, lazyerrors.Error(err) } + + return b, nil } -// Marshal returns [*types.Document] for that collection. -func (c *Collection) Marshal() *types.Document { +// Scan implements sql.Scanner interface. +func (c *Collection) Scan(src any) error { + var doc *types.Document + var err error + + switch src := src.(type) { + case nil: + *c = Collection{} + return nil + case []byte: + doc, err = sjson.Unmarshal(src) + case string: + doc, err = sjson.Unmarshal([]byte(src)) + default: + panic("can't scan collection") + } + + if err != nil { + return lazyerrors.Error(err) + } + + if err = c.unmarshal(doc); err != nil { + return lazyerrors.Error(err) + } + + return nil +} + +// marshal returns [*types.Document] for that collection. +func (c *Collection) marshal() *types.Document { return must.NotFail(types.NewDocument( "_id", c.Name, "table", c.TableName, + "indexes", c.Indexes.marshal(), )) } -// Unmarshal sets collection metadata from [*types.Document]. -func (c *Collection) Unmarshal(doc *types.Document) error { - c.Name = must.NotFail(doc.Get("_id")).(string) - c.TableName = must.NotFail(doc.Get("table")).(string) +// unmarshal sets collection metadata from [*types.Document]. +func (c *Collection) unmarshal(doc *types.Document) error { + v, _ := doc.Get("_id") + c.Name, _ = v.(string) + + if c.Name == "" { + return lazyerrors.New("collection name is empty") + } + + v, _ = doc.Get("table") + c.TableName, _ = v.(string) + + if c.TableName == "" { + return lazyerrors.New("table name is empty") + } + + v, _ = doc.Get("indexes") + i, _ := v.(*types.Array) + + if i == nil { + return lazyerrors.New("indexes are empty") + } + + if err := c.Indexes.unmarshal(i); err != nil { + return lazyerrors.Error(err) + } return nil } + +// check interfaces +var ( + _ driver.Valuer = Collection{} + _ sql.Scanner = (*Collection)(nil) +) diff --git a/internal/backends/postgresql/metadata/pool/dummy_test.go b/internal/backends/postgresql/metadata/pool/dummy_test.go new file mode 100644 index 000000000000..6d845abd4831 --- /dev/null +++ b/internal/backends/postgresql/metadata/pool/dummy_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 pool + +import "testing" + +func TestDummy(t *testing.T) { + // we need at least one test per package to correctly calculate coverage +} diff --git a/internal/backends/postgresql/metadata/pool/transaction_test.go b/internal/backends/postgresql/metadata/pool/transaction_test.go deleted file mode 100644 index 55ac36935f38..000000000000 --- a/internal/backends/postgresql/metadata/pool/transaction_test.go +++ /dev/null @@ -1,155 +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 pool - -import ( - "context" - "errors" - "fmt" - "testing" - - "github.com/jackc/pgx/v5" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/FerretDB/FerretDB/internal/clientconn/conninfo" - "github.com/FerretDB/FerretDB/internal/util/state" - "github.com/FerretDB/FerretDB/internal/util/testutil" -) - -func TestInTransaction(t *testing.T) { - if testing.Short() { - t.Skip("skipping in -short mode") - } - - t.Parallel() - - ctx := conninfo.Ctx(testutil.Ctx(t), conninfo.New()) - username, password := conninfo.Get(ctx).Auth() - - sp, err := state.NewProvider("") - require.NoError(t, err) - - u := "postgres://username:password@127.0.0.1:5432/ferretdb?pool_min_conns=1" - - pp, err := New(u, testutil.Logger(t), sp) - require.NoError(t, err) - - t.Cleanup(pp.Close) - - p, err := pp.Get(username, password) - require.NoError(t, err) - - t.Cleanup(p.Close) - - tableName := t.Name() - _, err = p.Exec(ctx, fmt.Sprintf(`DROP TABLE IF EXISTS %[1]s; CREATE TABLE %[1]s(s TEXT);`, tableName)) - require.NoError(t, err) - - t.Cleanup(func() { - _, err = p.Exec(ctx, fmt.Sprintf(`DROP TABLE %s`, tableName)) - require.NoError(t, err) - }) - - t.Run("Commit", func(t *testing.T) { - t.Parallel() - - ctx := conninfo.Ctx(testutil.Ctx(t), conninfo.New()) // create new instance of ctx to avoid using canceled ctx - err := err // avoid data race - - v := testutil.CollectionName(t) - err = InTransaction(ctx, p, func(tx pgx.Tx) error { - _, err = tx.Exec(ctx, fmt.Sprintf(`INSERT INTO %s(s) VALUES ($1)`, tableName), v) - require.NoError(t, err) - return nil - }) - require.NoError(t, err) - - var res string - err = p.QueryRow(ctx, fmt.Sprintf(`SELECT s FROM %s WHERE s = $1`, tableName), v).Scan(&res) - require.NoError(t, err) - require.Equal(t, v, res) - }) - - t.Run("Rollback", func(t *testing.T) { - t.Parallel() - - ctx := conninfo.Ctx(testutil.Ctx(t), conninfo.New()) // create new instance of ctx to avoid using canceled ctx - err := err // avoid data race - - v := testutil.CollectionName(t) - err = InTransaction(ctx, p, func(tx pgx.Tx) error { - _, err = tx.Exec(ctx, fmt.Sprintf(`INSERT INTO %s(s) VALUES ($1)`, tableName), v) - require.NoError(t, err) - return errors.New("boom") - }) - require.Error(t, err) - - var res string - err = p.QueryRow(ctx, fmt.Sprintf(`SELECT s FROM %s WHERE s = $1`, tableName), v).Scan(&res) - require.Equal(t, pgx.ErrNoRows, err) - require.Empty(t, res) - }) - - t.Run("ContextCancelRollback", func(t *testing.T) { - t.Parallel() - - ctx := conninfo.Ctx(testutil.Ctx(t), conninfo.New()) // create new instance of ctx to avoid using canceled ctx - err := err // avoid data race - - var cancel func() - ctx, cancel = context.WithCancel(ctx) - - v := testutil.CollectionName(t) - err = InTransaction(ctx, p, func(tx pgx.Tx) error { - _, err = tx.Exec(ctx, fmt.Sprintf(`INSERT INTO %s(s) VALUES ($1)`, tableName), v) - require.NoError(t, err) - - cancel() - - return nil - }) - require.Error(t, err) - - var res string - err = p.QueryRow(context.WithoutCancel(ctx), fmt.Sprintf(`SELECT s FROM %s WHERE s = $1`, tableName), v).Scan(&res) - require.Equal(t, pgx.ErrNoRows, err) - require.Empty(t, res) - }) - - t.Run("Panic", func(t *testing.T) { - t.Parallel() - - ctx := conninfo.Ctx(testutil.Ctx(t), conninfo.New()) // create new instance of ctx to avoid using canceled ctx - err := err // avoid data race - - v := testutil.CollectionName(t) - assert.Panics(t, func() { - err = InTransaction(ctx, p, func(tx pgx.Tx) error { - _, err = tx.Exec(ctx, fmt.Sprintf(`INSERT INTO %s(s) VALUES ($1)`, tableName), v) - require.NoError(t, err) - - //nolint:vet // need it for testing - panic(nil) - }) - require.Equal(t, pgx.ErrNoRows, err) - }) - - var res string - err = p.QueryRow(ctx, fmt.Sprintf(`SELECT s FROM %s WHERE s = $1`, tableName), v).Scan(&res) - require.Equal(t, pgx.ErrNoRows, err) - require.Empty(t, res) - }) -} diff --git a/internal/backends/postgresql/metadata/registry.go b/internal/backends/postgresql/metadata/registry.go index 6544041ac489..6b49f0a0c792 100644 --- a/internal/backends/postgresql/metadata/registry.go +++ b/internal/backends/postgresql/metadata/registry.go @@ -34,7 +34,6 @@ import ( "github.com/FerretDB/FerretDB/internal/backends/postgresql/metadata/pool" "github.com/FerretDB/FerretDB/internal/clientconn/conninfo" "github.com/FerretDB/FerretDB/internal/handlers/sjson" - "github.com/FerretDB/FerretDB/internal/types" "github.com/FerretDB/FerretDB/internal/util/lazyerrors" "github.com/FerretDB/FerretDB/internal/util/must" "github.com/FerretDB/FerretDB/internal/util/observability" @@ -47,6 +46,9 @@ const ( // PostgreSQL max table name length. maxTableNameLength = 63 + + // PostgreSQL max index name length. + maxIndexNameLength = 63 ) // Parts of Prometheus metric names. @@ -215,19 +217,8 @@ func (r *Registry) initCollections(ctx context.Context, dbName string, p *pgxpoo colls := map[string]*Collection{} for rows.Next() { - var b []byte - if err = rows.Scan(&b); err != nil { - return lazyerrors.Error(err) - } - - var doc *types.Document - - if doc, err = sjson.Unmarshal(b); err != nil { - return lazyerrors.Error(err) - } - var c Collection - if err = c.Unmarshal(doc); err != nil { + if err = rows.Scan(&c); err != nil { return lazyerrors.Error(err) } @@ -338,6 +329,30 @@ func (r *Registry) databaseGetOrCreate(ctx context.Context, p *pgxpool.Pool, dbN return nil, lazyerrors.Error(err) } + q = fmt.Sprintf( + `CREATE UNIQUE INDEX %s ON %s (((%s)))`, + pgx.Identifier{metadataTableName + "_id_idx"}.Sanitize(), + pgx.Identifier{dbName, metadataTableName}.Sanitize(), + IDColumn, + ) + + if _, err = p.Exec(ctx, q); err != nil { + _, _ = r.databaseDrop(ctx, p, dbName) + return nil, lazyerrors.Error(err) + } + + q = fmt.Sprintf( + `CREATE UNIQUE INDEX %s ON %s (((%s->'table')))`, + pgx.Identifier{metadataTableName + "_table_idx"}.Sanitize(), + pgx.Identifier{dbName, metadataTableName}.Sanitize(), + DefaultColumn, + ) + + if _, err = p.Exec(ctx, q); err != nil { + _, _ = r.databaseDrop(ctx, p, dbName) + return nil, lazyerrors.Error(err) + } + r.colls[dbName] = map[string]*Collection{} return p, nil @@ -492,17 +507,11 @@ func (r *Registry) collectionCreate(ctx context.Context, p *pgxpool.Pool, dbName TableName: tableName, } - b, err := sjson.Marshal(c.Marshal()) - if err != nil { - return false, lazyerrors.Error(err) - } - q := fmt.Sprintf( `CREATE TABLE %s (%s jsonb)`, pgx.Identifier{dbName, tableName}.Sanitize(), DefaultColumn, ) - if _, err = p.Exec(ctx, q); err != nil { return false, lazyerrors.Error(err) } @@ -512,8 +521,7 @@ func (r *Registry) collectionCreate(ctx context.Context, p *pgxpool.Pool, dbName pgx.Identifier{dbName, metadataTableName}.Sanitize(), DefaultColumn, ) - - if _, err = p.Exec(ctx, q, string(b)); err != nil { + if _, err = p.Exec(ctx, q, c); err != nil { q = fmt.Sprintf(`DROP TABLE %s`, pgx.Identifier{dbName, tableName}.Sanitize()) _, _ = p.Exec(ctx, q) @@ -525,11 +533,15 @@ func (r *Registry) collectionCreate(ctx context.Context, p *pgxpool.Pool, dbName } r.colls[dbName][collectionName] = c - // create PG index for collection name - // TODO https://github.com/FerretDB/FerretDB/issues/3375 - - // create PG index for table name - // TODO https://github.com/FerretDB/FerretDB/issues/3375 + err = r.indexesCreate(ctx, p, dbName, collectionName, []IndexInfo{{ + Name: "_id_", + Key: []IndexKeyPair{{Field: "_id"}}, + Unique: true, + }}) + if err != nil { + _, _ = r.collectionDrop(ctx, p, dbName, collectionName) + return false, lazyerrors.Error(err) + } return true, nil } @@ -669,7 +681,7 @@ func (r *Registry) CollectionRename(ctx context.Context, dbName, oldCollectionNa c.Name = newCollectionName - b, err := sjson.Marshal(c.Marshal()) + b, err := sjson.Marshal(c.marshal()) if err != nil { return false, lazyerrors.Error(err) } @@ -696,6 +708,256 @@ func (r *Registry) CollectionRename(ctx context.Context, dbName, oldCollectionNa return true, nil } +// IndexesCreate creates indexes in the collection. +// +// Existing indexes with given names are ignored. +// +// If the user is not authenticated, it returns error. +func (r *Registry) IndexesCreate(ctx context.Context, dbName, collectionName string, indexes []IndexInfo) error { + defer observability.FuncCall(ctx)() + + p, err := r.getPool(ctx) + if err != nil { + return lazyerrors.Error(err) + } + + r.rw.Lock() + defer r.rw.Unlock() + + return r.indexesCreate(ctx, p, dbName, collectionName, indexes) +} + +// indexesCreate creates indexes in the collection. +// +// Existing indexes with given names are ignored. +// +// It does not hold the lock. +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) + if err != nil { + return lazyerrors.Error(err) + } + + db := r.colls[dbName] + if db == nil { + panic("database does not exist") + } + + c := r.collectionGet(dbName, collectionName) + if c == nil { + panic("collection does not exist") + } + + allIndexes := make(map[string]string, len(db)) // to check if the index already exists + allPgIndexes := make(map[string]string, len(db)) // to ensure there are no indexes with the same name in the pg schema + + for _, coll := range db { + for _, index := range coll.Indexes { + allIndexes[index.Name] = coll.Name + allPgIndexes[index.PgIndex] = coll.Name + } + } + + created := make([]string, 0, len(indexes)) + + for _, index := range indexes { + if coll, ok := allIndexes[index.Name]; ok && coll == collectionName { + continue + } + + tableNamePart := c.TableName + tableNamePartMax := maxIndexNameLength/2 - 1 // 1 for the separator between table name and index name + + if len(tableNamePart) > tableNamePartMax { + tableNamePart = tableNamePart[:tableNamePartMax] + } + + indexNamePart := specialCharacters.ReplaceAllString(strings.ToLower(index.Name), "_") + + h := fnv.New32a() + must.NotFail(h.Write([]byte(collectionName))) + s := h.Sum32() + + var pgIndexName string + + for { + suffixHash := fmt.Sprintf("_%08x_idx", s) + if l := maxIndexNameLength/2 - len(suffixHash); len(indexNamePart) > l { + indexNamePart = indexNamePart[:l] + } + + pgIndexName = fmt.Sprintf("%s_%s", tableNamePart, indexNamePart) + + // indexes must be unique across the whole database, so we check for duplicates for all collections + _, duplicate := allPgIndexes[pgIndexName] + + if !duplicate { + break + } + + s++ + } + + index.PgIndex = pgIndexName + + q := "CREATE " + + if index.Unique { + q += "UNIQUE " + } + + q += "INDEX %s ON %s (%s)" + + columns := make([]string, len(index.Key)) + + for i, key := range index.Key { + // if the field is nested (e.g. foo.bar), it needs to be translated to the correct json path (foo -> bar) + fs := strings.Split(key.Field, ".") + transformedParts := make([]string, len(fs)) + + for j, f := range fs { + // It's important to sanitize field.Field data here, as it's a user-provided value. + transformedParts[j] = quoteString(f) + } + + columns[i] = fmt.Sprintf("((%s->%s))", DefaultColumn, strings.Join(transformedParts, " -> ")) + if key.Descending { + columns[i] += " DESC" + } + } + + q = fmt.Sprintf( + q, + pgx.Identifier{index.PgIndex}.Sanitize(), + pgx.Identifier{dbName, c.TableName}.Sanitize(), + strings.Join(columns, ", "), + ) + + if _, err = p.Exec(ctx, q); err != nil { + _ = r.indexesDrop(ctx, p, dbName, collectionName, created) + return lazyerrors.Error(err) + } + + created = append(created, index.Name) + c.Indexes = append(c.Indexes, index) + } + + b, err := sjson.Marshal(c.marshal()) + if err != nil { + return lazyerrors.Error(err) + } + + arg, err := sjson.MarshalSingleValue(collectionName) + if err != nil { + return lazyerrors.Error(err) + } + + q := fmt.Sprintf( + `UPDATE %s SET %s = $1 WHERE %s = $2`, + pgx.Identifier{dbName, metadataTableName}.Sanitize(), + DefaultColumn, + IDColumn, + ) + + if _, err := p.Exec(ctx, q, string(b), arg); err != nil { + return lazyerrors.Error(err) + } + + r.colls[dbName][collectionName] = c + + return nil +} + +// IndexesDrop removes given connection's indexes. +// +// Non-existing indexes are ignored. +// +// If database or collection does not exist, nil is returned. +// +// If the user is not authenticated, it returns error. +func (r *Registry) IndexesDrop(ctx context.Context, dbName, collectionName string, indexNames []string) error { + defer observability.FuncCall(ctx)() + + p, err := r.getPool(ctx) + if err != nil { + return lazyerrors.Error(err) + } + + r.rw.Lock() + defer r.rw.Unlock() + + return r.indexesDrop(ctx, p, dbName, collectionName, indexNames) +} + +// indexesDrop removes given connection's indexes. +// +// Non-existing indexes are ignored. +// +// If database or collection does not exist, nil is returned. +// +// It does not hold the lock. +func (r *Registry) indexesDrop(ctx context.Context, p *pgxpool.Pool, dbName, collectionName string, indexNames []string) error { + defer observability.FuncCall(ctx)() + + c := r.collectionGet(dbName, collectionName) + if c == nil { + return nil + } + + for _, name := range indexNames { + i := slices.IndexFunc(c.Indexes, func(i IndexInfo) bool { return name == i.Name }) + if i < 0 { + continue + } + + q := fmt.Sprintf("DROP INDEX %s", pgx.Identifier{dbName, c.Indexes[i].PgIndex}.Sanitize()) + if _, err := p.Exec(ctx, q); err != nil { + return lazyerrors.Error(err) + } + + c.Indexes = slices.Delete(c.Indexes, i, i+1) + } + + b, err := sjson.Marshal(c.marshal()) + if err != nil { + return lazyerrors.Error(err) + } + + arg, err := sjson.MarshalSingleValue(collectionName) + if err != nil { + return lazyerrors.Error(err) + } + + q := fmt.Sprintf( + `UPDATE %s SET %s = $1 WHERE %s = $2`, + pgx.Identifier{dbName, metadataTableName}.Sanitize(), + DefaultColumn, + IDColumn, + ) + + if _, err := p.Exec(ctx, q, string(b), arg); err != nil { + return lazyerrors.Error(err) + } + + r.colls[dbName][collectionName] = c + + return nil +} + +// quoteString returns a string that is safe to use in SQL queries. +// +// Deprecated: Warning! Avoid using this function unless there is no other way. +// Ideally, use a placeholder and pass the value as a parameter instead of calling this function. +// +// This approach is used in github.com/jackc/pgx/v4@v4.18.1/internal/sanitize/sanitize.go. +func quoteString(str string) string { + // We need "standard_conforming_strings=on" and "client_encoding=UTF8" (checked in checkConnection), + // otherwise we can't sanitize safely: https://github.com/jackc/pgx/issues/868#issuecomment-725544647 + return "'" + strings.ReplaceAll(str, "'", "''") + "'" +} + // Describe implements prometheus.Collector. func (r *Registry) Describe(ch chan<- *prometheus.Desc) { prometheus.DescribeByCollect(r, ch) diff --git a/internal/backends/postgresql/metadata/registry_test.go b/internal/backends/postgresql/metadata/registry_test.go index de064d45e4d3..6ca89e690416 100644 --- a/internal/backends/postgresql/metadata/registry_test.go +++ b/internal/backends/postgresql/metadata/registry_test.go @@ -17,13 +17,14 @@ package metadata import ( "context" "fmt" - "strings" "sync/atomic" "testing" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/exp/slices" "github.com/FerretDB/FerretDB/internal/clientconn/conninfo" "github.com/FerretDB/FerretDB/internal/util/state" @@ -147,49 +148,6 @@ func TestCheckAuth(t *testing.T) { } } -func TestCreateDrop(t *testing.T) { - if testing.Short() { - t.Skip("skipping in -short mode") - } - - t.Parallel() - - connInfo := conninfo.New() - ctx := conninfo.Ctx(testutil.Ctx(t), connInfo) - - r, db, dbName := createDatabase(t, ctx) - collectionName := testutil.CollectionName(t) - testCollection(t, ctx, r, db, dbName, collectionName) -} - -func TestCreateLongCollectionName(t *testing.T) { - if testing.Short() { - t.Skip("skipping in -short mode") - } - - t.Parallel() - - connInfo := conninfo.New() - ctx := conninfo.Ctx(testutil.Ctx(t), connInfo) - - r, _, dbName := createDatabase(t, ctx) - - collectionName := strings.Repeat("a", 63) - created, err := r.CollectionCreate(ctx, dbName, collectionName) - require.NoError(t, err) - require.True(t, created) - - collectionName = strings.Repeat("a", 63) - created, err = r.CollectionCreate(ctx, dbName, collectionName) - require.NoError(t, err) - require.False(t, created) - - collectionName = strings.Repeat("a", 64) - created, err = r.CollectionCreate(ctx, dbName, collectionName) - require.NoError(t, err) - require.True(t, created) -} - func TestCreateDropStress(t *testing.T) { if testing.Short() { t.Skip("skipping in -short mode") @@ -406,6 +364,8 @@ func TestRenameCollection(t *testing.T) { t.Skip("skipping in -short mode") } + t.Skip("https://github.com/FerretDB/FerretDB/issues/3409") + t.Parallel() connInfo := conninfo.New() @@ -437,6 +397,7 @@ func TestRenameCollection(t *testing.T) { expected := &Collection{ Name: newCollectionName, TableName: oldCollection.TableName, + Indexes: oldCollection.Indexes, } actual, err := r.CollectionGet(ctx, dbName, newCollectionName) @@ -444,3 +405,237 @@ func TestRenameCollection(t *testing.T) { require.Equal(t, expected, actual) }) } + +func TestIndexesCreateDrop(t *testing.T) { + if testing.Short() { + t.Skip("skipping in -short mode") + } + + t.Parallel() + + connInfo := conninfo.New() + ctx := conninfo.Ctx(testutil.Ctx(t), connInfo) + + r, db, dbName := createDatabase(t, ctx) + collectionName := testutil.CollectionName(t) + + toCreate := []IndexInfo{{ + Name: "index_non_unique", + Key: []IndexKeyPair{{ + Field: "f1", + Descending: false, + }, { + Field: "f2", + Descending: true, + }}, + }, { + Name: "index_unique", + Key: []IndexKeyPair{{ + Field: "foo", + Descending: false, + }}, + Unique: true, + }, { + Name: "nested_fields", + Key: []IndexKeyPair{{ + Field: "foo.bar", + }, { + Field: "foo.baz", + Descending: true, + }}, + }} + + err := r.IndexesCreate(ctx, dbName, collectionName, toCreate) + require.NoError(t, err) + + collection, err := r.CollectionGet(ctx, dbName, collectionName) + require.NoError(t, err) + + t.Run("CreateIndexes", func(t *testing.T) { + t.Run("NonUniqueIndex", func(t *testing.T) { + t.Parallel() + + i := slices.IndexFunc(collection.Indexes, func(ii IndexInfo) bool { + return ii.Name == "index_non_unique" + }) + require.GreaterOrEqual(t, i, 0) + tableIndexName := collection.Indexes[i].PgIndex + + var sql string + err := db.QueryRow( + ctx, + "SELECT indexdef FROM pg_indexes WHERE schemaname = $1 AND tablename = $2 AND indexname = $3", + dbName, collection.TableName, tableIndexName, + ).Scan(&sql) + require.NoError(t, err) + + expected := fmt.Sprintf( + `CREATE INDEX %s ON %q.%s USING btree (((_jsonb -> 'f1'::text)), ((_jsonb -> 'f2'::text)) DESC)`, + tableIndexName, dbName, collection.TableName, + ) + require.Equal(t, expected, sql) + }) + + t.Run("UniqueIndex", func(t *testing.T) { + t.Parallel() + + i := slices.IndexFunc(collection.Indexes, func(ii IndexInfo) bool { + return ii.Name == "index_unique" + }) + require.GreaterOrEqual(t, i, 0) + tableIndexName := collection.Indexes[i].PgIndex + + var sql string + err := db.QueryRow( + ctx, + "SELECT indexdef FROM pg_indexes WHERE schemaname = $1 AND tablename = $2 AND indexname = $3", + dbName, collection.TableName, tableIndexName, + ).Scan(&sql) + require.NoError(t, err) + + expected := fmt.Sprintf( + `CREATE UNIQUE INDEX %s ON %q.%s USING btree (((_jsonb -> 'foo'::text)))`, + tableIndexName, dbName, collection.TableName, + ) + require.Equal(t, expected, sql) + }) + + t.Run("NestedFields", func(t *testing.T) { + t.Parallel() + + i := slices.IndexFunc(collection.Indexes, func(ii IndexInfo) bool { + return ii.Name == "nested_fields" + }) + require.GreaterOrEqual(t, i, 0) + tableIndexName := collection.Indexes[i].PgIndex + + var sql string + err := db.QueryRow( + ctx, + "SELECT indexdef FROM pg_indexes WHERE schemaname = $1 AND tablename = $2 AND indexname = $3", + dbName, collection.TableName, tableIndexName, + ).Scan(&sql) + require.NoError(t, err) + + expected := fmt.Sprintf( + `CREATE INDEX %s ON %q.%s USING btree`+ + ` ((((_jsonb -> 'foo'::text) -> 'bar'::text)), (((_jsonb -> 'foo'::text) -> 'baz'::text)) DESC)`, + tableIndexName, dbName, collection.TableName, + ) + require.Equal(t, expected, sql) + }) + + t.Run("DefaultIndex", func(t *testing.T) { + t.Parallel() + + i := slices.IndexFunc(collection.Indexes, func(ii IndexInfo) bool { + return ii.Name == "_id_" + }) + require.GreaterOrEqual(t, i, 0) + tableIndexName := collection.Indexes[i].PgIndex + + var sql string + err := db.QueryRow( + ctx, + "SELECT indexdef FROM pg_indexes WHERE schemaname = $1 AND tablename = $2 AND indexname = $3", + dbName, collection.TableName, tableIndexName, + ).Scan(&sql) + require.NoError(t, err) + + expected := fmt.Sprintf( + `CREATE UNIQUE INDEX %s ON %q.%s USING btree (((_jsonb -> '_id'::text)))`, + tableIndexName, dbName, collection.TableName, + ) + require.Equal(t, expected, sql) + }) + }) + + t.Run("CheckSettingsAfterCreation", func(t *testing.T) { + err := r.initCollections(ctx, dbName, db) + require.NoError(t, err) + + var refreshedCollection *Collection + refreshedCollection, err = r.CollectionGet(ctx, dbName, collectionName) + require.NoError(t, err) + + require.Equal(t, 4, len(refreshedCollection.Indexes)) + + for _, index := range refreshedCollection.Indexes { + switch index.Name { + case "_id_": + assert.Equal(t, 1, len(index.Key)) + case "index_non_unique": + assert.Equal(t, 2, len(index.Key)) + case "index_unique": + assert.Equal(t, 1, len(index.Key)) + case "nested_fields": + assert.Equal(t, 2, len(index.Key)) + default: + t.Errorf("unexpected index: %s", index.Name) + } + } + }) + + t.Run("DropIndexes", func(t *testing.T) { + toDrop := []string{"index_non_unique", "nested_fields"} + err := r.IndexesDrop(ctx, dbName, collectionName, toDrop) + require.NoError(t, err) + + q := "SELECT count(indexdef) FROM pg_indexes WHERE schemaname = $1 AND tablename = $2" + row := db.QueryRow(ctx, q, dbName, collection.TableName) + + var count int + require.NoError(t, row.Scan(&count)) + require.Equal(t, 2, count) // only default index and index_unique should be left + + // check settings after dropping indexes + err = r.initCollections(ctx, dbName, db) + require.NoError(t, err) + + collection, err = r.CollectionGet(ctx, dbName, collectionName) + require.NoError(t, err) + require.Equal(t, 2, len(collection.Indexes)) + + for _, index := range collection.Indexes { + switch index.Name { + case "_id_": + assert.Equal(t, 1, len(index.Key)) + case "index_unique": + assert.Equal(t, 1, len(index.Key)) + default: + t.Errorf("unexpected index: %s", index.Name) + } + } + }) + + t.Run("MetadataIndexes", func(t *testing.T) { + t.Parallel() + + var sql string + err := db.QueryRow( + ctx, + "SELECT indexdef FROM pg_indexes WHERE schemaname = $1 AND tablename = $2 AND indexname = $3", + dbName, metadataTableName, metadataTableName+"_id_idx", + ).Scan(&sql) + require.NoError(t, err) + + expected := fmt.Sprintf( + `CREATE UNIQUE INDEX %s ON %q.%s USING btree (((_jsonb -> '_id'::text)))`, + metadataTableName+"_id_idx", dbName, metadataTableName, + ) + require.Equal(t, expected, sql) + + err = db.QueryRow( + ctx, + "SELECT indexdef FROM pg_indexes WHERE schemaname = $1 AND tablename = $2 AND indexname = $3", + dbName, metadataTableName, metadataTableName+"_table_idx", + ).Scan(&sql) + assert.NoError(t, err) + + expected = fmt.Sprintf( + `CREATE UNIQUE INDEX %s ON %q.%s USING btree (((_jsonb -> 'table'::text)))`, + metadataTableName+"_table_idx", dbName, metadataTableName, + ) + assert.Equal(t, expected, sql) + }) +} diff --git a/internal/backends/sqlite/collection_test.go b/internal/backends/sqlite/collection_test.go index 5c40e0b5f47a..90296a31dbc7 100644 --- a/internal/backends/sqlite/collection_test.go +++ b/internal/backends/sqlite/collection_test.go @@ -26,37 +26,6 @@ import ( "github.com/FerretDB/FerretDB/internal/util/testutil" ) -func TestInsert(t *testing.T) { - sp, err := state.NewProvider("") - require.NoError(t, err) - - b, err := NewBackend(&NewBackendParams{URI: "file:./?mode=memory", L: testutil.Logger(t), P: sp}) - require.NoError(t, err) - - defer b.Close() - - db, err := b.Database(testutil.DatabaseName(t)) - require.NoError(t, err) - - c, err := db.Collection(testutil.CollectionName(t)) - require.NoError(t, err) - - ctx := testutil.Ctx(t) - - doc, err := types.NewDocument("_id", types.NewObjectID()) - require.NoError(t, err) - - _, err = c.InsertAll(ctx, &backends.InsertAllParams{ - Docs: []*types.Document{doc}, - }) - require.NoError(t, err) - - _, err = c.InsertAll(ctx, &backends.InsertAllParams{ - Docs: []*types.Document{doc}, - }) - require.True(t, backends.ErrorCodeIs(err, backends.ErrorCodeInsertDuplicateID)) -} - func TestCollectionStats(t *testing.T) { t.Parallel() ctx := testutil.Ctx(t) diff --git a/internal/backends/sqlite/metadata/metadata.go b/internal/backends/sqlite/metadata/metadata.go index 72c54b881611..27527153cdb8 100644 --- a/internal/backends/sqlite/metadata/metadata.go +++ b/internal/backends/sqlite/metadata/metadata.go @@ -15,16 +15,7 @@ // Package metadata provides access to databases and collections information. package metadata -import ( - "database/sql" - "database/sql/driver" - "encoding/json" - - "golang.org/x/exp/slices" - - "github.com/FerretDB/FerretDB/internal/backends" - "github.com/FerretDB/FerretDB/internal/util/lazyerrors" -) +import "github.com/FerretDB/FerretDB/internal/backends" // Collection will probably have a method for getting column name / SQLite path expression for the given document field // once we implement field extraction. @@ -61,76 +52,3 @@ func (c *Collection) deepCopy() *Collection { Settings: c.Settings.deepCopy(), } } - -// Settings represents collection settings. -type Settings struct { - Indexes []IndexInfo `json:"indexes"` -} - -// deepCopy returns a deep copy. -func (s Settings) deepCopy() Settings { - indexes := make([]IndexInfo, len(s.Indexes)) - - for i, index := range s.Indexes { - indexes[i] = IndexInfo{ - Name: index.Name, - Key: slices.Clone(index.Key), - Unique: index.Unique, - } - } - - return Settings{ - Indexes: indexes, - } -} - -// Value implements driver.Valuer interface. -func (s Settings) Value() (driver.Value, error) { - res, err := json.Marshal(s) - if err != nil { - return nil, lazyerrors.Error(err) - } - - return string(res), nil -} - -// Scan implements sql.Scanner interface. -func (s *Settings) Scan(src any) error { - var err error - - switch src := src.(type) { - case nil: - *s = Settings{} - case []byte: - err = json.Unmarshal(src, s) - case string: - err = json.Unmarshal([]byte(src), s) - default: - panic("can't scan collection settings") - } - - if err != nil { - return lazyerrors.Error(err) - } - - return nil -} - -// IndexInfo represents information about a single index. -type IndexInfo struct { - Name string `json:"name"` - Key []IndexKeyPair `json:"key"` - Unique bool `json:"unique"` -} - -// IndexKeyPair consists of a field name and a sort order that are part of the index. -type IndexKeyPair struct { - Field string `json:"field"` - Descending bool `json:"descending"` -} - -// check interfaces -var ( - _ driver.Valuer = Settings{} - _ sql.Scanner = (*Settings)(nil) -) diff --git a/internal/backends/sqlite/metadata/registry.go b/internal/backends/sqlite/metadata/registry.go index c379b95bd178..df3b111f22a3 100644 --- a/internal/backends/sqlite/metadata/registry.go +++ b/internal/backends/sqlite/metadata/registry.go @@ -427,7 +427,7 @@ func (r *Registry) CollectionRename(ctx context.Context, dbName, oldCollectionNa // IndexesCreate creates indexes in the collection. // -// Existing indexes with given names are ignored (TODO?). +// Existing indexes with given names are ignored. func (r *Registry) IndexesCreate(ctx context.Context, dbName, collectionName string, indexes []IndexInfo) error { defer observability.FuncCall(ctx)() @@ -439,7 +439,7 @@ func (r *Registry) IndexesCreate(ctx context.Context, dbName, collectionName str // indexesCreate creates indexes in the collection. // -// Existing indexes with given names are ignored (TODO?). +// Existing indexes with given names are ignored. // // It does not hold the lock. func (r *Registry) indexesCreate(ctx context.Context, dbName, collectionName string, indexes []IndexInfo) error { @@ -506,21 +506,25 @@ func (r *Registry) indexesCreate(ctx context.Context, dbName, collectionName str return nil } -// IndexesDrop drops provided indexes for the given collection. -func (r *Registry) IndexesDrop(ctx context.Context, dbName, collectionName string, toDrop []string) error { +// IndexesDrop removes given connection's indexes. +// +// Non-existing indexes are ignored. +// +// If database or collection does not exist, nil is returned. +func (r *Registry) IndexesDrop(ctx context.Context, dbName, collectionName string, indexNames []string) error { defer observability.FuncCall(ctx)() r.rw.Lock() defer r.rw.Unlock() - return r.indexesDrop(ctx, dbName, collectionName, toDrop) + return r.indexesDrop(ctx, dbName, collectionName, indexNames) } -// indexesDrop remove given connection's indexes. +// indexesDrop removes given connection's indexes. // -// Non-existing indexes are ignored (TODO?). +// Non-existing indexes are ignored. // -// If database or collection does not exist, nil is returned (TODO?). +// If database or collection does not exist, nil is returned. // // It does not hold the lock. func (r *Registry) indexesDrop(ctx context.Context, dbName, collectionName string, indexNames []string) error { diff --git a/internal/backends/sqlite/metadata/settings.go b/internal/backends/sqlite/metadata/settings.go new file mode 100644 index 000000000000..94867fcf8c17 --- /dev/null +++ b/internal/backends/sqlite/metadata/settings.go @@ -0,0 +1,98 @@ +// 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 metadata + +import ( + "database/sql" + "database/sql/driver" + "encoding/json" + + "golang.org/x/exp/slices" + + "github.com/FerretDB/FerretDB/internal/util/lazyerrors" +) + +// Settings represents collection settings. +type Settings struct { + Indexes []IndexInfo `json:"indexes"` +} + +// IndexInfo represents information about a single index. +type IndexInfo struct { + Name string `json:"name"` + Key []IndexKeyPair `json:"key"` + Unique bool `json:"unique"` +} + +// IndexKeyPair consists of a field name and a sort order that are part of the index. +type IndexKeyPair struct { + Field string `json:"field"` + Descending bool `json:"descending"` +} + +// deepCopy returns a deep copy. +func (s Settings) deepCopy() Settings { + indexes := make([]IndexInfo, len(s.Indexes)) + + for i, index := range s.Indexes { + indexes[i] = IndexInfo{ + Name: index.Name, + Key: slices.Clone(index.Key), + Unique: index.Unique, + } + } + + return Settings{ + Indexes: indexes, + } +} + +// Value implements driver.Valuer interface. +func (s Settings) Value() (driver.Value, error) { + res, err := json.Marshal(s) + if err != nil { + return nil, lazyerrors.Error(err) + } + + return string(res), nil +} + +// Scan implements sql.Scanner interface. +func (s *Settings) Scan(src any) error { + var err error + + switch src := src.(type) { + case nil: + *s = Settings{} + case []byte: + err = json.Unmarshal(src, s) + case string: + err = json.Unmarshal([]byte(src), s) + default: + panic("can't scan collection settings") + } + + if err != nil { + return lazyerrors.Error(err) + } + + return nil +} + +// check interfaces +var ( + _ driver.Valuer = Settings{} + _ sql.Scanner = (*Settings)(nil) +) diff --git a/internal/util/state/state.go b/internal/util/state/state.go index 578f258ae9bb..c06397710bd8 100644 --- a/internal/util/state/state.go +++ b/internal/util/state/state.go @@ -20,8 +20,6 @@ import ( "github.com/AlekSi/pointer" "github.com/google/uuid" - - "github.com/FerretDB/FerretDB/internal/util/must" ) // State represents FerretDB process state. @@ -68,7 +66,7 @@ func (s *State) EnableTelemetry() { // fill replaces all unset or invalid values with default. func (s *State) fill() { if _, err := uuid.Parse(s.UUID); err != nil { - s.UUID = must.NotFail(uuid.NewRandom()).String() + s.UUID = uuid.NewString() } if s.Start.IsZero() {