2020-05-05 13:29:47 -04:00
package db
import (
"context"
"database/sql"
"fmt"
"reflect"
"strings"
"time"
2020-08-10 17:15:02 -04:00
"github.com/hashicorp/boundary/internal/db/common"
2020-11-05 16:54:03 -05:00
"github.com/hashicorp/boundary/internal/errors"
2020-08-10 17:15:02 -04:00
"github.com/hashicorp/boundary/internal/oplog"
"github.com/hashicorp/boundary/internal/oplog/store"
2020-06-29 08:18:57 -04:00
wrapping "github.com/hashicorp/go-kms-wrapping"
2020-05-05 13:29:47 -04:00
"github.com/jinzhu/gorm"
"google.golang.org/protobuf/proto"
)
2020-06-16 18:59:30 -04:00
const (
NoRowsAffected = 0
2020-08-10 17:15:02 -04:00
// DefaultLimit is the default for results for boundary
2020-06-16 18:59:30 -04:00
DefaultLimit = 10000
)
2020-05-12 11:57:28 -04:00
2021-04-02 18:19:48 -04:00
// OrderBy defines an enum type for declaring a column's order by criteria.
type OrderBy int
const (
// UnknownOrderBy would designate an unknown ordering of the column, which
// is the standard ordering for any select without an order by clause.
UnknownOrderBy = iota
// AscendingOrderBy would designate ordering the column in ascending order.
AscendingOrderBy
// DescendingOrderBy would designate ordering the column in decending order.
DescendingOrderBy
)
2020-05-05 13:29:47 -04:00
// Reader interface defines lookups/searching for resources
type Reader interface {
2020-06-29 08:18:57 -04:00
// LookupById will lookup a resource by its primary key id, which must be
// unique. The resourceWithIder must implement either ResourcePublicIder or
// ResourcePrivateIder interface.
LookupById ( ctx context . Context , resourceWithIder interface {}, opt ... Option ) error
// LookupByPublicId will lookup resource by its public_id which must be unique.
2020-05-05 13:29:47 -04:00
LookupByPublicId ( ctx context . Context , resource ResourcePublicIder , opt ... Option ) error
// LookupWhere will lookup and return the first resource using a where clause with parameters
LookupWhere ( ctx context . Context , resource interface {}, where string , args ... interface {}) error
2020-06-16 18:59:30 -04:00
// SearchWhere will search for all the resources it can find using a where
// clause with parameters. Supports the WithLimit option. If
// WithLimit < 0, then unlimited results are returned. If WithLimit == 0, then
// default limits are used for results.
SearchWhere ( ctx context . Context , resources interface {}, where string , args [] interface {}, opt ... Option ) error
2020-05-05 13:29:47 -04:00
2020-09-25 13:35:32 -04:00
// Query will run the raw query and return the *sql.Rows results. Query will
// operate within the context of any ongoing transaction for the db.Reader. The
// caller must close the returned *sql.Rows. Query can/should be used in
// combination with ScanRows.
2020-09-30 18:11:59 -04:00
Query ( ctx context . Context , sql string , values [] interface {}, opt ... Option ) ( * sql . Rows , error )
2020-09-25 13:35:32 -04:00
2020-05-05 13:29:47 -04:00
// ScanRows will scan sql rows into the interface provided
ScanRows ( rows * sql . Rows , result interface {}) error
}
// Writer interface defines create, update and retryable transaction handlers
type Writer interface {
// DoTx will wrap the TxHandler in a retryable transaction
DoTx ( ctx context . Context , retries uint , backOff Backoff , Handler TxHandler ) ( RetryInfo , error )
2020-05-31 19:55:56 -04:00
// Update an object in the db, fieldMask is required and provides
// field_mask.proto paths for fields that should be updated. The i interface
// parameter is the type the caller wants to update in the db and its
// fields are set to the update values. setToNullPaths is optional and
// provides field_mask.proto paths for the fields that should be set to
// null. fieldMaskPaths and setToNullPaths must not intersect. The caller
// is responsible for the transaction life cycle of the writer and if an
// error is returned the caller must decide what to do with the transaction,
// which almost always should be to rollback. Update returns the number of
// rows updated or an error. Supported options: WithOplog.
Update ( ctx context . Context , i interface {}, fieldMaskPaths [] string , setToNullPaths [] string , opt ... Option ) ( int , error )
2020-05-05 13:29:47 -04:00
// Create an object in the db with options: WithOplog
// the caller is responsible for the transaction life cycle of the writer
// and if an error is returned the caller must decide what to do with
// the transaction, which almost always should be to rollback.
Create ( ctx context . Context , i interface {}, opt ... Option ) error
2020-06-29 19:28:15 -04:00
// CreateItems will create multiple items of the same type.
2020-07-01 05:40:02 -04:00
// Supported options: WithOplog and WithOplogMsgs. WithOplog and
// WithOplogMsgs may not be used together. WithLookup is not a
// supported option. The caller is responsible for the transaction life
// cycle of the writer and if an error is returned the caller must decide
// what to do with the transaction, which almost always should be to
// rollback.
2020-06-29 19:28:15 -04:00
CreateItems ( ctx context . Context , createItems [] interface {}, opt ... Option ) error
2020-05-05 13:29:47 -04:00
// Delete an object in the db with options: WithOplog
// the caller is responsible for the transaction life cycle of the writer
// and if an error is returned the caller must decide what to do with
2020-05-12 11:57:28 -04:00
// the transaction, which almost always should be to rollback. Delete
// returns the number of rows deleted or an error.
Delete ( ctx context . Context , i interface {}, opt ... Option ) ( int , error )
2020-05-05 13:29:47 -04:00
2020-06-29 19:28:15 -04:00
// DeleteItems will delete multiple items of the same type.
2020-07-01 05:40:02 -04:00
// Supported options: WithOplog and WithOplogMsgs. WithOplog and
// WithOplogMsgs may not be used together. The caller is responsible for the
2020-06-29 19:28:15 -04:00
// transaction life cycle of the writer and if an error is returned the
// caller must decide what to do with the transaction, which almost always
// should be to rollback. Delete returns the number of rows deleted or an error.
DeleteItems ( ctx context . Context , deleteItems [] interface {}, opt ... Option ) ( int , error )
2020-09-16 12:02:02 -04:00
// Exec will execute the sql with the values as parameters. The int returned
// is the number of rows affected by the sql. No options are currently
// supported.
2020-09-30 18:11:59 -04:00
Exec ( ctx context . Context , sql string , values [] interface {}, opt ... Option ) ( int , error )
2020-09-16 12:02:02 -04:00
2021-04-08 20:54:32 -04:00
// Query will run the raw query and return the *sql.Rows results. Query will
// operate within the context of any ongoing transaction for the db.Writer. The
// caller must close the returned *sql.Rows. Query can/should be used in
// combination with ScanRows. Query is included in the Writer interface
// so callers can execute updates and inserts with returning values.
Query ( ctx context . Context , sql string , values [] interface {}, opt ... Option ) ( * sql . Rows , error )
2020-06-29 08:18:57 -04:00
// GetTicket returns an oplog ticket for the aggregate root of "i" which can
// be used to WriteOplogEntryWith for that aggregate root.
GetTicket ( i interface {}) ( * store . Ticket , error )
// WriteOplogEntryWith will write an oplog entry with the msgs provided for
// the ticket's aggregateName. No options are currently supported.
WriteOplogEntryWith (
ctx context . Context ,
wrapper wrapping . Wrapper ,
ticket * store . Ticket ,
metadata oplog . Metadata ,
msgs [] * oplog . Message ,
opt ... Option ,
) error
2020-05-05 13:29:47 -04:00
}
2020-05-08 11:36:28 -04:00
const (
StdRetryCnt = 20
)
2020-05-05 13:29:47 -04:00
// RetryInfo provides information on the retries of a transaction
type RetryInfo struct {
Retries int
Backoff time . Duration
}
// TxHandler defines a handler for a func that writes a transaction for use with DoTx
2020-06-22 13:27:26 -07:00
type TxHandler func ( Reader , Writer ) error
2020-05-05 13:29:47 -04:00
2020-06-29 08:18:57 -04:00
// ResourcePublicIder defines an interface that LookupByPublicId() can use to
// get the resource's public id.
2020-05-05 13:29:47 -04:00
type ResourcePublicIder interface {
GetPublicId () string
}
2020-06-29 08:18:57 -04:00
// ResourcePrivateIder defines an interface that LookupById() can use to get the
// resource's private id.
type ResourcePrivateIder interface {
GetPrivateId () string
}
2020-05-05 13:29:47 -04:00
type OpType int
const (
UnknownOp OpType = 0
CreateOp OpType = 1
UpdateOp OpType = 2
DeleteOp OpType = 3
)
2020-05-31 19:55:56 -04:00
// VetForWriter provides an interface that Create and Update can use to vet the
// resource before before writing it to the db. For optType == UpdateOp,
// options WithFieldMaskPath and WithNullPaths are supported. For optType ==
// CreateOp, no options are supported
2020-05-05 13:29:47 -04:00
type VetForWriter interface {
2020-05-08 11:36:28 -04:00
VetForWrite ( ctx context . Context , r Reader , opType OpType , opt ... Option ) error
2020-05-05 13:29:47 -04:00
}
2020-05-11 16:54:05 -04:00
// Db uses a gorm DB connection for read/write
type Db struct {
2020-05-11 21:05:11 -04:00
underlying * gorm . DB
2020-05-05 13:29:47 -04:00
}
2020-05-11 16:54:05 -04:00
// ensure that Db implements the interfaces of: Reader and Writer
2021-01-07 10:02:32 -05:00
var (
_ Reader = ( * Db )( nil )
_ Writer = ( * Db )( nil )
)
2020-05-05 13:29:47 -04:00
2020-05-11 21:05:11 -04:00
func New ( underlying * gorm . DB ) * Db {
return & Db { underlying : underlying }
}
2020-09-16 12:02:02 -04:00
// Exec will execute the sql with the values as parameters. The int returned
// is the number of rows affected by the sql. No options are currently
// supported.
2020-12-21 10:07:34 -08:00
func ( rw * Db ) Exec ( _ context . Context , sql string , values [] interface {}, _ ... Option ) ( int , error ) {
const op = "db.Exec"
2020-09-16 12:02:02 -04:00
if sql == "" {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . New ( errors . InvalidParameter , op , "missing sql" )
2020-09-16 12:02:02 -04:00
}
gormDb := rw . underlying . Exec ( sql , values ... )
if gormDb . Error != nil {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . Wrap ( gormDb . Error , op )
2020-09-16 12:02:02 -04:00
}
return int ( gormDb . RowsAffected ), nil
}
2020-09-25 13:35:32 -04:00
// Query will run the raw query and return the *sql.Rows results. Query will
// operate within the context of any ongoing transaction for the db.Reader. The
// caller must close the returned *sql.Rows. Query can/should be used in
// combination with ScanRows.
2020-12-21 10:07:34 -08:00
func ( rw * Db ) Query ( _ context . Context , sql string , values [] interface {}, _ ... Option ) ( * sql . Rows , error ) {
const op = "db.Query"
2020-09-25 13:35:32 -04:00
if sql == "" {
2020-12-21 10:07:34 -08:00
return nil , errors . New ( errors . InvalidParameter , op , "missing sql" )
2020-09-25 13:35:32 -04:00
}
gormDb := rw . underlying . Raw ( sql , values ... )
if gormDb . Error != nil {
2020-12-21 10:07:34 -08:00
return nil , errors . Wrap ( gormDb . Error , op )
2020-09-25 13:35:32 -04:00
}
return gormDb . Rows ()
}
2020-05-05 13:29:47 -04:00
// Scan rows will scan the rows into the interface
2020-05-11 16:54:05 -04:00
func ( rw * Db ) ScanRows ( rows * sql . Rows , result interface {}) error {
2020-12-21 10:07:34 -08:00
const op = "db.ScanRows"
2020-05-21 09:24:37 -04:00
if rw . underlying == nil {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "missing underlying db" )
2020-05-21 09:24:37 -04:00
}
2020-05-31 19:55:56 -04:00
if isNil ( result ) {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "missing result" )
2020-05-21 09:24:37 -04:00
}
2020-05-11 21:05:11 -04:00
return rw . underlying . ScanRows ( rows , result )
2020-05-05 13:29:47 -04:00
}
2020-05-11 16:54:05 -04:00
func ( rw * Db ) lookupAfterWrite ( ctx context . Context , i interface {}, opt ... Option ) error {
2020-12-21 10:07:34 -08:00
const op = "db.lookupAfterWrite"
2020-05-08 11:36:28 -04:00
opts := GetOpts ( opt ... )
2020-05-05 13:29:47 -04:00
withLookup := opts . withLookup
if ! withLookup {
return nil
}
2020-07-30 13:44:14 -04:00
if err := rw . LookupById ( ctx , i , opt ... ); err != nil {
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op )
2020-05-05 13:29:47 -04:00
}
2020-07-30 13:44:14 -04:00
return nil
2020-05-05 13:29:47 -04:00
}
2020-06-29 08:18:57 -04:00
// Create an object in the db with options: WithOplog, NewOplogMsg and
// WithLookup. WithOplog will write an oplog entry for the create.
// NewOplogMsg will return in-memory oplog message. WithOplog and NewOplogMsg
// cannot be used together. WithLookup with to force a lookup after create.
2020-05-11 16:54:05 -04:00
func ( rw * Db ) Create ( ctx context . Context , i interface {}, opt ... Option ) error {
2020-12-21 10:07:34 -08:00
const op = "db.Create"
2020-05-11 21:05:11 -04:00
if rw . underlying == nil {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "missing underlying db" )
2020-05-21 09:24:37 -04:00
}
2020-05-31 19:55:56 -04:00
if isNil ( i ) {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "missing interface" )
2020-05-05 13:29:47 -04:00
}
2020-05-08 11:36:28 -04:00
opts := GetOpts ( opt ... )
2020-05-05 13:29:47 -04:00
withOplog := opts . withOplog
2020-06-29 08:18:57 -04:00
if withOplog && opts . newOplogMsg != nil {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "both WithOplog and NewOplogMsg options have been specified" )
2020-06-29 08:18:57 -04:00
}
2020-05-12 11:57:28 -04:00
if withOplog {
// let's validate oplog options before we start writing to the database
_ , err := validateOplogArgs ( i , opts )
if err != nil {
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op , errors . WithMsg ( "oplog validation failed" ))
2020-05-12 11:57:28 -04:00
}
}
2020-05-21 09:24:37 -04:00
// these fields should be nil, since they are not writeable and we want the
// db to manage them
setFieldsToNil ( i , [] string { "CreateTime" , "UpdateTime" })
2020-07-08 17:02:01 -04:00
if ! opts . withSkipVetForWrite {
if vetter , ok := i .( VetForWriter ); ok {
if err := vetter . VetForWrite ( ctx , rw , CreateOp ); err != nil {
2020-12-23 11:00:00 -08:00
return errors . Wrap ( err , op )
2020-07-08 17:02:01 -04:00
}
2020-05-05 13:29:47 -04:00
}
}
2020-06-18 21:24:55 -04:00
var ticket * store . Ticket
if withOplog {
var err error
2020-06-29 08:18:57 -04:00
ticket , err = rw . GetTicket ( i )
2020-06-18 21:24:55 -04:00
if err != nil {
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op , errors . WithMsg ( "unable to get ticket" ))
2020-06-18 21:24:55 -04:00
}
}
2020-05-11 21:05:11 -04:00
if err := rw . underlying . Create ( i ). Error ; err != nil {
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op , errors . WithMsg ( "create failed" ))
2020-05-05 13:29:47 -04:00
}
if withOplog {
2020-06-18 21:24:55 -04:00
if err := rw . addOplog ( ctx , CreateOp , opts , ticket , i ); err != nil {
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op )
2020-05-05 13:29:47 -04:00
}
}
2020-06-29 08:18:57 -04:00
if opts . newOplogMsg != nil {
msg , err := rw . newOplogMessage ( ctx , CreateOp , i )
if err != nil {
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op , errors . WithMsg ( "returning oplog failed" ))
2020-06-29 08:18:57 -04:00
}
* opts . newOplogMsg = * msg
}
2020-05-05 13:29:47 -04:00
if err := rw . lookupAfterWrite ( ctx , i , opt ... ); err != nil {
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op )
2020-05-05 13:29:47 -04:00
}
return nil
}
2020-07-01 05:40:02 -04:00
// CreateItems will create multiple items of the same type. Supported options:
// WithOplog and WithOplogMsgs. WithOplog and WithOplogMsgs may not be used
// together. WithLookup is not a supported option.
2020-06-29 06:31:23 -04:00
func ( rw * Db ) CreateItems ( ctx context . Context , createItems [] interface {}, opt ... Option ) error {
2020-12-21 10:07:34 -08:00
const op = "db.CreateItems"
2020-06-29 06:31:23 -04:00
if rw . underlying == nil {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "missing underlying db" )
2020-06-29 06:31:23 -04:00
}
if len ( createItems ) == 0 {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "missing interfaces" )
2020-06-29 06:31:23 -04:00
}
opts := GetOpts ( opt ... )
if opts . withLookup {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "with lookup not a supported option" )
2020-07-01 05:40:02 -04:00
}
if opts . newOplogMsg != nil {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "new oplog msg (singular) is not a supported option" )
2020-07-01 05:40:02 -04:00
}
if opts . withOplog && opts . newOplogMsgs != nil {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "both WithOplog and NewOplogMsgs options have been specified" )
2020-06-29 06:31:23 -04:00
}
// verify that createItems are all the same type.
var foundType reflect . Type
for i , v := range createItems {
if i == 0 {
foundType = reflect . TypeOf ( v )
}
currentType := reflect . TypeOf ( v )
if foundType != currentType {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , fmt . Sprintf ( "create items contains disparate types. item %d is not a %s" , i , foundType . Name ()))
2020-06-29 06:31:23 -04:00
}
}
var ticket * store . Ticket
if opts . withOplog {
_ , err := validateOplogArgs ( createItems [ 0 ], opts )
if err != nil {
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op , errors . WithMsg ( "oplog validation failed" ))
2020-06-29 06:31:23 -04:00
}
2020-06-29 08:18:57 -04:00
ticket , err = rw . GetTicket ( createItems [ 0 ])
2020-06-29 06:31:23 -04:00
if err != nil {
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op , errors . WithMsg ( "unable to get ticket" ))
2020-06-29 06:31:23 -04:00
}
}
for _ , item := range createItems {
2020-06-29 19:28:15 -04:00
if err := rw . Create ( ctx , item ); err != nil {
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op )
2020-06-29 19:28:15 -04:00
}
2020-06-29 06:31:23 -04:00
}
if opts . withOplog {
if err := rw . addOplogForItems ( ctx , CreateOp , opts , ticket , createItems ); err != nil {
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op , errors . WithMsg ( "unable to add oplog" ))
2020-06-29 06:31:23 -04:00
}
}
2020-07-01 05:40:02 -04:00
if opts . newOplogMsgs != nil {
msgs , err := rw . oplogMsgsForItems ( ctx , CreateOp , opts , createItems )
if err != nil {
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op , errors . WithMsg ( "returning oplog msgs failed" ))
2020-07-01 05:40:02 -04:00
}
* opts . newOplogMsgs = append ( * opts . newOplogMsgs , msgs ... )
}
2020-06-29 06:31:23 -04:00
return nil
}
2020-05-31 19:55:56 -04:00
// Update an object in the db, fieldMask is required and provides
// field_mask.proto paths for fields that should be updated. The i interface
// parameter is the type the caller wants to update in the db and its
// fields are set to the update values. setToNullPaths is optional and
// provides field_mask.proto paths for the fields that should be set to
// null. fieldMaskPaths and setToNullPaths must not intersect. The caller
// is responsible for the transaction life cycle of the writer and if an
// error is returned the caller must decide what to do with the transaction,
// which almost always should be to rollback. Update returns the number of
2020-06-29 08:18:57 -04:00
// rows updated.
//
// Supported options: WithOplog, NewOplogMsg and WithVersion.
// WithOplog will write an oplog entry for the update. NewOplogMsg
// will return in-memory oplog message. WithOplog and NewOplogMsg cannot be
// used together. If WithVersion is used, then the update will include the
// version number in the update where clause, which basically makes the update
// use optimistic locking and the update will only succeed if the existing rows
2020-07-15 13:12:15 -04:00
// version matches the WithVersion option. Zero is not a valid value for the
// WithVersion option and will return an error.
2020-05-31 19:55:56 -04:00
func ( rw * Db ) Update ( ctx context . Context , i interface {}, fieldMaskPaths [] string , setToNullPaths [] string , opt ... Option ) ( int , error ) {
2020-12-21 10:07:34 -08:00
const op = "db.Update"
2020-05-11 21:05:11 -04:00
if rw . underlying == nil {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . New ( errors . InvalidParameter , op , "missing underlying db" )
2020-05-21 09:24:37 -04:00
}
2020-05-31 19:55:56 -04:00
if isNil ( i ) {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . New ( errors . InvalidParameter , op , "missing interface" )
2020-05-21 09:24:37 -04:00
}
2020-05-31 19:55:56 -04:00
if len ( fieldMaskPaths ) == 0 && len ( setToNullPaths ) == 0 {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . New ( errors . InvalidParameter , op , "both fieldMaskPaths and setToNullPaths are missing" )
2020-05-31 19:55:56 -04:00
}
2020-06-29 08:18:57 -04:00
opts := GetOpts ( opt ... )
withOplog := opts . withOplog
if withOplog && opts . newOplogMsg != nil {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . New ( errors . InvalidParameter , op , "both WithOplog and NewOplogMsg options have been specified" )
2020-06-29 08:18:57 -04:00
}
2020-05-31 19:55:56 -04:00
// we need to filter out some non-updatable fields (like: CreateTime, etc)
fieldMaskPaths = filterPaths ( fieldMaskPaths )
setToNullPaths = filterPaths ( setToNullPaths )
if len ( fieldMaskPaths ) == 0 && len ( setToNullPaths ) == 0 {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . New ( errors . InvalidParameter , op , "after filtering non-updated fields, there are no fields left in fieldMaskPaths or setToNullPaths" )
2020-05-31 19:55:56 -04:00
}
updateFields , err := common . UpdateFields ( i , fieldMaskPaths , setToNullPaths )
if err != nil {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . Wrap ( err , op , errors . WithMsg ( "getting update fields failed" ))
2020-05-31 19:55:56 -04:00
}
if len ( updateFields ) == 0 {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . New ( errors . InvalidParameter , op , fmt . Sprintf ( "no fields matched using fieldMaskPaths %s" , fieldMaskPaths ))
2020-05-05 13:29:47 -04:00
}
2020-05-21 09:24:37 -04:00
2020-08-10 17:15:02 -04:00
// This is not a boundary scope, but rather a gorm Scope:
2020-05-21 09:24:37 -04:00
// https://godoc.org/github.com/jinzhu/gorm#DB.NewScope
scope := rw . underlying . NewScope ( i )
if scope . PrimaryKeyZero () {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . New ( errors . InvalidParameter , op , "primary key is not set" )
2020-05-21 09:24:37 -04:00
}
2020-06-03 09:51:34 -04:00
for _ , f := range scope . PrimaryFields () {
if contains ( fieldMaskPaths , f . Name ) {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . New ( errors . InvalidFieldMask , op , fmt . Sprintf ( "not allowed on primary key field %s" , f . Name ))
2020-06-03 09:51:34 -04:00
}
}
2020-05-12 11:57:28 -04:00
if withOplog {
// let's validate oplog options before we start writing to the database
_ , err := validateOplogArgs ( i , opts )
if err != nil {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . Wrap ( err , op , errors . WithMsg ( "oplog validation failed" ))
2020-05-12 11:57:28 -04:00
}
}
2020-07-08 17:02:01 -04:00
if ! opts . withSkipVetForWrite {
if vetter , ok := i .( VetForWriter ); ok {
if err := vetter . VetForWrite ( ctx , rw , UpdateOp , WithFieldMaskPaths ( fieldMaskPaths ), WithNullPaths ( setToNullPaths )); err != nil {
2020-12-23 11:00:00 -08:00
return NoRowsAffected , errors . Wrap ( err , op )
2020-07-08 17:02:01 -04:00
}
2020-05-05 13:29:47 -04:00
}
}
2020-06-18 21:24:55 -04:00
var ticket * store . Ticket
if withOplog {
var err error
2020-06-29 08:18:57 -04:00
ticket , err = rw . GetTicket ( i )
2020-06-18 21:24:55 -04:00
if err != nil {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . Wrap ( err , op , errors . WithMsg ( "unable to get ticket" ))
2020-06-18 21:24:55 -04:00
}
}
2020-06-24 12:33:49 -04:00
var underlying * gorm . DB
switch {
2020-08-10 15:17:15 -04:00
case opts . WithVersion != nil || opts . withWhereClause != "" :
var where [] string
var args [] interface {}
if opts . WithVersion != nil {
if * opts . WithVersion == 0 {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . New ( errors . InvalidParameter , op , "with version option is zero" )
2020-08-10 15:17:15 -04:00
}
if _ , ok := scope . FieldByName ( "version" ); ! ok {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . New ( errors . InvalidParameter , op , fmt . Sprintf ( "%s does not have a version field" , scope . TableName ()))
2020-08-10 15:17:15 -04:00
}
where , args = append ( where , "version = ?" ), append ( args , opts . WithVersion )
2020-07-15 13:12:15 -04:00
}
2020-08-10 15:17:15 -04:00
if opts . withWhereClause != "" {
where , args = append ( where , opts . withWhereClause ), append ( args , opts . withWhereClauseArgs ... )
2020-06-24 12:33:49 -04:00
}
2020-08-10 15:17:15 -04:00
underlying = rw . underlying . Model ( i ). Where ( strings . Join ( where , " and " ), args ... ). Updates ( updateFields )
2020-06-24 12:33:49 -04:00
default :
underlying = rw . underlying . Model ( i ). Updates ( updateFields )
}
2020-05-12 11:57:28 -04:00
if underlying . Error != nil {
2020-12-21 10:07:34 -08:00
if underlying . Error == gorm . ErrRecordNotFound {
return NoRowsAffected , errors . E ( errors . WithCode ( errors . RecordNotFound ), errors . WithOp ( op ))
2020-06-04 20:05:08 -04:00
}
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . Wrap ( underlying . Error , op )
2020-05-05 13:29:47 -04:00
}
2020-05-12 11:57:28 -04:00
rowsUpdated := int ( underlying . RowsAffected )
2020-06-29 08:18:57 -04:00
if rowsUpdated > 0 && ( withOplog || opts . newOplogMsg != nil ) {
2020-06-10 06:56:46 -04:00
// we don't want to change the inbound slices in opts, so we'll make our
// own copy to pass to addOplog()
oplogFieldMasks := make ([] string , len ( fieldMaskPaths ))
copy ( oplogFieldMasks , fieldMaskPaths )
oplogNullPaths := make ([] string , len ( setToNullPaths ))
copy ( oplogNullPaths , setToNullPaths )
oplogOpts := Options {
oplogOpts : opts . oplogOpts ,
withOplog : opts . withOplog ,
WithFieldMaskPaths : oplogFieldMasks ,
WithNullPaths : oplogNullPaths ,
}
2020-06-29 08:18:57 -04:00
if withOplog {
if err := rw . addOplog ( ctx , UpdateOp , oplogOpts , ticket , i ); err != nil {
2020-12-21 10:07:34 -08:00
return rowsUpdated , errors . Wrap ( err , op , errors . WithMsg ( "add oplog failed" ))
2020-06-29 08:18:57 -04:00
}
}
if opts . newOplogMsg != nil {
msg , err := rw . newOplogMessage ( ctx , UpdateOp , i , WithFieldMaskPaths ( oplogFieldMasks ), WithNullPaths ( oplogNullPaths ))
if err != nil {
2020-12-21 10:07:34 -08:00
return rowsUpdated , errors . Wrap ( err , op , errors . WithMsg ( "returning oplog failed" ))
2020-06-29 08:18:57 -04:00
}
* opts . newOplogMsg = * msg
2020-05-05 13:29:47 -04:00
}
}
2020-05-08 11:36:28 -04:00
// we need to force a lookupAfterWrite so the resource returned is correctly initialized
// from the db
opt = append ( opt , WithLookup ( true ))
2020-05-05 13:29:47 -04:00
if err := rw . lookupAfterWrite ( ctx , i , opt ... ); err != nil {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . Wrap ( err , op )
2020-05-05 13:29:47 -04:00
}
2020-05-12 11:57:28 -04:00
return rowsUpdated , nil
2020-05-05 13:29:47 -04:00
}
2020-08-24 13:22:10 -04:00
// Delete an object in the db with options: WithOplog, NewOplogMsg, WithWhere.
// WithOplog will write an oplog entry for the delete. NewOplogMsg will return
// in-memory oplog message. WithOplog and NewOplogMsg cannot be used together.
// WithWhere allows specifying a constraint. Delete returns the number of rows
// deleted and any errors.
2020-05-12 11:57:28 -04:00
func ( rw * Db ) Delete ( ctx context . Context , i interface {}, opt ... Option ) ( int , error ) {
2020-12-21 10:07:34 -08:00
const op = "db.Delete"
2020-05-11 21:05:11 -04:00
if rw . underlying == nil {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . New ( errors . InvalidParameter , op , "missing underlying db" )
2020-05-05 13:29:47 -04:00
}
2020-05-31 19:55:56 -04:00
if isNil ( i ) {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . New ( errors . InvalidParameter , op , "missing interface" )
2020-05-21 09:24:37 -04:00
}
2020-06-29 08:18:57 -04:00
opts := GetOpts ( opt ... )
withOplog := opts . withOplog
if withOplog && opts . newOplogMsg != nil {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . New ( errors . InvalidParameter , op , "both WithOplog and NewOplogMsg options have been specified" )
2020-06-29 08:18:57 -04:00
}
2020-08-10 17:15:02 -04:00
// This is not a boundary scope, but rather a gorm Scope:
2020-05-21 09:24:37 -04:00
// https://godoc.org/github.com/jinzhu/gorm#DB.NewScope
scope := rw . underlying . NewScope ( i )
2020-08-24 13:22:10 -04:00
if opts . withWhereClause == "" {
if scope . PrimaryKeyZero () {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . New ( errors . InvalidParameter , op , "primary key is not set" )
2020-08-24 13:22:10 -04:00
}
2020-05-05 13:29:47 -04:00
}
2020-05-12 11:57:28 -04:00
if withOplog {
_ , err := validateOplogArgs ( i , opts )
if err != nil {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . Wrap ( err , op , errors . WithMsg ( "oplog validation failed" ))
2020-05-12 11:57:28 -04:00
}
}
2020-06-18 21:24:55 -04:00
var ticket * store . Ticket
if withOplog {
var err error
2020-06-29 08:18:57 -04:00
ticket , err = rw . GetTicket ( i )
2020-06-18 21:24:55 -04:00
if err != nil {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . Wrap ( err , op , errors . WithMsg ( "unable to get ticket" ))
2020-06-18 21:24:55 -04:00
}
}
2020-08-24 13:22:10 -04:00
db := rw . underlying
if opts . withWhereClause != "" {
db = db . Where ( opts . withWhereClause , opts . withWhereClauseArgs ... )
2020-05-05 13:29:47 -04:00
}
2020-08-24 13:22:10 -04:00
db = db . Delete ( i )
if db . Error != nil {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . Wrap ( db . Error , op )
2020-08-24 13:22:10 -04:00
}
rowsDeleted := int ( db . RowsAffected )
2020-06-29 08:18:57 -04:00
if rowsDeleted > 0 && ( withOplog || opts . newOplogMsg != nil ) {
if withOplog {
if err := rw . addOplog ( ctx , DeleteOp , opts , ticket , i ); err != nil {
2021-01-12 08:50:19 -08:00
return rowsDeleted , errors . Wrap ( err , op , errors . WithMsg ( "add oplog failed" ))
2020-06-29 08:18:57 -04:00
}
}
if opts . newOplogMsg != nil {
msg , err := rw . newOplogMessage ( ctx , DeleteOp , i )
if err != nil {
2021-01-12 08:50:19 -08:00
return rowsDeleted , errors . Wrap ( err , op , errors . WithMsg ( "returning oplog failed" ))
2020-06-29 08:18:57 -04:00
}
* opts . newOplogMsg = * msg
2020-05-05 13:29:47 -04:00
}
}
2020-05-12 11:57:28 -04:00
return rowsDeleted , nil
2020-05-05 13:29:47 -04:00
}
2020-07-01 05:40:02 -04:00
// DeleteItems will delete multiple items of the same type. Supported options:
// WithOplog and WithOplogMsgs. WithOplog and WithOplogMsgs may not be used
// together.
2020-06-29 06:31:23 -04:00
func ( rw * Db ) DeleteItems ( ctx context . Context , deleteItems [] interface {}, opt ... Option ) ( int , error ) {
2020-12-21 10:07:34 -08:00
const op = "db.DeleteItems"
2020-06-29 06:31:23 -04:00
if rw . underlying == nil {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . New ( errors . InvalidParameter , op , "missing underlying db" )
2020-06-29 06:31:23 -04:00
}
if len ( deleteItems ) == 0 {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . New ( errors . InvalidParameter , op , "no interfaces to delete" )
2020-06-29 06:31:23 -04:00
}
2020-07-01 05:40:02 -04:00
opts := GetOpts ( opt ... )
if opts . newOplogMsg != nil {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . New ( errors . InvalidParameter , op , "new oplog msg (singular) is not a supported option" )
2020-07-01 05:40:02 -04:00
}
if opts . withOplog && opts . newOplogMsgs != nil {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . New ( errors . InvalidParameter , op , "both WithOplog and NewOplogMsgs options have been specified" )
2020-07-01 05:40:02 -04:00
}
2020-06-29 06:31:23 -04:00
// verify that createItems are all the same type.
var foundType reflect . Type
for i , v := range deleteItems {
if i == 0 {
foundType = reflect . TypeOf ( v )
}
currentType := reflect . TypeOf ( v )
if foundType != currentType {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . New ( errors . InvalidParameter , op , fmt . Sprintf ( "items contain disparate types. item %d is not a %s" , i , foundType . Name ()))
2020-06-29 06:31:23 -04:00
}
}
2020-07-01 05:40:02 -04:00
2020-06-29 06:31:23 -04:00
var ticket * store . Ticket
if opts . withOplog {
_ , err := validateOplogArgs ( deleteItems [ 0 ], opts )
if err != nil {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . Wrap ( err , op , errors . WithMsg ( "oplog validation failed" ))
2020-06-29 06:31:23 -04:00
}
2020-06-29 08:18:57 -04:00
ticket , err = rw . GetTicket ( deleteItems [ 0 ])
2020-06-29 06:31:23 -04:00
if err != nil {
2020-12-21 10:07:34 -08:00
return NoRowsAffected , errors . Wrap ( err , op , errors . WithMsg ( "unable to get ticket" ))
2020-06-29 06:31:23 -04:00
}
}
rowsDeleted := 0
for _ , item := range deleteItems {
// calling delete directly on the underlying db, since the writer.Delete
// doesn't provide capabilities needed here (which is different from the
// relationship between Create and CreateItems).
underlying := rw . underlying . Delete ( item )
if underlying . Error != nil {
2020-12-21 10:07:34 -08:00
return rowsDeleted , errors . Wrap ( underlying . Error , op )
2020-06-29 06:31:23 -04:00
}
rowsDeleted += int ( underlying . RowsAffected )
}
2020-07-01 05:40:02 -04:00
if rowsDeleted > 0 && ( opts . withOplog || opts . newOplogMsgs != nil ) {
if opts . withOplog {
if err := rw . addOplogForItems ( ctx , DeleteOp , opts , ticket , deleteItems ); err != nil {
2020-12-21 10:07:34 -08:00
return rowsDeleted , errors . Wrap ( err , op , errors . WithMsg ( "unable to add oplog" ))
2020-07-01 05:40:02 -04:00
}
}
if opts . newOplogMsgs != nil {
msgs , err := rw . oplogMsgsForItems ( ctx , DeleteOp , opts , deleteItems )
if err != nil {
2020-12-21 10:07:34 -08:00
return rowsDeleted , errors . Wrap ( err , op , errors . WithMsg ( "returning oplog msgs failed" ))
2020-07-01 05:40:02 -04:00
}
* opts . newOplogMsgs = append ( * opts . newOplogMsgs , msgs ... )
2020-06-29 06:31:23 -04:00
}
}
return rowsDeleted , nil
}
2020-05-12 11:57:28 -04:00
func validateOplogArgs ( i interface {}, opts Options ) ( oplog . ReplayableMessage , error ) {
2020-12-21 10:07:34 -08:00
const op = "db.validateOplogArgs"
2020-05-05 13:29:47 -04:00
oplogArgs := opts . oplogOpts
if oplogArgs . wrapper == nil {
2020-12-21 10:07:34 -08:00
return nil , errors . New ( errors . InvalidParameter , op , "missing wrapper" )
2020-05-05 13:29:47 -04:00
}
if len ( oplogArgs . metadata ) == 0 {
2020-12-21 10:07:34 -08:00
return nil , errors . New ( errors . InvalidParameter , op , "missing metadata" )
2020-05-05 13:29:47 -04:00
}
replayable , ok := i .( oplog . ReplayableMessage )
if ! ok {
2020-12-21 10:07:34 -08:00
return nil , errors . E ( errors . WithOp ( op ), errors . WithMsg ( "not a replayable message" ))
2020-05-12 11:57:28 -04:00
}
return replayable , nil
}
2020-06-29 06:31:23 -04:00
func ( rw * Db ) getTicketFor ( aggregateName string ) ( * store . Ticket , error ) {
2020-12-21 10:07:34 -08:00
const op = "db.getTicketFor"
2020-06-29 06:31:23 -04:00
if rw . underlying == nil {
2020-12-21 10:07:34 -08:00
return nil , errors . New ( errors . InvalidParameter , op , fmt . Sprintf ( "%s: underlying db missing" , aggregateName ))
2020-06-29 06:31:23 -04:00
}
ticketer , err := oplog . NewGormTicketer ( rw . underlying , oplog . WithAggregateNames ( true ))
if err != nil {
2020-12-21 10:07:34 -08:00
return nil , errors . Wrap ( err , op , errors . WithMsg ( fmt . Sprintf ( "%s: unable to get Ticketer" , aggregateName )))
2020-06-29 06:31:23 -04:00
}
ticket , err := ticketer . GetTicket ( aggregateName )
if err != nil {
2020-12-21 10:07:34 -08:00
return nil , errors . Wrap ( err , op , errors . WithMsg ( fmt . Sprintf ( "%s: unable to get ticket" , aggregateName )))
2020-06-29 06:31:23 -04:00
}
return ticket , nil
}
2020-06-29 08:18:57 -04:00
// GetTicket returns an oplog ticket for the aggregate root of "i" which can
// be used to WriteOplogEntryWith for that aggregate root.
func ( rw * Db ) GetTicket ( i interface {}) ( * store . Ticket , error ) {
2020-12-21 10:07:34 -08:00
const op = "db.GetTicket"
2020-06-18 21:24:55 -04:00
if rw . underlying == nil {
2020-12-21 10:07:34 -08:00
return nil , errors . New ( errors . InvalidParameter , op , "missing underlying db" )
2020-06-18 21:24:55 -04:00
}
2020-06-29 08:18:57 -04:00
if isNil ( i ) {
2020-12-21 10:07:34 -08:00
return nil , errors . New ( errors . InvalidParameter , op , "missing interface" )
2020-06-29 08:18:57 -04:00
}
2020-06-18 21:24:55 -04:00
replayable , ok := i .( oplog . ReplayableMessage )
if ! ok {
2020-12-21 10:07:34 -08:00
return nil , errors . New ( errors . InvalidParameter , op , "not a replayable message" )
2020-06-18 21:24:55 -04:00
}
2020-06-29 06:31:23 -04:00
return rw . getTicketFor ( replayable . TableName ())
}
2020-07-01 05:40:02 -04:00
func ( rw * Db ) oplogMsgsForItems ( ctx context . Context , opType OpType , opts Options , items [] interface {}) ([] * oplog . Message , error ) {
2020-12-21 10:07:34 -08:00
const op = "db.oplogMsgsForItems"
2020-07-01 05:40:02 -04:00
if len ( items ) == 0 {
2020-12-21 10:07:34 -08:00
return nil , errors . New ( errors . InvalidParameter , op , "missing items" )
2020-07-01 05:40:02 -04:00
}
oplogMsgs := [] * oplog . Message {}
var foundType reflect . Type
for i , item := range items {
if i == 0 {
foundType = reflect . TypeOf ( item )
}
currentType := reflect . TypeOf ( item )
if foundType != currentType {
2020-12-21 10:07:34 -08:00
return nil , errors . New ( errors . InvalidParameter , op , fmt . Sprintf ( "items contains disparate types. item (%d) %s is not a %s" , i , currentType , foundType ))
2020-07-01 05:40:02 -04:00
}
msg , err := rw . newOplogMessage ( ctx , opType , item , WithFieldMaskPaths ( opts . WithFieldMaskPaths ), WithNullPaths ( opts . WithNullPaths ))
if err != nil {
2020-12-21 10:07:34 -08:00
return nil , errors . Wrap ( err , op )
2020-07-01 05:40:02 -04:00
}
oplogMsgs = append ( oplogMsgs , msg )
}
return oplogMsgs , nil
}
2020-06-29 06:31:23 -04:00
// addOplogForItems will add a multi-message oplog entry with one msg for each
// item. Items must all be of the same type. Only CreateOp and DeleteOp are
// currently supported operations.
func ( rw * Db ) addOplogForItems ( ctx context . Context , opType OpType , opts Options , ticket * store . Ticket , items [] interface {}) error {
2020-12-21 10:07:34 -08:00
const op = "db.addOplogForItems"
2020-06-29 06:31:23 -04:00
oplogArgs := opts . oplogOpts
if ticket == nil {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "missing ticket" )
2020-06-29 06:31:23 -04:00
}
if len ( items ) == 0 {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "missing items" )
2020-06-29 06:31:23 -04:00
}
if oplogArgs . metadata == nil {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "missing metadata" )
2020-06-29 06:31:23 -04:00
}
if oplogArgs . wrapper == nil {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "missing wrapper" )
2020-06-29 06:31:23 -04:00
}
2020-07-01 05:40:02 -04:00
oplogMsgs , err := rw . oplogMsgsForItems ( ctx , opType , opts , items )
if err != nil {
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op )
2020-07-01 05:40:02 -04:00
}
2020-06-29 06:31:23 -04:00
replayable , err := validateOplogArgs ( items [ 0 ], opts )
if err != nil {
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op , errors . WithMsg ( "oplog validation failed" ))
2020-06-29 06:31:23 -04:00
}
2020-06-18 21:24:55 -04:00
ticketer , err := oplog . NewGormTicketer ( rw . underlying , oplog . WithAggregateNames ( true ))
if err != nil {
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op , errors . WithMsg ( "unable to get Ticketer" ))
2020-06-18 21:24:55 -04:00
}
2020-06-29 06:31:23 -04:00
entry , err := oplog . NewEntry (
replayable . TableName (),
oplogArgs . metadata ,
oplogArgs . wrapper ,
ticketer ,
)
2020-06-18 21:24:55 -04:00
if err != nil {
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op , errors . WithMsg ( "unable to create oplog entry" ))
2020-06-29 06:31:23 -04:00
}
if err := entry . WriteEntryWith (
ctx ,
& oplog . GormWriter { Tx : rw . underlying },
ticket ,
oplogMsgs ... ,
); err != nil {
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op , errors . WithMsg ( "unable to write oplog entry" ))
2020-06-29 06:31:23 -04:00
}
return nil
2020-06-18 21:24:55 -04:00
}
2020-07-01 05:40:02 -04:00
2020-06-18 21:24:55 -04:00
func ( rw * Db ) addOplog ( ctx context . Context , opType OpType , opts Options , ticket * store . Ticket , i interface {}) error {
2020-12-21 10:07:34 -08:00
const op = "db.addOplog"
2020-05-12 11:57:28 -04:00
oplogArgs := opts . oplogOpts
replayable , err := validateOplogArgs ( i , opts )
if err != nil {
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op )
2020-05-05 13:29:47 -04:00
}
2020-06-18 21:24:55 -04:00
if ticket == nil {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "missing ticket" )
2020-05-05 13:29:47 -04:00
}
2020-06-18 21:24:55 -04:00
ticketer , err := oplog . NewGormTicketer ( rw . underlying , oplog . WithAggregateNames ( true ))
2020-05-05 13:29:47 -04:00
if err != nil {
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op , errors . WithMsg ( "unable to get Ticketer" ))
2020-05-05 13:29:47 -04:00
}
entry , err := oplog . NewEntry (
replayable . TableName (),
oplogArgs . metadata ,
oplogArgs . wrapper ,
ticketer ,
)
2020-05-18 12:57:45 -04:00
if err != nil {
2020-12-23 11:00:00 -08:00
return errors . Wrap ( err , op )
2020-05-18 12:57:45 -04:00
}
2020-07-01 05:40:02 -04:00
msg , err := rw . newOplogMessage ( ctx , opType , i , WithFieldMaskPaths ( opts . WithFieldMaskPaths ), WithNullPaths ( opts . WithNullPaths ))
if err != nil {
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op )
2020-05-05 13:29:47 -04:00
}
err = entry . WriteEntryWith (
ctx ,
2020-06-18 21:24:55 -04:00
& oplog . GormWriter { Tx : rw . underlying },
2020-05-05 13:29:47 -04:00
ticket ,
2020-07-01 05:40:02 -04:00
msg ,
2020-05-05 13:29:47 -04:00
)
if err != nil {
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op , errors . WithMsg ( "unable to write oplog entry" ))
2020-05-05 13:29:47 -04:00
}
return nil
}
2020-06-29 08:18:57 -04:00
// WriteOplogEntryWith will write an oplog entry with the msgs provided for
// the ticket's aggregateName. No options are currently supported.
2020-12-21 10:07:34 -08:00
func ( rw * Db ) WriteOplogEntryWith ( ctx context . Context , wrapper wrapping . Wrapper , ticket * store . Ticket , metadata oplog . Metadata , msgs [] * oplog . Message , _ ... Option ) error {
const op = "db.WriteOplogEntryWith"
2020-06-29 08:18:57 -04:00
if wrapper == nil {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "missing wrapper" )
2020-06-29 08:18:57 -04:00
}
if ticket == nil {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "missing ticket" )
2020-06-29 08:18:57 -04:00
}
if len ( msgs ) == 0 {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "missing msgs" )
2020-06-29 08:18:57 -04:00
}
if rw . underlying == nil {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "missing underlying db" )
2020-06-29 08:18:57 -04:00
}
if len ( metadata ) == 0 {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "missing metadata" )
2020-06-29 08:18:57 -04:00
}
ticketer , err := oplog . NewGormTicketer ( rw . underlying , oplog . WithAggregateNames ( true ))
if err != nil {
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op , errors . WithMsg ( "unable to get Ticketer" ))
2020-06-29 08:18:57 -04:00
}
entry , err := oplog . NewEntry (
ticket . Name ,
metadata ,
wrapper ,
ticketer ,
)
if err != nil {
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op , errors . WithMsg ( "unable to create oplog entry" ))
2020-06-29 08:18:57 -04:00
}
err = entry . WriteEntryWith (
ctx ,
& oplog . GormWriter { Tx : rw . underlying },
ticket ,
msgs ... ,
)
if err != nil {
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op , errors . WithMsg ( "unable to write oplog entry" ))
2020-06-29 08:18:57 -04:00
}
return nil
}
2020-12-21 10:07:34 -08:00
func ( rw * Db ) newOplogMessage ( _ context . Context , opType OpType , i interface {}, opt ... Option ) ( * oplog . Message , error ) {
const op = "db.newOplogMessage"
2020-06-29 08:18:57 -04:00
opts := GetOpts ( opt ... )
replayable , ok := i .( oplog . ReplayableMessage )
if ! ok {
2020-12-21 10:07:34 -08:00
return nil , errors . New ( errors . InvalidParameter , op , "not a replayable interface" )
2020-06-29 08:18:57 -04:00
}
msg := oplog . Message {
Message : i .( proto . Message ),
TypeName : replayable . TableName (),
}
switch opType {
case CreateOp :
msg . OpType = oplog . OpType_OP_TYPE_CREATE
case UpdateOp :
msg . OpType = oplog . OpType_OP_TYPE_UPDATE
msg . FieldMaskPaths = opts . WithFieldMaskPaths
msg . SetToNullPaths = opts . WithNullPaths
case DeleteOp :
msg . OpType = oplog . OpType_OP_TYPE_DELETE
default :
2020-12-21 10:07:34 -08:00
return nil , errors . New ( errors . InvalidParameter , op , fmt . Sprintf ( "operation type %v is not supported" , opType ))
2020-06-29 08:18:57 -04:00
}
return & msg , nil
}
2020-05-05 13:29:47 -04:00
// DoTx will wrap the Handler func passed within a transaction with retries
// you should ensure that any objects written to the db in your TxHandler are retryable, which
// means that the object may be sent to the db several times (retried), so things like the primary key must
// be reset before retry
2020-05-11 16:54:05 -04:00
func ( w * Db ) DoTx ( ctx context . Context , retries uint , backOff Backoff , Handler TxHandler ) ( RetryInfo , error ) {
2020-12-21 10:07:34 -08:00
const op = "db.DoTx"
2020-05-11 21:05:11 -04:00
if w . underlying == nil {
2020-12-21 10:07:34 -08:00
return RetryInfo {}, errors . New ( errors . InvalidParameter , op , "missing underlying db" )
2020-05-05 13:29:47 -04:00
}
info := RetryInfo {}
for attempts := uint ( 1 ); ; attempts ++ {
if attempts > retries + 1 {
2020-12-21 10:07:34 -08:00
return info , errors . New ( errors . MaxRetries , op , fmt . Sprintf ( "Too many retries: %d of %d" , attempts - 1 , retries + 1 ))
2020-05-05 13:29:47 -04:00
}
2020-05-15 08:38:36 -04:00
// step one of this, start a transaction...
2020-05-11 21:05:11 -04:00
newTx := w . underlying . BeginTx ( ctx , nil )
2020-05-15 08:38:36 -04:00
2020-06-22 13:27:26 -07:00
rw := & Db { newTx }
if err := Handler ( rw , rw ); err != nil {
2020-05-15 08:38:36 -04:00
if err := newTx . Rollback (). Error ; err != nil {
2020-12-21 10:07:34 -08:00
return info , errors . Wrap ( err , op )
2020-05-15 08:38:36 -04:00
}
2021-01-13 15:50:07 -08:00
if errors . Match ( errors . T ( errors . TicketAlreadyRedeemed ), err ) {
2020-05-05 13:29:47 -04:00
d := backOff . Duration ( attempts )
info . Retries ++
info . Backoff = info . Backoff + d
time . Sleep ( d )
continue
}
2020-12-21 10:07:34 -08:00
return info , errors . Wrap ( err , op )
2020-05-15 08:38:36 -04:00
}
if err := newTx . Commit (). Error ; err != nil {
if err := newTx . Rollback (). Error ; err != nil {
2020-12-21 10:07:34 -08:00
return info , errors . Wrap ( err , op )
2020-05-05 13:29:47 -04:00
}
2020-12-21 10:07:34 -08:00
return info , errors . Wrap ( err , op )
2020-05-05 13:29:47 -04:00
}
2020-05-15 08:38:36 -04:00
return info , nil // it all worked!!!
2020-05-05 13:29:47 -04:00
}
}
2020-06-29 08:18:57 -04:00
// LookupByPublicId will lookup resource by its public_id or private_id, which
// must be unique. Options are ignored.
2020-12-21 10:07:34 -08:00
func ( rw * Db ) LookupById ( _ context . Context , resourceWithIder interface {}, _ ... Option ) error {
const op = "db.LookupById"
2020-05-11 21:05:11 -04:00
if rw . underlying == nil {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "missing underlying db" )
2020-05-05 13:29:47 -04:00
}
2020-06-29 08:18:57 -04:00
if reflect . ValueOf ( resourceWithIder ). Kind () != reflect . Ptr {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "interface parameter must to be a pointer" )
2020-05-05 13:29:47 -04:00
}
2020-06-29 08:18:57 -04:00
primaryKey , where , err := primaryKeyWhere ( resourceWithIder )
if err != nil {
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op )
2020-05-05 13:29:47 -04:00
}
2020-06-29 08:18:57 -04:00
if err := rw . underlying . Where ( where , primaryKey ). First ( resourceWithIder ). Error ; err != nil {
2020-05-11 16:21:09 -04:00
if err == gorm . ErrRecordNotFound {
2020-12-21 10:07:34 -08:00
return errors . E ( errors . WithCode ( errors . RecordNotFound ), errors . WithOp ( op ))
2020-05-11 16:21:09 -04:00
}
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op )
2020-05-11 16:21:09 -04:00
}
return nil
2020-05-05 13:29:47 -04:00
}
2020-06-29 08:18:57 -04:00
func primaryKeyWhere ( resourceWithIder interface {}) ( pkey string , w string , e error ) {
2020-12-21 10:07:34 -08:00
const op = "db.primaryKeyWhere"
2020-06-29 08:18:57 -04:00
var primaryKey , where string
switch resourceType := resourceWithIder .( type ) {
case ResourcePublicIder :
primaryKey = resourceType . GetPublicId ()
where = "public_id = ?"
case ResourcePrivateIder :
primaryKey = resourceType . GetPrivateId ()
where = "private_id = ?"
default :
2020-12-21 10:07:34 -08:00
return "" , "" , errors . New ( errors . InvalidParameter , op , fmt . Sprintf ( "unsupported interface type %T" , resourceWithIder ))
2020-06-29 08:18:57 -04:00
}
if primaryKey == "" {
2020-12-21 10:07:34 -08:00
return "" , "" , errors . New ( errors . InvalidParameter , op , "missing primary key" )
2020-06-29 08:18:57 -04:00
}
return primaryKey , where , nil
}
// LookupByPublicId will lookup resource by its public_id, which must be unique.
// Options are ignored.
func ( rw * Db ) LookupByPublicId ( ctx context . Context , resource ResourcePublicIder , opt ... Option ) error {
return rw . LookupById ( ctx , resource , opt ... )
}
2020-05-05 13:29:47 -04:00
// LookupWhere will lookup the first resource using a where clause with parameters (it only returns the first one)
2020-12-21 10:07:34 -08:00
func ( rw * Db ) LookupWhere ( _ context . Context , resource interface {}, where string , args ... interface {}) error {
const op = "db.LookupWhere"
2020-05-11 21:05:11 -04:00
if rw . underlying == nil {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "missing underlying db" )
2020-05-05 13:29:47 -04:00
}
if reflect . ValueOf ( resource ). Kind () != reflect . Ptr {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "interface parameter must to be a pointer" )
2020-05-05 13:29:47 -04:00
}
2020-05-18 12:57:45 -04:00
if err := rw . underlying . Where ( where , args ... ). First ( resource ). Error ; err != nil {
if err == gorm . ErrRecordNotFound {
2020-12-21 10:07:34 -08:00
return errors . E ( errors . WithCode ( errors . RecordNotFound ), errors . WithOp ( op ))
2020-05-18 12:57:45 -04:00
}
2020-12-23 11:00:00 -08:00
return errors . Wrap ( err , op )
2020-05-18 12:57:45 -04:00
}
return nil
2020-05-05 13:29:47 -04:00
}
2020-06-16 18:59:30 -04:00
// SearchWhere will search for all the resources it can find using a where
// clause with parameters. Supports the WithLimit option. If
// WithLimit < 0, then unlimited results are returned. If WithLimit == 0, then
2020-08-19 16:47:18 -04:00
// default limits are used for results. Supports the WithOrder option.
2020-12-21 10:07:34 -08:00
func ( rw * Db ) SearchWhere ( _ context . Context , resources interface {}, where string , args [] interface {}, opt ... Option ) error {
const op = "db.SearchWhere"
2020-06-16 18:59:30 -04:00
opts := GetOpts ( opt ... )
2020-05-11 21:05:11 -04:00
if rw . underlying == nil {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "missing underlying db" )
2020-05-05 13:29:47 -04:00
}
if reflect . ValueOf ( resources ). Kind () != reflect . Ptr {
2020-12-21 10:07:34 -08:00
return errors . New ( errors . InvalidParameter , op , "interface parameter must to be a pointer" )
2020-05-05 13:29:47 -04:00
}
2020-06-16 18:59:30 -04:00
var err error
2020-08-24 13:22:10 -04:00
db := rw . underlying . Order ( opts . withOrder )
// Perform limiting
2020-06-16 18:59:30 -04:00
switch {
case opts . WithLimit < 0 : // any negative number signals unlimited results
case opts . WithLimit == 0 : // zero signals the default value and default limits
2020-08-24 13:22:10 -04:00
db = db . Limit ( DefaultLimit )
2020-06-16 18:59:30 -04:00
default :
2020-08-24 13:22:10 -04:00
db = db . Limit ( opts . WithLimit )
2020-06-16 18:59:30 -04:00
}
2020-08-24 13:22:10 -04:00
// Perform argument subst
switch len ( args ) {
case 0 :
default :
db = db . Where ( where , args ... )
}
// Perform the query
err = db . Find ( resources ). Error
2020-06-16 18:59:30 -04:00
if err != nil {
2020-05-18 12:57:45 -04:00
// searching with a slice parameter does not return a gorm.ErrRecordNotFound
2020-12-21 10:07:34 -08:00
return errors . Wrap ( err , op )
2020-05-18 12:57:45 -04:00
}
return nil
2020-05-05 13:29:47 -04:00
}
2020-05-21 09:24:37 -04:00
2020-05-31 19:55:56 -04:00
// filterPaths will filter out non-updatable fields
func filterPaths ( paths [] string ) [] string {
if len ( paths ) == 0 {
2020-05-21 09:24:37 -04:00
return nil
}
filtered := [] string {}
2020-05-31 19:55:56 -04:00
for _ , p := range paths {
2020-05-21 09:24:37 -04:00
switch {
case strings . EqualFold ( p , "CreateTime" ):
continue
case strings . EqualFold ( p , "UpdateTime" ):
continue
2020-05-21 09:30:52 -04:00
case strings . EqualFold ( p , "PublicId" ):
2020-05-21 09:24:37 -04:00
continue
default :
filtered = append ( filtered , p )
}
}
return filtered
}
func setFieldsToNil ( i interface {}, fieldNames [] string ) {
2021-01-21 16:27:20 -05:00
// Note: error cases are not handled
_ = Clear ( i , fieldNames , 2 )
2020-05-21 09:24:37 -04:00
}
2020-05-31 19:55:56 -04:00
func isNil ( i interface {}) bool {
if i == nil {
return true
}
switch reflect . TypeOf ( i ). Kind () {
case reflect . Ptr , reflect . Map , reflect . Array , reflect . Chan , reflect . Slice :
return reflect . ValueOf ( i ). IsNil ()
}
return false
}
2020-06-03 09:51:34 -04:00
func contains ( ss [] string , t string ) bool {
for _ , s := range ss {
if strings . EqualFold ( s , t ) {
return true
}
}
return false
}
2020-06-28 16:55:57 -04:00
// Clear sets fields in the value pointed to by i to their zero value.
// Clear descends i to depth clearing fields at each level. i must be a
// pointer to a struct. Cycles in i are not detected.
//
// A depth of 2 will change i and i's children. A depth of 1 will change i
// but no children of i. A depth of 0 will return with no changes to i.
func Clear ( i interface {}, fields [] string , depth int ) error {
2020-12-21 10:07:34 -08:00
const op = "db.Clear"
2020-06-28 16:55:57 -04:00
if len ( fields ) == 0 || depth == 0 {
return nil
}
fm := make ( map [ string ] bool )
for _ , f := range fields {
fm [ f ] = true
}
v := reflect . ValueOf ( i )
switch v . Kind () {
case reflect . Ptr :
if v . IsNil () || v . Elem (). Kind () != reflect . Struct {
2020-12-21 10:07:34 -08:00
return errors . E ( errors . WithCode ( errors . InvalidParameter ), errors . WithOp ( op ))
2020-06-28 16:55:57 -04:00
}
clear ( v , fm , depth )
2020-12-21 10:07:34 -08:00
default :
return errors . E ( errors . WithCode ( errors . InvalidParameter ), errors . WithOp ( op ))
2020-06-28 16:55:57 -04:00
}
return nil
}
func clear ( v reflect . Value , fields map [ string ] bool , depth int ) {
if depth == 0 {
return
}
depth --
switch v . Kind () {
case reflect . Ptr :
clear ( v . Elem (), fields , depth + 1 )
case reflect . Struct :
typeOfT := v . Type ()
for i := 0 ; i < v . NumField (); i ++ {
f := v . Field ( i )
if ok := fields [ typeOfT . Field ( i ). Name ]; ok {
if f . IsValid () && f . CanSet () {
f . Set ( reflect . Zero ( f . Type ()))
}
continue
}
clear ( f , fields , depth )
}
}
}