schemable

package module
v0.0.0-...-ea859f2 Latest Latest
Warning

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

Go to latest
Published: Mar 7, 2022 License: MIT Imports: 8 Imported by: 0

README

Schemable

Schemable provides basic struct mapping against a database, using the squirrel package.

NOTE: Only works on go 1.18, since it uses generics.

DB tests

How to Use

Schemable works with annotated structs, and schemers that bind those structs to tables.

type ComicTitle struct {
	ID     int64  `db:"id, PRIMARY KEY, AUTO INCREMENT"`
	Name   string `db:"name"`
	Volume int    `db:"volume"`
}

var ComicTitles = schemable.Bind[ComicTitle]("comic_titles")

Initialize the client and store in a context. This lets queries take advantage of context cancellation and timeouts.

// calls sql.Open(...)
client := schemable.New("sqlite3", "connection")
client.SetLogger(...) // optional, for logging queries
ctx := schemable.WithClient(context.Background(), client)

Schemers can list and delete multiple records:

import sq "github.com/Masterminds/squirrel"

recorders, err := ComicTitles.ListWhere(ctx, func(q sq.SelectBuilder) sq.SelectBuilder {
  return q.Limit(10)
})

// Target is the actual *ComicTitle instance
recorders[0].Target

sqlResult, err := ComicTitles.DeleteWhere(ctx, func(q sq.DeleteBuilder) sq.DeleteBuilder {
  return q.Where(sq.Eq{"id": 1})
})

Records are managed in Recorders that can Load, Insert, Update, and Delete. Updating only updates fields that have changed.

// initialize an empty instance
newRec := ComicTitles.Record(nil)
newRec.Target.Name = "The X-Men"
newRec.Target.Volume = 1

err := newRec.Insert(ctx)

// load record by primary key
rec := ComicTitles.Record(&ComicTitle{ID: 1})
ok, err := rec.Exists(ctx)
err = rec.Load(ctx)

// only updates name column
rec.Target.Name = "The Uncanny X-Men"
err = rec.Update(ctx)

// deletes record
err = rec.Delete(ctx)

Schemable works with db transactions too:

// TxOptions is optional and can be nil
txclient, err := client.Begin(ctx, &sql.TxOptions{...})

tctx := schemable.WithClient(ctx, txclient)

// alternatively, begin the transaction directly from the context:
// (*sql.TxOptions is still optional)
tctx, txclient, err := schemable.WithTransaction(ctx, nil)

txRec := ComicTitles.Record(nil)
txRec.Target.Title = "The Immortal X-Men"
err = txRec.Insert(tctx)

err = txclient.Commit() // or txclient.Rollback()

Both *DBClient and *TxnClient offer custom query support through squirrel:

q, args, err := client.Builder().Select("id").From("comic_titles").ToSql()

q, args, err := txclient.Builder().Delete("comic_titles").ToSql()
txclient.Exec(ctx, qu, args...)
txclient.Rollback() // whew!

Where are the tests?

In an effort to keep go.mod tidy, the tests are implemented in the schemabletest package, designed to run in packages like sqlitetest:

# both schemable and schemable_sqlitetest need to be in the same dir for
# bin/test.
$ git clone https://github.com/refractionist/schemable
$ git clone https://github.com/refractionist/schemable_sqlitetest
$ cd schemable_sqlitetest

# add -v to see the raw sql queries
$ bin/test
PASS
ok  	github.com/refractionist/schemable_sqlitetest	0.419s

TODO

  • verify sqlite3 support
  • verify mysql support
  • verify postgres support
  • GitHub Actions for supported databases

Inspiration

Heavily inspired by the structable package, and this Facilitator pattern for Go generics.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrNoClient = errors.New("no client in context")

Functions

func DBDurationFrom

func DBDurationFrom(ctx context.Context) time.Duration

DBDurationFrom extracts db execution duration from the given context.

func Targets

func Targets[T any](recs []*Recorder[T]) []*T

Targets returns a slice of target records from the given Recorders.

func WithClient

func WithClient(ctx context.Context, c Client) context.Context

WithClient returns a modified variant of the given context with an embedded client.

func WithDBDuration

func WithDBDuration(ctx context.Context, start time.Time) context.Context

WithDBDuration wraps the context with the db execution duration based on the given start time.

Types

type Client

type Client interface {
	Exec(ctx context.Context, q string, args ...any) (sql.Result, error)
	Query(ctx context.Context, q string, args ...any) (*sql.Rows, error)
	QueryRow(ctx context.Context, q string, args ...any) *sql.Row
	Builder() *sq.StatementBuilderType
	LogQuery(ctx context.Context, q string, args []any)
}

Client represents a DB or Transaction client that runs queries with a squirrel builder.

func ClientFrom

func ClientFrom(ctx context.Context) Client

ClientFrom fetches the embedded client from the given context.

func Delete

func Delete(ctx context.Context, table string) (Client, sq.DeleteBuilder)

