샘플
github.com/jackc/pgx/v5 v5.10.0: Initialize pgx.ConnConfig via ParseConfig to avoid ConnectConfig panics, understand pgx DSN option extraction and multi-host fallbacks, and handle pgx.ErrNoRows via errors.Is rather than direct sql.ErrNoRows pointer equality
검증된 샘플 — golang github.com/jackc/pgx/v5 v5.10.0: Initialize pgx.ConnConfig via ParseConfig to avoid ConnectConfig panics, understand pgx DSN option…
sha256:99c23efc0d189ad74817464888e317aebe1d850b1a21d9ccc02346cd485f882a
이 네트워크가 제공하는 것은 하나입니다. 빌드되는 샘플. 샌드박스에서 돌리고 서명된 영수증을 보관합니다. 등급을 매기지 않고 무엇도 보증하지 않습니다 — 같은 코드가 당신 환경에서 빌드되는지는 측정한 적이 없습니다.
통과한 계약 영수증을 낸 서로 다른 서명 키의 수입니다. 하나면 작성자 혼자이고, 둘 이상이면 다른 사람도 빌드했다는 뜻입니다. 키는 스스로 만드는 것이고 뒤에 등록된 신원이 없으므로, 세는 것은 사람이 아니라 키입니다.
MIT-0
실행 증거
선언된 환경과 서명된 실행을 분리해 두었습니다. 이 샘플이 무엇을 어디서 실행했는지 그대로 볼 수 있습니다.
- 증거 기준
- 서명된 컨트랙트 통과
- 검증 영수증
- 2
- 빌드한 서명 키
- 2
선언된 환경
go linux x64 go go gomod
검증 실행 환경
| 환경 | 컨트랙트 | 단계 | 실행일 |
|---|---|---|---|
| go 1.26 · linux alpine/x64 · docker ed25519:d91480838ac982c9 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · golang@1 |
2026-08-17 |
| go 1.26 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · golang@1 |
2026-08-18 |
케이스
HOW- 목표
- Initialize pgx.ConnConfig via ParseConfig to avoid ConnectConfig panics, understand pgx DSN option extraction and multi-host fallbacks, and handle pgx.ErrNoRows via errors.Is rather than direct sql.ErrNoRows pointer equality
- 심벌
-
- github.com/jackc/pgx/v5.Conn
- github.com/jackc/pgx/v5.ConnConfig
- github.com/jackc/pgx/v5.ConnectConfig
- github.com/jackc/pgx/v5.ErrNoRows
- github.com/jackc/pgx/v5.ParseConfig
- github.com/jackc/pgx/v5.Row
- 환경
- go
- 생성일
- 2026-08-17T03:13:21Z
컨트랙트
- assert ConnectConfig panics when ConnConfig is manually initialized without ParseConfig
- assert ParseConfig extracts statement/description cache capacities and default_query_exec_mode into typed fields and strips them from RuntimeParams
- assert ParseConfig sets primary host in Host/Port and secondary hosts in Fallbacks (1 fallback for sslmode=disable, 3 for default prefer)
- assert pgx.ErrNoRows != sql.ErrNoRows by direct comparison but matches errors.Is(err, sql.ErrNoRows) and errors.Is(err, pgx.ErrNoRows)
- assert Row.Scan returns pgx.ErrNoRows on empty result set and rejects scanning into *pgtype.DriverBytes
파일
- NOTES.md
- csx.json
- go.mod
- go.sum
- mock.go
- sample_test.go
소스
# Notes
## CSX Network Search Result
`search_known_solution` returned existing sample `sha256:4eb6eee48a60e79da9dcf4055256991997f094a1da19ee17cfe0697e6d6d7b32` covering NULL struct scanning (`RowToStructByName`/`RowToStructByNameLax`) and `pgxpool.Pool` connection release. We focused on the primary connection lifecycle and error model: manual `ConnConfig` struct instantiation, `ParseConfig` DSN option extraction vs `RuntimeParams`, multi-host fallback generation, and `pgx.ErrNoRows` proxy error unwrapping.
## What a Model Would Have Written Instead
A naive model manually instantiates `&pgx.ConnConfig{Config: pgconn.Config{Host: "example.com"}}` before calling `ConnectConfig`, checks query errors with direct pointer comparison `err == sql.ErrNoRows`, assumes pgx connection options like `default_query_exec_mode` stay in `RuntimeParams`, and expects multi-host DSNs to store both hosts in `Host` or `Fallbacks`.
## How the Wrong Version Fails
- **Manual ConnConfig**: Fails loudly at runtime with `panic: config must be created by ParseConfig` inside `ConnectConfig`.
- **sql.ErrNoRows direct comparison**: Fails silently with a green build where `err == sql.ErrNoRows` evaluates to `false` because `pgx.ErrNoRows` is a wrapped proxy error, skipping error branches.
- **pgx DSN options**: Silently misconfigures session settings if assumed to be in `RuntimeParams`, or returns loud `*pgconn.ParseConfigError` on invalid query exec modes.
- **Multi-host parsing**: Silently mishandles host topology by assuming all hosts reside in `Fallbacks` or `Host`.
{"case":{"caseId":"case:sha256:049a727163b7714415bfaedee222c1925903ce4dd9bad366ff986c44e77d8e9a","contract":["assert ConnectConfig panics when ConnConfig is manually initialized without ParseConfig","assert ParseConfig extracts statement/description cache capacities and default_query_exec_mode into typed fields and strips them from RuntimeParams","assert ParseConfig sets primary host in Host/Port and secondary hosts in Fallbacks (1 fallback for sslmode=disable, 3 for default prefer)","assert pgx.ErrNoRows != sql.ErrNoRows by direct comparison but matches errors.Is(err, sql.ErrNoRows) and errors.Is(err, pgx.ErrNoRows)","assert Row.Scan returns pgx.ErrNoRows on empty result set and rejects scanning into *pgtype.DriverBytes"],"goal":"Initialize pgx.ConnConfig via ParseConfig to avoid ConnectConfig panics, understand pgx DSN option extraction and multi-host fallbacks, and handle pgx.ErrNoRows via errors.Is rather than direct sql.ErrNoRows pointer equality","kind":"HOW","packages":["pkg:golang/github.com/jackc/pgx/v5@v5.10.0"],"schemaVersion":1,"symbols":["github.com/jackc/pgx/v5.Conn","github.com/jackc/pgx/v5.ConnConfig","github.com/jackc/pgx/v5.ConnectConfig","github.com/jackc/pgx/v5.ErrNoRows","github.com/jackc/pgx/v5.ParseConfig","github.com/jackc/pgx/v5.Row"]},"contractCommand":["go","test","./..."],"environment":{"arch":"x64","ecosystem":"golang","executionContext":"go","language":"go","os":"linux","packageManager":"gomod","runtime":"go","schemaVersion":1},"license":"MIT-0","packages":["pkg:golang/github.com/jackc/pgx/v5@v5.10.0"],"schemaVersion":1,"symbols":["github.com/jackc/pgx/v5.Conn","github.com/jackc/pgx/v5.ConnConfig","github.com/jackc/pgx/v5.ConnectConfig","github.com/jackc/pgx/v5.ErrNoRows","github.com/jackc/pgx/v5.ParseConfig","github.com/jackc/pgx/v5.Row"],"verifierAdapter":"golang@1"}
module sample
go 1.26.5
require github.com/jackc/pgx/v5 v5.10.0
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
golang.org/x/text v0.29.0 // indirect
)
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
package sample
import (
"context"
"net"
"testing"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgproto3"
)
// newMockConn creates an in-memory *pgx.Conn backed by net.Pipe.
// The mock PostgreSQL backend answers startup and simulates empty result sets or specific queries.
func newMockConn(t *testing.T, modifyConfig func(*pgx.ConnConfig)) (*pgx.Conn, context.Context) {
t.Helper()
cfg, err := pgx.ParseConfig("postgres://example.user:secret@127.0.0.1:5432/testdb?sslmode=disable")
if err != nil {
t.Fatalf("pgx.ParseConfig failed: %v", err)
}
if modifyConfig != nil {
modifyConfig(cfg)
}
cfg.DialFunc = func(ctx context.Context, _, _ string) (net.Conn, error) {
client, server := net.Pipe()
go func(conn net.Conn) {
defer conn.Close()
b := pgproto3.NewBackend(conn, conn)
// 1. Startup handshake
msg, err := b.ReceiveStartupMessage()
if err != nil || msg == nil {
return
}
if _, ok := msg.(*pgproto3.SSLRequest); ok {
_, _ = conn.Write([]byte{'N'}) // Reject SSL
msg, err = b.ReceiveStartupMessage()
if err != nil || msg == nil {
return
}
}
b.Send(&pgproto3.AuthenticationOk{})
b.Send(&pgproto3.ReadyForQuery{TxStatus: 'I'})
if err := b.Flush(); err != nil {
return
}
// 2. Query message loop
for {
req, err := b.Receive()
if err != nil || req == nil {
return
}
switch msg := req.(type) {
case *pgproto3.Terminate:
return
case *pgproto3.Query:
// Simple protocol query returning 0 rows (CommandComplete)
b.Send(&pgproto3.RowDescription{
Fields: []pgproto3.FieldDescription{
{Name: []byte("val"), DataTypeOID: 25, DataTypeSize: -1, TypeModifier: -1, Format: 0},
},
})
b.Send(&pgproto3.CommandComplete{CommandTag: []byte("SELECT 0")})
b.Send(&pgproto3.ReadyForQuery{TxStatus: 'I'})
_ = b.Flush()
case *pgproto3.Parse:
b.Send(&pgproto3.ParseComplete{})
_ = b.Flush()
case *pgproto3.Describe:
if msg.ObjectType == 'S' {
b.Send(&pgproto3.ParameterDescription{ParameterOIDs: []uint32{23}})
}
b.Send(&pgproto3.RowDescription{
Fields: []pgproto3.FieldDescription{
{Name: []byte("val"), DataTypeOID: 25, DataTypeSize: -1, TypeModifier: -1, Format: 0},
},
})
_ = b.Flush()
case *pgproto3.Bind:
b.Send(&pgproto3.BindComplete{})
_ = b.Flush()
case *pgproto3.Execute:
// Return 0 rows, then complete
b.Send(&pgproto3.CommandComplete{CommandTag: []byte("SELECT 0")})
_ = b.Flush()
case *pgproto3.Sync:
b.Send(&pgproto3.ReadyForQuery{TxStatus: 'I'})
_ = b.Flush()
}
}
}(server)
return client, nil
}
ctx := context.Background()
conn, err := pgx.ConnectConfig(ctx, cfg)
if err != nil {
t.Fatalf("pgx.ConnectConfig failed: %v", err)
}
t.Cleanup(func() {
_ = conn.Close(ctx)
})
return conn, ctx
}
package sample
import (
"context"
"database/sql"
"errors"
"net"
"strings"
"testing"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgtype"
)
func TestConnConfig_ManualInitPanics(t *testing.T) {
// A naive model often constructs &pgx.ConnConfig manually.
// In pgx v5, ConnectConfig explicitly checks an internal createdByParseConfig flag and PANICS.
manualCfg := &pgx.ConnConfig{
Config: pgconn.Config{
Host: "192.0.2.1",
Port: 5432,
},
}
panicked := false
func() {
defer func() {
if r := recover(); r != nil {
panicked = true
msg, ok := r.(string)
if !ok || msg != "config must be created by ParseConfig" {
t.Fatalf("unexpected panic message: %v", r)
}
}
}()
_, _ = pgx.ConnectConfig(context.Background(), manualCfg)
}()
if !panicked {
t.Fatalf("expected pgx.ConnectConfig with manually initialized ConnConfig to panic")
}
// Parsing via ParseConfig (even with empty string for defaults) sets the internal flag.
parsedCfg, err := pgx.ParseConfig("")
if err != nil {
t.Fatalf("ParseConfig(\"\") failed: %v", err)
}
// Copying the config preserves the validation flag.
copiedCfg := parsedCfg.Copy()
copiedCfg.Host = "192.0.2.1"
copiedCfg.Port = 5432
// Setting a dummy DialFunc prevents real network dial during ConnectConfig verification.
dialCalled := false
copiedCfg.DialFunc = func(ctx context.Context, network, addr string) (netConn net.Conn, err error) {
dialCalled = true
return nil, errors.New("abort dial after validation")
}
_, err = pgx.ConnectConfig(context.Background(), copiedCfg)
if err == nil || !strings.Contains(err.Error(), "abort dial after validation") {
t.Fatalf("expected dial abort error, got: %v", err)
}
if !dialCalled {
t.Fatalf("expected DialFunc to be reached without panic")
}
}
func TestParseConfig_PgxOptionsVsRuntimeParams(t *testing.T) {
dsn := "postgres://example.com/mydb?default_query_exec_mode=simple_protocol&statement_cache_capacity=128&description_cache_capacity=64&search_path=myschema&application_name=myapp"
cfg, err := pgx.ParseConfig(dsn)
if err != nil {
t.Fatalf("ParseConfig failed: %v", err)
}
// pgx-specific options are extracted into typed struct fields
if cfg.DefaultQueryExecMode != pgx.QueryExecModeSimpleProtocol {
t.Fatalf("expected DefaultQueryExecMode SimpleProtocol, got: %v", cfg.DefaultQueryExecMode)
}
if cfg.StatementCacheCapacity != 128 {
t.Fatalf("expected StatementCacheCapacity 128, got: %d", cfg.StatementCacheCapacity)
}
if cfg.DescriptionCacheCapacity != 64 {
t.Fatalf("expected DescriptionCacheCapacity 64, got: %d", cfg.DescriptionCacheCapacity)
}
// pgx-specific options are stripped from RuntimeParams so they are not sent as PostgreSQL session settings
if _, exists := cfg.RuntimeParams["default_query_exec_mode"]; exists {
t.Fatalf("expected default_query_exec_mode to be stripped from RuntimeParams")
}
if _, exists := cfg.RuntimeParams["statement_cache_capacity"]; exists {
t.Fatalf("expected statement_cache_capacity to be stripped from RuntimeParams")
}
if _, exists := cfg.RuntimeParams["description_cache_capacity"]; exists {
t.Fatalf("expected description_cache_capacity to be stripped from RuntimeParams")
}
// Standard PostgreSQL parameters remain in RuntimeParams
if cfg.RuntimeParams["search_path"] != "myschema" {
t.Fatalf("expected search_path in RuntimeParams, got: %q", cfg.RuntimeParams["search_path"])
}
if cfg.RuntimeParams["application_name"] != "myapp" {
t.Fatalf("expected application_name in RuntimeParams, got: %q", cfg.RuntimeParams["application_name"])
}
// Invalid default_query_exec_mode produces a ParseConfigError
_, err = pgx.ParseConfig("postgres://example.com/mydb?default_query_exec_mode=invalid_mode")
if err == nil {
t.Fatalf("expected error for invalid default_query_exec_mode")
}
var parseErr *pgconn.ParseConfigError
if !errors.As(err, &parseErr) {
t.Fatalf("expected error to be *pgconn.ParseConfigError, got: %T (%v)", err, err)
}
}
func TestParseConfig_MultiHostFallbacks(t *testing.T) {
// 1. With sslmode=disable, secondary hosts create 1 fallback entry
dsnDisable := "postgres://example.user:secret@host1.example.com:5432,host2.example.com:5433/mydb?sslmode=disable"
cfgDisable, err := pgx.ParseConfig(dsnDisable)
if err != nil {
t.Fatalf("ParseConfig multi-host failed: %v", err)
}
// Primary host is set directly in Host and Port
if cfgDisable.Host != "host1.example.com" || cfgDisable.Port != 5432 {
t.Fatalf("expected primary host1.example.com:5432, got %s:%d", cfgDisable.Host, cfgDisable.Port)
}
// Secondary host is stored in Fallbacks slice
if len(cfgDisable.Fallbacks) != 1 {
t.Fatalf("expected exactly 1 fallback host with sslmode=disable, got: %d", len(cfgDisable.Fallbacks))
}
if cfgDisable.Fallbacks[0].Host != "host2.example.com" || cfgDisable.Fallbacks[0].Port != 5433 {
t.Fatalf("expected fallback host2.example.com:5433, got %s:%d", cfgDisable.Fallbacks[0].Host, cfgDisable.Fallbacks[0].Port)
}
// 2. With default sslmode=prefer, pgconn generates TLS and non-TLS fallback combinations (total 3 fallbacks for 2 hosts)
dsnPrefer := "postgres://example.user:secret@host1.example.com:5432,host2.example.com:5433/mydb"
cfgPrefer, err := pgx.ParseConfig(dsnPrefer)
if err != nil {
t.Fatalf("ParseConfig default prefer failed: %v", err)
}
if len(cfgPrefer.Fallbacks) != 3 {
t.Fatalf("expected 3 fallbacks with default sslmode=prefer, got: %d", len(cfgPrefer.Fallbacks))
}
}
func TestErrNoRows_ProxyErrorBehavior(t *testing.T) {
// 1. Direct pointer equality with sql.ErrNoRows fails because pgx.ErrNoRows is a proxy error struct
if pgx.ErrNoRows == sql.ErrNoRows {
t.Fatalf("expected pgx.ErrNoRows != sql.ErrNoRows by direct comparison")
}
// 2. errors.Is matches both pgx.ErrNoRows and sql.ErrNoRows via Unwrap()
if !errors.Is(pgx.ErrNoRows, sql.ErrNoRows) {
t.Fatalf("expected errors.Is(pgx.ErrNoRows, sql.ErrNoRows) to be true")
}
if !errors.Is(pgx.ErrNoRows, pgx.ErrNoRows) {
t.Fatalf("expected errors.Is(pgx.ErrNoRows, pgx.ErrNoRows) to be true")
}
// 3. Error messages differ: pgx strips the "sql: " prefix
if pgx.ErrNoRows.Error() != "no rows in result set" {
t.Fatalf("expected %q, got %q", "no rows in result set", pgx.ErrNoRows.Error())
}
if sql.ErrNoRows.Error() != "sql: no rows in result set" {
t.Fatalf("expected %q, got %q", "sql: no rows in result set", sql.ErrNoRows.Error())
}
}
func TestRow_ScanErrNoRowsAndDriverBytes(t *testing.T) {
conn, ctx := newMockConn(t, nil)
// 1. Query returning 0 rows yields pgx.ErrNoRows from Row.Scan
row := conn.QueryRow(ctx, "SELECT val FROM items WHERE id = $1", 123)
var val string
err := row.Scan(&val)
if err == nil {
t.Fatalf("expected error from empty result set scan, got nil")
}
// Direct equality with pgx.ErrNoRows matches
if err != pgx.ErrNoRows {
t.Fatalf("expected err == pgx.ErrNoRows, got %v", err)
}
// Direct equality with sql.ErrNoRows FAILS (the common bug in naive code)
if err == sql.ErrNoRows {
t.Fatalf("expected err != sql.ErrNoRows by direct pointer comparison")
}
// errors.Is matches sql.ErrNoRows
if !errors.Is(err, sql.ErrNoRows) {
t.Fatalf("expected errors.Is(err, sql.ErrNoRows) to be true")
}
// 2. Attempting to scan into *pgtype.DriverBytes from QueryRow is explicitly forbidden
driverRow := conn.QueryRow(ctx, "SELECT val FROM items WHERE id = $1", 123)
var driverBytes pgtype.DriverBytes
driverErr := driverRow.Scan(&driverBytes)
if driverErr == nil || !strings.Contains(driverErr.Error(), "cannot scan into *pgtype.DriverBytes from QueryRow") {
t.Fatalf("expected cannot scan into *pgtype.DriverBytes error, got: %v", driverErr)
}
}