Skip to content

Commit f035ce6

Browse files
authored
Merge pull request #2824 from dolthub/zachmu/sql-driver-tests
sql driver tests
2 parents 49adb2c + 55c7601 commit f035ce6

46 files changed

Lines changed: 11405 additions & 1 deletion

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Test Go SQL Server Driver
2+
3+
on: [pull_request]
4+
5+
concurrency:
6+
group: ci-go-sql-server-driver-${{ github.event.pull_request.number || github.ref }}
7+
cancel-in-progress: true
8+
9+
jobs:
10+
test:
11+
name: Go SQL Server Driver tests
12+
defaults:
13+
run:
14+
shell: bash
15+
runs-on: ${{ matrix.os }}
16+
strategy:
17+
fail-fast: false
18+
matrix:
19+
os: [ ubuntu-22.04, macos-latest ]
20+
steps:
21+
- uses: actions/checkout@v6
22+
- name: Set up Go
23+
uses: actions/setup-go@v5
24+
with:
25+
go-version-file: go.mod
26+
id: go
27+
- name: Create CI Bin
28+
run: |
29+
mkdir -p ./.ci_bin
30+
echo "$(pwd)/.ci_bin" >> $GITHUB_PATH
31+
- name: Install ICU4C (MacOS)
32+
if: ${{ matrix.os == 'macos-latest' }}
33+
run: |
34+
dir=$(brew --cellar icu4c)
35+
dir="$dir"/$(ls "$dir")
36+
echo CGO_CPPFLAGS=-I$dir/include >> $GITHUB_ENV
37+
echo CGO_LDFLAGS=-L$dir/lib >> $GITHUB_ENV
38+
- name: Build SQL Syntax
39+
run: ./build.sh
40+
working-directory: ./postgres/parser
41+
shell: bash
42+
- name: Install DoltgreSQL
43+
working-directory: ./
44+
run: |
45+
go build -mod=readonly -o .ci_bin/doltgres ./cmd/doltgres
46+
echo "DOLTGRES_BIN_PATH=$(pwd)/.ci_bin/doltgres" >> $GITHUB_ENV
47+
- name: Test Go SQL Server Driver
48+
working-directory: ./integration-tests/go-sql-server-driver
49+
run: |
50+
go test --timeout=20m ./...
Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
// Copyright 2023 Dolthub, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"context"
19+
"database/sql"
20+
sqldriver "database/sql/driver"
21+
"fmt"
22+
"strings"
23+
"testing"
24+
"time"
25+
26+
"github.com/stretchr/testify/assert"
27+
"github.com/stretchr/testify/require"
28+
"golang.org/x/sync/errgroup"
29+
30+
driver "github.com/dolthub/doltgresql/integration-tests/go-sql-server-driver/driver"
31+
)
32+
33+
func TestConcurrentGC(t *testing.T) {
34+
t.Skip("Doltgres does not yet handle dolt_gc() concurrently with active write connections the way this test expects: writers fail with 'unexpected EOF' / connection resets rather than the retriable safepoint errors Dolt produces (behavioral difference)")
35+
t.Parallel()
36+
type dimension struct {
37+
names []string
38+
factors func(gcTest) []gcTest
39+
}
40+
commits := dimension{
41+
names: []string{"NoCommits", "WithCommits"},
42+
factors: func(base gcTest) []gcTest {
43+
no, yes := base, base
44+
no.commit = false
45+
yes.commit = true
46+
return []gcTest{no, yes}
47+
},
48+
}
49+
full := dimension{
50+
names: []string{"NotFull", "Full"},
51+
factors: func(base gcTest) []gcTest {
52+
no, yes := base, base
53+
no.full = false
54+
yes.full = true
55+
return []gcTest{no, yes}
56+
},
57+
}
58+
safepoint := dimension{
59+
names: []string{"KillConnections", "SessionAware"},
60+
factors: func(base gcTest) []gcTest {
61+
no, yes := base, base
62+
no.sessionAware = false
63+
yes.sessionAware = true
64+
return []gcTest{no, yes}
65+
},
66+
}
67+
var doDimensions func(t *testing.T, base gcTest, dims []dimension)
68+
doDimensions = func(t *testing.T, base gcTest, dims []dimension) {
69+
if len(dims) == 0 {
70+
base.run(t)
71+
return
72+
}
73+
dim, dims := dims[0], dims[1:]
74+
dimf := dim.factors(base)
75+
for i := range dim.names {
76+
t.Run(dim.names[i], func(t *testing.T) {
77+
t.Parallel()
78+
doDimensions(t, dimf[i], dims)
79+
})
80+
}
81+
}
82+
dimensions := []dimension{commits, full, safepoint}
83+
doDimensions(t, gcTest{
84+
numThreads: 8,
85+
duration: 10 * time.Second,
86+
}, dimensions)
87+
}
88+
89+
type gcTest struct {
90+
numThreads int
91+
duration time.Duration
92+
commit bool
93+
full bool
94+
sessionAware bool
95+
}
96+
97+
func (gct gcTest) createDB(t *testing.T, ctx context.Context, db *sql.DB) {
98+
conn, err := db.Conn(ctx)
99+
require.NoError(t, err)
100+
defer conn.Close()
101+
102+
// We're going to bootstrap the database with a table which has id, val, id == [0,7*1024], val == 0.
103+
_, err = conn.ExecContext(ctx, "create table vals (id int primary key, val int)")
104+
require.NoError(t, err)
105+
vals := []string{}
106+
for i := 0; i <= (gct.numThreads-1)*1024; i++ {
107+
vals = append(vals, fmt.Sprintf("(%d,0)", i))
108+
}
109+
_, err = conn.ExecContext(ctx, "insert into vals values "+strings.Join(vals, ","))
110+
require.NoError(t, err)
111+
_, err = conn.ExecContext(ctx, "select dolt_commit('-Am', 'create vals table')")
112+
require.NoError(t, err)
113+
}
114+
115+
// When running with kill_connections GC safepoints, asserts that the
116+
// error we got is not an error that was not allowed.
117+
func assertExpectedGCError(t *testing.T, err error) bool {
118+
if !assert.NotContains(t, err.Error(), "dangling ref") {
119+
return false
120+
}
121+
if !assert.NotContains(t, err.Error(), "is unexpected noms value") {
122+
return false
123+
}
124+
if !assert.NotContains(t, err.Error(), "interface conversion: types.Value is nil") {
125+
return false
126+
}
127+
return true
128+
}
129+
130+
func (gct gcTest) doUpdate(t *testing.T, ctx context.Context, db *sql.DB, i int) error {
131+
conn, err := db.Conn(ctx)
132+
if gct.sessionAware {
133+
if !assert.NoError(t, err) {
134+
return nil
135+
}
136+
} else if err != nil {
137+
if !assert.NotContains(t, err.Error(), "connection refused") {
138+
return err
139+
}
140+
t.Logf("err in Conn: %v", err)
141+
return nil
142+
}
143+
defer conn.Close()
144+
145+
tx, err := conn.BeginTx(ctx, nil)
146+
if gct.sessionAware {
147+
assert.NoError(t, err)
148+
}
149+
if err != nil {
150+
// Ignore and try with a different connection...
151+
return nil
152+
}
153+
defer tx.Rollback()
154+
_, err = tx.ExecContext(ctx, "update vals set val = val+1 where id = $1", i)
155+
if gct.sessionAware {
156+
assert.NoError(t, err)
157+
} else if err != nil {
158+
if !assertExpectedGCError(t, err) {
159+
return err
160+
}
161+
t.Logf("err in Exec update: %v", err)
162+
}
163+
if err != nil {
164+
// Early return so we do not try to continue using the
165+
// broken connection or attempt to commit something
166+
// with no changes.
167+
return nil
168+
}
169+
if gct.commit {
170+
_, err = tx.ExecContext(ctx, fmt.Sprintf("select dolt_commit('-am', 'increment vals id = %d')", i))
171+
if gct.sessionAware {
172+
assert.NoError(t, err)
173+
} else if err != nil {
174+
if !assertExpectedGCError(t, err) {
175+
return err
176+
}
177+
t.Logf("err in Exec select dolt_commit: %v", err)
178+
}
179+
} else {
180+
err = tx.Commit()
181+
if gct.sessionAware {
182+
assert.NoError(t, err)
183+
} else if err != nil {
184+
if !assertExpectedGCError(t, err) {
185+
return err
186+
}
187+
t.Logf("err in tx commit: %v", err)
188+
}
189+
}
190+
return nil
191+
}
192+
193+
func (gct gcTest) doGC(t *testing.T, ctx context.Context, db *sql.DB) error {
194+
conn, err := db.Conn(ctx)
195+
if gct.sessionAware {
196+
if !assert.NoError(t, err) {
197+
return nil
198+
}
199+
} else if err != nil {
200+
if !assert.NotContains(t, err.Error(), "connection refused") {
201+
return err
202+
}
203+
t.Logf("err in Conn for dolt_gc: %v", err)
204+
return nil
205+
}
206+
if !gct.sessionAware {
207+
defer func() {
208+
// After calling dolt_gc, the connection is bad. Remove it from the connection pool.
209+
conn.Raw(func(_ any) error {
210+
return sqldriver.ErrBadConn
211+
})
212+
}()
213+
} else {
214+
defer conn.Close()
215+
}
216+
b := time.Now()
217+
if !gct.full {
218+
_, err = conn.ExecContext(ctx, "select dolt_gc()")
219+
} else {
220+
_, err = conn.ExecContext(ctx, `select dolt_gc('--full')`)
221+
}
222+
if assert.NoError(t, err) {
223+
t.Logf("successful dolt_gc took %v", time.Since(b))
224+
}
225+
return nil
226+
}
227+
228+
func (gct gcTest) finalize(t *testing.T, ctx context.Context, db *sql.DB) {
229+
conn, err := db.Conn(ctx)
230+
require.NoError(t, err)
231+
var ids []any
232+
var qmarks []string
233+
n := 1
234+
for i := 0; i < gct.numThreads*1024; i += 1024 {
235+
ids = append(ids, i)
236+
qmarks = append(qmarks, fmt.Sprintf("$%d", n))
237+
n++
238+
}
239+
rows, err := conn.QueryContext(ctx, fmt.Sprintf("select val from vals where id in (%s)", strings.Join(qmarks, ",")), ids...)
240+
require.NoError(t, err)
241+
i := 0
242+
cnt := 0
243+
for rows.Next() {
244+
var val int
245+
i += 1
246+
require.NoError(t, rows.Scan(&val))
247+
cnt += val
248+
}
249+
require.Equal(t, len(ids), i)
250+
t.Logf("successfully updated val %d times", cnt)
251+
require.NoError(t, rows.Close())
252+
require.NoError(t, rows.Err())
253+
254+
rows, err = conn.QueryContext(ctx, "select count(*) from dolt_log")
255+
require.NoError(t, err)
256+
require.True(t, rows.Next())
257+
require.NoError(t, rows.Scan(&cnt))
258+
t.Logf("database has %d commit(s)", cnt)
259+
require.False(t, rows.Next())
260+
require.NoError(t, rows.Close())
261+
require.NoError(t, rows.Err())
262+
require.NoError(t, conn.Close())
263+
}
264+
265+
func (gct gcTest) run(t *testing.T) {
266+
ports := newPorts(t)
267+
u, err := driver.NewDoltUser()
268+
require.NoError(t, err)
269+
t.Cleanup(func() {
270+
u.Cleanup()
271+
})
272+
273+
rs, err := u.MakeRepoStore()
274+
require.NoError(t, err)
275+
276+
_, err = rs.MakeRepo("concurrent_gc_test")
277+
require.NoError(t, err)
278+
279+
srvSettings := &driver.Server{
280+
Args: []string{},
281+
DynamicPort: "server_port",
282+
}
283+
if gct.sessionAware {
284+
srvSettings.Envs = append(srvSettings.Envs, "DOLT_GC_SAFEPOINT_CONTROLLER_CHOICE=session_aware")
285+
} else {
286+
srvSettings.Envs = append(srvSettings.Envs, "DOLT_GC_SAFEPOINT_CONTROLLER_CHOICE=kill_connections")
287+
}
288+
289+
server := StartServer(t, rs, "concurrent_gc_test", srvSettings, ports)
290+
291+
db, err := server.DB(driver.Connection{})
292+
require.NoError(t, err)
293+
defer db.Close()
294+
295+
gct.createDB(t, context.Background(), db)
296+
297+
start := time.Now()
298+
299+
eg, egCtx := errgroup.WithContext(context.Background())
300+
301+
// We're going to spawn 8 threads, each running mutations on their own part of the table...
302+
for i := 0; i < gct.numThreads; i++ {
303+
i := i * 1024
304+
eg.Go(func() error {
305+
for j := 0; time.Since(start) < gct.duration && egCtx.Err() == nil; j++ {
306+
if err := gct.doUpdate(t, egCtx, db, i); err != nil {
307+
return err
308+
}
309+
}
310+
return nil
311+
})
312+
}
313+
314+
// We spawn a thread which calls dolt_gc() periodically
315+
eg.Go(func() error {
316+
for time.Since(start) < gct.duration && egCtx.Err() == nil {
317+
if err := gct.doGC(t, egCtx, db); err != nil {
318+
return err
319+
}
320+
time.Sleep(100 * time.Millisecond)
321+
}
322+
return nil
323+
})
324+
325+
require.NoError(t, eg.Wait())
326+
327+
// Recreate the connection pool here, since idle connections in the
328+
// connection pool may be stale.
329+
db.Close()
330+
db, err = server.DB(driver.Connection{})
331+
require.NoError(t, err)
332+
gct.finalize(t, context.Background(), db)
333+
db.Close()
334+
}

0 commit comments

Comments
 (0)