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

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