github.com/friesencr/pop/v6@v6.1.6/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/friesencr/pop/v6/internal/defaults" 14 "github.com/friesencr/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 }