├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── auth_scram.go ├── benchmark_test.go ├── ci ├── script.bash └── setup_test.bash ├── config.go ├── config_test.go ├── defaults.go ├── defaults_windows.go ├── doc.go ├── errors.go ├── errors_test.go ├── export_test.go ├── frontend_test.go ├── go.mod ├── go.sum ├── helper_test.go ├── internal └── ctxwatch │ ├── context_watcher.go │ └── context_watcher_test.go ├── krb5.go ├── pgconn.go ├── pgconn_stress_test.go ├── pgconn_test.go └── stmtcache ├── lru.go ├── lru_test.go └── stmtcache.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | test: 12 | name: Test 13 | runs-on: ubuntu-20.04 14 | 15 | strategy: 16 | matrix: 17 | go-version: [1.17, 1.18] 18 | pg-version: [9.6, 10, 11, 12, 13, cockroachdb] 19 | include: 20 | - pg-version: 9.6 21 | pgx-test-conn-string: postgres://pgx_md5:secret@127.0.0.1/pgx_test 22 | pgx-test-unix-socket-conn-string: "host=/var/run/postgresql dbname=pgx_test" 23 | pgx-test-tcp-conn-string: postgres://pgx_md5:secret@127.0.0.1/pgx_test 24 | pgx-test-tls-conn-string: postgres://pgx_md5:secret@127.0.0.1/pgx_test?sslmode=require 25 | pgx-test-md5-password-conn-string: postgres://pgx_md5:secret@127.0.0.1/pgx_test 26 | pgx-test-plain-password-conn-string: postgres://pgx_pw:secret@127.0.0.1/pgx_test 27 | - pg-version: 10 28 | pgx-test-conn-string: postgres://pgx_md5:secret@127.0.0.1/pgx_test 29 | pgx-test-unix-socket-conn-string: "host=/var/run/postgresql dbname=pgx_test" 30 | pgx-test-tcp-conn-string: postgres://pgx_md5:secret@127.0.0.1/pgx_test 31 | pgx-test-tls-conn-string: postgres://pgx_md5:secret@127.0.0.1/pgx_test?sslmode=require 32 | pgx-test-md5-password-conn-string: postgres://pgx_md5:secret@127.0.0.1/pgx_test 33 | pgx-test-plain-password-conn-string: postgres://pgx_pw:secret@127.0.0.1/pgx_test 34 | - pg-version: 11 35 | pgx-test-conn-string: postgres://pgx_md5:secret@127.0.0.1/pgx_test 36 | pgx-test-unix-socket-conn-string: "host=/var/run/postgresql dbname=pgx_test" 37 | pgx-test-tcp-conn-string: postgres://pgx_md5:secret@127.0.0.1/pgx_test 38 | pgx-test-tls-conn-string: postgres://pgx_md5:secret@127.0.0.1/pgx_test?sslmode=require 39 | pgx-test-md5-password-conn-string: postgres://pgx_md5:secret@127.0.0.1/pgx_test 40 | pgx-test-plain-password-conn-string: postgres://pgx_pw:secret@127.0.0.1/pgx_test 41 | - pg-version: 12 42 | pgx-test-conn-string: postgres://pgx_md5:secret@127.0.0.1/pgx_test 43 | pgx-test-unix-socket-conn-string: "host=/var/run/postgresql dbname=pgx_test" 44 | pgx-test-tcp-conn-string: postgres://pgx_md5:secret@127.0.0.1/pgx_test 45 | pgx-test-tls-conn-string: postgres://pgx_md5:secret@127.0.0.1/pgx_test?sslmode=require 46 | pgx-test-md5-password-conn-string: postgres://pgx_md5:secret@127.0.0.1/pgx_test 47 | pgx-test-plain-password-conn-string: postgres://pgx_pw:secret@127.0.0.1/pgx_test 48 | - pg-version: 13 49 | pgx-test-conn-string: postgres://pgx_md5:secret@127.0.0.1/pgx_test 50 | pgx-test-unix-socket-conn-string: "host=/var/run/postgresql dbname=pgx_test" 51 | pgx-test-tcp-conn-string: postgres://pgx_md5:secret@127.0.0.1/pgx_test 52 | pgx-test-tls-conn-string: postgres://pgx_md5:secret@127.0.0.1/pgx_test?sslmode=require 53 | pgx-test-md5-password-conn-string: postgres://pgx_md5:secret@127.0.0.1/pgx_test 54 | pgx-test-plain-password-conn-string: postgres://pgx_pw:secret@127.0.0.1/pgx_test 55 | - pg-version: cockroachdb 56 | pgx-test-conn-string: "postgresql://root@127.0.0.1:26257/pgx_test?sslmode=disable&experimental_enable_temp_tables=on" 57 | 58 | steps: 59 | 60 | - name: Set up Go 1.x 61 | uses: actions/setup-go@v2 62 | with: 63 | go-version: ${{ matrix.go-version }} 64 | 65 | - name: Check out code into the Go module directory 66 | uses: actions/checkout@v2 67 | 68 | - name: Setup database server for testing 69 | run: ci/setup_test.bash 70 | env: 71 | PGVERSION: ${{ matrix.pg-version }} 72 | 73 | - name: Test 74 | run: go test -v -race ./... 75 | env: 76 | PGX_TEST_CONN_STRING: ${{ matrix.pgx-test-conn-string }} 77 | PGX_TEST_UNIX_SOCKET_CONN_STRING: ${{ matrix.pgx-test-unix-socket-conn-string }} 78 | PGX_TEST_TCP_CONN_STRING: ${{ matrix.pgx-test-tcp-conn-string }} 79 | PGX_TEST_TLS_CONN_STRING: ${{ matrix.pgx-test-tls-conn-string }} 80 | PGX_TEST_MD5_PASSWORD_CONN_STRING: ${{ matrix.pgx-test-md5-password-conn-string }} 81 | PGX_TEST_PLAIN_PASSWORD_CONN_STRING: ${{ matrix.pgx-test-plain-password-conn-string }} 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .envrc 2 | vendor/ 3 | .vscode 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.14.3 (March 4, 2024) 2 | 3 | * Update golang.org/x/crypto and golang.org/x/text 4 | 5 | # 1.14.2 (March 4, 2024) 6 | 7 | * Fix CVE-2024-27304. SQL injection can occur if an attacker can cause a single query or bind message to exceed 4 GB in 8 | size. An integer overflow in the calculated message size can cause the one large message to be sent as multiple messages 9 | under the attacker's control. 10 | 11 | # 1.14.1 (July 19, 2023) 12 | 13 | * Fix: Enable failover efforts when pg_hba.conf disallows non-ssl connections (Brandon Kauffman) 14 | * Fix: connect_timeout is not obeyed for sslmode=allow|prefer (smaher-edb) 15 | * Optimize redundant pgpass parsing in case password is explicitly set (Aleksandr Alekseev) 16 | 17 | # 1.14.0 (February 11, 2023) 18 | 19 | * Fix: each connection attempt to new node gets own timeout (Nathan Giardina) 20 | * Set SNI for SSL connections (Stas Kelvich) 21 | * Fix: CopyFrom I/O race (Tommy Reilly) 22 | * Minor dependency upgrades 23 | 24 | # 1.13.0 (August 6, 2022) 25 | 26 | * Add sslpassword support (Eric McCormack and yun.xu) 27 | * Add prefer-standby target_session_attrs support (sergey.bashilov) 28 | * Fix GSS ErrorResponse handling (Oliver Tan) 29 | 30 | # 1.12.1 (May 7, 2022) 31 | 32 | * Fix: setting krbspn and krbsrvname in connection string (sireax) 33 | * Add support for Unix sockets on Windows (Eno Compton) 34 | * Stop ignoring ErrorResponse during SCRAM auth (Rafi Shamim) 35 | 36 | # 1.12.0 (April 21, 2022) 37 | 38 | * Add pluggable GSSAPI support (Oliver Tan) 39 | * Fix: Consider any "0A000" error a possible cached plan changed error due to locale 40 | * Better match psql fallback behavior with multiple hosts 41 | 42 | # 1.11.0 (February 7, 2022) 43 | 44 | * Support port in ip from LookupFunc to override config (James Hartig) 45 | * Fix TLS connection timeout (Blake Embrey) 46 | * Add support for read-only, primary, standby, prefer-standby target_session_attributes (Oscar) 47 | * Fix connect when receiving NoticeResponse 48 | 49 | # 1.10.1 (November 20, 2021) 50 | 51 | * Close without waiting for response (Kei Kamikawa) 52 | * Save waiting for network round-trip in CopyFrom (Rueian) 53 | * Fix concurrency issue with ContextWatcher 54 | * LRU.Get always checks context for cancellation / expiration (Georges Varouchas) 55 | 56 | # 1.10.0 (July 24, 2021) 57 | 58 | * net.Timeout errors are no longer returned when a query is canceled via context. A wrapped context error is returned. 59 | 60 | # 1.9.0 (July 10, 2021) 61 | 62 | * pgconn.Timeout only is true for errors originating in pgconn (Michael Darr) 63 | * Add defaults for sslcert, sslkey, and sslrootcert (Joshua Brindle) 64 | * Solve issue with 'sslmode=verify-full' when there are multiple hosts (mgoddard) 65 | * Fix default host when parsing URL without host but with port 66 | * Allow dbname query parameter in URL conn string 67 | * Update underlying dependencies 68 | 69 | # 1.8.1 (March 25, 2021) 70 | 71 | * Better connection string sanitization (ip.novikov) 72 | * Use proper pgpass location on Windows (Moshe Katz) 73 | * Use errors instead of golang.org/x/xerrors 74 | * Resume fallback on server error in Connect (Andrey Borodin) 75 | 76 | # 1.8.0 (December 3, 2020) 77 | 78 | * Add StatementErrored method to stmtcache.Cache. This allows the cache to purge invalidated prepared statements. (Ethan Pailes) 79 | 80 | # 1.7.2 (November 3, 2020) 81 | 82 | * Fix data value slices into work buffer with capacities larger than length. 83 | 84 | # 1.7.1 (October 31, 2020) 85 | 86 | * Do not asyncClose after receiving FATAL error from PostgreSQL server 87 | 88 | # 1.7.0 (September 26, 2020) 89 | 90 | * Exec(Params|Prepared) return ResultReader with FieldDescriptions loaded 91 | * Add ReceiveResults (Sebastiaan Mannem) 92 | * Fix parsing DSN connection with bad backslash 93 | * Add PgConn.CleanupDone so connection pools can determine when async close is complete 94 | 95 | # 1.6.4 (July 29, 2020) 96 | 97 | * Fix deadlock on error after CommandComplete but before ReadyForQuery 98 | * Fix panic on parsing DSN with trailing '=' 99 | 100 | # 1.6.3 (July 22, 2020) 101 | 102 | * Fix error message after AppendCertsFromPEM failure (vahid-sohrabloo) 103 | 104 | # 1.6.2 (July 14, 2020) 105 | 106 | * Update pgservicefile library 107 | 108 | # 1.6.1 (June 27, 2020) 109 | 110 | * Update golang.org/x/crypto to latest 111 | * Update golang.org/x/text to 0.3.3 112 | * Fix error handling for bad PGSERVICE definition 113 | * Redact passwords in ParseConfig errors (Lukas Vogel) 114 | 115 | # 1.6.0 (June 6, 2020) 116 | 117 | * Fix panic when closing conn during cancellable query 118 | * Fix behavior of sslmode=require with sslrootcert present (Petr Jediný) 119 | * Fix field descriptions available after command concluded (Tobias Salzmann) 120 | * Support connect_timeout (georgysavva) 121 | * Handle IPv6 in connection URLs (Lukas Vogel) 122 | * Fix ValidateConnect with cancelable context 123 | * Improve CopyFrom performance 124 | * Add Config.Copy (georgysavva) 125 | 126 | # 1.5.0 (March 30, 2020) 127 | 128 | * Update golang.org/x/crypto for security fix 129 | * Implement "verify-ca" SSL mode (Greg Curtis) 130 | 131 | # 1.4.0 (March 7, 2020) 132 | 133 | * Fix ExecParams and ExecPrepared handling of empty query. 134 | * Support reading config from PostgreSQL service files. 135 | 136 | # 1.3.2 (February 14, 2020) 137 | 138 | * Update chunkreader to v2.0.1 for optimized default buffer size. 139 | 140 | # 1.3.1 (February 5, 2020) 141 | 142 | * Fix CopyFrom deadlock when multiple NoticeResponse received during copy 143 | 144 | # 1.3.0 (January 23, 2020) 145 | 146 | * Add Hijack and Construct. 147 | * Update pgproto3 to v2.0.1. 148 | 149 | # 1.2.1 (January 13, 2020) 150 | 151 | * Fix data race in context cancellation introduced in v1.2.0. 152 | 153 | # 1.2.0 (January 11, 2020) 154 | 155 | ## Features 156 | 157 | * Add Insert(), Update(), Delete(), and Select() statement type query methods to CommandTag. 158 | * Add PgError.SQLState method. This could be used for compatibility with other drivers and databases. 159 | 160 | ## Performance 161 | 162 | * Improve performance when context.Background() is used. (bakape) 163 | * CommandTag.RowsAffected is faster and does not allocate. 164 | 165 | ## Fixes 166 | 167 | * Try to cancel any in-progress query when a conn is closed by ctx cancel. 168 | * Handle NoticeResponse during CopyFrom. 169 | * Ignore errors sending Terminate message while closing connection. This mimics the behavior of libpq PGfinish. 170 | 171 | # 1.1.0 (October 12, 2019) 172 | 173 | * Add PgConn.IsBusy() method. 174 | 175 | # 1.0.1 (September 19, 2019) 176 | 177 | * Fix statement cache not properly cleaning discarded statements. 178 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-2021 Jack Christensen 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://godoc.org/github.com/jackc/pgconn?status.svg)](https://godoc.org/github.com/jackc/pgconn) 2 | ![CI](https://github.com/jackc/pgconn/workflows/CI/badge.svg) 3 | 4 | --- 5 | 6 | This version is used with pgx `v4`. In pgx `v5` it is part of the https://github.com/jackc/pgx repository. This `v4` 7 | version will reach end-of-life on July 1, 2025. Only security bug fixes will be made to this version. 8 | 9 | --- 10 | 11 | # pgconn 12 | 13 | Package pgconn is a low-level PostgreSQL database driver. It operates at nearly the same level as the C library libpq. 14 | It is primarily intended to serve as the foundation for higher level libraries such as https://github.com/jackc/pgx. 15 | Applications should handle normal queries with a higher level library and only use pgconn directly when required for 16 | low-level access to PostgreSQL functionality. 17 | 18 | ## Example Usage 19 | 20 | ```go 21 | pgConn, err := pgconn.Connect(context.Background(), os.Getenv("DATABASE_URL")) 22 | if err != nil { 23 | log.Fatalln("pgconn failed to connect:", err) 24 | } 25 | defer pgConn.Close(context.Background()) 26 | 27 | result := pgConn.ExecParams(context.Background(), "SELECT email FROM users WHERE id=$1", [][]byte{[]byte("123")}, nil, nil, nil) 28 | for result.NextRow() { 29 | fmt.Println("User 123 has email:", string(result.Values()[0])) 30 | } 31 | _, err = result.Close() 32 | if err != nil { 33 | log.Fatalln("failed reading result:", err) 34 | } 35 | ``` 36 | 37 | ## Testing 38 | 39 | The pgconn tests require a PostgreSQL database. It will connect to the database specified in the `PGX_TEST_CONN_STRING` 40 | environment variable. The `PGX_TEST_CONN_STRING` environment variable can be a URL or DSN. In addition, the standard `PG*` 41 | environment variables will be respected. Consider using [direnv](https://github.com/direnv/direnv) to simplify 42 | environment variable handling. 43 | 44 | ### Example Test Environment 45 | 46 | Connect to your PostgreSQL server and run: 47 | 48 | ``` 49 | create database pgx_test; 50 | ``` 51 | 52 | Now you can run the tests: 53 | 54 | ```bash 55 | PGX_TEST_CONN_STRING="host=/var/run/postgresql dbname=pgx_test" go test ./... 56 | ``` 57 | 58 | ### Connection and Authentication Tests 59 | 60 | Pgconn supports multiple connection types and means of authentication. These tests are optional. They 61 | will only run if the appropriate environment variable is set. Run `go test -v | grep SKIP` to see if any tests are being 62 | skipped. Most developers will not need to enable these tests. See `ci/setup_test.bash` for an example set up if you need change 63 | authentication code. 64 | -------------------------------------------------------------------------------- /auth_scram.go: -------------------------------------------------------------------------------- 1 | // SCRAM-SHA-256 authentication 2 | // 3 | // Resources: 4 | // https://tools.ietf.org/html/rfc5802 5 | // https://tools.ietf.org/html/rfc8265 6 | // https://www.postgresql.org/docs/current/sasl-authentication.html 7 | // 8 | // Inspiration drawn from other implementations: 9 | // https://github.com/lib/pq/pull/608 10 | // https://github.com/lib/pq/pull/788 11 | // https://github.com/lib/pq/pull/833 12 | 13 | package pgconn 14 | 15 | import ( 16 | "bytes" 17 | "crypto/hmac" 18 | "crypto/rand" 19 | "crypto/sha256" 20 | "encoding/base64" 21 | "errors" 22 | "fmt" 23 | "strconv" 24 | 25 | "github.com/jackc/pgproto3/v2" 26 | "golang.org/x/crypto/pbkdf2" 27 | "golang.org/x/text/secure/precis" 28 | ) 29 | 30 | const clientNonceLen = 18 31 | 32 | // Perform SCRAM authentication. 33 | func (c *PgConn) scramAuth(serverAuthMechanisms []string) error { 34 | sc, err := newScramClient(serverAuthMechanisms, c.config.Password) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | // Send client-first-message in a SASLInitialResponse 40 | saslInitialResponse := &pgproto3.SASLInitialResponse{ 41 | AuthMechanism: "SCRAM-SHA-256", 42 | Data: sc.clientFirstMessage(), 43 | } 44 | buf, err := saslInitialResponse.Encode(nil) 45 | if err != nil { 46 | return err 47 | } 48 | _, err = c.conn.Write(buf) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | // Receive server-first-message payload in a AuthenticationSASLContinue. 54 | saslContinue, err := c.rxSASLContinue() 55 | if err != nil { 56 | return err 57 | } 58 | err = sc.recvServerFirstMessage(saslContinue.Data) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | // Send client-final-message in a SASLResponse 64 | saslResponse := &pgproto3.SASLResponse{ 65 | Data: []byte(sc.clientFinalMessage()), 66 | } 67 | buf, err = saslResponse.Encode(nil) 68 | if err != nil { 69 | return err 70 | } 71 | _, err = c.conn.Write(buf) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | // Receive server-final-message payload in a AuthenticationSASLFinal. 77 | saslFinal, err := c.rxSASLFinal() 78 | if err != nil { 79 | return err 80 | } 81 | return sc.recvServerFinalMessage(saslFinal.Data) 82 | } 83 | 84 | func (c *PgConn) rxSASLContinue() (*pgproto3.AuthenticationSASLContinue, error) { 85 | msg, err := c.receiveMessage() 86 | if err != nil { 87 | return nil, err 88 | } 89 | switch m := msg.(type) { 90 | case *pgproto3.AuthenticationSASLContinue: 91 | return m, nil 92 | case *pgproto3.ErrorResponse: 93 | return nil, ErrorResponseToPgError(m) 94 | } 95 | 96 | return nil, fmt.Errorf("expected AuthenticationSASLContinue message but received unexpected message %T", msg) 97 | } 98 | 99 | func (c *PgConn) rxSASLFinal() (*pgproto3.AuthenticationSASLFinal, error) { 100 | msg, err := c.receiveMessage() 101 | if err != nil { 102 | return nil, err 103 | } 104 | switch m := msg.(type) { 105 | case *pgproto3.AuthenticationSASLFinal: 106 | return m, nil 107 | case *pgproto3.ErrorResponse: 108 | return nil, ErrorResponseToPgError(m) 109 | } 110 | 111 | return nil, fmt.Errorf("expected AuthenticationSASLFinal message but received unexpected message %T", msg) 112 | } 113 | 114 | type scramClient struct { 115 | serverAuthMechanisms []string 116 | password []byte 117 | clientNonce []byte 118 | 119 | clientFirstMessageBare []byte 120 | 121 | serverFirstMessage []byte 122 | clientAndServerNonce []byte 123 | salt []byte 124 | iterations int 125 | 126 | saltedPassword []byte 127 | authMessage []byte 128 | } 129 | 130 | func newScramClient(serverAuthMechanisms []string, password string) (*scramClient, error) { 131 | sc := &scramClient{ 132 | serverAuthMechanisms: serverAuthMechanisms, 133 | } 134 | 135 | // Ensure server supports SCRAM-SHA-256 136 | hasScramSHA256 := false 137 | for _, mech := range sc.serverAuthMechanisms { 138 | if mech == "SCRAM-SHA-256" { 139 | hasScramSHA256 = true 140 | break 141 | } 142 | } 143 | if !hasScramSHA256 { 144 | return nil, errors.New("server does not support SCRAM-SHA-256") 145 | } 146 | 147 | // precis.OpaqueString is equivalent to SASLprep for password. 148 | var err error 149 | sc.password, err = precis.OpaqueString.Bytes([]byte(password)) 150 | if err != nil { 151 | // PostgreSQL allows passwords invalid according to SCRAM / SASLprep. 152 | sc.password = []byte(password) 153 | } 154 | 155 | buf := make([]byte, clientNonceLen) 156 | _, err = rand.Read(buf) 157 | if err != nil { 158 | return nil, err 159 | } 160 | sc.clientNonce = make([]byte, base64.RawStdEncoding.EncodedLen(len(buf))) 161 | base64.RawStdEncoding.Encode(sc.clientNonce, buf) 162 | 163 | return sc, nil 164 | } 165 | 166 | func (sc *scramClient) clientFirstMessage() []byte { 167 | sc.clientFirstMessageBare = []byte(fmt.Sprintf("n=,r=%s", sc.clientNonce)) 168 | return []byte(fmt.Sprintf("n,,%s", sc.clientFirstMessageBare)) 169 | } 170 | 171 | func (sc *scramClient) recvServerFirstMessage(serverFirstMessage []byte) error { 172 | sc.serverFirstMessage = serverFirstMessage 173 | buf := serverFirstMessage 174 | if !bytes.HasPrefix(buf, []byte("r=")) { 175 | return errors.New("invalid SCRAM server-first-message received from server: did not include r=") 176 | } 177 | buf = buf[2:] 178 | 179 | idx := bytes.IndexByte(buf, ',') 180 | if idx == -1 { 181 | return errors.New("invalid SCRAM server-first-message received from server: did not include s=") 182 | } 183 | sc.clientAndServerNonce = buf[:idx] 184 | buf = buf[idx+1:] 185 | 186 | if !bytes.HasPrefix(buf, []byte("s=")) { 187 | return errors.New("invalid SCRAM server-first-message received from server: did not include s=") 188 | } 189 | buf = buf[2:] 190 | 191 | idx = bytes.IndexByte(buf, ',') 192 | if idx == -1 { 193 | return errors.New("invalid SCRAM server-first-message received from server: did not include i=") 194 | } 195 | saltStr := buf[:idx] 196 | buf = buf[idx+1:] 197 | 198 | if !bytes.HasPrefix(buf, []byte("i=")) { 199 | return errors.New("invalid SCRAM server-first-message received from server: did not include i=") 200 | } 201 | buf = buf[2:] 202 | iterationsStr := buf 203 | 204 | var err error 205 | sc.salt, err = base64.StdEncoding.DecodeString(string(saltStr)) 206 | if err != nil { 207 | return fmt.Errorf("invalid SCRAM salt received from server: %w", err) 208 | } 209 | 210 | sc.iterations, err = strconv.Atoi(string(iterationsStr)) 211 | if err != nil || sc.iterations <= 0 { 212 | return fmt.Errorf("invalid SCRAM iteration count received from server: %w", err) 213 | } 214 | 215 | if !bytes.HasPrefix(sc.clientAndServerNonce, sc.clientNonce) { 216 | return errors.New("invalid SCRAM nonce: did not start with client nonce") 217 | } 218 | 219 | if len(sc.clientAndServerNonce) <= len(sc.clientNonce) { 220 | return errors.New("invalid SCRAM nonce: did not include server nonce") 221 | } 222 | 223 | return nil 224 | } 225 | 226 | func (sc *scramClient) clientFinalMessage() string { 227 | clientFinalMessageWithoutProof := []byte(fmt.Sprintf("c=biws,r=%s", sc.clientAndServerNonce)) 228 | 229 | sc.saltedPassword = pbkdf2.Key([]byte(sc.password), sc.salt, sc.iterations, 32, sha256.New) 230 | sc.authMessage = bytes.Join([][]byte{sc.clientFirstMessageBare, sc.serverFirstMessage, clientFinalMessageWithoutProof}, []byte(",")) 231 | 232 | clientProof := computeClientProof(sc.saltedPassword, sc.authMessage) 233 | 234 | return fmt.Sprintf("%s,p=%s", clientFinalMessageWithoutProof, clientProof) 235 | } 236 | 237 | func (sc *scramClient) recvServerFinalMessage(serverFinalMessage []byte) error { 238 | if !bytes.HasPrefix(serverFinalMessage, []byte("v=")) { 239 | return errors.New("invalid SCRAM server-final-message received from server") 240 | } 241 | 242 | serverSignature := serverFinalMessage[2:] 243 | 244 | if !hmac.Equal(serverSignature, computeServerSignature(sc.saltedPassword, sc.authMessage)) { 245 | return errors.New("invalid SCRAM ServerSignature received from server") 246 | } 247 | 248 | return nil 249 | } 250 | 251 | func computeHMAC(key, msg []byte) []byte { 252 | mac := hmac.New(sha256.New, key) 253 | mac.Write(msg) 254 | return mac.Sum(nil) 255 | } 256 | 257 | func computeClientProof(saltedPassword, authMessage []byte) []byte { 258 | clientKey := computeHMAC(saltedPassword, []byte("Client Key")) 259 | storedKey := sha256.Sum256(clientKey) 260 | clientSignature := computeHMAC(storedKey[:], authMessage) 261 | 262 | clientProof := make([]byte, len(clientSignature)) 263 | for i := 0; i < len(clientSignature); i++ { 264 | clientProof[i] = clientKey[i] ^ clientSignature[i] 265 | } 266 | 267 | buf := make([]byte, base64.StdEncoding.EncodedLen(len(clientProof))) 268 | base64.StdEncoding.Encode(buf, clientProof) 269 | return buf 270 | } 271 | 272 | func computeServerSignature(saltedPassword []byte, authMessage []byte) []byte { 273 | serverKey := computeHMAC(saltedPassword, []byte("Server Key")) 274 | serverSignature := computeHMAC(serverKey, authMessage) 275 | buf := make([]byte, base64.StdEncoding.EncodedLen(len(serverSignature))) 276 | base64.StdEncoding.Encode(buf, serverSignature) 277 | return buf 278 | } 279 | -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | package pgconn_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/jackc/pgconn" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func BenchmarkConnect(b *testing.B) { 15 | benchmarks := []struct { 16 | name string 17 | env string 18 | }{ 19 | {"Unix socket", "PGX_TEST_UNIX_SOCKET_CONN_STRING"}, 20 | {"TCP", "PGX_TEST_TCP_CONN_STRING"}, 21 | } 22 | 23 | for _, bm := range benchmarks { 24 | bm := bm 25 | b.Run(bm.name, func(b *testing.B) { 26 | connString := os.Getenv(bm.env) 27 | if connString == "" { 28 | b.Skipf("Skipping due to missing environment variable %v", bm.env) 29 | } 30 | 31 | for i := 0; i < b.N; i++ { 32 | conn, err := pgconn.Connect(context.Background(), connString) 33 | require.Nil(b, err) 34 | 35 | err = conn.Close(context.Background()) 36 | require.Nil(b, err) 37 | } 38 | }) 39 | } 40 | } 41 | 42 | func BenchmarkExec(b *testing.B) { 43 | expectedValues := [][]byte{[]byte("hello"), []byte("42"), []byte("2019-01-01")} 44 | benchmarks := []struct { 45 | name string 46 | ctx context.Context 47 | }{ 48 | // Using an empty context other than context.Background() to compare 49 | // performance 50 | {"background context", context.Background()}, 51 | {"empty context", context.TODO()}, 52 | } 53 | 54 | for _, bm := range benchmarks { 55 | bm := bm 56 | b.Run(bm.name, func(b *testing.B) { 57 | conn, err := pgconn.Connect(bm.ctx, os.Getenv("PGX_TEST_CONN_STRING")) 58 | require.Nil(b, err) 59 | defer closeConn(b, conn) 60 | 61 | b.ResetTimer() 62 | 63 | for i := 0; i < b.N; i++ { 64 | mrr := conn.Exec(bm.ctx, "select 'hello'::text as a, 42::int4 as b, '2019-01-01'::date") 65 | 66 | for mrr.NextResult() { 67 | rr := mrr.ResultReader() 68 | 69 | rowCount := 0 70 | for rr.NextRow() { 71 | rowCount++ 72 | if len(rr.Values()) != len(expectedValues) { 73 | b.Fatalf("unexpected number of values: %d", len(rr.Values())) 74 | } 75 | for i := range rr.Values() { 76 | if !bytes.Equal(rr.Values()[i], expectedValues[i]) { 77 | b.Fatalf("unexpected values: %s %s", rr.Values()[i], expectedValues[i]) 78 | } 79 | } 80 | } 81 | _, err = rr.Close() 82 | 83 | if err != nil { 84 | b.Fatal(err) 85 | } 86 | if rowCount != 1 { 87 | b.Fatalf("unexpected rowCount: %d", rowCount) 88 | } 89 | } 90 | 91 | err := mrr.Close() 92 | if err != nil { 93 | b.Fatal(err) 94 | } 95 | } 96 | }) 97 | } 98 | } 99 | 100 | func BenchmarkExecPossibleToCancel(b *testing.B) { 101 | conn, err := pgconn.Connect(context.Background(), os.Getenv("PGX_TEST_CONN_STRING")) 102 | require.Nil(b, err) 103 | defer closeConn(b, conn) 104 | 105 | expectedValues := [][]byte{[]byte("hello"), []byte("42"), []byte("2019-01-01")} 106 | 107 | b.ResetTimer() 108 | 109 | ctx, cancel := context.WithCancel(context.Background()) 110 | defer cancel() 111 | 112 | for i := 0; i < b.N; i++ { 113 | mrr := conn.Exec(ctx, "select 'hello'::text as a, 42::int4 as b, '2019-01-01'::date") 114 | 115 | for mrr.NextResult() { 116 | rr := mrr.ResultReader() 117 | 118 | rowCount := 0 119 | for rr.NextRow() { 120 | rowCount++ 121 | if len(rr.Values()) != len(expectedValues) { 122 | b.Fatalf("unexpected number of values: %d", len(rr.Values())) 123 | } 124 | for i := range rr.Values() { 125 | if !bytes.Equal(rr.Values()[i], expectedValues[i]) { 126 | b.Fatalf("unexpected values: %s %s", rr.Values()[i], expectedValues[i]) 127 | } 128 | } 129 | } 130 | _, err = rr.Close() 131 | 132 | if err != nil { 133 | b.Fatal(err) 134 | } 135 | if rowCount != 1 { 136 | b.Fatalf("unexpected rowCount: %d", rowCount) 137 | } 138 | } 139 | 140 | err := mrr.Close() 141 | if err != nil { 142 | b.Fatal(err) 143 | } 144 | } 145 | } 146 | 147 | func BenchmarkExecPrepared(b *testing.B) { 148 | expectedValues := [][]byte{[]byte("hello"), []byte("42"), []byte("2019-01-01")} 149 | 150 | benchmarks := []struct { 151 | name string 152 | ctx context.Context 153 | }{ 154 | // Using an empty context other than context.Background() to compare 155 | // performance 156 | {"background context", context.Background()}, 157 | {"empty context", context.TODO()}, 158 | } 159 | 160 | for _, bm := range benchmarks { 161 | bm := bm 162 | b.Run(bm.name, func(b *testing.B) { 163 | conn, err := pgconn.Connect(bm.ctx, os.Getenv("PGX_TEST_CONN_STRING")) 164 | require.Nil(b, err) 165 | defer closeConn(b, conn) 166 | 167 | _, err = conn.Prepare(bm.ctx, "ps1", "select 'hello'::text as a, 42::int4 as b, '2019-01-01'::date", nil) 168 | require.Nil(b, err) 169 | 170 | b.ResetTimer() 171 | 172 | for i := 0; i < b.N; i++ { 173 | rr := conn.ExecPrepared(bm.ctx, "ps1", nil, nil, nil) 174 | 175 | rowCount := 0 176 | for rr.NextRow() { 177 | rowCount++ 178 | if len(rr.Values()) != len(expectedValues) { 179 | b.Fatalf("unexpected number of values: %d", len(rr.Values())) 180 | } 181 | for i := range rr.Values() { 182 | if !bytes.Equal(rr.Values()[i], expectedValues[i]) { 183 | b.Fatalf("unexpected values: %s %s", rr.Values()[i], expectedValues[i]) 184 | } 185 | } 186 | } 187 | _, err = rr.Close() 188 | 189 | if err != nil { 190 | b.Fatal(err) 191 | } 192 | if rowCount != 1 { 193 | b.Fatalf("unexpected rowCount: %d", rowCount) 194 | } 195 | } 196 | }) 197 | } 198 | } 199 | 200 | func BenchmarkExecPreparedPossibleToCancel(b *testing.B) { 201 | conn, err := pgconn.Connect(context.Background(), os.Getenv("PGX_TEST_CONN_STRING")) 202 | require.Nil(b, err) 203 | defer closeConn(b, conn) 204 | 205 | ctx, cancel := context.WithCancel(context.Background()) 206 | defer cancel() 207 | 208 | _, err = conn.Prepare(ctx, "ps1", "select 'hello'::text as a, 42::int4 as b, '2019-01-01'::date", nil) 209 | require.Nil(b, err) 210 | 211 | expectedValues := [][]byte{[]byte("hello"), []byte("42"), []byte("2019-01-01")} 212 | 213 | b.ResetTimer() 214 | 215 | for i := 0; i < b.N; i++ { 216 | rr := conn.ExecPrepared(ctx, "ps1", nil, nil, nil) 217 | 218 | rowCount := 0 219 | for rr.NextRow() { 220 | rowCount += 1 221 | if len(rr.Values()) != len(expectedValues) { 222 | b.Fatalf("unexpected number of values: %d", len(rr.Values())) 223 | } 224 | for i := range rr.Values() { 225 | if !bytes.Equal(rr.Values()[i], expectedValues[i]) { 226 | b.Fatalf("unexpected values: %s %s", rr.Values()[i], expectedValues[i]) 227 | } 228 | } 229 | } 230 | _, err = rr.Close() 231 | 232 | if err != nil { 233 | b.Fatal(err) 234 | } 235 | if rowCount != 1 { 236 | b.Fatalf("unexpected rowCount: %d", rowCount) 237 | } 238 | } 239 | } 240 | 241 | // func BenchmarkChanToSetDeadlinePossibleToCancel(b *testing.B) { 242 | // conn, err := pgconn.Connect(context.Background(), os.Getenv("PGX_TEST_CONN_STRING")) 243 | // require.Nil(b, err) 244 | // defer closeConn(b, conn) 245 | 246 | // ctx, cancel := context.WithCancel(context.Background()) 247 | // defer cancel() 248 | 249 | // b.ResetTimer() 250 | 251 | // for i := 0; i < b.N; i++ { 252 | // conn.ChanToSetDeadline().Watch(ctx) 253 | // conn.ChanToSetDeadline().Ignore() 254 | // } 255 | // } 256 | 257 | func BenchmarkCommandTagRowsAffected(b *testing.B) { 258 | benchmarks := []struct { 259 | commandTag string 260 | rowsAffected int64 261 | }{ 262 | {"UPDATE 1", 1}, 263 | {"UPDATE 123456789", 123456789}, 264 | {"INSERT 0 1", 1}, 265 | {"INSERT 0 123456789", 123456789}, 266 | } 267 | 268 | for _, bm := range benchmarks { 269 | ct := pgconn.CommandTag(bm.commandTag) 270 | b.Run(bm.commandTag, func(b *testing.B) { 271 | var n int64 272 | for i := 0; i < b.N; i++ { 273 | n = ct.RowsAffected() 274 | } 275 | if n != bm.rowsAffected { 276 | b.Errorf("expected %d got %d", bm.rowsAffected, n) 277 | } 278 | }) 279 | } 280 | } 281 | 282 | func BenchmarkCommandTagTypeFromString(b *testing.B) { 283 | ct := pgconn.CommandTag("UPDATE 1") 284 | 285 | var update bool 286 | for i := 0; i < b.N; i++ { 287 | update = strings.HasPrefix(ct.String(), "UPDATE") 288 | } 289 | if !update { 290 | b.Error("expected update") 291 | } 292 | } 293 | 294 | func BenchmarkCommandTagInsert(b *testing.B) { 295 | benchmarks := []struct { 296 | commandTag string 297 | is bool 298 | }{ 299 | {"INSERT 1", true}, 300 | {"INSERT 1234567890", true}, 301 | {"UPDATE 1", false}, 302 | {"UPDATE 1234567890", false}, 303 | {"DELETE 1", false}, 304 | {"DELETE 1234567890", false}, 305 | {"SELECT 1", false}, 306 | {"SELECT 1234567890", false}, 307 | {"UNKNOWN 1234567890", false}, 308 | } 309 | 310 | for _, bm := range benchmarks { 311 | ct := pgconn.CommandTag(bm.commandTag) 312 | b.Run(bm.commandTag, func(b *testing.B) { 313 | var is bool 314 | for i := 0; i < b.N; i++ { 315 | is = ct.Insert() 316 | } 317 | if is != bm.is { 318 | b.Errorf("expected %v got %v", bm.is, is) 319 | } 320 | }) 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /ci/script.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eux 3 | 4 | if [ "${PGVERSION-}" != "" ] 5 | then 6 | go test -v -race ./... 7 | elif [ "${CRATEVERSION-}" != "" ] 8 | then 9 | go test -v -race -run 'TestCrateDBConnect' 10 | fi 11 | -------------------------------------------------------------------------------- /ci/setup_test.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eux 3 | 4 | if [[ "${PGVERSION-}" =~ ^[0-9.]+$ ]] 5 | then 6 | sudo apt-get remove -y --purge postgresql libpq-dev libpq5 postgresql-client-common postgresql-common 7 | sudo rm -rf /var/lib/postgresql 8 | wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - 9 | sudo sh -c "echo deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main $PGVERSION >> /etc/apt/sources.list.d/postgresql.list" 10 | sudo apt-get update -qq 11 | sudo apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::="--force-confnew" install postgresql-$PGVERSION postgresql-server-dev-$PGVERSION postgresql-contrib-$PGVERSION 12 | sudo chmod 777 /etc/postgresql/$PGVERSION/main/pg_hba.conf 13 | echo "local all postgres trust" > /etc/postgresql/$PGVERSION/main/pg_hba.conf 14 | echo "local all all trust" >> /etc/postgresql/$PGVERSION/main/pg_hba.conf 15 | echo "host all pgx_md5 127.0.0.1/32 md5" >> /etc/postgresql/$PGVERSION/main/pg_hba.conf 16 | echo "host all pgx_pw 127.0.0.1/32 password" >> /etc/postgresql/$PGVERSION/main/pg_hba.conf 17 | echo "hostssl all pgx_ssl 127.0.0.1/32 md5" >> /etc/postgresql/$PGVERSION/main/pg_hba.conf 18 | echo "host replication pgx_replication 127.0.0.1/32 md5" >> /etc/postgresql/$PGVERSION/main/pg_hba.conf 19 | echo "host pgx_test pgx_replication 127.0.0.1/32 md5" >> /etc/postgresql/$PGVERSION/main/pg_hba.conf 20 | sudo chmod 777 /etc/postgresql/$PGVERSION/main/postgresql.conf 21 | if $(dpkg --compare-versions $PGVERSION ge 9.6) ; then 22 | echo "wal_level='logical'" >> /etc/postgresql/$PGVERSION/main/postgresql.conf 23 | echo "max_wal_senders=5" >> /etc/postgresql/$PGVERSION/main/postgresql.conf 24 | echo "max_replication_slots=5" >> /etc/postgresql/$PGVERSION/main/postgresql.conf 25 | fi 26 | sudo /etc/init.d/postgresql restart 27 | 28 | # The tricky test user, below, has to actually exist so that it can be used in a test 29 | # of aclitem formatting. It turns out aclitems cannot contain non-existing users/roles. 30 | psql -U postgres -c 'create database pgx_test' 31 | psql -U postgres pgx_test -c 'create extension hstore' 32 | psql -U postgres pgx_test -c 'create domain uint64 as numeric(20,0)' 33 | psql -U postgres -c "create user pgx_ssl SUPERUSER PASSWORD 'secret'" 34 | psql -U postgres -c "create user pgx_md5 SUPERUSER PASSWORD 'secret'" 35 | psql -U postgres -c "create user pgx_pw SUPERUSER PASSWORD 'secret'" 36 | psql -U postgres -c "create user `whoami`" 37 | psql -U postgres -c "create user pgx_replication with replication password 'secret'" 38 | psql -U postgres -c "create user \" tricky, ' } \"\" \\ test user \" superuser password 'secret'" 39 | fi 40 | 41 | if [[ "${PGVERSION-}" =~ ^cockroach ]] 42 | then 43 | wget -qO- https://binaries.cockroachdb.com/cockroach-v22.1.8.linux-amd64.tgz | tar xvz 44 | sudo mv cockroach-v22.1.8.linux-amd64/cockroach /usr/local/bin/ 45 | cockroach start-single-node --insecure --background --listen-addr=localhost 46 | cockroach sql --insecure -e 'create database pgx_test' 47 | fi 48 | 49 | if [ "${CRATEVERSION-}" != "" ] 50 | then 51 | docker run \ 52 | -p "6543:5432" \ 53 | -d \ 54 | crate:"$CRATEVERSION" \ 55 | crate \ 56 | -Cnetwork.host=0.0.0.0 \ 57 | -Ctransport.host=localhost \ 58 | -Clicense.enterprise=false 59 | fi 60 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package pgconn 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "encoding/pem" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "math" 13 | "net" 14 | "net/url" 15 | "os" 16 | "path/filepath" 17 | "strconv" 18 | "strings" 19 | "time" 20 | 21 | "github.com/jackc/chunkreader/v2" 22 | "github.com/jackc/pgpassfile" 23 | "github.com/jackc/pgproto3/v2" 24 | "github.com/jackc/pgservicefile" 25 | ) 26 | 27 | type AfterConnectFunc func(ctx context.Context, pgconn *PgConn) error 28 | type ValidateConnectFunc func(ctx context.Context, pgconn *PgConn) error 29 | type GetSSLPasswordFunc func(ctx context.Context) string 30 | 31 | // Config is the settings used to establish a connection to a PostgreSQL server. It must be created by ParseConfig. A 32 | // manually initialized Config will cause ConnectConfig to panic. 33 | type Config struct { 34 | Host string // host (e.g. localhost) or absolute path to unix domain socket directory (e.g. /private/tmp) 35 | Port uint16 36 | Database string 37 | User string 38 | Password string 39 | TLSConfig *tls.Config // nil disables TLS 40 | ConnectTimeout time.Duration 41 | DialFunc DialFunc // e.g. net.Dialer.DialContext 42 | LookupFunc LookupFunc // e.g. net.Resolver.LookupHost 43 | BuildFrontend BuildFrontendFunc 44 | RuntimeParams map[string]string // Run-time parameters to set on connection as session default values (e.g. search_path or application_name) 45 | 46 | KerberosSrvName string 47 | KerberosSpn string 48 | Fallbacks []*FallbackConfig 49 | 50 | // ValidateConnect is called during a connection attempt after a successful authentication with the PostgreSQL server. 51 | // It can be used to validate that the server is acceptable. If this returns an error the connection is closed and the next 52 | // fallback config is tried. This allows implementing high availability behavior such as libpq does with target_session_attrs. 53 | ValidateConnect ValidateConnectFunc 54 | 55 | // AfterConnect is called after ValidateConnect. It can be used to set up the connection (e.g. Set session variables 56 | // or prepare statements). If this returns an error the connection attempt fails. 57 | AfterConnect AfterConnectFunc 58 | 59 | // OnNotice is a callback function called when a notice response is received. 60 | OnNotice NoticeHandler 61 | 62 | // OnNotification is a callback function called when a notification from the LISTEN/NOTIFY system is received. 63 | OnNotification NotificationHandler 64 | 65 | createdByParseConfig bool // Used to enforce created by ParseConfig rule. 66 | } 67 | 68 | // ParseConfigOptions contains options that control how a config is built such as getsslpassword. 69 | type ParseConfigOptions struct { 70 | // GetSSLPassword gets the password to decrypt a SSL client certificate. This is analogous to the the libpq function 71 | // PQsetSSLKeyPassHook_OpenSSL. 72 | GetSSLPassword GetSSLPasswordFunc 73 | } 74 | 75 | // Copy returns a deep copy of the config that is safe to use and modify. 76 | // The only exception is the TLSConfig field: 77 | // according to the tls.Config docs it must not be modified after creation. 78 | func (c *Config) Copy() *Config { 79 | newConf := new(Config) 80 | *newConf = *c 81 | if newConf.TLSConfig != nil { 82 | newConf.TLSConfig = c.TLSConfig.Clone() 83 | } 84 | if newConf.RuntimeParams != nil { 85 | newConf.RuntimeParams = make(map[string]string, len(c.RuntimeParams)) 86 | for k, v := range c.RuntimeParams { 87 | newConf.RuntimeParams[k] = v 88 | } 89 | } 90 | if newConf.Fallbacks != nil { 91 | newConf.Fallbacks = make([]*FallbackConfig, len(c.Fallbacks)) 92 | for i, fallback := range c.Fallbacks { 93 | newFallback := new(FallbackConfig) 94 | *newFallback = *fallback 95 | if newFallback.TLSConfig != nil { 96 | newFallback.TLSConfig = fallback.TLSConfig.Clone() 97 | } 98 | newConf.Fallbacks[i] = newFallback 99 | } 100 | } 101 | return newConf 102 | } 103 | 104 | // FallbackConfig is additional settings to attempt a connection with when the primary Config fails to establish a 105 | // network connection. It is used for TLS fallback such as sslmode=prefer and high availability (HA) connections. 106 | type FallbackConfig struct { 107 | Host string // host (e.g. localhost) or path to unix domain socket directory (e.g. /private/tmp) 108 | Port uint16 109 | TLSConfig *tls.Config // nil disables TLS 110 | } 111 | 112 | // isAbsolutePath checks if the provided value is an absolute path either 113 | // beginning with a forward slash (as on Linux-based systems) or with a capital 114 | // letter A-Z followed by a colon and a backslash, e.g., "C:\", (as on Windows). 115 | func isAbsolutePath(path string) bool { 116 | isWindowsPath := func(p string) bool { 117 | if len(p) < 3 { 118 | return false 119 | } 120 | drive := p[0] 121 | colon := p[1] 122 | backslash := p[2] 123 | if drive >= 'A' && drive <= 'Z' && colon == ':' && backslash == '\\' { 124 | return true 125 | } 126 | return false 127 | } 128 | return strings.HasPrefix(path, "/") || isWindowsPath(path) 129 | } 130 | 131 | // NetworkAddress converts a PostgreSQL host and port into network and address suitable for use with 132 | // net.Dial. 133 | func NetworkAddress(host string, port uint16) (network, address string) { 134 | if isAbsolutePath(host) { 135 | network = "unix" 136 | address = filepath.Join(host, ".s.PGSQL.") + strconv.FormatInt(int64(port), 10) 137 | } else { 138 | network = "tcp" 139 | address = net.JoinHostPort(host, strconv.Itoa(int(port))) 140 | } 141 | return network, address 142 | } 143 | 144 | // ParseConfig builds a *Config from connString with similar behavior to the PostgreSQL standard C library libpq. It 145 | // uses the same defaults as libpq (e.g. port=5432) and understands most PG* environment variables. ParseConfig closely 146 | // matches the parsing behavior of libpq. connString may either be in URL format or keyword = value format (DSN style). 147 | // See https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING for details. connString also may be 148 | // empty to only read from the environment. If a password is not supplied it will attempt to read the .pgpass file. 149 | // 150 | // # Example DSN 151 | // user=jack password=secret host=pg.example.com port=5432 dbname=mydb sslmode=verify-ca 152 | // 153 | // # Example URL 154 | // postgres://jack:secret@pg.example.com:5432/mydb?sslmode=verify-ca 155 | // 156 | // The returned *Config may be modified. However, it is strongly recommended that any configuration that can be done 157 | // through the connection string be done there. In particular the fields Host, Port, TLSConfig, and Fallbacks can be 158 | // interdependent (e.g. TLSConfig needs knowledge of the host to validate the server certificate). These fields should 159 | // not be modified individually. They should all be modified or all left unchanged. 160 | // 161 | // ParseConfig supports specifying multiple hosts in similar manner to libpq. Host and port may include comma separated 162 | // values that will be tried in order. This can be used as part of a high availability system. See 163 | // https://www.postgresql.org/docs/11/libpq-connect.html#LIBPQ-MULTIPLE-HOSTS for more information. 164 | // 165 | // # Example URL 166 | // postgres://jack:secret@foo.example.com:5432,bar.example.com:5432/mydb 167 | // 168 | // ParseConfig currently recognizes the following environment variable and their parameter key word equivalents passed 169 | // via database URL or DSN: 170 | // 171 | // PGHOST 172 | // PGPORT 173 | // PGDATABASE 174 | // PGUSER 175 | // PGPASSWORD 176 | // PGPASSFILE 177 | // PGSERVICE 178 | // PGSERVICEFILE 179 | // PGSSLMODE 180 | // PGSSLCERT 181 | // PGSSLKEY 182 | // PGSSLROOTCERT 183 | // PGSSLPASSWORD 184 | // PGAPPNAME 185 | // PGCONNECT_TIMEOUT 186 | // PGTARGETSESSIONATTRS 187 | // 188 | // See http://www.postgresql.org/docs/11/static/libpq-envars.html for details on the meaning of environment variables. 189 | // 190 | // See https://www.postgresql.org/docs/11/libpq-connect.html#LIBPQ-PARAMKEYWORDS for parameter key word names. They are 191 | // usually but not always the environment variable name downcased and without the "PG" prefix. 192 | // 193 | // Important Security Notes: 194 | // 195 | // ParseConfig tries to match libpq behavior with regard to PGSSLMODE. This includes defaulting to "prefer" behavior if 196 | // not set. 197 | // 198 | // See http://www.postgresql.org/docs/11/static/libpq-ssl.html#LIBPQ-SSL-PROTECTION for details on what level of 199 | // security each sslmode provides. 200 | // 201 | // The sslmode "prefer" (the default), sslmode "allow", and multiple hosts are implemented via the Fallbacks field of 202 | // the Config struct. If TLSConfig is manually changed it will not affect the fallbacks. For example, in the case of 203 | // sslmode "prefer" this means it will first try the main Config settings which use TLS, then it will try the fallback 204 | // which does not use TLS. This can lead to an unexpected unencrypted connection if the main TLS config is manually 205 | // changed later but the unencrypted fallback is present. Ensure there are no stale fallbacks when manually setting 206 | // TLSConfig. 207 | // 208 | // Other known differences with libpq: 209 | // 210 | // When multiple hosts are specified, libpq allows them to have different passwords set via the .pgpass file. pgconn 211 | // does not. 212 | // 213 | // In addition, ParseConfig accepts the following options: 214 | // 215 | // min_read_buffer_size 216 | // The minimum size of the internal read buffer. Default 8192. 217 | // servicefile 218 | // libpq only reads servicefile from the PGSERVICEFILE environment variable. ParseConfig accepts servicefile as a 219 | // part of the connection string. 220 | func ParseConfig(connString string) (*Config, error) { 221 | var parseConfigOptions ParseConfigOptions 222 | return ParseConfigWithOptions(connString, parseConfigOptions) 223 | } 224 | 225 | // ParseConfigWithOptions builds a *Config from connString and options with similar behavior to the PostgreSQL standard 226 | // C library libpq. options contains settings that cannot be specified in a connString such as providing a function to 227 | // get the SSL password. 228 | func ParseConfigWithOptions(connString string, options ParseConfigOptions) (*Config, error) { 229 | defaultSettings := defaultSettings() 230 | envSettings := parseEnvSettings() 231 | 232 | connStringSettings := make(map[string]string) 233 | if connString != "" { 234 | var err error 235 | // connString may be a database URL or a DSN 236 | if strings.HasPrefix(connString, "postgres://") || strings.HasPrefix(connString, "postgresql://") { 237 | connStringSettings, err = parseURLSettings(connString) 238 | if err != nil { 239 | return nil, &parseConfigError{connString: connString, msg: "failed to parse as URL", err: err} 240 | } 241 | } else { 242 | connStringSettings, err = parseDSNSettings(connString) 243 | if err != nil { 244 | return nil, &parseConfigError{connString: connString, msg: "failed to parse as DSN", err: err} 245 | } 246 | } 247 | } 248 | 249 | settings := mergeSettings(defaultSettings, envSettings, connStringSettings) 250 | if service, present := settings["service"]; present { 251 | serviceSettings, err := parseServiceSettings(settings["servicefile"], service) 252 | if err != nil { 253 | return nil, &parseConfigError{connString: connString, msg: "failed to read service", err: err} 254 | } 255 | 256 | settings = mergeSettings(defaultSettings, envSettings, serviceSettings, connStringSettings) 257 | } 258 | 259 | minReadBufferSize, err := strconv.ParseInt(settings["min_read_buffer_size"], 10, 32) 260 | if err != nil { 261 | return nil, &parseConfigError{connString: connString, msg: "cannot parse min_read_buffer_size", err: err} 262 | } 263 | 264 | config := &Config{ 265 | createdByParseConfig: true, 266 | Database: settings["database"], 267 | User: settings["user"], 268 | Password: settings["password"], 269 | RuntimeParams: make(map[string]string), 270 | BuildFrontend: makeDefaultBuildFrontendFunc(int(minReadBufferSize)), 271 | } 272 | 273 | if connectTimeoutSetting, present := settings["connect_timeout"]; present { 274 | connectTimeout, err := parseConnectTimeoutSetting(connectTimeoutSetting) 275 | if err != nil { 276 | return nil, &parseConfigError{connString: connString, msg: "invalid connect_timeout", err: err} 277 | } 278 | config.ConnectTimeout = connectTimeout 279 | config.DialFunc = makeConnectTimeoutDialFunc(connectTimeout) 280 | } else { 281 | defaultDialer := makeDefaultDialer() 282 | config.DialFunc = defaultDialer.DialContext 283 | } 284 | 285 | config.LookupFunc = makeDefaultResolver().LookupHost 286 | 287 | notRuntimeParams := map[string]struct{}{ 288 | "host": {}, 289 | "port": {}, 290 | "database": {}, 291 | "user": {}, 292 | "password": {}, 293 | "passfile": {}, 294 | "connect_timeout": {}, 295 | "sslmode": {}, 296 | "sslkey": {}, 297 | "sslcert": {}, 298 | "sslrootcert": {}, 299 | "sslpassword": {}, 300 | "sslsni": {}, 301 | "krbspn": {}, 302 | "krbsrvname": {}, 303 | "target_session_attrs": {}, 304 | "min_read_buffer_size": {}, 305 | "service": {}, 306 | "servicefile": {}, 307 | } 308 | 309 | // Adding kerberos configuration 310 | if _, present := settings["krbsrvname"]; present { 311 | config.KerberosSrvName = settings["krbsrvname"] 312 | } 313 | if _, present := settings["krbspn"]; present { 314 | config.KerberosSpn = settings["krbspn"] 315 | } 316 | 317 | for k, v := range settings { 318 | if _, present := notRuntimeParams[k]; present { 319 | continue 320 | } 321 | config.RuntimeParams[k] = v 322 | } 323 | 324 | fallbacks := []*FallbackConfig{} 325 | 326 | hosts := strings.Split(settings["host"], ",") 327 | ports := strings.Split(settings["port"], ",") 328 | 329 | for i, host := range hosts { 330 | var portStr string 331 | if i < len(ports) { 332 | portStr = ports[i] 333 | } else { 334 | portStr = ports[0] 335 | } 336 | 337 | port, err := parsePort(portStr) 338 | if err != nil { 339 | return nil, &parseConfigError{connString: connString, msg: "invalid port", err: err} 340 | } 341 | 342 | var tlsConfigs []*tls.Config 343 | 344 | // Ignore TLS settings if Unix domain socket like libpq 345 | if network, _ := NetworkAddress(host, port); network == "unix" { 346 | tlsConfigs = append(tlsConfigs, nil) 347 | } else { 348 | var err error 349 | tlsConfigs, err = configTLS(settings, host, options) 350 | if err != nil { 351 | return nil, &parseConfigError{connString: connString, msg: "failed to configure TLS", err: err} 352 | } 353 | } 354 | 355 | for _, tlsConfig := range tlsConfigs { 356 | fallbacks = append(fallbacks, &FallbackConfig{ 357 | Host: host, 358 | Port: port, 359 | TLSConfig: tlsConfig, 360 | }) 361 | } 362 | } 363 | 364 | config.Host = fallbacks[0].Host 365 | config.Port = fallbacks[0].Port 366 | config.TLSConfig = fallbacks[0].TLSConfig 367 | config.Fallbacks = fallbacks[1:] 368 | 369 | if config.Password == "" { 370 | passfile, err := pgpassfile.ReadPassfile(settings["passfile"]) 371 | if err == nil { 372 | host := config.Host 373 | if network, _ := NetworkAddress(config.Host, config.Port); network == "unix" { 374 | host = "localhost" 375 | } 376 | 377 | config.Password = passfile.FindPassword(host, strconv.Itoa(int(config.Port)), config.Database, config.User) 378 | } 379 | } 380 | 381 | switch tsa := settings["target_session_attrs"]; tsa { 382 | case "read-write": 383 | config.ValidateConnect = ValidateConnectTargetSessionAttrsReadWrite 384 | case "read-only": 385 | config.ValidateConnect = ValidateConnectTargetSessionAttrsReadOnly 386 | case "primary": 387 | config.ValidateConnect = ValidateConnectTargetSessionAttrsPrimary 388 | case "standby": 389 | config.ValidateConnect = ValidateConnectTargetSessionAttrsStandby 390 | case "prefer-standby": 391 | config.ValidateConnect = ValidateConnectTargetSessionAttrsPreferStandby 392 | case "any": 393 | // do nothing 394 | default: 395 | return nil, &parseConfigError{connString: connString, msg: fmt.Sprintf("unknown target_session_attrs value: %v", tsa)} 396 | } 397 | 398 | return config, nil 399 | } 400 | 401 | func mergeSettings(settingSets ...map[string]string) map[string]string { 402 | settings := make(map[string]string) 403 | 404 | for _, s2 := range settingSets { 405 | for k, v := range s2 { 406 | settings[k] = v 407 | } 408 | } 409 | 410 | return settings 411 | } 412 | 413 | func parseEnvSettings() map[string]string { 414 | settings := make(map[string]string) 415 | 416 | nameMap := map[string]string{ 417 | "PGHOST": "host", 418 | "PGPORT": "port", 419 | "PGDATABASE": "database", 420 | "PGUSER": "user", 421 | "PGPASSWORD": "password", 422 | "PGPASSFILE": "passfile", 423 | "PGAPPNAME": "application_name", 424 | "PGCONNECT_TIMEOUT": "connect_timeout", 425 | "PGSSLMODE": "sslmode", 426 | "PGSSLKEY": "sslkey", 427 | "PGSSLCERT": "sslcert", 428 | "PGSSLSNI": "sslsni", 429 | "PGSSLROOTCERT": "sslrootcert", 430 | "PGSSLPASSWORD": "sslpassword", 431 | "PGTARGETSESSIONATTRS": "target_session_attrs", 432 | "PGSERVICE": "service", 433 | "PGSERVICEFILE": "servicefile", 434 | } 435 | 436 | for envname, realname := range nameMap { 437 | value := os.Getenv(envname) 438 | if value != "" { 439 | settings[realname] = value 440 | } 441 | } 442 | 443 | return settings 444 | } 445 | 446 | func parseURLSettings(connString string) (map[string]string, error) { 447 | settings := make(map[string]string) 448 | 449 | url, err := url.Parse(connString) 450 | if err != nil { 451 | return nil, err 452 | } 453 | 454 | if url.User != nil { 455 | settings["user"] = url.User.Username() 456 | if password, present := url.User.Password(); present { 457 | settings["password"] = password 458 | } 459 | } 460 | 461 | // Handle multiple host:port's in url.Host by splitting them into host,host,host and port,port,port. 462 | var hosts []string 463 | var ports []string 464 | for _, host := range strings.Split(url.Host, ",") { 465 | if host == "" { 466 | continue 467 | } 468 | if isIPOnly(host) { 469 | hosts = append(hosts, strings.Trim(host, "[]")) 470 | continue 471 | } 472 | h, p, err := net.SplitHostPort(host) 473 | if err != nil { 474 | return nil, fmt.Errorf("failed to split host:port in '%s', err: %w", host, err) 475 | } 476 | if h != "" { 477 | hosts = append(hosts, h) 478 | } 479 | if p != "" { 480 | ports = append(ports, p) 481 | } 482 | } 483 | if len(hosts) > 0 { 484 | settings["host"] = strings.Join(hosts, ",") 485 | } 486 | if len(ports) > 0 { 487 | settings["port"] = strings.Join(ports, ",") 488 | } 489 | 490 | database := strings.TrimLeft(url.Path, "/") 491 | if database != "" { 492 | settings["database"] = database 493 | } 494 | 495 | nameMap := map[string]string{ 496 | "dbname": "database", 497 | } 498 | 499 | for k, v := range url.Query() { 500 | if k2, present := nameMap[k]; present { 501 | k = k2 502 | } 503 | 504 | settings[k] = v[0] 505 | } 506 | 507 | return settings, nil 508 | } 509 | 510 | func isIPOnly(host string) bool { 511 | return net.ParseIP(strings.Trim(host, "[]")) != nil || !strings.Contains(host, ":") 512 | } 513 | 514 | var asciiSpace = [256]uint8{'\t': 1, '\n': 1, '\v': 1, '\f': 1, '\r': 1, ' ': 1} 515 | 516 | func parseDSNSettings(s string) (map[string]string, error) { 517 | settings := make(map[string]string) 518 | 519 | nameMap := map[string]string{ 520 | "dbname": "database", 521 | } 522 | 523 | for len(s) > 0 { 524 | var key, val string 525 | eqIdx := strings.IndexRune(s, '=') 526 | if eqIdx < 0 { 527 | return nil, errors.New("invalid dsn") 528 | } 529 | 530 | key = strings.Trim(s[:eqIdx], " \t\n\r\v\f") 531 | s = strings.TrimLeft(s[eqIdx+1:], " \t\n\r\v\f") 532 | if len(s) == 0 { 533 | } else if s[0] != '\'' { 534 | end := 0 535 | for ; end < len(s); end++ { 536 | if asciiSpace[s[end]] == 1 { 537 | break 538 | } 539 | if s[end] == '\\' { 540 | end++ 541 | if end == len(s) { 542 | return nil, errors.New("invalid backslash") 543 | } 544 | } 545 | } 546 | val = strings.Replace(strings.Replace(s[:end], "\\\\", "\\", -1), "\\'", "'", -1) 547 | if end == len(s) { 548 | s = "" 549 | } else { 550 | s = s[end+1:] 551 | } 552 | } else { // quoted string 553 | s = s[1:] 554 | end := 0 555 | for ; end < len(s); end++ { 556 | if s[end] == '\'' { 557 | break 558 | } 559 | if s[end] == '\\' { 560 | end++ 561 | } 562 | } 563 | if end == len(s) { 564 | return nil, errors.New("unterminated quoted string in connection info string") 565 | } 566 | val = strings.Replace(strings.Replace(s[:end], "\\\\", "\\", -1), "\\'", "'", -1) 567 | if end == len(s) { 568 | s = "" 569 | } else { 570 | s = s[end+1:] 571 | } 572 | } 573 | 574 | if k, ok := nameMap[key]; ok { 575 | key = k 576 | } 577 | 578 | if key == "" { 579 | return nil, errors.New("invalid dsn") 580 | } 581 | 582 | settings[key] = val 583 | } 584 | 585 | return settings, nil 586 | } 587 | 588 | func parseServiceSettings(servicefilePath, serviceName string) (map[string]string, error) { 589 | servicefile, err := pgservicefile.ReadServicefile(servicefilePath) 590 | if err != nil { 591 | return nil, fmt.Errorf("failed to read service file: %v", servicefilePath) 592 | } 593 | 594 | service, err := servicefile.GetService(serviceName) 595 | if err != nil { 596 | return nil, fmt.Errorf("unable to find service: %v", serviceName) 597 | } 598 | 599 | nameMap := map[string]string{ 600 | "dbname": "database", 601 | } 602 | 603 | settings := make(map[string]string, len(service.Settings)) 604 | for k, v := range service.Settings { 605 | if k2, present := nameMap[k]; present { 606 | k = k2 607 | } 608 | settings[k] = v 609 | } 610 | 611 | return settings, nil 612 | } 613 | 614 | // configTLS uses libpq's TLS parameters to construct []*tls.Config. It is 615 | // necessary to allow returning multiple TLS configs as sslmode "allow" and 616 | // "prefer" allow fallback. 617 | func configTLS(settings map[string]string, thisHost string, parseConfigOptions ParseConfigOptions) ([]*tls.Config, error) { 618 | host := thisHost 619 | sslmode := settings["sslmode"] 620 | sslrootcert := settings["sslrootcert"] 621 | sslcert := settings["sslcert"] 622 | sslkey := settings["sslkey"] 623 | sslpassword := settings["sslpassword"] 624 | sslsni := settings["sslsni"] 625 | 626 | // Match libpq default behavior 627 | if sslmode == "" { 628 | sslmode = "prefer" 629 | } 630 | if sslsni == "" { 631 | sslsni = "1" 632 | } 633 | 634 | tlsConfig := &tls.Config{} 635 | 636 | switch sslmode { 637 | case "disable": 638 | return []*tls.Config{nil}, nil 639 | case "allow", "prefer": 640 | tlsConfig.InsecureSkipVerify = true 641 | case "require": 642 | // According to PostgreSQL documentation, if a root CA file exists, 643 | // the behavior of sslmode=require should be the same as that of verify-ca 644 | // 645 | // See https://www.postgresql.org/docs/12/libpq-ssl.html 646 | if sslrootcert != "" { 647 | goto nextCase 648 | } 649 | tlsConfig.InsecureSkipVerify = true 650 | break 651 | nextCase: 652 | fallthrough 653 | case "verify-ca": 654 | // Don't perform the default certificate verification because it 655 | // will verify the hostname. Instead, verify the server's 656 | // certificate chain ourselves in VerifyPeerCertificate and 657 | // ignore the server name. This emulates libpq's verify-ca 658 | // behavior. 659 | // 660 | // See https://github.com/golang/go/issues/21971#issuecomment-332693931 661 | // and https://pkg.go.dev/crypto/tls?tab=doc#example-Config-VerifyPeerCertificate 662 | // for more info. 663 | tlsConfig.InsecureSkipVerify = true 664 | tlsConfig.VerifyPeerCertificate = func(certificates [][]byte, _ [][]*x509.Certificate) error { 665 | certs := make([]*x509.Certificate, len(certificates)) 666 | for i, asn1Data := range certificates { 667 | cert, err := x509.ParseCertificate(asn1Data) 668 | if err != nil { 669 | return errors.New("failed to parse certificate from server: " + err.Error()) 670 | } 671 | certs[i] = cert 672 | } 673 | 674 | // Leave DNSName empty to skip hostname verification. 675 | opts := x509.VerifyOptions{ 676 | Roots: tlsConfig.RootCAs, 677 | Intermediates: x509.NewCertPool(), 678 | } 679 | // Skip the first cert because it's the leaf. All others 680 | // are intermediates. 681 | for _, cert := range certs[1:] { 682 | opts.Intermediates.AddCert(cert) 683 | } 684 | _, err := certs[0].Verify(opts) 685 | return err 686 | } 687 | case "verify-full": 688 | tlsConfig.ServerName = host 689 | default: 690 | return nil, errors.New("sslmode is invalid") 691 | } 692 | 693 | if sslrootcert != "" { 694 | caCertPool := x509.NewCertPool() 695 | 696 | caPath := sslrootcert 697 | caCert, err := ioutil.ReadFile(caPath) 698 | if err != nil { 699 | return nil, fmt.Errorf("unable to read CA file: %w", err) 700 | } 701 | 702 | if !caCertPool.AppendCertsFromPEM(caCert) { 703 | return nil, errors.New("unable to add CA to cert pool") 704 | } 705 | 706 | tlsConfig.RootCAs = caCertPool 707 | tlsConfig.ClientCAs = caCertPool 708 | } 709 | 710 | if (sslcert != "" && sslkey == "") || (sslcert == "" && sslkey != "") { 711 | return nil, errors.New(`both "sslcert" and "sslkey" are required`) 712 | } 713 | 714 | if sslcert != "" && sslkey != "" { 715 | buf, err := ioutil.ReadFile(sslkey) 716 | if err != nil { 717 | return nil, fmt.Errorf("unable to read sslkey: %w", err) 718 | } 719 | block, _ := pem.Decode(buf) 720 | if block == nil { 721 | return nil, errors.New("failed to decode sslkey") 722 | } 723 | var pemKey []byte 724 | var decryptedKey []byte 725 | var decryptedError error 726 | // If PEM is encrypted, attempt to decrypt using pass phrase 727 | if x509.IsEncryptedPEMBlock(block) { 728 | // Attempt decryption with pass phrase 729 | // NOTE: only supports RSA (PKCS#1) 730 | if sslpassword != "" { 731 | decryptedKey, decryptedError = x509.DecryptPEMBlock(block, []byte(sslpassword)) 732 | } 733 | //if sslpassword not provided or has decryption error when use it 734 | //try to find sslpassword with callback function 735 | if sslpassword == "" || decryptedError != nil { 736 | if parseConfigOptions.GetSSLPassword != nil { 737 | sslpassword = parseConfigOptions.GetSSLPassword(context.Background()) 738 | } 739 | if sslpassword == "" { 740 | return nil, fmt.Errorf("unable to find sslpassword") 741 | } 742 | } 743 | decryptedKey, decryptedError = x509.DecryptPEMBlock(block, []byte(sslpassword)) 744 | // Should we also provide warning for PKCS#1 needed? 745 | if decryptedError != nil { 746 | return nil, fmt.Errorf("unable to decrypt key: %w", err) 747 | } 748 | 749 | pemBytes := pem.Block{ 750 | Type: "RSA PRIVATE KEY", 751 | Bytes: decryptedKey, 752 | } 753 | pemKey = pem.EncodeToMemory(&pemBytes) 754 | } else { 755 | pemKey = pem.EncodeToMemory(block) 756 | } 757 | certfile, err := ioutil.ReadFile(sslcert) 758 | if err != nil { 759 | return nil, fmt.Errorf("unable to read cert: %w", err) 760 | } 761 | cert, err := tls.X509KeyPair(certfile, pemKey) 762 | if err != nil { 763 | return nil, fmt.Errorf("unable to load cert: %w", err) 764 | } 765 | tlsConfig.Certificates = []tls.Certificate{cert} 766 | } 767 | 768 | // Set Server Name Indication (SNI), if enabled by connection parameters. 769 | // Per RFC 6066, do not set it if the host is a literal IP address (IPv4 770 | // or IPv6). 771 | if sslsni == "1" && net.ParseIP(host) == nil { 772 | tlsConfig.ServerName = host 773 | } 774 | 775 | switch sslmode { 776 | case "allow": 777 | return []*tls.Config{nil, tlsConfig}, nil 778 | case "prefer": 779 | return []*tls.Config{tlsConfig, nil}, nil 780 | case "require", "verify-ca", "verify-full": 781 | return []*tls.Config{tlsConfig}, nil 782 | default: 783 | panic("BUG: bad sslmode should already have been caught") 784 | } 785 | } 786 | 787 | func parsePort(s string) (uint16, error) { 788 | port, err := strconv.ParseUint(s, 10, 16) 789 | if err != nil { 790 | return 0, err 791 | } 792 | if port < 1 || port > math.MaxUint16 { 793 | return 0, errors.New("outside range") 794 | } 795 | return uint16(port), nil 796 | } 797 | 798 | func makeDefaultDialer() *net.Dialer { 799 | return &net.Dialer{KeepAlive: 5 * time.Minute} 800 | } 801 | 802 | func makeDefaultResolver() *net.Resolver { 803 | return net.DefaultResolver 804 | } 805 | 806 | func makeDefaultBuildFrontendFunc(minBufferLen int) BuildFrontendFunc { 807 | return func(r io.Reader, w io.Writer) Frontend { 808 | cr, err := chunkreader.NewConfig(r, chunkreader.Config{MinBufLen: minBufferLen}) 809 | if err != nil { 810 | panic(fmt.Sprintf("BUG: chunkreader.NewConfig failed: %v", err)) 811 | } 812 | frontend := pgproto3.NewFrontend(cr, w) 813 | 814 | return frontend 815 | } 816 | } 817 | 818 | func parseConnectTimeoutSetting(s string) (time.Duration, error) { 819 | timeout, err := strconv.ParseInt(s, 10, 64) 820 | if err != nil { 821 | return 0, err 822 | } 823 | if timeout < 0 { 824 | return 0, errors.New("negative timeout") 825 | } 826 | return time.Duration(timeout) * time.Second, nil 827 | } 828 | 829 | func makeConnectTimeoutDialFunc(timeout time.Duration) DialFunc { 830 | d := makeDefaultDialer() 831 | d.Timeout = timeout 832 | return d.DialContext 833 | } 834 | 835 | // ValidateConnectTargetSessionAttrsReadWrite is an ValidateConnectFunc that implements libpq compatible 836 | // target_session_attrs=read-write. 837 | func ValidateConnectTargetSessionAttrsReadWrite(ctx context.Context, pgConn *PgConn) error { 838 | result := pgConn.ExecParams(ctx, "show transaction_read_only", nil, nil, nil, nil).Read() 839 | if result.Err != nil { 840 | return result.Err 841 | } 842 | 843 | if string(result.Rows[0][0]) == "on" { 844 | return errors.New("read only connection") 845 | } 846 | 847 | return nil 848 | } 849 | 850 | // ValidateConnectTargetSessionAttrsReadOnly is an ValidateConnectFunc that implements libpq compatible 851 | // target_session_attrs=read-only. 852 | func ValidateConnectTargetSessionAttrsReadOnly(ctx context.Context, pgConn *PgConn) error { 853 | result := pgConn.ExecParams(ctx, "show transaction_read_only", nil, nil, nil, nil).Read() 854 | if result.Err != nil { 855 | return result.Err 856 | } 857 | 858 | if string(result.Rows[0][0]) != "on" { 859 | return errors.New("connection is not read only") 860 | } 861 | 862 | return nil 863 | } 864 | 865 | // ValidateConnectTargetSessionAttrsStandby is an ValidateConnectFunc that implements libpq compatible 866 | // target_session_attrs=standby. 867 | func ValidateConnectTargetSessionAttrsStandby(ctx context.Context, pgConn *PgConn) error { 868 | result := pgConn.ExecParams(ctx, "select pg_is_in_recovery()", nil, nil, nil, nil).Read() 869 | if result.Err != nil { 870 | return result.Err 871 | } 872 | 873 | if string(result.Rows[0][0]) != "t" { 874 | return errors.New("server is not in hot standby mode") 875 | } 876 | 877 | return nil 878 | } 879 | 880 | // ValidateConnectTargetSessionAttrsPrimary is an ValidateConnectFunc that implements libpq compatible 881 | // target_session_attrs=primary. 882 | func ValidateConnectTargetSessionAttrsPrimary(ctx context.Context, pgConn *PgConn) error { 883 | result := pgConn.ExecParams(ctx, "select pg_is_in_recovery()", nil, nil, nil, nil).Read() 884 | if result.Err != nil { 885 | return result.Err 886 | } 887 | 888 | if string(result.Rows[0][0]) == "t" { 889 | return errors.New("server is in standby mode") 890 | } 891 | 892 | return nil 893 | } 894 | 895 | // ValidateConnectTargetSessionAttrsPreferStandby is an ValidateConnectFunc that implements libpq compatible 896 | // target_session_attrs=prefer-standby. 897 | func ValidateConnectTargetSessionAttrsPreferStandby(ctx context.Context, pgConn *PgConn) error { 898 | result := pgConn.ExecParams(ctx, "select pg_is_in_recovery()", nil, nil, nil, nil).Read() 899 | if result.Err != nil { 900 | return result.Err 901 | } 902 | 903 | if string(result.Rows[0][0]) != "t" { 904 | return &NotPreferredError{err: errors.New("server is not in hot standby mode")} 905 | } 906 | 907 | return nil 908 | } 909 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package pgconn_test 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "os/user" 10 | "runtime" 11 | "strings" 12 | "testing" 13 | "time" 14 | 15 | "github.com/jackc/pgconn" 16 | "github.com/stretchr/testify/assert" 17 | "github.com/stretchr/testify/require" 18 | ) 19 | 20 | func TestParseConfig(t *testing.T) { 21 | t.Parallel() 22 | 23 | var osUserName string 24 | osUser, err := user.Current() 25 | if err == nil { 26 | // Windows gives us the username here as `DOMAIN\user` or `LOCALPCNAME\user`, 27 | // but the libpq default is just the `user` portion, so we strip off the first part. 28 | if runtime.GOOS == "windows" && strings.Contains(osUser.Username, "\\") { 29 | osUserName = osUser.Username[strings.LastIndex(osUser.Username, "\\")+1:] 30 | } else { 31 | osUserName = osUser.Username 32 | } 33 | } 34 | 35 | config, err := pgconn.ParseConfig("") 36 | require.NoError(t, err) 37 | defaultHost := config.Host 38 | 39 | tests := []struct { 40 | name string 41 | connString string 42 | config *pgconn.Config 43 | }{ 44 | // Test all sslmodes 45 | { 46 | name: "sslmode not set (prefer)", 47 | connString: "postgres://jack:secret@localhost:5432/mydb", 48 | config: &pgconn.Config{ 49 | User: "jack", 50 | Password: "secret", 51 | Host: "localhost", 52 | Port: 5432, 53 | Database: "mydb", 54 | TLSConfig: &tls.Config{ 55 | InsecureSkipVerify: true, 56 | ServerName: "localhost", 57 | }, 58 | RuntimeParams: map[string]string{}, 59 | Fallbacks: []*pgconn.FallbackConfig{ 60 | &pgconn.FallbackConfig{ 61 | Host: "localhost", 62 | Port: 5432, 63 | TLSConfig: nil, 64 | }, 65 | }, 66 | }, 67 | }, 68 | { 69 | name: "sslmode disable", 70 | connString: "postgres://jack:secret@localhost:5432/mydb?sslmode=disable", 71 | config: &pgconn.Config{ 72 | User: "jack", 73 | Password: "secret", 74 | Host: "localhost", 75 | Port: 5432, 76 | Database: "mydb", 77 | TLSConfig: nil, 78 | RuntimeParams: map[string]string{}, 79 | }, 80 | }, 81 | { 82 | name: "sslmode allow", 83 | connString: "postgres://jack:secret@localhost:5432/mydb?sslmode=allow", 84 | config: &pgconn.Config{ 85 | User: "jack", 86 | Password: "secret", 87 | Host: "localhost", 88 | Port: 5432, 89 | Database: "mydb", 90 | TLSConfig: nil, 91 | RuntimeParams: map[string]string{}, 92 | Fallbacks: []*pgconn.FallbackConfig{ 93 | &pgconn.FallbackConfig{ 94 | Host: "localhost", 95 | Port: 5432, 96 | TLSConfig: &tls.Config{ 97 | InsecureSkipVerify: true, 98 | ServerName: "localhost", 99 | }, 100 | }, 101 | }, 102 | }, 103 | }, 104 | { 105 | name: "sslmode prefer", 106 | connString: "postgres://jack:secret@localhost:5432/mydb?sslmode=prefer", 107 | config: &pgconn.Config{ 108 | 109 | User: "jack", 110 | Password: "secret", 111 | Host: "localhost", 112 | Port: 5432, 113 | Database: "mydb", 114 | TLSConfig: &tls.Config{ 115 | InsecureSkipVerify: true, 116 | ServerName: "localhost", 117 | }, 118 | RuntimeParams: map[string]string{}, 119 | Fallbacks: []*pgconn.FallbackConfig{ 120 | &pgconn.FallbackConfig{ 121 | Host: "localhost", 122 | Port: 5432, 123 | TLSConfig: nil, 124 | }, 125 | }, 126 | }, 127 | }, 128 | { 129 | name: "sslmode require", 130 | connString: "postgres://jack:secret@localhost:5432/mydb?sslmode=require", 131 | config: &pgconn.Config{ 132 | User: "jack", 133 | Password: "secret", 134 | Host: "localhost", 135 | Port: 5432, 136 | Database: "mydb", 137 | TLSConfig: &tls.Config{ 138 | InsecureSkipVerify: true, 139 | ServerName: "localhost", 140 | }, 141 | RuntimeParams: map[string]string{}, 142 | }, 143 | }, 144 | { 145 | name: "sslmode verify-ca", 146 | connString: "postgres://jack:secret@localhost:5432/mydb?sslmode=verify-ca", 147 | config: &pgconn.Config{ 148 | User: "jack", 149 | Password: "secret", 150 | Host: "localhost", 151 | Port: 5432, 152 | Database: "mydb", 153 | TLSConfig: &tls.Config{ 154 | InsecureSkipVerify: true, 155 | ServerName: "localhost", 156 | }, 157 | RuntimeParams: map[string]string{}, 158 | }, 159 | }, 160 | { 161 | name: "sslmode verify-full", 162 | connString: "postgres://jack:secret@localhost:5432/mydb?sslmode=verify-full", 163 | config: &pgconn.Config{ 164 | User: "jack", 165 | Password: "secret", 166 | Host: "localhost", 167 | Port: 5432, 168 | Database: "mydb", 169 | TLSConfig: &tls.Config{ServerName: "localhost"}, 170 | RuntimeParams: map[string]string{}, 171 | }, 172 | }, 173 | { 174 | name: "database url everything", 175 | connString: "postgres://jack:secret@localhost:5432/mydb?sslmode=disable&application_name=pgxtest&search_path=myschema&connect_timeout=5", 176 | config: &pgconn.Config{ 177 | User: "jack", 178 | Password: "secret", 179 | Host: "localhost", 180 | Port: 5432, 181 | Database: "mydb", 182 | TLSConfig: nil, 183 | ConnectTimeout: 5 * time.Second, 184 | RuntimeParams: map[string]string{ 185 | "application_name": "pgxtest", 186 | "search_path": "myschema", 187 | }, 188 | }, 189 | }, 190 | { 191 | name: "database url missing password", 192 | connString: "postgres://jack@localhost:5432/mydb?sslmode=disable", 193 | config: &pgconn.Config{ 194 | User: "jack", 195 | Host: "localhost", 196 | Port: 5432, 197 | Database: "mydb", 198 | TLSConfig: nil, 199 | RuntimeParams: map[string]string{}, 200 | }, 201 | }, 202 | { 203 | name: "database url missing user and password", 204 | connString: "postgres://localhost:5432/mydb?sslmode=disable", 205 | config: &pgconn.Config{ 206 | User: osUserName, 207 | Host: "localhost", 208 | Port: 5432, 209 | Database: "mydb", 210 | TLSConfig: nil, 211 | RuntimeParams: map[string]string{}, 212 | }, 213 | }, 214 | { 215 | name: "database url missing port", 216 | connString: "postgres://jack:secret@localhost:5432/mydb?sslmode=disable", 217 | config: &pgconn.Config{ 218 | User: "jack", 219 | Password: "secret", 220 | Host: "localhost", 221 | Port: 5432, 222 | Database: "mydb", 223 | TLSConfig: nil, 224 | RuntimeParams: map[string]string{}, 225 | }, 226 | }, 227 | { 228 | name: "database url unix domain socket host", 229 | connString: "postgres:///foo?host=/tmp", 230 | config: &pgconn.Config{ 231 | User: osUserName, 232 | Host: "/tmp", 233 | Port: 5432, 234 | Database: "foo", 235 | TLSConfig: nil, 236 | RuntimeParams: map[string]string{}, 237 | }, 238 | }, 239 | { 240 | name: "database url unix domain socket host on windows", 241 | connString: "postgres:///foo?host=C:\\tmp", 242 | config: &pgconn.Config{ 243 | User: osUserName, 244 | Host: "C:\\tmp", 245 | Port: 5432, 246 | Database: "foo", 247 | TLSConfig: nil, 248 | RuntimeParams: map[string]string{}, 249 | }, 250 | }, 251 | { 252 | name: "database url dbname", 253 | connString: "postgres://localhost/?dbname=foo&sslmode=disable", 254 | config: &pgconn.Config{ 255 | User: osUserName, 256 | Host: "localhost", 257 | Port: 5432, 258 | Database: "foo", 259 | TLSConfig: nil, 260 | RuntimeParams: map[string]string{}, 261 | }, 262 | }, 263 | { 264 | name: "database url postgresql protocol", 265 | connString: "postgresql://jack@localhost:5432/mydb?sslmode=disable", 266 | config: &pgconn.Config{ 267 | User: "jack", 268 | Host: "localhost", 269 | Port: 5432, 270 | Database: "mydb", 271 | TLSConfig: nil, 272 | RuntimeParams: map[string]string{}, 273 | }, 274 | }, 275 | { 276 | name: "database url IPv4 with port", 277 | connString: "postgresql://jack@127.0.0.1:5433/mydb?sslmode=disable", 278 | config: &pgconn.Config{ 279 | User: "jack", 280 | Host: "127.0.0.1", 281 | Port: 5433, 282 | Database: "mydb", 283 | TLSConfig: nil, 284 | RuntimeParams: map[string]string{}, 285 | }, 286 | }, 287 | { 288 | name: "database url IPv6 with port", 289 | connString: "postgresql://jack@[2001:db8::1]:5433/mydb?sslmode=disable", 290 | config: &pgconn.Config{ 291 | User: "jack", 292 | Host: "2001:db8::1", 293 | Port: 5433, 294 | Database: "mydb", 295 | TLSConfig: nil, 296 | RuntimeParams: map[string]string{}, 297 | }, 298 | }, 299 | { 300 | name: "database url IPv6 no port", 301 | connString: "postgresql://jack@[2001:db8::1]/mydb?sslmode=disable", 302 | config: &pgconn.Config{ 303 | User: "jack", 304 | Host: "2001:db8::1", 305 | Port: 5432, 306 | Database: "mydb", 307 | TLSConfig: nil, 308 | RuntimeParams: map[string]string{}, 309 | }, 310 | }, 311 | { 312 | name: "DSN everything", 313 | connString: "user=jack password=secret host=localhost port=5432 dbname=mydb sslmode=disable application_name=pgxtest search_path=myschema connect_timeout=5", 314 | config: &pgconn.Config{ 315 | User: "jack", 316 | Password: "secret", 317 | Host: "localhost", 318 | Port: 5432, 319 | Database: "mydb", 320 | TLSConfig: nil, 321 | ConnectTimeout: 5 * time.Second, 322 | RuntimeParams: map[string]string{ 323 | "application_name": "pgxtest", 324 | "search_path": "myschema", 325 | }, 326 | }, 327 | }, 328 | { 329 | name: "DSN with escaped single quote", 330 | connString: "user=jack\\'s password=secret host=localhost port=5432 dbname=mydb sslmode=disable", 331 | config: &pgconn.Config{ 332 | User: "jack's", 333 | Password: "secret", 334 | Host: "localhost", 335 | Port: 5432, 336 | Database: "mydb", 337 | TLSConfig: nil, 338 | RuntimeParams: map[string]string{}, 339 | }, 340 | }, 341 | { 342 | name: "DSN with escaped backslash", 343 | connString: "user=jack password=sooper\\\\secret host=localhost port=5432 dbname=mydb sslmode=disable", 344 | config: &pgconn.Config{ 345 | User: "jack", 346 | Password: "sooper\\secret", 347 | Host: "localhost", 348 | Port: 5432, 349 | Database: "mydb", 350 | TLSConfig: nil, 351 | RuntimeParams: map[string]string{}, 352 | }, 353 | }, 354 | { 355 | name: "DSN with single quoted values", 356 | connString: "user='jack' host='localhost' dbname='mydb' sslmode='disable'", 357 | config: &pgconn.Config{ 358 | User: "jack", 359 | Host: "localhost", 360 | Port: 5432, 361 | Database: "mydb", 362 | TLSConfig: nil, 363 | RuntimeParams: map[string]string{}, 364 | }, 365 | }, 366 | { 367 | name: "DSN with single quoted value with escaped single quote", 368 | connString: "user='jack\\'s' host='localhost' dbname='mydb' sslmode='disable'", 369 | config: &pgconn.Config{ 370 | User: "jack's", 371 | Host: "localhost", 372 | Port: 5432, 373 | Database: "mydb", 374 | TLSConfig: nil, 375 | RuntimeParams: map[string]string{}, 376 | }, 377 | }, 378 | { 379 | name: "DSN with empty single quoted value", 380 | connString: "user='jack' password='' host='localhost' dbname='mydb' sslmode='disable'", 381 | config: &pgconn.Config{ 382 | User: "jack", 383 | Host: "localhost", 384 | Port: 5432, 385 | Database: "mydb", 386 | TLSConfig: nil, 387 | RuntimeParams: map[string]string{}, 388 | }, 389 | }, 390 | { 391 | name: "DSN with space between key and value", 392 | connString: "user = 'jack' password = '' host = 'localhost' dbname = 'mydb' sslmode='disable'", 393 | config: &pgconn.Config{ 394 | User: "jack", 395 | Host: "localhost", 396 | Port: 5432, 397 | Database: "mydb", 398 | TLSConfig: nil, 399 | RuntimeParams: map[string]string{}, 400 | }, 401 | }, 402 | { 403 | name: "URL multiple hosts", 404 | connString: "postgres://jack:secret@foo,bar,baz/mydb?sslmode=disable", 405 | config: &pgconn.Config{ 406 | User: "jack", 407 | Password: "secret", 408 | Host: "foo", 409 | Port: 5432, 410 | Database: "mydb", 411 | TLSConfig: nil, 412 | RuntimeParams: map[string]string{}, 413 | Fallbacks: []*pgconn.FallbackConfig{ 414 | &pgconn.FallbackConfig{ 415 | Host: "bar", 416 | Port: 5432, 417 | TLSConfig: nil, 418 | }, 419 | &pgconn.FallbackConfig{ 420 | Host: "baz", 421 | Port: 5432, 422 | TLSConfig: nil, 423 | }, 424 | }, 425 | }, 426 | }, 427 | { 428 | name: "URL multiple hosts and ports", 429 | connString: "postgres://jack:secret@foo:1,bar:2,baz:3/mydb?sslmode=disable", 430 | config: &pgconn.Config{ 431 | User: "jack", 432 | Password: "secret", 433 | Host: "foo", 434 | Port: 1, 435 | Database: "mydb", 436 | TLSConfig: nil, 437 | RuntimeParams: map[string]string{}, 438 | Fallbacks: []*pgconn.FallbackConfig{ 439 | &pgconn.FallbackConfig{ 440 | Host: "bar", 441 | Port: 2, 442 | TLSConfig: nil, 443 | }, 444 | &pgconn.FallbackConfig{ 445 | Host: "baz", 446 | Port: 3, 447 | TLSConfig: nil, 448 | }, 449 | }, 450 | }, 451 | }, 452 | // https://github.com/jackc/pgconn/issues/72 453 | { 454 | name: "URL without host but with port still uses default host", 455 | connString: "postgres://jack:secret@:1/mydb?sslmode=disable", 456 | config: &pgconn.Config{ 457 | User: "jack", 458 | Password: "secret", 459 | Host: defaultHost, 460 | Port: 1, 461 | Database: "mydb", 462 | TLSConfig: nil, 463 | RuntimeParams: map[string]string{}, 464 | }, 465 | }, 466 | { 467 | name: "DSN multiple hosts one port", 468 | connString: "user=jack password=secret host=foo,bar,baz port=5432 dbname=mydb sslmode=disable", 469 | config: &pgconn.Config{ 470 | User: "jack", 471 | Password: "secret", 472 | Host: "foo", 473 | Port: 5432, 474 | Database: "mydb", 475 | TLSConfig: nil, 476 | RuntimeParams: map[string]string{}, 477 | Fallbacks: []*pgconn.FallbackConfig{ 478 | &pgconn.FallbackConfig{ 479 | Host: "bar", 480 | Port: 5432, 481 | TLSConfig: nil, 482 | }, 483 | &pgconn.FallbackConfig{ 484 | Host: "baz", 485 | Port: 5432, 486 | TLSConfig: nil, 487 | }, 488 | }, 489 | }, 490 | }, 491 | { 492 | name: "DSN multiple hosts multiple ports", 493 | connString: "user=jack password=secret host=foo,bar,baz port=1,2,3 dbname=mydb sslmode=disable", 494 | config: &pgconn.Config{ 495 | User: "jack", 496 | Password: "secret", 497 | Host: "foo", 498 | Port: 1, 499 | Database: "mydb", 500 | TLSConfig: nil, 501 | RuntimeParams: map[string]string{}, 502 | Fallbacks: []*pgconn.FallbackConfig{ 503 | &pgconn.FallbackConfig{ 504 | Host: "bar", 505 | Port: 2, 506 | TLSConfig: nil, 507 | }, 508 | &pgconn.FallbackConfig{ 509 | Host: "baz", 510 | Port: 3, 511 | TLSConfig: nil, 512 | }, 513 | }, 514 | }, 515 | }, 516 | { 517 | name: "multiple hosts and fallback tsl", 518 | connString: "user=jack password=secret host=foo,bar,baz dbname=mydb sslmode=prefer", 519 | config: &pgconn.Config{ 520 | User: "jack", 521 | Password: "secret", 522 | Host: "foo", 523 | Port: 5432, 524 | Database: "mydb", 525 | TLSConfig: &tls.Config{ 526 | InsecureSkipVerify: true, 527 | ServerName: "foo", 528 | }, 529 | RuntimeParams: map[string]string{}, 530 | Fallbacks: []*pgconn.FallbackConfig{ 531 | &pgconn.FallbackConfig{ 532 | Host: "foo", 533 | Port: 5432, 534 | TLSConfig: nil, 535 | }, 536 | &pgconn.FallbackConfig{ 537 | Host: "bar", 538 | Port: 5432, 539 | TLSConfig: &tls.Config{ 540 | InsecureSkipVerify: true, 541 | ServerName: "bar", 542 | }}, 543 | &pgconn.FallbackConfig{ 544 | Host: "bar", 545 | Port: 5432, 546 | TLSConfig: nil, 547 | }, 548 | &pgconn.FallbackConfig{ 549 | Host: "baz", 550 | Port: 5432, 551 | TLSConfig: &tls.Config{ 552 | InsecureSkipVerify: true, 553 | ServerName: "baz", 554 | }}, 555 | &pgconn.FallbackConfig{ 556 | Host: "baz", 557 | Port: 5432, 558 | TLSConfig: nil, 559 | }, 560 | }, 561 | }, 562 | }, 563 | { 564 | name: "target_session_attrs read-write", 565 | connString: "postgres://jack:secret@localhost:5432/mydb?sslmode=disable&target_session_attrs=read-write", 566 | config: &pgconn.Config{ 567 | User: "jack", 568 | Password: "secret", 569 | Host: "localhost", 570 | Port: 5432, 571 | Database: "mydb", 572 | TLSConfig: nil, 573 | RuntimeParams: map[string]string{}, 574 | ValidateConnect: pgconn.ValidateConnectTargetSessionAttrsReadWrite, 575 | }, 576 | }, 577 | { 578 | name: "target_session_attrs read-only", 579 | connString: "postgres://jack:secret@localhost:5432/mydb?sslmode=disable&target_session_attrs=read-only", 580 | config: &pgconn.Config{ 581 | User: "jack", 582 | Password: "secret", 583 | Host: "localhost", 584 | Port: 5432, 585 | Database: "mydb", 586 | TLSConfig: nil, 587 | RuntimeParams: map[string]string{}, 588 | ValidateConnect: pgconn.ValidateConnectTargetSessionAttrsReadOnly, 589 | }, 590 | }, 591 | { 592 | name: "target_session_attrs primary", 593 | connString: "postgres://jack:secret@localhost:5432/mydb?sslmode=disable&target_session_attrs=primary", 594 | config: &pgconn.Config{ 595 | User: "jack", 596 | Password: "secret", 597 | Host: "localhost", 598 | Port: 5432, 599 | Database: "mydb", 600 | TLSConfig: nil, 601 | RuntimeParams: map[string]string{}, 602 | ValidateConnect: pgconn.ValidateConnectTargetSessionAttrsPrimary, 603 | }, 604 | }, 605 | { 606 | name: "target_session_attrs standby", 607 | connString: "postgres://jack:secret@localhost:5432/mydb?sslmode=disable&target_session_attrs=standby", 608 | config: &pgconn.Config{ 609 | User: "jack", 610 | Password: "secret", 611 | Host: "localhost", 612 | Port: 5432, 613 | Database: "mydb", 614 | TLSConfig: nil, 615 | RuntimeParams: map[string]string{}, 616 | ValidateConnect: pgconn.ValidateConnectTargetSessionAttrsStandby, 617 | }, 618 | }, 619 | { 620 | name: "target_session_attrs prefer-standby", 621 | connString: "postgres://jack:secret@localhost:5432/mydb?sslmode=disable&target_session_attrs=prefer-standby", 622 | config: &pgconn.Config{ 623 | User: "jack", 624 | Password: "secret", 625 | Host: "localhost", 626 | Port: 5432, 627 | Database: "mydb", 628 | TLSConfig: nil, 629 | RuntimeParams: map[string]string{}, 630 | ValidateConnect: pgconn.ValidateConnectTargetSessionAttrsPreferStandby, 631 | }, 632 | }, 633 | { 634 | name: "target_session_attrs any", 635 | connString: "postgres://jack:secret@localhost:5432/mydb?sslmode=disable&target_session_attrs=any", 636 | config: &pgconn.Config{ 637 | User: "jack", 638 | Password: "secret", 639 | Host: "localhost", 640 | Port: 5432, 641 | Database: "mydb", 642 | TLSConfig: nil, 643 | RuntimeParams: map[string]string{}, 644 | }, 645 | }, 646 | { 647 | name: "target_session_attrs not set (any)", 648 | connString: "postgres://jack:secret@localhost:5432/mydb?sslmode=disable", 649 | config: &pgconn.Config{ 650 | User: "jack", 651 | Password: "secret", 652 | Host: "localhost", 653 | Port: 5432, 654 | Database: "mydb", 655 | TLSConfig: nil, 656 | RuntimeParams: map[string]string{}, 657 | }, 658 | }, 659 | { 660 | name: "SNI is set by default", 661 | connString: "postgres://jack:secret@sni.test:5432/mydb?sslmode=require", 662 | config: &pgconn.Config{ 663 | User: "jack", 664 | Password: "secret", 665 | Host: "sni.test", 666 | Port: 5432, 667 | Database: "mydb", 668 | TLSConfig: &tls.Config{ 669 | InsecureSkipVerify: true, 670 | ServerName: "sni.test", 671 | }, 672 | RuntimeParams: map[string]string{}, 673 | }, 674 | }, 675 | { 676 | name: "SNI is not set for IPv4", 677 | connString: "postgres://jack:secret@1.1.1.1:5432/mydb?sslmode=require", 678 | config: &pgconn.Config{ 679 | User: "jack", 680 | Password: "secret", 681 | Host: "1.1.1.1", 682 | Port: 5432, 683 | Database: "mydb", 684 | TLSConfig: &tls.Config{ 685 | InsecureSkipVerify: true, 686 | }, 687 | RuntimeParams: map[string]string{}, 688 | }, 689 | }, 690 | { 691 | name: "SNI is not set for IPv6", 692 | connString: "postgres://jack:secret@[::1]:5432/mydb?sslmode=require", 693 | config: &pgconn.Config{ 694 | User: "jack", 695 | Password: "secret", 696 | Host: "::1", 697 | Port: 5432, 698 | Database: "mydb", 699 | TLSConfig: &tls.Config{ 700 | InsecureSkipVerify: true, 701 | }, 702 | RuntimeParams: map[string]string{}, 703 | }, 704 | }, 705 | { 706 | name: "SNI is not set when disabled (URL-style)", 707 | connString: "postgres://jack:secret@sni.test:5432/mydb?sslmode=require&sslsni=0", 708 | config: &pgconn.Config{ 709 | User: "jack", 710 | Password: "secret", 711 | Host: "sni.test", 712 | Port: 5432, 713 | Database: "mydb", 714 | TLSConfig: &tls.Config{ 715 | InsecureSkipVerify: true, 716 | }, 717 | RuntimeParams: map[string]string{}, 718 | }, 719 | }, 720 | { 721 | name: "SNI is not set when disabled (key/value style)", 722 | connString: "user=jack password=secret host=sni.test dbname=mydb sslmode=require sslsni=0", 723 | config: &pgconn.Config{ 724 | User: "jack", 725 | Password: "secret", 726 | Host: "sni.test", 727 | Port: 5432, 728 | Database: "mydb", 729 | TLSConfig: &tls.Config{ 730 | InsecureSkipVerify: true, 731 | }, 732 | RuntimeParams: map[string]string{}, 733 | }, 734 | }, 735 | } 736 | 737 | for i, tt := range tests { 738 | config, err := pgconn.ParseConfig(tt.connString) 739 | if !assert.Nilf(t, err, "Test %d (%s)", i, tt.name) { 740 | continue 741 | } 742 | 743 | assertConfigsEqual(t, tt.config, config, fmt.Sprintf("Test %d (%s)", i, tt.name)) 744 | } 745 | } 746 | 747 | // https://github.com/jackc/pgconn/issues/47 748 | func TestParseConfigDSNWithTrailingEmptyEqualDoesNotPanic(t *testing.T) { 749 | _, err := pgconn.ParseConfig("host= user= password= port= database=") 750 | require.NoError(t, err) 751 | } 752 | 753 | func TestParseConfigDSNLeadingEqual(t *testing.T) { 754 | _, err := pgconn.ParseConfig("= user=jack") 755 | require.Error(t, err) 756 | } 757 | 758 | // https://github.com/jackc/pgconn/issues/49 759 | func TestParseConfigDSNTrailingBackslash(t *testing.T) { 760 | _, err := pgconn.ParseConfig(`x=x\`) 761 | require.Error(t, err) 762 | assert.Contains(t, err.Error(), "invalid backslash") 763 | } 764 | 765 | func TestConfigCopyReturnsEqualConfig(t *testing.T) { 766 | connString := "postgres://jack:secret@localhost:5432/mydb?application_name=pgxtest&search_path=myschema&connect_timeout=5" 767 | original, err := pgconn.ParseConfig(connString) 768 | require.NoError(t, err) 769 | 770 | copied := original.Copy() 771 | assertConfigsEqual(t, original, copied, "Test Config.Copy() returns equal config") 772 | } 773 | 774 | func TestConfigCopyOriginalConfigDidNotChange(t *testing.T) { 775 | connString := "postgres://jack:secret@localhost:5432/mydb?application_name=pgxtest&search_path=myschema&connect_timeout=5&sslmode=prefer" 776 | original, err := pgconn.ParseConfig(connString) 777 | require.NoError(t, err) 778 | 779 | copied := original.Copy() 780 | assertConfigsEqual(t, original, copied, "Test Config.Copy() returns equal config") 781 | 782 | copied.Port = uint16(5433) 783 | copied.RuntimeParams["foo"] = "bar" 784 | copied.Fallbacks[0].Port = uint16(5433) 785 | 786 | assert.Equal(t, uint16(5432), original.Port) 787 | assert.Equal(t, "", original.RuntimeParams["foo"]) 788 | assert.Equal(t, uint16(5432), original.Fallbacks[0].Port) 789 | } 790 | 791 | func TestConfigCopyCanBeUsedToConnect(t *testing.T) { 792 | connString := os.Getenv("PGX_TEST_CONN_STRING") 793 | original, err := pgconn.ParseConfig(connString) 794 | require.NoError(t, err) 795 | 796 | copied := original.Copy() 797 | assert.NotPanics(t, func() { 798 | _, err = pgconn.ConnectConfig(context.Background(), copied) 799 | }) 800 | assert.NoError(t, err) 801 | } 802 | 803 | func TestNetworkAddress(t *testing.T) { 804 | tests := []struct { 805 | name string 806 | host string 807 | wantNet string 808 | }{ 809 | { 810 | name: "Default Unix socket address", 811 | host: "/var/run/postgresql", 812 | wantNet: "unix", 813 | }, 814 | { 815 | name: "Windows Unix socket address (standard drive name)", 816 | host: "C:\\tmp", 817 | wantNet: "unix", 818 | }, 819 | { 820 | name: "Windows Unix socket address (first drive name)", 821 | host: "A:\\tmp", 822 | wantNet: "unix", 823 | }, 824 | { 825 | name: "Windows Unix socket address (last drive name)", 826 | host: "Z:\\tmp", 827 | wantNet: "unix", 828 | }, 829 | { 830 | name: "Assume TCP for unknown formats", 831 | host: "a/tmp", 832 | wantNet: "tcp", 833 | }, 834 | { 835 | name: "loopback interface", 836 | host: "localhost", 837 | wantNet: "tcp", 838 | }, 839 | { 840 | name: "IP address", 841 | host: "127.0.0.1", 842 | wantNet: "tcp", 843 | }, 844 | } 845 | for i, tt := range tests { 846 | gotNet, _ := pgconn.NetworkAddress(tt.host, 5432) 847 | 848 | assert.Equalf(t, tt.wantNet, gotNet, "Test %d (%s)", i, tt.name) 849 | } 850 | } 851 | 852 | func assertConfigsEqual(t *testing.T, expected, actual *pgconn.Config, testName string) { 853 | if !assert.NotNil(t, expected) { 854 | return 855 | } 856 | if !assert.NotNil(t, actual) { 857 | return 858 | } 859 | 860 | assert.Equalf(t, expected.Host, actual.Host, "%s - Host", testName) 861 | assert.Equalf(t, expected.Database, actual.Database, "%s - Database", testName) 862 | assert.Equalf(t, expected.Port, actual.Port, "%s - Port", testName) 863 | assert.Equalf(t, expected.User, actual.User, "%s - User", testName) 864 | assert.Equalf(t, expected.Password, actual.Password, "%s - Password", testName) 865 | assert.Equalf(t, expected.ConnectTimeout, actual.ConnectTimeout, "%s - ConnectTimeout", testName) 866 | assert.Equalf(t, expected.RuntimeParams, actual.RuntimeParams, "%s - RuntimeParams", testName) 867 | 868 | // Can't test function equality, so just test that they are set or not. 869 | assert.Equalf(t, expected.ValidateConnect == nil, actual.ValidateConnect == nil, "%s - ValidateConnect", testName) 870 | assert.Equalf(t, expected.AfterConnect == nil, actual.AfterConnect == nil, "%s - AfterConnect", testName) 871 | 872 | if assert.Equalf(t, expected.TLSConfig == nil, actual.TLSConfig == nil, "%s - TLSConfig", testName) { 873 | if expected.TLSConfig != nil { 874 | assert.Equalf(t, expected.TLSConfig.InsecureSkipVerify, actual.TLSConfig.InsecureSkipVerify, "%s - TLSConfig InsecureSkipVerify", testName) 875 | assert.Equalf(t, expected.TLSConfig.ServerName, actual.TLSConfig.ServerName, "%s - TLSConfig ServerName", testName) 876 | } 877 | } 878 | 879 | if assert.Equalf(t, len(expected.Fallbacks), len(actual.Fallbacks), "%s - Fallbacks", testName) { 880 | for i := range expected.Fallbacks { 881 | assert.Equalf(t, expected.Fallbacks[i].Host, actual.Fallbacks[i].Host, "%s - Fallback %d - Host", testName, i) 882 | assert.Equalf(t, expected.Fallbacks[i].Port, actual.Fallbacks[i].Port, "%s - Fallback %d - Port", testName, i) 883 | 884 | if assert.Equalf(t, expected.Fallbacks[i].TLSConfig == nil, actual.Fallbacks[i].TLSConfig == nil, "%s - Fallback %d - TLSConfig", testName, i) { 885 | if expected.Fallbacks[i].TLSConfig != nil { 886 | assert.Equalf(t, expected.Fallbacks[i].TLSConfig.InsecureSkipVerify, actual.Fallbacks[i].TLSConfig.InsecureSkipVerify, "%s - Fallback %d - TLSConfig InsecureSkipVerify", testName) 887 | assert.Equalf(t, expected.Fallbacks[i].TLSConfig.ServerName, actual.Fallbacks[i].TLSConfig.ServerName, "%s - Fallback %d - TLSConfig ServerName", testName) 888 | } 889 | } 890 | } 891 | } 892 | } 893 | 894 | func TestParseConfigEnvLibpq(t *testing.T) { 895 | var osUserName string 896 | osUser, err := user.Current() 897 | if err == nil { 898 | // Windows gives us the username here as `DOMAIN\user` or `LOCALPCNAME\user`, 899 | // but the libpq default is just the `user` portion, so we strip off the first part. 900 | if runtime.GOOS == "windows" && strings.Contains(osUser.Username, "\\") { 901 | osUserName = osUser.Username[strings.LastIndex(osUser.Username, "\\")+1:] 902 | } else { 903 | osUserName = osUser.Username 904 | } 905 | } 906 | 907 | pgEnvvars := []string{"PGHOST", "PGPORT", "PGDATABASE", "PGUSER", "PGPASSWORD", "PGAPPNAME", "PGSSLMODE", "PGCONNECT_TIMEOUT", "PGSSLSNI"} 908 | 909 | savedEnv := make(map[string]string) 910 | for _, n := range pgEnvvars { 911 | savedEnv[n] = os.Getenv(n) 912 | } 913 | defer func() { 914 | for k, v := range savedEnv { 915 | err := os.Setenv(k, v) 916 | if err != nil { 917 | t.Fatalf("Unable to restore environment: %v", err) 918 | } 919 | } 920 | }() 921 | 922 | tests := []struct { 923 | name string 924 | envvars map[string]string 925 | config *pgconn.Config 926 | }{ 927 | { 928 | // not testing no environment at all as that would use default host and that can vary. 929 | name: "PGHOST only", 930 | envvars: map[string]string{"PGHOST": "123.123.123.123"}, 931 | config: &pgconn.Config{ 932 | User: osUserName, 933 | Host: "123.123.123.123", 934 | Port: 5432, 935 | TLSConfig: &tls.Config{ 936 | InsecureSkipVerify: true, 937 | }, 938 | RuntimeParams: map[string]string{}, 939 | Fallbacks: []*pgconn.FallbackConfig{ 940 | &pgconn.FallbackConfig{ 941 | Host: "123.123.123.123", 942 | Port: 5432, 943 | TLSConfig: nil, 944 | }, 945 | }, 946 | }, 947 | }, 948 | { 949 | name: "All non-TLS environment", 950 | envvars: map[string]string{ 951 | "PGHOST": "123.123.123.123", 952 | "PGPORT": "7777", 953 | "PGDATABASE": "foo", 954 | "PGUSER": "bar", 955 | "PGPASSWORD": "baz", 956 | "PGCONNECT_TIMEOUT": "10", 957 | "PGSSLMODE": "disable", 958 | "PGAPPNAME": "pgxtest", 959 | }, 960 | config: &pgconn.Config{ 961 | Host: "123.123.123.123", 962 | Port: 7777, 963 | Database: "foo", 964 | User: "bar", 965 | Password: "baz", 966 | ConnectTimeout: 10 * time.Second, 967 | TLSConfig: nil, 968 | RuntimeParams: map[string]string{"application_name": "pgxtest"}, 969 | }, 970 | }, 971 | { 972 | name: "SNI can be disabled via environment variable", 973 | envvars: map[string]string{ 974 | "PGHOST": "test.foo", 975 | "PGSSLMODE": "require", 976 | "PGSSLSNI": "0", 977 | }, 978 | config: &pgconn.Config{ 979 | User: osUserName, 980 | Host: "test.foo", 981 | Port: 5432, 982 | TLSConfig: &tls.Config{ 983 | InsecureSkipVerify: true, 984 | }, 985 | RuntimeParams: map[string]string{}, 986 | }, 987 | }, 988 | } 989 | 990 | for i, tt := range tests { 991 | for _, n := range pgEnvvars { 992 | err := os.Unsetenv(n) 993 | require.NoError(t, err) 994 | } 995 | 996 | for k, v := range tt.envvars { 997 | err := os.Setenv(k, v) 998 | require.NoError(t, err) 999 | } 1000 | 1001 | config, err := pgconn.ParseConfig("") 1002 | if !assert.Nilf(t, err, "Test %d (%s)", i, tt.name) { 1003 | continue 1004 | } 1005 | 1006 | assertConfigsEqual(t, tt.config, config, fmt.Sprintf("Test %d (%s)", i, tt.name)) 1007 | } 1008 | } 1009 | 1010 | func TestParseConfigReadsPgPassfile(t *testing.T) { 1011 | t.Parallel() 1012 | 1013 | tf, err := ioutil.TempFile("", "") 1014 | require.NoError(t, err) 1015 | 1016 | defer tf.Close() 1017 | defer os.Remove(tf.Name()) 1018 | 1019 | _, err = tf.Write([]byte("test1:5432:curlydb:curly:nyuknyuknyuk")) 1020 | require.NoError(t, err) 1021 | 1022 | connString := fmt.Sprintf("postgres://curly@test1:5432/curlydb?sslmode=disable&passfile=%s", tf.Name()) 1023 | expected := &pgconn.Config{ 1024 | User: "curly", 1025 | Password: "nyuknyuknyuk", 1026 | Host: "test1", 1027 | Port: 5432, 1028 | Database: "curlydb", 1029 | TLSConfig: nil, 1030 | RuntimeParams: map[string]string{}, 1031 | } 1032 | 1033 | actual, err := pgconn.ParseConfig(connString) 1034 | assert.NoError(t, err) 1035 | 1036 | assertConfigsEqual(t, expected, actual, "passfile") 1037 | } 1038 | 1039 | func TestParseConfigReadsPgServiceFile(t *testing.T) { 1040 | t.Parallel() 1041 | 1042 | tf, err := ioutil.TempFile("", "") 1043 | require.NoError(t, err) 1044 | 1045 | defer tf.Close() 1046 | defer os.Remove(tf.Name()) 1047 | 1048 | _, err = tf.Write([]byte(` 1049 | [abc] 1050 | host=abc.example.com 1051 | port=9999 1052 | dbname=abcdb 1053 | user=abcuser 1054 | 1055 | [def] 1056 | host = def.example.com 1057 | dbname = defdb 1058 | user = defuser 1059 | application_name = spaced string 1060 | `)) 1061 | require.NoError(t, err) 1062 | 1063 | tests := []struct { 1064 | name string 1065 | connString string 1066 | config *pgconn.Config 1067 | }{ 1068 | { 1069 | name: "abc", 1070 | connString: fmt.Sprintf("postgres:///?servicefile=%s&service=%s", tf.Name(), "abc"), 1071 | config: &pgconn.Config{ 1072 | Host: "abc.example.com", 1073 | Database: "abcdb", 1074 | User: "abcuser", 1075 | Port: 9999, 1076 | TLSConfig: &tls.Config{ 1077 | InsecureSkipVerify: true, 1078 | ServerName: "abc.example.com", 1079 | }, 1080 | RuntimeParams: map[string]string{}, 1081 | Fallbacks: []*pgconn.FallbackConfig{ 1082 | &pgconn.FallbackConfig{ 1083 | Host: "abc.example.com", 1084 | Port: 9999, 1085 | TLSConfig: nil, 1086 | }, 1087 | }, 1088 | }, 1089 | }, 1090 | { 1091 | name: "def", 1092 | connString: fmt.Sprintf("postgres:///?servicefile=%s&service=%s", tf.Name(), "def"), 1093 | config: &pgconn.Config{ 1094 | Host: "def.example.com", 1095 | Port: 5432, 1096 | Database: "defdb", 1097 | User: "defuser", 1098 | TLSConfig: &tls.Config{ 1099 | InsecureSkipVerify: true, 1100 | ServerName: "def.example.com", 1101 | }, 1102 | RuntimeParams: map[string]string{"application_name": "spaced string"}, 1103 | Fallbacks: []*pgconn.FallbackConfig{ 1104 | &pgconn.FallbackConfig{ 1105 | Host: "def.example.com", 1106 | Port: 5432, 1107 | TLSConfig: nil, 1108 | }, 1109 | }, 1110 | }, 1111 | }, 1112 | { 1113 | name: "conn string has precedence", 1114 | connString: fmt.Sprintf("postgres://other.example.com:7777/?servicefile=%s&service=%s&sslmode=disable", tf.Name(), "abc"), 1115 | config: &pgconn.Config{ 1116 | Host: "other.example.com", 1117 | Database: "abcdb", 1118 | User: "abcuser", 1119 | Port: 7777, 1120 | TLSConfig: nil, 1121 | RuntimeParams: map[string]string{}, 1122 | }, 1123 | }, 1124 | } 1125 | 1126 | for i, tt := range tests { 1127 | config, err := pgconn.ParseConfig(tt.connString) 1128 | if !assert.NoErrorf(t, err, "Test %d (%s)", i, tt.name) { 1129 | continue 1130 | } 1131 | 1132 | assertConfigsEqual(t, tt.config, config, fmt.Sprintf("Test %d (%s)", i, tt.name)) 1133 | } 1134 | } 1135 | 1136 | func TestParseConfigExtractsMinReadBufferSize(t *testing.T) { 1137 | t.Parallel() 1138 | 1139 | config, err := pgconn.ParseConfig("min_read_buffer_size=0") 1140 | require.NoError(t, err) 1141 | _, present := config.RuntimeParams["min_read_buffer_size"] 1142 | require.False(t, present) 1143 | 1144 | // The buffer size is internal so there isn't much that can be done to test it other than see that the runtime param 1145 | // was removed. 1146 | } 1147 | -------------------------------------------------------------------------------- /defaults.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package pgconn 5 | 6 | import ( 7 | "os" 8 | "os/user" 9 | "path/filepath" 10 | ) 11 | 12 | func defaultSettings() map[string]string { 13 | settings := make(map[string]string) 14 | 15 | settings["host"] = defaultHost() 16 | settings["port"] = "5432" 17 | 18 | // Default to the OS user name. Purposely ignoring err getting user name from 19 | // OS. The client application will simply have to specify the user in that 20 | // case (which they typically will be doing anyway). 21 | user, err := user.Current() 22 | if err == nil { 23 | settings["user"] = user.Username 24 | settings["passfile"] = filepath.Join(user.HomeDir, ".pgpass") 25 | settings["servicefile"] = filepath.Join(user.HomeDir, ".pg_service.conf") 26 | sslcert := filepath.Join(user.HomeDir, ".postgresql", "postgresql.crt") 27 | sslkey := filepath.Join(user.HomeDir, ".postgresql", "postgresql.key") 28 | if _, err := os.Stat(sslcert); err == nil { 29 | if _, err := os.Stat(sslkey); err == nil { 30 | // Both the cert and key must be present to use them, or do not use either 31 | settings["sslcert"] = sslcert 32 | settings["sslkey"] = sslkey 33 | } 34 | } 35 | sslrootcert := filepath.Join(user.HomeDir, ".postgresql", "root.crt") 36 | if _, err := os.Stat(sslrootcert); err == nil { 37 | settings["sslrootcert"] = sslrootcert 38 | } 39 | } 40 | 41 | settings["target_session_attrs"] = "any" 42 | 43 | settings["min_read_buffer_size"] = "8192" 44 | 45 | return settings 46 | } 47 | 48 | // defaultHost attempts to mimic libpq's default host. libpq uses the default unix socket location on *nix and localhost 49 | // on Windows. The default socket location is compiled into libpq. Since pgx does not have access to that default it 50 | // checks the existence of common locations. 51 | func defaultHost() string { 52 | candidatePaths := []string{ 53 | "/var/run/postgresql", // Debian 54 | "/private/tmp", // OSX - homebrew 55 | "/tmp", // standard PostgreSQL 56 | } 57 | 58 | for _, path := range candidatePaths { 59 | if _, err := os.Stat(path); err == nil { 60 | return path 61 | } 62 | } 63 | 64 | return "localhost" 65 | } 66 | -------------------------------------------------------------------------------- /defaults_windows.go: -------------------------------------------------------------------------------- 1 | package pgconn 2 | 3 | import ( 4 | "os" 5 | "os/user" 6 | "path/filepath" 7 | "strings" 8 | ) 9 | 10 | func defaultSettings() map[string]string { 11 | settings := make(map[string]string) 12 | 13 | settings["host"] = defaultHost() 14 | settings["port"] = "5432" 15 | 16 | // Default to the OS user name. Purposely ignoring err getting user name from 17 | // OS. The client application will simply have to specify the user in that 18 | // case (which they typically will be doing anyway). 19 | user, err := user.Current() 20 | appData := os.Getenv("APPDATA") 21 | if err == nil { 22 | // Windows gives us the username here as `DOMAIN\user` or `LOCALPCNAME\user`, 23 | // but the libpq default is just the `user` portion, so we strip off the first part. 24 | username := user.Username 25 | if strings.Contains(username, "\\") { 26 | username = username[strings.LastIndex(username, "\\")+1:] 27 | } 28 | 29 | settings["user"] = username 30 | settings["passfile"] = filepath.Join(appData, "postgresql", "pgpass.conf") 31 | settings["servicefile"] = filepath.Join(user.HomeDir, ".pg_service.conf") 32 | sslcert := filepath.Join(appData, "postgresql", "postgresql.crt") 33 | sslkey := filepath.Join(appData, "postgresql", "postgresql.key") 34 | if _, err := os.Stat(sslcert); err == nil { 35 | if _, err := os.Stat(sslkey); err == nil { 36 | // Both the cert and key must be present to use them, or do not use either 37 | settings["sslcert"] = sslcert 38 | settings["sslkey"] = sslkey 39 | } 40 | } 41 | sslrootcert := filepath.Join(appData, "postgresql", "root.crt") 42 | if _, err := os.Stat(sslrootcert); err == nil { 43 | settings["sslrootcert"] = sslrootcert 44 | } 45 | } 46 | 47 | settings["target_session_attrs"] = "any" 48 | 49 | settings["min_read_buffer_size"] = "8192" 50 | 51 | return settings 52 | } 53 | 54 | // defaultHost attempts to mimic libpq's default host. libpq uses the default unix socket location on *nix and localhost 55 | // on Windows. The default socket location is compiled into libpq. Since pgx does not have access to that default it 56 | // checks the existence of common locations. 57 | func defaultHost() string { 58 | return "localhost" 59 | } 60 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package pgconn is a low-level PostgreSQL database driver. 2 | /* 3 | pgconn provides lower level access to a PostgreSQL connection than a database/sql or pgx connection. It operates at 4 | nearly the same level is the C library libpq. 5 | 6 | Establishing a Connection 7 | 8 | Use Connect to establish a connection. It accepts a connection string in URL or DSN and will read the environment for 9 | libpq style environment variables. 10 | 11 | Executing a Query 12 | 13 | ExecParams and ExecPrepared execute a single query. They return readers that iterate over each row. The Read method 14 | reads all rows into memory. 15 | 16 | Executing Multiple Queries in a Single Round Trip 17 | 18 | Exec and ExecBatch can execute multiple queries in a single round trip. They return readers that iterate over each query 19 | result. The ReadAll method reads all query results into memory. 20 | 21 | Context Support 22 | 23 | All potentially blocking operations take a context.Context. If a context is canceled while the method is in progress the 24 | method immediately returns. In most circumstances, this will close the underlying connection. 25 | 26 | The CancelRequest method may be used to request the PostgreSQL server cancel an in-progress query without forcing the 27 | client to abort. 28 | */ 29 | package pgconn 30 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package pgconn 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "net/url" 9 | "regexp" 10 | "strings" 11 | ) 12 | 13 | // SafeToRetry checks if the err is guaranteed to have occurred before sending any data to the server. 14 | func SafeToRetry(err error) bool { 15 | var retryableErr interface{ SafeToRetry() bool } 16 | if errors.As(err, &retryableErr) { 17 | return retryableErr.SafeToRetry() 18 | } 19 | return false 20 | } 21 | 22 | // Timeout checks if err was was caused by a timeout. To be specific, it is true if err was caused within pgconn by a 23 | // context.Canceled, context.DeadlineExceeded or an implementer of net.Error where Timeout() is true. 24 | func Timeout(err error) bool { 25 | var timeoutErr *errTimeout 26 | return errors.As(err, &timeoutErr) 27 | } 28 | 29 | // PgError represents an error reported by the PostgreSQL server. See 30 | // http://www.postgresql.org/docs/11/static/protocol-error-fields.html for 31 | // detailed field description. 32 | type PgError struct { 33 | Severity string 34 | Code string 35 | Message string 36 | Detail string 37 | Hint string 38 | Position int32 39 | InternalPosition int32 40 | InternalQuery string 41 | Where string 42 | SchemaName string 43 | TableName string 44 | ColumnName string 45 | DataTypeName string 46 | ConstraintName string 47 | File string 48 | Line int32 49 | Routine string 50 | } 51 | 52 | func (pe *PgError) Error() string { 53 | return pe.Severity + ": " + pe.Message + " (SQLSTATE " + pe.Code + ")" 54 | } 55 | 56 | // SQLState returns the SQLState of the error. 57 | func (pe *PgError) SQLState() string { 58 | return pe.Code 59 | } 60 | 61 | type connectError struct { 62 | config *Config 63 | msg string 64 | err error 65 | } 66 | 67 | func (e *connectError) Error() string { 68 | sb := &strings.Builder{} 69 | fmt.Fprintf(sb, "failed to connect to `host=%s user=%s database=%s`: %s", e.config.Host, e.config.User, e.config.Database, e.msg) 70 | if e.err != nil { 71 | fmt.Fprintf(sb, " (%s)", e.err.Error()) 72 | } 73 | return sb.String() 74 | } 75 | 76 | func (e *connectError) Unwrap() error { 77 | return e.err 78 | } 79 | 80 | type connLockError struct { 81 | status string 82 | } 83 | 84 | func (e *connLockError) SafeToRetry() bool { 85 | return true // a lock failure by definition happens before the connection is used. 86 | } 87 | 88 | func (e *connLockError) Error() string { 89 | return e.status 90 | } 91 | 92 | type parseConfigError struct { 93 | connString string 94 | msg string 95 | err error 96 | } 97 | 98 | func (e *parseConfigError) Error() string { 99 | connString := redactPW(e.connString) 100 | if e.err == nil { 101 | return fmt.Sprintf("cannot parse `%s`: %s", connString, e.msg) 102 | } 103 | return fmt.Sprintf("cannot parse `%s`: %s (%s)", connString, e.msg, e.err.Error()) 104 | } 105 | 106 | func (e *parseConfigError) Unwrap() error { 107 | return e.err 108 | } 109 | 110 | // preferContextOverNetTimeoutError returns ctx.Err() if ctx.Err() is present and err is a net.Error with Timeout() == 111 | // true. Otherwise returns err. 112 | func preferContextOverNetTimeoutError(ctx context.Context, err error) error { 113 | if err, ok := err.(net.Error); ok && err.Timeout() && ctx.Err() != nil { 114 | return &errTimeout{err: ctx.Err()} 115 | } 116 | return err 117 | } 118 | 119 | type pgconnError struct { 120 | msg string 121 | err error 122 | safeToRetry bool 123 | } 124 | 125 | func (e *pgconnError) Error() string { 126 | if e.msg == "" { 127 | return e.err.Error() 128 | } 129 | if e.err == nil { 130 | return e.msg 131 | } 132 | return fmt.Sprintf("%s: %s", e.msg, e.err.Error()) 133 | } 134 | 135 | func (e *pgconnError) SafeToRetry() bool { 136 | return e.safeToRetry 137 | } 138 | 139 | func (e *pgconnError) Unwrap() error { 140 | return e.err 141 | } 142 | 143 | // errTimeout occurs when an error was caused by a timeout. Specifically, it wraps an error which is 144 | // context.Canceled, context.DeadlineExceeded, or an implementer of net.Error where Timeout() is true. 145 | type errTimeout struct { 146 | err error 147 | } 148 | 149 | func (e *errTimeout) Error() string { 150 | return fmt.Sprintf("timeout: %s", e.err.Error()) 151 | } 152 | 153 | func (e *errTimeout) SafeToRetry() bool { 154 | return SafeToRetry(e.err) 155 | } 156 | 157 | func (e *errTimeout) Unwrap() error { 158 | return e.err 159 | } 160 | 161 | type contextAlreadyDoneError struct { 162 | err error 163 | } 164 | 165 | func (e *contextAlreadyDoneError) Error() string { 166 | return fmt.Sprintf("context already done: %s", e.err.Error()) 167 | } 168 | 169 | func (e *contextAlreadyDoneError) SafeToRetry() bool { 170 | return true 171 | } 172 | 173 | func (e *contextAlreadyDoneError) Unwrap() error { 174 | return e.err 175 | } 176 | 177 | // newContextAlreadyDoneError double-wraps a context error in `contextAlreadyDoneError` and `errTimeout`. 178 | func newContextAlreadyDoneError(ctx context.Context) (err error) { 179 | return &errTimeout{&contextAlreadyDoneError{err: ctx.Err()}} 180 | } 181 | 182 | type writeError struct { 183 | err error 184 | safeToRetry bool 185 | } 186 | 187 | func (e *writeError) Error() string { 188 | return fmt.Sprintf("write failed: %s", e.err.Error()) 189 | } 190 | 191 | func (e *writeError) SafeToRetry() bool { 192 | return e.safeToRetry 193 | } 194 | 195 | func (e *writeError) Unwrap() error { 196 | return e.err 197 | } 198 | 199 | func redactPW(connString string) string { 200 | if strings.HasPrefix(connString, "postgres://") || strings.HasPrefix(connString, "postgresql://") { 201 | if u, err := url.Parse(connString); err == nil { 202 | return redactURL(u) 203 | } 204 | } 205 | quotedDSN := regexp.MustCompile(`password='[^']*'`) 206 | connString = quotedDSN.ReplaceAllLiteralString(connString, "password=xxxxx") 207 | plainDSN := regexp.MustCompile(`password=[^ ]*`) 208 | connString = plainDSN.ReplaceAllLiteralString(connString, "password=xxxxx") 209 | brokenURL := regexp.MustCompile(`:[^:@]+?@`) 210 | connString = brokenURL.ReplaceAllLiteralString(connString, ":xxxxxx@") 211 | return connString 212 | } 213 | 214 | func redactURL(u *url.URL) string { 215 | if u == nil { 216 | return "" 217 | } 218 | if _, pwSet := u.User.Password(); pwSet { 219 | u.User = url.UserPassword(u.User.Username(), "xxxxx") 220 | } 221 | return u.String() 222 | } 223 | 224 | type NotPreferredError struct { 225 | err error 226 | safeToRetry bool 227 | } 228 | 229 | func (e *NotPreferredError) Error() string { 230 | return fmt.Sprintf("standby server not found: %s", e.err.Error()) 231 | } 232 | 233 | func (e *NotPreferredError) SafeToRetry() bool { 234 | return e.safeToRetry 235 | } 236 | 237 | func (e *NotPreferredError) Unwrap() error { 238 | return e.err 239 | } 240 | -------------------------------------------------------------------------------- /errors_test.go: -------------------------------------------------------------------------------- 1 | package pgconn_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/jackc/pgconn" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestConfigError(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | err error 14 | expectedMsg string 15 | }{ 16 | { 17 | name: "url with password", 18 | err: pgconn.NewParseConfigError("postgresql://foo:password@host", "msg", nil), 19 | expectedMsg: "cannot parse `postgresql://foo:xxxxx@host`: msg", 20 | }, 21 | { 22 | name: "dsn with password unquoted", 23 | err: pgconn.NewParseConfigError("host=host password=password user=user", "msg", nil), 24 | expectedMsg: "cannot parse `host=host password=xxxxx user=user`: msg", 25 | }, 26 | { 27 | name: "dsn with password quoted", 28 | err: pgconn.NewParseConfigError("host=host password='pass word' user=user", "msg", nil), 29 | expectedMsg: "cannot parse `host=host password=xxxxx user=user`: msg", 30 | }, 31 | { 32 | name: "weird url", 33 | err: pgconn.NewParseConfigError("postgresql://foo::pasword@host:1:", "msg", nil), 34 | expectedMsg: "cannot parse `postgresql://foo:xxxxx@host:1:`: msg", 35 | }, 36 | { 37 | name: "weird url with slash in password", 38 | err: pgconn.NewParseConfigError("postgres://user:pass/word@host:5432/db_name", "msg", nil), 39 | expectedMsg: "cannot parse `postgres://user:xxxxxx@host:5432/db_name`: msg", 40 | }, 41 | { 42 | name: "url without password", 43 | err: pgconn.NewParseConfigError("postgresql://other@host/db", "msg", nil), 44 | expectedMsg: "cannot parse `postgresql://other@host/db`: msg", 45 | }, 46 | } 47 | for _, tt := range tests { 48 | tt := tt 49 | t.Run(tt.name, func(t *testing.T) { 50 | t.Parallel() 51 | assert.EqualError(t, tt.err, tt.expectedMsg) 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /export_test.go: -------------------------------------------------------------------------------- 1 | // File export_test exports some methods for better testing. 2 | 3 | package pgconn 4 | 5 | func NewParseConfigError(conn, msg string, err error) error { 6 | return &parseConfigError{ 7 | connString: conn, 8 | msg: msg, 9 | err: err, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend_test.go: -------------------------------------------------------------------------------- 1 | package pgconn_test 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "os" 7 | "testing" 8 | 9 | "github.com/jackc/pgconn" 10 | "github.com/jackc/pgproto3/v2" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | // frontendWrapper allows to hijack a regular frontend, and inject a specific response 16 | type frontendWrapper struct { 17 | front pgconn.Frontend 18 | 19 | msg pgproto3.BackendMessage 20 | } 21 | 22 | // frontendWrapper implements the pgconn.Frontend interface 23 | var _ pgconn.Frontend = (*frontendWrapper)(nil) 24 | 25 | func (f *frontendWrapper) Receive() (pgproto3.BackendMessage, error) { 26 | if f.msg != nil { 27 | return f.msg, nil 28 | } 29 | 30 | return f.front.Receive() 31 | } 32 | 33 | func TestFrontendFatalErrExec(t *testing.T) { 34 | t.Parallel() 35 | 36 | config, err := pgconn.ParseConfig(os.Getenv("PGX_TEST_CONN_STRING")) 37 | require.NoError(t, err) 38 | 39 | buildFrontend := config.BuildFrontend 40 | var front *frontendWrapper 41 | 42 | config.BuildFrontend = func(r io.Reader, w io.Writer) pgconn.Frontend { 43 | wrapped := buildFrontend(r, w) 44 | front = &frontendWrapper{wrapped, nil} 45 | 46 | return front 47 | } 48 | 49 | conn, err := pgconn.ConnectConfig(context.Background(), config) 50 | require.NoError(t, err) 51 | require.NotNil(t, conn) 52 | require.NotNil(t, front) 53 | 54 | // set frontend to return a "FATAL" message on next call 55 | front.msg = &pgproto3.ErrorResponse{Severity: "FATAL", Message: "unit testing fatal error"} 56 | 57 | _, err = conn.Exec(context.Background(), "SELECT 1").ReadAll() 58 | assert.Error(t, err) 59 | 60 | err = conn.Close(context.Background()) 61 | assert.NoError(t, err) 62 | 63 | select { 64 | case <-conn.CleanupDone(): 65 | t.Log("ok, CleanupDone() is not blocking") 66 | 67 | default: 68 | assert.Fail(t, "connection closed but CleanupDone() still blocking") 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jackc/pgconn 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/jackc/chunkreader/v2 v2.0.1 7 | github.com/jackc/pgio v1.0.0 8 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 9 | github.com/jackc/pgpassfile v1.0.0 10 | github.com/jackc/pgproto3/v2 v2.3.3 11 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a 12 | github.com/stretchr/testify v1.8.1 13 | golang.org/x/crypto v0.20.0 14 | golang.org/x/text v0.14.0 15 | ) 16 | 17 | require ( 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/pmezard/go-difflib v1.0.0 // indirect 20 | gopkg.in/yaml.v3 v3.0.1 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 2 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 3 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 4 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 9 | github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= 10 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 11 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 12 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 13 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 14 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 15 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 16 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 17 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= 18 | github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= 19 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 20 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 21 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 22 | github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= 23 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= 24 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= 25 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 26 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 27 | github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= 28 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 29 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 30 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 31 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 32 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 33 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 34 | github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 35 | github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= 36 | github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 37 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 38 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= 39 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 40 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 41 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 42 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 43 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 44 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 45 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 46 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 47 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 48 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 49 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 50 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 51 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 52 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 53 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 54 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 55 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 56 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 57 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 58 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 59 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 60 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 61 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 62 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 63 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 64 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 65 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 66 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 67 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 68 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 69 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 70 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 71 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 72 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 73 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 74 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 75 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 76 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 77 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 78 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 79 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 80 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 81 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 82 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 83 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 84 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 85 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 86 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 87 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 88 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 89 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 90 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 91 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 92 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 93 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 94 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 95 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 96 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 97 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 98 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 99 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 100 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 101 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 102 | golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= 103 | golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= 104 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 105 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 106 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 107 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 108 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 109 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 110 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 111 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 112 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 113 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 114 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 115 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 116 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 117 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 118 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 119 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 120 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 121 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 122 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 123 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 124 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 125 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 126 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 127 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 128 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 129 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 130 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 131 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 132 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 133 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 134 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 135 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 136 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 137 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 138 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 139 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 140 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 141 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 142 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 143 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 144 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 145 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 146 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 147 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 148 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 149 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 150 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 151 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 152 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 153 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 154 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 155 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 156 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 157 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 158 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 159 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 160 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 161 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 162 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 163 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 164 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 165 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 166 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 167 | -------------------------------------------------------------------------------- /helper_test.go: -------------------------------------------------------------------------------- 1 | package pgconn_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/jackc/pgconn" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func closeConn(t testing.TB, conn *pgconn.PgConn) { 15 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 16 | defer cancel() 17 | require.NoError(t, conn.Close(ctx)) 18 | select { 19 | case <-conn.CleanupDone(): 20 | case <-time.After(5 * time.Second): 21 | t.Fatal("Connection cleanup exceeded maximum time") 22 | } 23 | } 24 | 25 | // Do a simple query to ensure the connection is still usable 26 | func ensureConnValid(t *testing.T, pgConn *pgconn.PgConn) { 27 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 28 | result := pgConn.ExecParams(ctx, "select generate_series(1,$1)", [][]byte{[]byte("3")}, nil, nil, nil).Read() 29 | cancel() 30 | 31 | require.Nil(t, result.Err) 32 | assert.Equal(t, 3, len(result.Rows)) 33 | assert.Equal(t, "1", string(result.Rows[0][0])) 34 | assert.Equal(t, "2", string(result.Rows[1][0])) 35 | assert.Equal(t, "3", string(result.Rows[2][0])) 36 | } 37 | -------------------------------------------------------------------------------- /internal/ctxwatch/context_watcher.go: -------------------------------------------------------------------------------- 1 | package ctxwatch 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | ) 7 | 8 | // ContextWatcher watches a context and performs an action when the context is canceled. It can watch one context at a 9 | // time. 10 | type ContextWatcher struct { 11 | onCancel func() 12 | onUnwatchAfterCancel func() 13 | unwatchChan chan struct{} 14 | 15 | lock sync.Mutex 16 | watchInProgress bool 17 | onCancelWasCalled bool 18 | } 19 | 20 | // NewContextWatcher returns a ContextWatcher. onCancel will be called when a watched context is canceled. 21 | // OnUnwatchAfterCancel will be called when Unwatch is called and the watched context had already been canceled and 22 | // onCancel called. 23 | func NewContextWatcher(onCancel func(), onUnwatchAfterCancel func()) *ContextWatcher { 24 | cw := &ContextWatcher{ 25 | onCancel: onCancel, 26 | onUnwatchAfterCancel: onUnwatchAfterCancel, 27 | unwatchChan: make(chan struct{}), 28 | } 29 | 30 | return cw 31 | } 32 | 33 | // Watch starts watching ctx. If ctx is canceled then the onCancel function passed to NewContextWatcher will be called. 34 | func (cw *ContextWatcher) Watch(ctx context.Context) { 35 | cw.lock.Lock() 36 | defer cw.lock.Unlock() 37 | 38 | if cw.watchInProgress { 39 | panic("Watch already in progress") 40 | } 41 | 42 | cw.onCancelWasCalled = false 43 | 44 | if ctx.Done() != nil { 45 | cw.watchInProgress = true 46 | go func() { 47 | select { 48 | case <-ctx.Done(): 49 | cw.onCancel() 50 | cw.onCancelWasCalled = true 51 | <-cw.unwatchChan 52 | case <-cw.unwatchChan: 53 | } 54 | }() 55 | } else { 56 | cw.watchInProgress = false 57 | } 58 | } 59 | 60 | // Unwatch stops watching the previously watched context. If the onCancel function passed to NewContextWatcher was 61 | // called then onUnwatchAfterCancel will also be called. 62 | func (cw *ContextWatcher) Unwatch() { 63 | cw.lock.Lock() 64 | defer cw.lock.Unlock() 65 | 66 | if cw.watchInProgress { 67 | cw.unwatchChan <- struct{}{} 68 | if cw.onCancelWasCalled { 69 | cw.onUnwatchAfterCancel() 70 | } 71 | cw.watchInProgress = false 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /internal/ctxwatch/context_watcher_test.go: -------------------------------------------------------------------------------- 1 | package ctxwatch_test 2 | 3 | import ( 4 | "context" 5 | "sync/atomic" 6 | "testing" 7 | "time" 8 | 9 | "github.com/jackc/pgconn/internal/ctxwatch" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestContextWatcherContextCancelled(t *testing.T) { 14 | canceledChan := make(chan struct{}) 15 | cleanupCalled := false 16 | cw := ctxwatch.NewContextWatcher(func() { 17 | canceledChan <- struct{}{} 18 | }, func() { 19 | cleanupCalled = true 20 | }) 21 | 22 | ctx, cancel := context.WithCancel(context.Background()) 23 | cw.Watch(ctx) 24 | cancel() 25 | 26 | select { 27 | case <-canceledChan: 28 | case <-time.NewTimer(time.Second).C: 29 | t.Fatal("Timed out waiting for cancel func to be called") 30 | } 31 | 32 | cw.Unwatch() 33 | 34 | require.True(t, cleanupCalled, "Cleanup func was not called") 35 | } 36 | 37 | func TestContextWatcherUnwatchdBeforeContextCancelled(t *testing.T) { 38 | cw := ctxwatch.NewContextWatcher(func() { 39 | t.Error("cancel func should not have been called") 40 | }, func() { 41 | t.Error("cleanup func should not have been called") 42 | }) 43 | 44 | ctx, cancel := context.WithCancel(context.Background()) 45 | cw.Watch(ctx) 46 | cw.Unwatch() 47 | cancel() 48 | } 49 | 50 | func TestContextWatcherMultipleWatchPanics(t *testing.T) { 51 | cw := ctxwatch.NewContextWatcher(func() {}, func() {}) 52 | 53 | ctx, cancel := context.WithCancel(context.Background()) 54 | defer cancel() 55 | cw.Watch(ctx) 56 | 57 | ctx2, cancel2 := context.WithCancel(context.Background()) 58 | defer cancel2() 59 | require.Panics(t, func() { cw.Watch(ctx2) }, "Expected panic when Watch called multiple times") 60 | } 61 | 62 | func TestContextWatcherUnwatchWhenNotWatchingIsSafe(t *testing.T) { 63 | cw := ctxwatch.NewContextWatcher(func() {}, func() {}) 64 | cw.Unwatch() // unwatch when not / never watching 65 | 66 | ctx, cancel := context.WithCancel(context.Background()) 67 | defer cancel() 68 | cw.Watch(ctx) 69 | cw.Unwatch() 70 | cw.Unwatch() // double unwatch 71 | } 72 | 73 | func TestContextWatcherUnwatchIsConcurrencySafe(t *testing.T) { 74 | cw := ctxwatch.NewContextWatcher(func() {}, func() {}) 75 | 76 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 77 | defer cancel() 78 | cw.Watch(ctx) 79 | 80 | go cw.Unwatch() 81 | go cw.Unwatch() 82 | 83 | <-ctx.Done() 84 | } 85 | 86 | func TestContextWatcherStress(t *testing.T) { 87 | var cancelFuncCalls int64 88 | var cleanupFuncCalls int64 89 | 90 | cw := ctxwatch.NewContextWatcher(func() { 91 | atomic.AddInt64(&cancelFuncCalls, 1) 92 | }, func() { 93 | atomic.AddInt64(&cleanupFuncCalls, 1) 94 | }) 95 | 96 | cycleCount := 100000 97 | 98 | for i := 0; i < cycleCount; i++ { 99 | ctx, cancel := context.WithCancel(context.Background()) 100 | cw.Watch(ctx) 101 | if i%2 == 0 { 102 | cancel() 103 | } 104 | 105 | // Without time.Sleep, cw.Unwatch will almost always run before the cancel func which means cancel will never happen. This gives us a better mix. 106 | if i%3 == 0 { 107 | time.Sleep(time.Nanosecond) 108 | } 109 | 110 | cw.Unwatch() 111 | if i%2 == 1 { 112 | cancel() 113 | } 114 | } 115 | 116 | actualCancelFuncCalls := atomic.LoadInt64(&cancelFuncCalls) 117 | actualCleanupFuncCalls := atomic.LoadInt64(&cleanupFuncCalls) 118 | 119 | if actualCancelFuncCalls == 0 { 120 | t.Fatal("actualCancelFuncCalls == 0") 121 | } 122 | 123 | maxCancelFuncCalls := int64(cycleCount) / 2 124 | if actualCancelFuncCalls > maxCancelFuncCalls { 125 | t.Errorf("cancel func calls should be no more than %d but was %d", actualCancelFuncCalls, maxCancelFuncCalls) 126 | } 127 | 128 | if actualCancelFuncCalls != actualCleanupFuncCalls { 129 | t.Errorf("cancel func calls (%d) should be equal to cleanup func calls (%d) but was not", actualCancelFuncCalls, actualCleanupFuncCalls) 130 | } 131 | } 132 | 133 | func BenchmarkContextWatcherUncancellable(b *testing.B) { 134 | cw := ctxwatch.NewContextWatcher(func() {}, func() {}) 135 | 136 | for i := 0; i < b.N; i++ { 137 | cw.Watch(context.Background()) 138 | cw.Unwatch() 139 | } 140 | } 141 | 142 | func BenchmarkContextWatcherCancelled(b *testing.B) { 143 | cw := ctxwatch.NewContextWatcher(func() {}, func() {}) 144 | 145 | for i := 0; i < b.N; i++ { 146 | ctx, cancel := context.WithCancel(context.Background()) 147 | cw.Watch(ctx) 148 | cancel() 149 | cw.Unwatch() 150 | } 151 | } 152 | 153 | func BenchmarkContextWatcherCancellable(b *testing.B) { 154 | cw := ctxwatch.NewContextWatcher(func() {}, func() {}) 155 | 156 | ctx, cancel := context.WithCancel(context.Background()) 157 | defer cancel() 158 | 159 | for i := 0; i < b.N; i++ { 160 | cw.Watch(ctx) 161 | cw.Unwatch() 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /krb5.go: -------------------------------------------------------------------------------- 1 | package pgconn 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/jackc/pgproto3/v2" 8 | ) 9 | 10 | // NewGSSFunc creates a GSS authentication provider, for use with 11 | // RegisterGSSProvider. 12 | type NewGSSFunc func() (GSS, error) 13 | 14 | var newGSS NewGSSFunc 15 | 16 | // RegisterGSSProvider registers a GSS authentication provider. For example, if 17 | // you need to use Kerberos to authenticate with your server, add this to your 18 | // main package: 19 | // 20 | // import "github.com/otan/gopgkrb5" 21 | // 22 | // func init() { 23 | // pgconn.RegisterGSSProvider(func() (pgconn.GSS, error) { return gopgkrb5.NewGSS() }) 24 | // } 25 | func RegisterGSSProvider(newGSSArg NewGSSFunc) { 26 | newGSS = newGSSArg 27 | } 28 | 29 | // GSS provides GSSAPI authentication (e.g., Kerberos). 30 | type GSS interface { 31 | GetInitToken(host string, service string) ([]byte, error) 32 | GetInitTokenFromSPN(spn string) ([]byte, error) 33 | Continue(inToken []byte) (done bool, outToken []byte, err error) 34 | } 35 | 36 | func (c *PgConn) gssAuth() error { 37 | if newGSS == nil { 38 | return errors.New("kerberos error: no GSSAPI provider registered, see https://github.com/otan/gopgkrb5") 39 | } 40 | cli, err := newGSS() 41 | if err != nil { 42 | return err 43 | } 44 | 45 | var nextData []byte 46 | if c.config.KerberosSpn != "" { 47 | // Use the supplied SPN if provided. 48 | nextData, err = cli.GetInitTokenFromSPN(c.config.KerberosSpn) 49 | } else { 50 | // Allow the kerberos service name to be overridden 51 | service := "postgres" 52 | if c.config.KerberosSrvName != "" { 53 | service = c.config.KerberosSrvName 54 | } 55 | nextData, err = cli.GetInitToken(c.config.Host, service) 56 | } 57 | if err != nil { 58 | return err 59 | } 60 | 61 | for { 62 | gssResponse := &pgproto3.GSSResponse{ 63 | Data: nextData, 64 | } 65 | buf, err := gssResponse.Encode(nil) 66 | if err != nil { 67 | return err 68 | } 69 | _, err = c.conn.Write(buf) 70 | if err != nil { 71 | return err 72 | } 73 | resp, err := c.rxGSSContinue() 74 | if err != nil { 75 | return err 76 | } 77 | var done bool 78 | done, nextData, err = cli.Continue(resp.Data) 79 | if err != nil { 80 | return err 81 | } 82 | if done { 83 | break 84 | } 85 | } 86 | return nil 87 | } 88 | 89 | func (c *PgConn) rxGSSContinue() (*pgproto3.AuthenticationGSSContinue, error) { 90 | msg, err := c.receiveMessage() 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | switch m := msg.(type) { 96 | case *pgproto3.AuthenticationGSSContinue: 97 | return m, nil 98 | case *pgproto3.ErrorResponse: 99 | return nil, ErrorResponseToPgError(m) 100 | } 101 | 102 | return nil, fmt.Errorf("expected AuthenticationGSSContinue message but received unexpected message %T", msg) 103 | } 104 | -------------------------------------------------------------------------------- /pgconn_stress_test.go: -------------------------------------------------------------------------------- 1 | package pgconn_test 2 | 3 | import ( 4 | "context" 5 | "math/rand" 6 | "os" 7 | "runtime" 8 | "strconv" 9 | "testing" 10 | 11 | "github.com/jackc/pgconn" 12 | 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestConnStress(t *testing.T) { 17 | pgConn, err := pgconn.Connect(context.Background(), os.Getenv("PGX_TEST_CONN_STRING")) 18 | require.NoError(t, err) 19 | defer closeConn(t, pgConn) 20 | 21 | actionCount := 10000 22 | if s := os.Getenv("PGX_TEST_STRESS_FACTOR"); s != "" { 23 | stressFactor, err := strconv.ParseInt(s, 10, 64) 24 | require.Nil(t, err, "Failed to parse PGX_TEST_STRESS_FACTOR") 25 | actionCount *= int(stressFactor) 26 | } 27 | 28 | setupStressDB(t, pgConn) 29 | 30 | actions := []struct { 31 | name string 32 | fn func(*pgconn.PgConn) error 33 | }{ 34 | {"Exec Select", stressExecSelect}, 35 | {"ExecParams Select", stressExecParamsSelect}, 36 | {"Batch", stressBatch}, 37 | } 38 | 39 | for i := 0; i < actionCount; i++ { 40 | action := actions[rand.Intn(len(actions))] 41 | err := action.fn(pgConn) 42 | require.Nilf(t, err, "%d: %s", i, action.name) 43 | } 44 | 45 | // Each call with a context starts a goroutine. Ensure they are cleaned up when context is not canceled. 46 | numGoroutine := runtime.NumGoroutine() 47 | require.Truef(t, numGoroutine < 1000, "goroutines appear to be orphaned: %d in process", numGoroutine) 48 | } 49 | 50 | func setupStressDB(t *testing.T, pgConn *pgconn.PgConn) { 51 | _, err := pgConn.Exec(context.Background(), ` 52 | create temporary table widgets( 53 | id serial primary key, 54 | name varchar not null, 55 | description text, 56 | creation_time timestamptz default now() 57 | ); 58 | 59 | insert into widgets(name, description) values 60 | ('Foo', 'bar'), 61 | ('baz', 'Something really long Something really long Something really long Something really long Something really long'), 62 | ('a', 'b')`).ReadAll() 63 | require.NoError(t, err) 64 | } 65 | 66 | func stressExecSelect(pgConn *pgconn.PgConn) error { 67 | ctx, cancel := context.WithCancel(context.Background()) 68 | defer cancel() 69 | _, err := pgConn.Exec(ctx, "select * from widgets").ReadAll() 70 | return err 71 | } 72 | 73 | func stressExecParamsSelect(pgConn *pgconn.PgConn) error { 74 | ctx, cancel := context.WithCancel(context.Background()) 75 | defer cancel() 76 | result := pgConn.ExecParams(ctx, "select * from widgets where id < $1", [][]byte{[]byte("10")}, nil, nil, nil).Read() 77 | return result.Err 78 | } 79 | 80 | func stressBatch(pgConn *pgconn.PgConn) error { 81 | ctx, cancel := context.WithCancel(context.Background()) 82 | defer cancel() 83 | 84 | batch := &pgconn.Batch{} 85 | 86 | batch.ExecParams("select * from widgets", nil, nil, nil, nil) 87 | batch.ExecParams("select * from widgets where id < $1", [][]byte{[]byte("10")}, nil, nil, nil) 88 | _, err := pgConn.ExecBatch(ctx, batch).ReadAll() 89 | return err 90 | } 91 | -------------------------------------------------------------------------------- /stmtcache/lru.go: -------------------------------------------------------------------------------- 1 | package stmtcache 2 | 3 | import ( 4 | "container/list" 5 | "context" 6 | "fmt" 7 | "sync/atomic" 8 | 9 | "github.com/jackc/pgconn" 10 | ) 11 | 12 | var lruCount uint64 13 | 14 | // LRU implements Cache with a Least Recently Used (LRU) cache. 15 | type LRU struct { 16 | conn *pgconn.PgConn 17 | mode int 18 | cap int 19 | prepareCount int 20 | m map[string]*list.Element 21 | l *list.List 22 | psNamePrefix string 23 | stmtsToClear []string 24 | } 25 | 26 | // NewLRU creates a new LRU. mode is either ModePrepare or ModeDescribe. cap is the maximum size of the cache. 27 | func NewLRU(conn *pgconn.PgConn, mode int, cap int) *LRU { 28 | mustBeValidMode(mode) 29 | mustBeValidCap(cap) 30 | 31 | n := atomic.AddUint64(&lruCount, 1) 32 | 33 | return &LRU{ 34 | conn: conn, 35 | mode: mode, 36 | cap: cap, 37 | m: make(map[string]*list.Element), 38 | l: list.New(), 39 | psNamePrefix: fmt.Sprintf("lrupsc_%d", n), 40 | } 41 | } 42 | 43 | // Get returns the prepared statement description for sql preparing or describing the sql on the server as needed. 44 | func (c *LRU) Get(ctx context.Context, sql string) (*pgconn.StatementDescription, error) { 45 | if ctx != context.Background() { 46 | select { 47 | case <-ctx.Done(): 48 | return nil, ctx.Err() 49 | default: 50 | } 51 | } 52 | 53 | // flush an outstanding bad statements 54 | txStatus := c.conn.TxStatus() 55 | if (txStatus == 'I' || txStatus == 'T') && len(c.stmtsToClear) > 0 { 56 | for _, stmt := range c.stmtsToClear { 57 | err := c.clearStmt(ctx, stmt) 58 | if err != nil { 59 | return nil, err 60 | } 61 | } 62 | } 63 | 64 | if el, ok := c.m[sql]; ok { 65 | c.l.MoveToFront(el) 66 | return el.Value.(*pgconn.StatementDescription), nil 67 | } 68 | 69 | if c.l.Len() == c.cap { 70 | err := c.removeOldest(ctx) 71 | if err != nil { 72 | return nil, err 73 | } 74 | } 75 | 76 | psd, err := c.prepare(ctx, sql) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | el := c.l.PushFront(psd) 82 | c.m[sql] = el 83 | 84 | return psd, nil 85 | } 86 | 87 | // Clear removes all entries in the cache. Any prepared statements will be deallocated from the PostgreSQL session. 88 | func (c *LRU) Clear(ctx context.Context) error { 89 | for c.l.Len() > 0 { 90 | err := c.removeOldest(ctx) 91 | if err != nil { 92 | return err 93 | } 94 | } 95 | 96 | return nil 97 | } 98 | 99 | func (c *LRU) StatementErrored(sql string, err error) { 100 | pgErr, ok := err.(*pgconn.PgError) 101 | if !ok { 102 | return 103 | } 104 | 105 | // https://github.com/jackc/pgx/issues/1162 106 | // 107 | // We used to look for the message "cached plan must not change result type". However, that message can be localized. 108 | // Unfortunately, error code "0A000" - "FEATURE NOT SUPPORTED" is used for many different errors and the only way to 109 | // tell the difference is by the message. But all that happens is we clear a statement that we otherwise wouldn't 110 | // have so it should be safe. 111 | possibleInvalidCachedPlanError := pgErr.Code == "0A000" 112 | if possibleInvalidCachedPlanError { 113 | c.stmtsToClear = append(c.stmtsToClear, sql) 114 | } 115 | } 116 | 117 | func (c *LRU) clearStmt(ctx context.Context, sql string) error { 118 | elem, inMap := c.m[sql] 119 | if !inMap { 120 | // The statement probably fell off the back of the list. In that case, we've 121 | // ensured that it isn't in the cache, so we can declare victory. 122 | return nil 123 | } 124 | 125 | c.l.Remove(elem) 126 | 127 | psd := elem.Value.(*pgconn.StatementDescription) 128 | delete(c.m, psd.SQL) 129 | if c.mode == ModePrepare { 130 | return c.conn.Exec(ctx, fmt.Sprintf("deallocate %s", psd.Name)).Close() 131 | } 132 | return nil 133 | } 134 | 135 | // Len returns the number of cached prepared statement descriptions. 136 | func (c *LRU) Len() int { 137 | return c.l.Len() 138 | } 139 | 140 | // Cap returns the maximum number of cached prepared statement descriptions. 141 | func (c *LRU) Cap() int { 142 | return c.cap 143 | } 144 | 145 | // Mode returns the mode of the cache (ModePrepare or ModeDescribe) 146 | func (c *LRU) Mode() int { 147 | return c.mode 148 | } 149 | 150 | func (c *LRU) prepare(ctx context.Context, sql string) (*pgconn.StatementDescription, error) { 151 | var name string 152 | if c.mode == ModePrepare { 153 | name = fmt.Sprintf("%s_%d", c.psNamePrefix, c.prepareCount) 154 | c.prepareCount += 1 155 | } 156 | 157 | return c.conn.Prepare(ctx, name, sql, nil) 158 | } 159 | 160 | func (c *LRU) removeOldest(ctx context.Context) error { 161 | oldest := c.l.Back() 162 | c.l.Remove(oldest) 163 | psd := oldest.Value.(*pgconn.StatementDescription) 164 | delete(c.m, psd.SQL) 165 | if c.mode == ModePrepare { 166 | return c.conn.Exec(ctx, fmt.Sprintf("deallocate %s", psd.Name)).Close() 167 | } 168 | return nil 169 | } 170 | -------------------------------------------------------------------------------- /stmtcache/lru_test.go: -------------------------------------------------------------------------------- 1 | package stmtcache_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "os" 8 | "regexp" 9 | "testing" 10 | "time" 11 | 12 | "github.com/jackc/pgconn" 13 | "github.com/jackc/pgconn/stmtcache" 14 | 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | func TestLRUModePrepare(t *testing.T) { 19 | t.Parallel() 20 | 21 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 22 | defer cancel() 23 | 24 | conn, err := pgconn.Connect(ctx, os.Getenv("PGX_TEST_CONN_STRING")) 25 | require.NoError(t, err) 26 | defer conn.Close(ctx) 27 | 28 | cache := stmtcache.NewLRU(conn, stmtcache.ModePrepare, 2) 29 | require.EqualValues(t, 0, cache.Len()) 30 | require.EqualValues(t, 2, cache.Cap()) 31 | require.EqualValues(t, stmtcache.ModePrepare, cache.Mode()) 32 | 33 | psd, err := cache.Get(ctx, "select 1") 34 | require.NoError(t, err) 35 | require.NotNil(t, psd) 36 | require.EqualValues(t, 1, cache.Len()) 37 | require.ElementsMatch(t, []string{"select 1"}, fetchServerStatements(t, ctx, conn)) 38 | 39 | psd, err = cache.Get(ctx, "select 1") 40 | require.NoError(t, err) 41 | require.NotNil(t, psd) 42 | require.EqualValues(t, 1, cache.Len()) 43 | require.ElementsMatch(t, []string{"select 1"}, fetchServerStatements(t, ctx, conn)) 44 | 45 | psd, err = cache.Get(ctx, "select 2") 46 | require.NoError(t, err) 47 | require.NotNil(t, psd) 48 | require.EqualValues(t, 2, cache.Len()) 49 | require.ElementsMatch(t, []string{"select 1", "select 2"}, fetchServerStatements(t, ctx, conn)) 50 | 51 | psd, err = cache.Get(ctx, "select 3") 52 | require.NoError(t, err) 53 | require.NotNil(t, psd) 54 | require.EqualValues(t, 2, cache.Len()) 55 | require.ElementsMatch(t, []string{"select 2", "select 3"}, fetchServerStatements(t, ctx, conn)) 56 | 57 | err = cache.Clear(ctx) 58 | require.NoError(t, err) 59 | require.EqualValues(t, 0, cache.Len()) 60 | require.Empty(t, fetchServerStatements(t, ctx, conn)) 61 | } 62 | 63 | func TestLRUStmtInvalidation(t *testing.T) { 64 | t.Parallel() 65 | 66 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 67 | defer cancel() 68 | 69 | conn, err := pgconn.Connect(ctx, os.Getenv("PGX_TEST_CONN_STRING")) 70 | require.NoError(t, err) 71 | defer conn.Close(ctx) 72 | 73 | // we construct a fake error because its not super straightforward to actually call 74 | // a prepared statement from the LRU cache without the helper routines which live 75 | // in pgx proper. 76 | fakeInvalidCachePlanError := &pgconn.PgError{ 77 | Severity: "ERROR", 78 | Code: "0A000", 79 | Message: "cached plan must not change result type", 80 | } 81 | 82 | cache := stmtcache.NewLRU(conn, stmtcache.ModePrepare, 2) 83 | 84 | // 85 | // outside of a transaction, we eagerly flush the statement 86 | // 87 | 88 | _, err = cache.Get(ctx, "select 1") 89 | require.NoError(t, err) 90 | require.EqualValues(t, 1, cache.Len()) 91 | require.ElementsMatch(t, []string{"select 1"}, fetchServerStatements(t, ctx, conn)) 92 | 93 | cache.StatementErrored("select 1", fakeInvalidCachePlanError) 94 | _, err = cache.Get(ctx, "select 2") 95 | require.NoError(t, err) 96 | require.EqualValues(t, 1, cache.Len()) 97 | require.ElementsMatch(t, []string{"select 2"}, fetchServerStatements(t, ctx, conn)) 98 | 99 | err = cache.Clear(ctx) 100 | require.NoError(t, err) 101 | 102 | // 103 | // within an errored transaction, we defer the flush to after the first get 104 | // that happens after the transaction is rolled back 105 | // 106 | 107 | _, err = cache.Get(ctx, "select 1") 108 | require.NoError(t, err) 109 | require.EqualValues(t, 1, cache.Len()) 110 | require.ElementsMatch(t, []string{"select 1"}, fetchServerStatements(t, ctx, conn)) 111 | 112 | res := conn.Exec(ctx, "begin") 113 | require.NoError(t, res.Close()) 114 | require.Equal(t, byte('T'), conn.TxStatus()) 115 | 116 | res = conn.Exec(ctx, "selec") 117 | require.Error(t, res.Close()) 118 | require.Equal(t, byte('E'), conn.TxStatus()) 119 | 120 | cache.StatementErrored("select 1", fakeInvalidCachePlanError) 121 | require.EqualValues(t, 1, cache.Len()) 122 | 123 | res = conn.Exec(ctx, "rollback") 124 | require.NoError(t, res.Close()) 125 | 126 | _, err = cache.Get(ctx, "select 2") 127 | require.EqualValues(t, 1, cache.Len()) 128 | require.ElementsMatch(t, []string{"select 2"}, fetchServerStatements(t, ctx, conn)) 129 | } 130 | 131 | func TestLRUStmtInvalidationIntegration(t *testing.T) { 132 | t.Parallel() 133 | 134 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 135 | defer cancel() 136 | 137 | conn, err := pgconn.Connect(ctx, os.Getenv("PGX_TEST_CONN_STRING")) 138 | require.NoError(t, err) 139 | defer conn.Close(ctx) 140 | 141 | cache := stmtcache.NewLRU(conn, stmtcache.ModePrepare, 2) 142 | 143 | result := conn.ExecParams(ctx, "create temporary table stmtcache_table (a text)", nil, nil, nil, nil).Read() 144 | require.NoError(t, result.Err) 145 | 146 | sql := "select * from stmtcache_table" 147 | sd1, err := cache.Get(ctx, sql) 148 | require.NoError(t, err) 149 | 150 | result = conn.ExecPrepared(ctx, sd1.Name, nil, nil, nil).Read() 151 | require.NoError(t, result.Err) 152 | 153 | result = conn.ExecParams(ctx, "alter table stmtcache_table add column b text", nil, nil, nil, nil).Read() 154 | require.NoError(t, result.Err) 155 | 156 | result = conn.ExecPrepared(ctx, sd1.Name, nil, nil, nil).Read() 157 | require.EqualError(t, result.Err, "ERROR: cached plan must not change result type (SQLSTATE 0A000)") 158 | 159 | cache.StatementErrored(sql, result.Err) 160 | 161 | sd2, err := cache.Get(ctx, sql) 162 | require.NoError(t, err) 163 | require.NotEqual(t, sd1.Name, sd2.Name) 164 | 165 | result = conn.ExecPrepared(ctx, sd2.Name, nil, nil, nil).Read() 166 | require.NoError(t, result.Err) 167 | } 168 | 169 | func TestLRUModePrepareStress(t *testing.T) { 170 | t.Parallel() 171 | 172 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 173 | defer cancel() 174 | 175 | conn, err := pgconn.Connect(ctx, os.Getenv("PGX_TEST_CONN_STRING")) 176 | require.NoError(t, err) 177 | defer conn.Close(ctx) 178 | 179 | cache := stmtcache.NewLRU(conn, stmtcache.ModePrepare, 8) 180 | require.EqualValues(t, 0, cache.Len()) 181 | require.EqualValues(t, 8, cache.Cap()) 182 | require.EqualValues(t, stmtcache.ModePrepare, cache.Mode()) 183 | 184 | for i := 0; i < 1000; i++ { 185 | psd, err := cache.Get(ctx, fmt.Sprintf("select %d", rand.Intn(50))) 186 | require.NoError(t, err) 187 | require.NotNil(t, psd) 188 | result := conn.ExecPrepared(ctx, psd.Name, nil, nil, nil).Read() 189 | require.NoError(t, result.Err) 190 | } 191 | } 192 | 193 | func TestLRUModeDescribe(t *testing.T) { 194 | t.Parallel() 195 | 196 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 197 | defer cancel() 198 | 199 | conn, err := pgconn.Connect(ctx, os.Getenv("PGX_TEST_CONN_STRING")) 200 | require.NoError(t, err) 201 | defer conn.Close(ctx) 202 | 203 | cache := stmtcache.NewLRU(conn, stmtcache.ModeDescribe, 2) 204 | require.EqualValues(t, 0, cache.Len()) 205 | require.EqualValues(t, 2, cache.Cap()) 206 | require.EqualValues(t, stmtcache.ModeDescribe, cache.Mode()) 207 | 208 | psd, err := cache.Get(ctx, "select 1") 209 | require.NoError(t, err) 210 | require.NotNil(t, psd) 211 | require.EqualValues(t, 1, cache.Len()) 212 | require.Empty(t, fetchServerStatements(t, ctx, conn)) 213 | 214 | psd, err = cache.Get(ctx, "select 1") 215 | require.NoError(t, err) 216 | require.NotNil(t, psd) 217 | require.EqualValues(t, 1, cache.Len()) 218 | require.Empty(t, fetchServerStatements(t, ctx, conn)) 219 | 220 | psd, err = cache.Get(ctx, "select 2") 221 | require.NoError(t, err) 222 | require.NotNil(t, psd) 223 | require.EqualValues(t, 2, cache.Len()) 224 | require.Empty(t, fetchServerStatements(t, ctx, conn)) 225 | 226 | psd, err = cache.Get(ctx, "select 3") 227 | require.NoError(t, err) 228 | require.NotNil(t, psd) 229 | require.EqualValues(t, 2, cache.Len()) 230 | require.Empty(t, fetchServerStatements(t, ctx, conn)) 231 | 232 | err = cache.Clear(ctx) 233 | require.NoError(t, err) 234 | require.EqualValues(t, 0, cache.Len()) 235 | require.Empty(t, fetchServerStatements(t, ctx, conn)) 236 | } 237 | 238 | func TestLRUContext(t *testing.T) { 239 | t.Parallel() 240 | 241 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 242 | defer cancel() 243 | 244 | conn, err := pgconn.Connect(ctx, os.Getenv("PGX_TEST_CONN_STRING")) 245 | require.NoError(t, err) 246 | defer conn.Close(ctx) 247 | 248 | cache := stmtcache.NewLRU(conn, stmtcache.ModeDescribe, 2) 249 | 250 | // test 1 : getting a value for the first time with a cancelled context returns an error 251 | ctx1, cancel1 := context.WithCancel(ctx) 252 | cancel1() 253 | 254 | desc, err := cache.Get(ctx1, "SELECT 1") 255 | require.Error(t, err) 256 | require.Nil(t, desc) 257 | 258 | // test 2 : when querying for the 2nd time a cached value, if the context is canceled return an error 259 | ctx2, cancel2 := context.WithCancel(ctx) 260 | 261 | desc, err = cache.Get(ctx2, "SELECT 2") 262 | require.NoError(t, err) 263 | require.NotNil(t, desc) 264 | 265 | cancel2() 266 | 267 | desc, err = cache.Get(ctx2, "SELECT 2") 268 | require.Error(t, err) 269 | require.Nil(t, desc) 270 | } 271 | 272 | func fetchServerStatements(t testing.TB, ctx context.Context, conn *pgconn.PgConn) []string { 273 | result := conn.ExecParams(ctx, `select statement from pg_prepared_statements`, nil, nil, nil, nil).Read() 274 | require.NoError(t, result.Err) 275 | var statements []string 276 | for _, r := range result.Rows { 277 | statement := string(r[0]) 278 | if conn.ParameterStatus("crdb_version") != "" { 279 | if statement == "PREPARE AS select statement from pg_prepared_statements" { 280 | // CockroachDB includes the currently running unnamed prepared statement while PostgreSQL does not. Ignore it. 281 | continue 282 | } 283 | 284 | // CockroachDB includes the "PREPARE ... AS" text in the statement even if it was prepared through the extended 285 | // protocol will PostgreSQL does not. Normalize the statement. 286 | re := regexp.MustCompile(`^PREPARE lrupsc[0-9_]+ AS `) 287 | statement = re.ReplaceAllString(statement, "") 288 | } 289 | statements = append(statements, statement) 290 | } 291 | return statements 292 | } 293 | -------------------------------------------------------------------------------- /stmtcache/stmtcache.go: -------------------------------------------------------------------------------- 1 | // Package stmtcache is a cache that can be used to implement lazy prepared statements. 2 | package stmtcache 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/jackc/pgconn" 8 | ) 9 | 10 | const ( 11 | ModePrepare = iota // Cache should prepare named statements. 12 | ModeDescribe // Cache should prepare the anonymous prepared statement to only fetch the description of the statement. 13 | ) 14 | 15 | // Cache prepares and caches prepared statement descriptions. 16 | type Cache interface { 17 | // Get returns the prepared statement description for sql preparing or describing the sql on the server as needed. 18 | Get(ctx context.Context, sql string) (*pgconn.StatementDescription, error) 19 | 20 | // Clear removes all entries in the cache. Any prepared statements will be deallocated from the PostgreSQL session. 21 | Clear(ctx context.Context) error 22 | 23 | // StatementErrored informs the cache that the given statement resulted in an error when it 24 | // was last used against the database. In some cases, this will cause the cache to maer that 25 | // statement as bad. The bad statement will instead be flushed during the next call to Get 26 | // that occurs outside of a failed transaction. 27 | StatementErrored(sql string, err error) 28 | 29 | // Len returns the number of cached prepared statement descriptions. 30 | Len() int 31 | 32 | // Cap returns the maximum number of cached prepared statement descriptions. 33 | Cap() int 34 | 35 | // Mode returns the mode of the cache (ModePrepare or ModeDescribe) 36 | Mode() int 37 | } 38 | 39 | // New returns the preferred cache implementation for mode and cap. mode is either ModePrepare or ModeDescribe. cap is 40 | // the maximum size of the cache. 41 | func New(conn *pgconn.PgConn, mode int, cap int) Cache { 42 | mustBeValidMode(mode) 43 | mustBeValidCap(cap) 44 | 45 | return NewLRU(conn, mode, cap) 46 | } 47 | 48 | func mustBeValidMode(mode int) { 49 | if mode != ModePrepare && mode != ModeDescribe { 50 | panic("mode must be ModePrepare or ModeDescribe") 51 | } 52 | } 53 | 54 | func mustBeValidCap(cap int) { 55 | if cap < 1 { 56 | panic("cache must have cap of >= 1") 57 | } 58 | } 59 | --------------------------------------------------------------------------------