Skip to content

Commit

Permalink
Add MySQL backend metadata (#3828)
Browse files Browse the repository at this point in the history
  • Loading branch information
adetunjii authored Dec 11, 2023
1 parent c0eace0 commit 1290378
Show file tree
Hide file tree
Showing 8 changed files with 560 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ linters-settings:
- "!**/internal/backends/sqlite/*.go"
- "!**/internal/backends/postgresql/*.go"
- "!**/internal/backends/postgresql/metadata/*.go"
- "!**/internal/backends/mysql/*.go"
- "!**/internal/backends/mysql/metadata/*.go"
- "!**/internal/backends/hana/*.go"
deny:
- pkg: github.com/FerretDB/FerretDB/internal/handler/sjson
Expand Down
121 changes: 121 additions & 0 deletions internal/backends/mysql/metadata/indexes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// 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"

"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
Index 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
}

// 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(
"name", index.Name,
"index", index.Index,
"key", key,
"unique", index.Unique,
)))
}

return res
}

func (s *Indexes) unmarshal(a *types.Array) error {
res := make([]IndexInfo, 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 = append(key, IndexKeyPair{
Field: f,
Descending: descending,
})
}

v, _ = index.Get("unique")
unique, _ := v.(bool)

res[i] = IndexInfo{
Name: must.NotFail(index.Get("name")).(string),
Index: must.NotFail(index.Get("index")).(string),
Key: key,
Unique: unique,
}
}

*s = res

return nil
}
132 changes: 132 additions & 0 deletions internal/backends/mysql/metadata/metadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// 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 provides access to databases and collections information.
package metadata

import (
"database/sql"
"database/sql/driver"

"github.com/FerretDB/FerretDB/internal/handler/sjson"
"github.com/FerretDB/FerretDB/internal/types"
"github.com/FerretDB/FerretDB/internal/util/lazyerrors"
"github.com/FerretDB/FerretDB/internal/util/must"
)

// Collection represents collection metadata.
//
// Collection value should be immutable to avoid data races.
// Use [deepCopy] to replace whole value instead of modifying fields of existing value.
type Collection struct {
Name string
TableName string
Indexes Indexes
CappedSize int64
CappedDocuments int64
}

// Capped returns true if collection is capped.
func (c Collection) Capped() bool {
return c.CappedSize > 0
}

// 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
}

// 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 the [*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(),
"cappedSize", c.CappedSize,
"cappedDocuments", c.CappedDocuments,
))
}

// 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 err := c.Indexes.unmarshal(i); err != nil {
return lazyerrors.Error(err)
}

if v, _ := doc.Get("cappedSize"); v != nil {
c.CappedSize = v.(int64)
}

if v, _ := doc.Get("cappedDocuments"); v != nil {
c.CappedSize = v.(int64)
}

return nil
}

// check interfaces
var (
_ driver.Valuer = Collection{}
_ sql.Scanner = (*Collection)(nil)
)
68 changes: 68 additions & 0 deletions internal/backends/mysql/metadata/pool/opendb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// 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"
"database/sql"
"time"

_ "github.com/go-sql-driver/mysql" // register database/sql driver
"go.uber.org/zap"

"github.com/FerretDB/FerretDB/internal/util/fsql"
"github.com/FerretDB/FerretDB/internal/util/lazyerrors"
"github.com/FerretDB/FerretDB/internal/util/state"
)

// openDB creates a pool of connections to MySQL database
// and check that it works (authentication passes, settings are okay).
func openDB(uri string, l *zap.Logger, sp *state.Provider) (*fsql.DB, error) {
mysqlURL, err := parseURI(uri)
if err != nil {
return nil, lazyerrors.Error(err)
}

db, err := sql.Open("mysql", mysqlURL)
if err != nil {
return nil, lazyerrors.Error(err)
}

db.SetConnMaxIdleTime(1 * time.Minute)
db.SetMaxIdleConns(50)
db.SetMaxOpenConns(50)

if err = db.Ping(); err != nil {
_ = db.Close()
return nil, lazyerrors.Error(err)
}

// set backend version
if sp.Get().BackendVersion == "" {
err := sp.Update(func(s *state.State) {
s.BackendName = "MySQL"

row := db.QueryRowContext(context.Background(), `SELECT version()`)
if err := row.Scan(&s.BackendVersion); err != nil {
l.Error("mysql.metadata.pool.openDB: failed to query MySQL version", zap.Error(err))
}
})
if err != nil {
l.Error("mysql.metadata.pool.openDB: failed to query MySQL version", zap.Error(err))
}
}

return fsql.WrapDB(db, "", l), nil
}
Loading

0 comments on commit 1290378

Please sign in to comment.