Skip to content

Commit 859f104

Browse files
committed
Test CockroachDB
1 parent 12e464c commit 859f104

File tree

17 files changed

+182
-50
lines changed

17 files changed

+182
-50
lines changed

.github/workflows/test.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,27 @@ jobs:
7474
echo PGPORT=6432 'PQTEST_BINARY_PARAMETERS=yes go test -race ./...'
7575
PGPORT=6432 PQTEST_BINARY_PARAMETERS=yes go test -race ./...
7676
77+
cockroach:
78+
runs-on: 'ubuntu-latest'
79+
steps:
80+
- uses: 'actions/checkout@v6'
81+
- uses: 'actions/setup-go@v6'
82+
with: {go-version: '1.26'}
83+
- name: 'Start CockroachDB'
84+
run: |
85+
docker compose up cockroach -d --wait || {
86+
docker compose logs
87+
exit 1
88+
}
89+
echo '127.0.0.1 postgres postgres-invalid' | sudo tee -a /etc/hosts
90+
- name: 'Run tests'
91+
run: |
92+
echo 'PGPORT=26257 PQTEST_BINARY_PARAMETERS=no go test -race -skip='^Example' ./...'
93+
PGPORT=26257 PQTEST_BINARY_PARAMETERS=no go test -race -skip='^Example' ./...
94+
95+
echo PGPORT=26257 'PQTEST_BINARY_PARAMETERS=yes go test -race -skip='^Example' ./...'
96+
PGPORT=26257 PQTEST_BINARY_PARAMETERS=yes go test -race -skip='^Example' ./...
97+
7798
# Disabled for now as it's kind of finecky and flaky.
7899
#pgpool:
79100
# runs-on: 'ubuntu-latest'

compose.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ services:
2020
'PGPOOL_PARAMS_PORT': '7432'
2121
'PGPOOL_PARAMS_BACKEND_HOSTNAME0': 'pg18'
2222

23+
cockroach:
24+
profiles: ['cockroach']
25+
image: 'cockroachdb/cockroach:latest-v26.1'
26+
ports: ['127.0.0.1:26257:26257']
27+
volumes: ['./testdata/cockroach:/docker-entrypoint-initdb.d', './testdata/ssl:/ssl']
28+
command: ['start-single-node', '--accept-sql-without-tls', '--certs-dir=/ssl2']
29+
healthcheck: {test: ['CMD-SHELL', '/cockroach/cockroach node status --insecure --user=pqgo'], start_period: '30s', start_interval: '1s'}
30+
2331
pg18:
2432
image: 'postgres:18'
2533
ports: ['127.0.0.1:5432:5432']

conn_test.go

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
)
2929

