chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
// Copyright 2023 Dolthub, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
sqldriver "database/sql/driver"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
driver "github.com/dolthub/doltgresql/integration-tests/go-sql-server-driver/driver"
|
||||
)
|
||||
|
||||
func TestConcurrentGC(t *testing.T) {
|
||||
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)")
|
||||
t.Parallel()
|
||||
type dimension struct {
|
||||
names []string
|
||||
factors func(gcTest) []gcTest
|
||||
}
|
||||
commits := dimension{
|
||||
names: []string{"NoCommits", "WithCommits"},
|
||||
factors: func(base gcTest) []gcTest {
|
||||
no, yes := base, base
|
||||
no.commit = false
|
||||
yes.commit = true
|
||||
return []gcTest{no, yes}
|
||||
},
|
||||
}
|
||||
full := dimension{
|
||||
names: []string{"NotFull", "Full"},
|
||||
factors: func(base gcTest) []gcTest {
|
||||
no, yes := base, base
|
||||
no.full = false
|
||||
yes.full = true
|
||||
return []gcTest{no, yes}
|
||||
},
|
||||
}
|
||||
safepoint := dimension{
|
||||
names: []string{"KillConnections", "SessionAware"},
|
||||
factors: func(base gcTest) []gcTest {
|
||||
no, yes := base, base
|
||||
no.sessionAware = false
|
||||
yes.sessionAware = true
|
||||
return []gcTest{no, yes}
|
||||
},
|
||||
}
|
||||
var doDimensions func(t *testing.T, base gcTest, dims []dimension)
|
||||
doDimensions = func(t *testing.T, base gcTest, dims []dimension) {
|
||||
if len(dims) == 0 {
|
||||
base.run(t)
|
||||
return
|
||||
}
|
||||
dim, dims := dims[0], dims[1:]
|
||||
dimf := dim.factors(base)
|
||||
for i := range dim.names {
|
||||
t.Run(dim.names[i], func(t *testing.T) {
|
||||
t.Parallel()
|
||||
doDimensions(t, dimf[i], dims)
|
||||
})
|
||||
}
|
||||
}
|
||||
dimensions := []dimension{commits, full, safepoint}
|
||||
doDimensions(t, gcTest{
|
||||
numThreads: 8,
|
||||
duration: 10 * time.Second,
|
||||
}, dimensions)
|
||||
}
|
||||
|
||||
type gcTest struct {
|
||||
numThreads int
|
||||
duration time.Duration
|
||||
commit bool
|
||||
full bool
|
||||
sessionAware bool
|
||||
}
|
||||
|
||||
func (gct gcTest) createDB(t *testing.T, ctx context.Context, db *sql.DB) {
|
||||
conn, err := db.Conn(ctx)
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
|
||||
// We're going to bootstrap the database with a table which has id, val, id == [0,7*1024], val == 0.
|
||||
_, err = conn.ExecContext(ctx, "create table vals (id int primary key, val int)")
|
||||
require.NoError(t, err)
|
||||
vals := []string{}
|
||||
for i := 0; i <= (gct.numThreads-1)*1024; i++ {
|
||||
vals = append(vals, fmt.Sprintf("(%d,0)", i))
|
||||
}
|
||||
_, err = conn.ExecContext(ctx, "insert into vals values "+strings.Join(vals, ","))
|
||||
require.NoError(t, err)
|
||||
_, err = conn.ExecContext(ctx, "select dolt_commit('-Am', 'create vals table')")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// When running with kill_connections GC safepoints, asserts that the
|
||||
// error we got is not an error that was not allowed.
|
||||
func assertExpectedGCError(t *testing.T, err error) bool {
|
||||
if !assert.NotContains(t, err.Error(), "dangling ref") {
|
||||
return false
|
||||
}
|
||||
if !assert.NotContains(t, err.Error(), "is unexpected noms value") {
|
||||
return false
|
||||
}
|
||||
if !assert.NotContains(t, err.Error(), "interface conversion: types.Value is nil") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (gct gcTest) doUpdate(t *testing.T, ctx context.Context, db *sql.DB, i int) error {
|
||||
conn, err := db.Conn(ctx)
|
||||
if gct.sessionAware {
|
||||
if !assert.NoError(t, err) {
|
||||
return nil
|
||||
}
|
||||
} else if err != nil {
|
||||
if !assert.NotContains(t, err.Error(), "connection refused") {
|
||||
return err
|
||||
}
|
||||
t.Logf("err in Conn: %v", err)
|
||||
return nil
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
tx, err := conn.BeginTx(ctx, nil)
|
||||
if gct.sessionAware {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
if err != nil {
|
||||
// Ignore and try with a different connection...
|
||||
return nil
|
||||
}
|
||||
defer tx.Rollback()
|
||||
_, err = tx.ExecContext(ctx, "update vals set val = val+1 where id = $1", i)
|
||||
if gct.sessionAware {
|
||||
assert.NoError(t, err)
|
||||
} else if err != nil {
|
||||
if !assertExpectedGCError(t, err) {
|
||||
return err
|
||||
}
|
||||
t.Logf("err in Exec update: %v", err)
|
||||
}
|
||||
if err != nil {
|
||||
// Early return so we do not try to continue using the
|
||||
// broken connection or attempt to commit something
|
||||
// with no changes.
|
||||
return nil
|
||||
}
|
||||
if gct.commit {
|
||||
_, err = tx.ExecContext(ctx, fmt.Sprintf("select dolt_commit('-am', 'increment vals id = %d')", i))
|
||||
if gct.sessionAware {
|
||||
assert.NoError(t, err)
|
||||
} else if err != nil {
|
||||
if !assertExpectedGCError(t, err) {
|
||||
return err
|
||||
}
|
||||
t.Logf("err in Exec select dolt_commit: %v", err)
|
||||
}
|
||||
} else {
|
||||
err = tx.Commit()
|
||||
if gct.sessionAware {
|
||||
assert.NoError(t, err)
|
||||
} else if err != nil {
|
||||
if !assertExpectedGCError(t, err) {
|
||||
return err
|
||||
}
|
||||
t.Logf("err in tx commit: %v", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gct gcTest) doGC(t *testing.T, ctx context.Context, db *sql.DB) error {
|
||||
conn, err := db.Conn(ctx)
|
||||
if gct.sessionAware {
|
||||
if !assert.NoError(t, err) {
|
||||
return nil
|
||||
}
|
||||
} else if err != nil {
|
||||
if !assert.NotContains(t, err.Error(), "connection refused") {
|
||||
return err
|
||||
}
|
||||
t.Logf("err in Conn for dolt_gc: %v", err)
|
||||
return nil
|
||||
}
|
||||
if !gct.sessionAware {
|
||||
defer func() {
|
||||
// After calling dolt_gc, the connection is bad. Remove it from the connection pool.
|
||||
conn.Raw(func(_ any) error {
|
||||
return sqldriver.ErrBadConn
|
||||
})
|
||||
}()
|
||||
} else {
|
||||
defer conn.Close()
|
||||
}
|
||||
b := time.Now()
|
||||
if !gct.full {
|
||||
_, err = conn.ExecContext(ctx, "select dolt_gc()")
|
||||
} else {
|
||||
_, err = conn.ExecContext(ctx, `select dolt_gc('--full')`)
|
||||
}
|
||||
if assert.NoError(t, err) {
|
||||
t.Logf("successful dolt_gc took %v", time.Since(b))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gct gcTest) finalize(t *testing.T, ctx context.Context, db *sql.DB) {
|
||||
conn, err := db.Conn(ctx)
|
||||
require.NoError(t, err)
|
||||
var ids []any
|
||||
var qmarks []string
|
||||
n := 1
|
||||
for i := 0; i < gct.numThreads*1024; i += 1024 {
|
||||
ids = append(ids, i)
|
||||
qmarks = append(qmarks, fmt.Sprintf("$%d", n))
|
||||
n++
|
||||
}
|
||||
rows, err := conn.QueryContext(ctx, fmt.Sprintf("select val from vals where id in (%s)", strings.Join(qmarks, ",")), ids...)
|
||||
require.NoError(t, err)
|
||||
i := 0
|
||||
cnt := 0
|
||||
for rows.Next() {
|
||||
var val int
|
||||
i += 1
|
||||
require.NoError(t, rows.Scan(&val))
|
||||
cnt += val
|
||||
}
|
||||
require.Equal(t, len(ids), i)
|
||||
t.Logf("successfully updated val %d times", cnt)
|
||||
require.NoError(t, rows.Close())
|
||||
require.NoError(t, rows.Err())
|
||||
|
||||
rows, err = conn.QueryContext(ctx, "select count(*) from dolt_log")
|
||||
require.NoError(t, err)
|
||||
require.True(t, rows.Next())
|
||||
require.NoError(t, rows.Scan(&cnt))
|
||||
t.Logf("database has %d commit(s)", cnt)
|
||||
require.False(t, rows.Next())
|
||||
require.NoError(t, rows.Close())
|
||||
require.NoError(t, rows.Err())
|
||||
require.NoError(t, conn.Close())
|
||||
}
|
||||
|
||||
func (gct gcTest) run(t *testing.T) {
|
||||
ports := newPorts(t)
|
||||
u, err := driver.NewDoltUser()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
u.Cleanup()
|
||||
})
|
||||
|
||||
rs, err := u.MakeRepoStore()
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = rs.MakeRepo("concurrent_gc_test")
|
||||
require.NoError(t, err)
|
||||
|
||||
srvSettings := &driver.Server{
|
||||
Args: []string{},
|
||||
DynamicPort: "server_port",
|
||||
}
|
||||
if gct.sessionAware {
|
||||
srvSettings.Envs = append(srvSettings.Envs, "DOLT_GC_SAFEPOINT_CONTROLLER_CHOICE=session_aware")
|
||||
} else {
|
||||
srvSettings.Envs = append(srvSettings.Envs, "DOLT_GC_SAFEPOINT_CONTROLLER_CHOICE=kill_connections")
|
||||
}
|
||||
|
||||
server := StartServer(t, rs, "concurrent_gc_test", srvSettings, ports)
|
||||
|
||||
db, err := server.DB(driver.Connection{})
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
gct.createDB(t, context.Background(), db)
|
||||
|
||||
start := time.Now()
|
||||
|
||||
eg, egCtx := errgroup.WithContext(context.Background())
|
||||
|
||||
// We're going to spawn 8 threads, each running mutations on their own part of the table...
|
||||
for i := 0; i < gct.numThreads; i++ {
|
||||
i := i * 1024
|
||||
eg.Go(func() error {
|
||||
for j := 0; time.Since(start) < gct.duration && egCtx.Err() == nil; j++ {
|
||||
if err := gct.doUpdate(t, egCtx, db, i); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// We spawn a thread which calls dolt_gc() periodically
|
||||
eg.Go(func() error {
|
||||
for time.Since(start) < gct.duration && egCtx.Err() == nil {
|
||||
if err := gct.doGC(t, egCtx, db); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
require.NoError(t, eg.Wait())
|
||||
|
||||
// Recreate the connection pool here, since idle connections in the
|
||||
// connection pool may be stale.
|
||||
db.Close()
|
||||
db, err = server.DB(driver.Connection{})
|
||||
require.NoError(t, err)
|
||||
gct.finalize(t, context.Background(), db)
|
||||
db.Close()
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// Copyright 2024 Dolthub, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
driver "github.com/dolthub/doltgresql/integration-tests/go-sql-server-driver/driver"
|
||||
)
|
||||
|
||||
// TestConcurrentWrites verifies concurrent write behavior and transaction locking in the SQL server driver.
|
||||
func TestConcurrentWrites(t *testing.T) {
|
||||
t.Parallel()
|
||||
ports := newPorts(t)
|
||||
u, err := driver.NewDoltUser()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
u.Cleanup()
|
||||
})
|
||||
|
||||
rs, err := u.MakeRepoStore()
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = rs.MakeRepo("concurrent_writes_test")
|
||||
require.NoError(t, err)
|
||||
|
||||
srvSettings := &driver.Server{
|
||||
Args: []string{},
|
||||
DynamicPort: "server_port",
|
||||
}
|
||||
server := StartServer(t, rs, "concurrent_writes_test", srvSettings, ports)
|
||||
|
||||
db, err := server.DB(driver.Connection{})
|
||||
require.NoError(t, err)
|
||||
db.SetMaxIdleConns(0)
|
||||
defer func() {
|
||||
require.NoError(t, db.Close())
|
||||
}()
|
||||
ctx := t.Context()
|
||||
func() {
|
||||
conn, err := db.Conn(ctx)
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
// Create table and initial data.
|
||||
_, err = conn.ExecContext(ctx, "CREATE TABLE data (id VARCHAR(64) PRIMARY KEY, worker INT, data TEXT, created_at TIMESTAMP)")
|
||||
require.NoError(t, err)
|
||||
_, err = conn.ExecContext(ctx, "SELECT DOLT_COMMIT('-Am', 'init with table')")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
nextInt := uint32(0)
|
||||
const numWriters = 32
|
||||
startCh := make(chan struct{})
|
||||
readyCh := make(chan struct{})
|
||||
for i := range numWriters {
|
||||
eg.Go(func() error {
|
||||
db, err := server.DB(driver.Connection{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
db.SetMaxOpenConns(1)
|
||||
conn, err := db.Conn(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
select {
|
||||
case readyCh <- struct{}{}:
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-startCh:
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
// Each thread writes 16.
|
||||
for j := range 16 {
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
key := fmt.Sprintf("main-%d-%d", i, j)
|
||||
_, err := conn.ExecContext(ctx, "INSERT INTO data VALUES ($1,$2,$3,$4)", key, i, key, time.Now())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
atomic.AddUint32(&nextInt, 1)
|
||||
_, err = conn.ExecContext(ctx, fmt.Sprintf("SELECT DOLT_COMMIT('-Am', 'insert %s')", key))
|
||||
if err != nil && !strings.Contains(err.Error(), "nothing to commit") {
|
||||
// Technically we can get "nothing to commit" because of how DOLT_COMMIT works.
|
||||
// It first commits this transaction to the working set and then commits the
|
||||
// merged working set as a dolt commit.
|
||||
//
|
||||
// If we ever fix DOLT_COMMIT, we should fix this exception here as well.
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
for range numWriters {
|
||||
select {
|
||||
case <-readyCh:
|
||||
case <-ctx.Done():
|
||||
// This will fail.
|
||||
require.NoError(t, eg.Wait())
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
close(startCh)
|
||||
require.NoError(t, eg.Wait())
|
||||
require.Equal(t, 512, int(nextInt))
|
||||
t.Logf("wrote %d", nextInt)
|
||||
ctx = t.Context()
|
||||
conn, err := db.Conn(ctx)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, conn.Close())
|
||||
}()
|
||||
var i int
|
||||
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM data").Scan(&i)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int(nextInt), i)
|
||||
err = conn.QueryRowContext(ctx, "SELECT COUNT(*) FROM dolt_log").Scan(&i)
|
||||
require.NoError(t, err)
|
||||
t.Logf("ended with %d commits", i)
|
||||
}
|
||||
@@ -0,0 +1,579 @@
|
||||
// Copyright 2024 Dolthub, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package driver
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/stdlib"
|
||||
)
|
||||
|
||||
var DoltgresPath string
|
||||
var DelvePath string
|
||||
|
||||
const TestUserName = "Bats Tests"
|
||||
const TestEmailAddress = "bats@email.fake"
|
||||
|
||||
const ConnectAttempts = 50
|
||||
const RetrySleepDuration = 50 * time.Millisecond
|
||||
|
||||
// EnvDoltgresBinPath is the environment variable used to locate the doltgres
|
||||
// binary used by these tests. If unset, "doltgres" on the PATH is used.
|
||||
const EnvDoltgresBinPath = "DOLTGRES_BIN_PATH"
|
||||
|
||||
func init() {
|
||||
path := os.Getenv(EnvDoltgresBinPath)
|
||||
if path == "" {
|
||||
path = "doltgres"
|
||||
}
|
||||
path = filepath.Clean(path)
|
||||
var err error
|
||||
|
||||
DoltgresPath, err = exec.LookPath(path)
|
||||
if err != nil {
|
||||
log.Printf("did not find doltgres binary: %v\n", err.Error())
|
||||
}
|
||||
|
||||
DelvePath, _ = exec.LookPath("dlv")
|
||||
}
|
||||
|
||||
// DoltUser is an abstraction for a user account that calls the `doltgres`
|
||||
// server. All of our doltgres binary invocations are done through DoltUser.
|
||||
//
|
||||
// For our purposes, it does the following:
|
||||
//
|
||||
// - owns a tmpdir, to which it sets DOLT_ROOT_PATH when invoking doltgres.
|
||||
//
|
||||
// - writes some initial dolt global config (user.name, user.email,
|
||||
// metrics.disabled = true) into that root path. Doltgres has no `config`
|
||||
// CLI command, so unlike Dolt we write the config file directly.
|
||||
//
|
||||
// - can create repo stores, which will be a tmpdir to store a data-dir
|
||||
// containing one or more databases.
|
||||
type DoltUser struct {
|
||||
tmpdir string
|
||||
}
|
||||
|
||||
var _ DoltCmdable = DoltUser{}
|
||||
var _ DoltDebuggable = DoltUser{}
|
||||
|
||||
func NewDoltUser() (DoltUser, error) {
|
||||
tmpdir, err := os.MkdirTemp("", "go-sql-server-driver-")
|
||||
if err != nil {
|
||||
return DoltUser{}, err
|
||||
}
|
||||
res := DoltUser{tmpdir}
|
||||
// Doltgres has no `config --global` CLI command, so we write the global
|
||||
// config file directly. Doltgres uses Dolt's env loading, which reads the
|
||||
// global config from $DOLT_ROOT_PATH/.dolt/config_global.json.
|
||||
cfgDir := filepath.Join(tmpdir, ".dolt")
|
||||
if err := os.MkdirAll(cfgDir, 0750); err != nil {
|
||||
return DoltUser{}, err
|
||||
}
|
||||
contents := fmt.Sprintf(`{"metrics.disabled":"true","user.name":%q,"user.email":%q}`+"\n", TestUserName, TestEmailAddress)
|
||||
if err := os.WriteFile(filepath.Join(cfgDir, "config_global.json"), []byte(contents), 0640); err != nil {
|
||||
return DoltUser{}, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (u DoltUser) DoltCmd(args ...string) *exec.Cmd {
|
||||
cmd := exec.Command(DoltgresPath, args...)
|
||||
cmd.Dir = u.tmpdir
|
||||
cmd.Env = append(os.Environ(), "DOLT_ROOT_PATH="+u.tmpdir)
|
||||
ApplyCmdAttributes(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (u DoltUser) DoltDebug(debuggerPort int, args ...string) *exec.Cmd {
|
||||
if DelvePath != "" {
|
||||
dlvArgs := []string{
|
||||
fmt.Sprintf("--listen=:%d", debuggerPort),
|
||||
"--headless",
|
||||
"--api-version=2",
|
||||
"--accept-multiclient",
|
||||
"exec",
|
||||
DoltgresPath,
|
||||
"--",
|
||||
}
|
||||
cmd := exec.Command(DelvePath, append(dlvArgs, args...)...)
|
||||
cmd.Dir = u.tmpdir
|
||||
cmd.Env = append(os.Environ(), "DOLT_ROOT_PATH="+u.tmpdir)
|
||||
ApplyCmdAttributes(cmd)
|
||||
return cmd
|
||||
} else {
|
||||
panic("dlv not found")
|
||||
}
|
||||
}
|
||||
|
||||
func (u DoltUser) MakeRepoStore() (RepoStore, error) {
|
||||
tmpdir, err := os.MkdirTemp(u.tmpdir, "repo-store-")
|
||||
if err != nil {
|
||||
return RepoStore{}, err
|
||||
}
|
||||
return RepoStore{u, tmpdir}, nil
|
||||
}
|
||||
|
||||
func (u DoltUser) Cleanup() error {
|
||||
return os.RemoveAll(u.tmpdir)
|
||||
}
|
||||
|
||||
type RepoStore struct {
|
||||
user DoltUser
|
||||
Dir string
|
||||
}
|
||||
|
||||
var _ DoltCmdable = RepoStore{}
|
||||
var _ DoltDebuggable = RepoStore{}
|
||||
|
||||
// MakeRepo creates a new doltgres database named |name| within this store's
|
||||
// data-dir. Because doltgres has no `init` CLI command, the database is
|
||||
// initialized by briefly running a doltgres server with DOLTGRES_DB set to the
|
||||
// database name and waiting for the on-disk database to be created.
|
||||
func (rs RepoStore) MakeRepo(name string) (Repo, error) {
|
||||
ret := Repo{rs.user, rs, filepath.Join(rs.Dir, name), name}
|
||||
err := rs.initDatabase(name, nil)
|
||||
if err != nil {
|
||||
return Repo{}, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (rs RepoStore) DoltCmd(args ...string) *exec.Cmd {
|
||||
cmd := rs.user.DoltCmd(args...)
|
||||
cmd.Dir = rs.Dir
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (rs RepoStore) DoltDebug(debuggerPort int, args ...string) *exec.Cmd {
|
||||
cmd := rs.user.DoltDebug(debuggerPort, args...)
|
||||
cmd.Dir = rs.Dir
|
||||
return cmd
|
||||
}
|
||||
|
||||
// initDatabase starts a short-lived doltgres server bound to a free port with
|
||||
// the data-dir set to this store, creates the database |name| if it does not
|
||||
// already exist, runs the optional |fn| against a connection to that database,
|
||||
// and then shuts the server down. This is the doltgres equivalent of
|
||||
// `dolt init` plus any pre-server setup such as adding remotes.
|
||||
func (rs RepoStore) initDatabase(name string, fn func(db *sql.DB) error) error {
|
||||
port, err := freePort()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfgPath := filepath.Join(rs.Dir, fmt.Sprintf(".init-%s-config.yaml", sanitize(name)))
|
||||
// The unix socket path must stay well under the OS limit (~108 chars), so
|
||||
// it is placed directly in the temp dir keyed by the (unique) port rather
|
||||
// than inside the potentially-long data-dir path.
|
||||
sockPath := filepath.Join(os.TempDir(), fmt.Sprintf("dg-init-%d.sock", port))
|
||||
cfgContents := fmt.Sprintf("log_level: warn\nlistener:\n host: 127.0.0.1\n port: %d\n socket: %s\n", port, sockPath)
|
||||
if err := os.WriteFile(cfgPath, []byte(cfgContents), 0640); err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(cfgPath)
|
||||
|
||||
cmd := rs.DoltCmd("--data-dir=.", "--config="+cfgPath)
|
||||
cmd.Env = append(cmd.Env, "DOLTGRES_DB="+name)
|
||||
output := new(bytes.Buffer)
|
||||
cmd.Stdout = output
|
||||
cmd.Stderr = output
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = cmd.Process.Signal(os.Interrupt)
|
||||
_, _ = cmd.Process.Wait()
|
||||
}()
|
||||
|
||||
db, err := ConnectDB("postgres", "password", name, "127.0.0.1", port, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not connect to init server for %s: %w (output: %s)", name, err, output.String())
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if _, err := db.Exec(fmt.Sprintf(`CREATE DATABASE IF NOT EXISTS %s`, name)); err != nil {
|
||||
return err
|
||||
}
|
||||
if fn != nil {
|
||||
if err := fn(db); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sanitize(s string) string {
|
||||
out := make([]rune, 0, len(s))
|
||||
for _, r := range s {
|
||||
if r == '/' || r == '\\' || r == ' ' {
|
||||
out = append(out, '_')
|
||||
} else {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// freePort asks the OS for an available TCP port on the loopback interface.
|
||||
func freePort() (int, error) {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer l.Close()
|
||||
return l.Addr().(*net.TCPAddr).Port, nil
|
||||
}
|
||||
|
||||
type Repo struct {
|
||||
user DoltUser
|
||||
rs RepoStore
|
||||
Dir string
|
||||
Name string
|
||||
}
|
||||
|
||||
func (r Repo) DoltCmd(args ...string) *exec.Cmd {
|
||||
cmd := r.user.DoltCmd(args...)
|
||||
cmd.Dir = r.Dir
|
||||
return cmd
|
||||
}
|
||||
|
||||
// CreateRemote adds a remote named |name| with the given |url| to this
|
||||
// database. Doltgres has no `remote` CLI command, so this is done by briefly
|
||||
// running a server and calling the dolt_remote() stored function.
|
||||
func (r Repo) CreateRemote(name, url string) error {
|
||||
return r.rs.initDatabase(r.Name, func(db *sql.DB) error {
|
||||
_, err := db.Exec(`SELECT dolt_remote('add', $1, $2)`, name, url)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
type SqlServer struct {
|
||||
Name string
|
||||
Done chan struct{}
|
||||
Cmd *exec.Cmd
|
||||
CmdWaitErr error
|
||||
Port int
|
||||
DebugPort int
|
||||
Output *bytes.Buffer
|
||||
DBName string
|
||||
RecreateCmd func(args ...string) *exec.Cmd
|
||||
|
||||
// Where to write server log output for display. If nil,
|
||||
// defaults to os.Stdout.
|
||||
LogWriter io.Writer
|
||||
|
||||
// If non-nil, called with each complete line of server output.
|
||||
OutputVisitor func(string)
|
||||
}
|
||||
|
||||
type SqlServerOpt func(s *SqlServer)
|
||||
|
||||
func WithArgs(args ...string) SqlServerOpt {
|
||||
return func(s *SqlServer) {
|
||||
s.Cmd.Args = append(s.Cmd.Args, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func WithName(name string) SqlServerOpt {
|
||||
return func(s *SqlServer) {
|
||||
s.Name = name
|
||||
}
|
||||
}
|
||||
|
||||
func WithEnvs(envs ...string) SqlServerOpt {
|
||||
return func(s *SqlServer) {
|
||||
s.Cmd.Env = append(s.Cmd.Env, envs...)
|
||||
}
|
||||
}
|
||||
|
||||
func WithPort(port int) SqlServerOpt {
|
||||
return func(s *SqlServer) {
|
||||
s.Port = port
|
||||
}
|
||||
}
|
||||
|
||||
func WithOutputVisitor(f func(string)) SqlServerOpt {
|
||||
return func(s *SqlServer) {
|
||||
s.OutputVisitor = f
|
||||
}
|
||||
}
|
||||
|
||||
func WithLogWriter(w io.Writer) SqlServerOpt {
|
||||
return func(s *SqlServer) {
|
||||
s.LogWriter = w
|
||||
}
|
||||
}
|
||||
|
||||
func WithDebugPort(port int) SqlServerOpt {
|
||||
return func(s *SqlServer) {
|
||||
s.DebugPort = port
|
||||
}
|
||||
}
|
||||
|
||||
type DoltCmdable interface {
|
||||
DoltCmd(args ...string) *exec.Cmd
|
||||
}
|
||||
|
||||
type DoltDebuggable interface {
|
||||
DoltDebug(debuggerPort int, args ...string) *exec.Cmd
|
||||
}
|
||||
|
||||
// StartSqlServer starts a doltgres server. Unlike Dolt, doltgres has no
|
||||
// `sql-server` subcommand; running the binary itself starts the server.
|
||||
func StartSqlServer(dc DoltCmdable, opts ...SqlServerOpt) (*SqlServer, error) {
|
||||
cmd := dc.DoltCmd()
|
||||
return runSqlServerCommand(dc, opts, cmd)
|
||||
}
|
||||
|
||||
func DebugSqlServer(dc DoltCmdable, debuggerPort int, opts ...SqlServerOpt) (*SqlServer, error) {
|
||||
ddb, ok := dc.(DoltDebuggable)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%T does not implement DoltDebuggable", dc)
|
||||
}
|
||||
|
||||
cmd := ddb.DoltDebug(debuggerPort)
|
||||
return runSqlServerCommand(dc, append(opts, WithDebugPort(debuggerPort)), cmd)
|
||||
}
|
||||
|
||||
func runSqlServerCommand(dc DoltCmdable, opts []SqlServerOpt, cmd *exec.Cmd) (*SqlServer, error) {
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmd.Stderr = cmd.Stdout
|
||||
output := new(bytes.Buffer)
|
||||
done := make(chan struct{})
|
||||
|
||||
server := &SqlServer{
|
||||
Done: done,
|
||||
Cmd: cmd,
|
||||
Port: 5432,
|
||||
Output: output,
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(server)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
server.CmdWaitErr = server.Cmd.Wait()
|
||||
close(done)
|
||||
}()
|
||||
logw := server.LogWriter
|
||||
if logw == nil {
|
||||
logw = os.Stdout
|
||||
}
|
||||
multiCopyWithNamePrefix(logw, output, stdout, server.Name, server.OutputVisitor)
|
||||
}()
|
||||
|
||||
server.RecreateCmd = func(args ...string) *exec.Cmd {
|
||||
if server.DebugPort > 0 {
|
||||
ddb, ok := dc.(DoltDebuggable)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("%T does not implement DoltDebuggable", dc))
|
||||
}
|
||||
return ddb.DoltDebug(server.DebugPort, args...)
|
||||
} else {
|
||||
return dc.DoltCmd(args...)
|
||||
}
|
||||
}
|
||||
|
||||
err = server.Cmd.Start()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func (s *SqlServer) ErrorStop() error {
|
||||
<-s.Done
|
||||
return s.CmdWaitErr
|
||||
}
|
||||
|
||||
func multiCopyWithNamePrefix(stdout, captured io.Writer, in io.Reader, name string, visitor func(string)) {
|
||||
reader := bufio.NewReader(in)
|
||||
multiOut := io.MultiWriter(stdout, captured)
|
||||
var lineBuf []byte
|
||||
wantsPrefix := true
|
||||
for {
|
||||
line, isPrefix, err := reader.ReadLine()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if wantsPrefix && name != "" {
|
||||
stdout.Write([]byte("["))
|
||||
stdout.Write([]byte(name))
|
||||
stdout.Write([]byte("] "))
|
||||
}
|
||||
multiOut.Write(line)
|
||||
if isPrefix {
|
||||
if visitor != nil {
|
||||
lineBuf = append(lineBuf, line...)
|
||||
}
|
||||
wantsPrefix = false
|
||||
} else {
|
||||
multiOut.Write([]byte("\n"))
|
||||
if visitor != nil {
|
||||
if lineBuf != nil {
|
||||
lineBuf = append(lineBuf, line...)
|
||||
visitor(string(lineBuf))
|
||||
lineBuf = nil
|
||||
} else {
|
||||
visitor(string(line))
|
||||
}
|
||||
}
|
||||
wantsPrefix = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SqlServer) Restart(newargs *[]string, newenvs *[]string) error {
|
||||
err := s.GracefulStop()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args := s.Cmd.Args[1:]
|
||||
if newargs != nil {
|
||||
args = *newargs
|
||||
}
|
||||
s.Cmd = s.RecreateCmd(args...)
|
||||
if newenvs != nil {
|
||||
s.Cmd.Env = append(s.Cmd.Env, (*newenvs)...)
|
||||
}
|
||||
stdout, err := s.Cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.CmdWaitErr = nil
|
||||
s.Cmd.Stderr = s.Cmd.Stdout
|
||||
s.Done = make(chan struct{})
|
||||
go func() {
|
||||
defer func() {
|
||||
s.CmdWaitErr = s.Cmd.Wait()
|
||||
close(s.Done)
|
||||
}()
|
||||
logw := s.LogWriter
|
||||
if logw == nil {
|
||||
logw = os.Stdout
|
||||
}
|
||||
multiCopyWithNamePrefix(logw, s.Output, stdout, s.Name, s.OutputVisitor)
|
||||
}()
|
||||
return s.Cmd.Start()
|
||||
}
|
||||
|
||||
func (s *SqlServer) DB(c Connection) (*sql.DB, error) {
|
||||
connector, err := s.Connector(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return OpenDB(connector)
|
||||
}
|
||||
|
||||
// Connector returns a database/sql driver.Connector for a connection to this
|
||||
// server using the pgx stdlib driver.
|
||||
func (s *SqlServer) Connector(c Connection) (driver.Connector, error) {
|
||||
pass, err := c.Password()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user := c.User
|
||||
if user == "" {
|
||||
user = "postgres"
|
||||
}
|
||||
dsn := GetDSN(user, pass, s.DBName, "127.0.0.1", s.Port, c.DriverParams)
|
||||
cfg, err := pgx.ParseConfig(dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stdlib.GetConnector(*cfg), nil
|
||||
}
|
||||
|
||||
// GetDSN builds a postgres connection URL.
|
||||
func GetDSN(user, password, name, host string, port int, driverParams map[string]string) string {
|
||||
params := make(url.Values)
|
||||
params.Set("sslmode", "prefer")
|
||||
for k, v := range driverParams {
|
||||
params.Set(k, v)
|
||||
}
|
||||
u := url.URL{
|
||||
Scheme: "postgres",
|
||||
User: url.UserPassword(user, password),
|
||||
Host: fmt.Sprintf("%s:%d", host, port),
|
||||
Path: "/" + name,
|
||||
RawQuery: params.Encode(),
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func OpenDB(connector driver.Connector) (*sql.DB, error) {
|
||||
db := sql.OpenDB(connector)
|
||||
var err error
|
||||
for i := 0; i < ConnectAttempts; i++ {
|
||||
err = db.Ping()
|
||||
if err == nil {
|
||||
return db, nil
|
||||
}
|
||||
time.Sleep(RetrySleepDuration)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func ConnectDB(user, password, name, host string, port int, params map[string]string) (*sql.DB, error) {
|
||||
dsn := GetDSN(user, password, name, host, port, params)
|
||||
db, err := sql.Open("pgx", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := 0; i < ConnectAttempts; i++ {
|
||||
err = db.Ping()
|
||||
if err == nil {
|
||||
return db, nil
|
||||
}
|
||||
time.Sleep(RetrySleepDuration)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// WithConnectRetriesDisabled is retained for API-compatibility with the Dolt
|
||||
// test driver. The mysql driver performed automatic connection retries that
|
||||
// some tests needed to circumvent; the pgx stdlib driver does not perform the
|
||||
// same retries, so this currently returns the context unchanged.
|
||||
//
|
||||
// TODO: If we add tests that require precise control over connection-retry
|
||||
// behavior against doltgres (e.g. max-connection tests), implement an
|
||||
// equivalent hook here.
|
||||
func WithConnectRetriesDisabled(ctx context.Context) context.Context {
|
||||
return ctx
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright 2024 Dolthub, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd
|
||||
// +build darwin dragonfly freebsd linux netbsd openbsd
|
||||
|
||||
package driver
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func ApplyCmdAttributes(cmd *exec.Cmd) {
|
||||
// nothing to do on unix / darwin
|
||||
}
|
||||
|
||||
func (s *SqlServer) GracefulStop() error {
|
||||
select {
|
||||
case <-s.Done:
|
||||
return s.CmdWaitErr
|
||||
default:
|
||||
}
|
||||
err := s.Cmd.Process.Signal(syscall.SIGTERM)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
<-s.Done
|
||||
return s.CmdWaitErr
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2024 Dolthub, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package driver
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func ApplyCmdAttributes(cmd *exec.Cmd) {
|
||||
// Creating a new process group for the process will allow GracefulStop to send the break signal to that process
|
||||
// without also killing the parent process
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SqlServer) GracefulStop() error {
|
||||
err := windows.GenerateConsoleCtrlEvent(windows.CTRL_BREAK_EVENT, uint32(s.Cmd.Process.Pid))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
<-s.Done
|
||||
|
||||
_, err = s.Cmd.Process.Wait()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
// Copyright 2024 Dolthub, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package driver
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/creasty/defaults"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// |Connection| represents a single connection to a sql-server instance defined
|
||||
// in the test. The connection will be established and every |Query| in
|
||||
// |Queries| will be run against it. At the end, the connection will be torn down.
|
||||
// If |RestartServer| is non-nil, the server which the connection targets will
|
||||
// be restarted after the connection is terminated.
|
||||
type Connection struct {
|
||||
On string `yaml:"on"`
|
||||
Queries []Query `yaml:"queries"`
|
||||
RestartServer *RestartArgs `yaml:"restart_server"`
|
||||
|
||||
// Rarely needed, allows the entire connection assertion to be retried
|
||||
// on an assertion failure. Use this is only for idempotent connection
|
||||
// interactions and only if the sql-server is prone to tear down the
|
||||
// connection based on things that are happening, such as cluster role
|
||||
// transitions.
|
||||
RetryAttempts int `yaml:"retry_attempts"`
|
||||
|
||||
// The user to connect as. For Doltgres this defaults to the Postgres
|
||||
// superuser, "postgres".
|
||||
User string `default:"postgres" yaml:"user"`
|
||||
// The password to connect with.
|
||||
Pass string `yaml:"password"`
|
||||
PassFile string `yaml:"password_file"`
|
||||
// Any driver params to pass in the connection string.
|
||||
DriverParams map[string]string `yaml:"driver_params"`
|
||||
}
|
||||
|
||||
func (c *Connection) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
defaults.Set(c)
|
||||
type plain Connection
|
||||
if err := unmarshal((*plain)(c)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Connection) Password() (string, error) {
|
||||
if c.PassFile != "" {
|
||||
passFile := c.PassFile
|
||||
if v := os.Getenv("TESTGENDIR"); v != "" {
|
||||
passFile = strings.ReplaceAll(passFile, "$TESTGENDIR", v)
|
||||
}
|
||||
bs, err := os.ReadFile(passFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(bs)), nil
|
||||
}
|
||||
// Doltgres connections default to the well-known development password
|
||||
// "password" when none is specified, matching DefaultPass in the
|
||||
// doltgres server config.
|
||||
if c.Pass == "" {
|
||||
return "password", nil
|
||||
}
|
||||
return c.Pass, nil
|
||||
}
|
||||
|
||||
// |RestartArgs| are possible arguments, to change the arguments which are
|
||||
// provided to the sql-server process when it is restarted. This is used, for
|
||||
// example, to change server config on a restart.
|
||||
type RestartArgs struct {
|
||||
Args *[]string `yaml:"args"`
|
||||
Envs *[]string `yaml:"envs"`
|
||||
}
|
||||
|
||||
// |TestRepo| represents an init'd doltgres database that is available to a
|
||||
// server instance. It can be created with some files and with remotes defined.
|
||||
// |Name| can include path components separated by `/`, which will create the
|
||||
// repository in a subdirectory.
|
||||
type TestRepo struct {
|
||||
Name string `yaml:"name"`
|
||||
WithFiles []WithFile `yaml:"with_files"`
|
||||
WithRemotes []WithRemote `yaml:"with_remotes"`
|
||||
|
||||
// Only valid on Test.Repos, not in Test.MultiRepos.Repos. If set, a
|
||||
// sql-server process will be run against this TestRepo. It will be
|
||||
// available as TestRepo.Name.
|
||||
Server *Server `yaml:"server"`
|
||||
ExternalServer *ExternalServer `yaml:"external-server"`
|
||||
}
|
||||
|
||||
// |MultiRepo| is a subdirectory where many |TestRepo|s can be defined. You can
|
||||
// start a sql-server on a |MultiRepo|, in which case there will be no default
|
||||
// database to connect to.
|
||||
type MultiRepo struct {
|
||||
Name string `yaml:"name"`
|
||||
Repos []TestRepo `yaml:"repos"`
|
||||
WithFiles []WithFile `yaml:"with_files"`
|
||||
|
||||
// If set, a sql-server process will be run against this TestRepo. It
|
||||
// will be available as MultiRepo.Name.
|
||||
Server *Server `yaml:"server"`
|
||||
}
|
||||
|
||||
// |WithRemote| defines remotes which should be defined on the repository
|
||||
// before the sql-server is started.
|
||||
type WithRemote struct {
|
||||
Name string `yaml:"name"`
|
||||
URL string `yaml:"url"`
|
||||
}
|
||||
|
||||
// |WithFile| defines a file and its contents to be created in a |Repo| or
|
||||
// |MultiRepo| before the servers are started.
|
||||
type WithFile struct {
|
||||
Name string `yaml:"name"`
|
||||
|
||||
// The contents of the file, provided inline in the YAML.
|
||||
Contents string `yaml:"contents"`
|
||||
|
||||
// A source file path to copy to |Name|. Mutually exclusive with
|
||||
// Contents.
|
||||
SourcePath string `yaml:"source_path"`
|
||||
|
||||
// If this is non-nil, the template will be applied to the
|
||||
// contents of the file as they are written through |WriteAtDir|.
|
||||
Template func(string) string
|
||||
}
|
||||
|
||||
func (f WithFile) WriteAtDir(dir string) error {
|
||||
path := filepath.Join(dir, f.Name)
|
||||
d := filepath.Dir(path)
|
||||
err := os.MkdirAll(d, 0750)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if f.SourcePath != "" {
|
||||
sourcePath := f.SourcePath
|
||||
if v := os.Getenv("TESTGENDIR"); v != "" {
|
||||
sourcePath = strings.ReplaceAll(sourcePath, "$TESTGENDIR", v)
|
||||
}
|
||||
source, err := os.Open(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer source.Close()
|
||||
dest, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0550)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
contents, err := io.ReadAll(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if f.Template != nil {
|
||||
str := f.Template(string(contents))
|
||||
contents = []byte(str)
|
||||
}
|
||||
_, err = dest.Write(contents)
|
||||
return err
|
||||
} else {
|
||||
contents := f.Contents
|
||||
if f.Template != nil {
|
||||
contents = f.Template(contents)
|
||||
}
|
||||
return os.WriteFile(path, []byte(contents), 0550)
|
||||
}
|
||||
}
|
||||
|
||||
// |Server| defines a sql-server process to start. |Name| must match the
|
||||
// top-level |Name| of a |TestRepo| or |MultiRepo|.
|
||||
type Server struct {
|
||||
Name string `yaml:"name"`
|
||||
Args []string `yaml:"args"`
|
||||
Envs []string `yaml:"envs"`
|
||||
|
||||
// The |Port| which the server will be running on. For now, it is up to
|
||||
// the |Args| or config file to make sure this is true. Defaults to 5432.
|
||||
Port int `yaml:"port"`
|
||||
|
||||
// This can be used with templating of dynamic ports to
|
||||
// specify the SQL listener port which will have been filled
|
||||
// in by a call to `{{get_port "server_name"}}` within the
|
||||
// config or args of the server.
|
||||
//
|
||||
// A |Port| != 0 with a |DynamicPort| != "" is an error.
|
||||
DynamicPort string `yaml:"dynamic_port"`
|
||||
|
||||
// DebugPort if set to a non-zero value will cause this server to be started with |dlv| listening for a debugger
|
||||
// connection on the port given.
|
||||
DebugPort int `yaml:"debug_port"`
|
||||
|
||||
// Assertions to be run against the log output of the server process
|
||||
// after the server process successfully terminates.
|
||||
LogMatches []string `yaml:"log_matches"`
|
||||
|
||||
// Assertions to be run against the log output of the server process
|
||||
// after the server process successfully terminates. Each entry is a
|
||||
// regex that must NOT match. The test fails if any pattern matches.
|
||||
LogNotMatches []string `yaml:"log_not_matches"`
|
||||
|
||||
// Assertions to be run against the log output of the server process
|
||||
// after the server process exits with an error. If |ErrorMatches| is
|
||||
// defined, then the server process must exit with a non-0 exit code
|
||||
// after it is launched. This will be asserted before any |Connections|
|
||||
// interactions are performed.
|
||||
ErrorMatches []string `yaml:"error_matches"`
|
||||
}
|
||||
|
||||
type ExternalServer struct {
|
||||
Name string `yaml:"name"`
|
||||
Host string `yaml:"host"`
|
||||
User string `yaml:"user"`
|
||||
Password string `yaml:"password"`
|
||||
// The |Port| which the server will be running on. For now, it is up to
|
||||
// the |Args| to make sure this is true. Defaults to 5432.
|
||||
Port int `yaml:"port"`
|
||||
}
|
||||
|
||||
// The primary interaction of a |Connection|. Either |Query| or |Exec| should
|
||||
// be set, not both.
|
||||
type Query struct {
|
||||
// Run a query against the connection.
|
||||
Query string `yaml:"query"`
|
||||
|
||||
// Run a command against the connection.
|
||||
Exec string `yaml:"exec"`
|
||||
|
||||
// Args to be passed as query parameters to either Query or Exec.
|
||||
Args []string `yaml:"args"`
|
||||
|
||||
// This can only be non-empty for a |Query|. Asserts the results of the
|
||||
// |Query|.
|
||||
Result QueryResult `yaml:"result"`
|
||||
|
||||
// If this is non-empty, asserts the |Query| or the |Exec|
|
||||
// generates an error that matches this string.
|
||||
ErrorMatch string `yaml:"error_match"`
|
||||
|
||||
// If this is non-zero, it represents the number of times to try the
|
||||
// |Query| or the |Exec| and to check its assertions before we fail the
|
||||
// test as a result of failed assertions. When interacting with queries
|
||||
// that introspect things like replication state, this can be used to
|
||||
// wait for quiescence in an inherently racey process. Interactions
|
||||
// will be delayed slightly between each failure.
|
||||
RetryAttempts int `yaml:"retry_attempts"`
|
||||
}
|
||||
|
||||
// |QueryResult| specifies assertions on the results of a |Query|. Columns must
|
||||
// be specified for a |Query| and the query results must fully match. If Rows
|
||||
// are omitted, anything is allowed as long as all rows are read successfully.
|
||||
// All assertions here are string equality.
|
||||
type QueryResult struct {
|
||||
Columns []string `yaml:"columns"`
|
||||
Rows ResultRows `yaml:"rows"`
|
||||
}
|
||||
|
||||
type ResultRows struct {
|
||||
Or *[][][]string
|
||||
}
|
||||
|
||||
func (r *ResultRows) UnmarshalYAML(value *yaml.Node) error {
|
||||
if value.Kind == yaml.SequenceNode {
|
||||
res := make([][][]string, 1)
|
||||
r.Or = &res
|
||||
return value.Decode(&(*r.Or)[0])
|
||||
}
|
||||
var or struct {
|
||||
Or *[][][]string `yaml:"or"`
|
||||
}
|
||||
err := value.Decode(&or)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Or = or.Or
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
// Copyright 2026 Dolthub, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
driver "github.com/dolthub/doltgresql/integration-tests/go-sql-server-driver/driver"
|
||||
)
|
||||
|
||||
// This test asserts that running a full gc into the old gen at the
|
||||
// point that it is at its conjoin limit will not generate a conjoin
|
||||
// in the middle of the GC.
|
||||
//
|
||||
// It first generates empty commits and runs GC in a loop. After every
|
||||
// GC, it checks how many table files are in the oldgen. When it sees
|
||||
// them get conjoined, it assumes max table files is the observed high
|
||||
// water mark.
|
||||
//
|
||||
// It then creates exactly that many table files in oldgen by creating
|
||||
// empty commits and running gc however many times it needs to.
|
||||
//
|
||||
// Then it needs to create one more commit and run a
|
||||
// dolt_gc(--full). Currently the sql-server logs whenever it begins
|
||||
// a conjoin, and that determination is made synchronously on the
|
||||
// write path. So, no real attempt is made to get a theoretically
|
||||
// started conjoin to win the race against the newgen collection, for
|
||||
// example.
|
||||
//
|
||||
// As described above, this test is tightly coupled with
|
||||
// GenerationalNBS, NomsBlockStore and the file persisters, with
|
||||
// conjoin strategy behavior. Doltgres uses the same Dolt storage
|
||||
// engine, so the conjoin log lines (pkg=store.noms) and the on-disk
|
||||
// oldgen table-file layout are identical.
|
||||
//
|
||||
// At the end of the test, there should be one file in the oldgen, the
|
||||
// results of the --full. There should be exactly one "beginning
|
||||
// conjoin of database" in the server logs and it should correspond to
|
||||
// when we measured the high water mark. After shutting down the
|
||||
// server should happily start again.
|
||||
//
|
||||
// NOTE: a successful online dolt_gc() invalidates every open connection
|
||||
// to the doltgres server, so the helpers below open a fresh connection
|
||||
// for each commit/gc iteration (db.SetMaxIdleConns(0) ensures each
|
||||
// connection is a new one rather than a pooled, now-invalid connection).
|
||||
func TestFullGCNoOldgenConjoin(t *testing.T) {
|
||||
t.Parallel()
|
||||
ports := newPorts(t)
|
||||
u, err := driver.NewDoltUser()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
u.Cleanup()
|
||||
})
|
||||
|
||||
dbname := "full_gc_no_oldgen_conjoin_test"
|
||||
|
||||
rs, err := u.MakeRepoStore()
|
||||
require.NoError(t, err)
|
||||
repo, err := rs.MakeRepo(dbname)
|
||||
require.NoError(t, err)
|
||||
srvSettings := &driver.Server{
|
||||
Args: []string{},
|
||||
DynamicPort: "server",
|
||||
}
|
||||
var conjoinStarted, conjoinFinished atomic.Bool
|
||||
upstreamLenOnConjoin := 0
|
||||
server := MakeServer(t, rs, rs.Dir, srvSettings, ports, driver.WithOutputVisitor(func(out string) {
|
||||
// Matching a line like:
|
||||
// time="..." level=info msg="beginning conjoin of database" database=full_gc_no_oldgen_conjoin_test generation=old pkg=store.noms upstream_len=257
|
||||
// Extracting its upstream_len to see exactly when the conjoin was triggered.
|
||||
if upstreamLenOnConjoin <= 0 && strings.Contains(out, "beginning conjoin of database") {
|
||||
if i := strings.Index(out, "upstream_len="); i != -1 {
|
||||
i += len("upstream_len=")
|
||||
if _, err := fmt.Sscanf(out[i:], "%d", &upstreamLenOnConjoin); err != nil {
|
||||
upstreamLenOnConjoin = -1
|
||||
}
|
||||
}
|
||||
conjoinStarted.Store(true)
|
||||
}
|
||||
// Matching the line:
|
||||
// time="..." level=info msg="conjoin completed successfully" database=full_gc_no_oldgen_conjoin_test generation=old new_upstream_len=2 pkg=store.noms
|
||||
// So we can block on further operations until conjoin is done.
|
||||
if strings.Contains(out, "conjoin completed successfully") {
|
||||
conjoinFinished.Store(true)
|
||||
}
|
||||
}))
|
||||
server.DBName = dbname
|
||||
|
||||
oldgendir := filepath.Join(repo.Dir, "/.dolt/noms/oldgen")
|
||||
|
||||
CommitAndGCUntilConjoin(t, server, &conjoinStarted, oldgendir)
|
||||
require.Greater(t, upstreamLenOnConjoin, 0)
|
||||
require.Eventually(t, func() bool {
|
||||
return conjoinFinished.Load()
|
||||
}, 5*time.Second, 32*time.Millisecond)
|
||||
|
||||
CreateUpToNumFiles(t, server, oldgendir, upstreamLenOnConjoin)
|
||||
cnt := CountTableFiles(t, oldgendir)
|
||||
t.Logf("now there are %d", cnt)
|
||||
|
||||
RunGCFull(t, server, oldgendir)
|
||||
|
||||
require.NoError(t, server.GracefulStop())
|
||||
output := server.Output.String()
|
||||
assert.Equal(t, 1, strings.Count(output, "beginning conjoin of database"))
|
||||
// The line for triggering a conjoin on policy but not proceeding because
|
||||
// conjoin is dynamically disabled looks like:
|
||||
// time="..." level=info msg="conjoin dynamically disabled. not conjoining." database=full_gc_no_oldgen_conjoin_test generation=old pkg=store.noms
|
||||
assert.Equal(t, 1, strings.Count(output, "conjoin dynamically disabled"))
|
||||
cnt = CountTableFiles(t, oldgendir)
|
||||
assert.Equal(t, 1, cnt)
|
||||
|
||||
newServer := MakeServer(t, rs, rs.Dir, srvSettings, ports)
|
||||
newServer.DBName = dbname
|
||||
db, err := newServer.DB(driver.Connection{})
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
require.NoError(t, db.PingContext(t.Context()))
|
||||
}
|
||||
|
||||
// isTableFileName reports whether n is the name of a Dolt/Noms table file: a
|
||||
// 32-character string in Noms' base32 hash alphabet. This mirrors
|
||||
// hash.MaybeParse from Dolt's store/hash package without importing it.
|
||||
func isTableFileName(n string) bool {
|
||||
const alphabet = "0123456789abcdefghijklmnopqrstuv"
|
||||
if len(n) != 32 {
|
||||
return false
|
||||
}
|
||||
for _, c := range n {
|
||||
if !strings.ContainsRune(alphabet, c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func CountTableFiles(t *testing.T, dir string) int {
|
||||
var count int
|
||||
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n := d.Name()
|
||||
// Archive table files carry a .darc suffix.
|
||||
n = strings.TrimSuffix(n, ".darc")
|
||||
if isTableFileName(n) {
|
||||
count += 1
|
||||
}
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return count
|
||||
}
|
||||
|
||||
func CommitAndGCUntilConjoin(t *testing.T, srv *driver.SqlServer, conjoinStarted *atomic.Bool, path string) {
|
||||
ctx := t.Context()
|
||||
db, err := srv.DB(driver.Connection{})
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
// Ensure each db.Conn() hands out a new connection rather than a pooled
|
||||
// one that a prior online GC has invalidated.
|
||||
db.SetMaxIdleConns(0)
|
||||
|
||||
for {
|
||||
func() {
|
||||
conn, err := db.Conn(ctx)
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
_, err = conn.ExecContext(ctx, "SELECT DOLT_COMMIT('-A', '--allow-empty', '-m', 'creating a new commit')")
|
||||
require.NoError(t, err)
|
||||
_, err = conn.ExecContext(ctx, "SELECT DOLT_GC()")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
if conjoinStarted.Load() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func CreateUpToNumFiles(t *testing.T, srv *driver.SqlServer, path string, numFiles int) {
|
||||
ctx := t.Context()
|
||||
db, err := srv.DB(driver.Connection{})
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
db.SetMaxIdleConns(0)
|
||||
|
||||
for {
|
||||
cnt := CountTableFiles(t, path)
|
||||
if cnt == numFiles {
|
||||
return
|
||||
}
|
||||
func() {
|
||||
conn, err := db.Conn(ctx)
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
_, err = conn.ExecContext(ctx, "SELECT DOLT_COMMIT('-A', '--allow-empty', '-m', 'creating a new commit')")
|
||||
require.NoError(t, err)
|
||||
_, err = conn.ExecContext(ctx, "SELECT DOLT_GC()")
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func RunGCFull(t *testing.T, srv *driver.SqlServer, path string) {
|
||||
ctx := t.Context()
|
||||
db, err := srv.DB(driver.Connection{})
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
db.SetMaxIdleConns(0)
|
||||
|
||||
conn, err := db.Conn(ctx)
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
_ = CountTableFiles(t, path)
|
||||
_, err = conn.ExecContext(ctx, "SELECT DOLT_GC('--full')")
|
||||
require.NoError(t, err)
|
||||
cnt := CountTableFiles(t, path)
|
||||
require.Equal(t, 1, cnt)
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
// Copyright 2026 Dolthub, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
driver "github.com/dolthub/doltgresql/integration-tests/go-sql-server-driver/driver"
|
||||
)
|
||||
|
||||
// Archive levels accepted by dolt_gc's --archive-level option. These mirror
|
||||
// chunks.GCArchiveLevel in Dolt (NoArchive=0, SimpleArchive=1); we use plain
|
||||
// ints here to avoid depending on Dolt's internal store/chunks package.
|
||||
const (
|
||||
noArchive = 0
|
||||
simpleArchive = 1
|
||||
)
|
||||
|
||||
func TestGCIncremental(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
archiveLevels := []int{noArchive, simpleArchive}
|
||||
|
||||
// smallFileSize is chosen so that every leaf chunk gets put in its own table file. In this test, we know
|
||||
// exactly how many table files to expect after GC.
|
||||
smallFileSize := 1
|
||||
|
||||
// mediumFileSize is chosen so that multiple chunks are put into each table file, and ensures that we correctly handle
|
||||
// an in-progress table file at the end of the mark-and-sweep.
|
||||
mediumFileSize := 5_000
|
||||
|
||||
// largeFileSize is arbitrarily large and tests the case where all leaf chunks fit in a single table file.
|
||||
largeFileSize := 1_000_0000
|
||||
|
||||
fileSizes := []int{smallFileSize, mediumFileSize, largeFileSize}
|
||||
|
||||
for _, archiveLevel := range archiveLevels {
|
||||
t.Run(fmt.Sprintf("archiveLevel=%d", archiveLevel), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, fileSize := range fileSizes {
|
||||
t.Run(fmt.Sprintf("fileSize=%d", fileSize), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, full := range []bool{true, false} {
|
||||
t.Run(fmt.Sprintf("full=%v", full), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
runGCIncrementalTest(t, archiveLevel, fileSize, full)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func runGCIncrementalTest(t *testing.T, archiveLevel int, fileSize int, full bool) {
|
||||
ports := newPorts(t)
|
||||
|
||||
u, err := driver.NewDoltUser()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { u.Cleanup() })
|
||||
|
||||
rs, err := u.MakeRepoStore()
|
||||
require.NoError(t, err)
|
||||
|
||||
repoName := fmt.Sprintf("incremental_gc_test_archivelevel_%d_filesize_%d_full_%v", archiveLevel, fileSize, full)
|
||||
repo, err := rs.MakeRepo(repoName)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
server := StartServer(t, rs, repoName, &driver.Server{
|
||||
Args: []string{},
|
||||
DynamicPort: "server",
|
||||
}, ports)
|
||||
|
||||
// Create the database and run GC
|
||||
func() {
|
||||
db, err := server.DB(driver.Connection{})
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
conn, err := db.Conn(ctx)
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
|
||||
populateDB(t, conn)
|
||||
// dolt_gc takes its options as string arguments; build the statement
|
||||
// with literals rather than bind parameters so the string-typed args
|
||||
// are passed exactly as the procedure expects.
|
||||
gcSQL := fmt.Sprintf("select dolt_gc('--archive-level','%d','--incremental-file-size','%d')", archiveLevel, fileSize)
|
||||
if full {
|
||||
gcSQL = fmt.Sprintf("select dolt_gc('--archive-level','%d','--incremental-file-size','%d','--full')", archiveLevel, fileSize)
|
||||
}
|
||||
_, err = conn.ExecContext(ctx, gcSQL)
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
// Verify that the DB is in a valid state after GC. Doltgres does not expose
|
||||
// an fsck command (Dolt's `dolt fsck` CLI / dolt_fsck() are unavailable
|
||||
// here), so as a stand-in we reconnect (the previous connection is
|
||||
// invalidated by the online GC) and confirm the database is still
|
||||
// queryable and the committed data survived.
|
||||
requireDatabaseQueryable(t, server)
|
||||
|
||||
repoSize, err := GetRepoSize(repo.Dir)
|
||||
require.NoError(t, err)
|
||||
// Both newgen and oldgen should contain multiple chunk files
|
||||
require.Greater(t, repoSize.NewGenC, 1)
|
||||
require.Greater(t, repoSize.OldGenC, 1)
|
||||
}
|
||||
|
||||
// requireDatabaseQueryable opens a fresh connection to the server and confirms
|
||||
// the committed table is readable. A successful online GC invalidates prior
|
||||
// connections, so callers must not reuse a connection held across a GC.
|
||||
func requireDatabaseQueryable(t *testing.T, server *driver.SqlServer) {
|
||||
ctx := context.Background()
|
||||
db, err := server.DB(driver.Connection{})
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
var n int
|
||||
err = db.QueryRowContext(ctx, "select count(*) from vals").Scan(&n)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, n, 0)
|
||||
}
|
||||
|
||||
func populateDB(t *testing.T, conn *sql.Conn) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create some chunks that will be collected into oldgen because they're referenced by a commit
|
||||
_, err := conn.ExecContext(ctx, "create table vals (id bigint primary key, val bigint)")
|
||||
require.NoError(t, err)
|
||||
vals := []string{}
|
||||
for i := 0; i <= 1024; i++ {
|
||||
vals = append(vals, fmt.Sprintf("(%d,0)", i))
|
||||
}
|
||||
_, err = conn.ExecContext(ctx, "insert into vals values "+strings.Join(vals, ","))
|
||||
require.NoError(t, err)
|
||||
_, err = conn.ExecContext(ctx, "select dolt_commit('-Am', 'create vals table')")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create some chunks that will be collected into newgen because they aren't referenced by a commit
|
||||
_, err = conn.ExecContext(ctx, "create table vals2 (id bigint primary key, val bigint)")
|
||||
require.NoError(t, err)
|
||||
_, err = conn.ExecContext(ctx, "insert into vals2 values "+strings.Join(vals, ","))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
type RepoSize struct {
|
||||
Journal int64
|
||||
NewGen int64
|
||||
NewGenC int
|
||||
OldGen int64
|
||||
OldGenC int
|
||||
}
|
||||
|
||||
func GetRepoSize(dir string) (RepoSize, error) {
|
||||
skipFile := func(filename string) bool {
|
||||
return filename == "manifest" || filename == "LOCK"
|
||||
}
|
||||
var ret RepoSize
|
||||
entries, err := os.ReadDir(filepath.Join(dir, ".dolt/noms"))
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
for _, e := range entries {
|
||||
if skipFile(e.Name()) {
|
||||
continue
|
||||
}
|
||||
stat, err := e.Info()
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
if stat.IsDir() {
|
||||
continue
|
||||
}
|
||||
ret.NewGen += stat.Size()
|
||||
ret.NewGenC += 1
|
||||
if e.Name() == "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv" {
|
||||
ret.Journal += stat.Size()
|
||||
}
|
||||
}
|
||||
entries, err = os.ReadDir(filepath.Join(dir, ".dolt/noms/oldgen"))
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
for _, e := range entries {
|
||||
if skipFile(e.Name()) {
|
||||
continue
|
||||
}
|
||||
stat, err := e.Info()
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
if stat.IsDir() {
|
||||
continue
|
||||
}
|
||||
ret.OldGen += stat.Size()
|
||||
ret.OldGenC += 1
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func TestResumableGC(t *testing.T) {
|
||||
t.Parallel()
|
||||
ports := newPorts(t)
|
||||
|
||||
u, err := driver.NewDoltUser()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { u.Cleanup() })
|
||||
|
||||
rs, err := u.MakeRepoStore()
|
||||
require.NoError(t, err)
|
||||
|
||||
repoName := "resumable_gc_test"
|
||||
repo, err := rs.MakeRepo(repoName)
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
server := StartServer(t, rs, repoName, &driver.Server{
|
||||
Args: []string{},
|
||||
DynamicPort: "server",
|
||||
Envs: []string{"DOLT_TEST_ABORT_GC_AFTER_INCREMENTAL_FILE_WRITE=true"},
|
||||
}, ports)
|
||||
|
||||
// Create the database and run GC
|
||||
func() {
|
||||
db, err := server.DB(driver.Connection{})
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
conn, err := db.Conn(ctx)
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
|
||||
populateDB(t, conn)
|
||||
|
||||
// Test 1: Oldgen is not amended when --full is set
|
||||
_, err = conn.ExecContext(ctx, "select dolt_gc('--archive-level','0','--incremental-file-size','1','--full')")
|
||||
require.Errorf(t, err, "GC aborting after writing incremental table file")
|
||||
requireFileAddedToManifest(t, filepath.Join(repo.Dir, ".dolt/noms/oldgen"), false)
|
||||
|
||||
// Test 2: Oldgen is amended when --full is not set
|
||||
_, err = conn.ExecContext(ctx, "select dolt_gc('--archive-level','0','--incremental-file-size','1')")
|
||||
require.Errorf(t, err, "GC aborting after writing incremental table file")
|
||||
requireFileAddedToManifest(t, filepath.Join(repo.Dir, ".dolt/noms/oldgen"), true)
|
||||
|
||||
_, err = conn.ExecContext(ctx, "select dolt_gc('--archive-level','0','--incremental-file-size','1')")
|
||||
|
||||
// Test 3: Newgen is not amended even when --full is not set (but there are no changes to oldgen)
|
||||
_, err = conn.ExecContext(ctx, "insert into vals2 values (1025, 0)")
|
||||
require.NoError(t, err)
|
||||
_, err = conn.ExecContext(ctx, "select dolt_gc('--archive-level','0','--incremental-file-size','1')")
|
||||
require.Errorf(t, err, "GC aborting after writing incremental table file")
|
||||
requireFileAddedToManifest(t, filepath.Join(repo.Dir, ".dolt/noms/oldgen"), false)
|
||||
}()
|
||||
}
|
||||
|
||||
func requireFileAddedToManifest(t *testing.T, path string, expectedResult bool) {
|
||||
// Verify that the manifest contains the new chunk file
|
||||
|
||||
manifest, err := os.Open(filepath.Join(path, "manifest"))
|
||||
if os.IsNotExist(err) {
|
||||
// A nonexistent manifest contains no files
|
||||
require.False(t, expectedResult)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
entries, err := os.ReadDir(path)
|
||||
require.NoError(t, err)
|
||||
var tablefileName string
|
||||
for _, e := range entries {
|
||||
if e.Name() == "manifest" || e.Name() == "LOCK" {
|
||||
continue
|
||||
}
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
tablefileName = e.Name()
|
||||
break
|
||||
}
|
||||
require.NotEmpty(t, tablefileName)
|
||||
|
||||
manifestBuffer := make([]byte, 200)
|
||||
_, err = manifest.Read(manifestBuffer)
|
||||
require.NoError(t, err)
|
||||
|
||||
if expectedResult {
|
||||
require.Contains(t, string(manifestBuffer), tablefileName)
|
||||
} else {
|
||||
require.NotContains(t, string(manifestBuffer), tablefileName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
// Copyright 2022 Dolthub, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Generates a 4096-bit RSA chain and an ed25519 chain.
|
||||
// Each chain includes a root, an intermediate, a leaf with DNS and URI SANs.
|
||||
// Root and intermediate have isCA=true and key usage CertSign.
|
||||
// Leaf has isCA=false and key usage digitalSignature and extKeyUsage ServerAuth.
|
||||
//
|
||||
// Generates separate expired leafs for each key type.
|
||||
//
|
||||
// Emits private keys of the leafs. RSA keys are emitted PEM encoded PKCS1.
|
||||
// ed25519 keys are emitted PEM encoded PKCS8.
|
||||
//
|
||||
// These certificates and private keys are used by
|
||||
// tests/sql-server-cluster-tls.yaml and tests/sql-server-tls.yaml, for
|
||||
// example.
|
||||
//
|
||||
// TODO: Further tests which should not verify? (SHA-1 signatures, expired
|
||||
// roots or intermediates, wrong isCA, wrong key usage, etc.)
|
||||
|
||||
func GenerateX509Certs(dir string) error {
|
||||
rsacerts, err := MakeRSACerts()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not make rsa certs: %w", err)
|
||||
}
|
||||
|
||||
err = WriteRSACerts(dir, rsacerts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not write rsa certs: %w", err)
|
||||
}
|
||||
|
||||
edcerts, err := MakeEd25519Certs()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not make ed25519 certs: %w", err)
|
||||
}
|
||||
|
||||
err = WriteEd25519Certs(dir, edcerts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not write ed25519 certs: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func WriteRSACerts(dir string, rsacerts TestCerts) error {
|
||||
err := os.WriteFile(filepath.Join(dir, "rsa_root.pem"), pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: rsacerts.Root.Raw,
|
||||
}), 0664)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(dir, "rsa_chain.pem"), append(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: rsacerts.Leaf.Raw,
|
||||
}), pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: rsacerts.Intermediate.Raw,
|
||||
})...), 0664)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(dir, "rsa_key.pem"), pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(rsacerts.LeafKey.(*rsa.PrivateKey)),
|
||||
}), 0664)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.WriteFile(filepath.Join(dir, "rsa_exp_chain.pem"), append(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: rsacerts.ExpiredLeaf.Raw,
|
||||
}), pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: rsacerts.Intermediate.Raw,
|
||||
})...), 0664)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(dir, "rsa_exp_key.pem"), pem.EncodeToMemory(&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(rsacerts.ExpiredLeafKey.(*rsa.PrivateKey)),
|
||||
}), 0664)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func WriteEd25519Certs(dir string, edcerts TestCerts) error {
|
||||
err := os.WriteFile(filepath.Join(dir, "ed25519_root.pem"), pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: edcerts.Root.Raw,
|
||||
}), 0664)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(dir, "ed25519_chain.pem"), append(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: edcerts.Leaf.Raw,
|
||||
}), pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: edcerts.Intermediate.Raw,
|
||||
})...), 0664)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keybytes, err := x509.MarshalPKCS8PrivateKey(edcerts.LeafKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(dir, "ed25519_key.pem"), pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: keybytes,
|
||||
}), 0664)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.WriteFile(filepath.Join(dir, "ed25519_exp_chain.pem"), append(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: edcerts.ExpiredLeaf.Raw,
|
||||
}), pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: edcerts.Intermediate.Raw,
|
||||
})...), 0664)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keybytes, err = x509.MarshalPKCS8PrivateKey(edcerts.ExpiredLeafKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.WriteFile(filepath.Join(dir, "edcerts_exp_key.pem"), pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: keybytes,
|
||||
}), 0664)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type TestCerts struct {
|
||||
Root *x509.Certificate
|
||||
Intermediate *x509.Certificate
|
||||
Leaf *x509.Certificate
|
||||
LeafKey any
|
||||
ExpiredLeaf *x509.Certificate
|
||||
ExpiredLeafKey any
|
||||
}
|
||||
|
||||
func MakeRSACerts() (TestCerts, error) {
|
||||
genKey := func() (any, any, error) {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return key.Public(), key, nil
|
||||
}
|
||||
return MakeCerts("RSA 4096-bit", genKey)
|
||||
}
|
||||
|
||||
func MakeEd25519Certs() (TestCerts, error) {
|
||||
genKey := func() (any, any, error) {
|
||||
return ed25519.GenerateKey(rand.Reader)
|
||||
}
|
||||
return MakeCerts("ed25519", genKey)
|
||||
}
|
||||
|
||||
func MakeCerts(desc string, genKey func() (any, any, error)) (TestCerts, error) {
|
||||
nbf := time.Now().Add(-24 * time.Hour)
|
||||
exp := nbf.Add(24 * 365 * 10 * time.Hour)
|
||||
badExp := nbf.Add(12 * time.Hour)
|
||||
|
||||
rootpub, rootpriv, err := genKey()
|
||||
if err != nil {
|
||||
return TestCerts{}, err
|
||||
}
|
||||
intpub, intpriv, err := genKey()
|
||||
if err != nil {
|
||||
return TestCerts{}, err
|
||||
}
|
||||
leafpub, leafpriv, err := genKey()
|
||||
if err != nil {
|
||||
return TestCerts{}, err
|
||||
}
|
||||
exppub, exppriv, err := genKey()
|
||||
if err != nil {
|
||||
return TestCerts{}, err
|
||||
}
|
||||
|
||||
signer, err := NewRootSigner(&x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
Country: []string{"US"},
|
||||
Organization: []string{"DoltHub, Inc."},
|
||||
CommonName: "dolt integration tests " + desc + " Root",
|
||||
},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
KeyUsage: x509.KeyUsageCertSign,
|
||||
NotBefore: nbf,
|
||||
NotAfter: exp,
|
||||
}, rootpub, rootpriv)
|
||||
if err != nil {
|
||||
return TestCerts{}, err
|
||||
}
|
||||
|
||||
intcert, err := signer.Sign(&x509.Certificate{
|
||||
SerialNumber: big.NewInt(2),
|
||||
Subject: pkix.Name{
|
||||
Country: []string{"US"},
|
||||
Organization: []string{"DoltHub, Inc."},
|
||||
CommonName: "dolt integration tests " + desc + " Intermediate",
|
||||
},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
KeyUsage: x509.KeyUsageCertSign,
|
||||
NotBefore: nbf,
|
||||
NotAfter: exp,
|
||||
}, intpub)
|
||||
if err != nil {
|
||||
return TestCerts{}, err
|
||||
}
|
||||
intsigner := Signer{intcert, intpriv}
|
||||
|
||||
leafdns := "dolt-instance.dolt-integration-test.example"
|
||||
leafurl, err := url.Parse("spiffe://dolt-integration-tests.dev.trust.dolthub.com.example/dolt-instance")
|
||||
if err != nil {
|
||||
return TestCerts{}, err
|
||||
}
|
||||
|
||||
leafcert, err := intsigner.Sign(&x509.Certificate{
|
||||
SerialNumber: big.NewInt(3),
|
||||
Subject: pkix.Name{
|
||||
Country: []string{"US"},
|
||||
Organization: []string{"DoltHub, Inc."},
|
||||
CommonName: "dolt integration tests " + desc + " Leaf",
|
||||
},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: false,
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
NotBefore: nbf,
|
||||
NotAfter: exp,
|
||||
DNSNames: []string{leafdns},
|
||||
URIs: []*url.URL{leafurl},
|
||||
}, leafpub)
|
||||
if err != nil {
|
||||
return TestCerts{}, err
|
||||
}
|
||||
|
||||
expcert, err := intsigner.Sign(&x509.Certificate{
|
||||
SerialNumber: big.NewInt(4),
|
||||
Subject: pkix.Name{
|
||||
Country: []string{"US"},
|
||||
Organization: []string{"DoltHub, Inc."},
|
||||
CommonName: "dolt integration tests " + desc + " Expired Leaf",
|
||||
},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: false,
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
NotBefore: nbf,
|
||||
NotAfter: badExp,
|
||||
DNSNames: []string{leafdns},
|
||||
URIs: []*url.URL{leafurl},
|
||||
}, exppub)
|
||||
if err != nil {
|
||||
return TestCerts{}, err
|
||||
}
|
||||
|
||||
return TestCerts{
|
||||
Root: signer.Cert,
|
||||
Intermediate: intsigner.Cert,
|
||||
Leaf: leafcert,
|
||||
ExpiredLeaf: expcert,
|
||||
LeafKey: leafpriv,
|
||||
ExpiredLeafKey: exppriv,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Signer struct {
|
||||
Cert *x509.Certificate
|
||||
Key interface{}
|
||||
}
|
||||
|
||||
func (s Signer) Sign(cert *x509.Certificate, pub any) (*x509.Certificate, error) {
|
||||
der, err := x509.CreateCertificate(rand.Reader, cert, s.Cert, pub, s.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x509.ParseCertificate(der)
|
||||
}
|
||||
|
||||
func NewRootSigner(cert *x509.Certificate, pub, priv any) (Signer, error) {
|
||||
der, err := x509.CreateCertificate(rand.Reader, cert, cert, pub, priv)
|
||||
if err != nil {
|
||||
return Signer{}, err
|
||||
}
|
||||
cert, err = x509.ParseCertificate(der)
|
||||
if err != nil {
|
||||
return Signer{}, err
|
||||
}
|
||||
return Signer{cert, priv}, nil
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
// Copyright 2022 Dolthub, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gopkg.in/go-jose/go-jose.v2"
|
||||
"gopkg.in/go-jose/go-jose.v2/jwt"
|
||||
)
|
||||
|
||||
var sub = "test_user"
|
||||
var iss = "dolthub.com"
|
||||
var aud = "my_resource"
|
||||
var onBehalfOf = "my_user"
|
||||
|
||||
// Generates a JWKS and a JWT for authenticating against it. Outputs it into
|
||||
// files `|dir|/token.jwt` and `|dir|/test_jwks.json`.
|
||||
//
|
||||
// These files are used by sql-server-jwt-auth.yaml, for example.
|
||||
|
||||
func GenerateTestJWTs(dir string) error {
|
||||
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not generate rsa key: %w", err)
|
||||
}
|
||||
pubKey := privKey.Public()
|
||||
|
||||
kid, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not generate random uuid: %w", err)
|
||||
}
|
||||
|
||||
err = writeJWKSToFile(dir, pubKey, kid.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not write jwks to file: %w", err)
|
||||
}
|
||||
|
||||
jwt, err := generateJWT(privKey, kid.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not generate jwt: %w", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(filepath.Join(dir, "token.jwt"), []byte(jwt), 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not write jwt to file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeJWKSToFile(dir string, pubKey crypto.PublicKey, kid string) error {
|
||||
jwk := jose.JSONWebKey{
|
||||
KeyID: kid,
|
||||
Key: pubKey,
|
||||
Use: "sig",
|
||||
Algorithm: "RS256",
|
||||
}
|
||||
jwks := jose.JSONWebKeySet{Keys: []jose.JSONWebKey{jwk}}
|
||||
|
||||
jwksjson, err := json.Marshal(jwks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.WriteFile(filepath.Join(dir, "test_jwks.json"), jwksjson, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateJWT(privKey *rsa.PrivateKey, kid string) (string, error) {
|
||||
id, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
claims := jwt.Claims{
|
||||
ID: id.String(),
|
||||
Audience: []string{aud},
|
||||
Issuer: iss,
|
||||
Subject: sub,
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
Expiry: jwt.NewNumericDate(now.Add(364 * 24 * time.Hour)),
|
||||
}
|
||||
privClaims := struct {
|
||||
OnBehalfOf string `json:"on_behalf_of"`
|
||||
}{
|
||||
onBehalfOf,
|
||||
}
|
||||
|
||||
sig := jose.SigningKey{Algorithm: jose.RS256, Key: privKey}
|
||||
opts := (&jose.SignerOptions{ExtraHeaders: map[jose.HeaderKey]interface{}{
|
||||
"kid": kid,
|
||||
}}).WithType("JWT")
|
||||
|
||||
signer, err := jose.NewSigner(sig, opts)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
jwtBuilder := jwt.Signed(signer)
|
||||
jwtBuilder = jwtBuilder.Claims(claims)
|
||||
jwtBuilder = jwtBuilder.Claims(privClaims)
|
||||
|
||||
com, err := jwtBuilder.CompactSerialize()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return com, nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
module github.com/dolthub/doltgresql/integration-tests/go-sql-server-driver
|
||||
|
||||
go 1.26.2
|
||||
|
||||
require (
|
||||
github.com/creasty/defaults v1.6.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jackc/pgx/v5 v5.9.2
|
||||
github.com/stretchr/testify v1.11.1
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/sys v0.38.0
|
||||
gopkg.in/go-jose/go-jose.v2 v2.6.3
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.15.0 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,47 @@
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creasty/defaults v1.6.0 h1:ltuE9cfphUtlrBeomuu8PEyISTXnxqkBIoQfXgv7BSc=
|
||||
github.com/creasty/defaults v1.6.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM=
|
||||
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/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
||||
github.com/jackc/pgx/v5 v5.9.2/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/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
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/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
|
||||
github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
|
||||
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/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs=
|
||||
gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI=
|
||||
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=
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright 2024 Dolthub, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
driver "github.com/dolthub/doltgresql/integration-tests/go-sql-server-driver/driver"
|
||||
)
|
||||
|
||||
// newPorts returns a DynamicResources bound to the global port pool for use in
|
||||
// standalone Go tests.
|
||||
func newPorts(t *testing.T) *DynamicResources {
|
||||
ports := &DynamicResources{}
|
||||
ports.global = &GlobalPorts
|
||||
ports.t = t
|
||||
return ports
|
||||
}
|
||||
|
||||
// StartServer is the doltgres convenience wrapper used by standalone Go tests
|
||||
// to start a server for the database |dbName| in the store |rs|. The doltgres
|
||||
// server runs from the store directory (the data-dir) and serves |dbName| as a
|
||||
// database. The returned server's DBName is set to |dbName| so connections
|
||||
// target it by default.
|
||||
func StartServer(t *testing.T, rs driver.RepoStore, dbName string, s *driver.Server, ports *DynamicResources) *driver.SqlServer {
|
||||
server := MakeServer(t, rs, rs.Dir, s, ports)
|
||||
if server != nil {
|
||||
server.DBName = dbName
|
||||
}
|
||||
return server
|
||||
}
|
||||
|
||||
// RunServerUntilEndOfTest runs a server until the end of the test. Because we
|
||||
// do not return the server for doing things like making connections to it,
|
||||
// this is only useful for asserting the behavior of other commands which
|
||||
// interact with the server.
|
||||
func RunServerUntilEndOfTest(t *testing.T, rs driver.RepoStore, s *driver.Server, ports *DynamicResources) {
|
||||
server := MakeServer(t, rs, rs.Dir, s, ports)
|
||||
require.NotNil(t, server)
|
||||
db, err := server.DB(driver.Connection{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.Close())
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,165 @@
|
||||
// Copyright 2024 Dolthub, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// We generate various TLS keys and certificates and some JWKS/JWT material
|
||||
// which the tests reference. We do this once for the test run, because it can
|
||||
// be expensive, and we expose the location of the generated files through an
|
||||
// environment variable. The test definitions interpolate that environment
|
||||
// variable into a few fields.
|
||||
//
|
||||
// It's good enough for now, and it keeps us from checking in certificates or
|
||||
// JWT which will expire at some point in the future.
|
||||
func TestMain(m *testing.M) {
|
||||
old := os.Getenv("TESTGENDIR")
|
||||
defer func() {
|
||||
os.Setenv("TESTGENDIR", old)
|
||||
}()
|
||||
gendir, err := os.MkdirTemp(os.TempDir(), "go-sql-server-driver-gen-*")
|
||||
if err != nil {
|
||||
log.Fatalf("could not create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(gendir)
|
||||
err = GenerateTestJWTs(gendir)
|
||||
if err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
err = GenerateX509Certs(gendir)
|
||||
if err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
os.Setenv("TESTGENDIR", gendir)
|
||||
flag.Parse()
|
||||
InitGlobalDynamicPorts()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func InitGlobalDynamicPorts() {
|
||||
// XXX: Max and min here could be supplied by flags. Currently
|
||||
// tests use at most 6 ports, so this may run out of ports if
|
||||
// we have more than ~40 concurrent processes.
|
||||
for i := 0; i < 256; i++ {
|
||||
GlobalPorts.available = append(GlobalPorts.available, 5432+i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
RunTestsFile(t, "tests/sql-server-config.yaml")
|
||||
}
|
||||
|
||||
func TestCluster(t *testing.T) {
|
||||
t.Parallel()
|
||||
RunTestsFile(t, "tests/sql-server-cluster.yaml")
|
||||
}
|
||||
|
||||
func TestClusterUsersAndGrants(t *testing.T) {
|
||||
t.Parallel()
|
||||
RunTestsFile(t, "tests/sql-server-cluster-users-and-grants.yaml")
|
||||
}
|
||||
|
||||
func TestRemotesAPI(t *testing.T) {
|
||||
t.Parallel()
|
||||
RunTestsFile(t, "tests/sql-server-remotesapi.yaml")
|
||||
}
|
||||
|
||||
// TestSingle is a convenience method for running a single test from within an IDE. Unskip and set to the file and name
|
||||
// of the test you want to debug. See README.md in the `tests` directory for more debugging info.
|
||||
func TestSingle(t *testing.T) {
|
||||
t.Skip()
|
||||
RunSingleTest(t, "tests/sql-server-cluster.yaml", "primary comes up and replicates to standby")
|
||||
}
|
||||
|
||||
func TestClusterTLS(t *testing.T) {
|
||||
t.Parallel()
|
||||
RunTestsFile(t, "tests/sql-server-cluster-tls.yaml")
|
||||
}
|
||||
|
||||
func TestOriginal(t *testing.T) {
|
||||
t.Parallel()
|
||||
RunTestsFile(t, "tests/sql-server-orig.yaml")
|
||||
}
|
||||
|
||||
func TestTLS(t *testing.T) {
|
||||
t.Parallel()
|
||||
RunTestsFile(t, "tests/sql-server-tls.yaml")
|
||||
}
|
||||
|
||||
func TestClusterReadOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
RunTestsFile(t, "tests/sql-server-cluster-read-only.yaml")
|
||||
}
|
||||
|
||||
func TestLargeTextReplication(t *testing.T) {
|
||||
t.Parallel()
|
||||
RunTestsFile(t, "tests/sql-server-large-text-replication.yaml")
|
||||
}
|
||||
|
||||
func TestLargeBlobReplication(t *testing.T) {
|
||||
t.Parallel()
|
||||
RunTestsFile(t, "tests/sql-server-large-blob-replication.yaml")
|
||||
}
|
||||
|
||||
func TestLargeJSONReplication(t *testing.T) {
|
||||
t.Parallel()
|
||||
RunTestsFile(t, "tests/sql-server-large-json-replication.yaml")
|
||||
}
|
||||
|
||||
func TestLargeMultiColumnReplication(t *testing.T) {
|
||||
t.Parallel()
|
||||
RunTestsFile(t, "tests/sql-server-large-multi-column-replication.yaml")
|
||||
}
|
||||
|
||||
func TestLargeValuesGC(t *testing.T) {
|
||||
t.Parallel()
|
||||
RunTestsFile(t, "tests/sql-server-large-values-gc.yaml")
|
||||
}
|
||||
|
||||
func TestTypeDiversityCluster(t *testing.T) {
|
||||
t.Parallel()
|
||||
RunTestsFile(t, "tests/sql-server-type-diversity.yaml")
|
||||
}
|
||||
|
||||
func TestWideIntTable(t *testing.T) {
|
||||
t.Parallel()
|
||||
RunTestsFile(t, "tests/sql-server-wide-int-table.yaml")
|
||||
}
|
||||
|
||||
func TestWideVarcharTable(t *testing.T) {
|
||||
t.Parallel()
|
||||
RunTestsFile(t, "tests/sql-server-wide-varchar-table.yaml")
|
||||
}
|
||||
|
||||
func TestWideTextTable(t *testing.T) {
|
||||
t.Parallel()
|
||||
RunTestsFile(t, "tests/sql-server-wide-text-table.yaml")
|
||||
}
|
||||
|
||||
func TestLargeValuesFailover(t *testing.T) {
|
||||
t.Parallel()
|
||||
RunTestsFile(t, "tests/sql-server-large-values-failover.yaml")
|
||||
}
|
||||
|
||||
func TestWideTableFailover(t *testing.T) {
|
||||
t.Parallel()
|
||||
RunTestsFile(t, "tests/sql-server-wide-table-failover.yaml")
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
// Copyright 2025 Dolthub, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
driver "github.com/dolthub/doltgresql/integration-tests/go-sql-server-driver/driver"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/go-jose/go-jose.v2"
|
||||
)
|
||||
|
||||
type getConfigFunc func(serverPort, metricsPort int) string
|
||||
|
||||
// runJWKSServer starts a local HTTP server that serves the JWKS file at /jwks.json.
|
||||
// The server is started in a goroutine and will be shut down via t.Cleanup.
|
||||
func runJWKSServer(t *testing.T, jwksFilePath string, port int) {
|
||||
data, err := os.ReadFile(jwksFilePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/jwks.json", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write(data)
|
||||
})
|
||||
|
||||
addr := fmt.Sprintf("127.0.0.1:%d", port)
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
require.NoError(t, err)
|
||||
|
||||
go func() {
|
||||
if err := server.Serve(ln); err != nil && err != http.ErrServerClosed {
|
||||
// Use t.Log instead of t.Fatal because Serve runs in goroutine
|
||||
t.Logf("runJWKSServer: server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
t.Logf("Started test JWKS server on %s serving %s", addr, jwksFilePath)
|
||||
|
||||
t.Cleanup(func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
defer cancel()
|
||||
if err := server.Shutdown(ctx); err != nil {
|
||||
t.Logf("runJWKSServer: shutdown error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func createJWT(t *testing.T, issuer, audience, subject string) string {
|
||||
const kid = "749df841-6e38-48f1-a178-20ecdd0b09f7"
|
||||
|
||||
// load jwks from testdata
|
||||
data, err := os.ReadFile("testdata/test_jwks_private.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
var jwks jose.JSONWebKeySet
|
||||
err = json.Unmarshal(data, &jwks)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, jwks.Keys)
|
||||
|
||||
// choose key by kid or default to first
|
||||
var jwk *jose.JSONWebKey
|
||||
if kid == "" {
|
||||
jwk = &jwks.Keys[0]
|
||||
} else {
|
||||
for i := range jwks.Keys {
|
||||
if jwks.Keys[i].KeyID == kid {
|
||||
jwk = &jwks.Keys[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotNil(t, jwk)
|
||||
}
|
||||
|
||||
// ensure we have a private key
|
||||
require.False(t, jwk.IsPublic())
|
||||
|
||||
// create signer with kid header (if present)
|
||||
opts := (&jose.SignerOptions{}).WithType("JWT")
|
||||
if jwk.KeyID != "" {
|
||||
opts = opts.WithHeader("kid", jwk.KeyID)
|
||||
}
|
||||
|
||||
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: jwk.Key}, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
// build claims
|
||||
now := time.Now().UTC()
|
||||
claims := map[string]interface{}{
|
||||
"iss": issuer,
|
||||
"aud": audience,
|
||||
"sub": subject,
|
||||
"iat": now.Unix(),
|
||||
"exp": now.Add(time.Hour).Unix(),
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(claims)
|
||||
require.NoError(t, err)
|
||||
|
||||
jws, err := signer.Sign(payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
compact, err := jws.CompactSerialize()
|
||||
require.NoError(t, err)
|
||||
|
||||
return compact
|
||||
}
|
||||
|
||||
func makeMetricsCall(t *testing.T, metricsPort int, bearerToken string) *http.Response {
|
||||
url := fmt.Sprintf("http://127.0.0.1:%d/metrics", metricsPort)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
if bearerToken != "" {
|
||||
req.Header.Add("Authorization", "Bearer "+bearerToken)
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
var jwksPort int
|
||||
|
||||
func TestMetricsAuth(t *testing.T) {
|
||||
// Doltgres serves a /metrics HTTP endpoint, but its metrics configuration
|
||||
// (DoltgesMetricsConfig) does not support the `jwks` /
|
||||
// `jwt_required_for_localhost` options this test relies on, and the metrics
|
||||
// server does not enforce JWT auth (MetricsJwksConfig() returns nil). The
|
||||
// full logic below is a faithful port and should be enabled once Doltgres
|
||||
// supports JWT-protected metrics.
|
||||
t.Skip("metrics JWKS / JWT-auth configuration (metrics.jwks, jwt_required_for_localhost) is unsupported in Doltgres")
|
||||
jwksPort = GlobalPorts.GetPort(t)
|
||||
absPath, err := filepath.Abs("./testdata/test_jwks.json")
|
||||
require.NoError(t, err)
|
||||
runJWKSServer(t, absPath, jwksPort)
|
||||
|
||||
t.Parallel()
|
||||
t.Run("No Metrics Auth", testNoMetricsAuth)
|
||||
t.Run("Missing Metrics Auth", testMissingMetricsAuth)
|
||||
t.Run("Valid Metrics Auth", testValidMetricsAuth)
|
||||
t.Run("Bad Audience Claim", testBadAudienceClaim)
|
||||
t.Run("Bad Issuer Claim", testBadIssuerClaim)
|
||||
t.Run("Bad Subject Claim", testBadSubjectClaim)
|
||||
}
|
||||
|
||||
func startServerWithMetrics(t *testing.T, getConfig getConfigFunc) int {
|
||||
ports := newPorts(t)
|
||||
|
||||
serverPort := ports.GetOrAllocatePort("server")
|
||||
metricsPort := ports.GetOrAllocatePort("metrics")
|
||||
|
||||
config := getConfig(serverPort, metricsPort)
|
||||
|
||||
u, err := driver.NewDoltUser()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
u.Cleanup()
|
||||
})
|
||||
|
||||
rs, err := u.MakeRepoStore()
|
||||
require.NoError(t, err)
|
||||
_, err = rs.MakeRepo("metrics_auth_test")
|
||||
require.NoError(t, err)
|
||||
|
||||
f, err := os.CreateTemp("", "config-*.yaml")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
os.Remove(f.Name())
|
||||
})
|
||||
|
||||
_, err = f.WriteString(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
args := []string{"--config", f.Name()}
|
||||
srvSettings := &driver.Server{
|
||||
Args: args,
|
||||
DynamicPort: "server",
|
||||
}
|
||||
|
||||
t.Log("Starting server with config:\n" + config)
|
||||
MakeServer(t, rs, rs.Dir, srvSettings, ports)
|
||||
|
||||
// Wait for the metrics HTTP listener to be ready.
|
||||
require.Eventually(t, func() bool {
|
||||
resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/metrics", metricsPort))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
resp.Body.Close()
|
||||
return true
|
||||
}, 5*time.Second, 50*time.Millisecond)
|
||||
|
||||
return metricsPort
|
||||
}
|
||||
|
||||
func testNoMetricsAuth(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
getConfig := func(serverPort, metricsPort int) string {
|
||||
return fmt.Sprintf(`
|
||||
listener:
|
||||
host: localhost
|
||||
port: %d
|
||||
|
||||
metrics:
|
||||
host: localhost
|
||||
port: %d
|
||||
jwt_required_for_localhost: true
|
||||
`, serverPort, metricsPort)
|
||||
}
|
||||
|
||||
metricsPort := startServerWithMetrics(t, getConfig)
|
||||
resp := makeMetricsCall(t, metricsPort, "")
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
func testMissingMetricsAuth(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
getConfig := func(serverPort, metricsPort int) string {
|
||||
return fmt.Sprintf(`listener:
|
||||
host: localhost
|
||||
port: %d
|
||||
|
||||
metrics:
|
||||
host: localhost
|
||||
port: %d
|
||||
jwks:
|
||||
name: jwksname
|
||||
location_url: http://127.0.0.1:%d/jwks.json
|
||||
claims:
|
||||
alg: RS256
|
||||
iss: dolthub.com
|
||||
sub: test_sub
|
||||
aud: test_aud
|
||||
jwt_required_for_localhost: true
|
||||
`, serverPort, metricsPort, jwksPort)
|
||||
}
|
||||
|
||||
metricsPort := startServerWithMetrics(t, getConfig)
|
||||
resp := makeMetricsCall(t, metricsPort, "")
|
||||
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
}
|
||||
|
||||
func testValidMetricsAuth(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
getConfig := func(serverPort, metricsPort int) string {
|
||||
return fmt.Sprintf(`listener:
|
||||
host: localhost
|
||||
port: %d
|
||||
|
||||
metrics:
|
||||
host: localhost
|
||||
port: %d
|
||||
jwks:
|
||||
name: jwksname
|
||||
location_url: http://127.0.0.1:%d/jwks.json
|
||||
claims:
|
||||
iss: dolthub.com
|
||||
sub: test_sub
|
||||
aud: test_aud
|
||||
jwt_required_for_localhost: true
|
||||
`, serverPort, metricsPort, jwksPort)
|
||||
}
|
||||
|
||||
metricsPort := startServerWithMetrics(t, getConfig)
|
||||
jwt := createJWT(t, "dolthub.com", "test_aud", "test_sub")
|
||||
resp := makeMetricsCall(t, metricsPort, jwt)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
func testBadIssuerClaim(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
getConfig := func(serverPort, metricsPort int) string {
|
||||
return fmt.Sprintf(`listener:
|
||||
host: localhost
|
||||
port: %d
|
||||
|
||||
metrics:
|
||||
host: localhost
|
||||
port: %d
|
||||
jwks:
|
||||
name: jwksname
|
||||
location_url: http://127.0.0.1:%d/jwks.json
|
||||
claims:
|
||||
iss: dolthub.com
|
||||
sub: test_sub
|
||||
aud: test_aud
|
||||
jwt_required_for_localhost: true
|
||||
`, serverPort, metricsPort, jwksPort)
|
||||
}
|
||||
|
||||
metricsPort := startServerWithMetrics(t, getConfig)
|
||||
jwt := createJWT(t, "badissuer.com", "test_aud", "test_sub")
|
||||
resp := makeMetricsCall(t, metricsPort, jwt)
|
||||
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
}
|
||||
|
||||
func testBadAudienceClaim(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
getConfig := func(serverPort, metricsPort int) string {
|
||||
return fmt.Sprintf(`listener:
|
||||
host: localhost
|
||||
port: %d
|
||||
|
||||
metrics:
|
||||
host: localhost
|
||||
port: %d
|
||||
jwks:
|
||||
name: jwksname
|
||||
location_url: http://127.0.0.1:%d/jwks.json
|
||||
claims:
|
||||
iss: dolthub.com
|
||||
sub: test_sub
|
||||
aud: test_aud
|
||||
jwt_required_for_localhost: true
|
||||
`, serverPort, metricsPort, jwksPort)
|
||||
}
|
||||
|
||||
metricsPort := startServerWithMetrics(t, getConfig)
|
||||
jwt := createJWT(t, "dolthub.com", "bad_aud", "test_sub")
|
||||
resp := makeMetricsCall(t, metricsPort, jwt)
|
||||
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
}
|
||||
|
||||
func testBadSubjectClaim(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
getConfig := func(serverPort, metricsPort int) string {
|
||||
return fmt.Sprintf(`listener:
|
||||
host: localhost
|
||||
port: %d
|
||||
|
||||
metrics:
|
||||
host: localhost
|
||||
port: %d
|
||||
jwks:
|
||||
name: jwksname
|
||||
location_url: http://127.0.0.1:%d/jwks.json
|
||||
claims:
|
||||
iss: dolthub.com
|
||||
sub: test_sub
|
||||
aud: test_aud
|
||||
jwt_required_for_localhost: true
|
||||
`, serverPort, metricsPort, jwksPort)
|
||||
}
|
||||
|
||||
metricsPort := startServerWithMetrics(t, getConfig)
|
||||
jwt := createJWT(t, "dolthub.com", "test_aud", "bad_sub")
|
||||
resp := makeMetricsCall(t, metricsPort, jwt)
|
||||
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
// Copyright 2024 Dolthub, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
driver "github.com/dolthub/doltgresql/integration-tests/go-sql-server-driver/driver"
|
||||
)
|
||||
|
||||
func TestCommitConcurrency(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("SQL transaction with amend commit", testSQLTransactionWithAmendCommit)
|
||||
t.Run("SQL racing amend", testSQLRacingAmend)
|
||||
t.Run("SQL racing amend with serial column", testSQLRacingAmendWithSerial)
|
||||
}
|
||||
|
||||
// testSQLTransactionWithAmendCommit verifies that two transactions started at the same state will not both be able
|
||||
// to commit using --amend. The first transaction will be able to commit, but the second should get an error.
|
||||
func testSQLTransactionWithAmendCommit(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
ports := newPorts(t)
|
||||
u, err := driver.NewDoltUser()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
u.Cleanup()
|
||||
})
|
||||
|
||||
rs, err := u.MakeRepoStore()
|
||||
require.NoError(t, err)
|
||||
_, err = rs.MakeRepo("commit_concurrency_test")
|
||||
require.NoError(t, err)
|
||||
|
||||
srvSettings := &driver.Server{
|
||||
Args: []string{},
|
||||
DynamicPort: "server_port",
|
||||
}
|
||||
server := StartServer(t, rs, "commit_concurrency_test", srvSettings, ports)
|
||||
|
||||
// Connect to the database
|
||||
db, err := server.DB(driver.Connection{})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
db.Close()
|
||||
})
|
||||
|
||||
_, err = db.ExecContext(ctx, `
|
||||
CREATE TABLE test_table (
|
||||
id serial PRIMARY KEY,
|
||||
value VARCHAR(20)
|
||||
);`)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.ExecContext(ctx, "INSERT INTO test_table (value) VALUES ('initial')")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.ExecContext(ctx, "SELECT dolt_commit('-A','-m', 'initial commit')")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a new context for the first (failing) transaction
|
||||
ctx1, cancel1 := context.WithCancel(ctx)
|
||||
defer cancel1()
|
||||
tx1, err := db.BeginTx(ctx1, nil)
|
||||
require.NoError(t, err)
|
||||
_, err = tx1.ExecContext(ctx1, "UPDATE test_table SET value = 'amended by tx1' WHERE id = 1")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a new context for the second (succeeding) transaction
|
||||
ctx2, cancel2 := context.WithCancel(ctx)
|
||||
defer cancel2()
|
||||
tx2, err := db.BeginTx(ctx2, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Update data within the second transaction
|
||||
_, err = tx2.ExecContext(ctx2, "UPDATE test_table SET value = 'amended by tx2' WHERE id = 1")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = tx2.ExecContext(ctx2, "SELECT dolt_commit('--amend', '-m', 'tx2 amended commit')")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Commit --amend will result in tx2 being committed. You can still make updates on tx1, but any commit should fail
|
||||
_, err = tx1.ExecContext(ctx1, "INSERT INTO test_table (value) VALUES ('new row by tx1')")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = tx1.ExecContext(ctx1, "SELECT dolt_commit('--amend', '-m', 'should fail')")
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "this transaction conflicts with a committed transaction from another client, try restarting transaction")
|
||||
|
||||
// Verify that the data in the head is what we would expect
|
||||
row := db.QueryRowContext(ctx, "SELECT value FROM test_table WHERE id = 1")
|
||||
var value string
|
||||
err = row.Scan(&value)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "amended by tx2", value)
|
||||
|
||||
// Verify the commit message
|
||||
row = db.QueryRowContext(ctx, "SELECT message FROM dolt_log ORDER BY date DESC LIMIT 1")
|
||||
var commitMessage string
|
||||
err = row.Scan(&commitMessage)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "tx2 amended commit", commitMessage)
|
||||
|
||||
}
|
||||
|
||||
func testSQLRacingAmend(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
ports := newPorts(t)
|
||||
u, err := driver.NewDoltUser()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
u.Cleanup()
|
||||
})
|
||||
|
||||
rs, err := u.MakeRepoStore()
|
||||
require.NoError(t, err)
|
||||
_, err = rs.MakeRepo("racing_amend_test")
|
||||
require.NoError(t, err)
|
||||
|
||||
srvSettings := &driver.Server{
|
||||
Args: []string{},
|
||||
DynamicPort: "server_port",
|
||||
}
|
||||
server := StartServer(t, rs, "racing_amend_test", srvSettings, ports)
|
||||
|
||||
db, err := server.DB(driver.Connection{})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
db.Close()
|
||||
})
|
||||
|
||||
_, err = db.ExecContext(ctx, `
|
||||
CREATE TABLE test_table (
|
||||
id bigint PRIMARY KEY,
|
||||
value VARCHAR(20)
|
||||
);`)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.ExecContext(ctx, "INSERT INTO test_table VALUES (1, 'initial')")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.ExecContext(ctx, "SELECT dolt_commit('-A','-m', 'initial commit')")
|
||||
require.NoError(t, err)
|
||||
|
||||
type txIdFunc struct {
|
||||
txNum int
|
||||
txFunc func() error
|
||||
}
|
||||
|
||||
var transactions []txIdFunc
|
||||
for txNum := 1; txNum <= 200; txNum++ {
|
||||
txCtx, cancel := context.WithTimeout(ctx, 15*time.Second) // Should never hit, but just in case
|
||||
tx, err := db.BeginTx(txCtx, nil)
|
||||
require.NoError(t, err)
|
||||
f := func() error {
|
||||
defer cancel()
|
||||
// update required to get a transaction conflict.
|
||||
_, e2 := tx.ExecContext(txCtx, "UPDATE test_table SET value = $1 WHERE id = 1", fmt.Sprintf("tx%d value", txNum))
|
||||
require.NoError(t, e2)
|
||||
_, e2 = tx.ExecContext(txCtx, "INSERT INTO test_table (id, value) VALUES ($1, $2)", txNum+1, fmt.Sprintf("tx%d new row", txNum))
|
||||
require.NoError(t, e2)
|
||||
|
||||
// We want other transactions to have the chance to mess with the db. We'll verify that what's committed is what we expect.
|
||||
time.Sleep(time.Duration(rand.Intn(1000)+500) * time.Millisecond)
|
||||
|
||||
// This will commit the transaction, or error.
|
||||
_, e2 = tx.ExecContext(txCtx, "SELECT dolt_commit('--amend','-a', '-m', $1)", fmt.Sprintf("tx%d amend", txNum))
|
||||
if e2 != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
return e2
|
||||
}
|
||||
transactions = append(transactions, txIdFunc{txNum: txNum, txFunc: f})
|
||||
}
|
||||
|
||||
rand.Shuffle(len(transactions), func(i, j int) {
|
||||
transactions[i], transactions[j] = transactions[j], transactions[i]
|
||||
})
|
||||
|
||||
var atomicInt atomic.Int32
|
||||
atomicInt.Store(-1)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, txn := range transactions {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
// radomly sleep .5 - 1.5 seconds
|
||||
time.Sleep(time.Duration(rand.Intn(1000)+500) * time.Millisecond)
|
||||
err := txn.txFunc()
|
||||
|
||||
if err == nil {
|
||||
// If there are multiple updates, something went wrong.
|
||||
assert.True(t, atomicInt.CompareAndSwap(-1, int32(txn.txNum)))
|
||||
}
|
||||
// Errors are expected.
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
winner := atomicInt.Load()
|
||||
require.NotEqual(t, -1, winner)
|
||||
|
||||
// Verify there are only 2 rows in the table
|
||||
rows, err := db.QueryContext(ctx, "SELECT COUNT(*) FROM test_table")
|
||||
require.NoError(t, err)
|
||||
defer rows.Close()
|
||||
rows.Next()
|
||||
var count int
|
||||
err = rows.Scan(&count)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, count)
|
||||
|
||||
// Verify the final state
|
||||
row := db.QueryRowContext(ctx, "SELECT value FROM test_table WHERE id = 1")
|
||||
var value string
|
||||
err = row.Scan(&value)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fmt.Sprintf("tx%d value", winner), value)
|
||||
|
||||
row = db.QueryRowContext(ctx, "SELECT value FROM test_table WHERE id != 1")
|
||||
err = row.Scan(&value)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fmt.Sprintf("tx%d new row", winner), value)
|
||||
|
||||
// Verify the commit message
|
||||
row = db.QueryRowContext(ctx, "SELECT message FROM dolt_log ORDER BY date DESC LIMIT 1")
|
||||
var commitMessage string
|
||||
err = row.Scan(&commitMessage)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fmt.Sprintf("tx%d amend", winner), commitMessage)
|
||||
}
|
||||
|
||||
func testSQLRacingAmendWithSerial(t *testing.T) {
|
||||
t.Skip("concurrent inserts on the same sequence fail with duplicate keys")
|
||||
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
ports := newPorts(t)
|
||||
u, err := driver.NewDoltUser()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
u.Cleanup()
|
||||
})
|
||||
|
||||
rs, err := u.MakeRepoStore()
|
||||
require.NoError(t, err)
|
||||
_, err = rs.MakeRepo("racing_amend_test")
|
||||
require.NoError(t, err)
|
||||
|
||||
srvSettings := &driver.Server{
|
||||
Args: []string{},
|
||||
DynamicPort: "server_port",
|
||||
}
|
||||
server := StartServer(t, rs, "racing_amend_test", srvSettings, ports)
|
||||
|
||||
db, err := server.DB(driver.Connection{})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
db.Close()
|
||||
})
|
||||
|
||||
_, err = db.ExecContext(ctx, `
|
||||
CREATE TABLE test_table (
|
||||
id serial PRIMARY KEY,
|
||||
value VARCHAR(20)
|
||||
);`)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.ExecContext(ctx, "INSERT INTO test_table VALUES (1, 'initial')")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.ExecContext(ctx, "SELECT dolt_commit('-A','-m', 'initial commit')")
|
||||
require.NoError(t, err)
|
||||
|
||||
type txIdFunc struct {
|
||||
txNum int
|
||||
txFunc func() error
|
||||
}
|
||||
|
||||
var transactions []txIdFunc
|
||||
for txNum := 1; txNum <= 200; txNum++ {
|
||||
txCtx, cancel := context.WithTimeout(ctx, 15*time.Second) // Should never hit, but just in case
|
||||
tx, err := db.BeginTx(txCtx, nil)
|
||||
require.NoError(t, err)
|
||||
f := func() error {
|
||||
defer cancel()
|
||||
// update required to get a transaction conflict.
|
||||
_, e2 := tx.ExecContext(txCtx, "UPDATE test_table SET value = $1 WHERE id = 1", fmt.Sprintf("tx%d value", txNum))
|
||||
require.NoError(t, e2)
|
||||
_, e2 = tx.ExecContext(txCtx, "INSERT INTO test_table (value) VALUES ($1)", fmt.Sprintf("tx%d new row", txNum))
|
||||
require.NoError(t, e2)
|
||||
|
||||
// We want other transactions to have the chance to mess with the db. We'll verify that what's committed is what we expect.
|
||||
time.Sleep(time.Duration(rand.Intn(1000)+500) * time.Millisecond)
|
||||
|
||||
// This will commit the transaction, or error.
|
||||
_, e2 = tx.ExecContext(txCtx, "SELECT dolt_commit('--amend','-a', '-m', $1)", fmt.Sprintf("tx%d amend", txNum))
|
||||
if e2 != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
return e2
|
||||
}
|
||||
transactions = append(transactions, txIdFunc{txNum: txNum, txFunc: f})
|
||||
}
|
||||
|
||||
rand.Shuffle(len(transactions), func(i, j int) {
|
||||
transactions[i], transactions[j] = transactions[j], transactions[i]
|
||||
})
|
||||
|
||||
var atomicInt atomic.Int32
|
||||
atomicInt.Store(-1)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, txn := range transactions {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
// radomly sleep .5 - 1.5 seconds
|
||||
time.Sleep(time.Duration(rand.Intn(1000)+500) * time.Millisecond)
|
||||
err := txn.txFunc()
|
||||
|
||||
if err == nil {
|
||||
// If there are multiple updates, something went wrong.
|
||||
assert.True(t, atomicInt.CompareAndSwap(-1, int32(txn.txNum)))
|
||||
}
|
||||
// Errors are expected.
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
winner := atomicInt.Load()
|
||||
require.NotEqual(t, -1, winner)
|
||||
|
||||
// Verify there are only 2 rows in the table
|
||||
rows, err := db.QueryContext(ctx, "SELECT COUNT(*) FROM test_table")
|
||||
require.NoError(t, err)
|
||||
defer rows.Close()
|
||||
rows.Next()
|
||||
var count int
|
||||
err = rows.Scan(&count)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, count)
|
||||
|
||||
// Verify the final state
|
||||
row := db.QueryRowContext(ctx, "SELECT value FROM test_table WHERE id = 1")
|
||||
var value string
|
||||
err = row.Scan(&value)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fmt.Sprintf("tx%d value", winner), value)
|
||||
|
||||
row = db.QueryRowContext(ctx, "SELECT value FROM test_table WHERE id != 1")
|
||||
err = row.Scan(&value)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fmt.Sprintf("tx%d new row", winner), value)
|
||||
|
||||
// Verify the commit message
|
||||
row = db.QueryRowContext(ctx, "SELECT message FROM dolt_log ORDER BY date DESC LIMIT 1")
|
||||
var commitMessage string
|
||||
err = row.Scan(&commitMessage)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fmt.Sprintf("tx%d amend", winner), commitMessage)
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
// Copyright 2025 Dolthub, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
driver "github.com/dolthub/doltgresql/integration-tests/go-sql-server-driver/driver"
|
||||
)
|
||||
|
||||
func TestStatsGCConcurrency(t *testing.T) {
|
||||
// Doltgres' statistics provider does not implement the ExtendedStatsProvider
|
||||
// interface that dolt_stats_gc() requires ("provider does not implement
|
||||
// ExtendedStatsProvider"), so this test cannot run yet. The logic below is a
|
||||
// faithful port of the Dolt test and should be enabled once Doltgres supports
|
||||
// statistics GC.
|
||||
t.Skip("dolt_stats_gc() is unsupported in Doltgres (statistics provider does not implement ExtendedStatsProvider)")
|
||||
t.Parallel()
|
||||
// At one time, it was possible for a GC of stats to block the
|
||||
// analyzer from accessing stats for a long time. This test sets
|
||||
// up the following condition:
|
||||
//
|
||||
// * Several branches, each with 10 tables, each with several indexes.
|
||||
// All of the branches and tables and indexes are non-empty.
|
||||
//
|
||||
// * One thread calls `select dolt_stats_gc()`
|
||||
//
|
||||
// * Other threads continue to connect to the database and run
|
||||
// queries.
|
||||
//
|
||||
// * We expect all queries to complete without much delay. If
|
||||
// there is a long delay on the read queries, this fails the
|
||||
// test.
|
||||
ctx := context.Background()
|
||||
u, err := driver.NewDoltUser()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
u.Cleanup()
|
||||
})
|
||||
|
||||
rs, err := u.MakeRepoStore()
|
||||
require.NoError(t, err)
|
||||
_, err = rs.MakeRepo("stats_gc_concurrency_test")
|
||||
require.NoError(t, err)
|
||||
|
||||
srvSettings := &driver.Server{
|
||||
Args: []string{},
|
||||
DynamicPort: "server",
|
||||
}
|
||||
ports := newPorts(t)
|
||||
server := StartServer(t, rs, "stats_gc_concurrency_test", srvSettings, ports)
|
||||
|
||||
// Connect to the database
|
||||
db, err := server.DB(driver.Connection{})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
db.Close()
|
||||
})
|
||||
db.SetMaxOpenConns(32)
|
||||
db.SetMaxIdleConns(32)
|
||||
|
||||
numbranches := 3
|
||||
|
||||
var branches = []string{"main"}
|
||||
for i := 0; i < numbranches; i++ {
|
||||
branches = append(branches, uuid.New().String())
|
||||
}
|
||||
|
||||
var tables = []string{
|
||||
"t0", "t1", "t2", "t3", "t4",
|
||||
"t5", "t6", "t7", "t8", "t9",
|
||||
}
|
||||
var columns = []string{
|
||||
"c0", "c1", "c2", "c3", "c4",
|
||||
"c5", "c6", "c7", "c8", "c9",
|
||||
}
|
||||
var columndefs = []string{}
|
||||
for _, col := range columns {
|
||||
columndefs = append(columndefs, fmt.Sprintf("%s bigint unique", col))
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
_, err = db.ExecContext(ctx, fmt.Sprintf(strings.Join([]string{
|
||||
"CREATE TABLE %s (",
|
||||
"id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,",
|
||||
strings.Join(columndefs, ",\n"),
|
||||
");",
|
||||
}, "\n"), table))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
_, err = db.ExecContext(ctx, "select dolt_commit('-Am', 'initial commit')")
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, branch := range branches[1:] {
|
||||
_, err = db.ExecContext(ctx, fmt.Sprintf("select dolt_checkout('-b', '%s')", branch))
|
||||
require.NoError(t, err)
|
||||
for _, table := range tables {
|
||||
_, err = db.ExecContext(ctx, fmt.Sprintf("insert into %s values %s", table, t0_values_str()))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = db.ExecContext(ctx, "select dolt_checkout('main')")
|
||||
require.NoError(t, err)
|
||||
for _, table := range tables {
|
||||
_, err = db.ExecContext(ctx, fmt.Sprintf("insert into %s values %s", table, t0_values_str()))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
numThreads := 10
|
||||
// The workers will run until this is closed. This will be closed after the stats_gc() is finished.
|
||||
stop := make(chan struct{})
|
||||
maxQuery := make([]time.Duration, numThreads)
|
||||
eg, egCtx := errgroup.WithContext(ctx)
|
||||
for i := 0; i < numThreads; i++ {
|
||||
eg.Go(func() error {
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
err := func() (err error) {
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
elapsed := time.Since(start)
|
||||
if elapsed > maxQuery[i] {
|
||||
maxQuery[i] = elapsed
|
||||
}
|
||||
}()
|
||||
conn, err := db.Conn(egCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
err = errors.Join(err, conn.Close())
|
||||
}()
|
||||
branch := branches[rand.IntN(len(branches))]
|
||||
_, err = conn.ExecContext(egCtx, fmt.Sprintf("select dolt_checkout('%s')", branch))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t1 := tables[rand.IntN(len(tables))]
|
||||
t2 := tables[rand.IntN(len(tables))]
|
||||
for t2 == t1 {
|
||||
t2 = tables[rand.IntN(len(tables))]
|
||||
}
|
||||
c1 := columns[rand.IntN(len(columns))]
|
||||
c2 := columns[rand.IntN(len(columns))]
|
||||
rows, err := conn.QueryContext(egCtx, fmt.Sprintf("select * from %s join %s on %s.%s = %s.%s",
|
||||
t1, t2, t1, c1, t2, c2))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
err = errors.Join(err, rows.Close())
|
||||
}()
|
||||
for rows.Next() {
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var gcDuration time.Duration
|
||||
eg.Go(func() (err error) {
|
||||
defer close(stop)
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
gcDuration = time.Since(start)
|
||||
}()
|
||||
conn, err := db.Conn(egCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
err = errors.Join(err, conn.Close())
|
||||
}()
|
||||
_, err = conn.ExecContext(egCtx, "select dolt_stats_gc()")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
require.NoError(t, eg.Wait())
|
||||
|
||||
for _, d := range maxQuery {
|
||||
assert.Greater(t, 3*time.Second, d)
|
||||
assert.Greater(t, gcDuration, d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatsAnalyzeTableSpeed(t *testing.T) {
|
||||
// Doltgres does not provide the dolt_stats_timers() procedure that this
|
||||
// test uses to slow background stats refresh, so the rate-limiting scenario
|
||||
// it asserts cannot be set up. The logic below is a faithful port and
|
||||
// should be enabled once Doltgres supports dolt_stats_timers().
|
||||
t.Skip("dolt_stats_timers() is unsupported in Doltgres")
|
||||
t.Parallel()
|
||||
// At one time, calling ANALYZE would go through the rate-limiting system
|
||||
// that background stats refresh goes through. This would make ANALYZE take
|
||||
// a longer time than necessary, and it would be very dependent on the stats
|
||||
// timers that were in effect.
|
||||
//
|
||||
// This tests that even with very large stats timers, ANALYZE behaves
|
||||
// reasonably.
|
||||
ctx := context.Background()
|
||||
u, err := driver.NewDoltUser()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
u.Cleanup()
|
||||
})
|
||||
|
||||
rs, err := u.MakeRepoStore()
|
||||
require.NoError(t, err)
|
||||
_, err = rs.MakeRepo("stats_analyze_table_test")
|
||||
require.NoError(t, err)
|
||||
|
||||
srvSettings := &driver.Server{
|
||||
Args: []string{},
|
||||
DynamicPort: "server",
|
||||
}
|
||||
ports := newPorts(t)
|
||||
server := StartServer(t, rs, "stats_analyze_table_test", srvSettings, ports)
|
||||
|
||||
// Connect to the database
|
||||
db, err := server.DB(driver.Connection{})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
db.Close()
|
||||
})
|
||||
|
||||
// Task rate-limiting of one per minute will make things more
|
||||
// than slow enough that ANALYZE down below will fail on
|
||||
// the context timeout if these rates are affecting the
|
||||
// ANALYZE call.
|
||||
_, err = db.ExecContext(ctx, "select dolt_stats_timers(60000000000, 60000000000)")
|
||||
require.NoError(t, err)
|
||||
|
||||
var columns = []string{
|
||||
"c0", "c1", "c2", "c3", "c4",
|
||||
"c5", "c6", "c7", "c8", "c9",
|
||||
}
|
||||
var columndefs = []string{}
|
||||
for _, col := range columns {
|
||||
columndefs = append(columndefs, fmt.Sprintf("%s bigint unique", col))
|
||||
}
|
||||
|
||||
_, err = db.ExecContext(ctx, fmt.Sprintf(strings.Join([]string{
|
||||
"CREATE TABLE %s (",
|
||||
"id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,",
|
||||
strings.Join(columndefs, ",\n"),
|
||||
");",
|
||||
}, "\n"), "t0"))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.ExecContext(ctx, "select dolt_commit('-Am', 'initial commit')")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.ExecContext(ctx, "select dolt_checkout('main')")
|
||||
require.NoError(t, err)
|
||||
_, err = db.ExecContext(ctx, fmt.Sprintf("insert into %s values %s", "t0", t0_values_strs(65536)))
|
||||
require.NoError(t, err)
|
||||
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, time.Second*30)
|
||||
defer cancel()
|
||||
_, err = db.ExecContext(timeoutCtx, "ANALYZE t0")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func t0_values_str() string {
|
||||
return fmt.Sprintf("(%d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %d)",
|
||||
rand.IntN(1<<48), rand.IntN(1<<48), rand.IntN(1<<48), rand.IntN(1<<48),
|
||||
rand.IntN(1<<48), rand.IntN(1<<48), rand.IntN(1<<48), rand.IntN(1<<48),
|
||||
rand.IntN(1<<48), rand.IntN(1<<48), rand.IntN(1<<48))
|
||||
}
|
||||
|
||||
func t0_values_strs(n int) string {
|
||||
vals := make([]string, n)
|
||||
for i := range vals {
|
||||
vals[i] = t0_values_str()
|
||||
}
|
||||
return strings.Join(vals, ",")
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pk,c1,c2,c3,c4,c5
|
||||
0,1,2,3,4,5
|
||||
1,1,2,3,4,5
|
||||
|
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"keys": [
|
||||
{
|
||||
"kty": "RSA",
|
||||
"e": "AQAB",
|
||||
"use": "sig",
|
||||
"kid": "749df841-6e38-48f1-a178-20ecdd0b09f7",
|
||||
"alg": "RS256",
|
||||
"n": "uPU3mTbLgki6xF05WLp1xFL3t9ZfrYygcgBq5-qJhz-n5dTicxiCt1X0PvDzomqGJFap7q_yB4z97w9GcoeRXzPrGyeVN324cOfCqgyrhR7pMXfrkAY885eqp3068Wo2V7OgS0yLlzD4XRjj4A9g8ssuZKhwgDcUqdiWk3ar33e3KMnz-GYqRrfNHnHkZ-WPqSwseM5ng9YgmKy8j3JeKLG336yGIRXQGAtZdekWBCBSlT_dW9bxiLdUMzS4ENzJzmYTrfcfQY6mm9C-DTprv3Lwq1h95Yhh7taIhRcBpin6Il5sBD7OLUUGHktDR_frEqkN8a2DeL5c81HOqT9-JQ"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"keys": [
|
||||
{
|
||||
"p": "-dh54E1ei0vAT6CR-JjXnpQjBQLpILN1d9CY-hFl0S56oMlt1tU46nHGM5COVsQJv8OKCFWGQJY9BixlGAk9UGKRAxSKjhePsKDFr7vOdwmTg6mrF1JFa02Zuz3f7zRAsFd1rAtDcHTMmxISaIEZuBMeOougMJ1XdbLxqjkZLLM",
|
||||
"kty": "RSA",
|
||||
"q": "vYOPShvpX2wHMdLU7WTXPHre4P1sKvdZzEtFbOXnsfRNLeslAxFeTd7FpHc8wRA3cB5PTMzZU7jEyj3pHmZAq-2R_gu63gLjl8erI36cAmIgzB_seFQM8licuS6TbZvlYFM2M1NNjqTASLO5feGAtSoUSJmYvndn9OhvSa-Yxcc",
|
||||
"d": "FQFsfg8WD8bYx0JbJ_ONOm29yngjR5-H_UqE2a_uTJjzJYwG59Fpzw6I_bj5woFcmLXq-LusviTKFiNi-dDhtrE7y0q0jKfPkasQlaV4uVaoX0DiVOoQdA3OiNUVI6PPZih1VPftho8-NbyE7MZyWUCwFSh4FmerBhseBsNcg7VwBr4CZimmSEKMJXHLMR7jZfrVJAuFGSHJWrdIS04sPcCyBX4V7FLCRBr8BRiM7LznwP4tDHG8-bferqtXH4-XwPY3HjaHRTK7gRQspCKPv-kPdnQl2rH0zJa5kY7MQq4RDtINXSefM_sek3RrhXUXvG2JF-1JiQ3EUi59eY7ZxQ",
|
||||
"e": "AQAB",
|
||||
"use": "sig",
|
||||
"kid": "749df841-6e38-48f1-a178-20ecdd0b09f7",
|
||||
"qi": "isaLZv3bKV0Osmfl5ay6LABeefMkun1tw2AJD6Ouqo9Z7mDLaTUDP5XxwypKry3ZVd-N1ZMkfgGi6Q-O-xLq2AU5mnI5bHbDAiN3P9Jp-Tex6c6rzT24fIbGQMV-Rul8z10_FsYbFv5CWr9V_jFBNrR8gX4XGtHn1qFVBo_K5Rk",
|
||||
"dp": "tRlEvmFWdoGiFBW_uQKQyFF4UNmbQijSrNZ3DEwwEUAvgvx-sYo8hzORBy9w_VN7_ZQvKXtUpNxBv4fOf22zE-FeW204QWaysMTYhlkLfx1h373MVks8JltJY3-mIi0t9qRulxZS--Ctrnma_kUV72dsMeOjaZmjG51prolUxiE",
|
||||
"alg": "RS256",
|
||||
"dq": "skqLC9WmgLdJLX6EA7LTK3sNI-5HTUTXnnNSJVlF2Q1VbtXCRFiat_fVSR1Ecv2mqjxZro8qBrHVsc78-jSIszcWGkM-0o81Px4By6rZawSWhnOiLLImW_kxuKYw3PXFnhGq9C5y0Lf-jmdHIz57r_SekI6wPMBpdOcXi-M_fxE",
|
||||
"n": "uPU3mTbLgki6xF05WLp1xFL3t9ZfrYygcgBq5-qJhz-n5dTicxiCt1X0PvDzomqGJFap7q_yB4z97w9GcoeRXzPrGyeVN324cOfCqgyrhR7pMXfrkAY885eqp3068Wo2V7OgS0yLlzD4XRjj4A9g8ssuZKhwgDcUqdiWk3ar33e3KMnz-GYqRrfNHnHkZ-WPqSwseM5ng9YgmKy8j3JeKLG336yGIRXQGAtZdekWBCBSlT_dW9bxiLdUMzS4ENzJzmYTrfcfQY6mm9C-DTprv3Lwq1h95Yhh7taIhRcBpin6Il5sBD7OLUUGHktDR_frEqkN8a2DeL5c81HOqT9-JQ"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,704 @@
|
||||
// Copyright 2024 Dolthub, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
|
||||
driver "github.com/dolthub/doltgresql/integration-tests/go-sql-server-driver/driver"
|
||||
)
|
||||
|
||||
var GlobalPorts GlobalDynamicResources
|
||||
|
||||
type TestDef struct {
|
||||
Tests []Test `yaml:"tests"`
|
||||
|
||||
// If true, RunTestfile will run each subtest in parallel.
|
||||
Parallel bool `yaml:"parallel"`
|
||||
}
|
||||
|
||||
// Test is a single test to run. The Repos and MultiRepos will be created, and
|
||||
// any Servers defined within them will be started. The interactions and
|
||||
// assertions defined in Conns will be run.
|
||||
type Test struct {
|
||||
Name string `yaml:"name"`
|
||||
Repos []driver.TestRepo `yaml:"repos"`
|
||||
MultiRepos []driver.MultiRepo `yaml:"multi_repos"`
|
||||
Conns []driver.Connection `yaml:"connections"`
|
||||
|
||||
// Skip the entire test with this reason.
|
||||
Skip string `yaml:"skip"`
|
||||
}
|
||||
|
||||
// Set this environment variable to effectively disable timeouts for debugging.
|
||||
const debugEnvKey = "DOLTGRES_SQL_SERVER_TEST_DEBUG"
|
||||
|
||||
var timeout = 20 * time.Second
|
||||
|
||||
func init() {
|
||||
_, ok := os.LookupEnv(debugEnvKey)
|
||||
if ok {
|
||||
timeout = 1000 * time.Hour
|
||||
}
|
||||
}
|
||||
|
||||
func ParseTestsFile(path string) (TestDef, error) {
|
||||
contents, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return TestDef{}, err
|
||||
}
|
||||
dec := yaml.NewDecoder(bytes.NewReader(contents))
|
||||
dec.KnownFields(true)
|
||||
var res TestDef
|
||||
err = dec.Decode(&res)
|
||||
return res, err
|
||||
}
|
||||
|
||||
// MakeRepo creates the database for |r| within the store |rs| and writes any
|
||||
// with_files for the repo. Unlike Dolt, the doltgres server runs from the
|
||||
// data-dir (the store) and each repo is a database subdirectory, so with_files
|
||||
// are written relative to the store directory.
|
||||
func MakeRepo(t *testing.T, rs driver.RepoStore, r driver.TestRepo, ports *DynamicResources) driver.Repo {
|
||||
repo, err := rs.MakeRepo(r.Name)
|
||||
require.NoError(t, err)
|
||||
for _, f := range r.WithFiles {
|
||||
f.Template = func(s string) string {
|
||||
return ports.ApplyTemplate(s)
|
||||
}
|
||||
require.NoError(t, f.WriteAtDir(rs.Dir))
|
||||
}
|
||||
for _, remote := range r.WithRemotes {
|
||||
url := remote.URL
|
||||
url = ports.ApplyTemplate(url)
|
||||
require.NoError(t, repo.CreateRemote(remote.Name, url))
|
||||
}
|
||||
return repo
|
||||
}
|
||||
|
||||
// Simple interface for wrapping *testing.T. Used for retryingT.
|
||||
type TestingT interface {
|
||||
Fatal(...any)
|
||||
|
||||
FailNow()
|
||||
|
||||
Errorf(string, ...any)
|
||||
|
||||
Cleanup(func())
|
||||
|
||||
TempDir() string
|
||||
}
|
||||
|
||||
// Globally available dynamic ports, backs every instance of
|
||||
// DynamicPorts and hands them out in a thread-safe manner.
|
||||
//
|
||||
// XXX: This structure and its initialization does not currently look
|
||||
// for "available" ports on the running host. It simply avoids handing
|
||||
// out the same port to two separate tests that are running at the
|
||||
// same time. It recycles ports as tests complete.
|
||||
type GlobalDynamicResources struct {
|
||||
mu sync.Mutex
|
||||
available []int
|
||||
}
|
||||
|
||||
func (g *GlobalDynamicResources) GetPort(t TestingT) int {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
if len(g.available) == 0 {
|
||||
t.Fatal("cannot get a port; we are all out.")
|
||||
}
|
||||
next := g.available[len(g.available)-1]
|
||||
g.available = g.available[:len(g.available)-1]
|
||||
return next
|
||||
}
|
||||
|
||||
func (g *GlobalDynamicResources) Return(n int) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
g.available = append(g.available, n)
|
||||
}
|
||||
|
||||
// Tracks dynamic resources available for expansion in test
|
||||
// definitions, for example through `{{get_port ...}}` templates for
|
||||
// server args and config files.
|
||||
type DynamicResources struct {
|
||||
// Where we go when we need a new one.
|
||||
global *GlobalDynamicResources
|
||||
|
||||
t TestingT
|
||||
|
||||
// Where we put allocated ports. For a given test, the first
|
||||
// use will get a new unused port from
|
||||
// GlobalDynamicPorts. Then that same port will be returned
|
||||
// from here for all uses. When the test finishes, its Cleanup
|
||||
// returns the port to GlobalDynamicResources.
|
||||
allocatedPorts map[string]int
|
||||
|
||||
// Where we put allocated temp directories. These get removed
|
||||
// on cleanup.
|
||||
allocatedTempDirs map[string]string
|
||||
}
|
||||
|
||||
func (d *DynamicResources) GetPort(name string) (int, bool) {
|
||||
if d.allocatedPorts != nil {
|
||||
v, ok := d.allocatedPorts[name]
|
||||
return v, ok
|
||||
} else {
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DynamicResources) GetTempDir(name string) (string, bool) {
|
||||
if d.allocatedTempDirs != nil {
|
||||
v, ok := d.allocatedTempDirs[name]
|
||||
return v, ok
|
||||
} else {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DynamicResources) GetOrAllocatePort(name string) int {
|
||||
v, ok := d.GetPort(name)
|
||||
if ok {
|
||||
return v
|
||||
}
|
||||
v = d.global.GetPort(d.t)
|
||||
if d.allocatedPorts == nil {
|
||||
d.allocatedPorts = make(map[string]int)
|
||||
// We register one cleanup function for the entire
|
||||
// DynamicPorts and we return them all at once.
|
||||
//
|
||||
// In cases where there are two dependent servers, we
|
||||
// want to return all ports after both servers have
|
||||
// been shut down. If we return them as we allocated
|
||||
// them, it's possible that we allocated them to
|
||||
// render the entire config for the first server, some
|
||||
// referring to the second server, for example. If
|
||||
// testing.T runs cleanups in FIFO order, and the
|
||||
// Cleanup for running the second server is
|
||||
// responsible for shutting it down, it is possible we
|
||||
// would return the second server's ports before it is
|
||||
// shut down if we didn't return them all at once.
|
||||
d.t.Cleanup(func() {
|
||||
for _, p := range d.allocatedPorts {
|
||||
d.global.Return(p)
|
||||
}
|
||||
})
|
||||
}
|
||||
d.allocatedPorts[name] = v
|
||||
return v
|
||||
}
|
||||
|
||||
func (d *DynamicResources) GetOrAllocateTempDir(name string) string {
|
||||
v, ok := d.GetTempDir(name)
|
||||
if ok {
|
||||
return v
|
||||
}
|
||||
v = d.t.TempDir()
|
||||
if d.allocatedTempDirs == nil {
|
||||
d.allocatedTempDirs = make(map[string]string)
|
||||
d.t.Cleanup(func() {
|
||||
d.allocatedTempDirs = nil
|
||||
})
|
||||
}
|
||||
d.allocatedTempDirs[name] = v
|
||||
return v
|
||||
}
|
||||
|
||||
func (d *DynamicResources) ApplyTemplate(s string) string {
|
||||
tmpl, err := template.New("sql").Funcs(map[string]any{
|
||||
"get_port": d.GetOrAllocatePort,
|
||||
"get_tempdir": d.GetOrAllocateTempDir,
|
||||
}).Parse(s)
|
||||
require.NoError(d.t, err)
|
||||
var buf bytes.Buffer
|
||||
err = tmpl.Execute(&buf, nil)
|
||||
require.NoError(d.t, err)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// testLogWriter is an io.Writer that sends each line to t.Log.
|
||||
// It buffers partial lines until a newline is received.
|
||||
type testLogWriter struct {
|
||||
t *testing.T
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func (w *testLogWriter) Write(p []byte) (int, error) {
|
||||
w.buf = append(w.buf, p...)
|
||||
for {
|
||||
i := bytes.IndexByte(w.buf, '\n')
|
||||
if i < 0 {
|
||||
break
|
||||
}
|
||||
w.t.Helper()
|
||||
w.t.Log(string(w.buf[:i]))
|
||||
w.buf = w.buf[i+1:]
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Flush any remaining partial line.
|
||||
func (w *testLogWriter) Flush() {
|
||||
if len(w.buf) > 0 {
|
||||
w.t.Helper()
|
||||
w.t.Log(string(w.buf))
|
||||
w.buf = w.buf[:0]
|
||||
}
|
||||
}
|
||||
|
||||
func newTestLogWriter(t *testing.T) io.Writer {
|
||||
w := &testLogWriter{t: t}
|
||||
t.Cleanup(w.Flush)
|
||||
return w
|
||||
}
|
||||
|
||||
// prepareDoltgresServerArgs translates the server |args| from a test
|
||||
// definition into arguments valid for the doltgres binary, generating a config
|
||||
// file in |cwd| that fixes the listener to |port| (and a unique unix socket so
|
||||
// parallel servers do not collide). doltgres has a minimal CLI (no -P, -l,
|
||||
// --max-connections, etc.), so the listener port can only be set via the
|
||||
// config file.
|
||||
//
|
||||
// If the incoming args reference a config file via `--config <path>` (or
|
||||
// `--config=<path>`), that file is loaded as the base config and the listener
|
||||
// settings are overlaid onto it; otherwise a fresh config is generated. The
|
||||
// `--data-dir` argument is added (as `.`) if not already present.
|
||||
func prepareDoltgresServerArgs(t *testing.T, cwd, name string, port int, args []string) []string {
|
||||
base := map[string]any{}
|
||||
var passthrough []string
|
||||
configPath := ""
|
||||
for i := 0; i < len(args); i++ {
|
||||
a := args[i]
|
||||
switch {
|
||||
case a == "--config" || a == "-config":
|
||||
if i+1 < len(args) {
|
||||
configPath = args[i+1]
|
||||
i++
|
||||
}
|
||||
case len(a) > 9 && a[:9] == "--config=":
|
||||
configPath = a[9:]
|
||||
case len(a) > 8 && a[:8] == "-config=":
|
||||
configPath = a[8:]
|
||||
default:
|
||||
passthrough = append(passthrough, a)
|
||||
}
|
||||
}
|
||||
|
||||
if configPath != "" {
|
||||
p := configPath
|
||||
if !filepath.IsAbs(p) {
|
||||
p = filepath.Join(cwd, p)
|
||||
}
|
||||
if contents, err := os.ReadFile(p); err == nil {
|
||||
_ = yaml.Unmarshal(contents, &base)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
listener, _ := base["listener"].(map[string]any)
|
||||
if listener == nil {
|
||||
listener = map[string]any{}
|
||||
}
|
||||
listener["host"] = "127.0.0.1"
|
||||
listener["port"] = port
|
||||
listener["socket"] = filepath.Join(os.TempDir(), fmt.Sprintf("dg-%d.sock", port))
|
||||
base["listener"] = listener
|
||||
|
||||
out, err := yaml.Marshal(base)
|
||||
require.NoError(t, err)
|
||||
genPath := filepath.Join(cwd, fmt.Sprintf(".generated-%s-config.yaml", sanitizeName(name)))
|
||||
require.NoError(t, os.WriteFile(genPath, out, 0640))
|
||||
|
||||
hasDataDir := false
|
||||
for _, a := range passthrough {
|
||||
if a == "--data-dir" || a == "-data-dir" || (len(a) > 11 && a[:11] == "--data-dir=") || (len(a) > 10 && a[:10] == "-data-dir=") {
|
||||
hasDataDir = true
|
||||
}
|
||||
}
|
||||
result := append([]string{}, passthrough...)
|
||||
if !hasDataDir {
|
||||
result = append(result, "--data-dir=.")
|
||||
}
|
||||
result = append(result, "--config="+genPath)
|
||||
return result
|
||||
}
|
||||
|
||||
func sanitizeName(s string) string {
|
||||
out := make([]rune, 0, len(s))
|
||||
for _, r := range s {
|
||||
if r == '/' || r == '\\' || r == ' ' {
|
||||
out = append(out, '_')
|
||||
} else {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func MakeServer(t *testing.T, dc driver.DoltCmdable, cwd string, s *driver.Server, resources *DynamicResources, manualOps ...driver.SqlServerOpt) *driver.SqlServer {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
if s.Port != 0 {
|
||||
t.Fatal("cannot specify s.Port on these tests; please use {{get_port ...}} and dynamic_port: to specify a dynamic port.")
|
||||
}
|
||||
if s.DynamicPort == "" {
|
||||
t.Fatal("you must specify s.DynamicPort on these tests; please use {{get_port ...}} and dynamic_port: to specify a dynamic port.")
|
||||
}
|
||||
port := resources.GetOrAllocatePort(s.DynamicPort)
|
||||
|
||||
rawArgs := make([]string, len(s.Args))
|
||||
for i := range rawArgs {
|
||||
rawArgs[i] = resources.ApplyTemplate(s.Args[i])
|
||||
}
|
||||
args := prepareDoltgresServerArgs(t, cwd, s.Name, port, rawArgs)
|
||||
|
||||
opts := []driver.SqlServerOpt{
|
||||
driver.WithArgs(args...),
|
||||
driver.WithName(s.Name),
|
||||
driver.WithLogWriter(newTestLogWriter(t)),
|
||||
driver.WithPort(port),
|
||||
}
|
||||
if len(s.Envs) > 0 {
|
||||
opts = append(opts, driver.WithEnvs(s.Envs...))
|
||||
}
|
||||
opts = append(opts, manualOps...)
|
||||
|
||||
var server *driver.SqlServer
|
||||
var err error
|
||||
if s.DebugPort != 0 {
|
||||
server, err = driver.DebugSqlServer(dc, s.DebugPort, opts...)
|
||||
} else {
|
||||
server, err = driver.StartSqlServer(dc, opts...)
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
if len(s.ErrorMatches) > 0 {
|
||||
err := server.ErrorStop()
|
||||
require.Error(t, err)
|
||||
output := server.Output.String()
|
||||
for _, a := range s.ErrorMatches {
|
||||
require.Regexp(t, a, output)
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
t.Cleanup(func() {
|
||||
// We use assert, not require here, since FailNow() in
|
||||
// a Cleanup does not make sense.
|
||||
err := server.GracefulStop()
|
||||
if assert.NoError(t, err) {
|
||||
output := server.Output.String()
|
||||
for _, a := range s.LogMatches {
|
||||
assert.Regexp(t, a, output)
|
||||
}
|
||||
for _, a := range s.LogNotMatches {
|
||||
assert.NotRegexp(t, a, output)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return server
|
||||
}
|
||||
}
|
||||
|
||||
// Runs the defined test, applying its asserts.
|
||||
func (test Test) Run(t *testing.T) {
|
||||
if test.Skip != "" {
|
||||
t.Skip(test.Skip)
|
||||
}
|
||||
|
||||
var ports DynamicResources
|
||||
ports.global = &GlobalPorts
|
||||
ports.t = t
|
||||
|
||||
u, err := driver.NewDoltUser()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
u.Cleanup()
|
||||
})
|
||||
|
||||
servers := make(map[string]*driver.SqlServer)
|
||||
|
||||
for _, r := range test.Repos {
|
||||
// Each repo with a server gets its own store (data-dir) so that
|
||||
// independent doltgres server processes do not collide on the same
|
||||
// on-disk databases.
|
||||
rs, err := u.MakeRepoStore()
|
||||
require.NoError(t, err)
|
||||
|
||||
MakeRepo(t, rs, r, &ports)
|
||||
|
||||
if r.Server != nil {
|
||||
if r.Server.Name == "" {
|
||||
r.Server.Name = r.Name
|
||||
}
|
||||
server := MakeServer(t, rs, rs.Dir, r.Server, &ports)
|
||||
if server != nil {
|
||||
server.DBName = r.Name
|
||||
servers[r.Name] = server
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, mr := range test.MultiRepos {
|
||||
// Each MultiRepo gets its own data-dir.
|
||||
rs, err := u.MakeRepoStore()
|
||||
require.NoError(t, err)
|
||||
for _, r := range mr.Repos {
|
||||
MakeRepo(t, rs, r, &ports)
|
||||
}
|
||||
for _, f := range mr.WithFiles {
|
||||
f.Template = func(s string) string {
|
||||
return ports.ApplyTemplate(s)
|
||||
}
|
||||
require.NoError(t, f.WriteAtDir(rs.Dir))
|
||||
}
|
||||
if mr.Server != nil {
|
||||
if mr.Server.Name == "" {
|
||||
mr.Server.Name = mr.Name
|
||||
}
|
||||
server := MakeServer(t, rs, rs.Dir, mr.Server, &ports)
|
||||
if server != nil {
|
||||
servers[mr.Name] = server
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i, c := range test.Conns {
|
||||
server := servers[c.On]
|
||||
require.NotNilf(t, server, "error in test spec: could not find server %s for connection %d", c.On, i)
|
||||
if c.RetryAttempts > 1 {
|
||||
RetryTestRun(t, c.RetryAttempts, func(t TestingT) {
|
||||
db, err := server.DB(c)
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
conn, err := db.Conn(context.Background())
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
|
||||
for _, q := range c.Queries {
|
||||
RunQueryAttempt(t, conn, q, &ports)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
func() {
|
||||
db, err := server.DB(c)
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
conn, err := db.Conn(context.Background())
|
||||
require.NoError(t, err)
|
||||
defer conn.Close()
|
||||
|
||||
for _, q := range c.Queries {
|
||||
RunQuery(t, conn, q, &ports)
|
||||
}
|
||||
}()
|
||||
}
|
||||
if c.RestartServer != nil {
|
||||
args := c.RestartServer.Args
|
||||
if args != nil {
|
||||
tmplArgs := make([]string, len(*args))
|
||||
for i := range tmplArgs {
|
||||
tmplArgs[i] = ports.ApplyTemplate((*args)[i])
|
||||
}
|
||||
prepared := prepareDoltgresServerArgs(t, server.Cmd.Dir, server.Name, server.Port, tmplArgs)
|
||||
args = &prepared
|
||||
}
|
||||
err := server.Restart(args, c.RestartServer.Envs)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func RunTestsFile(t *testing.T, path string) {
|
||||
def, err := ParseTestsFile(path)
|
||||
require.NoError(t, err)
|
||||
parallel := def.Parallel
|
||||
for _, test := range def.Tests {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
if parallel {
|
||||
t.Parallel()
|
||||
}
|
||||
test.Run(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func RunSingleTest(t *testing.T, path string, testName string) {
|
||||
def, err := ParseTestsFile(path)
|
||||
require.NoError(t, err)
|
||||
for _, test := range def.Tests {
|
||||
if test.Name == testName {
|
||||
t.Run(test.Name, test.Run)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type retryTestingT struct {
|
||||
*testing.T
|
||||
errorfStrings []string
|
||||
errorfArgs [][]interface{}
|
||||
failNow bool
|
||||
}
|
||||
|
||||
func (r *retryTestingT) Errorf(format string, args ...interface{}) {
|
||||
r.T.Helper()
|
||||
r.errorfStrings = append(r.errorfStrings, format)
|
||||
r.errorfArgs = append(r.errorfArgs, args)
|
||||
}
|
||||
|
||||
func (r *retryTestingT) FailNow() {
|
||||
r.T.Helper()
|
||||
r.failNow = true
|
||||
panic(r)
|
||||
}
|
||||
|
||||
func (r *retryTestingT) try(attempts int, test func(TestingT)) {
|
||||
for i := 0; i < attempts; i++ {
|
||||
r.errorfStrings = nil
|
||||
r.errorfArgs = nil
|
||||
r.failNow = false
|
||||
if i != 0 {
|
||||
time.Sleep(driver.RetrySleepDuration)
|
||||
}
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if _, ok := r.(*retryTestingT); ok {
|
||||
} else {
|
||||
panic(r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
test(r)
|
||||
}()
|
||||
if !r.failNow && len(r.errorfStrings) == 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
for i := range r.errorfStrings {
|
||||
r.T.Errorf(r.errorfStrings[i], r.errorfArgs[i]...)
|
||||
}
|
||||
if r.failNow {
|
||||
r.T.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func RetryTestRun(t *testing.T, attempts int, test func(TestingT)) {
|
||||
if attempts == 0 {
|
||||
attempts = 1
|
||||
}
|
||||
rtt := &retryTestingT{T: t}
|
||||
rtt.try(attempts, test)
|
||||
}
|
||||
|
||||
func RunQuery(t *testing.T, conn *sql.Conn, q driver.Query, ports *DynamicResources) {
|
||||
RetryTestRun(t, q.RetryAttempts, func(t TestingT) {
|
||||
RunQueryAttempt(t, conn, q, ports)
|
||||
})
|
||||
}
|
||||
|
||||
func RunQueryAttempt(t TestingT, conn *sql.Conn, q driver.Query, ports *DynamicResources) {
|
||||
args := make([]any, len(q.Args))
|
||||
for i := range q.Args {
|
||||
args[i] = q.Args[i]
|
||||
}
|
||||
if q.Query != "" {
|
||||
ctx, c := context.WithTimeout(context.Background(), timeout)
|
||||
defer c()
|
||||
rows, err := conn.QueryContext(ctx, q.Query, args...)
|
||||
if err == nil {
|
||||
defer rows.Close()
|
||||
}
|
||||
if q.ErrorMatch != "" {
|
||||
require.Error(t, err, "expected error running query %s", q.Query)
|
||||
require.Regexp(t, q.ErrorMatch, err.Error())
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
cols, err := rows.Columns()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, q.Result.Columns, cols)
|
||||
|
||||
rowstrings, err := RowsToStrings(len(cols), rows)
|
||||
require.NoError(t, err)
|
||||
if q.Result.Rows.Or != nil {
|
||||
match := *q.Result.Rows.Or
|
||||
for i := range match {
|
||||
for j := range match[i] {
|
||||
for k := range match[i][j] {
|
||||
match[i][j][k] = ports.ApplyTemplate(match[i][j][k])
|
||||
}
|
||||
}
|
||||
}
|
||||
require.Contains(t, match, rowstrings)
|
||||
}
|
||||
} else if q.Exec != "" {
|
||||
ctx, c := context.WithTimeout(context.Background(), timeout)
|
||||
defer c()
|
||||
exec := q.Exec
|
||||
exec = ports.ApplyTemplate(exec)
|
||||
_, err := conn.ExecContext(ctx, exec, args...)
|
||||
if q.ErrorMatch == "" {
|
||||
require.NoError(t, err, "error running query %s: %v", q.Exec, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
require.Regexp(t, q.ErrorMatch, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func RowsToStrings(cols int, rows *sql.Rows) ([][]string, error) {
|
||||
ret := make([][]string, 0)
|
||||
for rows.Next() {
|
||||
scanned := make([]any, cols)
|
||||
for j := range scanned {
|
||||
scanned[j] = new(sql.NullString)
|
||||
}
|
||||
err := rows.Scan(scanned...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
printed := make([]string, cols)
|
||||
for j := range scanned {
|
||||
s := scanned[j].(*sql.NullString)
|
||||
if !s.Valid {
|
||||
printed[j] = "NULL"
|
||||
} else {
|
||||
printed[j] = s.String
|
||||
}
|
||||
}
|
||||
ret = append(ret, printed)
|
||||
}
|
||||
return ret, rows.Err()
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
# go-sql-server-driver tests (Doltgres port)
|
||||
|
||||
These tests are ported from Dolt's
|
||||
`integration-tests/go-sql-server-driver`. They exercise a real `doltgres`
|
||||
server process over the Postgres wire protocol, driven either by the YAML test
|
||||
definition format (`tests/*.yaml`, run via `testdef.go`) or by standalone Go
|
||||
tests that use the `driver` package directly.
|
||||
|
||||
## Running
|
||||
|
||||
The tests locate the `doltgres` binary via the `DOLTGRES_BIN_PATH` environment
|
||||
variable, falling back to `doltgres` on the `PATH`.
|
||||
|
||||
```
|
||||
DOLTGRES_BIN_PATH=/path/to/doltgres go test ./...
|
||||
```
|
||||
|
||||
## Differences from the Dolt suite
|
||||
|
||||
The port preserves the structure and intent of the Dolt tests, translating only
|
||||
what the Postgres dialect and wire protocol, and the doltgres binary, require:
|
||||
|
||||
* **Server CLI.** `doltgres` has a minimal CLI (`--config`, `--data-dir`) and
|
||||
*no* `-P`/`-l`/`--max-connections` flags. The listener port can only be set
|
||||
via the config file, so the test framework (`MakeServer` in `testdef.go`)
|
||||
generates/merges a config file that injects the dynamic port, host, and a
|
||||
unique unix socket. Ported YAML expresses server settings via a config file
|
||||
(referenced with `--config`) rather than via Dolt CLI flags.
|
||||
|
||||
* **Data-dir model.** Doltgres serves databases out of a data-dir; each "repo"
|
||||
is a database subdirectory. The server runs from the store (data-dir) with
|
||||
`--data-dir=.`, and `with_files` are written relative to the store directory.
|
||||
Databases are initialized by briefly running a server (there is no
|
||||
`dolt init` equivalent).
|
||||
|
||||
* **Wire protocol.** Connections use the pgx stdlib driver. Default user is
|
||||
`postgres`, password `password`.
|
||||
|
||||
* **SQL dialect.** MySQL syntax is translated to Postgres (e.g.
|
||||
`AUTO_INCREMENT` -> `SERIAL`/`GENERATED`, backtick identifiers -> unquoted or
|
||||
double-quoted, `int`/`varchar(n)` are fine, `select ... from dual` ->
|
||||
`select ...`).
|
||||
|
||||
* **DOLT procedures.** Dolt's `CALL DOLT_*(...)` / `SELECT DOLT_*()` become
|
||||
`SELECT dolt_*(...)` in Doltgres. Result columns default to the function name
|
||||
(e.g. `dolt_add`), and the value is rendered as a Postgres array, e.g.
|
||||
`{0}`.
|
||||
|
||||
* **Result columns.** Postgres lowercases unquoted identifiers and uses the
|
||||
expression text / alias for computed columns; expected `columns:` and `rows:`
|
||||
are adjusted accordingly. Prefer explicit `AS` aliases.
|
||||
|
||||
* **Cluster / remotesapi replication.** Doltgres does not yet implement Dolt's
|
||||
cluster replication or the remotes API. Config structs for these are mirrored
|
||||
into doltgres so the config files parse, but the tests that depend on the
|
||||
feature are marked `skip:` with a reason until the feature lands.
|
||||
|
||||
When a ported test does not pass yet (missing feature or behavioral
|
||||
difference), it is marked with `skip:` (YAML) or `t.Skip(...)` (Go) with a short
|
||||
reason, per the porting instructions.
|
||||
@@ -0,0 +1,185 @@
|
||||
parallel: false
|
||||
tests:
|
||||
- name: users and grants cannot be run on standby
|
||||
skip: "cluster replication / MySQL grants not yet supported in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2
|
||||
connections:
|
||||
- on: server2
|
||||
queries:
|
||||
- exec: 'CREATE USER "brian" WITH PASSWORD ''brianspassword'''
|
||||
error_match: 'database server is set to read only mode'
|
||||
- exec: 'GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "aaron"'
|
||||
error_match: 'database server is set to read only mode'
|
||||
- name: create database cannot be run on standby
|
||||
skip: "cluster replication not yet implemented in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2
|
||||
connections:
|
||||
- on: server2
|
||||
queries:
|
||||
- exec: 'CREATE DATABASE my_db'
|
||||
error_match: 'database server is set to read only mode'
|
||||
- name: drop database cannot be run on standby
|
||||
skip: "cluster replication not yet implemented in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: "SET dolt_cluster_ack_writes_timeout_secs = 10"
|
||||
- exec: 'CREATE DATABASE repo1'
|
||||
- exec: 'USE repo1'
|
||||
- exec: 'CREATE TABLE vals (i INT PRIMARY KEY)'
|
||||
- exec: 'INSERT INTO vals VALUES (0),(1),(2),(3),(4)'
|
||||
- on: server2
|
||||
queries:
|
||||
- exec: 'DROP DATABASE repo1'
|
||||
error_match: 'database server is set to read only mode'
|
||||
- name: when a server becomes primary it accepts writes
|
||||
skip: "cluster replication not yet implemented in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2
|
||||
connections:
|
||||
- on: server2
|
||||
queries:
|
||||
- exec: "select dolt_assume_cluster_role('primary', 2)"
|
||||
- on: server2
|
||||
queries:
|
||||
- exec: "SET dolt_cluster_ack_writes_timeout_secs = 10"
|
||||
- exec: 'CREATE DATABASE repo1'
|
||||
- exec: 'USE repo1'
|
||||
- exec: 'CREATE TABLE vals (i INT PRIMARY KEY)'
|
||||
- exec: 'INSERT INTO vals VALUES (0),(1),(2),(3),(4)'
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: 'USE repo1'
|
||||
- query: 'SELECT COUNT(*) as count FROM vals'
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows:
|
||||
- ["5"]
|
||||
@@ -0,0 +1,460 @@
|
||||
parallel: true
|
||||
tests:
|
||||
- name: tls, bad root, failover to standby fails
|
||||
skip: "cluster replication not yet implemented in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: https://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
tls_key: key.pem
|
||||
tls_cert: cert.pem
|
||||
tls_ca: root.pem
|
||||
- name: key.pem
|
||||
source_path: $TESTGENDIR/rsa_key.pem
|
||||
- name: cert.pem
|
||||
source_path: $TESTGENDIR/rsa_chain.pem
|
||||
- name: root.pem
|
||||
source_path: $TESTGENDIR/ed25519_root.pem
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: https://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
tls_key: key.pem
|
||||
tls_cert: cert.pem
|
||||
tls_ca: root.pem
|
||||
- name: key.pem
|
||||
source_path: $TESTGENDIR/rsa_key.pem
|
||||
- name: cert.pem
|
||||
source_path: $TESTGENDIR/rsa_chain.pem
|
||||
- name: root.pem
|
||||
source_path: $TESTGENDIR/rsa_root.pem
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: 'create database repo1'
|
||||
- exec: "use repo1"
|
||||
- query: "select dolt_assume_cluster_role('standby', '11')"
|
||||
error_match: failed to transition from primary to standby gracefully
|
||||
- exec: "create table vals (i int primary key)"
|
||||
- exec: "insert into vals values (0)"
|
||||
- name: tls, expired leaf, failover to standby fails
|
||||
skip: "cluster replication not yet implemented in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: https://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
tls_key: key.pem
|
||||
tls_cert: cert.pem
|
||||
tls_ca: root.pem
|
||||
- name: key.pem
|
||||
source_path: $TESTGENDIR/rsa_key.pem
|
||||
- name: cert.pem
|
||||
source_path: $TESTGENDIR/rsa_chain.pem
|
||||
- name: root.pem
|
||||
source_path: $TESTGENDIR/rsa_root.pem
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: https://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
tls_key: key.pem
|
||||
tls_cert: cert.pem
|
||||
tls_ca: root.pem
|
||||
- name: key.pem
|
||||
source_path: $TESTGENDIR/rsa_exp_key.pem
|
||||
- name: cert.pem
|
||||
source_path: $TESTGENDIR/rsa_exp_chain.pem
|
||||
- name: root.pem
|
||||
source_path: $TESTGENDIR/rsa_root.pem
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: 'create database repo1'
|
||||
- exec: "use repo1"
|
||||
- query: "select dolt_assume_cluster_role('standby', '11')"
|
||||
error_match: failed to transition from primary to standby gracefully
|
||||
- exec: "create table vals (i int primary key)"
|
||||
- exec: "insert into vals values (0)"
|
||||
- name: tls, mismatched dns, failover to standby fails
|
||||
skip: "cluster replication not yet implemented in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: https://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
tls_key: key.pem
|
||||
tls_cert: cert.pem
|
||||
tls_ca: root.pem
|
||||
server_name_dns: ["does-not-match.dolt-instance.dolt-integration-test.example"]
|
||||
- name: key.pem
|
||||
source_path: $TESTGENDIR/rsa_key.pem
|
||||
- name: cert.pem
|
||||
source_path: $TESTGENDIR/rsa_chain.pem
|
||||
- name: root.pem
|
||||
source_path: $TESTGENDIR/rsa_root.pem
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: https://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
tls_key: key.pem
|
||||
tls_cert: cert.pem
|
||||
tls_ca: root.pem
|
||||
- name: key.pem
|
||||
source_path: $TESTGENDIR/rsa_key.pem
|
||||
- name: cert.pem
|
||||
source_path: $TESTGENDIR/rsa_chain.pem
|
||||
- name: root.pem
|
||||
source_path: $TESTGENDIR/rsa_root.pem
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: 'create database repo1'
|
||||
- exec: "use repo1"
|
||||
- query: "select dolt_assume_cluster_role('standby', '11')"
|
||||
error_match: failed to transition from primary to standby gracefully
|
||||
- exec: "create table vals (i int primary key)"
|
||||
- exec: "insert into vals values (0)"
|
||||
- name: tls, mismatched url, failover to standby fails
|
||||
skip: "cluster replication not yet implemented in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: https://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
tls_key: key.pem
|
||||
tls_cert: cert.pem
|
||||
tls_ca: root.pem
|
||||
server_name_urls: ["spiffe://dolt-integration-tests.dev.trust.dolthub.com.example/dolt-instance/does-not-match"]
|
||||
- name: key.pem
|
||||
source_path: $TESTGENDIR/rsa_key.pem
|
||||
- name: cert.pem
|
||||
source_path: $TESTGENDIR/rsa_chain.pem
|
||||
- name: root.pem
|
||||
source_path: $TESTGENDIR/rsa_root.pem
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: https://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
tls_key: key.pem
|
||||
tls_cert: cert.pem
|
||||
tls_ca: root.pem
|
||||
- name: key.pem
|
||||
source_path: $TESTGENDIR/rsa_key.pem
|
||||
- name: cert.pem
|
||||
source_path: $TESTGENDIR/rsa_chain.pem
|
||||
- name: root.pem
|
||||
source_path: $TESTGENDIR/rsa_root.pem
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: 'create database repo1'
|
||||
- exec: "use repo1"
|
||||
- query: "select dolt_assume_cluster_role('standby', '11')"
|
||||
error_match: failed to transition from primary to standby gracefully
|
||||
- exec: "create table vals (i int primary key)"
|
||||
- exec: "insert into vals values (0)"
|
||||
- name: tls, good rsa certs, create new database, primary replicates to standby, fails over, new primary replicates to standby, fails over, new primary has all writes
|
||||
skip: "cluster replication not yet implemented in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: https://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
tls_key: key.pem
|
||||
tls_cert: cert.pem
|
||||
tls_ca: root.pem
|
||||
- name: key.pem
|
||||
source_path: $TESTGENDIR/rsa_key.pem
|
||||
- name: cert.pem
|
||||
source_path: $TESTGENDIR/rsa_chain.pem
|
||||
- name: root.pem
|
||||
source_path: $TESTGENDIR/rsa_root.pem
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: https://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
tls_key: key.pem
|
||||
tls_cert: cert.pem
|
||||
tls_ca: root.pem
|
||||
- name: key.pem
|
||||
source_path: $TESTGENDIR/rsa_key.pem
|
||||
- name: cert.pem
|
||||
source_path: $TESTGENDIR/rsa_chain.pem
|
||||
- name: root.pem
|
||||
source_path: $TESTGENDIR/rsa_root.pem
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: 'create database repo1'
|
||||
- exec: 'use repo1'
|
||||
- exec: 'create table vals (i int primary key)'
|
||||
- exec: 'insert into vals values (0),(1),(2),(3),(4)'
|
||||
- query: "select dolt_assume_cluster_role('standby', 2)"
|
||||
result:
|
||||
columns: ["dolt_assume_cluster_role"]
|
||||
rows: [["{0}"]]
|
||||
- on: server2
|
||||
queries:
|
||||
- exec: 'use repo1'
|
||||
- query: "select count(*) as count from vals"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["5"]]
|
||||
- query: "select dolt_assume_cluster_role('primary', 2)"
|
||||
result:
|
||||
columns: ["dolt_assume_cluster_role"]
|
||||
rows: [["{0}"]]
|
||||
- on: server2
|
||||
queries:
|
||||
- exec: 'use repo1'
|
||||
- exec: 'insert into vals values (5),(6),(7),(8),(9)'
|
||||
- query: "select dolt_assume_cluster_role('standby', 3)"
|
||||
result:
|
||||
columns: ["dolt_assume_cluster_role"]
|
||||
rows: [["{0}"]]
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: 'use repo1'
|
||||
- query: "select count(*) as count from vals"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["10"]]
|
||||
- query: "select dolt_assume_cluster_role('primary', 3)"
|
||||
result:
|
||||
columns: ["dolt_assume_cluster_role"]
|
||||
rows: [["{0}"]]
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: 'use repo1'
|
||||
- exec: 'insert into vals values (10),(11),(12),(13),(14)'
|
||||
- query: "select count(*) as count from vals"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["15"]]
|
||||
- name: tls, good ed25519 certs, create new database, primary replicates to standby, fails over, new primary replicates to standby, fails over, new primary has all writes
|
||||
skip: "cluster replication not yet implemented in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: https://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
tls_key: key.pem
|
||||
tls_cert: cert.pem
|
||||
tls_ca: root.pem
|
||||
server_name_urls: ["spiffe://dolt-integration-tests.dev.trust.dolthub.com.example/dolt-instance"]
|
||||
server_name_dns: ["dolt-instance.dolt-integration-test.example"]
|
||||
- name: key.pem
|
||||
source_path: $TESTGENDIR/ed25519_key.pem
|
||||
- name: cert.pem
|
||||
source_path: $TESTGENDIR/ed25519_chain.pem
|
||||
- name: root.pem
|
||||
source_path: $TESTGENDIR/ed25519_root.pem
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: https://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
tls_key: key.pem
|
||||
tls_cert: cert.pem
|
||||
tls_ca: root.pem
|
||||
server_name_urls: ["spiffe://dolt-integration-tests.dev.trust.dolthub.com.example/dolt-instance"]
|
||||
server_name_dns: ["dolt-instance.dolt-integration-test.example"]
|
||||
- name: key.pem
|
||||
source_path: $TESTGENDIR/ed25519_key.pem
|
||||
- name: cert.pem
|
||||
source_path: $TESTGENDIR/ed25519_chain.pem
|
||||
- name: root.pem
|
||||
source_path: $TESTGENDIR/ed25519_root.pem
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: 'create database repo1'
|
||||
- exec: 'use repo1'
|
||||
- exec: 'create table vals (i int primary key)'
|
||||
- exec: 'insert into vals values (0),(1),(2),(3),(4)'
|
||||
- query: "select dolt_assume_cluster_role('standby', 2)"
|
||||
result:
|
||||
columns: ["dolt_assume_cluster_role"]
|
||||
rows: [["{0}"]]
|
||||
- on: server2
|
||||
queries:
|
||||
- exec: 'use repo1'
|
||||
- query: "select count(*) as count from vals"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["5"]]
|
||||
- query: "select dolt_assume_cluster_role('primary', 2)"
|
||||
result:
|
||||
columns: ["dolt_assume_cluster_role"]
|
||||
rows: [["{0}"]]
|
||||
- on: server2
|
||||
queries:
|
||||
- exec: 'use repo1'
|
||||
- exec: 'insert into vals values (5),(6),(7),(8),(9)'
|
||||
- query: "select dolt_assume_cluster_role('standby', 3)"
|
||||
result:
|
||||
columns: ["dolt_assume_cluster_role"]
|
||||
rows: [["{0}"]]
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: 'use repo1'
|
||||
- query: "select count(*) as count from vals"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["10"]]
|
||||
- query: "select dolt_assume_cluster_role('primary', 3)"
|
||||
result:
|
||||
columns: ["dolt_assume_cluster_role"]
|
||||
rows: [["{0}"]]
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: 'use repo1'
|
||||
- exec: 'insert into vals values (10),(11),(12),(13),(14)'
|
||||
- query: "select count(*) as count from vals"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["15"]]
|
||||
+418
@@ -0,0 +1,418 @@
|
||||
parallel: true
|
||||
tests:
|
||||
- name: users and grants replicate
|
||||
skip: "cluster replication / MySQL grants not yet supported in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: "SET dolt_cluster_ack_writes_timeout_secs = 10"
|
||||
- exec: 'create database repo1'
|
||||
- exec: "use repo1"
|
||||
- exec: 'create table vals (i int primary key)'
|
||||
- exec: 'insert into vals values (0),(1),(2),(3),(4)'
|
||||
- exec: 'create user aaron with password ''aaronspassword'''
|
||||
- exec: 'GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO aaron'
|
||||
- exec: 'insert into vals values (5),(6),(7),(8),(9)'
|
||||
- on: server1
|
||||
user: 'aaron'
|
||||
password: 'aaronspassword'
|
||||
queries:
|
||||
- exec: "use repo1"
|
||||
- exec: 'insert into vals values (10),(11),(12),(13),(14)'
|
||||
- on: server2
|
||||
user: 'aaron'
|
||||
password: 'aaronspassword'
|
||||
queries:
|
||||
- exec: "use repo1"
|
||||
- query: 'select count(*) as count from vals'
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["15"]]
|
||||
- name: branch control replicates
|
||||
skip: "cluster replication / MySQL grants not yet supported in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: "SET dolt_cluster_ack_writes_timeout_secs = 10"
|
||||
- exec: 'create database repo1'
|
||||
- exec: "use repo1"
|
||||
- exec: 'create table vals (i int primary key)'
|
||||
- exec: 'insert into vals values (0),(1),(2),(3),(4)'
|
||||
- exec: 'create user aaron with password ''aaronspassword'''
|
||||
- exec: 'GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO aaron'
|
||||
- exec: 'create user brian with password ''brianpassword'''
|
||||
- exec: 'GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO brian'
|
||||
- exec: 'delete from dolt_branch_control'
|
||||
- exec: 'insert into dolt_branch_control values (''repo1'', ''main'', ''aaron'', ''%'', ''admin'')'
|
||||
- on: server1
|
||||
user: 'aaron'
|
||||
password: 'aaronspassword'
|
||||
queries:
|
||||
- exec: "use repo1"
|
||||
- exec: 'insert into vals values (5),(6),(7),(8),(9)'
|
||||
- exec: 'insert into vals values (10),(11),(12),(13),(14)'
|
||||
# Assert brian cannot write to `main` on server1
|
||||
- on: server1
|
||||
user: 'brian'
|
||||
password: 'brianpassword'
|
||||
queries:
|
||||
- exec: "use repo1"
|
||||
- exec: 'insert into vals values (15),(16),(17),(18),(19)'
|
||||
error_match: 'does not have the correct permissions on branch `main`'
|
||||
# Failover to `server2` so we can assert brian is restricted there as well.
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: "use repo1"
|
||||
- exec: "select dolt_assume_cluster_role('standby', 2)"
|
||||
- on: server2
|
||||
queries:
|
||||
- exec: "use repo1"
|
||||
- exec: "select dolt_assume_cluster_role('primary', 2)"
|
||||
# Assert aaron can write to main on server2 and brian cannot.
|
||||
- on: server2
|
||||
user: 'aaron'
|
||||
password: 'aaronspassword'
|
||||
queries:
|
||||
- exec: "use repo1"
|
||||
- query: 'select count(*) as count from vals'
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["15"]]
|
||||
- exec: 'insert into vals values (20),(21),(22),(23),(24)'
|
||||
- on: server2
|
||||
user: 'brian'
|
||||
password: 'brianpassword'
|
||||
queries:
|
||||
- exec: "use repo1"
|
||||
- query: 'select count(*) as count from vals'
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["20"]]
|
||||
- exec: 'insert into vals values (25),(26),(27),(28),(29)'
|
||||
error_match: 'does not have the correct permissions on branch `main`'
|
||||
# Now we are going to grant brian permission, and fail back over to server1,
|
||||
# and ensure brian can write to the branch there.
|
||||
- on: server2
|
||||
user: 'aaron'
|
||||
password: 'aaronspassword'
|
||||
queries:
|
||||
- exec: "use repo1"
|
||||
- exec: 'insert into dolt_branch_control values (''repo1'', ''main'', ''brian'', ''%'', ''write'')'
|
||||
- exec: 'insert into vals values (30)'
|
||||
- on: server2
|
||||
queries:
|
||||
- exec: "select dolt_assume_cluster_role('standby', 3)"
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: "use repo1"
|
||||
- exec: "select dolt_assume_cluster_role('primary', 3)"
|
||||
# Here brian should be allowed to write now.
|
||||
- on: server1
|
||||
user: 'brian'
|
||||
password: 'brianpassword'
|
||||
queries:
|
||||
- exec: "use repo1"
|
||||
- exec: 'insert into vals values (31)'
|
||||
- name: branch control block on write replication
|
||||
skip: "cluster replication / MySQL grants not yet supported in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
- name: nocluster.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
server:
|
||||
args: ["--config", "nocluster.yaml"]
|
||||
dynamic_port: server2
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: 'create user aaron with password ''aaronspassword'''
|
||||
- exec: 'create database repo1'
|
||||
- exec: 'use repo1'
|
||||
- exec: "SET dolt_cluster_ack_writes_timeout_secs = 2"
|
||||
- exec: 'delete from dolt_branch_control'
|
||||
- query: 'show warnings'
|
||||
result:
|
||||
columns: ["Level", "Code", "Message"]
|
||||
rows: [["Warning", "3024", "Timed out replication of commit to 1 out of 1 replicas."]]
|
||||
- exec: 'insert into dolt_branch_control values (''repo1'', ''main'', ''aaron'', ''%'', ''admin'')'
|
||||
- on: server2
|
||||
restart_server:
|
||||
args: ["--config", "server.yaml"]
|
||||
- on: server1
|
||||
restart_server: {}
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: "SET dolt_cluster_ack_writes_timeout_secs = 10"
|
||||
- exec: 'use repo1'
|
||||
- exec: 'delete from dolt_branch_control'
|
||||
- query: 'show warnings'
|
||||
result:
|
||||
columns: ["Level", "Code", "Message"]
|
||||
rows: []
|
||||
- on: server2
|
||||
queries:
|
||||
- query: 'show warnings'
|
||||
result:
|
||||
columns: ["Level", "Code", "Message"]
|
||||
rows: []
|
||||
- name: users and grants block on write replication
|
||||
skip: "cluster replication / MySQL grants not yet supported in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
- name: nocluster.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
server:
|
||||
args: ["--config", "nocluster.yaml"]
|
||||
dynamic_port: server2
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: "SET dolt_cluster_ack_writes_timeout_secs = 2"
|
||||
- exec: 'create user aaron with password ''aaronspassword'''
|
||||
- query: 'show warnings'
|
||||
result:
|
||||
columns: ["Level", "Code", "Message"]
|
||||
rows: [["Warning", "3024", "Timed out replication of commit to 1 out of 1 replicas."]]
|
||||
- on: server2
|
||||
restart_server:
|
||||
args: ["--config", "server.yaml"]
|
||||
- on: server1
|
||||
restart_server: {}
|
||||
- on: server1
|
||||
queries:
|
||||
# Because we run in parallel, do not be too agressive about the timeout...
|
||||
- exec: "SET dolt_cluster_ack_writes_timeout_secs = 10"
|
||||
- exec: 'create user brian with password ''brianspassword'''
|
||||
- query: 'show warnings'
|
||||
result:
|
||||
columns: ["Level", "Code", "Message"]
|
||||
rows: []
|
||||
- on: server2
|
||||
user: 'brian'
|
||||
password: 'brianspassword'
|
||||
queries:
|
||||
- query: "show warnings"
|
||||
result:
|
||||
columns: ["Level", "Code", "Message"]
|
||||
rows: []
|
||||
- name: users and grants and branch control replicate to multiple standbys
|
||||
skip: "cluster replication / MySQL grants not yet supported in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby1
|
||||
remote_url_template: http://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
- name: standby2
|
||||
remote_url_template: http://localhost:{{get_port "server3_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby1
|
||||
remote_url_template: http://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
- name: standby2
|
||||
remote_url_template: http://localhost:{{get_port "server3_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2
|
||||
- name: server3
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby1
|
||||
remote_url_template: http://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
- name: standby2
|
||||
remote_url_template: http://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server3_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server3
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: "SET dolt_cluster_ack_writes_timeout_secs = 10"
|
||||
- exec: 'create database repo1'
|
||||
- exec: "use repo1"
|
||||
- exec: 'create table vals (i int primary key)'
|
||||
- exec: 'insert into vals values (0),(1),(2),(3),(4)'
|
||||
- exec: 'create user aaron with password ''aaronspassword'''
|
||||
- exec: 'GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO aaron'
|
||||
- exec: 'create user brian with password ''brianpassword'''
|
||||
- exec: 'GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO brian'
|
||||
- exec: 'delete from dolt_branch_control'
|
||||
- exec: 'insert into dolt_branch_control values (''repo1'', ''main'', ''aaron'', ''%'', ''admin'')'
|
||||
- on: server2
|
||||
user: 'aaron'
|
||||
password: 'aaronspassword'
|
||||
queries:
|
||||
- exec: "use repo1"
|
||||
- query: 'select count(*) as count from vals'
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["5"]]
|
||||
- query: 'select count(*) as count from dolt_branch_control'
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["1"]]
|
||||
- on: server3
|
||||
user: 'aaron'
|
||||
password: 'aaronspassword'
|
||||
queries:
|
||||
- exec: "use repo1"
|
||||
- query: 'select count(*) as count from vals'
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["5"]]
|
||||
- query: 'select count(*) as count from dolt_branch_control'
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["1"]]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,132 @@
|
||||
parallel: true
|
||||
tests:
|
||||
- name: dolt_transaction_commit is global variable
|
||||
skip: "dolt_transaction_commit not being respected after being set"
|
||||
repos:
|
||||
- name: repo1
|
||||
server:
|
||||
args: []
|
||||
dynamic_port: repo1
|
||||
connections:
|
||||
- on: repo1
|
||||
queries:
|
||||
- exec: "SET dolt_transaction_commit = 1"
|
||||
- query: "select current_setting('dolt_transaction_commit') as dolt_transaction_commit"
|
||||
result:
|
||||
columns: ["dolt_transaction_commit"]
|
||||
rows: [["1"]]
|
||||
- on: repo1
|
||||
queries:
|
||||
- query: "select count(*) from dolt_log"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["2"]]
|
||||
- exec: "create table tmp (i int)"
|
||||
- query: "select count(*) from dolt_log"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["3"]]
|
||||
- name: set max_connections with yaml config
|
||||
skip: "need to adapt to doltgres listener"
|
||||
repos:
|
||||
- name: repo1
|
||||
with_files:
|
||||
- name: "server.yaml"
|
||||
contents: |
|
||||
log_level: trace
|
||||
listener:
|
||||
max_connections: 999
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: repo1
|
||||
connections:
|
||||
- on: repo1
|
||||
queries:
|
||||
- query: "select current_setting('max_connections') as max_connections"
|
||||
result:
|
||||
columns: ["max_connections"]
|
||||
rows: [["999"]]
|
||||
- name: "dolt_auto_gc_enabled default"
|
||||
skip: "default not set yet"
|
||||
repos:
|
||||
- name: repo1
|
||||
server:
|
||||
args: []
|
||||
dynamic_port: repo1
|
||||
connections:
|
||||
- on: repo1
|
||||
queries:
|
||||
- query: "select current_setting('dolt_auto_gc_enabled') as dolt_auto_gc_enabled"
|
||||
result:
|
||||
columns: ["dolt_auto_gc_enabled"]
|
||||
rows: [["1"]]
|
||||
- exec: "SET dolt_auto_gc_enabled = 0"
|
||||
error_match: "Variable 'dolt_auto_gc_enabled' is a read only variable"
|
||||
- name: "@@global.dolt_auto_gc_enabled true"
|
||||
skip: "config field not supported yet"
|
||||
repos:
|
||||
- name: repo1
|
||||
with_files:
|
||||
- name: "config.yaml"
|
||||
contents: |
|
||||
behavior:
|
||||
auto_gc_behavior:
|
||||
enable: true
|
||||
server:
|
||||
args: ["--config", "config.yaml"]
|
||||
dynamic_port: repo1
|
||||
connections:
|
||||
- on: repo1
|
||||
queries:
|
||||
- query: "select current_setting('dolt_auto_gc_enabled') as dolt_auto_gc_enabled"
|
||||
result:
|
||||
columns: ["dolt_auto_gc_enabled"]
|
||||
rows: [["1"]]
|
||||
- name: "@@global.dolt_auto_gc_enabled false"
|
||||
skip: "config field not supported yet"
|
||||
repos:
|
||||
- name: repo1
|
||||
with_files:
|
||||
- name: "config.yaml"
|
||||
contents: |
|
||||
behavior:
|
||||
auto_gc_behavior:
|
||||
enable: false
|
||||
server:
|
||||
args: ["--config", "config.yaml"]
|
||||
dynamic_port: repo1
|
||||
connections:
|
||||
- on: repo1
|
||||
queries:
|
||||
- query: "select current_setting('dolt_auto_gc_enabled') as dolt_auto_gc_enabled"
|
||||
result:
|
||||
columns: ["dolt_auto_gc_enabled"]
|
||||
rows: [["0"]]
|
||||
- name: secure_file_priv set to /dev/null prevents loading files
|
||||
skip: "field / feature needs to be supported"
|
||||
repos:
|
||||
- name: repo1
|
||||
with_files:
|
||||
- name: "config.yaml"
|
||||
contents: |
|
||||
system_variables:
|
||||
secure_file_priv: "/dev/null"
|
||||
server:
|
||||
args: ["--config", "config.yaml"]
|
||||
dynamic_port: repo1
|
||||
connections:
|
||||
- on: repo1
|
||||
queries:
|
||||
- query: "select LOAD_FILE('config.yaml') as load_file"
|
||||
result:
|
||||
columns: ["load_file"]
|
||||
rows: [["NULL"]]
|
||||
- query: "select LOAD_FILE('/etc/passwd') as load_file"
|
||||
result:
|
||||
columns: ["load_file"]
|
||||
rows: [["NULL"]]
|
||||
- exec: "create table loaded (contents text)"
|
||||
- exec: "COPY loaded FROM 'config.yaml'"
|
||||
error_match: "must be superuser or have privileges of the pg_read_server_files role|secure_file_priv"
|
||||
- exec: "COPY loaded FROM '/config.yaml'"
|
||||
error_match: "must be superuser or have privileges of the pg_read_server_files role|secure_file_priv"
|
||||
@@ -0,0 +1,79 @@
|
||||
parallel: true
|
||||
tests:
|
||||
|
||||
- name: large blob values replicate to standby
|
||||
# Cluster replication is not yet implemented in Doltgres, so this test is
|
||||
# skipped. The cluster config and the large-blob schema/data (LONGBLOB ->
|
||||
# bytea) are translated faithfully so this can be enabled once replication
|
||||
# lands. The ASCII payloads are turned into bytea with convert_to(...,'UTF8'),
|
||||
# which preserves the byte length asserted by LENGTH().
|
||||
skip: "cluster/remotesapi replication not yet implemented in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
listener:
|
||||
host: 0.0.0.0
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
listener:
|
||||
host: 0.0.0.0
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: "SET dolt_cluster_ack_writes_timeout_secs = 10"
|
||||
- exec: 'CREATE DATABASE repo1'
|
||||
- exec: 'USE repo1'
|
||||
- exec: |
|
||||
CREATE TABLE large_blob (
|
||||
id BIGINT PRIMARY KEY,
|
||||
data BYTEA
|
||||
)
|
||||
- exec: "INSERT INTO large_blob VALUES (1, convert_to(REPEAT('abcdefghijklmnopqrst', 1250), 'UTF8'))"
|
||||
- exec: "INSERT INTO large_blob VALUES (2, convert_to(REPEAT('ABCDEFGHIJKLMNOPQRST', 1250), 'UTF8'))"
|
||||
- exec: "INSERT INTO large_blob VALUES (3, convert_to(REPEAT('01234567890123456789', 1250), 'UTF8'))"
|
||||
- exec: "INSERT INTO large_blob VALUES (4, convert_to(REPEAT('zyxwvutsrqponmlkjihg', 1250), 'UTF8'))"
|
||||
- exec: "INSERT INTO large_blob VALUES (5, convert_to(REPEAT('fedcba9876543210FEDC', 1250), 'UTF8'))"
|
||||
- query: "SELECT COUNT(*) AS count FROM large_blob WHERE LENGTH(data) = 25000"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["5"]]
|
||||
- on: server2
|
||||
retry_attempts: 100
|
||||
queries:
|
||||
- exec: 'USE repo1'
|
||||
- query: "SELECT COUNT(*) AS count FROM large_blob"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["5"]]
|
||||
- query: "SELECT COUNT(*) AS count FROM large_blob WHERE LENGTH(data) = 25000"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["5"]]
|
||||
@@ -0,0 +1,87 @@
|
||||
parallel: true
|
||||
tests:
|
||||
|
||||
- name: large json values replicate to standby
|
||||
# Cluster replication is not yet implemented in Doltgres, so this test is
|
||||
# skipped. The cluster config and the large-json schema/data are translated
|
||||
# faithfully (JSON_OBJECT/JSON_ARRAY -> json_build_object/json_build_array,
|
||||
# LENGTH(doc) -> LENGTH(doc::text), doc->>'$.id' -> doc->>'id') so this can be
|
||||
# enabled once replication lands.
|
||||
skip: "cluster/remotesapi replication not yet implemented in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
listener:
|
||||
host: 0.0.0.0
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
listener:
|
||||
host: 0.0.0.0
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: "SET dolt_cluster_ack_writes_timeout_secs = 10"
|
||||
- exec: 'CREATE DATABASE repo1'
|
||||
- exec: 'USE repo1'
|
||||
- exec: |
|
||||
CREATE TABLE large_json (
|
||||
id BIGINT PRIMARY KEY,
|
||||
doc JSON
|
||||
)
|
||||
- exec: "INSERT INTO large_json VALUES (1, json_build_object('id', 1, 'payload', REPEAT('abcde', 2200)))"
|
||||
- exec: "INSERT INTO large_json VALUES (2, json_build_object('id', 2, 'payload', REPEAT('fghij', 2200)))"
|
||||
- exec: "INSERT INTO large_json VALUES (3, json_build_object('id', 3, 'payload', REPEAT('klmno', 2200)))"
|
||||
- exec: "INSERT INTO large_json VALUES (4, json_build_object('id', 4, 'items', json_build_array(REPEAT('x',3700), REPEAT('y',3700), REPEAT('z',3700))))"
|
||||
- exec: "INSERT INTO large_json VALUES (5, json_build_object('id', 5, 'nested', json_build_object('a', REPEAT('p',5500), 'b', REPEAT('q',5500))))"
|
||||
- query: "SELECT COUNT(*) AS count FROM large_json WHERE LENGTH(doc::text) > 11000"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["5"]]
|
||||
- query: "SELECT doc->>'id' AS id FROM large_json WHERE id = 3"
|
||||
result:
|
||||
columns: ["id"]
|
||||
rows: [["3"]]
|
||||
- on: server2
|
||||
retry_attempts: 100
|
||||
queries:
|
||||
- exec: 'USE repo1'
|
||||
- query: "SELECT COUNT(*) AS count FROM large_json"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["5"]]
|
||||
- query: "SELECT COUNT(*) AS count FROM large_json WHERE LENGTH(doc::text) > 11000"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["5"]]
|
||||
- query: "SELECT doc->>'id' AS id FROM large_json WHERE id = 3"
|
||||
result:
|
||||
columns: ["id"]
|
||||
rows: [["3"]]
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
parallel: true
|
||||
tests:
|
||||
|
||||
- name: row with multiple large columns replicates correctly
|
||||
# Cluster replication is not yet implemented in Doltgres, so this test is
|
||||
# skipped. The cluster config and the multi-large-column schema/data are
|
||||
# translated faithfully (LONGTEXT/TEXT -> text, LONGBLOB -> bytea via
|
||||
# convert_to(...,'UTF8'), JSON_OBJECT -> json_build_object, LENGTH(doc) ->
|
||||
# LENGTH(doc::text), doc->>'$.row' -> doc->>'row') so this can be enabled once
|
||||
# replication lands.
|
||||
skip: "cluster/remotesapi replication not yet implemented in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
listener:
|
||||
host: 0.0.0.0
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
listener:
|
||||
host: 0.0.0.0
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: "SET dolt_cluster_ack_writes_timeout_secs = 10"
|
||||
- exec: 'CREATE DATABASE repo1'
|
||||
- exec: 'USE repo1'
|
||||
- exec: |
|
||||
CREATE TABLE multi_large (
|
||||
id BIGINT PRIMARY KEY,
|
||||
txt TEXT,
|
||||
blob_data BYTEA,
|
||||
doc JSON,
|
||||
note TEXT
|
||||
)
|
||||
- exec: |
|
||||
INSERT INTO multi_large VALUES (
|
||||
1,
|
||||
REPEAT('textcol-row1-', 1154),
|
||||
convert_to(REPEAT('blobcol-row1-', 1924), 'UTF8'),
|
||||
json_build_object('row', 1, 'data', REPEAT('jsoncol-row1-', 847)),
|
||||
REPEAT('note-col-row1-', 357)
|
||||
)
|
||||
- exec: |
|
||||
INSERT INTO multi_large VALUES (
|
||||
2,
|
||||
REPEAT('textcol-row2-', 1154),
|
||||
convert_to(REPEAT('blobcol-row2-', 1924), 'UTF8'),
|
||||
json_build_object('row', 2, 'data', REPEAT('jsoncol-row2-', 847)),
|
||||
REPEAT('note-col-row2-', 357)
|
||||
)
|
||||
- exec: |
|
||||
INSERT INTO multi_large VALUES (
|
||||
3,
|
||||
REPEAT('textcol-row3-', 1154),
|
||||
convert_to(REPEAT('blobcol-row3-', 1924), 'UTF8'),
|
||||
json_build_object('row', 3, 'data', REPEAT('jsoncol-row3-', 847)),
|
||||
REPEAT('note-col-row3-', 357)
|
||||
)
|
||||
- query: "SELECT COUNT(*) AS count FROM multi_large WHERE LENGTH(txt) > 10000 AND LENGTH(blob_data) > 20000"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["3"]]
|
||||
- on: server2
|
||||
retry_attempts: 100
|
||||
queries:
|
||||
- exec: 'USE repo1'
|
||||
- query: "SELECT COUNT(*) AS count FROM multi_large"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["3"]]
|
||||
- query: "SELECT COUNT(*) AS count FROM multi_large WHERE LENGTH(txt) > 10000 AND LENGTH(blob_data) > 20000 AND LENGTH(doc::text) > 11000 AND LENGTH(note) > 4000"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["3"]]
|
||||
- query: "SELECT doc->>'row' AS row FROM multi_large WHERE id = 2"
|
||||
result:
|
||||
columns: ["row"]
|
||||
rows: [["2"]]
|
||||
@@ -0,0 +1,78 @@
|
||||
parallel: true
|
||||
tests:
|
||||
|
||||
- name: large text values replicate to standby
|
||||
# Cluster replication is not yet implemented in Doltgres, so this test is
|
||||
# skipped. The cluster config and the large-text schema/data (LONGTEXT ->
|
||||
# text) are translated faithfully so this can be enabled once replication
|
||||
# lands.
|
||||
skip: "cluster/remotesapi replication not yet implemented in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
listener:
|
||||
host: 0.0.0.0
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
listener:
|
||||
host: 0.0.0.0
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: "SET dolt_cluster_ack_writes_timeout_secs = 10"
|
||||
- exec: 'CREATE DATABASE repo1'
|
||||
- exec: 'USE repo1'
|
||||
- exec: |
|
||||
CREATE TABLE large_text (
|
||||
id BIGINT PRIMARY KEY,
|
||||
txt TEXT
|
||||
)
|
||||
- exec: "INSERT INTO large_text VALUES (1, REPEAT('abcdefghij', 1500))"
|
||||
- exec: "INSERT INTO large_text VALUES (2, REPEAT('klmnopqrst', 1500))"
|
||||
- exec: "INSERT INTO large_text VALUES (3, REPEAT('uvwxyz0123', 1500))"
|
||||
- exec: "INSERT INTO large_text VALUES (4, REPEAT('4567890abcd', 1363))"
|
||||
- exec: "INSERT INTO large_text VALUES (5, REPEAT('efghijklmno', 1363))"
|
||||
- query: "SELECT COUNT(*) AS count FROM large_text WHERE LENGTH(txt) > 10000"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["5"]]
|
||||
- on: server2
|
||||
retry_attempts: 100
|
||||
queries:
|
||||
- exec: 'USE repo1'
|
||||
- query: "SELECT COUNT(*) AS count FROM large_text"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["5"]]
|
||||
- query: "SELECT COUNT(*) AS count FROM large_text WHERE LENGTH(txt) > 10000"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["5"]]
|
||||
@@ -0,0 +1,94 @@
|
||||
parallel: true
|
||||
tests:
|
||||
|
||||
- name: large values survive failover to new primary
|
||||
skip: "cluster replication/failover not yet implemented in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
behavior:
|
||||
auto_gc_behavior:
|
||||
enable: false
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
behavior:
|
||||
auto_gc_behavior:
|
||||
enable: false
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: 'SET dolt_cluster_ack_writes_timeout_secs = 10'
|
||||
- exec: 'CREATE DATABASE repo1'
|
||||
- exec: 'USE repo1'
|
||||
- exec: |
|
||||
CREATE TABLE large_vals (
|
||||
id BIGINT PRIMARY KEY,
|
||||
txt TEXT,
|
||||
blob_col BYTEA
|
||||
)
|
||||
- exec: "INSERT INTO large_vals VALUES (1, REPEAT('textrow1-', 1667), REPEAT('blobrow1-', 2778)::bytea)"
|
||||
- exec: "INSERT INTO large_vals VALUES (2, REPEAT('textrow2-', 1667), REPEAT('blobrow2-', 2778)::bytea)"
|
||||
- exec: "INSERT INTO large_vals VALUES (3, REPEAT('textrow3-', 1667), REPEAT('blobrow3-', 2778)::bytea)"
|
||||
- exec: "INSERT INTO large_vals VALUES (4, REPEAT('textrow4-', 1667), REPEAT('blobrow4-', 2778)::bytea)"
|
||||
- exec: "INSERT INTO large_vals VALUES (5, REPEAT('textrow5-', 1667), REPEAT('blobrow5-', 2778)::bytea)"
|
||||
# Transition to standby; server1's epoch advances to 2 on server2.
|
||||
- query: "SELECT dolt_assume_cluster_role('standby', 2)"
|
||||
result:
|
||||
columns: ["dolt_assume_cluster_role"]
|
||||
rows: [["{0}"]]
|
||||
# Verify the standby received all rows, then promote it to primary.
|
||||
- on: server2
|
||||
retry_attempts: 100
|
||||
queries:
|
||||
- exec: 'USE repo1'
|
||||
- query: "SELECT COUNT(*) AS count FROM large_vals WHERE LENGTH(txt) > 10000 AND LENGTH(blob_col) > 20000"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["5"]]
|
||||
- query: "SELECT dolt_assume_cluster_role('primary', 2)"
|
||||
result:
|
||||
columns: ["dolt_assume_cluster_role"]
|
||||
rows: [["{0}"]]
|
||||
# Open a fresh connection to the new primary and confirm large values are intact.
|
||||
# Also insert two additional large rows to prove the new primary is writable.
|
||||
- on: server2
|
||||
queries:
|
||||
- exec: 'USE repo1'
|
||||
- query: "SELECT COUNT(*) AS count FROM large_vals"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["5"]]
|
||||
- exec: "INSERT INTO large_vals VALUES (6, REPEAT('textrow6-', 1667), REPEAT('blobrow6-', 2778)::bytea)"
|
||||
- exec: "INSERT INTO large_vals VALUES (7, REPEAT('textrow7-', 1667), REPEAT('blobrow7-', 2778)::bytea)"
|
||||
- query: "SELECT COUNT(*) AS count FROM large_vals WHERE LENGTH(txt) > 10000 AND LENGTH(blob_col) > 20000"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["7"]]
|
||||
@@ -0,0 +1,95 @@
|
||||
parallel: true
|
||||
tests:
|
||||
|
||||
- name: large values survive gc on primary then replicate to standby
|
||||
skip: "cluster replication/failover not yet implemented in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
behavior:
|
||||
auto_gc_behavior:
|
||||
enable: false
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
behavior:
|
||||
auto_gc_behavior:
|
||||
enable: false
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: 'SET dolt_cluster_ack_writes_timeout_secs = 10'
|
||||
- exec: 'CREATE DATABASE repo1'
|
||||
- exec: 'USE repo1'
|
||||
- exec: |
|
||||
CREATE TABLE large_text (
|
||||
id BIGINT PRIMARY KEY,
|
||||
txt TEXT
|
||||
)
|
||||
- exec: "INSERT INTO large_text VALUES (1, REPEAT('abcdefghij', 1500))"
|
||||
- exec: "INSERT INTO large_text VALUES (2, REPEAT('klmnopqrst', 1500))"
|
||||
- exec: "INSERT INTO large_text VALUES (3, REPEAT('uvwxyz0123', 1500))"
|
||||
- exec: "INSERT INTO large_text VALUES (4, REPEAT('4567890abc', 1500))"
|
||||
- exec: "INSERT INTO large_text VALUES (5, REPEAT('defghijklm', 1500))"
|
||||
- exec: 'SELECT dolt_gc()'
|
||||
- query: 'SELECT COUNT(*) FROM large_text'
|
||||
error_match: "this connection can no longer be used"
|
||||
# After GC the connection is invalidated. Open a new connection to the primary
|
||||
# and wait for the cluster to finish replicating the post-GC state.
|
||||
- on: server1
|
||||
queries:
|
||||
- query: |
|
||||
SELECT "database", standby_remote, role, epoch, replication_lag_millis, current_error
|
||||
FROM dolt_cluster.dolt_cluster_status
|
||||
ORDER BY "database" ASC
|
||||
result:
|
||||
columns: ["database","standby_remote","role","epoch","replication_lag_millis","current_error"]
|
||||
rows:
|
||||
- ["repo1","standby","primary","1","0","NULL"]
|
||||
retry_attempts: 100
|
||||
- exec: 'USE repo1'
|
||||
- query: "SELECT COUNT(*) AS count FROM large_text WHERE LENGTH(txt) = 15000"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["5"]]
|
||||
# Verify the standby received and preserved all large values post-GC, then
|
||||
# confirm it can perform a shallow GC of its own.
|
||||
- on: server2
|
||||
queries:
|
||||
- exec: 'USE repo1'
|
||||
- query: "SELECT COUNT(*) AS count FROM large_text WHERE LENGTH(txt) = 15000"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["5"]]
|
||||
- exec: "SELECT dolt_gc('--shallow')"
|
||||
- query: "SELECT COUNT(*) AS count FROM large_text WHERE LENGTH(txt) = 15000"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["5"]]
|
||||
@@ -0,0 +1,445 @@
|
||||
parallel: true
|
||||
tests:
|
||||
- name: read-only flag prevents modification
|
||||
repos:
|
||||
- name: repo1
|
||||
server:
|
||||
args: ["--config", "readonly.yaml"]
|
||||
dynamic_port: server1
|
||||
with_files:
|
||||
- name: readonly.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
behavior:
|
||||
read_only: true
|
||||
connections:
|
||||
- on: repo1
|
||||
queries:
|
||||
- query: "select count(*) as count from information_schema.tables where table_schema = 'public'"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["0"]]
|
||||
- exec: "create table i_should_not_exist (c0 int)"
|
||||
error_match: "read only mode"
|
||||
- query: "select count(*) as count from information_schema.tables where table_schema = 'public'"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["0"]]
|
||||
- name: read-only flag still allows select
|
||||
repos:
|
||||
- name: repo1
|
||||
server:
|
||||
args: []
|
||||
dynamic_port: server1
|
||||
with_files:
|
||||
- name: readonly.yaml
|
||||
contents: |
|
||||
behavior:
|
||||
read_only: true
|
||||
connections:
|
||||
- on: repo1
|
||||
queries:
|
||||
- exec: "create table t(c0 int)"
|
||||
- exec: "insert into t values (1)"
|
||||
restart_server:
|
||||
args: ["--config", "readonly.yaml"]
|
||||
- on: repo1
|
||||
queries:
|
||||
- query: "select * from t"
|
||||
result:
|
||||
columns: ["c0"]
|
||||
rows: [["1"]]
|
||||
- name: read-only flag prevents dolt_commit
|
||||
skip: "Doltgres read-only mode does not block the dolt_commit() procedure; it is not classified as a write by the engine read-only guard"
|
||||
repos:
|
||||
- name: repo1
|
||||
server:
|
||||
args: ["--config", "readonly.yaml"]
|
||||
dynamic_port: server1
|
||||
with_files:
|
||||
- name: readonly.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
behavior:
|
||||
read_only: true
|
||||
connections:
|
||||
- on: repo1
|
||||
queries:
|
||||
- query: "select dolt_commit('--allow-empty', '-m', 'msg')"
|
||||
error_match: "read only"
|
||||
- name: read-only flag prevents dolt_reset
|
||||
skip: "Doltgres read-only mode does not block the dolt_reset() procedure; it is not classified as a write by the engine read-only guard"
|
||||
repos:
|
||||
- name: repo1
|
||||
server:
|
||||
args: []
|
||||
dynamic_port: server1
|
||||
with_files:
|
||||
- name: readonly.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
behavior:
|
||||
read_only: true
|
||||
connections:
|
||||
- on: repo1
|
||||
queries:
|
||||
- query: "select dolt_commit('--allow-empty', '-m', 'msg')"
|
||||
result:
|
||||
columns: ["dolt_commit"]
|
||||
rows: []
|
||||
restart_server:
|
||||
args: ["--config", "readonly.yaml"]
|
||||
- on: repo1
|
||||
queries:
|
||||
- query: "select dolt_reset('--hard', 'HEAD~1')"
|
||||
error_match: "read only"
|
||||
- name: port in use
|
||||
repos:
|
||||
- name: repo1
|
||||
server:
|
||||
args: []
|
||||
dynamic_port: server1
|
||||
- name: repo2
|
||||
server:
|
||||
args: []
|
||||
dynamic_port: server1
|
||||
error_matches:
|
||||
- "already in use"
|
||||
- name: test basic querying
|
||||
repos:
|
||||
- name: repo1
|
||||
server:
|
||||
args: []
|
||||
dynamic_port: server1
|
||||
connections:
|
||||
- on: repo1
|
||||
queries:
|
||||
- query: "select table_name from information_schema.tables where table_schema = 'public' order by table_name"
|
||||
result:
|
||||
columns: ["table_name"]
|
||||
rows: []
|
||||
- on: repo1
|
||||
queries:
|
||||
- exec: |
|
||||
CREATE TABLE one_pk (
|
||||
pk bigint NOT NULL,
|
||||
c1 bigint,
|
||||
c2 bigint,
|
||||
PRIMARY KEY (pk)
|
||||
)
|
||||
- on: repo1
|
||||
queries:
|
||||
- query: "select table_name from information_schema.tables where table_schema = 'public' order by table_name"
|
||||
result:
|
||||
columns: ["table_name"]
|
||||
rows: [["one_pk"]]
|
||||
- on: repo1
|
||||
queries:
|
||||
- exec: "INSERT INTO one_pk (pk) VALUES (0)"
|
||||
- on: repo1
|
||||
queries:
|
||||
- query: "SELECT * FROM one_pk ORDER BY pk"
|
||||
result:
|
||||
columns: ["pk","c1","c2"]
|
||||
rows: [["0","NULL","NULL"]]
|
||||
- on: repo1
|
||||
queries:
|
||||
- exec: "INSERT INTO one_pk (pk,c1) VALUES (1,1)"
|
||||
- on: repo1
|
||||
queries:
|
||||
- exec: "INSERT INTO one_pk (pk,c1,c2) VALUES (2,2,2),(3,3,3)"
|
||||
- on: repo1
|
||||
queries:
|
||||
- query: "SELECT * FROM one_pk ORDER BY pk"
|
||||
result:
|
||||
columns: ["pk","c1","c2"]
|
||||
rows:
|
||||
- ["0","NULL","NULL"]
|
||||
- ["1","1","NULL"]
|
||||
- ["2","2","2"]
|
||||
- ["3","3","3"]
|
||||
- on: repo1
|
||||
queries:
|
||||
- exec: "UPDATE one_pk SET c2=c1 WHERE c2 is NULL and c1 IS NOT NULL"
|
||||
- name: test multiple queries on same connection
|
||||
repos:
|
||||
- name: repo1
|
||||
server:
|
||||
args: []
|
||||
dynamic_port: server1
|
||||
connections:
|
||||
- on: repo1
|
||||
queries:
|
||||
- exec: |
|
||||
CREATE TABLE one_pk (
|
||||
pk bigint NOT NULL,
|
||||
c1 bigint,
|
||||
c2 bigint,
|
||||
PRIMARY KEY (pk)
|
||||
)
|
||||
- exec: "INSERT INTO one_pk (pk) VALUES (0)"
|
||||
- exec: "INSERT INTO one_pk (pk,c1) VALUES (1,1)"
|
||||
- exec: "INSERT INTO one_pk (pk,c1,c2) VALUES (2,2,2),(3,3,3)"
|
||||
- query: "SELECT * FROM one_pk ORDER BY pk"
|
||||
result:
|
||||
columns: ["pk","c1","c2"]
|
||||
rows:
|
||||
- ["0","NULL","NULL"]
|
||||
- ["1","1","NULL"]
|
||||
- ["2","2","2"]
|
||||
- ["3","3","3"]
|
||||
- on: repo1
|
||||
queries:
|
||||
- query: "SELECT * FROM one_pk ORDER BY pk"
|
||||
result:
|
||||
columns: ["pk","c1","c2"]
|
||||
rows:
|
||||
- ["0","NULL","NULL"]
|
||||
- ["1","1","NULL"]
|
||||
- ["2","2","2"]
|
||||
- ["3","3","3"]
|
||||
- name: test CREATE and DROP database via sql-server
|
||||
# The original test creates a database, then `USE`s it to operate on its
|
||||
# tables on the same connection. Postgres/Doltgres binds the database at
|
||||
# connection time (no USE across databases), and this test framework can only
|
||||
# open connections to servers registered by repo/multi-repo name (via `on:`),
|
||||
# so a freshly-created database cannot be reached by name. We still exercise
|
||||
# CREATE DATABASE / DROP DATABASE and verify pg_database visibility.
|
||||
repos:
|
||||
- name: repo1
|
||||
server:
|
||||
args: []
|
||||
dynamic_port: server1
|
||||
connections:
|
||||
- on: repo1
|
||||
queries:
|
||||
- exec: "CREATE DATABASE test"
|
||||
- query: "select datname from pg_database where datname in ('repo1','test') order by datname"
|
||||
result:
|
||||
columns: ["datname"]
|
||||
rows:
|
||||
- ["repo1"]
|
||||
- ["test"]
|
||||
- exec: "drop database test"
|
||||
- query: "select datname from pg_database where datname in ('repo1','test') order by datname"
|
||||
result:
|
||||
columns: ["datname"]
|
||||
rows:
|
||||
- ["repo1"]
|
||||
- name: COPY loads CSV data from a file
|
||||
repos:
|
||||
- name: repo1
|
||||
with_files:
|
||||
- name: "1pk5col-ints.csv"
|
||||
source_path: "testdata/1pk5col-ints.csv"
|
||||
server:
|
||||
args: []
|
||||
dynamic_port: server1
|
||||
connections:
|
||||
- on: repo1
|
||||
queries:
|
||||
- exec: "CREATE TABLE test(pk int primary key, c1 int, c2 int, c3 int, c4 int, c5 int)"
|
||||
- exec: "COPY test FROM '1pk5col-ints.csv' WITH (FORMAT csv, HEADER)"
|
||||
- query: "SELECT * FROM test"
|
||||
result:
|
||||
columns: ["pk","c1","c2","c3","c4","c5"]
|
||||
rows:
|
||||
- ["0","1","2","3","4","5"]
|
||||
- ["1","1","2","3","4","5"]
|
||||
- name: JSON queries
|
||||
repos:
|
||||
- name: repo1
|
||||
server:
|
||||
args: []
|
||||
dynamic_port: server1
|
||||
connections:
|
||||
- on: repo1
|
||||
queries:
|
||||
- exec: |
|
||||
CREATE TABLE js_test (
|
||||
pk int NOT NULL,
|
||||
js json,
|
||||
PRIMARY KEY (pk)
|
||||
)
|
||||
- exec: |
|
||||
INSERT INTO js_test VALUES (1, '{"a":1}')
|
||||
- query: "SELECT * FROM js_test"
|
||||
result:
|
||||
columns: ["pk","js"]
|
||||
rows:
|
||||
or:
|
||||
- [["1", '{"a": 1}']]
|
||||
- [["1", '{"a":1}']]
|
||||
- name: select a branch with dolt_checkout
|
||||
# The original test used MySQL `USE repo1/feature-branch` to switch to a
|
||||
# branch-revision database. Doltgres has no `USE` statement, so the equivalent
|
||||
# session branch switch is done with dolt_checkout().
|
||||
repos:
|
||||
- name: repo1
|
||||
server:
|
||||
args: []
|
||||
dynamic_port: server1
|
||||
connections:
|
||||
- on: repo1
|
||||
queries:
|
||||
- query: "select dolt_branch('feature-branch')"
|
||||
result:
|
||||
columns: ["dolt_branch"]
|
||||
rows: [["{0}"]]
|
||||
- query: "select dolt_checkout('feature-branch')"
|
||||
result:
|
||||
columns: ["dolt_checkout"]
|
||||
rows:
|
||||
or:
|
||||
- [["{0,\"Switched to branch 'feature-branch'\"}"]]
|
||||
- [["{0,Switched to branch 'feature-branch'}"]]
|
||||
- exec: |
|
||||
CREATE TABLE test (
|
||||
pk int,
|
||||
c1 int,
|
||||
PRIMARY KEY (pk)
|
||||
)
|
||||
- on: repo1
|
||||
queries:
|
||||
- query: "select table_name from information_schema.tables where table_schema = 'public' order by table_name"
|
||||
result:
|
||||
columns: ["table_name"]
|
||||
rows: []
|
||||
- query: "select dolt_checkout('feature-branch')"
|
||||
result:
|
||||
columns: ["dolt_checkout"]
|
||||
rows:
|
||||
or:
|
||||
- [["{0,\"Switched to branch 'feature-branch'\"}"]]
|
||||
- [["{0,Switched to branch 'feature-branch'}"]]
|
||||
- query: "select table_name from information_schema.tables where table_schema = 'public' order by table_name"
|
||||
result:
|
||||
columns: ["table_name"]
|
||||
rows: [["test"]]
|
||||
- name: auto increment for a table should reset between drops
|
||||
repos:
|
||||
- name: repo1
|
||||
server:
|
||||
args: []
|
||||
dynamic_port: server1
|
||||
connections:
|
||||
- on: repo1
|
||||
queries:
|
||||
- exec: "CREATE TABLE t1(pk serial primary key, val int)"
|
||||
- exec: "INSERT INTO t1 (val) VALUES (1),(2)"
|
||||
- query: "SELECT * FROM t1 ORDER BY pk"
|
||||
result:
|
||||
columns: ["pk", "val"]
|
||||
rows: [["1", "1"], ["2", "2"]]
|
||||
- exec: "DROP TABLE t1"
|
||||
- exec: "CREATE TABLE t1(pk serial primary key, val int)"
|
||||
- exec: "INSERT INTO t1 (val) VALUES (1),(2)"
|
||||
- query: "SELECT * FROM t1 ORDER BY pk"
|
||||
result:
|
||||
columns: ["pk", "val"]
|
||||
rows: [["1", "1"], ["2", "2"]]
|
||||
- name: Create a temporary table and validate that it doesn't persist after a session closes
|
||||
repos:
|
||||
- name: repo1
|
||||
server:
|
||||
args: []
|
||||
dynamic_port: server1
|
||||
connections:
|
||||
- on: repo1
|
||||
queries:
|
||||
- query: "select table_name from information_schema.tables where table_schema = 'public' order by table_name"
|
||||
result:
|
||||
columns: ["table_name"]
|
||||
rows: []
|
||||
- exec: "CREATE TEMPORARY TABLE t1(pk int primary key, val int)"
|
||||
- exec: "INSERT INTO t1 VALUES (1, 1),(2, 2)"
|
||||
- on: repo1
|
||||
queries:
|
||||
- exec: "INSERT INTO t1 VALUES (1, 1),(2, 2)"
|
||||
error_match: "(does not exist|not found)"
|
||||
- name: create a collated database connected to a non-dolt database
|
||||
skip: "MySQL-specific COLLATE database creation / non-Dolt database semantics not applicable to Doltgres"
|
||||
repos:
|
||||
- name: repo1
|
||||
server:
|
||||
args: []
|
||||
dynamic_port: server1
|
||||
connections:
|
||||
- on: repo1
|
||||
queries:
|
||||
- exec: "USE mysql"
|
||||
- exec: "CREATE DATABASE testdb COLLATE utf8mb4_unicode_ci"
|
||||
- exec: "USE testdb"
|
||||
- name: dolt_gc succeeds as first write on existing database without a journal after chunk journal is enabled
|
||||
skip: "Requires the database to be initialized with the chunk journal disabled; the doltgres test harness initializes databases with the journal enabled, so this storage-internals scenario cannot be reproduced yet"
|
||||
# The original test created the database via CREATE DATABASE on a multi-repo
|
||||
# server, then USE'd it. Doltgres binds the database at connect time and this
|
||||
# framework can only connect to databases registered by repo name (via `on:`),
|
||||
# so the database is created up front as a repo and connected to directly. The
|
||||
# server starts with the chunk journal disabled, data is written, then the
|
||||
# server is restarted (re-enabling the chunk journal) and dolt_gc is run as the
|
||||
# first write.
|
||||
repos:
|
||||
- name: mydb
|
||||
server:
|
||||
envs: ["DOLT_DISABLE_CHUNK_JOURNAL=true"]
|
||||
args: []
|
||||
dynamic_port: server1
|
||||
connections:
|
||||
- on: mydb
|
||||
queries:
|
||||
- exec: "CREATE TABLE vals (id int primary key, val int)"
|
||||
- exec: "INSERT INTO vals VALUES (1, 1),(2, 2)"
|
||||
restart_server:
|
||||
envs: []
|
||||
- on: mydb
|
||||
queries:
|
||||
- query: "select dolt_gc() as dolt_gc"
|
||||
result:
|
||||
columns: ["dolt_gc"]
|
||||
rows: [["{0}"]]
|
||||
- name: file remotes update appropriately
|
||||
skip: "remotesapi/file-remote replication across servers not yet supported in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
server:
|
||||
args: []
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
server:
|
||||
args: []
|
||||
dynamic_port: server2
|
||||
- name: server3
|
||||
server:
|
||||
args: []
|
||||
dynamic_port: server3
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: "CREATE DATABASE mydb"
|
||||
- exec: "select dolt_remote('add', 'origin', 'file://{{get_tempdir \"remote_storage\"}}')"
|
||||
- exec: "select dolt_push('origin', 'main:main')"
|
||||
- on: server2
|
||||
queries:
|
||||
- exec: "select dolt_clone('file://{{get_tempdir \"remote_storage\"}}', 'mydb')"
|
||||
- on: server3
|
||||
queries:
|
||||
- exec: "select dolt_clone('file://{{get_tempdir \"remote_storage\"}}', 'mydb')"
|
||||
- on: server2
|
||||
queries:
|
||||
- exec: "CREATE TABLE test (id int PRIMARY KEY)"
|
||||
- exec: "select dolt_commit('-Am', 'create test table')"
|
||||
- exec: "select dolt_push('origin', 'main')"
|
||||
- on: server3
|
||||
queries:
|
||||
- exec: "select dolt_pull('origin', 'main')"
|
||||
- exec: "INSERT INTO test VALUES (1), (2), (47)"
|
||||
- exec: "select dolt_commit('-am', 'Insert some initial values.')"
|
||||
- exec: "select dolt_push('origin', 'main')"
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: "select dolt_pull('origin', 'main')"
|
||||
- query: "SELECT * FROM test ORDER BY id ASC"
|
||||
result:
|
||||
columns: ["id"]
|
||||
rows: [["1"], ["2"], ["47"]]
|
||||
@@ -0,0 +1,100 @@
|
||||
parallel: true
|
||||
tests:
|
||||
- name: can clone from server1 remotesapi endpoint
|
||||
# The remotes API (used to serve/clone databases between servers) is not yet
|
||||
# implemented in Doltgres, so this test is skipped. The config, remotesapi
|
||||
# port wiring, and dolt_clone/dolt_commit flow are translated faithfully so it
|
||||
# can be enabled once the feature lands.
|
||||
skip: "cluster/remotesapi replication not yet implemented in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
repos:
|
||||
- name: repo1
|
||||
- name: repo2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
remotesapi:
|
||||
port: {{get_port "remotesapi_port"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server_port
|
||||
- name: server2
|
||||
repos:
|
||||
- name: repo1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
listener:
|
||||
host: 0.0.0.0
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2_port
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: "use repo2"
|
||||
- exec: "create table vals (id int primary key)"
|
||||
- exec: "insert into vals values (0),(1),(2),(3),(4)"
|
||||
- exec: "select dolt_commit('-Am', 'insert some data')"
|
||||
- on: server2
|
||||
queries:
|
||||
- exec: "use repo1"
|
||||
- exec: "select dolt_clone('http://localhost:{{get_port \"remotesapi_port\"}}/repo2')"
|
||||
- exec: "use repo2"
|
||||
- query: "select count(*) from vals"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["5"]]
|
||||
- name: can clone from server1 remotesapi endpoint after a gc
|
||||
skip: "cluster/remotesapi replication not yet implemented in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
repos:
|
||||
- name: repo1
|
||||
- name: repo2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
remotesapi:
|
||||
port: {{get_port "remotesapi_port"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server_port
|
||||
- name: server2
|
||||
repos:
|
||||
- name: repo1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
listener:
|
||||
host: 0.0.0.0
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2_port
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: "use repo2"
|
||||
- exec: "create table vals (id int primary key)"
|
||||
- exec: "insert into vals values (0),(1),(2),(3),(4)"
|
||||
- exec: "select dolt_commit('-Am', 'insert some data')"
|
||||
- exec: "select dolt_gc()"
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: "use repo2"
|
||||
- exec: "insert into vals values (5),(6),(7),(8),(9)"
|
||||
- exec: "select dolt_commit('-Am', 'insert some more data')"
|
||||
- on: server2
|
||||
queries:
|
||||
- exec: "use repo1"
|
||||
- exec: "select dolt_clone('http://localhost:{{get_port \"remotesapi_port\"}}/repo2')"
|
||||
- exec: "use repo2"
|
||||
- query: "select count(*) from vals"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["10"]]
|
||||
@@ -0,0 +1,80 @@
|
||||
parallel: true
|
||||
tests:
|
||||
- name: require_secure_transport no key or cert
|
||||
repos:
|
||||
- name: repo1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
listener:
|
||||
require_secure_transport: true
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server
|
||||
error_matches:
|
||||
- "require_secure_transport can only be `true` when a tls_key and tls_cert are provided."
|
||||
- name: tls_key non-existent
|
||||
repos:
|
||||
- name: repo1
|
||||
with_files:
|
||||
- name: chain_key.pem
|
||||
source_path: $TESTGENDIR/rsa_key.pem
|
||||
- name: chain_cert.pem
|
||||
source_path: $TESTGENDIR/rsa_chain.pem
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
listener:
|
||||
tls_key: doesnotexist_key.pem
|
||||
tls_cert: chain_cert.pem
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server
|
||||
error_matches:
|
||||
- "no such file or directory"
|
||||
- name: tls_cert non-existent
|
||||
repos:
|
||||
- name: repo1
|
||||
with_files:
|
||||
- name: chain_key.pem
|
||||
source_path: $TESTGENDIR/rsa_key.pem
|
||||
- name: chain_cert.pem
|
||||
source_path: $TESTGENDIR/rsa_chain.pem
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
listener:
|
||||
tls_key: chain_key.pem
|
||||
tls_cert: doesnotexist_key.pem
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server
|
||||
error_matches:
|
||||
- "no such file or directory"
|
||||
|
||||
# XXX: It would be nice to assert on the TLS use here using a status query or similar.
|
||||
# It would be nice to assert on failing to connect using sslmode=disable.
|
||||
- name: tls only server
|
||||
repos:
|
||||
- name: repo1
|
||||
with_files:
|
||||
- name: chain_key.pem
|
||||
source_path: $TESTGENDIR/rsa_key.pem
|
||||
- name: chain_cert.pem
|
||||
source_path: $TESTGENDIR/rsa_chain.pem
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
listener:
|
||||
tls_key: chain_key.pem
|
||||
tls_cert: chain_cert.pem
|
||||
require_secure_transport: true
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server
|
||||
connections:
|
||||
- on: repo1
|
||||
driver_params:
|
||||
sslmode: require
|
||||
queries:
|
||||
- query: "select 1 as one"
|
||||
result:
|
||||
columns: ["one"]
|
||||
rows: [["1"]]
|
||||
@@ -0,0 +1,184 @@
|
||||
parallel: true
|
||||
tests:
|
||||
|
||||
- name: type diverse table replicates correctly
|
||||
# Cluster replication is not yet implemented in Doltgres, so this test is
|
||||
# skipped. The cluster config and the type-diverse schema/data are translated
|
||||
# to Postgres faithfully so this can be enabled once replication lands.
|
||||
#
|
||||
# Type translation notes (MySQL -> Postgres):
|
||||
# TINYINT -> smallint (Postgres has no 1-byte int)
|
||||
# DOUBLE -> double precision
|
||||
# DATETIME -> timestamp
|
||||
# BLOB -> bytea
|
||||
# BOOLEAN -> boolean
|
||||
# JSON -> json
|
||||
# ENUM(...) / SET(...) -> varchar (Postgres has no inline ENUM/SET); the
|
||||
# spirit (constrained categorical / multi-value column) is preserved as
|
||||
# text values.
|
||||
skip: "cluster/remotesapi replication not yet implemented in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
listener:
|
||||
host: 0.0.0.0
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
listener:
|
||||
host: 0.0.0.0
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: "SET dolt_cluster_ack_writes_timeout_secs = 10"
|
||||
- exec: 'CREATE DATABASE repo1'
|
||||
- exec: 'USE repo1'
|
||||
- exec: |
|
||||
CREATE TABLE type_diverse (
|
||||
id BIGINT PRIMARY KEY,
|
||||
col_tinyint SMALLINT,
|
||||
col_smallint SMALLINT,
|
||||
col_int INT,
|
||||
col_bigint BIGINT,
|
||||
col_decimal DECIMAL(20,10),
|
||||
col_float REAL,
|
||||
col_double DOUBLE PRECISION,
|
||||
col_char CHAR(50),
|
||||
col_varchar VARCHAR(500),
|
||||
col_text TEXT,
|
||||
col_blob BYTEA,
|
||||
col_date DATE,
|
||||
col_time TIME,
|
||||
col_datetime TIMESTAMP,
|
||||
col_bool BOOLEAN,
|
||||
col_json JSON,
|
||||
col_enum VARCHAR(16),
|
||||
col_set VARCHAR(64)
|
||||
)
|
||||
# Row 1: typical values.
|
||||
- exec: |
|
||||
INSERT INTO type_diverse VALUES (
|
||||
1, 42, 1000, 1000000, 9000000000,
|
||||
12345678.9012345678, 3.14, 2.71828182845905,
|
||||
'hello', 'longer varchar value',
|
||||
'some text content', '\x736f6d6520626c6f6220636f6e74656e74',
|
||||
'2024-06-15', '12:30:00', '2024-06-15 12:30:00',
|
||||
true, json_build_object('key','value','count',42),
|
||||
'alpha', 'red,green'
|
||||
)
|
||||
# Row 2: minimum boundary values.
|
||||
- exec: |
|
||||
INSERT INTO type_diverse VALUES (
|
||||
2, -128, -32768, -2147483648, -9223372036854775808,
|
||||
-9999999999.9999999999, -1.5, -1.23456789,
|
||||
'', '',
|
||||
'', '\x',
|
||||
'1000-01-01', '00:00:00', '1000-01-01 00:00:00',
|
||||
false, json_build_object(),
|
||||
'beta', 'green'
|
||||
)
|
||||
# Row 3: maximum/large values - large TEXT and BLOB cross the out-of-band threshold.
|
||||
- exec: |
|
||||
INSERT INTO type_diverse VALUES (
|
||||
3, 127, 32767, 2147483647, 9223372036854775807,
|
||||
9999999999.9999999999, 1.5, 9.87654321,
|
||||
REPEAT('x',50), REPEAT('y',500),
|
||||
REPEAT('z',15000), decode(REPEAT('62',20000), 'hex'),
|
||||
'9999-12-31', '23:59:59', '9999-12-31 23:59:59',
|
||||
true, json_build_object('data', REPEAT('d',11000)),
|
||||
'gamma', 'red,green,blue'
|
||||
)
|
||||
# Row 4: all nullable columns are NULL.
|
||||
- exec: 'INSERT INTO type_diverse (id) VALUES (4)'
|
||||
- query: "SELECT COUNT(*) AS count FROM type_diverse"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["4"]]
|
||||
- query: "SELECT COUNT(*) AS count FROM type_diverse WHERE col_int = -2147483648 AND col_bigint = -9223372036854775808"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["1"]]
|
||||
- query: "SELECT COUNT(*) AS count FROM type_diverse WHERE LENGTH(col_text) = 15000 AND LENGTH(col_blob) = 20000"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["1"]]
|
||||
- query: "SELECT COUNT(*) AS count FROM type_diverse WHERE col_tinyint IS NULL AND col_date IS NULL AND col_json IS NULL"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["1"]]
|
||||
- on: server2
|
||||
retry_attempts: 100
|
||||
queries:
|
||||
- exec: 'USE repo1'
|
||||
- query: "SELECT COUNT(*) AS count FROM type_diverse"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["4"]]
|
||||
# Integer boundaries must survive the replication round-trip.
|
||||
- query: "SELECT COUNT(*) AS count FROM type_diverse WHERE col_tinyint = -128 AND col_int = -2147483648 AND col_bigint = -9223372036854775808"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["1"]]
|
||||
- query: "SELECT COUNT(*) AS count FROM type_diverse WHERE col_tinyint = 127 AND col_int = 2147483647 AND col_bigint = 9223372036854775807"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["1"]]
|
||||
# Large out-of-band values must be present with their correct sizes.
|
||||
- query: "SELECT COUNT(*) AS count FROM type_diverse WHERE LENGTH(col_text) = 15000 AND LENGTH(col_blob) = 20000"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["1"]]
|
||||
# Date and time boundary values must be preserved.
|
||||
- query: "SELECT COUNT(*) AS count FROM type_diverse WHERE col_date = '1000-01-01' AND col_time = '00:00:00'"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["1"]]
|
||||
- query: "SELECT COUNT(*) AS count FROM type_diverse WHERE col_date = '9999-12-31' AND col_time = '23:59:59'"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["1"]]
|
||||
# ENUM/SET (translated to varchar) must replicate correctly.
|
||||
- query: "SELECT col_enum, col_set FROM type_diverse WHERE id = 1"
|
||||
result:
|
||||
columns: ["col_enum","col_set"]
|
||||
rows: [["alpha","red,green"]]
|
||||
- query: "SELECT col_enum, col_set FROM type_diverse WHERE id = 3"
|
||||
result:
|
||||
columns: ["col_enum","col_set"]
|
||||
rows: [["gamma","red,green,blue"]]
|
||||
- query: "SELECT col_json->>'key' AS key FROM type_diverse WHERE id = 1"
|
||||
result:
|
||||
columns: ["key"]
|
||||
rows: [["value"]]
|
||||
# NULL values must survive replication.
|
||||
- query: "SELECT COUNT(*) AS count FROM type_diverse WHERE col_tinyint IS NULL AND col_date IS NULL AND col_json IS NULL"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["1"]]
|
||||
@@ -0,0 +1,99 @@
|
||||
parallel: true
|
||||
tests:
|
||||
|
||||
- name: wide integer column table replicates correctly
|
||||
skip: "cluster replication/failover not yet implemented in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
behavior:
|
||||
auto_gc_behavior:
|
||||
enable: false
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
behavior:
|
||||
auto_gc_behavior:
|
||||
enable: false
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: 'SET dolt_cluster_ack_writes_timeout_secs = 10'
|
||||
- exec: 'CREATE DATABASE repo1'
|
||||
- exec: 'USE repo1'
|
||||
- exec: |
|
||||
CREATE TABLE wide_int (
|
||||
id BIGINT PRIMARY KEY,
|
||||
c00 BIGINT, c01 BIGINT, c02 BIGINT, c03 BIGINT, c04 BIGINT,
|
||||
c05 BIGINT, c06 BIGINT, c07 BIGINT, c08 BIGINT, c09 BIGINT,
|
||||
c10 BIGINT, c11 BIGINT, c12 BIGINT, c13 BIGINT, c14 BIGINT,
|
||||
c15 BIGINT, c16 BIGINT, c17 BIGINT, c18 BIGINT, c19 BIGINT,
|
||||
c20 BIGINT, c21 BIGINT, c22 BIGINT, c23 BIGINT, c24 BIGINT,
|
||||
c25 BIGINT, c26 BIGINT, c27 BIGINT, c28 BIGINT, c29 BIGINT
|
||||
)
|
||||
- exec: |
|
||||
INSERT INTO wide_int VALUES
|
||||
(1, 100,101,102,103,104, 105,106,107,108,109, 110,111,112,113,114, 115,116,117,118,119, 120,121,122,123,124, 125,126,127,128,129),
|
||||
(2, 200,201,202,203,204, 205,206,207,208,209, 210,211,212,213,214, 215,216,217,218,219, 220,221,222,223,224, 225,226,227,228,229),
|
||||
(3, 300,301,302,303,304, 305,306,307,308,309, 310,311,312,313,314, 315,316,317,318,319, 320,321,322,323,324, 325,326,327,328,329),
|
||||
(4, -100,-101,-102,-103,-104,-105,-106,-107,-108,-109,-110,-111,-112,-113,-114,-115,-116,-117,-118,-119,-120,-121,-122,-123,-124,-125,-126,-127,-128,-129),
|
||||
(5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
|
||||
- query: "SELECT COUNT(*) AS count FROM wide_int"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["5"]]
|
||||
- query: "SELECT c00, c15, c29 FROM wide_int WHERE id = 1"
|
||||
result:
|
||||
columns: ["c00","c15","c29"]
|
||||
rows: [["100","115","129"]]
|
||||
- on: server2
|
||||
retry_attempts: 100
|
||||
queries:
|
||||
- exec: 'USE repo1'
|
||||
- query: "SELECT COUNT(*) AS count FROM wide_int"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["5"]]
|
||||
- query: "SELECT c00, c15, c29 FROM wide_int WHERE id = 1"
|
||||
result:
|
||||
columns: ["c00","c15","c29"]
|
||||
rows: [["100","115","129"]]
|
||||
- query: "SELECT c00, c15, c29 FROM wide_int WHERE id = 3"
|
||||
result:
|
||||
columns: ["c00","c15","c29"]
|
||||
rows: [["300","315","329"]]
|
||||
- query: "SELECT c00, c15, c29 FROM wide_int WHERE id = 4"
|
||||
result:
|
||||
columns: ["c00","c15","c29"]
|
||||
rows: [["-100","-115","-129"]]
|
||||
- query: "SELECT COUNT(*) AS count FROM wide_int WHERE c00 = 0 AND c29 = 0"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["1"]]
|
||||
@@ -0,0 +1,125 @@
|
||||
parallel: true
|
||||
tests:
|
||||
|
||||
- name: wide table survives failover
|
||||
skip: "cluster replication/failover not yet implemented in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
behavior:
|
||||
auto_gc_behavior:
|
||||
enable: false
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
behavior:
|
||||
auto_gc_behavior:
|
||||
enable: false
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: 'SET dolt_cluster_ack_writes_timeout_secs = 10'
|
||||
- exec: 'CREATE DATABASE repo1'
|
||||
- exec: 'USE repo1'
|
||||
- exec: |
|
||||
CREATE TABLE wide_mixed (
|
||||
id BIGINT PRIMARY KEY,
|
||||
i00 INT, i01 INT, i02 INT, i03 INT, i04 INT,
|
||||
i05 INT, i06 INT, i07 INT, i08 INT, i09 INT,
|
||||
t00 TEXT, t01 TEXT, t02 TEXT, t03 TEXT, t04 TEXT,
|
||||
v00 VARCHAR(300), v01 VARCHAR(300), v02 VARCHAR(300), v03 VARCHAR(300), v04 VARCHAR(300)
|
||||
)
|
||||
- exec: |
|
||||
INSERT INTO wide_mixed VALUES
|
||||
(1,
|
||||
1,2,3,4,5,6,7,8,9,10,
|
||||
REPEAT('t00',667), REPEAT('t01',667), REPEAT('t02',667), REPEAT('t03',667), REPEAT('t04',667),
|
||||
REPEAT('v',300), REPEAT('w',300), REPEAT('x',300), REPEAT('y',300), REPEAT('z',300))
|
||||
- exec: |
|
||||
INSERT INTO wide_mixed VALUES
|
||||
(2,
|
||||
11,12,13,14,15,16,17,18,19,20,
|
||||
REPEAT('T00',667), REPEAT('T01',667), REPEAT('T02',667), REPEAT('T03',667), REPEAT('T04',667),
|
||||
REPEAT('V',300), REPEAT('W',300), REPEAT('X',300), REPEAT('Y',300), REPEAT('Z',300))
|
||||
- exec: |
|
||||
INSERT INTO wide_mixed VALUES
|
||||
(3,
|
||||
-1,-2,-3,-4,-5,-6,-7,-8,-9,-10,
|
||||
REPEAT('neg',667), REPEAT('zero',501), REPEAT('mix',668), REPEAT('data',501), REPEAT('test',501),
|
||||
REPEAT('a',300), REPEAT('b',300), REPEAT('c',300), REPEAT('d',300), REPEAT('e',300))
|
||||
- query: "SELECT COUNT(*) AS count FROM wide_mixed"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["3"]]
|
||||
- query: "SELECT i00, i09, LEFT(t00,3) AS t00, LEFT(v04,1) AS v04 FROM wide_mixed WHERE id = 1"
|
||||
result:
|
||||
columns: ["i00","i09","t00","v04"]
|
||||
rows: [["1","10","t00","z"]]
|
||||
- query: "SELECT dolt_assume_cluster_role('standby', 2)"
|
||||
result:
|
||||
columns: ["dolt_assume_cluster_role"]
|
||||
rows: [["{0}"]]
|
||||
- on: server2
|
||||
retry_attempts: 100
|
||||
queries:
|
||||
- exec: 'USE repo1'
|
||||
- query: "SELECT COUNT(*) AS count FROM wide_mixed"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["3"]]
|
||||
- query: "SELECT i00, i09, LEFT(t00,3) AS t00, LEFT(v04,1) AS v04 FROM wide_mixed WHERE id = 2"
|
||||
result:
|
||||
columns: ["i00","i09","t00","v04"]
|
||||
rows: [["11","20","T00","Z"]]
|
||||
- query: "SELECT dolt_assume_cluster_role('primary', 2)"
|
||||
result:
|
||||
columns: ["dolt_assume_cluster_role"]
|
||||
rows: [["{0}"]]
|
||||
- on: server2
|
||||
queries:
|
||||
- exec: 'USE repo1'
|
||||
- exec: |
|
||||
INSERT INTO wide_mixed VALUES
|
||||
(4,
|
||||
100,200,300,400,500,600,700,800,900,1000,
|
||||
REPEAT('new',667), REPEAT('row',667), REPEAT('ins',667), REPEAT('ert',667), REPEAT('ed!',668),
|
||||
REPEAT('N',300), REPEAT('E',300), REPEAT('W',300), REPEAT('R',300), REPEAT('O',300))
|
||||
- query: "SELECT COUNT(*) AS count FROM wide_mixed"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["4"]]
|
||||
- query: "SELECT i00, LEFT(t00,3) AS t00 FROM wide_mixed WHERE id = 4"
|
||||
result:
|
||||
columns: ["i00","t00"]
|
||||
rows: [["100","new"]]
|
||||
- query: "SELECT COUNT(*) AS count FROM wide_mixed WHERE LENGTH(t00) > 1000"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["4"]]
|
||||
@@ -0,0 +1,105 @@
|
||||
parallel: true
|
||||
tests:
|
||||
|
||||
- name: wide text column table replicates correctly
|
||||
skip: "cluster replication/failover not yet implemented in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
behavior:
|
||||
auto_gc_behavior:
|
||||
enable: false
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
behavior:
|
||||
auto_gc_behavior:
|
||||
enable: false
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: 'SET dolt_cluster_ack_writes_timeout_secs = 10'
|
||||
- exec: 'CREATE DATABASE repo1'
|
||||
- exec: 'USE repo1'
|
||||
# 20 TEXT columns × 2 KB per cell = ~40 KB of out-of-band data per row.
|
||||
# Each row requires 20 separate external chunk references.
|
||||
- exec: |
|
||||
CREATE TABLE wide_text (
|
||||
id BIGINT PRIMARY KEY,
|
||||
t00 TEXT, t01 TEXT, t02 TEXT, t03 TEXT, t04 TEXT,
|
||||
t05 TEXT, t06 TEXT, t07 TEXT, t08 TEXT, t09 TEXT,
|
||||
t10 TEXT, t11 TEXT, t12 TEXT, t13 TEXT, t14 TEXT,
|
||||
t15 TEXT, t16 TEXT, t17 TEXT, t18 TEXT, t19 TEXT
|
||||
)
|
||||
- exec: |
|
||||
INSERT INTO wide_text VALUES
|
||||
(1,
|
||||
REPEAT('a',2000), REPEAT('b',2000), REPEAT('c',2000), REPEAT('d',2000), REPEAT('e',2000),
|
||||
REPEAT('f',2000), REPEAT('g',2000), REPEAT('h',2000), REPEAT('i',2000), REPEAT('j',2000),
|
||||
REPEAT('k',2000), REPEAT('l',2000), REPEAT('m',2000), REPEAT('n',2000), REPEAT('o',2000),
|
||||
REPEAT('p',2000), REPEAT('q',2000), REPEAT('r',2000), REPEAT('s',2000), REPEAT('t',2000))
|
||||
- exec: |
|
||||
INSERT INTO wide_text VALUES
|
||||
(2,
|
||||
REPEAT('A',2000), REPEAT('B',2000), REPEAT('C',2000), REPEAT('D',2000), REPEAT('E',2000),
|
||||
REPEAT('F',2000), REPEAT('G',2000), REPEAT('H',2000), REPEAT('I',2000), REPEAT('J',2000),
|
||||
REPEAT('K',2000), REPEAT('L',2000), REPEAT('M',2000), REPEAT('N',2000), REPEAT('O',2000),
|
||||
REPEAT('P',2000), REPEAT('Q',2000), REPEAT('R',2000), REPEAT('S',2000), REPEAT('T',2000))
|
||||
- exec: |
|
||||
INSERT INTO wide_text VALUES
|
||||
(3,
|
||||
REPEAT('1',2000), REPEAT('2',2000), REPEAT('3',2000), REPEAT('4',2000), REPEAT('5',2000),
|
||||
REPEAT('6',2000), REPEAT('7',2000), REPEAT('8',2000), REPEAT('9',2000), REPEAT('0',2000),
|
||||
REPEAT('1',2000), REPEAT('2',2000), REPEAT('3',2000), REPEAT('4',2000), REPEAT('5',2000),
|
||||
REPEAT('6',2000), REPEAT('7',2000), REPEAT('8',2000), REPEAT('9',2000), REPEAT('0',2000))
|
||||
- query: "SELECT COUNT(*) AS count FROM wide_text WHERE LENGTH(t00) = 2000 AND LENGTH(t19) = 2000"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["3"]]
|
||||
- query: "SELECT LEFT(t00,1) AS t00, LEFT(t10,1) AS t10 FROM wide_text WHERE id = 1"
|
||||
result:
|
||||
columns: ["t00","t10"]
|
||||
rows: [["a","k"]]
|
||||
- on: server2
|
||||
retry_attempts: 100
|
||||
queries:
|
||||
- exec: 'USE repo1'
|
||||
- query: "SELECT COUNT(*) AS count FROM wide_text WHERE LENGTH(t00) = 2000 AND LENGTH(t19) = 2000"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["3"]]
|
||||
- query: "SELECT LEFT(t00,1) AS t00, LEFT(t10,1) AS t10 FROM wide_text WHERE id = 1"
|
||||
result:
|
||||
columns: ["t00","t10"]
|
||||
rows: [["a","k"]]
|
||||
- query: "SELECT LEFT(t00,1) AS t00, LEFT(t10,1) AS t10 FROM wide_text WHERE id = 2"
|
||||
result:
|
||||
columns: ["t00","t10"]
|
||||
rows: [["A","K"]]
|
||||
@@ -0,0 +1,116 @@
|
||||
parallel: true
|
||||
tests:
|
||||
|
||||
- name: wide varchar column table replicates and survives gc
|
||||
skip: "cluster replication/failover not yet implemented in Doltgres"
|
||||
multi_repos:
|
||||
- name: server1
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
behavior:
|
||||
auto_gc_behavior:
|
||||
enable: false
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server2_cluster"}}/{database}
|
||||
bootstrap_role: primary
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server1_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server1
|
||||
- name: server2
|
||||
with_files:
|
||||
- name: server.yaml
|
||||
contents: |
|
||||
log_level: trace
|
||||
behavior:
|
||||
auto_gc_behavior:
|
||||
enable: false
|
||||
cluster:
|
||||
standby_remotes:
|
||||
- name: standby
|
||||
remote_url_template: http://localhost:{{get_port "server1_cluster"}}/{database}
|
||||
bootstrap_role: standby
|
||||
bootstrap_epoch: 1
|
||||
remotesapi:
|
||||
port: {{get_port "server2_cluster"}}
|
||||
server:
|
||||
args: ["--config", "server.yaml"]
|
||||
dynamic_port: server2
|
||||
connections:
|
||||
- on: server1
|
||||
queries:
|
||||
- exec: 'SET dolt_cluster_ack_writes_timeout_secs = 10'
|
||||
- exec: 'CREATE DATABASE repo1'
|
||||
- exec: 'USE repo1'
|
||||
# 8 VARCHAR(300) columns: Dolt's per-column accounting is ~8000 bytes for
|
||||
# VARCHAR(300), so 8 columns puts the row near (but under) Dolt's 64k inline limit.
|
||||
- exec: |
|
||||
CREATE TABLE wide_varchar (
|
||||
id BIGINT PRIMARY KEY,
|
||||
v00 VARCHAR(300), v01 VARCHAR(300), v02 VARCHAR(300), v03 VARCHAR(300),
|
||||
v04 VARCHAR(300), v05 VARCHAR(300), v06 VARCHAR(300), v07 VARCHAR(300)
|
||||
)
|
||||
- exec: |
|
||||
INSERT INTO wide_varchar VALUES
|
||||
(1,
|
||||
REPEAT('a',300), REPEAT('b',300), REPEAT('c',300), REPEAT('d',300),
|
||||
REPEAT('e',300), REPEAT('f',300), REPEAT('g',300), REPEAT('h',300))
|
||||
- exec: |
|
||||
INSERT INTO wide_varchar VALUES
|
||||
(2,
|
||||
REPEAT('A',300), REPEAT('B',300), REPEAT('C',300), REPEAT('D',300),
|
||||
REPEAT('E',300), REPEAT('F',300), REPEAT('G',300), REPEAT('H',300))
|
||||
- exec: |
|
||||
INSERT INTO wide_varchar VALUES
|
||||
(3,
|
||||
REPEAT('0',300), REPEAT('1',300), REPEAT('2',300), REPEAT('3',300),
|
||||
REPEAT('4',300), REPEAT('5',300), REPEAT('6',300), REPEAT('7',300))
|
||||
- query: "SELECT COUNT(*) AS count FROM wide_varchar WHERE LENGTH(v00) = 300 AND LENGTH(v07) = 300"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["3"]]
|
||||
- exec: 'SELECT dolt_gc()'
|
||||
- query: 'SELECT COUNT(*) FROM wide_varchar'
|
||||
error_match: "this connection can no longer be used"
|
||||
- on: server1
|
||||
queries:
|
||||
- query: |
|
||||
SELECT "database", standby_remote, role, epoch, replication_lag_millis, current_error
|
||||
FROM dolt_cluster.dolt_cluster_status
|
||||
ORDER BY "database" ASC
|
||||
result:
|
||||
columns: ["database","standby_remote","role","epoch","replication_lag_millis","current_error"]
|
||||
rows:
|
||||
- ["repo1","standby","primary","1","0","NULL"]
|
||||
retry_attempts: 100
|
||||
- exec: 'USE repo1'
|
||||
- query: "SELECT COUNT(*) AS count FROM wide_varchar WHERE LENGTH(v00) = 300 AND LENGTH(v07) = 300"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["3"]]
|
||||
- query: "SELECT LEFT(v00, 1) AS v00, LEFT(v07, 1) AS v07 FROM wide_varchar WHERE id = 1"
|
||||
result:
|
||||
columns: ["v00","v07"]
|
||||
rows: [["a","h"]]
|
||||
- on: server2
|
||||
queries:
|
||||
- exec: 'USE repo1'
|
||||
- query: "SELECT COUNT(*) AS count FROM wide_varchar WHERE LENGTH(v00) = 300 AND LENGTH(v07) = 300"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["3"]]
|
||||
- query: "SELECT LEFT(v00, 1) AS v00, LEFT(v07, 1) AS v07 FROM wide_varchar WHERE id = 2"
|
||||
result:
|
||||
columns: ["v00","v07"]
|
||||
rows: [["A","H"]]
|
||||
- exec: "SELECT dolt_gc('--shallow')"
|
||||
- query: "SELECT COUNT(*) AS count FROM wide_varchar WHERE LENGTH(v00) = 300 AND LENGTH(v07) = 300"
|
||||
result:
|
||||
columns: ["count"]
|
||||
rows: [["3"]]
|
||||
Reference in New Issue
Block a user