github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/pkg/conn/utils.go (about)

     1  // Copyright 2021 PingCAP, 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  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package conn
    15  
    16  import (
    17  	"context"
    18  	"fmt"
    19  	"math"
    20  	"strconv"
    21  	"strings"
    22  
    23  	gmysql "github.com/go-mysql-org/go-mysql/mysql"
    24  	"github.com/pingcap/failpoint"
    25  	"github.com/pingcap/tidb/dumpling/export"
    26  	tmysql "github.com/pingcap/tidb/pkg/parser/mysql"
    27  	tcontext "github.com/pingcap/tiflow/dm/pkg/context"
    28  	"github.com/pingcap/tiflow/dm/pkg/gtid"
    29  	"github.com/pingcap/tiflow/dm/pkg/terror"
    30  	"go.uber.org/zap"
    31  )
    32  
    33  // GetGlobalVariable gets server's global variable.
    34  func GetGlobalVariable(ctx *tcontext.Context, db *BaseDB, variable string) (value string, err error) {
    35  	failpoint.Inject("GetGlobalVariableFailed", func(val failpoint.Value) {
    36  		items := strings.Split(val.(string), ",")
    37  		if len(items) != 2 {
    38  			ctx.L().Fatal("failpoint GetGlobalVariableFailed's value is invalid", zap.String("val", val.(string)))
    39  		}
    40  		variableName := items[0]
    41  		errCode, err1 := strconv.ParseUint(items[1], 10, 16)
    42  		if err1 != nil {
    43  			ctx.L().Fatal("failpoint GetGlobalVariableFailed's value is invalid", zap.String("val", val.(string)))
    44  		}
    45  		if variable == variableName {
    46  			err = tmysql.NewErr(uint16(errCode))
    47  			ctx.L().Warn("GetGlobalVariable failed", zap.String("variable", variable), zap.String("failpoint", "GetGlobalVariableFailed"), zap.Error(err))
    48  			failpoint.Return("", terror.DBErrorAdapt(err, db.Scope, terror.ErrDBDriverError))
    49  		}
    50  	})
    51  
    52  	conn, err := db.GetBaseConn(ctx.Context())
    53  	if err != nil {
    54  		return "", err
    55  	}
    56  	defer db.CloseConnWithoutErr(conn)
    57  	return getVariable(ctx, conn, variable, true)
    58  }
    59  
    60  func getVariable(ctx *tcontext.Context, conn *BaseConn, variable string, isGlobal bool) (value string, err error) {
    61  	var template string
    62  	if isGlobal {
    63  		template = "SHOW GLOBAL VARIABLES LIKE '%s'"
    64  	} else {
    65  		template = "SHOW VARIABLES LIKE '%s'"
    66  	}
    67  	query := fmt.Sprintf(template, variable)
    68  	row, err := conn.QuerySQL(ctx, query)
    69  	if err != nil {
    70  		return "", err
    71  	}
    72  	defer func() {
    73  		_ = row.Close()
    74  		_ = row.Err()
    75  	}()
    76  
    77  	// Show an example.
    78  	/*
    79  		mysql> SHOW GLOBAL VARIABLES LIKE "binlog_format";
    80  		+---------------+-------+
    81  		| Variable_name | Value |
    82  		+---------------+-------+
    83  		| binlog_format | ROW   |
    84  		+---------------+-------+
    85  	*/
    86  
    87  	if !row.Next() {
    88  		return "", terror.WithScope(terror.ErrDBDriverError.Generatef("variable %s not found", variable), conn.Scope)
    89  	}
    90  
    91  	err = row.Scan(&variable, &value)
    92  	if err != nil {
    93  		return "", terror.DBErrorAdapt(err, conn.Scope, terror.ErrDBDriverError)
    94  	}
    95  	return value, nil
    96  }
    97  
    98  // GetMasterStatus gets status from master.
    99  // When the returned error is nil, the gtid must be not nil.
   100  func GetMasterStatus(ctx *tcontext.Context, db *BaseDB, flavor string) (
   101  	string, uint64, string, string, string, error,
   102  ) {
   103  	var (
   104  		binlogName     string
   105  		pos            uint64
   106  		binlogDoDB     string
   107  		binlogIgnoreDB string
   108  		gtidStr        string
   109  		err            error
   110  	)
   111  	// need REPLICATION SLAVE privilege
   112  	rows, err := db.QueryContext(ctx, `SHOW MASTER STATUS`)
   113  	if err != nil {
   114  		err = terror.DBErrorAdapt(err, db.Scope, terror.ErrDBDriverError)
   115  		return binlogName, pos, binlogDoDB, binlogIgnoreDB, gtidStr, err
   116  	}
   117  	defer rows.Close()
   118  
   119  	// Show an example.
   120  	/*
   121  		MySQL [test]> SHOW MASTER STATUS;
   122  		+-----------+----------+--------------+------------------+--------------------------------------------+
   123  		| File      | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set                          |
   124  		+-----------+----------+--------------+------------------+--------------------------------------------+
   125  		| ON.000001 |     4822 |              |                  | 85ab69d1-b21f-11e6-9c5e-64006a8978d2:1-46
   126  		+-----------+----------+--------------+------------------+--------------------------------------------+
   127  	*/
   128  	/*
   129  		For MariaDB,SHOW MASTER STATUS:
   130  		+--------------------+----------+--------------+------------------+
   131  		| File               | Position | Binlog_Do_DB | Binlog_Ignore_DB |
   132  		+--------------------+----------+--------------+------------------+
   133  		| mariadb-bin.000016 |      475 |              |                  |
   134  		+--------------------+----------+--------------+------------------+
   135  		SELECT @@global.gtid_binlog_pos;
   136  		+--------------------------+
   137  		| @@global.gtid_binlog_pos |
   138  		+--------------------------+
   139  		| 0-1-2                    |
   140  		+--------------------------+
   141  	*/
   142  
   143  	var rowsResult [][]string
   144  	if flavor == gmysql.MySQLFlavor {
   145  		rowsResult, err = export.GetSpecifiedColumnValuesAndClose(rows, "File", "Position", "Binlog_Do_DB", "Binlog_Ignore_DB", "Executed_Gtid_Set")
   146  		if err != nil {
   147  			err = terror.DBErrorAdapt(err, db.Scope, terror.ErrDBDriverError)
   148  			return binlogName, pos, binlogDoDB, binlogIgnoreDB, gtidStr, err
   149  		}
   150  
   151  		switch {
   152  		case len(rowsResult) == 0:
   153  			err = terror.ErrNoMasterStatus.Generate()
   154  			return binlogName, pos, binlogDoDB, binlogIgnoreDB, gtidStr, err
   155  		case len(rowsResult[0]) != 5:
   156  			ctx.L().DPanic("The number of columns that SHOW MASTER STATUS returns for MySQL is not equal to 5, will not use the retrieved information")
   157  			err = terror.ErrIncorrectReturnColumnsNum.Generate()
   158  			return binlogName, pos, binlogDoDB, binlogIgnoreDB, gtidStr, err
   159  		default:
   160  			binlogName = rowsResult[0][0]
   161  			var posInt uint64
   162  			posInt, err = strconv.ParseUint(rowsResult[0][1], 10, 64)
   163  			if err != nil {
   164  				err = terror.DBErrorAdapt(err, db.Scope, terror.ErrDBDriverError)
   165  				return binlogName, pos, binlogDoDB, binlogIgnoreDB, gtidStr, err
   166  			}
   167  			pos = posInt
   168  			binlogDoDB = rowsResult[0][2]
   169  			binlogIgnoreDB = rowsResult[0][3]
   170  			gtidStr = rowsResult[0][4]
   171  		}
   172  	} else {
   173  		rowsResult, err = export.GetSpecifiedColumnValuesAndClose(rows, "File", "Position", "Binlog_Do_DB", "Binlog_Ignore_DB")
   174  		if err != nil {
   175  			err = terror.DBErrorAdapt(err, db.Scope, terror.ErrDBDriverError)
   176  			return binlogName, pos, binlogDoDB, binlogIgnoreDB, gtidStr, err
   177  		}
   178  
   179  		switch {
   180  		case len(rowsResult) == 0:
   181  			err = terror.ErrNoMasterStatus.Generate()
   182  			return binlogName, pos, binlogDoDB, binlogIgnoreDB, gtidStr, err
   183  		case len(rowsResult[0]) != 4:
   184  			ctx.L().DPanic("The number of columns that SHOW MASTER STATUS returns for MariaDB is not equal to 4, will not use the retrieved information")
   185  			err = terror.ErrIncorrectReturnColumnsNum.Generate()
   186  			return binlogName, pos, binlogDoDB, binlogIgnoreDB, gtidStr, err
   187  		default:
   188  			binlogName = rowsResult[0][0]
   189  			var posInt uint64
   190  			posInt, err = strconv.ParseUint(rowsResult[0][1], 10, 64)
   191  			if err != nil {
   192  				err = terror.DBErrorAdapt(err, db.Scope, terror.ErrDBDriverError)
   193  				return binlogName, pos, binlogDoDB, binlogIgnoreDB, gtidStr, err
   194  			}
   195  			pos = posInt
   196  			binlogDoDB = rowsResult[0][2]
   197  			binlogIgnoreDB = rowsResult[0][3]
   198  		}
   199  	}
   200  
   201  	if flavor == gmysql.MariaDBFlavor {
   202  		gtidStr, err = GetGlobalVariable(ctx, db, "gtid_binlog_pos")
   203  		if err != nil {
   204  			return binlogName, pos, binlogDoDB, binlogIgnoreDB, gtidStr, err
   205  		}
   206  	}
   207  
   208  	if len(rowsResult) > 1 {
   209  		ctx.L().Warn("SHOW MASTER STATUS returns more than one row, will only use first row")
   210  	}
   211  	if rows.Close() != nil {
   212  		err = terror.DBErrorAdapt(rows.Err(), db.Scope, terror.ErrDBDriverError)
   213  		return binlogName, pos, binlogDoDB, binlogIgnoreDB, gtidStr, err
   214  	}
   215  	if rows.Err() != nil {
   216  		err = terror.DBErrorAdapt(rows.Err(), db.Scope, terror.ErrDBDriverError)
   217  		return binlogName, pos, binlogDoDB, binlogIgnoreDB, gtidStr, err
   218  	}
   219  
   220  	return binlogName, pos, binlogDoDB, binlogIgnoreDB, gtidStr, err
   221  }
   222  
   223  // GetPosAndGs get binlog position and gmysql.GTIDSet from `show master status`.
   224  func GetPosAndGs(ctx *tcontext.Context, db *BaseDB, flavor string) (
   225  	binlogPos gmysql.Position,
   226  	gs gmysql.GTIDSet,
   227  	err error,
   228  ) {
   229  	binlogName, pos, _, _, gtidStr, err := GetMasterStatus(ctx, db, flavor)
   230  	if err != nil {
   231  		return
   232  	}
   233  	if pos > math.MaxUint32 {
   234  		ctx.L().Warn("the pos returned by GetMasterStatus beyonds the range of uint32")
   235  	}
   236  	binlogPos = gmysql.Position{
   237  		Name: binlogName,
   238  		Pos:  uint32(pos),
   239  	}
   240  
   241  	gs, err = gtid.ParserGTID(flavor, gtidStr)
   242  	return
   243  }
   244  
   245  // GetBinlogDB get binlog_do_db and binlog_ignore_db from `show master status`.
   246  func GetBinlogDB(ctx *tcontext.Context, db *BaseDB, flavor string) (string, string, error) {
   247  	// nolint:dogsled
   248  	_, _, binlogDoDB, binlogIgnoreDB, _, err := GetMasterStatus(ctx, db, flavor)
   249  	return binlogDoDB, binlogIgnoreDB, err
   250  }
   251  
   252  // LowerCaseTableNamesFlavor represents the type of db `lower_case_table_names` settings.
   253  type LowerCaseTableNamesFlavor uint8
   254  
   255  const (
   256  	// LCTableNamesSensitive represent lower_case_table_names = 0, case sensitive.
   257  	LCTableNamesSensitive LowerCaseTableNamesFlavor = 0
   258  	// LCTableNamesInsensitive represent lower_case_table_names = 1, case insensitive.
   259  	LCTableNamesInsensitive = 1
   260  	// LCTableNamesMixed represent lower_case_table_names = 2, table names are case-sensitive, but case-insensitive in usage.
   261  	LCTableNamesMixed = 2
   262  )
   263  
   264  // GetDBCaseSensitive returns the case-sensitive setting of target db.
   265  func GetDBCaseSensitive(ctx context.Context, db *BaseDB) (bool, error) {
   266  	conn, err := db.GetBaseConn(ctx)
   267  	if err != nil {
   268  		return true, terror.DBErrorAdapt(err, db.Scope, terror.ErrDBDriverError)
   269  	}
   270  	defer db.CloseConnWithoutErr(conn)
   271  	lcFlavor, err := FetchLowerCaseTableNamesSetting(ctx, conn)
   272  	if err != nil {
   273  		return true, err
   274  	}
   275  	return lcFlavor == LCTableNamesSensitive, nil
   276  }
   277  
   278  // FetchLowerCaseTableNamesSetting return the `lower_case_table_names` setting of target db.
   279  func FetchLowerCaseTableNamesSetting(ctx context.Context, conn *BaseConn) (LowerCaseTableNamesFlavor, error) {
   280  	query := "SELECT @@lower_case_table_names;"
   281  	row := conn.DBConn.QueryRowContext(ctx, query)
   282  	if row.Err() != nil {
   283  		return LCTableNamesSensitive, terror.ErrDBExecuteFailed.Delegate(row.Err(), query)
   284  	}
   285  	var res uint8
   286  	if err := row.Scan(&res); err != nil {
   287  		return LCTableNamesSensitive, terror.ErrDBExecuteFailed.Delegate(err, query)
   288  	}
   289  	if res > LCTableNamesMixed {
   290  		return LCTableNamesSensitive, terror.ErrDBUnExpect.Generate(fmt.Sprintf("invalid `lower_case_table_names` value '%d'", res))
   291  	}
   292  	return LowerCaseTableNamesFlavor(res), nil
   293  }
   294  
   295  // FetchTableEstimatedBytes returns the estimated size (data + index) in bytes of the table.
   296  func FetchTableEstimatedBytes(ctx context.Context, db *BaseDB, schema string, table string) (int64, error) {
   297  	failpoint.Inject("VeryLargeTable", func(val failpoint.Value) {
   298  		tblName := val.(string)
   299  		if tblName == table {
   300  			failpoint.Return(1<<62, nil)
   301  		}
   302  	})
   303  	var size int64
   304  	err := db.DB.QueryRowContext(ctx, "SELECT data_length + index_length FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?", schema, table).Scan(&size)
   305  	if err != nil {
   306  		return 0, terror.DBErrorAdapt(err, db.Scope, terror.ErrDBDriverError)
   307  	}
   308  	return size, nil
   309  }