Skip to content

Instantly share code, notes, and snippets.

@voxxit
Last active January 30, 2025 03:28
Show Gist options
  • Save voxxit/56cbfc7bba7d166178f45cb2ec58c5d3 to your computer and use it in GitHub Desktop.
Save voxxit/56cbfc7bba7d166178f45cb2ec58c5d3 to your computer and use it in GitHub Desktop.
Nice Go

Relational Database (PostgreSQL/MySQL) Interface Implementation

Here is an implementation of the interfaces tailored for PostgreSQL or MySQL:

package sql

import (
	"context"
	"database/sql"
)

// Client represents a connection to a SQL database
type Client interface {
	Connect(ctx context.Context) error
	Disconnect(ctx context.Context) error
	DB(name string) Database
}

// Database represents a specific database within the client
type Database interface {
	Table(name string) Table
	Client() Client
}

// Table represents a table in the database and provides CRUD operations
type Table interface {
	InsertOne(ctx context.Context, data interface{}) (interface{}, error)
	InsertMany(ctx context.Context, data []interface{}) ([]interface{}, error)
	Update(ctx context.Context, where string, data interface{}, args ...interface{}) (int64, error)
	Delete(ctx context.Context, where string, args ...interface{}) (int64, error)
	SelectOne(ctx context.Context, query string, dest interface{}, args ...interface{}) error
	Select(ctx context.Context, query string, dest interface{}, args ...interface{}) ([]interface{}, error)
}

// sqlClient implements the Client interface for SQL databases
type sqlClient struct {
	db *sql.DB
}

// sqlDatabase implements the Database interface
type sqlDatabase struct {
	db  *sql.DB
	name string
}

// sqlTable implements the Table interface
type sqlTable struct {
	db   *sql.DB
	name string
}

func NewClient(dsn string) (Client, error) {
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		return nil, err
	}
	return &sqlClient{db: db}, nil
}

// Connect establishes a connection to the database
func (c *sqlClient) Connect(ctx context.Context) error {
	return c.db.PingContext(ctx)
}

// Disconnect closes the database connection
func (c *sqlClient) Disconnect(ctx context.Context) error {
	return c.db.Close()
}

// DB returns a Database interface for the specified database name
func (c *sqlClient) DB(name string) Database {
	return &sqlDatabase{db: c.db, name: name}
}

// Table returns a Table interface for the specified table name
func (d *sqlDatabase) Table(name string) Table {
	return &sqlTable{db: d.db, name: name}
}

// InsertOne inserts a single record into the table and returns the generated ID
func (t *sqlTable) InsertOne(ctx context.Context, data interface{}) (interface{}, error) {
	// Implementation would depend on the specific ORM or query builder used
	// This is just a placeholder implementation
	return nil, nil
}

// InsertMany inserts multiple records into the table and returns the generated IDs
func (t *sqlTable) InsertMany(ctx context.Context, data []interface{}) ([]interface{}, error) {
	// Implementation would depend on the specific ORM or query builder used
	// This is just a placeholder implementation
	return nil, nil
}

// Update updates records in the table based on the where clause and returns the number of affected rows
func (t *sqlTable) Update(ctx context.Context, where string, data interface{}, args ...interface{}) (int64, error) {
	// Implementation would depend on the specific ORM or query builder used
	// This is just a placeholder implementation
	return 0, nil
}

// Delete deletes records from the table based on the where clause and returns the number of affected rows
func (t *sqlTable) Delete(ctx context.Context, where string, args ...interface{}) (int64, error) {
	// Implementation would depend on the specific ORM or query builder used
	// This is just a placeholder implementation
	return 0, nil
}

// SelectOne retrieves a single record from the table based on the query and populates the destination struct
func (t *sqlTable) SelectOne(ctx context.Context, query string, dest interface{}, args ...interface{}) error {
	// Implementation would depend on the specific ORM or query builder used
	// This is just a placeholder implementation
	return nil
}

// Select retrieves multiple records from the table based on the query and populates the destination slice
func (t *sqlTable) Select(ctx context.Context, query string, dest interface{}, args ...interface{}) ([]interface{}, error) {
	// Implementation would depend on the specific ORM or query builder used
	// This is just a placeholder implementation
	return nil, nil
}

Key/Value Store (Redis) Interface Implementation

Here is an implementation of the interfaces tailored for Redis:

package redis

import (
	"context"
	"errors"

	"github.com/gomodule/redigo/redis"
)

// Client represents a connection to a Redis database
type Client interface {
	Connect(ctx context.Context) error
	Disconnect(ctx context.Context) error
	DB(number int) Database
}

