adapters

package
v0.5.1 Latest Latest
Warning

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

Go to latest
Published: Jul 14, 2026 License: MIT Imports: 3 Imported by: 0

README

Database Adapters

This package implements an adapter pattern for supporting multiple database providers in mkdb.

Overview

The adapter system allows for easy addition of new database providers without modifying core application logic. Each database type is implemented as an adapter that conforms to the DatabaseAdapter interface.

Architecture

Components
  1. DatabaseAdapter Interface (adapter.go)

    • Defines the contract that all database providers must implement
    • Includes methods for configuration, Docker setup, and database operations
  2. Concrete Adapters

    • PostgresAdapter (postgres.go) - PostgreSQL support
    • MySQLAdapter (mysql.go) - MySQL/MariaDB support
    • RedisAdapter (redis.go) - Redis support
  3. Registry (registry.go)

    • Manages all registered adapters
    • Provides lookup by name or alias
    • Thread-safe singleton pattern

Adding a New Database Provider

To add support for a new database (e.g., MongoDB), follow these steps:

1. Create the Adapter Implementation

Create a new file mongodb.go:

package adapters

import "fmt"

type MongoDBAdapter struct{}

func NewMongoDBAdapter() *MongoDBAdapter {
    return &MongoDBAdapter{}
}

func (m *MongoDBAdapter) GetName() string {
    return "mongodb"
}

func (m *MongoDBAdapter) GetAliases() []string {
    return []string{"mongodb", "mongo"}
}

func (m *MongoDBAdapter) GetImage(version string) string {
    if version == "" {
        version = "latest"
    }
    return fmt.Sprintf("mongo:%s", version)
}

func (m *MongoDBAdapter) GetDefaultPort() string {
    return "27017"
}

func (m *MongoDBAdapter) GetEnvVars(dbName, username, password string) []string {
    return []string{
        fmt.Sprintf("MONGO_INITDB_DATABASE=%s", dbName),
        fmt.Sprintf("MONGO_INITDB_ROOT_USERNAME=%s", username),
        fmt.Sprintf("MONGO_INITDB_ROOT_PASSWORD=%s", password),
    }
}

func (m *MongoDBAdapter) GetDataPath() string {
    return "/data/db"
}

func (m *MongoDBAdapter) GetConfigPath() string {
    return "/etc/mongo"
}

func (m *MongoDBAdapter) GetConfigFileName() string {
    return "mongod.conf"
}

func (m *MongoDBAdapter) GetDefaultConfig() string {
    return `# MongoDB configuration file
# Managed by mkdb

storage:
  dbPath: /data/db

net:
  bindIp: 0.0.0.0
  port: 27017
`
}

// adminUser/adminPassword are the container's privileged credentials, used to
// authenticate the administrative command (empty for unauthenticated databases).
func (m *MongoDBAdapter) CreateUserCommand(adminUser, adminPassword, username, password, dbName string) []string {
    return []string{
        "mongo", dbName, "--eval",
        fmt.Sprintf("db.createUser({user: '%s', pwd: '%s', roles: [{role: 'readWrite', db: '%s'}]})",
            username, password, dbName),
    }
}

func (m *MongoDBAdapter) DeleteUserCommand(adminUser, adminPassword, username, dbName string) []string {
    return []string{
        "mongo", dbName, "--eval",
        fmt.Sprintf("db.dropUser('%s')", username),
    }
}

func (m *MongoDBAdapter) RotatePasswordCommand(adminUser, adminPassword, username, newPassword, dbName string) []string {
    return []string{
        "mongo", dbName, "--eval",
        fmt.Sprintf("db.changeUserPassword('%s', '%s')", username, newPassword),
    }
}

// InitCommand returns a client that reads a script from stdin and applies it to
// dbName. Return nil if loading a script isn't supported for this engine.
func (m *MongoDBAdapter) InitCommand(adminUser, adminPassword, dbName string) []string {
    return []string{"mongosh", "--quiet", dbName}
}

func (m *MongoDBAdapter) FormatConnectionString(username, password, host, port, dbName string) string {
    if username == "" && password == "" {
        return fmt.Sprintf("mongodb://%s:%s/%s", host, port, dbName)
    }
    return fmt.Sprintf("mongodb://%s:%s@%s:%s/%s", username, password, host, port, dbName)
}

