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  }