// Database represents a specific database within the client
type Database interface {
	Get(key string) (string, error)
	Set(key string, value string, expiration time.Duration) error
	Del(keys ...string) (int, error)
	HGet(hashKey, field string) (string, error)
	HSet(hashKey, field string, value string) bool
	LPush(listKey string, values ...interface{}) (int, error)
	RPop(listKey string) (string, error)
}

// redisClient implements the Client interface for Redis
type redisClient struct {
	conn *redis.Pool
}

// redisDatabase implements the Database interface
type redisDatabase struct {
	pool  *redis.Pool
	db    int
}

func NewClient(address string, password string, dbNumber int) (Client, error) {
	pool := &redis.Pool{
		MaxIdle:     3,
		IdleTimeout: 240 * time.Second,
		Dial: func() (redis.Conn, error) {
			return redis.Dial("tcp", address, redis.Password(password))
		},
	}
	return &redisClient{conn: pool}, nil
}

// Connect establishes a connection to the Redis server
func (c *redisClient) Connect(ctx context.Context) error {
	conn := c.conn.Get()
	defer conn.Close()
	return nil // Redis connections are established on Dial, which is handled by the Pool
}

// Disconnect closes all connections in the pool
func (c *redisClient) Disconnect(ctx context.Context) error {
	c.conn.Close()
	return nil
}

// DB returns a Database interface for the specified database number
func (c *redisClient) DB(number int) Database {
	return &redisDatabase{pool: c.conn, db: number}
}

// Get retrieves the value of a key
func (d *redisDatabase) Get(key string) (string, error) {
	conn := d.pool.Get()
	defer conn.Close()

	result, err := redis.String(conn.Do("GET", key))
	if err != nil {
		return "", err
	}
	return result, nil
}

// Set sets the value of a key with an optional expiration time
func (d *redisDatabase) Set(key string, value string, expiration time.Duration) error {
	conn := d.pool.Get()
	defer conn.Close()

	args := []interface{}{key, value}
	if expiration != 0 {
		args = append(args, "EX", int(expiration.Seconds()))
	}

	_, err := redis.String(conn.Do("SET", args...))
	return err
}

// Del deletes one or more keys
func (d *redisDatabase) Del(keys ...string) (int, error) {
	conn := d.pool.Get()
	defer conn.Close()

	result, err := redis.Int(conn.Do("DEL", keys...))
	if err != nil {
		return 0, err
	}
	return result, nil
}

// HGet retrieves the value of a field in a hash
func (d *redisDatabase) HGet(hashKey, field string) (string, error) {
	conn := d.pool.Get()
	defer conn.Close()

	result, err := redis.String(conn.Do("HGET", hashKey, field))
	if err != nil {
		return "", err
	}
	return result, nil
}

// HSet sets the value of a field in a hash
func (d *redisDatabase) HSet(hashKey, field, value string) bool {
	conn := d.pool.Get()
	defer conn.Close()

	result, err := redis.Bool(conn.Do("HSET", hashKey, field, value))
	if err != nil {
		return false
	}
	return result
}

// LPush adds one or more values to the head of a list
func (d *redisDatabase) LPush(listKey string, values ...interface{}) (int, error) {
	conn := d.pool.Get()
	defer conn.Close()

	result, err := redis.Int(conn.Do("LPUSH", listKey, values...))
	if err != nil {
		return 0, err
	}
	return result, nil
}

// RPop removes and returns the last element of a list
func (d *redisDatabase) RPop(listKey string) (string, error) {
	conn := d.pool.Get()
	defer conn.Close()

	result, err := redis.String(conn.Do("RPOP", listKey))
	if err != nil {
		return "", err
	}
	return result, nil
}

Explanation of Changes

  1. Relational Database Implementation:

    • The Client interface now represents a connection to a SQL database (PostgreSQL/MySQL).
    • The Database interface provides access to specific databases within the client.
    • The Collection interface has been adapted to represent tables in a relational database, with methods for CRUD operations and queries.
    • Methods like FindOne, InsertOne, DeleteOne, etc., have been modified to fit SQL operations.
  2. Redis Implementation:

    • The Client interface now represents a connection to a Redis server.
    • The Database interface provides access to specific Redis databases (by number).
    • Methods like Get, Set, Del, HGet, etc., have been implemented to fit Redis operations.
  3. Common Features:

    • Both implementations maintain the context-aware design for asynchronous operations.
    • They include error handling and proper resource management (e.g., closing connections).
    • The interfaces are designed to be flexible and extensible, allowing for additional methods as needed.

These implementations provide a clean and consistent way to interact with different types of databases while maintaining the architectural principles seen in the MongoDB example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment