go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/db/config.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 "fmt" 13 "net/url" 14 "strings" 15 "time" 16 17 "go.charczuk.com/sdk/configutil" 18 ) 19 20 const ( 21 // DefaultEngine is the default database engine. 22 DefaultEngine = "pgx" // "postgres" 23 ) 24 25 const ( 26 // EnvVarDBEngine is the environment variable used to set the Go `sql` driver. 27 EnvVarDBEngine = "DB_ENGINE" 28 // EnvVarDatabaseURL is the environment variable used to set the entire 29 // database connection string. 30 EnvVarDatabaseURL = "DATABASE_URL" 31 // EnvVarDBHost is the environment variable used to set the host in a 32 // database connection string. 33 EnvVarDBHost = "DB_HOST" 34 // EnvVarDBPort is the environment variable used to set the port in a 35 // database connection string. 36 EnvVarDBPort = "DB_PORT" 37 // EnvVarDBName is the environment variable used to set the database name 38 // in a database connection string. 39 EnvVarDBName = "DB_NAME" 40 // EnvVarDBSchema is the environment variable used to set the database 41 // schema in a database connection string. 42 EnvVarDBSchema = "DB_SCHEMA" 43 // EnvVarDBApplicationName is the environment variable used to set the 44 // `application_name` configuration parameter in a `lib/pq` connection 45 // string. 46 // 47 // See: https://www.postgresql.org/docs/12/runtime-config-logging.html#GUC-APPLICATION-NAME 48 EnvVarDBApplicationName = "DB_APPLICATION_NAME" 49 // EnvVarDBUser is the environment variable used to set the user in a 50 // database connection string. 51 EnvVarDBUser = "DB_USER" 52 // EnvVarDBPassword is the environment variable used to set the password 53 // in a database connection string. 54 EnvVarDBPassword = "DB_PASSWORD" 55 // EnvVarDBConnectTimeout is is the environment variable used to set the 56 // connect timeout in a database connection string. 57 EnvVarDBConnectTimeout = "DB_CONNECT_TIMEOUT" 58 // EnvVarDBLockTimeout is is the environment variable used to set the lock 59 // timeout on a database config. 60 EnvVarDBLockTimeout = "DB_LOCK_TIMEOUT" 61 // EnvVarDBStatementTimeout is is the environment variable used to set the 62 // statement timeout on a database config. 63 EnvVarDBStatementTimeout = "DB_STATEMENT_TIMEOUT" 64 // EnvVarDBSSLMode is the environment variable used to set the SSL mode in 65 // a database connection string. 66 EnvVarDBSSLMode = "DB_SSLMODE" 67 // EnvVarDBIdleConnections is the environment variable used to set the 68 // maximum number of idle connections allowed in a connection pool. 69 EnvVarDBIdleConnections = "DB_IDLE_CONNECTIONS" 70 // EnvVarDBMaxConnections is the environment variable used to set the 71 // maximum number of connections allowed in a connection pool. 72 EnvVarDBMaxConnections = "DB_MAX_CONNECTIONS" 73 // EnvVarDBMaxLifetime is the environment variable used to set the maximum 74 // lifetime of a connection in a connection pool. 75 EnvVarDBMaxLifetime = "DB_MAX_LIFETIME" 76 // EnvVarDBMaxIdleTime is the environment variable used to set the maximum 77 // time a connection can be idle. 78 EnvVarDBMaxIdleTime = "DB_MAX_IDLE_TIME" 79 // EnvVarDBBufferPoolSize is the environment variable used to set the buffer 80 // pool size on a connection in a connection pool. 81 EnvVarDBBufferPoolSize = "DB_BUFFER_POOL_SIZE" 82 // EnvVarDBDialect is the environment variable used to set the dialect 83 // on a connection configuration (e.g. `postgres` or `cockroachdb`). 84 EnvVarDBDialect = "DB_DIALECT" 85 ) 86 87 const ( 88 // DefaultHost is the default database hostname, typically used 89 // when developing locally. 90 DefaultHost = "localhost" 91 // DefaultPort is the default postgres port. 92 DefaultPort = "5432" 93 // DefaultDatabase is the default database to connect to, we use 94 // `postgres` to not pollute the template databases. 95 DefaultDatabase = "postgres" 96 97 // DefaultSchema is the default schema to connect to 98 DefaultSchema = "public" 99 ) 100 101 const ( 102 // SSLModeDisable is an ssl mode. 103 // Postgres Docs: "I don't care about security, and I don't want to pay the overhead of encryption." 104 SSLModeDisable = "disable" 105 // SSLModeAllow is an ssl mode. 106 // Postgres Docs: "I don't care about security, but I will pay the overhead of encryption if the server insists on it." 107 SSLModeAllow = "allow" 108 // SSLModePrefer is an ssl mode. 109 // Postgres Docs: "I don't care about encryption, but I wish to pay the overhead of encryption if the server supports it" 110 SSLModePrefer = "prefer" 111 // SSLModeRequire is an ssl mode. 112 // Postgres Docs: "I want my data to be encrypted, and I accept the overhead. I trust that the network will make sure I always connect to the server I want." 113 SSLModeRequire = "require" 114 // SSLModeVerifyCA is an ssl mode. 115 // Postgres Docs: "I want my data encrypted, and I accept the overhead. I want to be sure that I connect to a server that I trust." 116 SSLModeVerifyCA = "verify-ca" 117 // SSLModeVerifyFull is an ssl mode. 118 // Postgres Docs: "I want my data encrypted, and I accept the overhead. I want to be sure that I connect to a server I trust, and that it's the one I specify." 119 SSLModeVerifyFull = "verify-full" 120 ) 121 122 const ( 123 // DefaultConnectTimeout is the default connect timeout. 124 DefaultConnectTimeout = 5 * time.Second 125 // DefaultIdleConnections is the default number of idle connections. 126 DefaultIdleConnections = 16 127 // DefaultMaxConnections is the default maximum number of connections. 128 DefaultMaxConnections = 32 129 // DefaultMaxLifetime is the default maximum lifetime of driver connections. 130 DefaultMaxLifetime = time.Duration(0) 131 // DefaultMaxIdleTime is the default maximum idle time of driver connections. 132 DefaultMaxIdleTime = time.Duration(0) 133 // DefaultBufferPoolSize is the default number of buffer pool entries to maintain. 134 DefaultBufferPoolSize = 1024 135 ) 136 137 // Config is a set of connection config options. 138 type Config struct { 139 // Engine is the database engine. 140 Engine string `json:"engine,omitempty" yaml:"engine,omitempty"` 141 // DSN is a fully formed DSN (this skips DSN formation from all other variables outside `schema`). 142 DSN string `json:"dsn,omitempty" yaml:"dsn,omitempty"` 143 // Host is the server to connect to. 144 Host string `json:"host,omitempty" yaml:"host,omitempty"` 145 // Port is the port to connect to. 146 Port string `json:"port,omitempty" yaml:"port,omitempty"` 147 // DBName is the database name 148 Database string `json:"database,omitempty" yaml:"database,omitempty"` 149 // Schema is the application schema within the database, defaults to `public`. This schema is used to set the 150 // Postgres "search_path" If you want to reference tables in other schemas, you'll need to specify those schemas 151 // in your queries e.g. "SELECT * FROM schema_two.table_one..." 152 // Using the public schema in a production application is considered bad practice as newly created roles will have 153 // visibility into this data by default. We strongly recommend specifying this option and using a schema that is 154 // owned by your service's role 155 // We recommend against setting a multi-schema search_path, but if you really want to, you provide multiple comma- 156 // separated schema names as the value for this config, or you can dbc.Invoke().Exec a SET statement on a newly 157 // opened connection such as "SET search_path = 'schema_one,schema_two';" Again, we recommend against this practice 158 // and encourage you to specify schema names beyond the first in your queries. 159 Schema string `json:"schema,omitempty" yaml:"schema,omitempty"` 160 // ApplicationName is the name set by an application connection to a database 161 // server, intended to be transmitted in the connection string. It can be 162 // used to uniquely identify an application and will be included in the 163 // `pg_stat_activity` view. 164 // 165 // See: https://www.postgresql.org/docs/12/runtime-config-logging.html#GUC-APPLICATION-NAME 166 ApplicationName string `json:"applicationName,omitempty" yaml:"applicationName,omitempty"` 167 // Username is the username for the connection via password auth. 168 Username string `json:"username,omitempty" yaml:"username,omitempty"` 169 // Password is the password for the connection via password auth. 170 Password string `json:"password,omitempty" yaml:"password,omitempty"` 171 // ConnectTimeout determines the maximum wait for connection. The minimum 172 // allowed timeout is 2 seconds, so anything below is treated the same 173 // as unset. PostgreSQL will only accept second precision so this value will be 174 // rounded to the nearest second before being set on a connection string. 175 // Use `Validate()` to confirm that `ConnectTimeout` is exact to second 176 // precision. 177 // 178 // See: https://www.postgresql.org/docs/10/libpq-connect.html#LIBPQ-CONNECT-CONNECT-TIMEOUT 179 ConnectTimeout time.Duration `json:"connectTimeout,omitempty" yaml:"connectTimeout,omitempty"` 180 // LockTimeout is the timeout to use when attempting to acquire a lock. 181 // PostgreSQL will only accept millisecond precision so this value will be 182 // rounded to the nearest millisecond before being set on a connection string. 183 // Use `Validate()` to confirm that `LockTimeout` is exact to millisecond 184 // precision. 185 // 186 // See: https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-LOCK-TIMEOUT 187 LockTimeout time.Duration `json:"lockTimeout,omitempty" yaml:"lockTimeout,omitempty"` 188 // StatementTimeout is the timeout to use when invoking a SQL statement. 189 // PostgreSQL will only accept millisecond precision so this value will be 190 // rounded to the nearest millisecond before being set on a connection string. 191 // Use `Validate()` to confirm that `StatementTimeout` is exact to millisecond 192 // precision. 193 // 194 // See: https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STATEMENT-TIMEOUT 195 StatementTimeout time.Duration `json:"statementTimeout,omitempty" yaml:"statementTimeout,omitempty"` 196 // SSLMode is the sslmode for the connection. 197 SSLMode string `json:"sslMode,omitempty" yaml:"sslMode,omitempty"` 198 // IdleConnections is the number of idle connections. 199 IdleConnections int `json:"idleConnections,omitempty" yaml:"idleConnections,omitempty"` 200 // MaxConnections is the maximum number of connections. 201 MaxConnections int `json:"maxConnections,omitempty" yaml:"maxConnections,omitempty"` 202 // MaxLifetime is the maximum time a connection can be open. 203 MaxLifetime time.Duration `json:"maxLifetime,omitempty" yaml:"maxLifetime,omitempty"` 204 // MaxIdleTime is the maximum time a connection can be idle. 205 MaxIdleTime time.Duration `json:"maxIdleTime,omitempty" yaml:"maxIdleTime,omitempty"` 206 // BufferPoolSize is the number of query composition buffers to maintain. 207 BufferPoolSize int `json:"bufferPoolSize,omitempty" yaml:"bufferPoolSize,omitempty"` 208 // Dialect includes hints to tweak specific sql semantics by database connection. 209 Dialect string `json:"dialect,omitempty" yaml:"dialect,omitempty"` 210 } 211 212 // IsZero returns if the config is unset. 213 func (c Config) IsZero() bool { 214 return c.DSN == "" && c.Host == "" && c.Port == "" && c.Database == "" && c.Schema == "" && c.Username == "" && c.Password == "" && c.SSLMode == "" 215 } 216 217 // Resolve applies any external data sources to the config. 218 func (c *Config) Resolve(ctx context.Context) error { 219 return configutil.Resolve(ctx, 220 configutil.Set(&c.Engine, configutil.Env[string](EnvVarDBEngine), configutil.Lazy(&c.Engine), configutil.Const(DefaultEngine)), 221 configutil.Set(&c.DSN, configutil.Env[string](EnvVarDatabaseURL), configutil.Lazy(&c.DSN)), 222 configutil.Set(&c.Host, configutil.Env[string](EnvVarDBHost), configutil.Lazy(&c.Host), configutil.Const(DefaultHost)), 223 configutil.Set(&c.Port, configutil.Env[string](EnvVarDBPort), configutil.Lazy(&c.Port), configutil.Const(DefaultPort)), 224 configutil.Set(&c.Database, configutil.Env[string](EnvVarDBName), configutil.Lazy(&c.Database), configutil.Const(DefaultDatabase)), 225 configutil.Set(&c.Schema, configutil.Env[string](EnvVarDBSchema), configutil.Lazy(&c.Schema)), 226 configutil.Set(&c.ApplicationName, configutil.Env[string](EnvVarDBApplicationName), configutil.Const(c.ApplicationName)), 227 configutil.Set(&c.Username, configutil.Env[string](EnvVarDBUser), configutil.Lazy(&c.Username), configutil.Env[string]("USER")), 228 configutil.Set(&c.Password, configutil.Env[string](EnvVarDBPassword), configutil.Lazy(&c.Password)), 229 configutil.Set(&c.ConnectTimeout, configutil.Env[time.Duration](EnvVarDBConnectTimeout), configutil.Lazy(&c.ConnectTimeout), configutil.Const(DefaultConnectTimeout)), 230 configutil.Set(&c.LockTimeout, configutil.Env[time.Duration](EnvVarDBLockTimeout), configutil.Lazy(&c.LockTimeout)), 231 configutil.Set(&c.StatementTimeout, configutil.Env[time.Duration](EnvVarDBStatementTimeout), configutil.Lazy(&c.StatementTimeout)), 232 configutil.Set(&c.SSLMode, configutil.Env[string](EnvVarDBSSLMode), configutil.Lazy(&c.SSLMode)), 233 configutil.Set(&c.IdleConnections, configutil.Env[int](EnvVarDBIdleConnections), configutil.Lazy(&c.IdleConnections), configutil.Const(DefaultIdleConnections)), 234 configutil.Set(&c.MaxConnections, configutil.Env[int](EnvVarDBMaxConnections), configutil.Lazy(&c.MaxConnections), configutil.Const(DefaultMaxConnections)), 235 configutil.Set(&c.MaxLifetime, configutil.Env[time.Duration](EnvVarDBMaxLifetime), configutil.Lazy(&c.MaxLifetime), configutil.Const(DefaultMaxLifetime)), 236 configutil.Set(&c.MaxIdleTime, configutil.Env[time.Duration](EnvVarDBMaxIdleTime), configutil.Lazy(&c.MaxIdleTime), configutil.Const(DefaultMaxIdleTime)), 237 configutil.Set(&c.BufferPoolSize, configutil.Env[int](EnvVarDBBufferPoolSize), configutil.Lazy(&c.BufferPoolSize), configutil.Const(DefaultBufferPoolSize)), 238 configutil.Set(&c.Dialect, configutil.Env[string](EnvVarDBDialect), configutil.Lazy(&c.Dialect), configutil.Const(string(DialectPostgres))), 239 ) 240 } 241 242 // CreateDSN creates a postgres connection string from the config. 243 func (c Config) CreateDSN() string { 244 if c.DSN != "" { 245 return c.DSN 246 } 247 248 host := c.Host 249 if c.Port != "" { 250 host = host + ":" + c.Port 251 } 252 253 dsn := &url.URL{ 254 Scheme: "postgres", 255 Host: host, 256 Path: c.Database, 257 } 258 259 if len(c.Username) > 0 { 260 if len(c.Password) > 0 { 261 dsn.User = url.UserPassword(c.Username, c.Password) 262 } else { 263 dsn.User = url.User(c.Username) 264 } 265 } 266 267 queryArgs := url.Values{} 268 if len(c.SSLMode) > 0 { 269 queryArgs.Add("sslmode", c.SSLMode) 270 } 271 if c.ConnectTimeout > 0 { 272 setTimeoutSeconds(queryArgs, "connect_timeout", c.ConnectTimeout) 273 } 274 if c.LockTimeout > 0 { 275 setTimeoutMilliseconds(queryArgs, "lock_timeout", c.LockTimeout) 276 } 277 if c.StatementTimeout > 0 { 278 setTimeoutMilliseconds(queryArgs, "statement_timeout", c.StatementTimeout) 279 } 280 if c.Schema != "" { 281 queryArgs.Add("search_path", c.Schema) 282 } 283 if c.ApplicationName != "" { 284 queryArgs.Add("application_name", c.ApplicationName) 285 } 286 287 dsn.RawQuery = queryArgs.Encode() 288 return dsn.String() 289 } 290 291 // CreateLoggingDSN creates a postgres connection string from the config suitable for logging. 292 // It will not include the password. 293 func (c Config) CreateLoggingDSN() string { 294 // NOTE: If we're provided a DSN, we _really_ want to have CreateDSN get 295 // skipped because it'll just dump the DSN raw. 296 if c.DSN != "" { 297 u, _ := url.Parse(c.DSN) 298 u.User = url.User(u.User.Username()) 299 return u.String() 300 } 301 // NOTE: Since `c` is a value receiver, we can modify it without 302 // mutating the actual value. 303 c.Password = "" 304 return c.CreateDSN() 305 } 306 307 // Validate validates that user-provided values are valid, e.g. that timeouts 308 // can be exactly rounded into a multiple of a given base value. 309 func (c Config) Validate() error { 310 if c.ConnectTimeout.Round(time.Second) != c.ConnectTimeout { 311 return fmt.Errorf("invalid configuration value; connect_timeout=%s; %w", c.ConnectTimeout, ErrDurationConversion) 312 } 313 if c.LockTimeout.Round(time.Millisecond) != c.LockTimeout { 314 return fmt.Errorf("invalid configuration value; lock_timeout=%s; %w", c.LockTimeout, ErrDurationConversion) 315 } 316 if c.StatementTimeout.Round(time.Millisecond) != c.StatementTimeout { 317 return fmt.Errorf("invalid configuration value; statement_timeout=%s; %w", c.StatementTimeout, ErrDurationConversion) 318 } 319 320 return nil 321 } 322 323 // ValidateProduction validates production configuration for the config. 324 func (c Config) ValidateProduction() error { 325 if !(len(c.SSLMode) == 0 || 326 strings.EqualFold(c.SSLMode, SSLModeRequire) || 327 strings.EqualFold(c.SSLMode, SSLModeVerifyCA) || 328 strings.EqualFold(c.SSLMode, SSLModeVerifyFull)) { 329 return ErrUnsafeSSLMode 330 } 331 if len(c.Username) == 0 { 332 return ErrUsernameUnset 333 } 334 if len(c.Password) == 0 { 335 return ErrPasswordUnset 336 } 337 return c.Validate() 338 } 339 340 // setTimeoutMilliseconds sets a timeout value in connection string query parameters. 341 // 342 // Valid units for this parameter in PostgresSQL are "ms", "s", "min", "h" 343 // and "d" and the value should be between 0 and 2147483647ms. We explicitly 344 // cast to milliseconds but leave validation on the value to PostgreSQL. 345 // 346 // See: 347 // - https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-LOCK-TIMEOUT 348 // - https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STATEMENT-TIMEOUT 349 func setTimeoutMilliseconds(q url.Values, name string, d time.Duration) { 350 ms := d.Round(time.Millisecond) / time.Millisecond 351 q.Add(name, fmt.Sprintf("%dms", ms)) 352 } 353 354 // setTimeoutSeconds sets a timeout value in connection string query parameters. 355 // 356 // This timeout is expected to be an exact number of seconds (as an integer) 357 // so we convert `d` to an integer first and set the value as a query parameter 358 // without units. 359 // 360 // See: 361 // - https://www.postgresql.org/docs/10/libpq-connect.html#LIBPQ-CONNECT-CONNECT-TIMEOUT 362 func setTimeoutSeconds(q url.Values, name string, d time.Duration) { 363 s := d.Round(time.Second) / time.Second 364 q.Add(name, fmt.Sprintf("%d", s)) 365 }