vitess.io/vitess@v0.16.2/go/mysql/flavor_mysql.go (about) 1 /* 2 Copyright 2019 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 mysql 18 19 import ( 20 "fmt" 21 "io" 22 "time" 23 24 "context" 25 26 "vitess.io/vitess/go/vt/proto/vtrpc" 27 "vitess.io/vitess/go/vt/vterrors" 28 ) 29 30 // mysqlFlavor implements the Flavor interface for Mysql. 31 type mysqlFlavor struct{} 32 type mysqlFlavor56 struct { 33 mysqlFlavor 34 } 35 type mysqlFlavor57 struct { 36 mysqlFlavor 37 } 38 type mysqlFlavor80 struct { 39 mysqlFlavor 40 } 41 42 var _ flavor = (*mysqlFlavor56)(nil) 43 var _ flavor = (*mysqlFlavor57)(nil) 44 var _ flavor = (*mysqlFlavor80)(nil) 45 46 // primaryGTIDSet is part of the Flavor interface. 47 func (mysqlFlavor) primaryGTIDSet(c *Conn) (GTIDSet, error) { 48 // keep @@global as lowercase, as some servers like the Ripple binlog server only honors a lowercase `global` value 49 qr, err := c.ExecuteFetch("SELECT @@global.gtid_executed", 1, false) 50 if err != nil { 51 return nil, err 52 } 53 if len(qr.Rows) != 1 || len(qr.Rows[0]) != 1 { 54 return nil, vterrors.Errorf(vtrpc.Code_INTERNAL, "unexpected result format for gtid_executed: %#v", qr) 55 } 56 return ParseMysql56GTIDSet(qr.Rows[0][0].ToString()) 57 } 58 59 // purgedGTIDSet is part of the Flavor interface. 60 func (mysqlFlavor) purgedGTIDSet(c *Conn) (GTIDSet, error) { 61 // keep @@global as lowercase, as some servers like the Ripple binlog server only honors a lowercase `global` value 62 qr, err := c.ExecuteFetch("SELECT @@global.gtid_purged", 1, false) 63 if err != nil { 64 return nil, err 65 } 66 if len(qr.Rows) != 1 || len(qr.Rows[0]) != 1 { 67 return nil, vterrors.Errorf(vtrpc.Code_INTERNAL, "unexpected result format for gtid_purged: %#v", qr) 68 } 69 return ParseMysql56GTIDSet(qr.Rows[0][0].ToString()) 70 } 71 72 // serverUUID is part of the Flavor interface. 73 func (mysqlFlavor) serverUUID(c *Conn) (string, error) { 74 // keep @@global as lowercase, as some servers like the Ripple binlog server only honors a lowercase `global` value 75 qr, err := c.ExecuteFetch("SELECT @@global.server_uuid", 1, false) 76 if err != nil { 77 return "", err 78 } 79 if len(qr.Rows) != 1 || len(qr.Rows[0]) != 1 { 80 return "", vterrors.Errorf(vtrpc.Code_INTERNAL, "unexpected result format for server_uuid: %#v", qr) 81 } 82 return qr.Rows[0][0].ToString(), nil 83 } 84 85 // gtidMode is part of the Flavor interface. 86 func (mysqlFlavor) gtidMode(c *Conn) (string, error) { 87 qr, err := c.ExecuteFetch("select @@global.gtid_mode", 1, false) 88 if err != nil { 89 return "", err 90 } 91 if len(qr.Rows) != 1 || len(qr.Rows[0]) != 1 { 92 return "", vterrors.Errorf(vtrpc.Code_INTERNAL, "unexpected result format for gtid_mode: %#v", qr) 93 } 94 return qr.Rows[0][0].ToString(), nil 95 } 96 97 func (mysqlFlavor) startReplicationCommand() string { 98 return "START SLAVE" 99 } 100 101 func (mysqlFlavor) restartReplicationCommands() []string { 102 return []string{ 103 "STOP SLAVE", 104 "RESET SLAVE", 105 "START SLAVE", 106 } 107 } 108 109 func (mysqlFlavor) startReplicationUntilAfter(pos Position) string { 110 return fmt.Sprintf("START SLAVE UNTIL SQL_AFTER_GTIDS = '%s'", pos) 111 } 112 113 func (mysqlFlavor) startSQLThreadUntilAfter(pos Position) string { 114 return fmt.Sprintf("START SLAVE SQL_THREAD UNTIL SQL_AFTER_GTIDS = '%s'", pos) 115 } 116 117 func (mysqlFlavor) stopReplicationCommand() string { 118 return "STOP SLAVE" 119 } 120 121 func (mysqlFlavor) stopIOThreadCommand() string { 122 return "STOP SLAVE IO_THREAD" 123 } 124 125 func (mysqlFlavor) stopSQLThreadCommand() string { 126 return "STOP SLAVE SQL_THREAD" 127 } 128 129 func (mysqlFlavor) startSQLThreadCommand() string { 130 return "START SLAVE SQL_THREAD" 131 } 132 133 // sendBinlogDumpCommand is part of the Flavor interface. 134 func (mysqlFlavor) sendBinlogDumpCommand(c *Conn, serverID uint32, binlogFilename string, startPos Position) error { 135 gtidSet, ok := startPos.GTIDSet.(Mysql56GTIDSet) 136 if !ok { 137 return vterrors.Errorf(vtrpc.Code_INTERNAL, "startPos.GTIDSet is wrong type - expected Mysql56GTIDSet, got: %#v", startPos.GTIDSet) 138 } 139 140 // Build the command. 141 sidBlock := gtidSet.SIDBlock() 142 return c.WriteComBinlogDumpGTID(serverID, binlogFilename, 4, 0, sidBlock) 143 } 144 145 // resetReplicationCommands is part of the Flavor interface. 146 func (mysqlFlavor) resetReplicationCommands(c *Conn) []string { 147 resetCommands := []string{ 148 "STOP SLAVE", 149 "RESET SLAVE ALL", // "ALL" makes it forget source host:port. 150 "RESET MASTER", // This will also clear gtid_executed and gtid_purged. 151 } 152 if c.SemiSyncExtensionLoaded() { 153 resetCommands = append(resetCommands, "SET GLOBAL rpl_semi_sync_master_enabled = false, GLOBAL rpl_semi_sync_slave_enabled = false") // semi-sync will be enabled if needed when replica is started. 154 } 155 return resetCommands 156 } 157 158 // resetReplicationParametersCommands is part of the Flavor interface. 159 func (mysqlFlavor) resetReplicationParametersCommands(c *Conn) []string { 160 resetCommands := []string{ 161 "RESET SLAVE ALL", // "ALL" makes it forget source host:port. 162 } 163 return resetCommands 164 } 165 166 // setReplicationPositionCommands is part of the Flavor interface. 167 func (mysqlFlavor) setReplicationPositionCommands(pos Position) []string { 168 return []string{ 169 "RESET MASTER", // We must clear gtid_executed before setting gtid_purged. 170 fmt.Sprintf("SET GLOBAL gtid_purged = '%s'", pos), 171 } 172 } 173 174 // setReplicationPositionCommands is part of the Flavor interface. 175 func (mysqlFlavor) changeReplicationSourceArg() string { 176 return "MASTER_AUTO_POSITION = 1" 177 } 178 179 // status is part of the Flavor interface. 180 func (mysqlFlavor) status(c *Conn) (ReplicationStatus, error) { 181 qr, err := c.ExecuteFetch("SHOW SLAVE STATUS", 100, true /* wantfields */) 182 if err != nil { 183 return ReplicationStatus{}, err 184 } 185 if len(qr.Rows) == 0 { 186 // The query returned no data, meaning the server 187 // is not configured as a replica. 188 return ReplicationStatus{}, ErrNotReplica 189 } 190 191 resultMap, err := resultToMap(qr) 192 if err != nil { 193 return ReplicationStatus{}, err 194 } 195 196 return parseMysqlReplicationStatus(resultMap) 197 } 198 199 func parseMysqlReplicationStatus(resultMap map[string]string) (ReplicationStatus, error) { 200 status := parseReplicationStatus(resultMap) 201 uuidString := resultMap["Master_UUID"] 202 if uuidString != "" { 203 sid, err := ParseSID(uuidString) 204 if err != nil { 205 return ReplicationStatus{}, vterrors.Wrapf(err, "cannot decode SourceUUID") 206 } 207 status.SourceUUID = sid 208 } 209 210 var err error 211 status.Position.GTIDSet, err = ParseMysql56GTIDSet(resultMap["Executed_Gtid_Set"]) 212 if err != nil { 213 return ReplicationStatus{}, vterrors.Wrapf(err, "ReplicationStatus can't parse MySQL 5.6 GTID (Executed_Gtid_Set: %#v)", resultMap["Executed_Gtid_Set"]) 214 } 215 relayLogGTIDSet, err := ParseMysql56GTIDSet(resultMap["Retrieved_Gtid_Set"]) 216 if err != nil { 217 return ReplicationStatus{}, vterrors.Wrapf(err, "ReplicationStatus can't parse MySQL 5.6 GTID (Retrieved_Gtid_Set: %#v)", resultMap["Retrieved_Gtid_Set"]) 218 } 219 // We take the union of the executed and retrieved gtidset, because the retrieved gtidset only represents GTIDs since 220 // the relay log has been reset. To get the full Position, we need to take a union of executed GTIDSets, since these would 221 // have been in the relay log's GTIDSet in the past, prior to a reset. 222 status.RelayLogPosition.GTIDSet = status.Position.GTIDSet.Union(relayLogGTIDSet) 223 224 return status, nil 225 } 226 227 // primaryStatus is part of the Flavor interface. 228 func (mysqlFlavor) primaryStatus(c *Conn) (PrimaryStatus, error) { 229 qr, err := c.ExecuteFetch("SHOW MASTER STATUS", 100, true /* wantfields */) 230 if err != nil { 231 return PrimaryStatus{}, err 232 } 233 if len(qr.Rows) == 0 { 234 // The query returned no data. We don't know how this could happen. 235 return PrimaryStatus{}, ErrNoPrimaryStatus 236 } 237 238 resultMap, err := resultToMap(qr) 239 if err != nil { 240 return PrimaryStatus{}, err 241 } 242 243 return parseMysqlPrimaryStatus(resultMap) 244 } 245 246 func parseMysqlPrimaryStatus(resultMap map[string]string) (PrimaryStatus, error) { 247 status := parsePrimaryStatus(resultMap) 248 249 var err error 250 status.Position.GTIDSet, err = ParseMysql56GTIDSet(resultMap["Executed_Gtid_Set"]) 251 if err != nil { 252 return PrimaryStatus{}, vterrors.Wrapf(err, "PrimaryStatus can't parse MySQL 5.6 GTID (Executed_Gtid_Set: %#v)", resultMap["Executed_Gtid_Set"]) 253 } 254 255 return status, nil 256 } 257 258 // waitUntilPositionCommand is part of the Flavor interface. 259 260 // waitUntilPositionCommand is part of the Flavor interface. 261 func (mysqlFlavor) waitUntilPositionCommand(ctx context.Context, pos Position) (string, error) { 262 // A timeout of 0 means wait indefinitely. 263 timeoutSeconds := 0 264 if deadline, ok := ctx.Deadline(); ok { 265 timeout := time.Until(deadline) 266 if timeout <= 0 { 267 return "", vterrors.Errorf(vtrpc.Code_DEADLINE_EXCEEDED, "timed out waiting for position %v", pos) 268 } 269 270 // Only whole numbers of seconds are supported. 271 timeoutSeconds = int(timeout.Seconds()) 272 if timeoutSeconds == 0 { 273 // We don't want a timeout <1.0s to truncate down to become infinite. 274 timeoutSeconds = 1 275 } 276 } 277 278 return fmt.Sprintf("SELECT WAIT_UNTIL_SQL_THREAD_AFTER_GTIDS('%s', %v)", pos, timeoutSeconds), nil 279 } 280 281 // readBinlogEvent is part of the Flavor interface. 282 func (mysqlFlavor) readBinlogEvent(c *Conn) (BinlogEvent, error) { 283 result, err := c.ReadPacket() 284 if err != nil { 285 return nil, err 286 } 287 switch result[0] { 288 case EOFPacket: 289 return nil, NewSQLError(CRServerLost, SSUnknownSQLState, "%v", io.EOF) 290 case ErrPacket: 291 return nil, ParseErrorPacket(result) 292 } 293 buf, semiSyncAckRequested, err := c.AnalyzeSemiSyncAckRequest(result[1:]) 294 if err != nil { 295 return nil, err 296 } 297 ev := NewMysql56BinlogEventWithSemiSyncInfo(buf, semiSyncAckRequested) 298 return ev, nil 299 } 300 301 // enableBinlogPlaybackCommand is part of the Flavor interface. 302 func (mysqlFlavor) enableBinlogPlaybackCommand() string { 303 return "" 304 } 305 306 // disableBinlogPlaybackCommand is part of the Flavor interface. 307 func (mysqlFlavor) disableBinlogPlaybackCommand() string { 308 return "" 309 } 310 311 // baseShowTables is part of the Flavor interface. 312 func (mysqlFlavor) baseShowTables() string { 313 return "SELECT table_name, table_type, unix_timestamp(create_time), table_comment FROM information_schema.tables WHERE table_schema = database()" 314 } 315 316 // TablesWithSize56 is a query to select table along with size for mysql 5.6 317 const TablesWithSize56 = `SELECT table_name, 318 table_type, 319 UNIX_TIMESTAMP(create_time) AS uts_create_time, 320 table_comment, 321 SUM(data_length + index_length), 322 SUM(data_length + index_length) 323 FROM information_schema.tables 324 WHERE table_schema = database() 325 GROUP BY table_name, 326 table_type, 327 uts_create_time, 328 table_comment` 329 330 // TablesWithSize57 is a query to select table along with size for mysql 5.7. 331 // 332 // It's a little weird, because the JOIN predicate only works if the table and databases do not contain weird characters. 333 // If the join does not return any data, we fall back to the same fields as used in the mysql 5.6 query. 334 // 335 // We join with a subquery that materializes the data from `information_schema.innodb_sys_tablespaces` 336 // early for performance reasons. This effectively causes only a single read of `information_schema.innodb_sys_tablespaces` 337 // per query. 338 const TablesWithSize57 = `SELECT t.table_name, 339 t.table_type, 340 UNIX_TIMESTAMP(t.create_time), 341 t.table_comment, 342 IFNULL(SUM(i.file_size), SUM(t.data_length + t.index_length)), 343 IFNULL(SUM(i.allocated_size), SUM(t.data_length + t.index_length)) 344 FROM information_schema.tables t 345 LEFT OUTER JOIN ( 346 SELECT space, file_size, allocated_size, name 347 FROM information_schema.innodb_sys_tablespaces 348 WHERE name LIKE CONCAT(database(), '/%') 349 GROUP BY space, file_size, allocated_size, name 350 ) i ON i.name = CONCAT(t.table_schema, '/', t.table_name) or i.name LIKE CONCAT(t.table_schema, '/', t.table_name, '#p#%') 351 WHERE t.table_schema = database() 352 GROUP BY t.table_name, t.table_type, t.create_time, t.table_comment` 353 354 // TablesWithSize80 is a query to select table along with size for mysql 8.0 355 // 356 // We join with a subquery that materializes the data from `information_schema.innodb_sys_tablespaces` 357 // early for performance reasons. This effectively causes only a single read of `information_schema.innodb_tablespaces` 358 // per query. 359 const TablesWithSize80 = `SELECT t.table_name, 360 t.table_type, 361 UNIX_TIMESTAMP(t.create_time), 362 t.table_comment, 363 SUM(i.file_size), 364 SUM(i.allocated_size) 365 FROM information_schema.tables t 366 LEFT JOIN information_schema.innodb_tablespaces i 367 ON i.name LIKE CONCAT(database(), '/%') AND (i.name = CONCAT(t.table_schema, '/', t.table_name) OR i.name LIKE CONCAT(t.table_schema, '/', t.table_name, '#p#%')) 368 WHERE t.table_schema = database() 369 GROUP BY t.table_name, t.table_type, t.create_time, t.table_comment` 370 371 // baseShowTablesWithSizes is part of the Flavor interface. 372 func (mysqlFlavor56) baseShowTablesWithSizes() string { 373 return TablesWithSize56 374 } 375 376 // supportsCapability is part of the Flavor interface. 377 func (mysqlFlavor56) supportsCapability(serverVersion string, capability FlavorCapability) (bool, error) { 378 switch capability { 379 default: 380 return false, nil 381 } 382 } 383 384 // baseShowTablesWithSizes is part of the Flavor interface. 385 func (mysqlFlavor57) baseShowTablesWithSizes() string { 386 return TablesWithSize57 387 } 388 389 // supportsCapability is part of the Flavor interface. 390 func (mysqlFlavor57) supportsCapability(serverVersion string, capability FlavorCapability) (bool, error) { 391 switch capability { 392 case MySQLJSONFlavorCapability: 393 return true, nil 394 default: 395 return false, nil 396 } 397 } 398 399 // baseShowTablesWithSizes is part of the Flavor interface. 400 func (mysqlFlavor80) baseShowTablesWithSizes() string { 401 return TablesWithSize80 402 } 403 404 // supportsCapability is part of the Flavor interface. 405 func (mysqlFlavor80) supportsCapability(serverVersion string, capability FlavorCapability) (bool, error) { 406 switch capability { 407 case InstantDDLFlavorCapability, 408 InstantExpandEnumCapability, 409 InstantAddLastColumnFlavorCapability, 410 InstantAddDropVirtualColumnFlavorCapability, 411 InstantChangeColumnDefaultFlavorCapability: 412 return true, nil 413 case InstantAddDropColumnFlavorCapability: 414 return ServerVersionAtLeast(serverVersion, 8, 0, 29) 415 case TransactionalGtidExecutedFlavorCapability: 416 return ServerVersionAtLeast(serverVersion, 8, 0, 17) 417 case FastDropTableFlavorCapability: 418 return ServerVersionAtLeast(serverVersion, 8, 0, 23) 419 case MySQLJSONFlavorCapability: 420 return true, nil 421 case MySQLUpgradeInServerFlavorCapability: 422 return ServerVersionAtLeast(serverVersion, 8, 0, 16) 423 case DynamicRedoLogCapacityFlavorCapability: 424 return ServerVersionAtLeast(serverVersion, 8, 0, 30) 425 case DisableRedoLogFlavorCapability: 426 return ServerVersionAtLeast(serverVersion, 8, 0, 21) 427 default: 428 return false, nil 429 } 430 }