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 }