db

package
v2.7.12 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 25, 2026 License: MIT Imports: 21 Imported by: 0

README

Database Package

Go Reference

Multi-database support with GORM, automated migrations, and OpenTelemetry instrumentation.

Overview

The db package provides a unified interface for connecting to multiple database systems with automatic OpenTelemetry tracing and metrics collection. Built on GORM and golang-migrate, it simplifies database operations while providing production-ready observability.

Features

  • Multi-Database Support: PostgreSQL, MySQL, MSSQL
  • GORM Integration: Full ORM capabilities with GORM v2
  • Automatic Tracing: Query-level distributed tracing
  • Connection Pool Metrics: Real-time pool health monitoring
  • Schema Migrations: Embedded migrations with golang-migrate
  • Type-Safe Configuration: Validation with struct tags
  • Zero Configuration OTel: Optional but seamless observability

Installation

go get github.com/jasoet/pkg/v2/db

Quick Start

Basic Connection
package main

import (
    "github.com/jasoet/pkg/v2/db"
    "time"
)

func main() {
    config := db.ConnectionConfig{
        DbType:       db.Postgresql,
        Host:         "localhost",
        Port:         5432,
        Username:     "admin",
        Password:     "secret",
        DbName:       "myapp",
        Timeout:      5 * time.Second,
        MaxIdleConns: 5,
        MaxOpenConns: 10,
    }

    pool, err := config.Pool()
    if err != nil {
        panic(err)
    }

    // Use GORM
    var users []User
    pool.Find(&users)
}
With OpenTelemetry
import (
    "github.com/jasoet/pkg/v2/db"
    "github.com/jasoet/pkg/v2/otel"
)

// Setup OTel
otelConfig := otel.NewConfig("my-service").
    WithTracerProvider(tracerProvider).
    WithMeterProvider(meterProvider)

// Configure database with OTel
config := db.ConnectionConfig{
    DbType:       db.Postgresql,
    Host:         "localhost",
    Port:         5432,
    Username:     "admin",
    Password:     "secret",
    DbName:       "myapp",
    Timeout:      5 * time.Second,
    MaxIdleConns: 5,
    MaxOpenConns: 10,
    OTelConfig:   otelConfig,  // Enable tracing & metrics
}

pool, _ := config.Pool()

// All queries are automatically traced
pool.Find(&users)  // Creates span "db.SELECT"
pool.Create(&user) // Creates span "db.INSERT"

Database Types

PostgreSQL
config := db.ConnectionConfig{
    DbType: db.Postgresql,
    Host:   "localhost",
    Port:   5432,
    // ...
}

DSN Format: user=admin password=secret host=localhost port=5432 dbname=myapp sslmode=disable connect_timeout=5

MySQL
config := db.ConnectionConfig{
    DbType: db.Mysql,
    Host:   "localhost",
    Port:   3306,
    // ...
}

DSN Format: admin:secret@tcp(localhost:3306)/myapp?parseTime=true&timeout=5s

SQL Server (MSSQL)
config := db.ConnectionConfig{
    DbType: db.MSSQL,
    Host:   "localhost",
    Port:   1433,
    // ...
}

DSN Format: sqlserver://admin:secret@localhost:1433?database=myapp&connectTimeout=5s&encrypt=disable

Configuration

ConnectionConfig
type ConnectionConfig struct {
    DbType       DatabaseType  `yaml:"dbType" validate:"required,oneof=MYSQL POSTGRES MSSQL"`
    Host         string        `yaml:"host" validate:"required,min=1"`
    Port         int           `yaml:"port"`
    Username     string        `yaml:"username" validate:"required,min=1"`
    Password     string        `yaml:"password"`
    DbName       string        `yaml:"dbName" validate:"required,min=1"`
    Timeout      time.Duration `yaml:"timeout" validate:"min=3s"`
    MaxIdleConns int           `yaml:"maxIdleConns" validate:"min=1"`
    MaxOpenConns int           `yaml:"maxOpenConns" validate:"min=2"`

    // Optional: Enable OpenTelemetry (nil = disabled)
    OTelConfig   *otel.Config  `yaml:"-"`
}
Methods
Method Description
Pool() Returns GORM DB instance with connection pooling
SQLDB() Returns raw *sql.DB for direct SQL access
Dsn() Generates database connection string

