depot

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Apr 24, 2021 License: Apache-2.0 Imports: 7 Imported by: 0

README

depot

CI Status Go Report Card Package Doc Releases

depot is a thin abstraction layer for accessing relational databases using Golang (technically, the concepts used by depot should be applicable to other databases as well).

depot is implemented to provide a more convenient API to applications while stil remaining what I consider to be idiomatic go.

depot is under heavy development and not ready for production systems.

Usage

depot requires at least Go 1.14.

Installation

$ go get github.com/halimath/depot

API

The fundamental type to interact with depot is the Session. A Session is bound to a Context and wraps a database transaction. To obtain a session, you use a SessionFactory which in turn wraps a sql.DB. You may create a SessionFactory either by calling the depot.Open function passing in the same arguments you would pass to sql.Open, or you create a sql.DB value yourself (i.e. if you want to configure connection pooling) and pass this to depot.NewSessionFactory.

Once you have a factory, call its Session method to create a session.

The Session provides methods to commit or rollback the wrapped transaction. Make sure you call one of these methods to finish the transaction.


factory := depot.Open("sqlite3", "./test.db", nil)

ctx := context.Background()
session, ctx, err := factory.Session(ctx)

err := session.Insert(depot.Into("test"), depot.Values{
    "id": 1,
    "message": "hello, world",
})
if err != nil {
    log.Fatal(err)
}

if err := session.CommitIfNoError(); err != nil {
    log.Fatal(err)
}

See acceptance-test.go for an almost complete API example.

Code Generator

The code generator provided by depot can be used to generate data access types - called repositories - for Go-struct types.

In order to generate a repository the struct's fields must be tagged using standard Go tags with the key depot. The value is the column name to map the field to.

The following struct shows the generation process:

type (
	// Message demonstrates a persistent struct showing several mapped fields.
	Message struct {
		ID         string    `depot:"id,id"`
		Text       string    `depot:"text"`
		OrderIndex int       `depot:"order_index"`
		Length     float32   `depot:"len"`
		Attachment []byte    `depot:"attachment"`
		Created    time.Time `depot:"created"`
	}
)

The struct Message should be mapped to a table messages with each struct field being mapped to a column. Every field tagged with a depot tag will be part of the mapping. All other fields are ignored.

The column name is given as the tag's value. The field ID being used as the primary key is further marked with id directive separated by a comma.

To generate a repository for this model, invoke depot with the following command line:

$ depot generate-repo --table=messages --repo-package repo --out ../repo/gen-messagerepo.go models.go Message

You can also use go:generate to do the same thing. Simply place a comment like

//go:generate depot generate-repo --table=messages --repo-package repo --out ../repo/gen-messagerepo.go $GOFILE Message

in the source file containing Message. You can place multiple comments of that type in a single source file containing multiple model structs.

The generated repository provides the following methods:

type MessageRepo struct {
	factory *depot.Factory
}

func NewMessageRepo(factory *depot.Factory) *MessageRepo
func (r *MessageRepo) Begin(ctx context.Context) (context.Context, error)
func (r *MessageRepo) Commit(ctx context.Context) error
func (r *MessageRepo) Rollback(ctx context.Context) error
func (r *MessageRepo) fromValues(vals depot.Values) (*models.Message, error)
func (r *MessageRepo) find(ctx context.Context, clauses ...depot.Clause) ([]*models.Message, error)
func (r *MessageRepo) count(ctx context.Context, clauses ...depot.Clause) (int, error)
func (r *MessageRepo) LoadByID(ctx context.Context, ID string) (*models.Message, error)
func (r *MessageRepo) toValues(entity *models.Message) depot.Values
func (r *MessageRepo) Insert(ctx context.Context, entity *models.Message) error
func (r *MessageRepo) delete(ctx context.Context, clauses ...depot.Clause) error
func (r *MessageRepo) Update(ctx context.Context, entity *models.Message) error
func (r *MessageRepo) DeleteByID(ctx context.Context, ID string) error
func (r *MessageRepo) Delete(ctx context.Context, entity *models.Message) error