Delete returns the Client with a DeleteBuilder from the given context, or a nil Client if there is none.

func Insert

func Insert(ctx context.Context, table string) (Client, sq.InsertBuilder)

Insert returns the Client with an InsertBuilder from the given context, or a nil Client if there is none.

func Select

func Select(ctx context.Context, table string, columns ...string) (Client, sq.SelectBuilder)

Select returns the Client with a SelectBuilder from the given context, or a nil Client if there is none.

func Update

func Update(ctx context.Context, table string) (Client, sq.UpdateBuilder)

Update returns the Client with an UpdateBuilder from the given context, or a nil Client if there is none.

type DBClient

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

DBClient is a wrapper around an *sql.DB instance, a QueryLogger, and a squirrel query builder.

func New

func New(driver, conn string) (*DBClient, error)

New initiates a new database connection with the given connection string options, returning a DBClient. See database/sql#Open.

func (*DBClient) Begin

func (c *DBClient) Begin(ctx context.Context, opts *sql.TxOptions) (*TxnClient, error)

Begin starts a transaction. See database/sql#DB.BeginTx.

func (*DBClient) Builder

func (c *DBClient) Builder() *sq.StatementBuilderType

Builder is the squirrel query builder for this db connection.

func (*DBClient) Close

func (c *DBClient) Close() error

Close closes the database and prevents new queries from starting. Close then waits for all queries that have started processing on the server to finish.

func (*DBClient) DB

func (c *DBClient) DB() *sql.DB

DB returns the open *sql.DB instance for this Client.

func (*DBClient) Exec

func (c *DBClient) Exec(ctx context.Context, q string, args ...any) (sql.Result, error)

Exec executes a query without returning any rows. The args are for any placeholder parameters in the query.

func (*DBClient) LogQuery

func (c *DBClient) LogQuery(ctx context.Context, q string, args []any)

LogQuery logs the given query info to this client's logger.

func (*DBClient) Ping

func (c *DBClient) Ping(ctx context.Context) error

Ping verifies the connection to the database is still alive.

func (*DBClient) Query

func (c *DBClient) Query(ctx context.Context, q string, args ...any) (*sql.Rows, error)

Query executes a query that returns rows, typically a SELECT. The args are for any placeholder parameters in the query.

func (*DBClient) QueryRow

func (c *DBClient) QueryRow(ctx context.Context, q string, args ...any) *sql.Row

QueryRow executes a query that is expected to return at most one row. QueryRowContext always returns a non-nil value. Errors are deferred until Row's Scan method is called. If the query selects no rows, the *Row's Scan will return ErrNoRows. Otherwise, the *Row's Scan scans the first selected row and discards the rest.

func (*DBClient) SetLogger

func (c *DBClient) SetLogger(l QueryLogger)

SetLogger sets the given logger, or resetting it to a no-op logger if nil.

type DeleteFunc

type DeleteFunc func(query sq.DeleteBuilder) sq.DeleteBuilder

DeleteFunc modifies a basic delete operation to add conditions.

Technically, conditions are not limited to adding where clauses. It will receive a select statement with the 'SELECT ... FROM tablename' portion composed already.

type QueryLogger

type QueryLogger interface {
	LogQuery(ctx context.Context, q string, args []any)
}

QueryLogger is a wrapper for a type that logs SQL queries.

type Recorder

type Recorder[T any] struct {
	Schemer *Schemer[T]
	Target  *T
	// contains filtered or unexported fields
}

Recorder records changes to Target of type T using its Schemer.

func (*Recorder[T]) Delete

func (r *Recorder[T]) Delete(ctx context.Context) error

Delete removes this Recorder's Target from its Schemer's table in the database.

func (*Recorder[T]) Exists

func (r *Recorder[T]) Exists(ctx context.Context) (bool, error)

Exists checks if the given Recorder Target exists in the db according to its primary keys.

func (*Recorder[T]) Insert

func (r *Recorder[T]) Insert(ctx context.Context) error

Insert uses the Recorder's Schemer to insert the Target into the database.

func (*Recorder[T]) Load

func (r *Recorder[T]) Load(ctx context.Context) error

Load reloads the Recorder Target's columns (except primary keys) from the database.

func (*Recorder[T]) LoadWhere

func (r *Recorder[T]) LoadWhere(ctx context.Context, pred any, args ...any) error

LoadWhere loads a single Recorder Target using the given predicate args for a Where clause on the squirrel query builder.

func (*Recorder[T]) Update

func (r *Recorder[T]) Update(ctx context.Context) error

Insert uses the Recorder's Schemer to update the Target in the database, skipping if no values were updated since this Recorder was instantiated.

func (*Recorder[T]) UpdatedValues

func (r *Recorder[T]) UpdatedValues() map[string]any

UpdatedValues returns an updated column/value map since this Recorder was instantiated.

func (*Recorder[T]) Values