// GetCommandArgs returns custom startup arguments (empty when none are needed).
func (m *MongoDBAdapter) GetCommandArgs(password string) []string {
    return []string{}
}

func (m *MongoDBAdapter) GetVersionCommand() []string {
    return []string{"mongod", "--version"}
}

func (m *MongoDBAdapter) ParseVersion(output string) string {
    // Input: "db version v7.0.5"; Output: "7.0.5"
    for _, part := range strings.Fields(output) {
        if strings.HasPrefix(part, "v") {
            return strings.TrimPrefix(part, "v")
        }
    }
    return strings.TrimSpace(output)
}

The example above implements every method of DatabaseAdapter. Add "strings" to the import block for ParseVersion.

2. Register the Adapter

Update the GetRegistry() function in registry.go to register your new adapter:

func GetRegistry() *Registry {
    once.Do(func() {
        defaultRegistry = &Registry{
            adapters:    make(map[string]DatabaseAdapter),
            aliasToName: make(map[string]string),
        }
        // Register default adapters
        defaultRegistry.Register(NewPostgresAdapter())
        defaultRegistry.Register(NewMySQLAdapter())
        defaultRegistry.Register(NewRedisAdapter())
        defaultRegistry.Register(NewMongoDBAdapter()) // Add this line
    })
    return defaultRegistry
}
3. That's It!

The adapter will automatically be:

  • Available in all database type selections
  • Handled by all Docker operations
  • Included in connection string generation
  • Managed by configuration systems

Interface Methods

Required Methods
Method Purpose Returns
GetName() Canonical database name string
GetAliases() Alternative names/aliases []string
GetImage(version) Docker image with version string
GetDefaultPort() Default connection port string
GetEnvVars(db, user, pass) Environment variables for container []string
GetDataPath() Data directory in container string
GetConfigPath() Config directory in container string
GetConfigFileName() Main config file name string
GetDefaultConfig() Default config file content string
FormatConnectionString(user, pass, host, port, db) Connection string string
GetCommandArgs(password) Custom container startup args []string
GetVersionCommand() Command to print the engine version []string or nil
ParseVersion(output) Extract a clean version from that output string
Optional Methods (can return nil)
Method Purpose Returns
CreateUserCommand(adminUser, adminPass, user, pass, db) Command to create database user []string or nil
DeleteUserCommand(adminUser, adminPass, user, db) Command to delete database user []string or nil
RotatePasswordCommand(adminUser, adminPass, user, newPass, db) Command to change user password []string or nil
InitCommand(adminUser, adminPass, db) Client that applies a stdin script to db (used by create --init) []string or nil

adminUser/adminPass are the container's privileged (default-user) credentials used to authenticate the operation; both are empty for unauthenticated databases. If these methods return nil, the operation returns an error indicating it's not supported for this database type.

Usage Examples

Getting an Adapter
import "github.com/pbzona/mkdb/internal/adapters"

// Get the global registry
registry := adapters.GetRegistry()

// Get adapter by canonical name
adapter, err := registry.Get("postgres")

// Get adapter by alias
adapter, err := registry.Get("pg")
adapter, err := registry.Get("postgresql")
Using an Adapter
// Get configuration
image := adapter.GetImage("15")
port := adapter.GetDefaultPort()
envVars := adapter.GetEnvVars("mydb", "user", "pass")

// Get paths
dataPath := adapter.GetDataPath()
configPath := adapter.GetConfigPath()

// Get user management commands (adminUser/adminPass authenticate the operation)
createCmd := adapter.CreateUserCommand("dbuser", "adminpass", "newuser", "password", "mydb")
if createCmd != nil {
    // Execute the command
}
Listing Available Databases
registry := adapters.GetRegistry()
dbTypes := registry.List() // ["postgres", "mysql", "redis"]
Normalizing Database Types
registry := adapters.GetRegistry()
canonical, err := registry.NormalizeType("pg") // Returns "postgres"

Testing

When adding a new adapter, add test cases to registry_test.go:

{
    name:      "mongodb by name",
    dbType:    "mongodb",
    wantName:  "mongodb",
    wantError: false,
},
{
    name:      "mongodb by alias",
    dbType:    "mongo",
    wantName:  "mongodb",
    wantError: false,
},