Use Begin, Commit and Rollback to control the transaction scope. The transaction is stored as part of the Context. Under the hood all of the methods use the Session described above.

find and count are methods that can be used by custom finder methods. They execute select queries for the entity. LoadByID uses find to load a single message by ID.

The mutation methods all handle single instances of Message. delete is provided similar to find to do batch deletes.

Note that all of these methods contain simple to read and debug go code with no reflection being used at all. The code is idiomatic and in most cases looks like being written by a human go programmer.

You can easily extend the repo with custom finder methods by writing a custom method being part of MessageRepo to another non-generated file and use find to do the actual work. Here is an example for a method to load Messages based on the value of the Text field.

func (r *MessageRepo) FindByText(ctx context.Context, text string) ([]*models.Message, error) {
	return r.find(ctx, depot.Where("text", text))
}
null values

If a mapped field should support SQL null values, you have to add the nullable directive to the field's mapping tag:

type Entity struct {
	// ...
	Foo *string `depot:"foo,nullable"`
}

You can either use a pointer type as in the example above or the plain type (string in this case). Using a pointer is recommended, as SQL null values will be represented as nil. When using the plain type, null is represented with the value's default type ("" in this case) which only works when reading null from the datase. If you wish to insert or update a null value you are required to use a pointer type.

List of directives

The following table lists all supported directives for field mappings.

Directive Used for Example Description
id Mark a field as the entity's ID. ID string "depot:\"id,id\"" Only a single field may be tagged with id. If one is given, the generated repo will contain the methods LoadByID and DeleteByID which are not generated when no ID is declared.
nullable Mark a field as being able to store a null value. Message *string "depot:\"msg,nullable\"" See the section above for null values.

See the example app for a working example.

Open Issues

depot is under heavy development. Expect a lot of bugs. A list of open features can be found in TODO.md.

License

Copyright 2021 Alexander Metzner.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Documentation

Overview

Package depot implements a small abstraction layer for accessing sql (and potentially other) databases.

depot is build around two concepts: values and clauses. See the respective descriptions for details.

depot also provides a code generator used to generate repository types from regulare go structs with field tags. The resulting code uses no reflection and provides a typesafe interface to interact with the database.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNoResult is returned when queries execpted to match (at least) on row
	// match no row at all.
	ErrNoResult = errors.New("no result")

	// ErrRollback is returned when trying to commit a session that has already been
	// rolled back.
	ErrRollback = errors.New("rolled back")
)

Functions

This section is empty.

Types

type Clause

type Clause interface {
	// SQL returns the SQL query part expressed by this clause.
	SQL() string

	// Args returns any positional arguments used by this clause.
	Args() []interface{}
}

Clause defines the interface implemented by all clauses used to describe different parts of a query. A clause captures optional arguments.

type ColsClause

type ColsClause interface {
	Clause

	// Names returns the list of column names to select in query order.
	Names() []string
	// contains filtered or unexported methods
}

ColsClause defines a Clause used to select columns.

func Cols

func Cols(cols ...string) ColsClause

Cols implements a factory for a ColsClause.

type Factory

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

Factory provides functions to create new Sessions.

func NewSessionFactory

func NewSessionFactory(pool *sql.DB, options *FactoryOptions) *Factory

NewSessionFactory creates a new Factory using connections from the given pool. When providing nil for the options, default options are used.

func Open

func Open(driver, dsn string, options *FactoryOptions) (*Factory, error)

Open opens a new database pool and wraps it as a SessionFactory.

func (*Factory) Close

func (f *Factory) Close()

Close closes the factory and the underlying pool

func (*Factory) Session

func (f *Factory) Session(ctx context.Context) (*Session, context.Context, error)

Session creates a new Session and binds it to the given context.

type FactoryOptions added in v0.2.0

type FactoryOptions struct {
	// When set to true all SQL statements will be logged using the log package.
	LogSQL bool
}

