repo

package module
v0.1.15 Latest Latest
Warning

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

Go to latest
Published: Aug 17, 2022 License: MPL-2.0 Imports: 13 Imported by: 0

README

GoDoc

repo module for database CRUD

repo provides a set of convenience methods for carrying out CRUD operations on a single sql-database table.

Install

go get gitlab.com/msts-public/general/gomods/repo

Description

repo is designed for use with a zerolog logger, and uses sqlx database connection and transaction types.

While repo has been set up to support some pgx types, and the integration test uses pgconn, it should still work if not using these. Limitations in type support mainly affect the mocking, and not the core functionality. The core functionality should support any type that supports sql.Value.

TODO: add support from more types to repomock, eg decimal

When initializing a repo, a record struct is provided, and repo extracts column names based on db tags. A basic set of methods for CRUD statements, Create, Update, Delete (logical delete), Remove (database delete), GetById, and GetByParam are then available for interacting with the database table. Also provided are Count and CountByParam methods.

Limitations:

  • builder does not support OR clauses
  • some operators, eg array operators and between, only support certain basic types of variables - string and ints for array IN and string, ints and time.Time for BETWEEN
    • work around for between is to use two clauses with <= and >= operators
  • comparison function in mock defaults to comparing values as strings, and only supports a limited set of types that are not easily/correctly compared as strings

Example - setup


import "gitlab.com/msts-public/general/gomods/repo"

type Record struct {
	ID      string            `db:"id"`
	Col1    string            `db:"col_1"`
	Col2    int               `db:"col_2"`
	Created time.Time         `db:"created"`
	Updated time.Time         `db:"updated"`
	Deleted sql.NullTimeStamp `db:"deleted"`
}

// filters - defines which column are excluded from the Create, Update and Return column lists
var filters = repo.ColumnFilters{
	Create: map[string]*string{"deleted": nil},
	// Note: for updates, this is the set of column that not will be updated. Updated record is based on the ID.
	Update: map[string]*string{"id": nil, "created": nil, "deleted": nil},
	Return: map[string]*string{"deleted": nil},
}
type RecordRepo struct {
	repo.Base
}

func NewRecordRepo(l zerolog.Logger, tx *sqlx.Tx) {*RecordRepo, error} {
	rr := &RecordRepo{
		repo.Base{
			Tx:                  tx,
			Logger:              l,
			Filters:             filters,
			LogicalDeleteColumn: "deleted",
		},
	}

	// initialize the column lists and statements
	err := rr.Init(&Record{})
	if err != nil {
		return err
	}

	// pass the transaction to the repo
	err := rr.InitTx(tx)
	if err != nil {
		return err
	}

	return rr, nil
}

// GetByID -
func (rr *RecordRepo) GetByID(id string, forUpdate bool) (*Record, error) {
	rec := &Record{}
	if err := rr.GetRecByID(rr.GetByIDSQL(), id, rec, forUpdate); err != nil {
		return nil, err
	}
	return rec, nil
}

// GetByParam -
func (rr *RecordRepo) GetByParam(params []repo.Restriction, options *repo.Options) ([]*Record, error) {
	// records
	recs := []*Record{}

	rows, err := rr.GetRowsByParam(rr.GetByParamSQL(), params, options)
	if err != nil {
		rr.Logger.Warn().Err(err).Msg("Failed querying row")
		return nil, err
	}
	defer rows.Close()

	for rows.Next() {
		rec := &Record{}
		err := rows.StructScan(rec)
		if err != nil {
			rr.Logger.Warn().Err(err).Msg("Failed executing struct scan")
			return nil, err
		}
		recs = append(recs, rec)
	}

	return recs, nil
}

// Create -
func (rr *RecordRepo) Create(rec *Record) error {

	rec.Created = time.Now().UTC()

	err := rr.CreateRec(rr.GetCreateSQL(), rec)
	if err != nil {
		rr.Logger.Warn().Err(err).Msg("Failed creating record")
		return err
	}

	return nil
}

// Update -
func (rr *RecordRepo) Update(rec *Record) error {
	prevUpd := rec.Updated
	rec.Updated = time.Now().UTC()

	err := rr.UpdateRec(rr.GetUpdateSQL(), rec)
	if err != nil {
		rec.ResetUpdated(prevUpd)
		rr.Logger.Warn().Err(err).Msg("Failed updating record")
		return err
	}

	return nil
}

// Delete -
func (rr *RecordRepo) Delete(id string) error {
	return rr.DeleteRec(rr.GetDeleteSQL(), id)
}

// Remove -
func (rr *RecordRepo) Remove(id string) error {
	return rr.RemoveRec(rr.GetRemoveSQL(), id)
}

Building a query

