chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:32:25 +08:00
commit e014feafe1
2285 changed files with 1131979 additions and 0 deletions
@@ -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
1 pk c1 c2 c3 c4 c5
2 0 1 2 3 4 5
3 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"]]
@@ -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"]]
@@ -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"]]