vitess.io/vitess@v0.16.2/go/test/endtoend/utils/mysql.go (about)

     1  /*
     2  Copyright 2022 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package utils
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"os"
    23  	"path"
    24  	"testing"
    25  
    26  	"github.com/stretchr/testify/assert"
    27  
    28  	"vitess.io/vitess/go/sqltypes"
    29  	"vitess.io/vitess/go/vt/dbconfigs"
    30  	"vitess.io/vitess/go/vt/sqlparser"
    31  
    32  	"vitess.io/vitess/go/mysql"
    33  	"vitess.io/vitess/go/test/endtoend/cluster"
    34  	"vitess.io/vitess/go/vt/mysqlctl"
    35  )
    36  
    37  // NewMySQL creates a new MySQL server using the local mysqld binary. The name of the database
    38  // will be set to `dbName`. SQL queries that need to be executed on the new MySQL instance
    39  // can be passed through the `schemaSQL` argument.
    40  // The mysql.ConnParams to connect to the new database is returned, along with a function to
    41  // teardown the database.
    42  func NewMySQL(cluster *cluster.LocalProcessCluster, dbName string, schemaSQL ...string) (mysql.ConnParams, func(), error) {
    43  	return NewMySQLWithDetails(cluster.GetAndReservePort(), cluster.Hostname, dbName, schemaSQL...)
    44  }
    45  
    46  // CreateMysqldAndMycnf returns a Mysqld and a Mycnf object to use for working with a MySQL
    47  // installation that hasn't been set up yet.
    48  func CreateMysqldAndMycnf(tabletUID uint32, mysqlSocket string, mysqlPort int32) (*mysqlctl.Mysqld, *mysqlctl.Mycnf, error) {
    49  	mycnf := mysqlctl.NewMycnf(tabletUID, mysqlPort)
    50  	if err := mycnf.RandomizeMysqlServerID(); err != nil {
    51  		return nil, nil, fmt.Errorf("couldn't generate random MySQL server_id: %v", err)
    52  	}
    53  	if mysqlSocket != "" {
    54  		mycnf.SocketFile = mysqlSocket
    55  	}
    56  	var cfg dbconfigs.DBConfigs
    57  	// ensure the DBA username is 'root' instead of the system's default username so that mysqladmin can shutdown
    58  	cfg.Dba.User = "root"
    59  	cfg.InitWithSocket(mycnf.SocketFile)
    60  	return mysqlctl.NewMysqld(&cfg), mycnf, nil
    61  }
    62  
    63  func NewMySQLWithDetails(port int, hostname, dbName string, schemaSQL ...string) (mysql.ConnParams, func(), error) {
    64  	mysqlDir, err := createMySQLDir()
    65  	if err != nil {
    66  		return mysql.ConnParams{}, nil, err
    67  	}
    68  	initMySQLFile, err := createInitSQLFile(mysqlDir, dbName)
    69  	if err != nil {
    70  		return mysql.ConnParams{}, nil, err
    71  	}
    72  
    73  	mysqlPort := port
    74  	mysqld, mycnf, err := CreateMysqldAndMycnf(0, "", int32(mysqlPort))
    75  	if err != nil {
    76  		return mysql.ConnParams{}, nil, err
    77  	}
    78  	err = initMysqld(mysqld, mycnf, initMySQLFile)
    79  	if err != nil {
    80  		return mysql.ConnParams{}, nil, err
    81  	}
    82  
    83  	params := mysql.ConnParams{
    84  		UnixSocket: mycnf.SocketFile,
    85  		Host:       hostname,
    86  		Uname:      "root",
    87  		DbName:     dbName,
    88  	}
    89  	for _, sql := range schemaSQL {
    90  		err = prepareMySQLWithSchema(params, sql)
    91  		if err != nil {
    92  			return mysql.ConnParams{}, nil, err
    93  		}
    94  	}
    95  	return params, func() {
    96  		ctx := context.Background()
    97  		_ = mysqld.Teardown(ctx, mycnf, true)
    98  	}, nil
    99  }
   100  
   101  func createMySQLDir() (string, error) {
   102  	mysqlDir := mysqlctl.TabletDir(0)
   103  	err := os.Mkdir(mysqlDir, 0700)
   104  	if err != nil {
   105  		return "", err
   106  	}
   107  	return mysqlDir, nil
   108  }
   109  
   110  func createInitSQLFile(mysqlDir, ksName string) (string, error) {
   111  	initSQLFile := path.Join(mysqlDir, "init.sql")
   112  	f, err := os.Create(initSQLFile)
   113  	if err != nil {
   114  		return "", err
   115  	}
   116  	defer f.Close()
   117  
   118  	_, err = f.WriteString(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s;", ksName))
   119  	if err != nil {
   120  		return "", err
   121  	}
   122  	return initSQLFile, nil
   123  }
   124  
   125  func initMysqld(mysqld *mysqlctl.Mysqld, mycnf *mysqlctl.Mycnf, initSQLFile string) error {
   126  	f, err := os.CreateTemp(path.Dir(mycnf.Path), "my.cnf")
   127  	if err != nil {
   128  		return err
   129  	}
   130  	f.Close()
   131  
   132  	ctx := context.Background()
   133  	err = mysqld.Init(ctx, mycnf, initSQLFile)
   134  	if err != nil {
   135  		return err
   136  	}
   137  	return nil
   138  }
   139  
   140  func prepareMySQLWithSchema(params mysql.ConnParams, sql string) error {
   141  	ctx := context.Background()
   142  	conn, err := mysql.Connect(ctx, &params)
   143  	if err != nil {
   144  		return err
   145  	}
   146  	_, err = conn.ExecuteFetch(sql, 1, false)
   147  	if err != nil {
   148  		return err
   149  	}
   150  	return nil
   151  }
   152  
   153  func compareVitessAndMySQLResults(t *testing.T, query string, vtQr, mysqlQr *sqltypes.Result, compareColumns bool) {
   154  	if vtQr == nil && mysqlQr == nil {
   155  		return
   156  	}
   157  	if vtQr == nil {
   158  		t.Error("Vitess result is 'nil' while MySQL's is not.")
   159  		return
   160  	}
   161  	if mysqlQr == nil {
   162  		t.Error("MySQL result is 'nil' while Vitess' is not.")
   163  		return
   164  	}
   165  	if compareColumns {
   166  		vtColCount := len(vtQr.Fields)
   167  		myColCount := len(mysqlQr.Fields)
   168  		if vtColCount > 0 && myColCount > 0 {
   169  			if vtColCount != myColCount {
   170  				t.Errorf("column count does not match: %d vs %d", vtColCount, myColCount)
   171  			}
   172  
   173  			var vtCols []string
   174  			var myCols []string
   175  			for i, vtField := range vtQr.Fields {
   176  				vtCols = append(vtCols, vtField.Name)
   177  				myCols = append(myCols, mysqlQr.Fields[i].Name)
   178  			}
   179  			assert.Equal(t, myCols, vtCols, "column names do not match - the expected values are what mysql produced")
   180  		}
   181  	}
   182  	stmt, err := sqlparser.Parse(query)
   183  	if err != nil {
   184  		t.Error(err)
   185  		return
   186  	}
   187  	orderBy := false
   188  	if selStmt, isSelStmt := stmt.(sqlparser.SelectStatement); isSelStmt {
   189  		orderBy = selStmt.GetOrderBy() != nil
   190  	}
   191  
   192  	if orderBy && sqltypes.ResultsEqual([]sqltypes.Result{*vtQr}, []sqltypes.Result{*mysqlQr}) {
   193  		return
   194  	} else if sqltypes.ResultsEqualUnordered([]sqltypes.Result{*vtQr}, []sqltypes.Result{*mysqlQr}) {
   195  		return
   196  	}
   197  
   198  	errStr := "Query (" + query + ") results mismatched.\nVitess Results:\n"
   199  	for _, row := range vtQr.Rows {
   200  		errStr += fmt.Sprintf("%s\n", row)
   201  	}
   202  	errStr += "MySQL Results:\n"
   203  	for _, row := range mysqlQr.Rows {
   204  		errStr += fmt.Sprintf("%s\n", row)
   205  	}
   206  	t.Error(errStr)
   207  }
   208  
   209  func compareVitessAndMySQLErrors(t *testing.T, vtErr, mysqlErr error) {
   210  	if vtErr != nil && mysqlErr != nil || vtErr == nil && mysqlErr == nil {
   211  		return
   212  	}
   213  	out := fmt.Sprintf("Vitess and MySQL are not erroring the same way.\nVitess error: %v\nMySQL error: %v", vtErr, mysqlErr)
   214  	t.Error(out)
   215  }