3030
func TestReconnect(t *testing.T) {
31+
pqtest.SkipCockroach(t) // Doesn't implement pg_terminate_backend()
3132
t.Parallel()
3233
db := pqtest.MustDB(t)
3334
tx := pqtest.Begin(t, db)
@@ -655,6 +656,7 @@ func TestErrorOnQueryRowSimpleQuery(t *testing.T) {
655656

656657
// Test the QueryRow bug workarounds in stmt.exec() and simpleQuery()
657658
func TestQueryRowBugWorkaround(t *testing.T) {
659+
pqtest.SkipCockroach(t) // check_function_bodies=false doesn't really work
658660
db := pqtest.MustDB(t)
659661

660662
pqtest.Exec(t, db, "create temp table notnulltemp (a varchar(10) not null)")
@@ -805,7 +807,7 @@ func TestExecNoData(t *testing.T) { // See #186
805807

806808
// Exec() a query which returns results
807809
pqtest.Exec(t, db, "values (1), (2), (3)")
808-
pqtest.Exec(t, db, "values ($1), ($2), ($3)", 1, 2, 3)
810+
pqtest.Exec(t, db, "values ($1::int), ($2::int), ($3::int)", 1, 2, 3)
809811

810812
// Query() a query which doesn't return any results
811813
tx := pqtest.Begin(t, db)
@@ -816,11 +818,13 @@ func TestExecNoData(t *testing.T) { // See #186
816818
t.Errorf("\nhave: %#v\nwant: %#v", have, want)
817819
}
818820

819-
// Get NoData from a parameterized query.
820-
pqtest.Exec(t, tx, `create rule nodata as on insert to foo do instead nothing`)
821-
have = pqtest.QueryRow[any](t, tx, `insert into foo values ($1)`, 1)
822-
if !reflect.DeepEqual(have, want) {
823-
t.Errorf("\nhave: %#v\nwant: %#v", have, want)
821+
if !pqtest.Cockroach() { // "unimplemented: this syntax (0A000)"
822+
// Get NoData from a parameterized query.
823+
pqtest.Exec(t, tx, `create rule nodata as on insert to foo do instead nothing`)
824+
have = pqtest.QueryRow[any](t, tx, `insert into foo values ($1)`, 1)
825+
if !reflect.DeepEqual(have, want) {
826+
t.Errorf("\nhave: %#v\nwant: %#v", have, want)
827+
}
824828
}
825829
}
826830

@@ -841,11 +845,18 @@ func TestIssue282(t *testing.T) {
841845
}
842846

843847
func TestFloatPrecision(t *testing.T) { // See #196
848+
// encode() sends float32 as a float64, which adds extra precision: 0.10000122338533401
849+
// This is done by driver.DefaultParameterConverter(); we can maybe fix it,
850+
// but it's really a cockroach bug.
851+
// https://github.com/cockroachdb/cockroach/issues/73743
852+
// https://github.com/cockroachdb/cockroach/issues/84326
853+
pqtest.SkipCockroach(t)
854+
844855
t.Parallel()
845856
db := pqtest.MustDB(t)
846857

847-
have := pqtest.Query[bool](t, db, `select '0.10000122'::float4 = $1 as f4, '35.03554004971999'::float8 = $2 as f8`,
848-
float32(0.10000122), float64(35.03554004971999))[0]
858+
have := pqtest.QueryRow[bool](t, db, `select '0.10000122'::float4 = $1::float4 as f4, '35.03554004971999'::float8 = $2 as f8`,
859+
float32(0.10000122), float64(35.03554004971999))
849860
want := map[string]bool{"f4": true, "f8": true}
850861
if !reflect.DeepEqual(have, want) {
851862
t.Errorf("\nhave: %#v\nwant: %#v", have, want)
@@ -1106,7 +1117,7 @@ func TestStmtQueryContext(t *testing.T) {
11061117
func() (context.Context, context.CancelFunc) {
11071118
return context.WithTimeout(context.Background(), 50*time.Millisecond)
11081119
},
1109-
`pq: canceling statement due to user request (57014)`,
1120+
`or:pq: canceling statement due to user request (57014)|pq: query execution canceled (57014)`,
11101121
},
11111122
{"select pg_sleep(0.05)",
11121123
func() (context.Context, context.CancelFunc) {
@@ -1150,7 +1161,7 @@ func TestStmtExecContext(t *testing.T) {
11501161
func() (context.Context, context.CancelFunc) {
11511162
return context.WithTimeout(context.Background(), 50*time.Millisecond)
11521163
},
1153-
`pq: canceling statement due to user request (57014)`,
1164+
`or:pq: canceling statement due to user request (57014)|pq: query execution canceled (57014)`,
11541165
},
11551166
{"select pg_sleep(0.05)",
11561167
func() (context.Context, context.CancelFunc) {
@@ -1361,6 +1372,12 @@ func TestTxOptions(t *testing.T) {
13611372
// go18_test.go:306: read/[write,only] not set: true != off for level serializable
13621373
// go18_test.go:296: wrong isolation level: read committed != serializable
13631374
pqtest.SkipPgpool(t)
1375+
// TODO: fails with:
1376+
// conn_test.go:1724: wrong isolation level: read committed != read uncommitted
1377+
// conn_test.go:1724: wrong isolation level: read committed != read uncommitted
1378+
// conn_test.go:1724: wrong isolation level: serializable != repeatable read
1379+
// conn_test.go:1724: wrong isolation level: serializable != repeatable read
1380+
pqtest.SkipCockroach(t)
13641381

13651382
db := pqtest.MustDB(t)
13661383
ctx := context.Background()
@@ -1424,7 +1441,8 @@ func TestTxOptions(t *testing.T) {
14241441

14251442
func TestPing(t *testing.T) {
14261443
t.Parallel()
1427-
pqtest.SkipPgpool(t) // TODO: hangs forever?
1444+
pqtest.SkipPgpool(t) // TODO: hangs forever?
1445+
pqtest.SkipCockroach(t) // Doesn't implement pg_terminate_backend()
14281446

14291447
ctx, cancel := context.WithCancel(context.Background())
14301448
defer cancel()
@@ -1534,19 +1552,19 @@ func TestAuth(t *testing.T) {
15341552
tests := []struct {
15351553
conn, wantErr string
15361554
}{
1537-
{"user=pqgomd5", `password authentication failed for user "pqgomd5"`},
1538-
{"user=pqgopassword", `empty password returned by client`},
1539-
{"user=pqgoscram", `password authentication failed for user "pqgoscram"`},
1555+
{"user=pqgomd5", `re:password authentication failed for user "?pqgomd5"?`},
1556+
{"user=pqgopassword", `or:empty password returned by client|password authentication failed for user pqgopassword`},
1557+
{"user=pqgoscram", `re:password authentication failed for user "?pqgoscram"?`},
15401558

1541-
{"user=pqgomd5 password=wrong", `password authentication failed for user "pqgomd5"`},
1542-
{"user=pqgopassword password=wrong", `password authentication failed for user "pqgopassword"`},
1543-
{"user=pqgoscram password=wrong", `password authentication failed for user "pqgoscram"`},
1559+
{"user=pqgomd5 password=wrong", `re:password authentication failed for user "?pqgomd5"?`},
1560+
{"user=pqgopassword password=wrong", `re:password authentication failed for user "?pqgopassword"?`},
1561+
{"user=pqgoscram password=wrong", `re:password authentication failed for user "?pqgoscram"?`},
15441562

15451563
{"user=pqgomd5 password=wordpass", ``},
15461564
{"user=pqgopassword password=wordpass", ``},
15471565
{"user=pqgoscram password=wordpass", ``},
15481566

1549-
{"user=pqgounknown password=wordpass", `role "pqgounknown" does not exist`},
1567+
{"user=pqgounknown password=wordpass", `or:role "pqgounknown" does not exist|password authentication failed for user pqgounknown`},
15501568
}
15511569

15521570
for _, tt := range tests {
@@ -1603,6 +1621,10 @@ func TestBytea(t *testing.T) {
16031621
}
16041622

16051623
func TestJSONRawMessage(t *testing.T) {
1624+
if pqtest.ForceBinaryParameters() {
1625+
// "expected JSONB version 1 (08P01)" – looks like it always expects jsonb (instead of json)?
1626+
pqtest.SkipCockroach(t) // TODO: can probably fix
1627+
}
16061628
db := pqtest.MustDB(t)
16071629

16081630
pqtest.Exec(t, db, `create temp table tbl (j json)`)
@@ -1611,10 +1633,11 @@ func TestJSONRawMessage(t *testing.T) {
16111633
// not converted to a PostgreSQL array. This was a bug in CheckNamedValue
16121634
// where named byte slice types would hit the reflect.Slice case and get
16131635
// incorrectly converted to a PostgreSQL array.
1614-
data := json.RawMessage(`{"key":"value"}`)
1636+
data := json.RawMessage(`{"key": "value"}`)
16151637
pqtest.Exec(t, db, `insert into tbl values ($1)`, data)
16161638

16171639
have := pqtest.QueryRow[json.RawMessage](t, db, `select j from tbl`)
1640+
have["j"] = bytes.ReplaceAll(have["j"], []byte(`":"`), []byte(`": "`)) // Cockroach adds a space
16181641
want := map[string]json.RawMessage{"j": data}
16191642
if !reflect.DeepEqual(have, want) {
16201643
t.Errorf("\nhave: %#v\nwant: %#v", have, want)

connector_test.go

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@ func TestNewConnector(t *testing.T) {
115115
})
116116

117117
t.Run("database=", func(t *testing.T) {
118+
// Cockroach allows connecting to any database name, although it does
119+
// give an error on DDL statements (but not "select;" / Ping()). Pretty
120+
// weird, but not really important to run this test there so whatever.
121+
pqtest.SkipCockroach(t)
122+
118123
want1, want2 := `or:pq: database "err" does not exist (3D000)|pq: no such database: err (08P01)`,
119124
`or:pq: database "two" does not exist (3D000)|pq: no such database: two (08P01)`
120125

@@ -194,34 +199,36 @@ func TestRuntimeParameters(t *testing.T) {
194199
want string
195200
wantErr string
196201
skipPgbouncer bool
202+
skipCockroach bool
197203
}{
198-
{"DOESNOTEXIST=foo", "", "", `or:unrecognized configuration parameter|unsupported startup parameter`, false},
204+
// cockroach does not error on unknown config options.
205+
{"DOESNOTEXIST=foo", "", "", `or:unrecognized configuration parameter|unsupported startup parameter`, false, true},
199206

200207
// we can only work with a specific value for these two
201-
{"client_encoding=SQL_ASCII", "", "", `unsupported client_encoding "SQL_ASCII": must be absent or "UTF8"`, false},
202-
{"datestyle='ISO, YDM'", "", "", `unsupported datestyle "ISO, YDM": must be absent or "ISO, MDY"`, false},
208+
{"client_encoding=SQL_ASCII", "", "", `unsupported client_encoding "SQL_ASCII": must be absent or "UTF8"`, false, false},
209+
{"datestyle='ISO, YDM'", "", "", `unsupported datestyle "ISO, YDM": must be absent or "ISO, MDY"`, false, false},
203210

204211
// "options" should work exactly as it does in libpq
205212
// Skipped on pgbouncer as it errors with:
206213
// pq: unsupported startup parameter in options: search_path
207-
{"options='-c search_path=pqgotest'", "search_path", "pqgotest", "", true},
214+
{"options='-c search_path=pqgotest'", "search_path", "pqgotest", "", true, false},
208215

209216
// pq should override client_encoding in this case
210217
// TODO: not set consistently with pgbouncer
211-
{"options='-c client_encoding=SQL_ASCII'", "client_encoding", "UTF8", "", true},
218+
{"options='-c client_encoding=SQL_ASCII'", "client_encoding", "UTF8", "", true, false},
212219

213220
// allow client_encoding to be set explicitly
214-
{"client_encoding=UTF8", "client_encoding", "UTF8", "", false},
221+
{"client_encoding=UTF8", "client_encoding", "UTF8", "", false, false},
215222

216223
// test a runtime parameter not supported by libpq
217224
// Skipped on pgbouncer as it errors with:
218-
// pq: unsupported startup parameter: work_mem
219-
{"work_mem='139kB'", "work_mem", "139kB", "", true},
225+
// pq: unsupported startup parameter: search_path (08P01)
226+
{"search_path='a, b'", "search_path", "a, b", "", true, false},
220227

221228
// test fallback_application_name
222-
{"application_name=foo fallback_application_name=bar", "application_name", "foo", "", false},
223-
{"application_name='' fallback_application_name=bar", "application_name", "", "", false},
224-
{"fallback_application_name=bar", "application_name", "bar", "", false},
229+
{"application_name=foo fallback_application_name=bar", "application_name", "foo", "", false, false},
230+
{"application_name='' fallback_application_name=bar", "application_name", "", "", false, false},
231+
{"fallback_application_name=bar", "application_name", "bar", "", false, false},
225232
}
226233

227234
pqtest.Unsetenv(t, "PGAPPNAME")
@@ -230,6 +237,9 @@ func TestRuntimeParameters(t *testing.T) {
230237
if tt.skipPgbouncer {
231238
pqtest.SkipPgbouncer(t)
232239
}
240+
if tt.skipCockroach {
241+
pqtest.SkipCockroach(t)
242+
}
233243

234244
db, err := pqtest.DB(t, tt.conninfo)
235245
if !pqtest.ErrorContains(err, tt.wantErr) {

copy.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/binary"
77
"errors"
88
"fmt"
9+
"os"
910
"sync"
1011

1112
"github.com/lib/pq/internal/proto"
@@ -121,9 +122,10 @@ func (ci *copyin) flush(buf []byte) error {
121122
if len(buf)-1 > proto.MaxUint32 {
122123
return errors.New("pq: too many columns")
123124
}
124-
// set message length (without message identifier)
125-
binary.BigEndian.PutUint32(buf[1:], uint32(len(buf)-1))
126-
125+
if debugProto {
126+
fmt.Fprintf(os.Stderr, "CLIENT → %-20s %5d %q\n", proto.RequestCode(buf[0]), len(buf)-5, buf[5:])
127+
}
128+
binary.BigEndian.PutUint32(buf[1:], uint32(len(buf)-1)) // Set message length (without message identifier).
127129
_, err := ci.cn.c.Write(buf)
128130
return err
129131
}

copy_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func TestCopyInError(t *testing.T) {
2121
}{
2222
{`copy tbl (num) from stdin with binary`, `only text format supported for COPY`},
2323
{"-- comment\n /* comment */ copy tbl (num) to stdout", `COPY TO is not supported`},
24-
{`copy syntax error`, `syntax error at or near "error" at column 13`},
24+
{`copy syntax error`, `or:syntax error at or near "error" at column 13|at or near "error": syntax error`},
2525
}
2626

2727
for _, tt := range tests {
@@ -164,6 +164,7 @@ func TestCopyInMultipleValues(t *testing.T) {
164164
}
165165

166166
func TestCopyInRaiseStmtTrigger(t *testing.T) {
167+
pqtest.SkipCockroach(t) // "unimplemented: cannot create user-defined functions under a temporary schema"
167168
t.Parallel()
168169
db := pqtest.MustDB(t)
169170
tx := pqtest.Begin(t, db)
@@ -199,6 +200,7 @@ func TestCopyInRaiseStmtTrigger(t *testing.T) {
199200
}
200201

201202
func TestCopyInTypes(t *testing.T) {
203+
pqtest.SkipCockroach(t) // https://github.com/cockroachdb/cockroach/issues/167309
202204
t.Parallel()
203205
db := pqtest.MustDB(t)
204206
tx := pqtest.Begin(t, db)
@@ -223,6 +225,8 @@ func TestCopyInTypes(t *testing.T) {
223225

224226
// Tests for connection errors in copyin.resploop()
225227
func TestCopyInRespLoopConnectionError(t *testing.T) {
228+
pqtest.SkipCockroach(t) // Doesn't implement pg_terminate_backend()
229+
226230
// Executes f in a backoff loop until it doesn't return an error. If this
227231
// doesn't happen within duration, t.Fatal is called with the latest error.
228232
retry := func(t *testing.T, duration time.Duration, f func() error) {

encode_test.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ func TestTimeScan(t *testing.T) {
5151
t.Parallel()
5252
for _, tt := range tests {
5353
t.Run(tt.in, func(t *testing.T) {
54+
if strings.Contains(tt.in, "Etc/") {
55+
pqtest.SkipCockroach(t) // https://github.com/cockroachdb/cockroach/issues/167310
56+
}
5457
have := pqtest.QueryRow[time.Time](t, db, fmt.Sprintf(`select $1::%s as t`, tt.typ), tt.in)["t"]
5558
if !tt.want.Equal(have) {
5659
t.Errorf("\nhave: %s\nwant: %s", have, tt.want)
@@ -248,21 +251,28 @@ func TestDecodeScan(t *testing.T) {
248251
wantErrBin string
249252
}{
250253
{`select $1::text`, []any{"hello\x00world"}, nil, `invalid byte sequence`, `X`},
254+
{`select $1::text`, []any{"hello\xffworld"}, nil, `re:invalid (byte|UTF-8) sequence`, `X`},
251255
{`select $1::text`, []any{[]byte("hello world")}, "hello world", ``, `X`},
252256
{`select $1::bytea`, []any{[]byte("hello world")}, []byte("hello world"), ``, `X`},
253257

254-
{`select $1::uuid`, []any{[]byte(uuid)}, []byte(uuid), ``, `pq: incorrect binary data format in bind parameter 1 (22P03)`},
255-
{`select $1::uuid`, []any{uuidb}, []byte(uuid), `invalid byte sequence`, ``},
258+
{`select $1::uuid`, []any{[]byte(uuid)}, []byte(uuid), ``, `or:incorrect binary data format in bind parameter 1 (22P03)|UUID must be exactly 16 bytes long, got 36 bytes (22P02)`},
259+
{`select $1::uuid`, []any{uuidb}, []byte(uuid), `or:invalid byte sequence|incorrect UUID length`, ``},
256260
{`select $1::uuid`, []any{uuid}, []byte(uuid), ``, `X`},
257261

258262
{`select $1::int`, []any{fmt.Append(nil, 12345678)}, int64(12345678), ``, `pq: incorrect binary data format in bind parameter 1 (22P03)`},
259-
{`select $1::int`, []any{[]byte{0x00, 0xbc, 0x61, 0x4e}}, int64(12345678), `invalid byte sequence`, ``},
263+
{`select $1::int`, []any{[]byte{0x00, 0xbc, 0x61, 0x4e}}, int64(12345678), `or:invalid byte sequence|could not parse "\x00\xbcaN" as type int4`, ``},
260264
}
261265

262266
t.Parallel()
263267
db := pqtest.MustDB(t)
264-
for _, tt := range tests {
268+
for i, tt := range tests {
265269
t.Run("", func(t *testing.T) {
270+
if s, ok := tt.params[0].(string); ok && strings.ContainsRune(s, 0x00) {
271+
pqtest.SkipCockroach(t) // https://github.com/cockroachdb/cockroach/issues/167287
272+
}
273+
if pqtest.ForceBinaryParameters() && i == 7 {
274+
pqtest.SkipCockroach(t) // TODO: no error? Don't really follow why
275+
}
266276
var have any
267277
err := db.QueryRow(tt.query, tt.params...).Scan(&have)
268278
wantErr := tt.wantErr

error_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ func TestErrorClass(t *testing.T) {
4646
}
4747

4848
func TestError(t *testing.T) {
49+
// Many error messages are quite different in Cockroach, are missing info, etc.
50+
// TODO: can probs do better than skipping it all outright.
51+
pqtest.SkipCockroach(t)
52+
4953
tests := []struct {
5054
in, want, wantDetail string
5155
}{

hstore/hstore_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
)
1212

1313
func TestHstore(t *testing.T) {
14+
pqtest.SkipCockroach(t) // "unimplemented: extension "hstore" is not yet supported (0A000)"
15+
1416
tr := strings.NewReplacer("\t", "", "\n", "", `\n`, "\n", `\t`, "\t")
1517
tests := []struct {
1618
in string

0 commit comments

Comments
 (0)