FactoryOptions defines the additional options for a Factory.

type OrderByClause

type OrderByClause interface {
	Clause
	// contains filtered or unexported methods
}

OrderByClause defines the interface used to sort rows.

func Asc

func Asc(column string) OrderByClause

Asc returns an OrderByClause sorting by the given column in ascending order.

func Desc

func Desc(column string) OrderByClause

Desc returns an OrderByClause sorting by the given column in descending order.

func OrderBy

func OrderBy(column string, asc bool) OrderByClause

OrderBy constructs a new OrderByClause.

type Scanner

type Scanner interface {
	Scan(vals ...interface{}) error
}

Scanner defines the Scan method provided by sql.Rows and sql.Row, as the sql package does not define such an interface.

type Session

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

Session defines an interaction session with the database. A session uses a single transaction and is bound to a single Context. A session provides an abstract interface built around Values and Clauses.

func GetSession

func GetSession(ctx context.Context) (*Session, bool)

GetSession returns the Session associated with the given Context and a boolean flag (ok) indicating if a session has been registered with the given context.

func MustGetSession added in v0.2.0

func MustGetSession(ctx context.Context) *Session

MustGetSession returns the Session associated with the given Context. This function panics when no Session has been stored in the Context.

func (*Session) Commit

func (s *Session) Commit() error

Commit commits the session's transaction and returns an error if the commit fails.

func (*Session) DeleteMany

func (s *Session) DeleteMany(from TableClause, where ...Clause) error

DeleteMany deletes all matching rows from the database.

func (*Session) Error

func (s *Session) Error(err error)

Error marks the transaction as failed so it cannot be committed later on. Calling Error with a nil error clears the error state of the transaction.

func (*Session) Exec added in v0.2.0

func (s *Session) Exec(query string, args ...interface{}) error

Exec executes the given query passing the given args and returns the resulting error or nil. This is just a wrapper for calling ExexContext on the wrapped transaction.

func (*Session) InsertOne

func (s *Session) InsertOne(into TableClause, values Values) error

InsertOne inserts a single row.

func (*Session) QueryCount

func (s *Session) QueryCount(from TableClause, clauses ...Clause) (count int, err error)

QueryCount executes a counting query and returns the number of matching rows.

func (*Session) QueryMany

func (s *Session) QueryMany(cols ColsClause, from TableClause, clauses ...Clause) ([]Values, error)

QueryMany executes a query that is expected to match any number of rows. The rows are returned as Values. If the query did not match any row an empty slice is returned.

func (*Session) QueryOne

func (s *Session) QueryOne(cols ColsClause, from TableClause, where ...Clause) (Values, error)

QueryOne executes a query that is expected to return a single result. The columns, table and selection criteria are given as Clauses. QueryOne returns the selected values which is never nil. ErrNoResult is returned when the query did not match any rows.

func (*Session) Rollback

func (s *Session) Rollback() (err error)

Rollback rolls the session's transaction back and returns any error raised during the rollback.

func (*Session) UpdateMany

func (s *Session) UpdateMany(table TableClause, values Values, where ...Clause) error

UpdateMany updates all matching rows with the same values given.

type TableClause

type TableClause interface {
	Clause
	// contains filtered or unexported methods
}

TableClause implements a clause used to name a table.

func From

func From(table string) TableClause

From is an alias for Table supporting a more DSL style interface.

func Into

func Into(table string) TableClause

Into is an alias for Table supporting a more DSL style interface.

func Table

func Table(table string) TableClause

Table creates a TableClause from the single table name.

type Values

type Values map[string]interface{}

Values contains the persistent column values for an entity either after reading the values from the database to re-create the entity value or to persist the entity's values in the database (either for insertion or update).

func (Values) GetBool added in v0.2.0

func (v Values) GetBool(key string) (bool, bool)

GetBool returns the value associated with key as a boolean.

func (Values) GetBytes added in v0.2.0

func (v Values) GetBytes(key string) ([]byte, bool)

