hermes

package module
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 22, 2022 License: MIT Imports: 8 Imported by: 0

README

Hermes PGX 1.0.0

Hermes PGX is an update to the https://github.com/sbowman/hermes package that wraps https://github.com/jackc/pgx in place of the older https://github.com/lib/pq package. This package is much lighter weight than the original Hermes, as much of the original wrapping functionality is baked into the newer pgx package.

At its heart, Hermes PGX supplies a common interface, hermes.Conn, to wrap the database connection pool and transactions so they share common functionality, and can be used interchangeably in the context of a test or function. This makes it easier to leverage functions in different combinations to build database APIs for Go applications.

Note that because this package is based on pgx, it only supports PostgreSQL. If you're using another database, https://github.com/sbowman/hermes remains agnostic.

Godoc license

Usage

// Sample can take either a reference to the pgx database connection pool, or to a transaction.
func Sample(conn hermes.Conn, name string) error {
    tx, err := conn.Begin()
    if err != nil {
        return err
    }
    
    // Will automatically rollback if an error short-circuits the return
    // before tx.Commit() is called...
    defer tx.Close() 

    res, err := conn.Exec("insert into samples (name) values ($1)", name)
    if err != nil {
        return err
    }

    check, err := res.RowsAffected()
    if check == 0 {
        return fmt.Errorf("Failed to insert row (%s)", err)
    }

    return tx.Commit()
}

func main() {
    // Create a connection pool with max 10 connections, min 2 idle connections...
    conn, err := hermes.Connect("postgres://postgres@127.0.0.1/my_db?sslmode=disable&connect_timeout=10")
    if err != nil {
        return err
    }

    // This works...
    if err := Sample(conn, "Bob"); err != nil {
        fmt.Println("Bob failed!", err.Error())
    }

    // So does this...
    tx, err := conn.Begin()
    if err != nil {
        panic(err)
    }

    // Will automatically rollback if call to sample fails...
    defer tx.Close() 

    if err := Sample(tx, "Frank"); err != nil {
        fmt.Println("Frank failed!", err.Error())
        return
    }

    // Don't forget to commit, or you'll automatically rollback on 
    // "defer tx.Close()" above!
    if err := tx.Commit(); err != nil {
        fmt.Println("Unable to save changes to the database:", err.Error())
    }
}

Using a hermes.Conn parameter in a function also opens up in situ testing of database functionality. You can create a transaction in the test case and pass it to a function that takes a hermes.Conn, run any tests on the results of that function, and simply let the transaction rollback at the end of the test to clean up.

var DB hermes.Conn

// We'll just open one database connection pool to speed up testing, so 
// we're not constantly opening and closing connections.
func TestMain(m *testing.M) {
    conn, err := hermes.Connect(DBTestURI)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Unable to open a database connection: %s\n", err)
        os.Exit(1)
	}
	defer conn.Close()
	
	DB = conn
	
	os.Exit(m.Run())
}

// Test getting a user account from the database.  The signature for the
// function is:  `func GetUser(conn hermes.Conn, email string) (User, error)`
// 
// Passing a hermes.Conn value to the function means we can pass in either
// a reference to the database pool and really update the data, or we can
// pass in the same transaction reference to both the SaveUser and GetUser
// functions.  If we use a transaction, we can let the transaction roll back 
// after we test these functions, or at any failure point in the test case,
// and we know the data is cleaned up. 
func TestGetUser(t *testing.T) {
    u := User{
        Email: "jdoe@nowhere.com",
        Name: "John Doe",
    }
    
    tx, err := db.Begin()
    if err != nil {
        t.Fatal(err)
    }
    defer tx.Close()
    
    if err := tx.SaveUser(tx, u); err != nil {
        t.Fatalf("Unable to create a new user account: %s", err)
    }
    
    check, err := tx.GetUser(tx, u.Email)
    if err != nil {
        t.Fatalf("Failed to get user by email address: %s", err)
    }
    
    if check.Email != u.Email {
        t.Errorf("Expected user email to be %s; was %s", u.Email, check.Email)
    } 
    
    if check.Name != u.Name {
        t.Errorf("Expected user name to be %s; was %s", u.Name, check.Name)
    } 
    
    // Note:  do nothing...when the test case ends, the `defer tx.Close()`
    // is called, and all the data in this transaction is rolled back out.
}

Using transactions, even if a test case fails a returns prematurely, the database transaction is automatically closed, thanks to defer. The database is cleaned up without any fuss or need to remember to delete the data you created at any point in the test.

References

https://github.com/jackc/pgx

Documentation

Index

Constants

View Source
const (
	OperatorIntervention = "57000"
	QueryCanceled        = "57014"
	AdminShutdown        = "57P01"
	CrashShutdown        = "57P02"
	CannotConnectNow     = "57P03"
	DatabaseDropped      = "57P04"
	IdleSessionTimeout   = "57P05"
)