func (r *Recorder[T]) Values() map[string]any

Values returns a column/value map of this Recorder Target's values, except the primary keys.

func (*Recorder[T]) WhereIDs

func (r *Recorder[T]) WhereIDs() map[string]any

WhereIDs returns a column/value map of this Recorder Target's primary keys.

type Schemer

type Schemer[T any] struct {
	// contains filtered or unexported fields
}

Schemer maintains the table and column mapping of the generic type T. It parses the column and primary key info from the struct field tags.

func Bind

func Bind[T any](table string) *Schemer[T]

Bind creates a Schemer table/column mapping for the given generic type T.

func (*Schemer[T]) Columns

func (s *Schemer[T]) Columns(withKeys bool) []string

Columns returns the column names for the Schemer's type T, optionally with or without primary key columns. Columns are disambiguated with the table name for join queries.

func (*Schemer[T]) DeleteWhere

func (s *Schemer[T]) DeleteWhere(ctx context.Context, fn DeleteFunc) (sql.Result, error)

DeleteWhere deletes rows filtered by the given DeleteFunc. The context must have a client embedded with WithClient().

func (*Schemer[T]) Exists

func (s *Schemer[T]) Exists(ctx context.Context, pred any, args ...any) (bool, error)

Exists checks if any Recorder Target exists using the given predicate args for a Where clause on the squirrel query builder.

func (*Schemer[T]) First

func (s *Schemer[T]) First(ctx context.Context, fn WhereFunc) (*Recorder[T], error)

First returns a *Recorder[T] of the first row, filtered by the given WhereFunc. The context must have a client embedded with WithClient().

func (*Schemer[T]) InsertColumns

func (s *Schemer[T]) InsertColumns() []string

InsertColumns returns column names for inserts for the Schemer's type T. Column names do not include the table prefix.

func (*Schemer[T]) List

func (s *Schemer[T]) List(ctx context.Context, limit, offset uint64) ([]*Recorder[T], error)

List returns rows of type T embedded in Recorders, using the given limit and offset values. The context must have a client embedded with WithClient().

func (*Schemer[T]) ListWhere

func (s *Schemer[T]) ListWhere(ctx context.Context, fn WhereFunc) ([]*Recorder[T], error)

ListWhere returns rows of type T embedded in Recorders, filtered by the given WhereFunc. The context must have a client embedded with WithClient().

func (*Schemer[T]) Record

func (s *Schemer[T]) Record(tgt *T) *Recorder[T]

Record returns a Recorder for the given instance, creating a new one if nil is provided. For updating purposes, the returned recorder's UpdatedValues() do not take the given target's fields into account. Be aware how default values (like "" or 0) will affect your database. Use one of the Load or List funcs before setting properties in order to take advantage of partial updates.

func (*Schemer[T]) Table

func (s *Schemer[T]) Table() string

Table returns the table name that the Schemer's type T uses.

type TxnClient

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

DBClient is a wrapper around an *sql.Tx instance, a QueryLogger, and a squirrel query builder.

func WithTransaction

func WithTransaction(ctx context.Context, opts *sql.TxOptions) (context.Context, *TxnClient, error)

WithTransaction begins a new transaction with a *DBClient in the given context, returning a new context with the *TxnClient.

func (*TxnClient) Builder

func (c *TxnClient) Builder() *sq.StatementBuilderType

Builder is the squirrel query builder for this db connection.

func (*TxnClient) Commit

func (c *TxnClient) Commit() error

Commit commits the transaction.

func (*TxnClient) Exec

func (c *TxnClient) Exec(ctx context.Context, q string, args ...any) (sql.Result, error)

Exec executes a query without returning any rows. The args are for any placeholder parameters in the query.

func (*TxnClient) LogQuery

func (c *TxnClient) LogQuery(ctx context.Context, q string, args []any)

LogQuery logs the given query info to this client's logger.

func (*TxnClient) Query

func (c *TxnClient) Query(ctx context.Context, q string, args ...any) (*sql.Rows, error)

Query executes a query that returns rows, typically a SELECT. The args are for any placeholder parameters in the query.

func (*TxnClient) QueryRow

func (c *TxnClient) QueryRow(ctx context.Context, q string, args ...any) *sql.Row

QueryRow executes a query that is expected to return at most one row. QueryRowContext always returns a non-nil value. Errors are deferred until Row's Scan method is called. If the query selects no rows, the *Row's Scan will return ErrNoRows. Otherwise, the *Row's Scan scans the first selected row and discards the rest.

func (*TxnClient) Rollback

func (c *TxnClient) Rollback() error

Rollback aborts the transaction.

type WhereFunc

type WhereFunc func(query sq.SelectBuilder) sq.SelectBuilder

WhereFunc modifies a basic select operation to add conditions.

Technically, conditions are not limited to adding where clauses. It will receive a select statement with the 'SELECT ... FROM tablename' portion composed already.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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