950 lines
30 KiB
Go
950 lines
30 KiB
Go
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
|
|
}
|