github.com/rjgonzale/pop/v5@v5.1.3-dev/connection.go (about)

     1  package pop
     2  
     3  import (
     4  	"context"
     5  	"sync/atomic"
     6  	"time"
     7  
     8  	"github.com/gobuffalo/pop/v5/internal/defaults"
     9  	"github.com/gobuffalo/pop/v5/internal/randx"
    10  	"github.com/jmoiron/sqlx"
    11  	"github.com/pkg/errors"
    12  )
    13  
    14  // Connections contains all available connections
    15  var Connections = map[string]*Connection{}
    16  
    17  // Connection represents all necessary details to talk with a datastore
    18  type Connection struct {
    19  	ID          string
    20  	Store       store
    21  	Dialect     dialect
    22  	Elapsed     int64
    23  	TX          *Tx
    24  	eager       bool
    25  	eagerFields []string
    26  }
    27  
    28  func (c *Connection) String() string {
    29  	return c.URL()
    30  }
    31  
    32  // URL returns the datasource connection string
    33  func (c *Connection) URL() string {
    34  	return c.Dialect.URL()
    35  }
    36  
    37  // MigrationURL returns the datasource connection string used for running the migrations
    38  func (c *Connection) MigrationURL() string {
    39  	return c.Dialect.MigrationURL()
    40  }
    41  
    42  // MigrationTableName returns the name of the table to track migrations
    43  func (c *Connection) MigrationTableName() string {
    44  	return c.Dialect.Details().MigrationTableName()
    45  }
    46  
    47  // NewConnection creates a new connection, and sets it's `Dialect`
    48  // appropriately based on the `ConnectionDetails` passed into it.
    49  func NewConnection(deets *ConnectionDetails) (*Connection, error) {
    50  	err := deets.Finalize()
    51  	if err != nil {
    52  		return nil, err
    53  	}
    54  	c := &Connection{
    55  		ID: randx.String(30),
    56  	}
    57  
    58  	if nc, ok := newConnection[deets.Dialect]; ok {
    59  		c.Dialect, err = nc(deets)
    60  		if err != nil {
    61  			return c, errors.Wrap(err, "could not create new connection")
    62  		}
    63  		return c, nil
    64  	}
    65  	return nil, errors.Errorf("could not found connection creator for %v", deets.Dialect)
    66  }
    67  
    68  // Connect takes the name of a connection, default is "development", and will
    69  // return that connection from the available `Connections`. If a connection with
    70  // that name can not be found an error will be returned. If a connection is
    71  // found, and it has yet to open a connection with its underlying datastore,
    72  // a connection to that store will be opened.
    73  func Connect(e string) (*Connection, error) {
    74  	if len(Connections) == 0 {
    75  		err := LoadConfigFile()
    76  		if err != nil {
    77  			return nil, err
    78  		}
    79  	}
    80  	e = defaults.String(e, "development")
    81  	c := Connections[e]
    82  	if c == nil {
    83  		return c, errors.Errorf("could not find connection named %s", e)
    84  	}
    85  	err := c.Open()
    86  	return c, errors.Wrapf(err, "couldn't open connection for %s", e)
    87  }
    88  
    89  // Open creates a new datasource connection
    90  func (c *Connection) Open() error {
    91  	if c.Store != nil {
    92  		return nil
    93  	}
    94  	if c.Dialect == nil {
    95  		return errors.New("invalid connection instance")
    96  	}
    97  	details := c.Dialect.Details()
    98  	driver := c.Dialect.DefaultDriver()
    99  	if details.Driver != "" {
   100  		driver = details.Driver
   101  	}
   102  
   103  	db, err := sqlx.Open(driver, c.Dialect.URL())
   104  	if err != nil {
   105  		return errors.Wrap(err, "could not open database connection")
   106  	}
   107  	db.SetMaxOpenConns(details.Pool)
   108  	if details.IdlePool != 0 {
   109  		db.SetMaxIdleConns(details.IdlePool)
   110  	}
   111  	if details.ConnMaxLifetime > 0 {
   112  		db.SetConnMaxLifetime(details.ConnMaxLifetime)
   113  	}
   114  	if details.Unsafe {
   115  		db = db.Unsafe()
   116  	}
   117  	c.Store = &dB{db}
   118  
   119  	if d, ok := c.Dialect.(afterOpenable); ok {
   120  		err = d.AfterOpen(c)
   121  		if err != nil {
   122  			c.Store = nil
   123  		}
   124  	}
   125  	return errors.Wrap(err, "could not open database connection")
   126  }
   127  
   128  // Close destroys an active datasource connection
   129  func (c *Connection) Close() error {
   130  	return errors.Wrap(c.Store.Close(), "couldn't close connection")
   131  }
   132  
   133  // Transaction will start a new transaction on the connection. If the inner function
   134  // returns an error then the transaction will be rolled back, otherwise the transaction
   135  // will automatically commit at the end.
   136  func (c *Connection) Transaction(fn func(tx *Connection) error) error {
   137  	return c.Dialect.Lock(func() error {
   138  		var dberr error
   139  		cn, err := c.NewTransaction()
   140  		if err != nil {
   141  			return err
   142  		}
   143  		err = fn(cn)
   144  		if err != nil {
   145  			dberr = cn.TX.Rollback()
   146  		} else {
   147  			dberr = cn.TX.Commit()
   148  		}
   149  		if err != nil {
   150  			return err
   151  		}
   152  		return errors.Wrap(dberr, "error committing or rolling back transaction")
   153  	})
   154  
   155  }
   156  
   157  // Rollback will open a new transaction and automatically rollback that transaction
   158  // when the inner function returns, regardless. This can be useful for tests, etc...
   159  func (c *Connection) Rollback(fn func(tx *Connection)) error {
   160  	cn, err := c.NewTransaction()
   161  	if err != nil {
   162  		return err
   163  	}
   164  	fn(cn)
   165  	return cn.TX.Rollback()
   166  }
   167  
   168  // NewTransaction starts a new transaction on the connection
   169  func (c *Connection) NewTransaction() (*Connection, error) {
   170  	var cn *Connection
   171  	if c.TX == nil {
   172  		tx, err := c.Store.Transaction()
   173  		if err != nil {
   174  			return cn, errors.Wrap(err, "couldn't start a new transaction")
   175  		}
   176  		var store store = tx
   177  
   178  		// Rewrap the store if it was a context store
   179  		if cs, ok := c.Store.(contextStore); ok {
   180  			store = contextStore{store: store, ctx: cs.ctx}
   181  		}
   182  		cn = &Connection{
   183  			ID:      randx.String(30),
   184  			Store:   store,
   185  			Dialect: c.Dialect,
   186  			TX:      tx,
   187  		}
   188  	} else {
   189  		cn = c
   190  	}
   191  	return cn, nil
   192  }
   193  
   194  // WithContext returns a copy of the connection, wrapped with a context.
   195  func (c *Connection) WithContext(ctx context.Context) *Connection {
   196  	cn := c.copy()
   197  	cn.Store = contextStore{
   198  		store: cn.Store,
   199  		ctx:   ctx,
   200  	}
   201  	return cn
   202  }
   203  
   204  func (c *Connection) copy() *Connection {
   205  	return &Connection{
   206  		ID:      randx.String(30),
   207  		Store:   c.Store,
   208  		Dialect: c.Dialect,
   209  		TX:      c.TX,
   210  	}
   211  }
   212  
   213  // Q creates a new "empty" query for the current connection.
   214  func (c *Connection) Q() *Query {
   215  	return Q(c)
   216  }
   217  
   218  // disableEager disables eager mode for current connection.
   219  func (c *Connection) disableEager() {
   220  	c.eager = false
   221  	c.eagerFields = []string{}
   222  }
   223  
   224  // TruncateAll truncates all data from the datasource
   225  func (c *Connection) TruncateAll() error {
   226  	return c.Dialect.TruncateAll(c)
   227  }
   228  
   229  func (c *Connection) timeFunc(name string, fn func() error) error {
   230  	start := time.Now()
   231  	err := fn()
   232  	atomic.AddInt64(&c.Elapsed, int64(time.Since(start)))
   233  	if err != nil {
   234  		return err
   235  	}
   236  	return nil
   237  }