Run tests:

go test ./internal/adapters/...

Benefits of the Adapter Pattern

  1. Extensibility: Add new database types without modifying existing code
  2. Maintainability: Database-specific logic is isolated in adapters
  3. Testability: Each adapter can be tested independently
  4. Consistency: All databases follow the same interface contract
  5. Type Safety: Compile-time checking ensures all methods are implemented
  6. Discovery: Registry provides runtime discovery of available databases

Database-Specific Implementations

Redis

Redis has some unique characteristics that are handled differently:

  1. Authentication: Redis doesn't use traditional username/password authentication. It only uses a password (requirepass). The adapter:

    • Returns nil from the user-management commands (user management is unsupported)
    • Uses command line args --requirepass (via GetCommandArgs) to set the password
    • Formats connection strings as redis://:<password>@host:port/db
  2. Database Selection: Redis uses numeric databases (0-15 by default). The dbName parameter is treated as the database number in the connection string.

  3. Connection String Format:

    • With password: redis://:password@localhost:6379/0
    • Without password: redis://localhost:6379/0
    • Note the : before the password (no username)
PostgreSQL
  • Uses environment variables for configuration
  • Standard username/password authentication
  • Connection string: postgresql://user:pass@host:port/dbname
MySQL
  • Uses environment variables for configuration
  • Standard username/password authentication
  • Connection string: mysql://user:pass@tcp(host:port)/dbname

Design Decisions

Why Not Use Type Switches?

The previous implementation used switch statements scattered throughout the codebase. The adapter pattern provides:

  • Better separation of concerns
  • Easier to add new providers
  • Reduced coupling between components
  • Single responsibility principle
Why a Registry?

The registry pattern provides:

  • Centralized management of adapters
  • Runtime discovery of available databases
  • Alias resolution
  • Thread-safe access to adapters
Why Return nil for Unsupported Operations?

Some databases don't support certain operations (e.g., Redis user management). Returning nil allows:

  • Clear indication of unsupported features
  • Graceful error handling
  • Optional functionality without breaking the interface
Why GetCommandArgs()?

Some databases (like Redis) need custom command line arguments to configure authentication, as they don't use environment variables. This method allows adapters to:

  • Specify custom startup commands
  • Configure authentication via command line
  • Override default container behavior when needed

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type DatabaseAdapter

type DatabaseAdapter interface {
	// GetName returns the canonical name of the database (e.g., "postgres", "mysql", "redis")
	GetName() string

	// GetAliases returns alternative names that map to this database type
	GetAliases() []string

	// GetImage returns the Docker image for the specified version
	GetImage(version string) string

	// GetDefaultPort returns the default port for this database
	GetDefaultPort() string

	// GetEnvVars returns the environment variables needed to configure the container
	// Pass empty strings for username and password to run in unauthenticated mode
	GetEnvVars(dbName, username, password string) []string

	// GetDataPath returns the path inside the container where data is stored
	GetDataPath() string

	// GetConfigPath returns the path inside the container where config files are stored
	GetConfigPath() string

	// GetConfigFileName returns the name of the main configuration file
	GetConfigFileName() string

	// GetDefaultConfig returns the default configuration file content
	GetDefaultConfig() string

	// CreateUserCommand returns the command to create a new user in the database.
	// adminUser/adminPassword are the credentials of the container's default
	// (privileged) user, used to authenticate the administrative command.
	// Returns nil if user creation is not supported.
	CreateUserCommand(adminUser, adminPassword, username, password, dbName string) []string

	// DeleteUserCommand returns the command to delete a user from the database.
	// Returns nil if user deletion is not supported.
	DeleteUserCommand(adminUser, adminPassword, username, dbName string) []string

	// RotatePasswordCommand returns the command to rotate a user's password.
	// Returns nil if password rotation is not supported.
	RotatePasswordCommand(adminUser, adminPassword, username, newPassword, dbName string) []string

	// InitCommand returns the command that reads a schema/seed script from
	// stdin and applies it to dbName, authenticated with the admin credentials.
	// Returns nil if loading a script is not supported (e.g. Redis).
	InitCommand(adminUser, adminPassword, dbName string) []string

	// FormatConnectionString returns the connection string for this database
	FormatConnectionString(username, password, host, port, dbName string) string

	// GetCommandArgs returns custom command line arguments for starting the container
	// Returns empty slice if no custom command is needed
	// Pass empty string for password to run in unauthenticated mode
	GetCommandArgs(password string) []string

	// GetVersionCommand returns the command to get the database version
	// Returns nil if version detection is not supported
	GetVersionCommand() []string

	// ParseVersion parses the version output from GetVersionCommand
	// Returns a clean version string (e.g., "16.1" instead of full output)
	ParseVersion(output string) string
}

DatabaseAdapter defines the interface that all database providers must implement

type MySQLAdapter

type MySQLAdapter struct{}

MySQLAdapter implements the DatabaseAdapter interface for MySQL

func NewMySQLAdapter

func NewMySQLAdapter() *MySQLAdapter

func (*MySQLAdapter) CreateUserCommand

func (m *MySQLAdapter) CreateUserCommand(adminUser, adminPassword, username, password, dbName string) []string

func (*MySQLAdapter) DeleteUserCommand

func (m *MySQLAdapter) DeleteUserCommand(adminUser, adminPassword, username, dbName string) []string

func (*MySQLAdapter) FormatConnectionString

func (m *MySQLAdapter) FormatConnectionString(username, password, host, port, dbName string) string

func (*MySQLAdapter) GetAliases

func (m *MySQLAdapter) GetAliases() []string

func (*MySQLAdapter) GetCommandArgs

func (m *MySQLAdapter) GetCommandArgs(password string) []string

func (*MySQLAdapter) GetConfigFileName

func (m *MySQLAdapter) GetConfigFileName() string

func (*MySQLAdapter) GetConfigPath

func (m *MySQLAdapter) GetConfigPath() string

func (*MySQLAdapter) GetDataPath

func (m *MySQLAdapter) GetDataPath() string

func (*MySQLAdapter) GetDefaultConfig

func (m *MySQLAdapter) GetDefaultConfig() string

func (*MySQLAdapter) GetDefaultPort

func (m *MySQLAdapter) GetDefaultPort() string

func (*MySQLAdapter) GetEnvVars

func (m *MySQLAdapter) GetEnvVars(dbName, username, password string) []string

func (*MySQLAdapter) GetImage

func (m *MySQLAdapter) GetImage(version string) string

func (*MySQLAdapter) GetName

func (m *MySQLAdapter) GetName() string

func (*MySQLAdapter) GetVersionCommand

func (m *MySQLAdapter) GetVersionCommand() []string

func (*MySQLAdapter) InitCommand added in v0.5.0

func (m *MySQLAdapter) InitCommand(adminUser, adminPassword, dbName string) []string

func (*MySQLAdapter) ParseVersion

func (m *MySQLAdapter) ParseVersion(output string) string

func (*MySQLAdapter) RotatePasswordCommand

func (m *MySQLAdapter) RotatePasswordCommand(adminUser, adminPassword, username, newPassword, dbName string) []string

type PostgresAdapter

type PostgresAdapter struct{}

PostgresAdapter implements the DatabaseAdapter interface for PostgreSQL

func NewPostgresAdapter

func NewPostgresAdapter() *PostgresAdapter

func (*PostgresAdapter) CreateUserCommand

func (p *PostgresAdapter) CreateUserCommand(adminUser, adminPassword, username, password, dbName string) []string

func (*PostgresAdapter) DeleteUserCommand

func (p *PostgresAdapter) DeleteUserCommand(adminUser, adminPassword, username, dbName string) []string

func (*PostgresAdapter) FormatConnectionString

func (p *PostgresAdapter) FormatConnectionString(username, password, host, port, dbName string) string

func (*PostgresAdapter) GetAliases

func (p *PostgresAdapter) GetAliases() []string

func (*PostgresAdapter) GetCommandArgs

func (p *PostgresAdapter) GetCommandArgs(password string) []string

func (*PostgresAdapter) GetConfigFileName

func (p *PostgresAdapter) GetConfigFileName() string

func (*PostgresAdapter) GetConfigPath

func (p *PostgresAdapter) GetConfigPath() string

func (*PostgresAdapter) GetDataPath

func (p *PostgresAdapter) GetDataPath() string