OpenTelemetry Integration

Automatic Tracing

When OTelConfig is provided, all database operations are automatically traced:

config := db.ConnectionConfig{
    // ... database config
    OTelConfig: otelConfig,
}

pool, _ := config.Pool()

// Each operation creates a span
pool.Create(&user)           // Span: "db.INSERT"
pool.Find(&users)            // Span: "db.SELECT"
pool.Where("age > ?", 18).Find(&users)  // Span: "db.SELECT"
pool.Update("name", "John")  // Span: "db.UPDATE"
pool.Delete(&user)           // Span: "db.DELETE"
Span Attributes

Each span includes:

Span Attributes:
  db.system: "POSTGRES" | "MYSQL" | "MSSQL"
  db.name: "myapp"
  db.statement: "SELECT * FROM users WHERE age > 18"
  db.collection.name: "users"
  db.rows_affected: 42
  db.duration_ms: 15
  server.address: "localhost"
  server.port: 5432
Metrics Collection

Connection pool metrics are automatically collected:

Metrics:
  db.client.connections.idle:    # Number of idle connections
  db.client.connections.active:  # Number of active connections
  db.client.connections.max:     # Maximum connections allowed

Attributes:
  db.system: "POSTGRES"
  db.name: "myapp"
  server.address: "localhost"
  server.port: 5432

Database Migrations

Using Embedded SQL Files
import (
    "context"
    "embed"
    "github.com/jasoet/pkg/v2/db"
)

//go:embed migrations/*.sql
var migrationsFS embed.FS

func main() {
    config := db.ConnectionConfig{/* ... */}
    pool, _ := config.Pool()

    ctx := context.Background()

    // Run migrations UP
    err := db.RunPostgresMigrationsWithGorm(
        ctx,
        pool,
        migrationsFS,
        "migrations",
    )
    if err != nil {
        panic(err)
    }
}
Migration File Structure
migrations/
├── 001_create_users.up.sql
├── 001_create_users.down.sql
├── 002_add_email_index.up.sql
└── 002_add_email_index.down.sql

Example Migration:

-- 001_create_users.up.sql
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT NOW()
);

-- 001_create_users.down.sql
DROP TABLE IF EXISTS users;
Migration Functions
Function Description
RunPostgresMigrationsWithGorm(ctx, gormDB, fs, path) Run migrations UP with GORM
RunPostgresMigrationsDownWithGorm(ctx, gormDB, fs, path) Roll back migrations with GORM
RunPostgresMigrations(ctx, sqlDB, fs, path) Run migrations UP with raw SQL DB
RunPostgresMigrationsDown(ctx, sqlDB, fs, path) Roll back migrations with raw SQL DB

Advanced Usage

Raw SQL Access
pool, _ := config.Pool()

// Get raw *sql.DB
sqlDB, err := pool.DB()
if err != nil {
    panic(err)
}

// Or use SQLDB() directly
sqlDB, err := config.SQLDB()

// Use standard database/sql
rows, err := sqlDB.Query("SELECT * FROM users WHERE age > ?", 18)
Connection Pooling
config := db.ConnectionConfig{
    // Connection pool settings
    MaxIdleConns: 10,   // Max idle connections
    MaxOpenConns: 100,  // Max open connections
    Timeout:      30 * time.Second,
    // ...
}

pool, _ := config.Pool()

// Pool is automatically managed
// Connections are reused efficiently
Transaction Support
// GORM transactions
err := pool.Transaction(func(tx *gorm.DB) error {
    if err := tx.Create(&user).Error; err != nil {
        return err
    }

    if err := tx.Create(&profile).Error; err != nil {
        return err
    }

    return nil
})

// Each query in transaction is traced separately
Configuration from YAML
import (
    "github.com/jasoet/pkg/v2/config"
    "github.com/jasoet/pkg/v2/db"
)

type AppConfig struct {
    Database db.ConnectionConfig `yaml:"database"`
}

yamlConfig := `
database:
  dbType: POSTGRES
  host: localhost
  port: 5432
  username: admin
  password: secret
  dbName: myapp
  timeout: 5s
  maxIdleConns: 5
  maxOpenConns: 10
`

cfg, _ := config.LoadString[AppConfig](yamlConfig)
pool, _ := cfg.Database.Pool()

Error Handling

pool, err := config.Pool()
if err != nil {
    switch {
    case strings.Contains(err.Error(), "dsn is empty"):
        // Invalid configuration
    case strings.Contains(err.Error(), "connection refused"):
        // Database not reachable
    case strings.Contains(err.Error(), "authentication failed"):
        // Invalid credentials
    default:
        // Other errors
    }
}

// GORM errors
result := pool.Find(&users)
if result.Error != nil {
    if errors.Is(result.Error, gorm.ErrRecordNotFound) {
        // No records found
    }
}

Best Practices

1. Use Environment Variables for Secrets
import (
    "github.com/jasoet/pkg/v2/config"
    "github.com/jasoet/pkg/v2/db"
)

type AppConfig struct {
    Database db.ConnectionConfig `yaml:"database"`
}

yamlConfig := `
database:
  dbType: POSTGRES
  host: localhost
  port: 5432
  # username and password from env vars
  dbName: myapp
  timeout: 5s
  maxIdleConns: 5
  maxOpenConns: 10
`

// Set via environment:
// ENV_DATABASE_USERNAME=admin
// ENV_DATABASE_PASSWORD=secret123

cfg, _ := config.LoadString[AppConfig](yamlConfig)
pool, _ := cfg.Database.Pool()
2. Connection Pool Sizing
import "runtime"

config := db.ConnectionConfig{
    // Rule of thumb: 2-3x number of CPU cores
    MaxOpenConns: runtime.NumCPU() * 3,
    // Keep some idle connections ready
    MaxIdleConns: runtime.NumCPU(),
    // ...
}
3. Always Enable OTel in Production
// ✅ Good: Observability enabled
config := db.ConnectionConfig{
    // ... database config
    OTelConfig: otelConfig,  // Tracing + Metrics
}

// ❌ Bad: No observability
config := db.ConnectionConfig{
    // ... database config
    OTelConfig: nil,  // No tracing, no metrics
}
4. Use Context for Tracing
// ✅ Good: Context propagates trace
ctx := context.Background()
ctx, span := tracer.Start(ctx, "user-service")
defer span.End()

pool.WithContext(ctx).Find(&users)  // Trace linked

// ❌ Bad: Trace not propagated
pool.Find(&users)  // New root span
5. Validate Configuration
import "github.com/go-playground/validator/v10"

config := db.ConnectionConfig{
    DbType:       db.Postgresql,
    Host:         "localhost",
    Port:         5432,
    Username:     "admin",
    DbName:       "myapp",
    Timeout:      5 * time.Second,
    MaxIdleConns: 5,
    MaxOpenConns: 10,
}

validate := validator.New()
if err := validate.Struct(config); err != nil {
    panic(fmt.Sprintf("invalid config: %v", err))
}

pool, _ := config.Pool()

Testing

The package includes comprehensive tests with 79.1% coverage:

# Unit tests
go test ./db -v

# Integration tests (requires Docker)
go test ./db -tags=integration -v

# With coverage
go test ./db -tags=integration -cover
Test Utilities
import (
    "github.com/jasoet/pkg/v2/db"
    "github.com/jasoet/pkg/v2/otel"
    noopt "go.opentelemetry.io/otel/trace/noop"
    noopm "go.opentelemetry.io/otel/metric/noop"
)

func TestWithTestcontainer(t *testing.T) {
    // Use testcontainers for integration tests
    ctx := context.Background()
    container, _ := setupPostgresContainer(ctx)
    defer container.Terminate(ctx)

    config := db.ConnectionConfig{
        DbType:   db.Postgresql,
        Host:     container.Host(ctx),
        Port:     container.MappedPort(ctx, "5432").Int(),
        Username: "test",
        Password: "test",
        DbName:   "testdb",
        OTelConfig: otel.NewConfig("test").
            WithTracerProvider(noopt.NewTracerProvider()).
            WithMeterProvider(noopm.NewMeterProvider()),
    }

    pool, err := config.Pool()
    assert.NoError(t, err)

    // Test your code
}

Troubleshooting

Connection Refused

Problem: connection refused error

Solutions:

// 1. Check database is running
// docker ps | grep postgres

// 2. Verify host and port
config := db.ConnectionConfig{
    Host: "localhost",  // or "127.0.0.1"
    Port: 5432,         // default PostgreSQL port
    // ...
}

// 3. Check timeout
config.Timeout = 30 * time.Second  // Increase timeout
Authentication Failed

Problem: authentication failed error

Solutions:

// 1. Verify credentials
config := db.ConnectionConfig{
    Username: "correct_username",
    Password: "correct_password",
    // ...
}

// 2. Check database exists
// psql -U admin -l

// 3. Verify user permissions
// GRANT ALL PRIVILEGES ON DATABASE myapp TO admin;
Too Many Connections

Problem: sorry, too many clients already error

Solutions:

// 1. Reduce max connections
config := db.ConnectionConfig{
    MaxOpenConns: 20,  // Lower value
    MaxIdleConns: 5,
    // ...
}

// 2. Check pool metrics (if OTel enabled)
// Look at db.client.connections.active metric

// 3. Increase database max_connections
// ALTER SYSTEM SET max_connections = 200;
Migrations Not Running

Problem: Migrations not applying

Solutions:

// 1. Check migration files exist
//go:embed migrations/*.sql
var migrationsFS embed.FS

// 2. Verify path
err := db.RunPostgresMigrationsWithGorm(
    ctx,
    pool,
    migrationsFS,
    "migrations",  // Correct path
)

// 3. Check migration version table
// SELECT * FROM schema_migrations;

Performance

  • Connection Pooling: Efficiently reuses connections
  • Prepared Statements: GORM uses prepared statements by default
  • Query Optimization: Use indexes and EXPLAIN ANALYZE
  • Batch Operations: Use GORM's batch features for bulk inserts

Benchmark (typical operations):

BenchmarkQuery-8         10000    ~500 µs/op
BenchmarkInsert-8         5000    ~800 µs/op
BenchmarkUpdate-8         8000    ~600 µs/op

Version Compatibility

  • GORM: v1.31.0+
  • golang-migrate: v4.19.0+
  • PostgreSQL: 12+
  • MySQL: 8.0+
  • SQL Server: 2019+
  • Go: 1.25+
  • pkg library: v2.0.0+

Examples

See examples/ directory for:

  • Basic database connection
  • Multi-database setup
  • OpenTelemetry integration
  • Migration management
  • Transaction handling
  • Connection pooling
  • Error handling
  • otel - OpenTelemetry configuration
  • config - Configuration management
  • logging - Structured logging

License

MIT License - see LICENSE for details.

Documentation

Overview

Package db provides database connection pooling with GORM, supporting PostgreSQL, MySQL, and MSSQL, with optional OpenTelemetry instrumentation.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func RunPostgresMigrations

func RunPostgresMigrations(ctx context.Context, db *sql.DB, migrationFs embed.FS, migrationsPath string) error

RunPostgresMigrations applies pending UP migrations using a raw *sql.DB connection.

Note: only PostgreSQL is supported. For MySQL or MSSQL, use a different migration tool.

func RunPostgresMigrationsDown

func RunPostgresMigrationsDown(ctx context.Context, db *sql.DB, migrationFs embed.FS, migrationsPath string) error

RunPostgresMigrationsDown rolls back all migrations using a raw *sql.DB connection.

Note: only PostgreSQL is supported. For MySQL or MSSQL, use a different migration tool.

