adapters

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jan 6, 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
`
}

func (m *MongoDBAdapter) CreateUserCommand(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(username, dbName string) []string {
    return []string{
        "mongo", dbName, "--eval",
        fmt.Sprintf("db.dropUser('%s')", username),
    }
}

func (m *MongoDBAdapter) RotatePasswordCommand(username, newPassword, dbName string) []string {
    return []string{
        "mongo", dbName, "--eval",
        fmt.Sprintf("db.changeUserPassword('%s', '%s')", username, newPassword),
    }
}
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
Optional Methods (can return nil)
Method Purpose Returns
CreateUserCommand(user, pass, db) Command to create database user []string or nil
DeleteUserCommand(user, db) Command to delete database user []string or nil
RotatePasswordCommand(user, pass, db) Command to change user password []string or nil

If these methods return nil, the operation will return 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
createCmd := adapter.CreateUserCommand("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 false for SupportsUsername()
    • Uses command line args --requirepass 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

	// SupportsUnauthenticated returns whether this database can run without authentication
	SupportsUnauthenticated() bool

	// 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
	// Returns nil if user creation is not supported
	CreateUserCommand(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(username, dbName string) []string

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

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

	// SupportsUsername returns whether this database supports username authentication
	SupportsUsername() bool

	// 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(username, password, dbName string) []string

func (*MySQLAdapter) DeleteUserCommand

func (m *MySQLAdapter) DeleteUserCommand(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) ParseVersion

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

func (*MySQLAdapter) RotatePasswordCommand

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

func (*MySQLAdapter) SupportsUnauthenticated added in v0.2.1

func (m *MySQLAdapter) SupportsUnauthenticated() bool

func (*MySQLAdapter) SupportsUsername

func (m *MySQLAdapter) SupportsUsername() bool

type PostgresAdapter

type PostgresAdapter struct{}

PostgresAdapter implements the DatabaseAdapter interface for PostgreSQL

func NewPostgresAdapter

func NewPostgresAdapter() *PostgresAdapter

func (*PostgresAdapter) CreateUserCommand

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

func (*PostgresAdapter) DeleteUserCommand

func (p *PostgresAdapter) DeleteUserCommand(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) ParseVersion

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

func (*PostgresAdapter) RotatePasswordCommand

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

func (*PostgresAdapter) SupportsUnauthenticated added in v0.2.1

func (p *PostgresAdapter) SupportsUnauthenticated() bool

func (*PostgresAdapter) SupportsUsername

func (p *PostgresAdapter) SupportsUsername() bool

type RedisAdapter

type RedisAdapter struct{}

RedisAdapter implements the DatabaseAdapter interface for Redis

func NewRedisAdapter

func NewRedisAdapter() *RedisAdapter

func (*RedisAdapter) CreateUserCommand

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

func (*RedisAdapter) DeleteUserCommand

func (r *RedisAdapter) DeleteUserCommand(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) ParseVersion

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

func (*RedisAdapter) RotatePasswordCommand

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

func (*RedisAdapter) SupportsUnauthenticated added in v0.2.1

func (r *RedisAdapter) SupportsUnauthenticated() bool

func (*RedisAdapter) SupportsUsername

func (r *RedisAdapter) SupportsUsername() bool

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) GetAllAliases

func (r *Registry) GetAllAliases() map[string]string

GetAllAliases returns a map of all aliases to their canonical names

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