PostgreSQL disconnect errors - https://www.postgresql.org/docs/current/errcodes-appendix.html

Variables

View Source
var (
	// Disconnects is the list of PostgreSQL error codes that indicate the connection failed.
	Disconnects = []string{
		OperatorIntervention,
		QueryCanceled,
		AdminShutdown,
		CrashShutdown,
		CannotConnectNow,
		DatabaseDropped,
		IdleSessionTimeout,
	}
)

Functions

func IsDisconnected added in v1.1.0

func IsDisconnected(err error) bool

IsDisconnected returns true if the error is a PostgreSQL disconnect error (SQLSTATE 57P01).

func NoRows added in v0.2.0

func NoRows(err error) bool

NoRows returns true if the supplied error is one of the NoRows indicators

func Register

func Register(dataType pgtype.DataType)

Register a new datatype to be associated with connections, such as a custom UUID or time data types. Best to call this before calling Connect.

Types

type Conn

type Conn interface {
	// Begin starts a transaction.  If Conn already represents a transaction, pgx will create a
	// savepoint instead.
	Begin(ctx context.Context) (Conn, error)

	// Commit the transaction.  Does nothing if Conn is a *pgxpool.Pool.  If the transaction is
	// a psuedo-transaction, i.e. a savepoint, releases the savepoint.  Otherwise commits the
	// transaction.
	Commit(ctx context.Context) error

	// Rollback the transaction. Does nothing if Conn is a *pgxpool.Pool.
	Rollback(ctx context.Context) error

	// Close rolls back the transaction if this is a real transaction or rolls back to the
	// savepoint if this is a pseudo nested transaction.  For a *pgxpool.Pool, this call is
	// ignored.
	//
	// Returns ErrTxClosed if the Conn is already closed, but is otherwise safe to call multiple
	// times. Hence, a defer conn.Close() is safe even if conn.Commit() will be called first in
	// a non-error condition.
	//
	// Any other failure of a real transaction will result in the connection being closed.
	Close(ctx context.Context) error

	CopyFrom(ctx context.Context, tableName pgx.Identifier, columnNames []string, rowSrc pgx.CopyFromSource) (int64, error)
	SendBatch(ctx context.Context, b *pgx.Batch) pgx.BatchResults

	Exec(ctx context.Context, sql string, arguments ...interface{}) (commandTag pgconn.CommandTag, err error)
	Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error)
	QueryRow(ctx context.Context, sql string, args ...interface{}) pgx.Row
}

Conn abstracts the *pgxpool.Pool struct and the pgx.Tx interface into a common interface. This can be useful for building domain models more functionally, i.e the same function could be used for a single database query outside of a transaction, or included in a transaction with other function calls.

It's also useful for testing, as you can pass a transaction into any database-related function, don't commit, and simply Close() at the end of the test to clean up the database.

type DB

type DB struct {
	*pgxpool.Pool
}

DB wraps the *pgxpool.Pool and provides the missing hermes function wrappers.

func Connect

func Connect(uri string) (*DB, error)

Connect creates a pgx database connection pool and returns it.

func ConnectConfig

func ConnectConfig(config *pgxpool.Config) (*DB, error)

ConnectConfig creates a pgx database connection pool based on a pool configuration and returns it.

func (*DB) Begin

func (db *DB) Begin(ctx context.Context) (Conn, error)

Begin a new transaction.

func (*DB) Close

func (db *DB) Close(context.Context) error

Close does nothing.

func (*DB) Commit

func (db *DB) Commit(context.Context) error

Commit does nothing.

func (*DB) Rollback added in v0.2.0

func (db *DB) Rollback(_ context.Context) error

Rollback does nothing

type RowScanner added in v0.2.0

type RowScanner interface {
	Scan(dest ...interface{}) error
}

RowScanner is a shared interface between pgx.Rows and pgx.Row

type Tx

type Tx struct {
	pgx.Tx
}

Tx wraps the pgx.Tx interface and provides the missing hermes function wrappers.

func (*Tx) Begin

func (tx *Tx) Begin(ctx context.Context) (Conn, error)

Begin starts a pseudo nested transaction.

func (*Tx) Close

func (tx *Tx) Close(ctx context.Context) error

Close rolls back the transaction if this is a real transaction or rolls back to the savepoint if this is a pseudo nested transaction.

Returns ErrTxClosed if the Conn is already closed, but is otherwise safe to call multiple times. Hence, a defer conn.Close() is safe even if conn.Commit() will be called first in a non-error condition.

Any other failure of a real transaction will result in the connection being closed.

Jump to

Keyboard shortcuts

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