GetBytes returns the value associated with key as a byte slice.

func (Values) GetFloat32 added in v0.2.0

func (v Values) GetFloat32(key string) (float32, bool)

GetFloat32 returns the value associated with key as a float32.

func (Values) GetFloat64 added in v0.2.0

func (v Values) GetFloat64(key string) (float64, bool)

GetFloat64 returns the value associated with key as a float64.

func (Values) GetInt added in v0.2.0

func (v Values) GetInt(key string) (int, bool)

GetInt returns the value associated with key as an int.

func (Values) GetInt16 added in v0.2.0

func (v Values) GetInt16(key string) (int16, bool)

GetInt16 returns the value associated with key as an int16.

func (Values) GetInt32 added in v0.2.0

func (v Values) GetInt32(key string) (int32, bool)

GetInt32 returns the value associated with key as an int32.

func (Values) GetInt64 added in v0.2.0

func (v Values) GetInt64(key string) (int64, bool)

GetInt64 returns the value associated with key as an int64.

func (Values) GetInt8 added in v0.2.0

func (v Values) GetInt8(key string) (int8, bool)

GetInt8 returns the value associated with key as an int8.

func (Values) GetString added in v0.2.0

func (v Values) GetString(key string) (string, bool)

GetString returns the names value converted to a string.

func (Values) GetTime added in v0.2.0

func (v Values) GetTime(key string) (time.Time, bool)

GetTime returns the value associated with key as a time.Time.

func (Values) GetUInt added in v0.2.0

func (v Values) GetUInt(key string) (uint, bool)

GetUInt returns the value associated with key as an uint.

func (Values) GetUInt16 added in v0.2.0

func (v Values) GetUInt16(key string) (uint16, bool)

GetUInt16 returns the value associated with key as a uint16.

func (Values) GetUInt32 added in v0.2.0

func (v Values) GetUInt32(key string) (uint32, bool)

GetUInt32 returns the value associated with key as an uint32.

func (Values) GetUInt64 added in v0.2.0

func (v Values) GetUInt64(key string) (uint64, bool)

GetUInt64 returns the value associated with key as an uint64.

func (Values) GetUInt8 added in v0.2.0

func (v Values) GetUInt8(key string) (uint8, bool)

GetUInt8 returns the value associated with key as an uint8.

func (Values) IsNull added in v0.3.0

func (v Values) IsNull(key string) bool

IsNull returns true if the value store for key is the SQL value `NULL`.

type WhereClause

type WhereClause interface {
	Clause
	// contains filtered or unexported methods
}

WhereClause defines the interface implemented by all clauses that contribute a "where" condition.

func EQ

func EQ(column string, value interface{}) WhereClause

EQ creates a WhereClause comparing a column's value for equality.

func GE

func GE(column string, value interface{}) WhereClause

GE creates a WhereClause comparing a column's value for greater or equal.

func GT

func GT(column string, value interface{}) WhereClause

GT creates a WhereClause comparing a column's value for greater than.

func In

func In(column string, values ...interface{}) WhereClause

In creates a WhereClause using the `in` operator.

func LE

func LE(column string, value interface{}) WhereClause

LE creates a WhereClause comparing a column's value for less equal.

func LT

func LT(column string, value interface{}) WhereClause

LT creates a WhereClause comparing a column's value for less than.

func Where

func Where(column string, value interface{}) WhereClause

Where is an alias for EQ.

Directories

Path Synopsis
cmd
depot
Package main contains the CLI for the generate command.
Package main contains the CLI for the generate command.
Package main contains a cli demonstrating the generated repo's usage.
Package main contains a cli demonstrating the generated repo's usage.
models
Package models contains the model definitions.
Package models contains the model definitions.
internal
generate
Package generate implements code generation for repository types.
Package generate implements code generation for repository types.
utils
Package utils contains utility functions for handling database interaction and mapping.
Package utils contains utility functions for handling database interaction and mapping.

Jump to

Keyboard shortcuts

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