A query can be built up to select base on additional criteria (other than ID) using the GetByParams function. This takes two parameters:

  • params - the set of column restrictions to apply to the query, ie the WHERE clause of the query. This is an array of structs containing
    • Column - the name of the column the restriction applies to (required)
    • Value - the value the column will be compared to (required for non-unary operators)
    • Operator - the operator used to make the comparison (defaults to '=' or 'IN' as appropriate based on the type of Value)
  • options - the set of additional clauses to apply to the query. These are:
    • OrderBy - array of structs defining sort order. Order in the array determines order in the ORDER BY clause. Consists of
      • Column - name of column to sort by
      • Direction - sort direction, either ASC or DESC (case insensitive), (defaults to ASC)
    • ForUpdate - FOR UPDATE clause, can be either skip or nowait
    • Limit - LIMIT clause to restrict the maximum number of records to return
    • Offset - OFFSET clause, number of records to skip before returning records. Should only be used in conjunction with an ORDER BY clause or the results could be unpredictable.

Example:

  params: = []repo.Restriction{
  	{Column: 'created_at', Value: time.Now().Add(0,0,-1), Operator: repo.OperatorGreaterThan}, // records created in the last 24 hours
  	{Column: 'status', Value: []string{'pending','open'},                                      // with a status of 'pending' or 'open' (uses IN operator)
  	{Column: 'assigned_to', Operator: OperatorIsNull},                                         // and not assigned (unary operator)
  }
  options := &repo.Options{
  	OrderBy: []repo.Ordering{{Column: 'priority'},{Column: 'impact', Direction: 'DESC'},{'created_at', 'asc'}},
  	Limit: 10,
  	Offset: 0,
  }

  for {
	  recs, err := r.GetByParams(params, options)
	  break if recs == nil
	  ...
	  options.Offset += options.Limit
  }

Documentation

Overview

Package repo provide a mechanism for accessing data in a single database table

Index

Constants

View Source
const (
	// Operators -
	OperatorEqual              = "="  // default operator for non-array values
	OperatorIn                 = "in" // default operator for array values
	OperatorNotIn              = "not in"
	OperatorBetween            = "between"
	OperatorLike               = "like"
	OperatorIsNull             = "is null"
	OperatorIsNotNull          = "is not null"
	OperatorNotEqualTo         = "!="
	OperatorLessThanEqualTo    = "<="
	OperatorLessThan           = "<"
	OperatorGreaterThanEqualTo = ">="
	OperatorGreaterThan        = ">"
	OperatorArrayContains      = "@>"
	OperatorArrayContainedIn   = "<@"

	// Clause Options -
	ClauseOrderByAscending  = "order_by_asc"
	ClauseOrderByDescending = "order_by_desc"
	ClauseOrderBy           = "order_by"
	ClauseForUpdate         = "for_update"
	ClauseLimit             = "limit"
	ClauseOffset            = "offset"

	// for update lock options
	LockOptionSkip   = "skip"
	LockOptionNoWait = "nowait"

	// error details
	ErrBetweenStringHasTwoVals = "String value for between operator on >%s< should have 2 values separated by a comma"
	ErrBetweenArrayHasTwoVals  = "Array value for between operator on >%s< should have exactly 2 values"
	ErrBetweenBounds           = "Upper bound must not be less than lower bound for between operator"
)

package constants for builder

View Source
const (
	TitleAccess    string = "Access Denied"
	TitleIntegrity string = "Data Integrity Error"
	TitleInvalid   string = "Invalid Request"
	TitleNoData    string = "Data Not Found"
	TitleData      string = "Data Exception"
	TitleDatabase  string = "Database Error"
	TitleQuery     string = "Query Error"
	TitleRepo      string = "Repo Setup Error"
	TitleSyntax    string = "Syntax Error"
	TitleService   string = "Service Not Available"
	TitleSystem    string = "System Error"

	QueryNoRows      string = "Query returned no records"
	NilRecord        string = "Nil record supplied"
	ErrInvalidColumn string = "Unrecognised column name for table"
)

Database errors -

View Source
const (
	StatementCreate               string = "create"
	StatementUpdate               string = "update"
	StatementDelete               string = "delete"
	StatementRemove               string = "remove"
	StatementSelectByID           string = "select by id"
	StatementSelectByIDs          string = "select by ids"
	StatementSelectByAltID        string = "select by alt id"
	StatementSelectByParam        string = "select by param"
	StatementSelectForUpdate      string = "select for update"
	StatementSelectByAltForUpdate string = "select by alt for update"
	StatementCount                string = "select count"

	ColumnsCreate string = "create"
	ColumnsUpdate string = "update"
	ColumnsReturn string = "return"

	ForUpdateNowait string = " for no key update nowait"
	ForUpdateSkip   string = " for no key update skip locked"
)

package constants

Variables

This section is empty.

Functions

func IsErrNoRows added in v0.1.5

func IsErrNoRows(err error) bool

IsErrNoRows checks if a given error is a database no rows found error

Types

type Base

type Base struct {
	// Services
	Logger      zerolog.Logger
	Tx          *sqlx.Tx
	TableName   string
	OrderByFunc func() string
	LockOption  string

	// bare SQL statements
	VarGetByIDSQL      string
	VarGetByAltIDSQL   string
	VarGetByParamSQL   string
	VarCreateRecordSQL string
	VarUpdateRecordSQL string
	VarDeleteRecordSQL string
	VarRemoveRecordSQL string

	// lists of table column names to be used in statments
	CreateColumns Columns
	UpdateColumns Columns
	ReturnColumns Columns

	ColumnMap map[string]Columns

	Filters             ColumnFilters // columns to exclude from lists
	LogicalDeleteColumn string        // name of column used for logical deletes
	AltIDColumn         string        // alternative unique key field name
	// contains filtered or unexported fields
}

Base is the base set of properties for a repository

func (*Base) AddCustom added in v0.1.8

func (r *Base) AddCustom(name, stmt, alias string) error

AddCustom adds a custom query to rebp. As any custom query is intended to return a record

(or array of records) of repo table type, this allows the column list to be specified
 by '%s' and substituted in from parsed table columns as in standard queries

func (*Base) Count

func (r *Base) Count() (int, error)

Count returns a count of the number of records in the table

func (*Base) CountByParam

func (r *Base) CountByParam(params []Restriction) (int, error)

CountByParam returns a count of the number of records in the table that match the given params

func (*Base) CreateRec

func (r *Base) CreateRec(createSQL string, rec interface{}) error

CreateRec creates a record in the database from the provided record struct

func (*Base) DeleteRec

func (r *Base) DeleteRec(deleteSQL string, recID interface{}) error

DeleteRec logically deletes a record in the database for the given primary key value

func (*Base) ErrEmptyParam

func (r *Base) ErrEmptyParam(param string) *Error

ErrEmptyParam is the error for a missing repo.Restriction Value when one is expected

func (*Base) ErrOperatorNotSupported

func (r *Base) ErrOperatorNotSupported(op string) *Error

ErrOperatorNotSupported is the error for an unrecognised operator specification in repo.Restriction

func (*Base) GenerateStatement

func (r *Base) GenerateStatement(statementType string, columns map[string]Columns) (stmt string)

GenerateStatement generates the standard SQL statements statements requiring non-standard column lists must be explicitly generated by the user

func (*Base) GetByAltIDSQL added in v0.1.5

func (r *Base) GetByAltIDSQL() string

GetByAltIDSQL generates the SQL statement for fetching records by unique key

func (*Base) GetByIDSQL added in v0.1.2

func (r *Base) GetByIDSQL() string

GetByIDSQL generates the SQL statement for fetching records by primary key

func (*Base) GetByParamSQL added in v0.1.2

func (r *Base) GetByParamSQL() string

GetByParamSQL generates the base SQL statement for fetching records by provided parameters

func (*Base) GetColumnList added in v0.1.2

func (r *Base) GetColumnList(name string) []string

GetColumnList - Colunm list getter

func (*Base) GetCreateSQL added in v0.1.2

func (r *Base) GetCreateSQL() string

GetCreateSQL generates the SQL statement for creating a record

func (*Base) GetDeleteSQL added in v0.1.2

func (r *Base) GetDeleteSQL() string

GetDeleteSQL generates the SQL statement for logically deleting a record

func (*Base) GetRecByAltID added in v0.1.5

func (r *Base) GetRecByAltID(querySQL string, recID interface{}, rec interface{}, forUpdate bool) error

GetRecByAltID fetchs a table record by the alternate unique key value

func (*Base) GetRecByID

func (r *Base) GetRecByID(querySQL string, recID interface{}, rec interface{}, forUpdate bool) error

GetRecByID fetches a table record by the primary key value

func (*Base) GetRemoveSQL added in v0.1.2

func (r *Base) GetRemoveSQL() string

GetRemoveSQL generates the SQL statement for deleting a record

func (*Base) GetRowByID

func (r *Base) GetRowByID(querySQL string, recID interface{}, forUpdate bool) (*sqlx.Row, error)

GetRowByID fetches a table row by the primary key value

func (*Base) GetRowsByParam

func (r *Base) GetRowsByParam(
	querySQL string,
	params []Restriction,
	options *Options,
) (rows *sqlx.Rows, err error)

GetRowsByParam - fetch set of table rows filtered by the provided parameters and options

func (*Base) GetRowsCustom added in v0.1.8

func (r *Base) GetRowsCustom(queryName string, params map[string]interface{}) (*sqlx.Rows, error)

GetRowsCustom fetches a set of table rows filtered by the custom query

func (*Base) GetUpdateSQL added in v0.1.2

func (r *Base) GetUpdateSQL() string

GetUpdateSQL generates the SQL statement for updating a record

func (*Base) Init

func (r *Base) Init(rec interface{}) error

Init initialises the repository

func (*Base) InitTx added in v0.1.2

func (r *Base) InitTx(tx *sqlx.Tx) error

InitTx initialises the repository with a new DB tx

func (*Base) OrderBy

func (r *Base) OrderBy() string

OrderBy returns the default order by clause for queries

func (*Base) RemoveRec

func (r *Base) RemoveRec(removeSQL string, recID interface{}) error

RemoveRec deletes a record in the database for the given primary key value

func (*Base) SetLockOption added in v0.1.14

func (r *Base) SetLockOption(lock string) error

SetLockOption allows the default lock option to be set on the repo. This will clear any cached 'for update' statemens. Valid option are "skip", "nowait", "default"

func (*Base) UpdateRec

func (r *Base) UpdateRec(updateSQL string, rec interface{}) error

UpdateRec updates a record in the database from the provided record struct

type BaseMock

type BaseMock struct {
	Logger    zerolog.Logger
	TableName string
}

BaseMock - Base properties for all repo's

func (*BaseMock) CompareParams

func (r *BaseMock) CompareParams(params []Restriction, rec interface{}) (bool, error)

CompareParams iterates over parameters and struct column to find matches, to mimic real query filtering. Not all possible options are necessarily covered, so some restrictions may not give the expected dataset.

func (*BaseMock) GetByParamSQL

func (r *BaseMock) GetByParamSQL() string

GetByParamSQL -

func (*BaseMock) GetRowsByParam

func (r *BaseMock) GetRowsByParam(querySQL string, params []Restriction, options *Options) (rows *sqlx.Rows, err error)

GetRowsByParam - satisfy interface requirements

func (*BaseMock) Init

func (r *BaseMock) Init() error

Init - satisfy interface requirements

func (*BaseMock) InitTx added in v0.1.5

func (r *BaseMock) InitTx(tx *sqlx.Tx) error

InitTx - satisfy interface requirements

func (*BaseMock) SetLockOption added in v0.1.14

func (r *BaseMock) SetLockOption(lock string) error

SetLockOption - satisfy interface requirements

type ColumnFilters added in v0.1.2

type ColumnFilters struct {
	Create map[string]*string
	Update map[string]*string
	Return map[string]*string
}

ColumnFilters specifies columns to exclude from lists and statements

type Columns

type Columns []string

Columns is an array of string representing the column names for a table

func GetColumns

func GetColumns(rec interface{}) Columns

GetColumns extracts column names from a given struct based on `db` tags

func (Columns) ToList

func (c Columns) ToList() string

ToList returns Colunms as a comma separated list in a string

func (Columns) ToPrefixedList

func (c Columns) ToPrefixedList(prefix string) string

ToPrefixedList returns Colunms as a comma separated list in string, with each item prepended with the given prefix

func (Columns) ToUpdateList

func (c Columns) ToUpdateList() string

ToUpdateList formats Colunms for a SET clause of a UPDATE statement with each column set to a bind variable of the same name

type Error

type Error struct {
	Title   string
	Context string
	Detail  string
	Value   interface{}
}

Error - struct for error details

func ErrNoRows

func ErrNoRows(context string) *Error

ErrNoRows provides standard for the database no rows found error

func NewAccessError

func NewAccessError(err error) *Error

NewAccessError returns a generic error wrapped in standard Error struct

func NewDatabaseError

func NewDatabaseError(context, message string, value interface{}) *Error

NewDatabaseError provides standard format for database error details

func NewFromDatabaseError

func NewFromDatabaseError(err error, defaultContext string) *Error

NewFromDatabaseError converts a pq.Error to a Error

func NewRepoError

func NewRepoError(context, message string, value interface{}) *Error

NewRepoError provides standard format for non-database errors

func (Error) Error

func (e Error) Error() string

Error returns error details as strinG

type Options added in v0.1.5

type Options struct {
	OrderBy   []Ordering
	ForUpdate string
	Limit     int
	Offset    int
}

Options specifies options for non-restriction clauses to apply to a query

type Ordering added in v0.1.7

type Ordering struct {
	Column    string
	Direction string // should only be ASC or DESC
}

Ordering specifies columns to order query results by

type QueryParams added in v0.1.5

type QueryParams = map[string]interface{}

QueryParams is the format in which parameters are provided to the database layer for queries

type Restriction

type Restriction struct {
	Column   string
	Operator string
	Value    interface{}
}

Restriction specifies a column restriction to apply to a query

Jump to

Keyboard shortcuts

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