func RunPostgresMigrationsDownWithGorm

func RunPostgresMigrationsDownWithGorm(ctx context.Context, db *gorm.DB, migrationFs embed.FS, migrationsPath string) error

RunPostgresMigrationsDownWithGorm rolls back migrations using a GORM connection.

Note: only PostgreSQL is supported. For MySQL or MSSQL, use a different migration tool.

func RunPostgresMigrationsWithGorm

func RunPostgresMigrationsWithGorm(ctx context.Context, db *gorm.DB, migrationFs embed.FS, migrationsPath string) error

RunPostgresMigrationsWithGorm applies pending UP migrations using a GORM connection.

Note: only PostgreSQL is supported. For MySQL or MSSQL, use a different migration tool.

Types

type ConnectionConfig

type ConnectionConfig struct {
	DbType       DatabaseType  `yaml:"dbType" validate:"required,oneof=MYSQL POSTGRES MSSQL" mapstructure:"dbType"`
	Host         string        `yaml:"host" validate:"required,min=1" mapstructure:"host"`
	Port         int           `yaml:"port" mapstructure:"port"`
	Username     string        `yaml:"username" validate:"required,min=1" mapstructure:"username"`
	Password     string        `yaml:"password" mapstructure:"password"`
	DbName       string        `yaml:"dbName" validate:"required,min=1" mapstructure:"dbName"`
	Timeout      time.Duration `yaml:"timeout" mapstructure:"timeout"`
	MaxIdleConns int           `yaml:"maxIdleConns" mapstructure:"maxIdleConns" validate:"min=1"`
	MaxOpenConns int           `yaml:"maxOpenConns" mapstructure:"maxOpenConns" validate:"min=2"`

	// ConnMaxLifetime sets the maximum duration a connection may be reused.
	// Zero means connections are not closed due to age.
	ConnMaxLifetime time.Duration `yaml:"connMaxLifetime" mapstructure:"connMaxLifetime"`

	// ConnMaxIdleTime sets the maximum duration a connection may sit idle.
	// Zero means connections are not closed due to idle time.
	ConnMaxIdleTime time.Duration `yaml:"connMaxIdleTime" mapstructure:"connMaxIdleTime"`

	// SSLMode configures TLS for the connection.
	// PostgreSQL: "disable", "require", "verify-ca", "verify-full" (default: "disable")
	// MSSQL: "disable", "true", "false" (default: "disable")
	// MySQL: handled via DSN parameters (this field is ignored for MySQL)
	SSLMode string `yaml:"sslMode" mapstructure:"sslMode"`

	// GormLogLevel sets the GORM logger verbosity (1=Silent, 2=Error, 3=Warn, 4=Info).
	// Default: 1 (Silent)
	GormLogLevel int `yaml:"gormLogLevel" mapstructure:"gormLogLevel"`

	// OpenTelemetry Configuration (optional - nil disables telemetry)
	OTelConfig *pkgotel.Config `yaml:"-" mapstructure:"-"` // Not serializable from config files
}

ConnectionConfig holds the connection parameters for a database pool.

func (*ConnectionConfig) Dsn

func (c *ConnectionConfig) Dsn() string

Dsn builds the data source name string for the configured database type.

func (*ConnectionConfig) Pool

func (c *ConnectionConfig) Pool() (*gorm.DB, error)

Pool creates a new GORM database connection pool.

It validates the DSN, opens the connection, configures pool parameters, pings to verify connectivity, and optionally installs OTel instrumentation.

func (*ConnectionConfig) SQLDB

func (c *ConnectionConfig) SQLDB() (*sql.DB, error)

SQLDB is a convenience wrapper that creates a new GORM pool via Pool() and returns the underlying *sql.DB. Each call creates a new connection pool; prefer Pool() when you need to reuse the connection.

type DatabaseType

type DatabaseType string

DatabaseType identifies the database backend.

const (
	Mysql      DatabaseType = "MYSQL"
	Postgresql DatabaseType = "POSTGRES"
	MSSQL      DatabaseType = "MSSQL"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL