github.com/Accefy/pop@v0.0.0-20230428174248-e9f677eab5b9/connection_details.go (about)

     1  package pop
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"net/url"
     7  	"regexp"
     8  	"strconv"
     9  	"strings"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/Accefy/pop/internal/defaults"
    14  	"github.com/gobuffalo/pop/v6/logging"
    15  	"github.com/luna-duclos/instrumentedsql"
    16  )
    17  
    18  // ConnectionDetails stores the data needed to connect to a datasource
    19  type ConnectionDetails struct {
    20  	// Dialect is the pop dialect to use. Example: "postgres" or "sqlite3" or "mysql"
    21  	Dialect string
    22  	// Driver specifies the database driver to use (optional)
    23  	Driver string
    24  	// The name of your database. Example: "foo_development"
    25  	Database string
    26  	// The host of your database. Example: "127.0.0.1"
    27  	Host string
    28  	// The port of your database. Example: 1234
    29  	// Will default to the "default" port for each dialect.
    30  	Port string
    31  	// The username of the database user. Example: "root"
    32  	User string
    33  	// The password of the database user. Example: "password"
    34  	Password string
    35  	// The encoding to use to create the database and communicate with it.
    36  	Encoding string
    37  	// Instead of specifying each individual piece of the
    38  	// connection you can instead just specify the URL of the
    39  	// database. Example: "postgres://postgres:postgres@localhost:5432/pop_test?sslmode=disable"
    40  	URL string
    41  	// Defaults to 0 "unlimited". See https://golang.org/pkg/database/sql/#DB.SetMaxOpenConns
    42  	Pool int
    43  	// Defaults to 2. See https://golang.org/pkg/database/sql/#DB.SetMaxIdleConns
    44  	IdlePool int
    45  	// Defaults to 0 "unlimited". See https://golang.org/pkg/database/sql/#DB.SetConnMaxLifetime
    46  	ConnMaxLifetime time.Duration
    47  	// Defaults to 0 "unlimited". See https://golang.org/pkg/database/sql/#DB.SetConnMaxIdleTime
    48  	ConnMaxIdleTime time.Duration
    49  	// Defaults to `false`. See https://godoc.org/github.com/jmoiron/sqlx#DB.Unsafe
    50  	Unsafe bool
    51  	// Options stores Connection Details options
    52  	Options     map[string]string
    53  	optionsLock *sync.Mutex
    54  	// Query string encoded options from URL. Example: "sslmode=disable"
    55  	RawOptions string
    56  	// UseInstrumentedDriver if set to true uses a wrapper for the underlying driver which exposes tracing
    57  	// information in the Open Tracing, Open Census, Google, and AWS Xray format. This is useful when using
    58  	// tracing with Jaeger, DataDog, Zipkin, or other tracing software.
    59  	UseInstrumentedDriver bool
    60  	// InstrumentedDriverOptions sets the options for the instrumented driver. These options are empty by default meaning
    61  	// that instrumentation is disabled.
    62  	//
    63  	// For more information check out the docs at https://github.com/luna-duclos/instrumentedsql. If you use Open Tracing, these options
    64  	// could looks as follows:
    65  	//
    66  	//		InstrumentedDriverOptions: []instrumentedsql.Opt{instrumentedsql.WithTracer(opentracing.NewTracer(true))}
    67  	//
    68  	// It is also recommended to include `instrumentedsql.WithOmitArgs()` which prevents SQL arguments (e.g. passwords)
    69  	// from being traced or logged.
    70  	InstrumentedDriverOptions []instrumentedsql.Opt
    71  }
    72  
    73  var dialectX = regexp.MustCompile(`\S+://`)
    74  
    75  // withURL parses and overrides all connection details with values
    76  // from standard URL except Dialect. It also calls dialect specific
    77  // URL parser if exists.
    78  func (cd *ConnectionDetails) withURL() error {
    79  	ul := cd.URL
    80  	if cd.Dialect == "" {
    81  		if dialectX.MatchString(ul) {
    82  			// Guess the dialect from the scheme
    83  			dialect := ul[:strings.Index(ul, ":")]
    84  			cd.Dialect = CanonicalDialect(dialect)
    85  		} else {
    86  			return errors.New("no dialect provided, and could not guess it from URL")
    87  		}
    88  	} else if !dialectX.MatchString(ul) {
    89  		ul = cd.Dialect + "://" + ul
    90  	}
    91  
    92  	if !DialectSupported(cd.Dialect) {
    93  		return fmt.Errorf("unsupported dialect '%s'", cd.Dialect)
    94  	}
    95  
    96  	// warning message is required to prevent confusion
    97  	// even though this behavior was documented.
    98  	if cd.Database+cd.Host+cd.Port+cd.User+cd.Password != "" {
    99  		log(logging.Warn, "One or more of connection details are specified in database.yml. Override them with values in URL.")
   100  	}
   101  
   102  	if up, ok := urlParser[cd.Dialect]; ok {
   103  		return up(cd)
   104  	}
   105  
   106  	// Fallback on generic parsing if no URL parser was found for the dialect.
   107  	u, err := url.Parse(ul)
   108  	if err != nil {
   109  		return fmt.Errorf("couldn't parse %s: %w", ul, err)
   110  	}
   111  	cd.Database = strings.TrimPrefix(u.Path, "/")
   112  
   113  	hp := strings.Split(u.Host, ":")
   114  	cd.Host = hp[0]
   115  	if len(hp) > 1 {
   116  		cd.Port = hp[1]
   117  	}
   118  
   119  	if u.User != nil {
   120  		cd.User = u.User.Username()
   121  		cd.Password, _ = u.User.Password()
   122  	}
   123  	cd.RawOptions = u.RawQuery
   124  
   125  	return nil
   126  }
   127  
   128  // Finalize cleans up the connection details by normalizing names,
   129  // filling in default values, etc...
   130  func (cd *ConnectionDetails) Finalize() error {
   131  	cd.Dialect = CanonicalDialect(cd.Dialect)
   132  
   133  	if cd.Options == nil { // for safety
   134  		cd.Options = make(map[string]string)
   135  	}
   136  
   137  	// Process the database connection string, if provided.
   138  	if cd.URL != "" {
   139  		if err := cd.withURL(); err != nil {
   140  			return err
   141  		}
   142  	}
   143  
   144  	if fin, ok := finalizer[cd.Dialect]; ok {
   145  		fin(cd)
   146  	}
   147  
   148  	if DialectSupported(cd.Dialect) {
   149  		if cd.Database != "" || cd.URL != "" {
   150  			return nil
   151  		}
   152  		return errors.New("no database or URL specified")
   153  	}
   154  	return fmt.Errorf("unsupported dialect '%v'", cd.Dialect)
   155  }
   156  
   157  // RetrySleep returns the amount of time to wait between two connection retries
   158  func (cd *ConnectionDetails) RetrySleep() time.Duration {
   159  	d, err := time.ParseDuration(defaults.String(cd.Options["retry_sleep"], "1ms"))
   160  	if err != nil {
   161  		return 1 * time.Millisecond
   162  	}
   163  	return d
   164  }
   165  
   166  // RetryLimit returns the maximum number of accepted connection retries
   167  func (cd *ConnectionDetails) RetryLimit() int {
   168  	i, err := strconv.Atoi(defaults.String(cd.Options["retry_limit"], "1000"))
   169  	if err != nil {
   170  		return 100
   171  	}
   172  	return i
   173  }
   174  
   175  // MigrationTableName returns the name of the table to track migrations
   176  func (cd *ConnectionDetails) MigrationTableName() string {
   177  	return defaults.String(cd.Options["migration_table_name"], "schema_migration")
   178  }
   179  
   180  // OptionsString returns URL parameter encoded string from options.
   181  func (cd *ConnectionDetails) OptionsString(s string) string {
   182  	if cd.RawOptions != "" {
   183  		return cd.RawOptions
   184  	}
   185  	if cd.Options != nil {
   186  		for k, v := range cd.Options {
   187  			if k == "migration_table_name" {
   188  				continue
   189  			}
   190  
   191  			s = fmt.Sprintf("%s&%s=%s", s, k, v)
   192  		}
   193  	}
   194  	return strings.TrimLeft(s, "&")
   195  }
   196  
   197  // option returns the value stored in ConnecitonDetails.Options with key k.
   198  func (cd *ConnectionDetails) option(k string) string {
   199  	if cd.Options == nil {
   200  		return ""
   201  	}
   202  	return defaults.String(cd.Options[k], "")
   203  }
   204  
   205  // setOptionWithDefault stores given value v in ConnectionDetails.Options
   206  // with key k. If v is empty string, it stores def instead.
   207  // It uses locking mechanism to make the operation safe.
   208  func (cd *ConnectionDetails) setOptionWithDefault(k, v, def string) {
   209  	cd.setOption(k, defaults.String(v, def))
   210  }
   211  
   212  // setOption stores given value v in ConnectionDetails.Options with key k.
   213  // It uses locking mechanism to make the operation safe.
   214  func (cd *ConnectionDetails) setOption(k, v string) {
   215  	if cd.optionsLock == nil {
   216  		cd.optionsLock = &sync.Mutex{}
   217  	}
   218  
   219  	cd.optionsLock.Lock()
   220  	if cd.Options == nil { // prevent panic
   221  		cd.Options = make(map[string]string)
   222  	}
   223  
   224  	cd.Options[k] = v
   225  	cd.optionsLock.Unlock()
   226  }