github.com/dkishere/pop/v6@v6.103.1/connection_details.go (about)

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