github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/merge/integration_test.go (about)

     1  // Copyright 2021 Dolthub, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package merge_test
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"io"
    21  	"math/rand"
    22  	"strconv"
    23  	"strings"
    24  	"testing"
    25  
    26  	"github.com/dolthub/go-mysql-server/sql"
    27  	"github.com/stretchr/testify/assert"
    28  	"github.com/stretchr/testify/require"
    29  	"golang.org/x/sync/errgroup"
    30  
    31  	cmd "github.com/dolthub/dolt/go/cmd/dolt/commands"
    32  	"github.com/dolthub/dolt/go/cmd/dolt/commands/cnfcmds"
    33  	"github.com/dolthub/dolt/go/cmd/dolt/commands/engine"
    34  	dtu "github.com/dolthub/dolt/go/libraries/doltcore/dtestutils"
    35  	"github.com/dolthub/dolt/go/libraries/doltcore/env"
    36  	"github.com/dolthub/dolt/go/libraries/doltcore/sqle"
    37  )
    38  
    39  func TestMerge(t *testing.T) {
    40  
    41  	setupCommon := []testCommand{
    42  		{cmd.SqlCmd{}, args{"-q", "CREATE TABLE test (pk int PRIMARY KEY, c0 int);"}},
    43  		{cmd.AddCmd{}, args{"."}},
    44  		{cmd.CommitCmd{}, args{"-am", "created table test"}},
    45  	}
    46  
    47  	tests := []struct {
    48  		name  string
    49  		setup []testCommand
    50  
    51  		query    string
    52  		expected []sql.Row
    53  	}{
    54  		{
    55  			name:  "smoke test",
    56  			query: "SELECT * FROM test;",
    57  		},
    58  		{
    59  			name: "fast-forward merge",
    60  			setup: []testCommand{
    61  				{cmd.CheckoutCmd{}, args{"-b", "other"}},
    62  				{cmd.SqlCmd{}, args{"-q", "INSERT INTO test VALUES (1,1),(2,2);"}},
    63  				{cmd.CommitCmd{}, args{"-am", "added rows on other"}},
    64  				{cmd.CheckoutCmd{}, args{env.DefaultInitBranch}},
    65  				{cmd.MergeCmd{}, args{"other"}},
    66  			},
    67  			query: "SELECT * FROM test",
    68  			expected: []sql.Row{
    69  				{int32(1), int32(1)},
    70  				{int32(2), int32(2)},
    71  			},
    72  		},
    73  		{
    74  			name: "three-way merge",
    75  			setup: []testCommand{
    76  				{cmd.BranchCmd{}, args{"other"}},
    77  				{cmd.SqlCmd{}, args{"-q", "INSERT INTO test VALUES (11,11),(22,22);"}},
    78  				{cmd.CommitCmd{}, args{"-am", "added rows on main"}},
    79  				{cmd.CheckoutCmd{}, args{"other"}},
    80  				{cmd.SqlCmd{}, args{"-q", "INSERT INTO test VALUES (1,1),(2,2);"}},
    81  				{cmd.CommitCmd{}, args{"-am", "added rows on other"}},
    82  				{cmd.CheckoutCmd{}, args{env.DefaultInitBranch}},
    83  				{cmd.MergeCmd{}, args{"other"}},
    84  			},
    85  			query: "SELECT * FROM test",
    86  			expected: []sql.Row{
    87  				{int32(1), int32(1)},
    88  				{int32(2), int32(2)},
    89  				{int32(11), int32(11)},
    90  				{int32(22), int32(22)},
    91  			},
    92  		},
    93  		{
    94  			name: "create the same table schema, with different row data, on two branches",
    95  			setup: []testCommand{
    96  				{cmd.BranchCmd{}, args{"other"}},
    97  				{cmd.SqlCmd{}, args{"-q", "CREATE TABLE quiz (pk varchar(120) primary key);"}},
    98  				{cmd.SqlCmd{}, args{"-q", "INSERT INTO quiz VALUES ('a'),('b'),('c');"}},
    99  				{cmd.AddCmd{}, []string{"."}},
   100  				{cmd.CommitCmd{}, args{"-am", "added rows on main"}},
   101  				{cmd.CheckoutCmd{}, args{"other"}},
   102  				{cmd.SqlCmd{}, args{"-q", "CREATE TABLE quiz (pk varchar(120) primary key);"}},
   103  				{cmd.AddCmd{}, []string{"."}},
   104  				{cmd.SqlCmd{}, args{"-q", "INSERT INTO quiz VALUES ('x'),('y'),('z');"}},
   105  				{cmd.CommitCmd{}, args{"-am", "added rows on other"}},
   106  				{cmd.CheckoutCmd{}, args{env.DefaultInitBranch}},
   107  				{cmd.MergeCmd{}, args{"other"}},
   108  			},
   109  			query: "SELECT * FROM quiz ORDER BY pk",
   110  			expected: []sql.Row{
   111  				{"a"},
   112  				{"b"},
   113  				{"c"},
   114  				{"x"},
   115  				{"y"},
   116  				{"z"},
   117  			},
   118  		},
   119  	}
   120  
   121  	for _, test := range tests {
   122  		t.Run(test.name, func(t *testing.T) {
   123  			ctx := context.Background()
   124  			dEnv := dtu.CreateTestEnv()
   125  			defer dEnv.DoltDB.Close()
   126  
   127  			for _, tc := range setupCommon {
   128  				exit := tc.exec(t, ctx, dEnv)
   129  				require.Equal(t, 0, exit)
   130  			}
   131  			for _, tc := range test.setup {
   132  				exit := tc.exec(t, ctx, dEnv)
   133  				require.Equal(t, 0, exit)
   134  			}
   135  
   136  			root, err := dEnv.WorkingRoot(ctx)
   137  			require.NoError(t, err)
   138  			actRows, err := sqle.ExecuteSelect(dEnv, root, test.query)
   139  			require.NoError(t, err)
   140  
   141  			require.Equal(t, len(test.expected), len(actRows))
   142  			for i := range test.expected {
   143  				assert.Equal(t, test.expected[i], actRows[i])
   144  			}
   145  		})
   146  	}
   147  }
   148  
   149  func TestMergeConflicts(t *testing.T) {
   150  
   151  	setupCommon := []testCommand{
   152  		{cmd.SqlCmd{}, args{"-q", "CREATE TABLE test (pk int PRIMARY KEY, c0 int);"}},
   153  		{cmd.AddCmd{}, []string{"."}},
   154  		{cmd.CommitCmd{}, args{"-am", "created table test"}},
   155  	}
   156  
   157  	tests := []struct {
   158  		name  string
   159  		setup []testCommand
   160  
   161  		query    string
   162  		expected []sql.Row
   163  	}{
   164  		{
   165  			name: "conflict on merge",
   166  			setup: []testCommand{
   167  				{cmd.CheckoutCmd{}, args{"-b", "other"}},
   168  				{cmd.SqlCmd{}, args{"-q", "INSERT INTO test VALUES (1,1),(2,2);"}},
   169  				{cmd.CommitCmd{}, args{"-am", "added rows on other"}},
   170  				{cmd.CheckoutCmd{}, args{env.DefaultInitBranch}},
   171  				{cmd.SqlCmd{}, args{"-q", "INSERT INTO test VALUES (1,11),(2,22);"}},
   172  				{cmd.CommitCmd{}, args{"-am", "added the same rows on main"}},
   173  				{cmd.MergeCmd{}, args{"other"}},
   174  			},
   175  			query: "SELECT * FROM dolt_conflicts",
   176  			expected: []sql.Row{
   177  				{"test", uint64(2)},
   178  			},
   179  		},
   180  		{
   181  			name: "conflict on merge, resolve with ours",
   182  			setup: []testCommand{
   183  				{cmd.CheckoutCmd{}, args{"-b", "other"}},
   184  				{cmd.SqlCmd{}, args{"-q", "INSERT INTO test VALUES (1,1),(2,2);"}},
   185  				{cmd.AddCmd{}, []string{"."}},
   186  				{cmd.CommitCmd{}, args{"-am", "added rows on other"}},
   187  				{cmd.CheckoutCmd{}, args{env.DefaultInitBranch}},
   188  				{cmd.SqlCmd{}, args{"-q", "INSERT INTO test VALUES (1,11),(2,22);"}},
   189  				{cmd.CommitCmd{}, args{"-am", "added the same rows on main"}},
   190  				{cmd.MergeCmd{}, args{"other"}},
   191  				{cnfcmds.ResolveCmd{}, args{"--ours", "test"}},
   192  			},
   193  			query: "SELECT * FROM test",
   194  			expected: []sql.Row{
   195  				{int32(1), int32(11)},
   196  				{int32(2), int32(22)},
   197  			},
   198  		},
   199  		{
   200  			name: "conflict on merge, no table in ancestor",
   201  			setup: []testCommand{
   202  				{cmd.CheckoutCmd{}, args{"-b", "other"}},
   203  				{cmd.SqlCmd{}, args{"-q", "CREATE TABLE quiz (pk int PRIMARY KEY, c0 int);"}},
   204  				{cmd.SqlCmd{}, args{"-q", "INSERT INTO quiz VALUES (1,1),(2,2);"}},
   205  				{cmd.AddCmd{}, []string{"."}},
   206  				{cmd.CommitCmd{}, args{"-am", "added rows on other"}},
   207  				{cmd.CheckoutCmd{}, args{env.DefaultInitBranch}},
   208  				{cmd.SqlCmd{}, args{"-q", "CREATE TABLE quiz (pk int PRIMARY KEY, c0 int);"}},
   209  				{cmd.SqlCmd{}, args{"-q", "INSERT INTO quiz VALUES (1,11),(2,22);"}},
   210  				{cmd.AddCmd{}, []string{"."}},
   211  				{cmd.CommitCmd{}, args{"-am", "added the same rows on main"}},
   212  				{cmd.MergeCmd{}, args{"other"}},
   213  			},
   214  			query: "SELECT * FROM dolt_conflicts",
   215  			expected: []sql.Row{
   216  				{"quiz", uint64(2)},
   217  			},
   218  		},
   219  		{
   220  			name: "conflict on merge, no table in ancestor, resolve with theirs",
   221  			setup: []testCommand{
   222  				{cmd.CheckoutCmd{}, args{"-b", "other"}},
   223  				{cmd.SqlCmd{}, args{"-q", "CREATE TABLE quiz (pk int PRIMARY KEY, c0 int);"}},
   224  				{cmd.SqlCmd{}, args{"-q", "INSERT INTO quiz VALUES (1,1),(2,2);"}},
   225  				{cmd.AddCmd{}, []string{"."}},
   226  				{cmd.CommitCmd{}, args{"-am", "added rows on other"}},
   227  				{cmd.CheckoutCmd{}, args{env.DefaultInitBranch}},
   228  				{cmd.SqlCmd{}, args{"-q", "CREATE TABLE quiz (pk int PRIMARY KEY, c0 int);"}},
   229  				{cmd.SqlCmd{}, args{"-q", "INSERT INTO quiz VALUES (1,11),(2,22);"}},
   230  				{cmd.AddCmd{}, []string{"."}},
   231  				{cmd.CommitCmd{}, args{"-am", "added the same rows on main"}},
   232  				{cmd.MergeCmd{}, args{"other"}},
   233  				{cnfcmds.ResolveCmd{}, args{"--theirs", "quiz"}},
   234  			},
   235  			query: "SELECT * FROM quiz",
   236  			expected: []sql.Row{
   237  				{int32(1), int32(1)},
   238  				{int32(2), int32(2)},
   239  			},
   240  		},
   241  	}
   242  
   243  	for _, test := range tests {
   244  		t.Run(test.name, func(t *testing.T) {
   245  			ctx := context.Background()
   246  			dEnv := dtu.CreateTestEnv()
   247  			defer dEnv.DoltDB.Close()
   248  
   249  			for _, tc := range setupCommon {
   250  				exit := tc.exec(t, ctx, dEnv)
   251  				// allow merge to fail with conflicts
   252  				if _, ok := tc.cmd.(cmd.MergeCmd); !ok {
   253  					require.Equal(t, 0, exit)
   254  				}
   255  			}
   256  			for _, tc := range test.setup {
   257  				exit := tc.exec(t, ctx, dEnv)
   258  				// allow merge to fail with conflicts
   259  				if _, ok := tc.cmd.(cmd.MergeCmd); !ok {
   260  					require.Equal(t, 0, exit)
   261  				}
   262  			}
   263  
   264  			root, err := dEnv.WorkingRoot(ctx)
   265  			require.NoError(t, err)
   266  			actRows, err := sqle.ExecuteSelect(dEnv, root, test.query)
   267  			require.NoError(t, err)
   268  
   269  			require.Equal(t, len(test.expected), len(actRows))
   270  			for i := range test.expected {
   271  				assert.Equal(t, test.expected[i], actRows[i])
   272  			}
   273  		})
   274  	}
   275  }
   276  
   277  const (
   278  	concurrentScale   = 10_000
   279  	concurrentIters   = 100
   280  	concurrentThreads = 8
   281  	concurrentTable   = "CREATE TABLE concurrent (" +
   282  		"  id int NOT NULL," +
   283  		"  c0 int NOT NULL," +
   284  		"  c1 int NOT NULL," +
   285  		"  PRIMARY KEY (id)," +
   286  		"  KEY `idx0` (c0)," +
   287  		"  KEY `idx1` (c1, c0)" +
   288  		");"
   289  )
   290  
   291  // TestMergeConcurrency runs current merges via
   292  // concurrent SQL transactions.
   293  func TestMergeConcurrency(t *testing.T) {
   294  	ctx := context.Background()
   295  	dEnv := setupConcurrencyTest(t, ctx)
   296  	defer dEnv.DoltDB.Close()
   297  	_, eng := engineFromEnvironment(ctx, dEnv)
   298  
   299  	eg, ctx := errgroup.WithContext(ctx)
   300  	for i := 0; i < concurrentThreads; i++ {
   301  		seed := i
   302  		eg.Go(func() error {
   303  			return runConcurrentTxs(ctx, eng, seed)
   304  		})
   305  	}
   306  	assert.NoError(t, eg.Wait())
   307  }
   308  
   309  func runConcurrentTxs(ctx context.Context, eng *engine.SqlEngine, seed int) error {
   310  	sess, err := eng.NewDoltSession(ctx, sql.NewBaseSession())
   311  	if err != nil {
   312  		return err
   313  	}
   314  	sctx := sql.NewContext(ctx, sql.WithSession(sess))
   315  	sctx.SetCurrentDatabase("dolt")
   316  	sctx.Session.SetClient(sql.Client{User: "root", Address: "%"})
   317  
   318  	rnd := rand.New(rand.NewSource(int64(seed)))
   319  	zipf := rand.NewZipf(rnd, 1.1, 1.0, concurrentScale)
   320  
   321  	for i := 0; i < concurrentIters; i++ {
   322  		if err := executeQuery(sctx, eng, "BEGIN"); err != nil {
   323  			return err
   324  		}
   325  
   326  		id := zipf.Uint64()
   327  		sum := fmt.Sprintf("SELECT sum(c0), sum(c1) "+
   328  			"FROM concurrent WHERE id BETWEEN %d AND %d", id, id+10)
   329  		update := fmt.Sprintf("UPDATE concurrent "+
   330  			"SET c0 = c0 + %d, c1 = c1 + %d WHERE id = %d",
   331  			seed, seed, id)
   332  
   333  		if err := executeQuery(sctx, eng, sum); err != nil {
   334  			return err
   335  		}
   336  		if err := executeQuery(sctx, eng, update); err != nil {
   337  			return err
   338  		}
   339  		if err := executeQuery(sctx, eng, sum); err != nil {
   340  			return err
   341  		}
   342  		if err := executeQuery(sctx, eng, "COMMIT"); err != nil {
   343  			// allow serialization errors
   344  			if !sql.ErrLockDeadlock.Is(err) {
   345  				return err
   346  			}
   347  		}
   348  	}
   349  	return nil
   350  }
   351  
   352  func setupConcurrencyTest(t *testing.T, ctx context.Context) (dEnv *env.DoltEnv) {
   353  	dEnv = dtu.CreateTestEnv()
   354  
   355  	dbName, eng := engineFromEnvironment(ctx, dEnv)
   356  	sqlCtx, err := eng.NewLocalContext(ctx)
   357  	require.NoError(t, err)
   358  	sqlCtx.SetCurrentDatabase(dbName)
   359  
   360  	require.NoError(t, executeQuery(sqlCtx, eng, concurrentTable))
   361  	require.NoError(t, executeQuery(sqlCtx, eng, generateTestData()))
   362  	return
   363  }
   364  
   365  func engineFromEnvironment(ctx context.Context, dEnv *env.DoltEnv) (dbName string, eng *engine.SqlEngine) {
   366  	mrEnv, err := env.MultiEnvForDirectory(ctx, dEnv.Config.WriteableConfig(), dEnv.FS, dEnv.Version, dEnv)
   367  	if err != nil {
   368  		panic(err)
   369  	}
   370  
   371  	eng, err = engine.NewSqlEngine(ctx, mrEnv, &engine.SqlEngineConfig{
   372  		IsReadOnly: false,
   373  		ServerUser: "root",
   374  		ServerHost: "localhost",
   375  		Autocommit: true,
   376  	})
   377  	if err != nil {
   378  		panic(err)
   379  	}
   380  
   381  	return mrEnv.GetFirstDatabase(), eng
   382  }
   383  
   384  func executeQuery(ctx *sql.Context, eng *engine.SqlEngine, query string) error {
   385  	_, iter, err := eng.Query(ctx, query)
   386  	if err != nil {
   387  		return err
   388  	}
   389  	for {
   390  		_, err = iter.Next(ctx)
   391  		if err == io.EOF {
   392  			break
   393  		}
   394  		if err != nil {
   395  			return err
   396  		}
   397  	}
   398  	return iter.Close(ctx) // tx commit
   399  }
   400  
   401  func generateTestData() string {
   402  	var sb strings.Builder
   403  	sb.WriteString("INSERT INTO concurrent VALUES ")
   404  	sb.WriteString("(0, 0, 0")
   405  	for i := 1; i < concurrentScale; i++ {
   406  		c0 := rand.Intn(concurrentScale)
   407  		c1 := rand.Intn(concurrentScale)
   408  		sb.WriteString("), (")
   409  		sb.WriteString(strconv.Itoa(i))
   410  		sb.WriteString(", ")
   411  		sb.WriteString(strconv.Itoa(c0))
   412  		sb.WriteString(", ")
   413  		sb.WriteString(strconv.Itoa(c1))
   414  	}
   415  	sb.WriteString(");")
   416  	return sb.String()
   417  }