go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/db/connection.go (about)

     1  /*
     2  
     3  Copyright (c) 2023 - Present. Will Charczuk. All rights reserved.
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository.
     5  
     6  */
     7  
     8  package db
     9  
    10  import (
    11  	"context"
    12  	"database/sql"
    13  	"reflect"
    14  	"sync"
    15  	"time"
    16  )
    17  
    18  // New returns a new Connection.
    19  // It will use very bare bones defaults for the config.
    20  func New(options ...Option) (*Connection, error) {
    21  	var c Connection
    22  	var err error
    23  	for _, opt := range options {
    24  		if err = opt(&c); err != nil {
    25  			return nil, err
    26  		}
    27  	}
    28  	return &c, nil
    29  }
    30  
    31  // MustNew returns a new connection and panics on error.
    32  func MustNew(options ...Option) *Connection {
    33  	c, err := New(options...)
    34  	if err != nil {
    35  		panic(err)
    36  	}
    37  	return c
    38  }
    39  
    40  // Connection is a
    41  type Connection struct {
    42  	Config
    43  
    44  	conn    *sql.DB
    45  	bp      *BufferPool
    46  	mcmu    sync.RWMutex
    47  	mc      map[string]*TypeMeta
    48  	onQuery []func(QueryEvent)
    49  }
    50  
    51  // RegisterOnQuery adds an on query listener.
    52  func (dbc *Connection) RegisterOnQuery(fn func(QueryEvent)) {
    53  	dbc.onQuery = append(dbc.onQuery, fn)
    54  }
    55  
    56  // Stats returns the stats for the connection.
    57  func (dbc *Connection) Stats() sql.DBStats {
    58  	return dbc.conn.Stats()
    59  }
    60  
    61  // Open returns a connection object, either a cached connection object or creating a new one in the process.
    62  func (dbc *Connection) Open() error {
    63  	// bail if we've already opened the connection.
    64  	if dbc.conn != nil {
    65  		return ErrConnectionAlreadyOpen
    66  	}
    67  	if dbc.Config.IsZero() {
    68  		return ErrConfigUnset
    69  	}
    70  	if dbc.bp == nil {
    71  		dbc.bp = NewBufferPool(dbc.Config.BufferPoolSize)
    72  	}
    73  	if dbc.mc == nil {
    74  		dbc.mc = make(map[string]*TypeMeta)
    75  	}
    76  
    77  	dbConn, err := sql.Open(dbc.Config.Engine, dbc.Config.CreateDSN())
    78  	if err != nil {
    79  		return err
    80  	}
    81  	dbc.conn = dbConn
    82  	dbc.conn.SetConnMaxLifetime(dbc.Config.MaxLifetime)
    83  	dbc.conn.SetConnMaxIdleTime(dbc.Config.MaxIdleTime)
    84  	dbc.conn.SetMaxIdleConns(dbc.Config.IdleConnections)
    85  	dbc.conn.SetMaxOpenConns(dbc.Config.MaxConnections)
    86  	return nil
    87  }
    88  
    89  // Close implements a closer.
    90  func (dbc *Connection) Close() error {
    91  	return dbc.conn.Close()
    92  }
    93  
    94  // TypeMeta returns the TypeMeta for an object.
    95  func (dbc *Connection) TypeMeta(object any) *TypeMeta {
    96  	objectType := reflect.TypeOf(object)
    97  	return dbc.TypeMetaFromType(newColumnCacheKey(objectType), objectType)
    98  }
    99  
   100  // TypeMetaFromType reflects a reflect.Type into a column collection.
   101  // The results of this are cached for speed.
   102  func (dbc *Connection) TypeMetaFromType(identifier string, t reflect.Type) *TypeMeta {
   103  	dbc.mcmu.RLock()
   104  	if value, ok := dbc.mc[identifier]; ok {
   105  		dbc.mcmu.RUnlock()
   106  		return value
   107  	}
   108  	dbc.mcmu.RUnlock()
   109  
   110  	dbc.mcmu.Lock()
   111  	defer dbc.mcmu.Unlock()
   112  
   113  	// check again once exclusive is acquired
   114  	// because we may have been waiting on another routine
   115  	// to set the identifier.
   116  	if value, ok := dbc.mc[identifier]; ok {
   117  		return value
   118  	}
   119  
   120  	metadata := NewTypeMetaFromColumns(generateColumnsForType(nil, t)...)
   121  	dbc.mc[identifier] = metadata
   122  	return metadata
   123  }
   124  
   125  // BeginTx starts a new transaction in a givent context.
   126  func (dbc *Connection) BeginTx(ctx context.Context, opts ...func(*sql.TxOptions)) (*sql.Tx, error) {
   127  	if dbc.conn == nil {
   128  		return nil, ErrConnectionClosed
   129  	}
   130  	var txOptions sql.TxOptions
   131  	for _, opt := range opts {
   132  		opt(&txOptions)
   133  	}
   134  	tx, err := dbc.conn.BeginTx(ctx, &txOptions)
   135  	return tx, err
   136  }
   137  
   138  // Prepare prepares a statement within a given context.
   139  // If a tx is provided, the tx is the target for the prepare.
   140  // This will trigger tracing on prepare.
   141  func (dbc *Connection) Prepare(ctx context.Context, statement string) (stmt *sql.Stmt, err error) {
   142  	stmt, err = dbc.conn.PrepareContext(ctx, statement)
   143  	return
   144  }
   145  
   146  // Invoke returns a new invocation.
   147  func (dbc *Connection) Invoke(options ...InvocationOption) *Invocation {
   148  	i := Invocation{
   149  		conn:    dbc,
   150  		ctx:     context.Background(),
   151  		started: time.Now().UTC(),
   152  	}
   153  	// we do a separate nil check here
   154  	// so that we don't end up with a non-nil pointer to nil
   155  	if dbc.conn != nil {
   156  		i.db = dbc.conn
   157  	}
   158  	for _, option := range options {
   159  		option(&i)
   160  	}
   161  	return &i
   162  }
   163  
   164  // Exec is a helper stub for .Invoke(...).Exec(...).
   165  func (dbc *Connection) Exec(statement string, args ...interface{}) (sql.Result, error) {
   166  	return dbc.Invoke().Exec(statement, args...)
   167  }
   168  
   169  // ExecContext is a helper stub for .Invoke(OptContext(ctx)).Exec(...).
   170  func (dbc *Connection) ExecContext(ctx context.Context, statement string, args ...interface{}) (sql.Result, error) {
   171  	return dbc.Invoke(OptContext(ctx)).Exec(statement, args...)
   172  }
   173  
   174  // Query is a helper stub for .Invoke(...).Query(...).
   175  func (dbc *Connection) Query(statement string, args ...interface{}) *Query {
   176  	return dbc.Invoke().Query(statement, args...)
   177  }
   178  
   179  // QueryContext is a helper stub for .Invoke(OptContext(ctx)).Query(...).
   180  func (dbc *Connection) QueryContext(ctx context.Context, statement string, args ...interface{}) *Query {
   181  	return dbc.Invoke(OptContext(ctx)).Query(statement, args...)
   182  }