package pq import ( "context" "database/sql/driver" "fmt" "math/rand" "net" "net/netip" neturl "net/url" "os" "path/filepath" "reflect" "slices" "sort" "strconv" "strings" "time" "unicode" "github.com/lib/pq/internal/pqutil" ) type ( // SSLMode is a sslmode setting. SSLMode string // SSLNegotiation is a sslnegotiation setting. SSLNegotiation string // TargetSessionAttrs is a target_session_attrs setting. TargetSessionAttrs string // LoadBalanceHosts is a load_balance_hosts setting. LoadBalanceHosts string ) // Values for [SSLMode] that pq supports. const ( // disable: No SSL SSLModeDisable = SSLMode("disable") // require: require SSL, but skip verification. SSLModeRequire = SSLMode("require") // verify-ca: require SSL and verify that the certificate was signed by a // trusted CA. SSLModeVerifyCA = SSLMode("verify-ca") // verify-full: require SSK and verify that the certificate was signed by a // trusted CA and the server host name matches the one in the certificate. SSLModeVerifyFull = SSLMode("verify-full") ) var sslModes = []SSLMode{SSLModeDisable, SSLModeRequire, SSLModeVerifyFull, SSLModeVerifyCA} // Values for [SSLNegotiation] that pq supports. const ( // Negotiate whether SSL should be used. This is the default. SSLNegotiationPostgres = SSLNegotiation("postgres") // Always use SSL, don't try to negotiate. SSLNegotiationDirect = SSLNegotiation("direct") ) var sslNegotiations = []SSLNegotiation{SSLNegotiationPostgres, SSLNegotiationDirect} // Values for [TargetSessionAttrs] that pq supports. const ( // Any successful connection is acceptable. This is the default. TargetSessionAttrsAny = TargetSessionAttrs("any") // Session must accept read-write transactions by default: the server must // not be in hot standby mode and default_transaction_read_only must be // off. TargetSessionAttrsReadWrite = TargetSessionAttrs("read-write") // Session must not accept read-write transactions by default. TargetSessionAttrsReadOnly = TargetSessionAttrs("read-only") // Server must not be in hot standby mode. TargetSessionAttrsPrimary = TargetSessionAttrs("primary") // Server must be in hot standby mode. TargetSessionAttrsStandby = TargetSessionAttrs("standby") // First try to find a standby server, but if none of the listed hosts is a // standby server, try again in any mode. TargetSessionAttrsPreferStandby = TargetSessionAttrs("prefer-standby") ) var targetSessionAttrs = []TargetSessionAttrs{TargetSessionAttrsAny, TargetSessionAttrsReadWrite, TargetSessionAttrsReadOnly, TargetSessionAttrsPrimary, TargetSessionAttrsStandby, TargetSessionAttrsPreferStandby} // Values for [LoadBalanceHosts] that pq supports. const ( // Don't load balance; try hosts in the order in which they're provided. // This is the default. LoadBalanceHostsDisable = LoadBalanceHosts("disable") // Hosts are tried in random order to balance connections across multiple // PostgreSQL servers. // // When using this value it's recommended to also configure a reasonable // value for connect_timeout. Because then, if one of the nodes that are // used for load balancing is not responding, a new node will be tried. LoadBalanceHostsRandom = LoadBalanceHosts("random") ) var loadBalanceHosts = []LoadBalanceHosts{LoadBalanceHostsDisable, LoadBalanceHostsRandom} // Connector represents a fixed configuration for the pq driver with a given // dsn. Connector satisfies the [database/sql/driver.Connector] interface and // can be used to create any number of DB Conn's via [sql.OpenDB]. type Connector struct { cfg Config dialer Dialer } // NewConnector returns a connector for the pq driver in a fixed configuration // with the given dsn. The returned connector can be used to create any number // of equivalent Conn's. The returned connector is intended to be used with // [sql.OpenDB]. func NewConnector(dsn string) (*Connector, error) { cfg, err := NewConfig(dsn) if err != nil { return nil, err } return NewConnectorConfig(cfg) } // NewConnectorConfig returns a connector for the pq driver in a fixed // configuration with the given [Config]. The returned connector can be used to // create any number of equivalent Conn's. The returned connector is intended to // be used with [sql.OpenDB]. func NewConnectorConfig(cfg Config) (*Connector, error) { return &Connector{cfg: cfg, dialer: defaultDialer{}}, nil } // Connect returns a connection to the database using the fixed configuration of // this Connector. Context is not used. func (c *Connector) Connect(ctx context.Context) (driver.Conn, error) { return c.open(ctx) } // Dialer allows change the dialer used to open connections. func (c *Connector) Dialer(dialer Dialer) { c.dialer = dialer } // Driver returns the underlying driver of this Connector. func (c *Connector) Driver() driver.Driver { return &Driver{} } // Config holds options pq supports when connecting to PostgreSQL. // // The postgres struct tag is used for the value from the DSN (e.g. // "dbname=abc"), and the env struct tag is used for the environment variable // (e.g. "PGDATABASE=abc") type Config struct { // The host to connect to. Absolute paths and values that start with @ are // for unix domain sockets. Defaults to localhost. // // A comma-separated list of host names is also accepted, in which case each // host name in the list is tried in order or randomly if load_balance_hosts // is set. An empty item selects the default of localhost. The // target_session_attrs option controls properties the host must have to be // considered acceptable. Host string `postgres:"host" env:"PGHOST"` // IPv4 or IPv6 address to connect to. Using hostaddr allows the application // to avoid a host name lookup, which might be important in applications // with time constraints. A hostname is required for sslmode=verify-full and // the GSSAPI or SSPI authentication methods. // // The following rules are used: // // - If host is given without hostaddr, a host name lookup occurs. // // - If hostaddr is given without host, the value for hostaddr gives the // server network address. The connection attempt will fail if the // authentication method requires a host name. // // - If both host and hostaddr are given, the value for hostaddr gives the // server network address. The value for host is ignored unless the // authentication method requires it, in which case it will be used as the // host name. // // A comma-separated list of hostaddr values is also accepted, in which case // each host in the list is tried in order or randonly if load_balance_hosts // is set. An empty item causes the corresponding host name to be used, or // the default host name if that is empty as well. The target_session_attrs // option controls properties the host must have to be considered // acceptable. Hostaddr netip.Addr `postgres:"hostaddr" env:"PGHOSTADDR"` // The port to connect to. Defaults to 5432. // // If multiple hosts were given in the host or hostaddr parameters, this // parameter may specify a comma-separated list of ports of the same length // as the host list, or it may specify a single port number to be used for // all hosts. An empty string, or an empty item in a comma-separated list, // specifies the default of 5432. Port uint16 `postgres:"port" env:"PGPORT"` // The name of the database to connect to. Database string `postgres:"dbname" env:"PGDATABASE"` // The user to sign in as. Defaults to the current user. User string `postgres:"user" env:"PGUSER"` // The user's password. Password string `postgres:"password" env:"PGPASSWORD"` // Path to [pgpass] file to store passwords; overrides Password. // // [pgpass]: http://www.postgresql.org/docs/current/static/libpq-pgpass.html Passfile string `postgres:"passfile" env:"PGPASSFILE"` // Commandline options to send to the server at connection start. Options string `postgres:"options" env:"PGOPTIONS"` // Application name, displayed in pg_stat_activity and log entries. ApplicationName string `postgres:"application_name" env:"PGAPPNAME"` // Used if application_name is not given. Specifying a fallback name is // useful in generic utility programs that wish to set a default application // name but allow it to be overridden by the user. FallbackApplicationName string `postgres:"fallback_application_name" env:"-"` // Whether to use SSL. Defaults to "require" (different from libpq's default // of "prefer"). // // [RegisterTLSConfig] can be used to registers a custom [tls.Config], which // can be used by setting sslmode=pqgo-«key» in the connection string. SSLMode SSLMode `postgres:"sslmode" env:"PGSSLMODE"` // When set to "direct" it will use SSL without negotiation (PostgreSQL ≥17 only). SSLNegotiation SSLNegotiation `postgres:"sslnegotiation" env:"PGSSLNEGOTIATION"` // Cert file location. The file must contain PEM encoded data. SSLCert string `postgres:"sslcert" env:"PGSSLCERT"` // Key file location. The file must contain PEM encoded data. SSLKey string `postgres:"sslkey" env:"PGSSLKEY"` // The location of the root certificate file. The file must contain PEM encoded data. SSLRootCert string `postgres:"sslrootcert" env:"PGSSLROOTCERT"` // By default SNI is on, any value which is not starting with "1" disables // SNI. SSLSNI bool `postgres:"sslsni" env:"PGSSLSNI"` // Interpert sslcert and sslkey as PEM encoded data, rather than a path to a // PEM file. This is a pq extension, not supported in libpq. SSLInline bool `postgres:"sslinline" env:"-"` // GSS (Kerberos) service name when constructing the SPN (default is // postgres). This will be combined with the host to form the full SPN: // krbsrvname/host. KrbSrvname string `postgres:"krbsrvname" env:"PGKRBSRVNAME"` // GSS (Kerberos) SPN. This takes priority over krbsrvname if present. This // is a pq extension, not supported in libpq. KrbSpn string `postgres:"krbspn" env:"-"` // Maximum time to wait while connecting, in seconds. Zero, negative, or not // specified means wait indefinitely ConnectTimeout time.Duration `postgres:"connect_timeout" env:"PGCONNECT_TIMEOUT"` // Whether to always send []byte parameters over as binary. Enables single // round-trip mode for non-prepared Query calls. This is a pq extension, not // supported in libpq. BinaryParameters bool `postgres:"binary_parameters" env:"-"` // This connection should never use the binary format when receiving query // results from prepared statements. Only provided for debugging. This is a // pq extension, not supported in libpq. DisablePreparedBinaryResult bool `postgres:"disable_prepared_binary_result" env:"-"` // Client encoding; pq only supports UTF8 and this must be blank or "UTF8". ClientEncoding string `postgres:"client_encoding" env:"PGCLIENTENCODING"` // Date/time representation to use; pq only supports "ISO, MDY" and this // must be blank or "ISO, MDY". Datestyle string `postgres:"datestyle" env:"PGDATESTYLE"` // Default time zone. TZ string `postgres:"tz" env:"PGTZ"` // Default mode for the genetic query optimizer. Geqo string `postgres:"geqo" env:"PGGEQO"` // Determine whether the session must have certain properties to be // acceptable. It's typically used in combination with multiple host names // to select the first acceptable alternative among several hosts. TargetSessionAttrs TargetSessionAttrs `postgres:"target_session_attrs" env:"PGTARGETSESSIONATTRS"` // Controls the order in which the client tries to connect to the available // hosts. Once a connection attempt is successful no other hosts will be // tried. This parameter is typically used in combination with multiple host // names. // // This parameter can be used in combination with target_session_attrs to, // for example, load balance over standby servers only. Once successfully // connected, subsequent queries on the returned connection will all be sent // to the same server. LoadBalanceHosts LoadBalanceHosts `postgres:"load_balance_hosts" env:"PGLOADBALANCEHOSTS"` // Runtime parameters: any unrecognized parameter in the DSN will be added // to this and sent to PostgreSQL during startup. Runtime map[string]string `postgres:"-" env:"-"` // Multi contains additional connection details. The first value is // available in [Config.Host], [Config.Hostaddr], and [Config.Port], and // additional ones (if any) are available here. Multi []ConfigMultihost // Record which parameters were given, so we can distinguish between an // empty string "not given at all". // // The alternative is to use pointers or sql.Null[..], but that's more // awkward to use. set []string `env:"set"` multiHost []string multiHostaddr []netip.Addr multiPort []uint16 } // ConfigMultihost specifies an additional server to try to connect to. type ConfigMultihost struct { Host string Hostaddr netip.Addr Port uint16 } // NewConfig creates a new [Config] from the current environment and given DSN. // // A subset of the connection parameters supported by PostgreSQL are supported // by pq; see the [Config] struct fields for supported parameters. pq also lets // you specify any [run-time parameter] (such as search_path or work_mem) // directly in the connection string. This is different from libpq, which does // not allow run-time parameters in the connection string, instead requiring you // to supply them in the options parameter. // // # key=value connection strings // // For key=value strings, use single quotes for values that contain whitespace // or empty values. A backslash will escape the next character: // // "user=pqgo password='with spaces'" // "user=''" // "user=space\ man password='it\'s valid'" // // # URL connection strings // // pq supports URL-style postgres:// or postgresql:// connection strings in the // form: // // postgres[ql]://[user[:pwd]@][net-location][:port][/dbname][?param1=value1&...] // // Go's [net/url.Parse] is more strict than PostgreSQL's URL parser and will // (correctly) reject %2F in the host part. This means that unix-socket URLs: // // postgres://[user[:pwd]@][unix-socket][:port[/dbname]][?param1=value1&...] // postgres://%2Ftmp%2Fpostgres/db // // will not work. You will need to use "host=/tmp/postgres dbname=db". // // Similarly, multiple ports also won't work, but ?port= will: // // postgres://host1,host2:5432,6543/dbname Doesn't work // postgres://host1,host2/dbname?port=5432,6543 Works // // # Environment // // Most [PostgreSQL environment variables] are supported by pq. Environment // variables have a lower precedence than explicitly provided connection // parameters. pq will return an error if environment variables it does not // support are set. Environment variables have a lower precedence than // explicitly provided connection parameters. // // [run-time parameter]: http://www.postgresql.org/docs/current/static/runtime-config.html // [PostgreSQL environment variables]: http://www.postgresql.org/docs/current/static/libpq-envars.html func NewConfig(dsn string) (Config, error) { return newConfig(dsn, os.Environ()) } // Clone returns a copy of the [Config]. func (cfg Config) Clone() Config { rt := make(map[string]string) for k, v := range cfg.Runtime { rt[k] = v } c := cfg c.Runtime = rt c.set = append([]string{}, cfg.set...) return c } // hosts returns a slice of copies of this config, one for each host. func (cfg Config) hosts() []Config { cfgs := make([]Config, 1, len(cfg.Multi)+1) cfgs[0] = cfg.Clone() for _, m := range cfg.Multi { c := cfg.Clone() c.Host, c.Hostaddr, c.Port = m.Host, m.Hostaddr, m.Port cfgs = append(cfgs, c) } if cfg.LoadBalanceHosts == LoadBalanceHostsRandom { rand.Shuffle(len(cfgs), func(i, j int) { cfgs[i], cfgs[j] = cfgs[j], cfgs[i] }) } return cfgs } func newConfig(dsn string, env []string) (Config, error) { cfg := Config{Host: "localhost", Port: 5432, SSLSNI: true} if err := cfg.fromEnv(env); err != nil { return Config{}, err } if err := cfg.fromDSN(dsn); err != nil { return Config{}, err } // Need to have exactly the same number of host and hostaddr, or only specify one. if cfg.isset("host") && cfg.Host != "" && cfg.Hostaddr != (netip.Addr{}) && len(cfg.multiHost) != len(cfg.multiHostaddr) { return Config{}, fmt.Errorf("pq: could not match %d host names to %d hostaddr values", len(cfg.multiHost)+1, len(cfg.multiHostaddr)+1) } // Need one port that applies to all or exactly the same number of ports as hosts. l, ll := max(len(cfg.multiHost), len(cfg.multiHostaddr)), len(cfg.multiPort) if l > 0 && ll > 0 && l != ll { return Config{}, fmt.Errorf("pq: could not match %d port numbers to %d hosts", ll+1, l+1) } // Populate Multi if len(cfg.multiHostaddr) > len(cfg.multiHost) { cfg.multiHost = make([]string, len(cfg.multiHostaddr)) } for i, h := range cfg.multiHost { p := cfg.Port if len(cfg.multiPort) > 0 { p = cfg.multiPort[i] } var addr netip.Addr if len(cfg.multiHostaddr) > 0 { addr = cfg.multiHostaddr[i] } cfg.Multi = append(cfg.Multi, ConfigMultihost{ Host: h, Port: p, Hostaddr: addr, }) } // Use the "fallback" application name if necessary if cfg.isset("fallback_application_name") && !cfg.isset("application_name") { cfg.ApplicationName = cfg.FallbackApplicationName } // We can't work with any client_encoding other than UTF-8 currently. // However, we have historically allowed the user to set it to UTF-8 // explicitly, and there's no reason to break such programs, so allow that. // Note that the "options" setting could also set client_encoding, but // parsing its value is not worth it. Instead, we always explicitly send // client_encoding as a separate run-time parameter, which should override // anything set in options. if cfg.isset("client_encoding") && !isUTF8(cfg.ClientEncoding) { return Config{}, fmt.Errorf(`pq: unsupported client_encoding %q: must be absent or "UTF8"`, cfg.ClientEncoding) } // DateStyle needs a similar treatment. if cfg.isset("datestyle") && cfg.Datestyle != "ISO, MDY" { return Config{}, fmt.Errorf(`pq: unsupported datestyle %q: must be absent or "ISO, MDY"`, cfg.Datestyle) } cfg.ClientEncoding, cfg.Datestyle = "UTF8", "ISO, MDY" // Set default user if not explicitly provided. if !cfg.isset("user") { u, err := pqutil.User() if err != nil { return Config{}, err } cfg.User = u } // SSL is not necessary or supported over UNIX domain sockets. if nw, _ := cfg.network(); nw == "unix" { cfg.SSLMode = SSLModeDisable } return cfg, nil } func (cfg Config) network() (string, string) { if cfg.Hostaddr != (netip.Addr{}) { return "tcp", net.JoinHostPort(cfg.Hostaddr.String(), strconv.Itoa(int(cfg.Port))) } // UNIX domain sockets are either represented by an (absolute) file system // path or they live in the abstract name space (starting with an @). if filepath.IsAbs(cfg.Host) || strings.HasPrefix(cfg.Host, "@") { sockPath := filepath.Join(cfg.Host, ".s.PGSQL."+strconv.Itoa(int(cfg.Port))) return "unix", sockPath } return "tcp", net.JoinHostPort(cfg.Host, strconv.Itoa(int(cfg.Port))) } func (cfg *Config) fromEnv(env []string) error { e := make(map[string]string) for _, v := range env { k, v, ok := strings.Cut(v, "=") if !ok { continue } switch k { case "PGREQUIREAUTH", "PGCHANNELBINDING", "PGSERVICE", "PGSERVICEFILE", "PGREALM", "PGSSLCERTMODE", "PGSSLCOMPRESSION", "PGREQUIRESSL", "PGSSLCRL", "PGREQUIREPEER", "PGSYSCONFDIR", "PGLOCALEDIR", "PGSSLCRLDIR", "PGSSLMINPROTOCOLVERSION", "PGSSLMAXPROTOCOLVERSION", "PGGSSENCMODE", "PGGSSDELEGATION", "PGMINPROTOCOLVERSION", "PGMAXPROTOCOLVERSION", "PGGSSLIB": return fmt.Errorf("pq: environment variable $%s is not supported", k) case "PGKRBSRVNAME": if newGss == nil { return fmt.Errorf("pq: environment variable $%s is not supported as Kerberos is not enabled", k) } } e[k] = v } return cfg.setFromTag(e, "env") } // parseOpts parses the options from name and adds them to the values. // // The parsing code is based on conninfo_parse from libpq's fe-connect.c func (cfg *Config) fromDSN(dsn string) error { if strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") { var err error dsn, err = convertURL(dsn) if err != nil { return err } } var ( opt = make(map[string]string) s = []rune(dsn) i int next = func() (rune, bool) { if i >= len(s) { return 0, false } r := s[i] i++ return r, true } skipSpaces = func() (rune, bool) { r, ok := next() for unicode.IsSpace(r) && ok { r, ok = next() } return r, ok } ) for { var ( keyRunes, valRunes []rune r rune ok bool ) if r, ok = skipSpaces(); !ok { break } // Scan the key for !unicode.IsSpace(r) && r != '=' { keyRunes = append(keyRunes, r) if r, ok = next(); !ok { break } } // Skip any whitespace if we're not at the = yet if r != '=' { r, ok = skipSpaces() } // The current character should be = if r != '=' || !ok { return fmt.Errorf(`missing "=" after %q in connection info string"`, string(keyRunes)) } // Skip any whitespace after the = if r, ok = skipSpaces(); !ok { // If we reach the end here, the last value is just an empty string as per libpq. opt[string(keyRunes)] = "" break } if r != '\'' { for !unicode.IsSpace(r) { if r == '\\' { if r, ok = next(); !ok { return fmt.Errorf(`missing character after backslash`) } } valRunes = append(valRunes, r) if r, ok = next(); !ok { break } } } else { quote: for { if r, ok = next(); !ok { return fmt.Errorf(`unterminated quoted string literal in connection string`) } switch r { case '\'': break quote case '\\': r, _ = next() fallthrough default: valRunes = append(valRunes, r) } } } opt[string(keyRunes)] = string(valRunes) } return cfg.setFromTag(opt, "postgres") } func (cfg *Config) setFromTag(o map[string]string, tag string) error { f := "pq: wrong value for %q: " if tag == "env" { f = "pq: wrong value for $%s: " } var ( types = reflect.TypeOf(cfg).Elem() values = reflect.ValueOf(cfg).Elem() ) for i := 0; i < types.NumField(); i++ { var ( rt = types.Field(i) rv = values.Field(i) k = rt.Tag.Get(tag) connectTimeout = (tag == "postgres" && k == "connect_timeout") || (tag == "env" && k == "PGCONNECT_TIMEOUT") host = (tag == "postgres" && k == "host") || (tag == "env" && k == "PGHOST") hostaddr = (tag == "postgres" && k == "hostaddr") || (tag == "env" && k == "PGHOSTADDR") port = (tag == "postgres" && k == "port") || (tag == "env" && k == "PGPORT") sslmode = (tag == "postgres" && k == "sslmode") || (tag == "env" && k == "PGSSLMODE") sslnegotiation = (tag == "postgres" && k == "sslnegotiation") || (tag == "env" && k == "PGSSLNEGOTIATION") targetsessionattrs = (tag == "postgres" && k == "target_session_attrs") || (tag == "env" && k == "PGTARGETSESSIONATTRS") loadbalancehosts = (tag == "postgres" && k == "load_balance_hosts") || (tag == "env" && k == "PGLOADBALANCEHOSTS") ) if k == "" || k == "-" { continue } v, ok := o[k] delete(o, k) if ok { if t, ok := rt.Tag.Lookup("postgres"); ok && t != "" && t != "-" { cfg.set = append(cfg.set, t) } switch rt.Type.Kind() { default: return fmt.Errorf("don't know how to set %s: unknown type %s", rt.Name, rt.Type.Kind()) case reflect.Struct: if rt.Type == reflect.TypeOf(netip.Addr{}) { if hostaddr { vv := strings.Split(v, ",") v = vv[0] for _, vvv := range vv[1:] { if vvv == "" { cfg.multiHostaddr = append(cfg.multiHostaddr, netip.Addr{}) } else { ip, err := netip.ParseAddr(vvv) if err != nil { return fmt.Errorf(f+"%w", k, err) } cfg.multiHostaddr = append(cfg.multiHostaddr, ip) } } } ip, err := netip.ParseAddr(v) if err != nil { return fmt.Errorf(f+"%w", k, err) } rv.Set(reflect.ValueOf(ip)) } else { return fmt.Errorf("don't know how to set %s: unknown type %s", rt.Name, rt.Type) } case reflect.String: if sslmode && !slices.Contains(sslModes, SSLMode(v)) && !(strings.HasPrefix(v, "pqgo-") && hasTLSConfig(v[5:])) { return fmt.Errorf(f+`%q is not supported; supported values are %s`, k, v, pqutil.Join(sslModes)) } if sslnegotiation && !slices.Contains(sslNegotiations, SSLNegotiation(v)) { return fmt.Errorf(f+`%q is not supported; supported values are %s`, k, v, pqutil.Join(sslNegotiations)) } if targetsessionattrs && !slices.Contains(targetSessionAttrs, TargetSessionAttrs(v)) { return fmt.Errorf(f+`%q is not supported; supported values are %s`, k, v, pqutil.Join(targetSessionAttrs)) } if loadbalancehosts && !slices.Contains(loadBalanceHosts, LoadBalanceHosts(v)) { return fmt.Errorf(f+`%q is not supported; supported values are %s`, k, v, pqutil.Join(loadBalanceHosts)) } if host { vv := strings.Split(v, ",") v = vv[0] for i, vvv := range vv[1:] { if vvv == "" { vv[i+1] = "localhost" } } cfg.multiHost = append(cfg.multiHost, vv[1:]...) } rv.SetString(v) case reflect.Int64: n, err := strconv.ParseInt(v, 10, 64) if err != nil { return fmt.Errorf(f+"%w", k, err) } if connectTimeout { n = int64(time.Duration(n) * time.Second) } rv.SetInt(n) case reflect.Uint16: if port { vv := strings.Split(v, ",") v = vv[0] for _, vvv := range vv[1:] { if vvv == "" { vvv = "5432" } n, err := strconv.ParseUint(vvv, 10, 16) if err != nil { return fmt.Errorf(f+"%w", k, err) } cfg.multiPort = append(cfg.multiPort, uint16(n)) } } n, err := strconv.ParseUint(v, 10, 16) if err != nil { return fmt.Errorf(f+"%w", k, err) } rv.SetUint(n) case reflect.Bool: b, err := pqutil.ParseBool(v) if err != nil { return fmt.Errorf(f+"%w", k, err) } rv.SetBool(b) } } } // Set run-time; we delete map keys as they're set in the struct. if tag == "postgres" { // Make sure database= sets dbname=, as that previously worked (kind of // by accident). // TODO(v2): remove if d, ok := o["database"]; ok { cfg.Database = d delete(o, "database") } cfg.Runtime = o } return nil } func (cfg Config) isset(name string) bool { return slices.Contains(cfg.set, name) } // Convert to a map; used only in tests. func (cfg Config) tomap() map[string]string { var ( o = make(map[string]string) values = reflect.ValueOf(cfg) types = reflect.TypeOf(cfg) ) for i := 0; i < types.NumField(); i++ { var ( rt = types.Field(i) rv = values.Field(i) k = rt.Tag.Get("postgres") ) if k == "" || k == "-" { continue } if !rv.IsZero() || slices.Contains(cfg.set, k) { switch rt.Type.Kind() { default: if s, ok := rv.Interface().(fmt.Stringer); ok { o[k] = s.String() } else { o[k] = rv.String() } case reflect.Uint16: n := rv.Uint() o[k] = strconv.FormatUint(n, 10) case reflect.Int64: n := rv.Int() if k == "connect_timeout" { n = int64(time.Duration(n) / time.Second) } o[k] = strconv.FormatInt(n, 10) case reflect.Bool: if rv.Bool() { o[k] = "yes" } else { o[k] = "no" } } } } for k, v := range cfg.Runtime { o[k] = v } return o } // Create DSN for this config; used only in tests. func (cfg Config) string() string { var ( m = cfg.tomap() keys = make([]string, 0, len(m)) ) for k := range m { switch k { case "datestyle", "client_encoding": continue case "host", "port", "user", "sslsni": if !cfg.isset(k) { continue } } if k == "host" && len(cfg.multiHost) > 0 { m[k] += "," + strings.Join(cfg.multiHost, ",") } if k == "hostaddr" && len(cfg.multiHostaddr) > 0 { for _, ha := range cfg.multiHostaddr { m[k] += "," if ha != (netip.Addr{}) { m[k] += ha.String() } } } if k == "port" && len(cfg.multiPort) > 0 { for _, p := range cfg.multiPort { m[k] += "," + strconv.Itoa(int(p)) } } keys = append(keys, k) } sort.Strings(keys) var b strings.Builder for i, k := range keys { if i > 0 { b.WriteByte(' ') } b.WriteString(k) b.WriteByte('=') var ( v = m[k] nv = make([]rune, 0, len(v)+2) quote = v == "" ) for _, c := range v { if c == ' ' { quote = true } if c == '\'' { nv = append(nv, '\\') } nv = append(nv, c) } if quote { b.WriteByte('\'') } b.WriteString(string(nv)) if quote { b.WriteByte('\'') } } return b.String() } // Recognize all sorts of silly things as "UTF-8", like Postgres does func isUTF8(name string) bool { s := strings.Map(func(c rune) rune { if 'A' <= c && c <= 'Z' { return c + ('a' - 'A') } if 'a' <= c && c <= 'z' || '0' <= c && c <= '9' { return c } return -1 // discard }, name) return s == "utf8" || s == "unicode" } func convertURL(url string) (string, error) { u, err := neturl.Parse(url) if err != nil { return "", err } if u.Scheme != "postgres" && u.Scheme != "postgresql" { return "", fmt.Errorf("invalid connection protocol: %s", u.Scheme) } var kvs []string escaper := strings.NewReplacer(`'`, `\'`, `\`, `\\`) accrue := func(k, v string) { if v != "" { kvs = append(kvs, k+"='"+escaper.Replace(v)+"'") } } if u.User != nil { pw, _ := u.User.Password() accrue("user", u.User.Username()) accrue("password", pw) } if host, port, err := net.SplitHostPort(u.Host); err != nil { accrue("host", u.Host) } else { accrue("host", host) accrue("port", port) } if u.Path != "" { accrue("dbname", u.Path[1:]) } q := u.Query() for k := range q { accrue(k, q.Get(k)) } sort.Strings(kvs) // Makes testing easier (not a performance concern) return strings.Join(kvs, " "), nil }