func (*PostgresAdapter) GetDefaultConfig

func (p *PostgresAdapter) GetDefaultConfig() string

func (*PostgresAdapter) GetDefaultPort

func (p *PostgresAdapter) GetDefaultPort() string

func (*PostgresAdapter) GetEnvVars

func (p *PostgresAdapter) GetEnvVars(dbName, username, password string) []string

func (*PostgresAdapter) GetImage

func (p *PostgresAdapter) GetImage(version string) string

func (*PostgresAdapter) GetName

func (p *PostgresAdapter) GetName() string

func (*PostgresAdapter) GetVersionCommand

func (p *PostgresAdapter) GetVersionCommand() []string

func (*PostgresAdapter) InitCommand added in v0.5.0

func (p *PostgresAdapter) InitCommand(adminUser, adminPassword, dbName string) []string

func (*PostgresAdapter) ParseVersion

func (p *PostgresAdapter) ParseVersion(output string) string

func (*PostgresAdapter) RotatePasswordCommand

func (p *PostgresAdapter) RotatePasswordCommand(adminUser, adminPassword, username, newPassword, dbName string) []string

type RedisAdapter

type RedisAdapter struct{}

RedisAdapter implements the DatabaseAdapter interface for Redis

func NewRedisAdapter

func NewRedisAdapter() *RedisAdapter

func (*RedisAdapter) CreateUserCommand

func (r *RedisAdapter) CreateUserCommand(adminUser, adminPassword, username, password, dbName string) []string

func (*RedisAdapter) DeleteUserCommand

func (r *RedisAdapter) DeleteUserCommand(adminUser, adminPassword, username, dbName string) []string

func (*RedisAdapter) FormatConnectionString

func (r *RedisAdapter) FormatConnectionString(username, password, host, port, dbName string) string

func (*RedisAdapter) GetAliases

func (r *RedisAdapter) GetAliases() []string

func (*RedisAdapter) GetCommandArgs

func (r *RedisAdapter) GetCommandArgs(password string) []string

GetCommandArgs returns the command line arguments to start Redis with password

func (*RedisAdapter) GetConfigFileName

func (r *RedisAdapter) GetConfigFileName() string

func (*RedisAdapter) GetConfigPath

func (r *RedisAdapter) GetConfigPath() string

func (*RedisAdapter) GetDataPath

func (r *RedisAdapter) GetDataPath() string

func (*RedisAdapter) GetDefaultConfig

func (r *RedisAdapter) GetDefaultConfig() string

func (*RedisAdapter) GetDefaultPort

func (r *RedisAdapter) GetDefaultPort() string

func (*RedisAdapter) GetEnvVars

func (r *RedisAdapter) GetEnvVars(dbName, username, password string) []string

func (*RedisAdapter) GetImage

func (r *RedisAdapter) GetImage(version string) string

func (*RedisAdapter) GetName

func (r *RedisAdapter) GetName() string

func (*RedisAdapter) GetVersionCommand

func (r *RedisAdapter) GetVersionCommand() []string

func (*RedisAdapter) InitCommand added in v0.5.0

func (r *RedisAdapter) InitCommand(adminUser, adminPassword, dbName string) []string

func (*RedisAdapter) ParseVersion

func (r *RedisAdapter) ParseVersion(output string) string

func (*RedisAdapter) RotatePasswordCommand

func (r *RedisAdapter) RotatePasswordCommand(adminUser, adminPassword, username, newPassword, dbName string) []string

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry manages all registered database adapters

func GetRegistry

func GetRegistry() *Registry

GetRegistry returns the global registry instance

func (*Registry) Get

func (r *Registry) Get(nameOrAlias string) (DatabaseAdapter, error)

Get retrieves an adapter by name or alias

func (*Registry) IsValidType

func (r *Registry) IsValidType(dbType string) bool

IsValidType checks if a database type is valid

func (*Registry) List

func (r *Registry) List() []string

List returns all registered adapter names in a consistent order

func (*Registry) NormalizeType

func (r *Registry) NormalizeType(dbType string) (string, error)

NormalizeType normalizes a database type to its canonical name

func (*Registry) Register

func (r *Registry) Register(adapter DatabaseAdapter)

Register registers a new database adapter

Jump to

Keyboard shortcuts

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