zitadel/internal/database/database.go
Tim Möhlmann 25dc7bfe72
perf(cache): pgx pool connector (#8703)
# Which Problems Are Solved

Cache implementation using a PGX connection pool.

# How the Problems Are Solved

Defines a new schema `cache` in the zitadel database.
A table for string keys and a table for objects is defined.
For postgreSQL, tables are unlogged and partitioned by cache name for
performance.

Cockroach does not have unlogged tables and partitioning is an
enterprise feature that uses alternative syntax combined with sharding.
Regular tables are used here.

# Additional Changes

- `postgres.Config` can return a pxg pool. See following discussion

# Additional Context

- Part of https://github.com/zitadel/zitadel/issues/8648
- Closes https://github.com/zitadel/zitadel/issues/8647

---------

Co-authored-by: Silvan <silvan.reusser@gmail.com>
2024-10-04 13:15:41 +00:00

184 lines
4.5 KiB
Go

package database
import (
"context"
"database/sql"
"encoding/json"
"errors"
"reflect"
"strings"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/mitchellh/mapstructure"
"github.com/zitadel/logging"
_ "github.com/zitadel/zitadel/internal/database/cockroach"
"github.com/zitadel/zitadel/internal/database/dialect"
_ "github.com/zitadel/zitadel/internal/database/postgres"
"github.com/zitadel/zitadel/internal/zerrors"
)
type Config struct {
Dialects map[string]interface{} `mapstructure:",remain"`
EventPushConnRatio float64
ProjectionSpoolerConnRatio float64
connector dialect.Connector
}
func (c *Config) SetConnector(connector dialect.Connector) {
c.connector = connector
}
type DB struct {
*sql.DB
dialect.Database
Pool *pgxpool.Pool
}
func (db *DB) Query(scan func(*sql.Rows) error, query string, args ...any) error {
return db.QueryContext(context.Background(), scan, query, args...)
}
func (db *DB) QueryContext(ctx context.Context, scan func(rows *sql.Rows) error, query string, args ...any) (err error) {
tx, err := db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true, Isolation: sql.LevelReadCommitted})
if err != nil {
return err
}
defer func() {
if err != nil {
rollbackErr := tx.Rollback()
logging.OnError(rollbackErr).Info("commit of read only transaction failed")
return
}
err = tx.Commit()
}()
rows, err := tx.QueryContext(ctx, query, args...)
if err != nil {
return err
}
defer func() {
closeErr := rows.Close()
logging.OnError(closeErr).Info("rows.Close failed")
}()
if err = scan(rows); err != nil {
return err
}
return rows.Err()
}
func (db *DB) QueryRow(scan func(*sql.Row) error, query string, args ...any) (err error) {
return db.QueryRowContext(context.Background(), scan, query, args...)
}
func (db *DB) QueryRowContext(ctx context.Context, scan func(row *sql.Row) error, query string, args ...any) (err error) {
tx, err := db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true, Isolation: sql.LevelReadCommitted})
if err != nil {
return err
}
defer func() {
if err != nil {
rollbackErr := tx.Rollback()
logging.OnError(rollbackErr).Info("commit of read only transaction failed")
return
}
err = tx.Commit()
}()
row := tx.QueryRowContext(ctx, query, args...)
logging.OnError(row.Err()).Error("unexpected query error")
err = scan(row)
if err != nil {
return err
}
return row.Err()
}
func QueryJSONObject[T any](ctx context.Context, db *DB, query string, args ...any) (*T, error) {
var data []byte
err := db.QueryRowContext(ctx, func(row *sql.Row) error {
return row.Scan(&data)
}, query, args...)
if errors.Is(err, sql.ErrNoRows) {
return nil, err
}
if err != nil {
return nil, zerrors.ThrowInternal(err, "DATAB-Oath6", "Errors.Internal")
}
obj := new(T)
if err = json.Unmarshal(data, obj); err != nil {
return nil, zerrors.ThrowInternal(err, "DATAB-Vohs6", "Errors.Internal")
}
return obj, nil
}
func Connect(config Config, useAdmin bool, purpose dialect.DBPurpose) (*DB, error) {
client, pool, err := config.connector.Connect(useAdmin, config.EventPushConnRatio, config.ProjectionSpoolerConnRatio, purpose)
if err != nil {
return nil, err
}
if err := client.Ping(); err != nil {
return nil, zerrors.ThrowPreconditionFailed(err, "DATAB-0pIWD", "Errors.Database.Connection.Failed")
}
return &DB{
DB: client,
Database: config.connector,
Pool: pool,
}, nil
}
func DecodeHook(from, to reflect.Value) (_ interface{}, err error) {
if to.Type() != reflect.TypeOf(Config{}) {
return from.Interface(), nil
}
config := new(Config)
if err = mapstructure.Decode(from.Interface(), config); err != nil {
return nil, err
}
configuredDialect := dialect.SelectByConfig(config.Dialects)
configs := make([]interface{}, 0, len(config.Dialects)-1)
for name, dialectConfig := range config.Dialects {
if !configuredDialect.Matcher.MatchName(name) {
continue
}
configs = append(configs, dialectConfig)
}
config.connector, err = configuredDialect.Matcher.Decode(configs)
if err != nil {
return nil, err
}
return config, nil
}
func (c Config) DatabaseName() string {
return c.connector.DatabaseName()
}
func (c Config) Username() string {
return c.connector.Username()
}
func (c Config) Password() string {
return c.connector.Password()
}
func (c Config) Type() string {
return c.connector.Type()
}
func EscapeLikeWildcards(value string) string {
value = strings.ReplaceAll(value, "%", "\\%")
value = strings.ReplaceAll(value, "_", "\\_")
return value
}