github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/sqle/binlogreplication/binlog_replica_applier.go (about)

     1  // Copyright 2023 Dolthub, 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  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package binlogreplication
    16  
    17  import (
    18  	"fmt"
    19  	"io"
    20  	"strconv"
    21  	"strings"
    22  	"sync/atomic"
    23  	"time"
    24  
    25  	gms "github.com/dolthub/go-mysql-server"
    26  	"github.com/dolthub/go-mysql-server/sql"
    27  	"github.com/dolthub/go-mysql-server/sql/binlogreplication"
    28  	"github.com/dolthub/go-mysql-server/sql/mysql_db"
    29  	"github.com/dolthub/go-mysql-server/sql/planbuilder"
    30  	"github.com/dolthub/go-mysql-server/sql/rowexec"
    31  	"github.com/dolthub/go-mysql-server/sql/types"
    32  	"github.com/dolthub/vitess/go/mysql"
    33  	"github.com/dolthub/vitess/go/sqltypes"
    34  	vquery "github.com/dolthub/vitess/go/vt/proto/query"
    35  	"github.com/sirupsen/logrus"
    36  
    37  	"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
    38  	"github.com/dolthub/dolt/go/libraries/doltcore/env"
    39  	"github.com/dolthub/dolt/go/libraries/doltcore/sqle"
    40  	"github.com/dolthub/dolt/go/libraries/doltcore/sqle/dsess"
    41  	"github.com/dolthub/dolt/go/libraries/doltcore/sqle/writer"
    42  	"github.com/dolthub/dolt/go/libraries/doltcore/sqlserver"
    43  )
    44  
    45  // positionStore is a singleton instance for loading/saving binlog position state to disk for durable storage.
    46  var positionStore = &binlogPositionStore{}
    47  
    48  const (
    49  	ERNetReadError      = 1158
    50  	ERFatalReplicaError = 13117
    51  )
    52  
    53  // binlogReplicaApplier represents the process that applies updates from a binlog connection.
    54  //
    55  // This type is NOT used concurrently – there is currently only one single applier process running to process binlog
    56  // events, so the state in this type is NOT protected with a mutex.
    57  type binlogReplicaApplier struct {
    58  	format                *mysql.BinlogFormat
    59  	tableMapsById         map[uint64]*mysql.TableMap
    60  	stopReplicationChan   chan struct{}
    61  	currentGtid           mysql.GTID
    62  	replicationSourceUuid string
    63  	currentPosition       *mysql.Position // successfully executed GTIDs
    64  	filters               *filterConfiguration
    65  	running               atomic.Bool
    66  }
    67  
    68  func newBinlogReplicaApplier(filters *filterConfiguration) *binlogReplicaApplier {
    69  	return &binlogReplicaApplier{
    70  		tableMapsById:       make(map[uint64]*mysql.TableMap),
    71  		stopReplicationChan: make(chan struct{}),
    72  		filters:             filters,
    73  	}
    74  }
    75  
    76  // Row Flags – https://mariadb.com/kb/en/rows_event_v1v2-rows_compressed_event_v1/
    77  
    78  // rowFlag_endOfStatement indicates that a row event with this flag set is the last event in a statement.
    79  const rowFlag_endOfStatement = 0x0001
    80  const rowFlag_noForeignKeyChecks = 0x0002
    81  const rowFlag_noUniqueKeyChecks = 0x0004
    82  const rowFlag_noCheckConstraints = 0x0010
    83  
    84  // rowFlag_rowsAreComplete indicates that rows in this event are complete, and contain values for all columns of the table.
    85  const rowFlag_rowsAreComplete = 0x0008
    86  
    87  // Go spawns a new goroutine to run the applier's binlog event handler.
    88  func (a *binlogReplicaApplier) Go(ctx *sql.Context) {
    89  	go func() {
    90  		a.running.Store(true)
    91  		err := a.replicaBinlogEventHandler(ctx)
    92  		a.running.Store(false)
    93  		if err != nil {
    94  			ctx.GetLogger().Errorf("unexpected error of type %T: '%v'", err, err.Error())
    95  			DoltBinlogReplicaController.setSqlError(mysql.ERUnknownError, err.Error())
    96  		}
    97  	}()
    98  }
    99  
   100  // IsRunning returns true if this binlog applier is running and has not been stopped, otherwise returns false.
   101  func (a *binlogReplicaApplier) IsRunning() bool {
   102  	return a.running.Load()
   103  }
   104  
   105  // connectAndStartReplicationEventStream connects to the configured MySQL replication source, including pausing
   106  // and retrying if errors are encountered.
   107  func (a *binlogReplicaApplier) connectAndStartReplicationEventStream(ctx *sql.Context) (*mysql.Conn, error) {
   108  	var maxConnectionAttempts uint64
   109  	var connectRetryDelay uint32
   110  	DoltBinlogReplicaController.updateStatus(func(status *binlogreplication.ReplicaStatus) {
   111  		status.ReplicaIoRunning = binlogreplication.ReplicaIoConnecting
   112  		status.ReplicaSqlRunning = binlogreplication.ReplicaSqlRunning
   113  		maxConnectionAttempts = status.SourceRetryCount
   114  		connectRetryDelay = status.ConnectRetry
   115  	})
   116  
   117  	var conn *mysql.Conn
   118  	var err error
   119  	for connectionAttempts := uint64(0); ; connectionAttempts++ {
   120  		replicaSourceInfo, err := loadReplicationConfiguration(ctx)
   121  
   122  		if replicaSourceInfo == nil {
   123  			err = ErrServerNotConfiguredAsReplica
   124  			DoltBinlogReplicaController.setIoError(ERFatalReplicaError, err.Error())
   125  			return nil, err
   126  		} else if replicaSourceInfo.Uuid != "" {
   127  			a.replicationSourceUuid = replicaSourceInfo.Uuid
   128  		}
   129  
   130  		if replicaSourceInfo.Host == "" {
   131  			DoltBinlogReplicaController.setIoError(ERFatalReplicaError, ErrEmptyHostname.Error())
   132  			return nil, ErrEmptyHostname
   133  		} else if replicaSourceInfo.User == "" {
   134  			DoltBinlogReplicaController.setIoError(ERFatalReplicaError, ErrEmptyUsername.Error())
   135  			return nil, ErrEmptyUsername
   136  		}
   137  
   138  		connParams := mysql.ConnParams{
   139  			Host:             replicaSourceInfo.Host,
   140  			Port:             int(replicaSourceInfo.Port),
   141  			Uname:            replicaSourceInfo.User,
   142  			Pass:             replicaSourceInfo.Password,
   143  			ConnectTimeoutMs: 4_000,
   144  		}
   145  
   146  		conn, err = mysql.Connect(ctx, &connParams)
   147  		if err != nil {
   148  			if connectionAttempts >= maxConnectionAttempts {
   149  				ctx.GetLogger().Errorf("Exceeded max connection attempts (%d) to source server", maxConnectionAttempts)
   150  				return nil, err
   151  			}
   152  			// If there was an error connecting (and we haven't used up all our retry attempts), listen for a
   153  			// STOP REPLICA signal or for the retry delay timer to fire. We need to use select here so that we don't
   154  			// block on our retry backoff and ignore the STOP REPLICA signal for a long time.
   155  			select {
   156  			case <-a.stopReplicationChan:
   157  				ctx.GetLogger().Debugf("Received stop replication signal while trying to connect")
   158  				return nil, ErrReplicationStopped
   159  			case <-time.After(time.Duration(connectRetryDelay) * time.Second):
   160  				// Nothing to do here if our timer completes; just fall through
   161  			}
   162  		} else {
   163  			break
   164  		}
   165  	}
   166  
   167  	// Request binlog events to start
   168  	// TODO: This should also have retry logic
   169  	err = a.startReplicationEventStream(ctx, conn)
   170  	if err != nil {
   171  		return nil, err
   172  	}
   173  
   174  	DoltBinlogReplicaController.updateStatus(func(status *binlogreplication.ReplicaStatus) {
   175  		status.ReplicaIoRunning = binlogreplication.ReplicaIoRunning
   176  	})
   177  
   178  	return conn, nil
   179  }
   180  
   181  // startReplicationEventStream sends a request over |conn|, the connection to the MySQL source server, to begin
   182  // sending binlog events.
   183  func (a *binlogReplicaApplier) startReplicationEventStream(ctx *sql.Context, conn *mysql.Conn) error {
   184  	serverId, err := loadReplicaServerId()
   185  	if err != nil {
   186  		return err
   187  	}
   188  
   189  	position, err := positionStore.Load(ctx)
   190  	if err != nil {
   191  		return err
   192  	}
   193  
   194  	if position == nil {
   195  		// If the positionStore doesn't have a record of executed GTIDs, check to see if the gtid_purged system
   196  		// variable is set. If it holds a GTIDSet, then we use that as our starting position. As part of loading
   197  		// a mysqldump onto a replica, gtid_purged will be set to indicate where to start replication.
   198  		_, value, ok := sql.SystemVariables.GetGlobal("gtid_purged")
   199  		gtidPurged, isString := value.(string)
   200  		if ok && value != nil && isString {
   201  			// Starting in MySQL 8.0, when setting the GTID_PURGED sys variable, if the new value starts with '+', then
   202  			// the specified GTID Set value is added to the current GTID Set value to get a new GTID Set that contains
   203  			// all the previous GTIDs, plus the new ones from the current assignment. Dolt doesn't support this
   204  			// special behavior for appending to GTID Sets yet, so in this case the GTID_PURGED sys var will end up
   205  			// with a "+" prefix. For now, just ignore the "+" prefix if we see it.
   206  			// https://dev.mysql.com/doc/refman/8.0/en/replication-options-gtids.html#sysvar_gtid_purged
   207  			if strings.HasPrefix(gtidPurged, "+") {
   208  				ctx.GetLogger().Warnf("Ignoring unsupported '+' prefix on @@GTID_PURGED value")
   209  				gtidPurged = gtidPurged[1:]
   210  			}
   211  
   212  			purged, err := mysql.ParsePosition(mysqlFlavor, gtidPurged)
   213  			if err != nil {
   214  				return err
   215  			}
   216  			position = &purged
   217  		}
   218  	}
   219  
   220  	if position == nil {
   221  		// If we still don't have any record of executed GTIDs, we create a GTIDSet with just one transaction ID
   222  		// for the 0000 server ID. There doesn't seem to be a cleaner way of saying "start at the very beginning".
   223  		//
   224  		// Also... "starting position" is a bit of a misnomer – it's actually the processed GTIDs, which
   225  		// indicate the NEXT GTID where replication should start, but it's not as direct as specifying
   226  		// a starting position, like the Vitess function signature seems to suggest.
   227  		gtid := mysql.Mysql56GTID{
   228  			Sequence: 1,
   229  		}
   230  		position = &mysql.Position{GTIDSet: gtid.GTIDSet()}
   231  	}
   232  
   233  	a.currentPosition = position
   234  
   235  	// Clear out the format description in case we're reconnecting, so that we don't use the old format description
   236  	// to interpret any event messages before we receive the new format description from the new stream.
   237  	a.format = nil
   238  
   239  	// If the source server has binlog checksums enabled (@@global.binlog_checksum), then the replica MUST
   240  	// set @master_binlog_checksum to handshake with the server to acknowledge that it knows that checksums
   241  	// are in use. Without this step, the server will just send back error messages saying that the replica
   242  	// does not support the binlog checksum algorithm in use on the primary.
   243  	// For more details, see: https://dev.mysql.com/worklog/task/?id=2540
   244  	_, err = conn.ExecuteFetch("set @master_binlog_checksum=@@global.binlog_checksum;", 0, false)
   245  	if err != nil {
   246  		return err
   247  	}
   248  
   249  	return conn.SendBinlogDumpCommand(serverId, *position)
   250  }
   251  
   252  // replicaBinlogEventHandler runs a loop, processing binlog events until the applier's stop replication channel
   253  // receives a signal to stop.
   254  func (a *binlogReplicaApplier) replicaBinlogEventHandler(ctx *sql.Context) error {
   255  	server := sqlserver.GetRunningServer()
   256  	if server == nil {
   257  		return fmt.Errorf("unable to access a running SQL server")
   258  	}
   259  	engine := server.Engine
   260  
   261  	var conn *mysql.Conn
   262  	var eventProducer *binlogEventProducer
   263  
   264  	// Process binlog events
   265  	for {
   266  		if conn == nil {
   267  			ctx.GetLogger().Debug("no binlog connection to source, attempting to establish one")
   268  			if eventProducer != nil {
   269  				eventProducer.Stop()
   270  			}
   271  
   272  			var err error
   273  			if conn, err = a.connectAndStartReplicationEventStream(ctx); err == ErrReplicationStopped {
   274  				return nil
   275  			} else if err != nil {
   276  				return err
   277  			}
   278  			eventProducer = newBinlogEventProducer(conn)
   279  			eventProducer.Go(ctx)
   280  		}
   281  
   282  		select {
   283  		case event := <-eventProducer.EventChan():
   284  			err := a.processBinlogEvent(ctx, engine, event)
   285  			if err != nil {
   286  				ctx.GetLogger().Errorf("unexpected error of type %T: '%v'", err, err.Error())
   287  				DoltBinlogReplicaController.setSqlError(mysql.ERUnknownError, err.Error())
   288  			}
   289  
   290  		case err := <-eventProducer.ErrorChan():
   291  			if sqlError, isSqlError := err.(*mysql.SQLError); isSqlError {
   292  				badConnection := sqlError.Message == io.EOF.Error() ||
   293  					strings.HasPrefix(sqlError.Message, io.ErrUnexpectedEOF.Error())
   294  				if badConnection {
   295  					DoltBinlogReplicaController.updateStatus(func(status *binlogreplication.ReplicaStatus) {
   296  						status.LastIoError = sqlError.Message
   297  						status.LastIoErrNumber = ERNetReadError
   298  						currentTime := time.Now()
   299  						status.LastIoErrorTimestamp = &currentTime
   300  					})
   301  					eventProducer.Stop()
   302  					eventProducer = nil
   303  					conn.Close()
   304  					conn = nil
   305  				}
   306  			} else {
   307  				// otherwise, log the error if it's something we don't expect and continue
   308  				ctx.GetLogger().Errorf("unexpected error of type %T: '%v'", err, err.Error())
   309  				DoltBinlogReplicaController.setIoError(mysql.ERUnknownError, err.Error())
   310  			}
   311  
   312  		case <-a.stopReplicationChan:
   313  			ctx.GetLogger().Trace("received stop replication signal")
   314  			eventProducer.Stop()
   315  			return nil
   316  		}
   317  	}
   318  }
   319  
   320  // processBinlogEvent processes a single binlog event message and returns an error if there were any problems
   321  // processing it.
   322  func (a *binlogReplicaApplier) processBinlogEvent(ctx *sql.Context, engine *gms.Engine, event mysql.BinlogEvent) error {
   323  	var err error
   324  	createCommit := false
   325  	commitToAllDatabases := false
   326  
   327  	// We don't support checksum validation, so we MUST strip off any checksum bytes if present, otherwise it gets
   328  	// interpreted as part of the payload and corrupts the data. Future checksum sizes, are not guaranteed to be the
   329  	// same size, so we can't strip the checksum until we've seen a valid Format binlog event that definitively
   330  	// tells us if checksums are enabled and what algorithm they use. We can NOT strip the checksum off of
   331  	// FormatDescription events, because FormatDescription always includes a CRC32 checksum, and Vitess depends on
   332  	// those bytes always being present when we parse the event into a FormatDescription type.
   333  	if a.format != nil && event.IsFormatDescription() == false {
   334  		var err error
   335  		event, _, err = event.StripChecksum(*a.format)
   336  		if err != nil {
   337  			msg := fmt.Sprintf("unable to strip checksum from binlog event: '%v'", err.Error())
   338  			ctx.GetLogger().Error(msg)
   339  			DoltBinlogReplicaController.setSqlError(mysql.ERUnknownError, msg)
   340  		}
   341  	}
   342  
   343  	switch {
   344  	case event.IsRand():
   345  		// A RAND_EVENT contains two seed values that set the rand_seed1 and rand_seed2 system variables that are
   346  		// used to compute the random number. For more details, see: https://mariadb.com/kb/en/rand_event/
   347  		// Note: it is written only before a QUERY_EVENT and is NOT used with row-based logging.
   348  		ctx.GetLogger().Debug("Received binlog event: Rand")
   349  
   350  	case event.IsXID():
   351  		// An XID event is generated for a COMMIT of a transaction that modifies one or more tables of an
   352  		// XA-capable storage engine. For more details, see: https://mariadb.com/kb/en/xid_event/
   353  		ctx.GetLogger().Debug("Received binlog event: XID")
   354  		createCommit = true
   355  		commitToAllDatabases = true
   356  
   357  	case event.IsQuery():
   358  		// A Query event represents a statement executed on the source server that should be executed on the
   359  		// replica. Used for all statements with statement-based replication, DDL statements with row-based replication
   360  		// as well as COMMITs for non-transactional engines such as MyISAM.
   361  		// For more details, see: https://mariadb.com/kb/en/query_event/
   362  		query, err := event.Query(*a.format)
   363  		if err != nil {
   364  			return err
   365  		}
   366  		ctx.GetLogger().WithFields(logrus.Fields{
   367  			"database": query.Database,
   368  			"charset":  query.Charset,
   369  			"query":    query.SQL,
   370  			"options":  fmt.Sprintf("0x%x", query.Options),
   371  			"sql_mode": fmt.Sprintf("0x%x", query.SqlMode),
   372  		}).Debug("Received binlog event: Query")
   373  
   374  		// When executing SQL statements sent from the primary, we can't be sure what database was modified unless we
   375  		// look closely at the statement. For example, we could be connected to db01, but executed
   376  		// "create table db02.t (...);" – i.e., looking at query.Database is NOT enough to always determine the correct
   377  		// database that was modified, so instead, we commit to all databases when we see a Query binlog event to
   378  		// avoid issues with correctness, at the cost of being slightly less efficient
   379  		commitToAllDatabases = true
   380  
   381  		if query.Options&mysql.QFlagOptionAutoIsNull > 0 {
   382  			ctx.GetLogger().Tracef("Setting sql_auto_is_null ON")
   383  			ctx.SetSessionVariable(ctx, "sql_auto_is_null", 1)
   384  		} else {
   385  			ctx.GetLogger().Tracef("Setting sql_auto_is_null OFF")
   386  			ctx.SetSessionVariable(ctx, "sql_auto_is_null", 0)
   387  		}
   388  
   389  		if query.Options&mysql.QFlagOptionNotAutocommit > 0 {
   390  			ctx.GetLogger().Tracef("Setting autocommit=0")
   391  			ctx.SetSessionVariable(ctx, "autocommit", 0)
   392  		} else {
   393  			ctx.GetLogger().Tracef("Setting autocommit=1")
   394  			ctx.SetSessionVariable(ctx, "autocommit", 1)
   395  		}
   396  
   397  		if query.Options&mysql.QFlagOptionNoForeignKeyChecks > 0 {
   398  			ctx.GetLogger().Tracef("Setting foreign_key_checks=0")
   399  			ctx.SetSessionVariable(ctx, "foreign_key_checks", 0)
   400  		} else {
   401  			ctx.GetLogger().Tracef("Setting foreign_key_checks=1")
   402  			ctx.SetSessionVariable(ctx, "foreign_key_checks", 1)
   403  		}
   404  
   405  		// NOTE: unique_checks is not currently honored by Dolt
   406  		if query.Options&mysql.QFlagOptionRelaxedUniqueChecks > 0 {
   407  			ctx.GetLogger().Tracef("Setting unique_checks=0")
   408  			ctx.SetSessionVariable(ctx, "unique_checks", 0)
   409  		} else {
   410  			ctx.GetLogger().Tracef("Setting unique_checks=1")
   411  			ctx.SetSessionVariable(ctx, "unique_checks", 1)
   412  		}
   413  
   414  		ctx.SetCurrentDatabase(query.Database)
   415  		executeQueryWithEngine(ctx, engine, query.SQL)
   416  		createCommit = strings.ToLower(query.SQL) != "begin"
   417  
   418  	case event.IsRotate():
   419  		// When a binary log file exceeds the configured size limit, a ROTATE_EVENT is written at the end of the file,
   420  		// pointing to the next file in the sequence. ROTATE_EVENT is generated locally and written to the binary log
   421  		// on the source server and it's also written when a FLUSH LOGS statement occurs on the source server.
   422  		// For more details, see: https://mariadb.com/kb/en/rotate_event/
   423  		ctx.GetLogger().Debug("Received binlog event: Rotate")
   424  
   425  	case event.IsFormatDescription():
   426  		// This is a descriptor event that is written to the beginning of a binary log file, at position 4 (after
   427  		// the 4 magic number bytes). For more details, see: https://mariadb.com/kb/en/format_description_event/
   428  		format, err := event.Format()
   429  		if err != nil {
   430  			return err
   431  		}
   432  		a.format = &format
   433  		ctx.GetLogger().WithFields(logrus.Fields{
   434  			"format":        a.format,
   435  			"formatVersion": a.format.FormatVersion,
   436  			"serverVersion": a.format.ServerVersion,
   437  			"checksum":      a.format.ChecksumAlgorithm,
   438  		}).Debug("Received binlog event: FormatDescription")
   439  
   440  	case event.IsPreviousGTIDs():
   441  		// Logged in every binlog to record the current replication state. Consists of the last GTID seen for each
   442  		// replication domain. For more details, see: https://mariadb.com/kb/en/gtid_list_event/
   443  		position, err := event.PreviousGTIDs(*a.format)
   444  		if err != nil {
   445  			return err
   446  		}
   447  		ctx.GetLogger().WithFields(logrus.Fields{
   448  			"previousGtids": position.GTIDSet.String(),
   449  		}).Debug("Received binlog event: PreviousGTIDs")
   450  
   451  	case event.IsGTID():
   452  		// For global transaction ID, used to start a new transaction event group, instead of the old BEGIN query event,
   453  		// and also to mark stand-alone (ddl). For more details, see: https://mariadb.com/kb/en/gtid_event/
   454  		gtid, isBegin, err := event.GTID(*a.format)
   455  		if err != nil {
   456  			return err
   457  		}
   458  		if isBegin {
   459  			ctx.GetLogger().Errorf("unsupported binlog protocol message: GTID event with 'isBegin' set to true")
   460  		}
   461  		ctx.GetLogger().WithFields(logrus.Fields{
   462  			"gtid":    gtid,
   463  			"isBegin": isBegin,
   464  		}).Debug("Received binlog event: GTID")
   465  		a.currentGtid = gtid
   466  		// if the source's UUID hasn't been set yet, set it and persist it
   467  		if a.replicationSourceUuid == "" {
   468  			uuid := fmt.Sprintf("%v", gtid.SourceServer())
   469  			err = persistSourceUuid(ctx, uuid)
   470  			if err != nil {
   471  				return err
   472  			}
   473  			a.replicationSourceUuid = uuid
   474  		}
   475  
   476  	case event.IsTableMap():
   477  		// Used for row-based binary logging beginning (binlog_format=ROW or MIXED). This event precedes each row
   478  		// operation event and maps a table definition to a number, where the table definition consists of database
   479  		// and table names. For more details, see: https://mariadb.com/kb/en/table_map_event/
   480  		// Note: TableMap events are sent before each row event, so there is no need to persist them between restarts.
   481  		tableId := event.TableID(*a.format)
   482  		tableMap, err := event.TableMap(*a.format)
   483  		if err != nil {
   484  			return err
   485  		}
   486  		ctx.GetLogger().WithFields(logrus.Fields{
   487  			"id":        tableId,
   488  			"tableName": tableMap.Name,
   489  			"database":  tableMap.Database,
   490  			"flags":     convertToHexString(tableMap.Flags),
   491  			"metadata":  tableMap.Metadata,
   492  			"types":     tableMap.Types,
   493  		}).Debug("Received binlog event: TableMap")
   494  
   495  		if tableId == 0xFFFFFF {
   496  			// Table ID 0xFFFFFF is a special value that indicates table maps can be freed.
   497  			ctx.GetLogger().Infof("binlog protocol message: table ID '0xFFFFFF'; clearing table maps")
   498  			a.tableMapsById = make(map[uint64]*mysql.TableMap)
   499  		} else {
   500  			flags := tableMap.Flags
   501  			if flags&rowFlag_endOfStatement == rowFlag_endOfStatement {
   502  				// nothing to be done for end of statement; just clear the flag
   503  				flags = flags &^ rowFlag_endOfStatement
   504  			}
   505  			if flags&rowFlag_noForeignKeyChecks == rowFlag_noForeignKeyChecks {
   506  				flags = flags &^ rowFlag_noForeignKeyChecks
   507  			}
   508  			if flags != 0 {
   509  				msg := fmt.Sprintf("unsupported binlog protocol message: TableMap event with unsupported flags '%x'", flags)
   510  				ctx.GetLogger().Errorf(msg)
   511  				DoltBinlogReplicaController.setSqlError(mysql.ERUnknownError, msg)
   512  			}
   513  			a.tableMapsById[tableId] = tableMap
   514  		}
   515  
   516  	case event.IsDeleteRows(), event.IsWriteRows(), event.IsUpdateRows():
   517  		// A ROWS_EVENT is written for row based replication if data is inserted, deleted or updated.
   518  		// For more details, see: https://mariadb.com/kb/en/rows_event_v1v2-rows_compressed_event_v1/
   519  		err = a.processRowEvent(ctx, event, engine)
   520  		if err != nil {
   521  			return err
   522  		}
   523  
   524  	default:
   525  		// We can't access the bytes directly because these non-interface types in Vitess are not exposed.
   526  		// Having a Bytes() or Type() method on the Vitess interface would let us clean this up.
   527  		byteString := fmt.Sprintf("%v", event)
   528  		if strings.HasPrefix(byteString, "{[0 0 0 0 27 ") {
   529  			// Type 27 is a Heartbeat event. This event does not appear in the binary log. It's only sent over the
   530  			// network by a primary to a replica to let it know that the primary is still alive, and is only sent
   531  			// when the primary has no binlog events to send to replica servers.
   532  			// For more details, see: https://mariadb.com/kb/en/heartbeat_log_event/
   533  			ctx.GetLogger().Debug("Received binlog event: Heartbeat")
   534  		} else {
   535  			return fmt.Errorf("received unknown event: %v", event)
   536  		}
   537  	}
   538  
   539  	if createCommit {
   540  		var databasesToCommit []string
   541  		if commitToAllDatabases {
   542  			databasesToCommit = getAllUserDatabaseNames(ctx, engine)
   543  			for _, database := range databasesToCommit {
   544  				executeQueryWithEngine(ctx, engine, "use `"+database+"`;")
   545  				executeQueryWithEngine(ctx, engine, "commit;")
   546  			}
   547  		}
   548  
   549  		// Record the last GTID processed after the commit
   550  		a.currentPosition.GTIDSet = a.currentPosition.GTIDSet.AddGTID(a.currentGtid)
   551  		err := sql.SystemVariables.AssignValues(map[string]interface{}{"gtid_executed": a.currentPosition.GTIDSet.String()})
   552  		if err != nil {
   553  			ctx.GetLogger().Errorf("unable to set @@GLOBAL.gtid_executed: %s", err.Error())
   554  		}
   555  		err = positionStore.Save(ctx, a.currentPosition)
   556  		if err != nil {
   557  			return fmt.Errorf("unable to store GTID executed metadata to disk: %s", err.Error())
   558  		}
   559  
   560  		// For now, create a Dolt commit from every data update. Eventually, we'll want to make this configurable.
   561  		ctx.GetLogger().Trace("Creating Dolt commit(s)")
   562  		for _, database := range databasesToCommit {
   563  			executeQueryWithEngine(ctx, engine, "use `"+database+"`;")
   564  			executeQueryWithEngine(ctx, engine,
   565  				fmt.Sprintf("call dolt_commit('-Am', 'Dolt binlog replica commit: GTID %s');", a.currentGtid))
   566  		}
   567  	}
   568  
   569  	return nil
   570  }
   571  
   572  // processRowEvent processes a WriteRows, DeleteRows, or UpdateRows binlog event and returns an error if any problems
   573  // were encountered.
   574  func (a *binlogReplicaApplier) processRowEvent(ctx *sql.Context, event mysql.BinlogEvent, engine *gms.Engine) error {
   575  	var eventType string
   576  	switch {
   577  	case event.IsDeleteRows():
   578  		eventType = "DeleteRows"
   579  	case event.IsWriteRows():
   580  		eventType = "WriteRows"
   581  	case event.IsUpdateRows():
   582  		eventType = "UpdateRows"
   583  	default:
   584  		return fmt.Errorf("unsupported event type: %v", event)
   585  	}
   586  	ctx.GetLogger().Debugf("Received binlog event: %s", eventType)
   587  
   588  	tableId := event.TableID(*a.format)
   589  	tableMap, ok := a.tableMapsById[tableId]
   590  	if !ok {
   591  		return fmt.Errorf("unable to find replication metadata for table ID: %d", tableId)
   592  	}
   593  
   594  	if a.filters.isTableFilteredOut(ctx, tableMap) {
   595  		return nil
   596  	}
   597  
   598  	rows, err := event.Rows(*a.format, tableMap)
   599  	if err != nil {
   600  		return err
   601  	}
   602  
   603  	ctx.GetLogger().WithFields(logrus.Fields{
   604  		"flags": fmt.Sprintf("%x", rows.Flags),
   605  	}).Debugf("Processing rows from %s event", eventType)
   606  
   607  	flags := rows.Flags
   608  	foreignKeyChecksDisabled := false
   609  	if flags&rowFlag_endOfStatement > 0 {
   610  		// nothing to be done for end of statement; just clear the flag and move on
   611  		flags = flags &^ rowFlag_endOfStatement
   612  	}
   613  	if flags&rowFlag_noForeignKeyChecks > 0 {
   614  		foreignKeyChecksDisabled = true
   615  		flags = flags &^ rowFlag_noForeignKeyChecks
   616  	}
   617  	if flags != 0 {
   618  		msg := fmt.Sprintf("unsupported binlog protocol message: row event with unsupported flags '%x'", flags)
   619  		ctx.GetLogger().Errorf(msg)
   620  		DoltBinlogReplicaController.setSqlError(mysql.ERUnknownError, msg)
   621  	}
   622  	schema, err := getTableSchema(ctx, engine, tableMap.Name, tableMap.Database)
   623  	if err != nil {
   624  		return err
   625  	}
   626  
   627  	switch {
   628  	case event.IsDeleteRows():
   629  		ctx.GetLogger().Debugf(" - Deleted Rows (table: %s)", tableMap.Name)
   630  	case event.IsUpdateRows():
   631  		ctx.GetLogger().Debugf(" - Updated Rows (table: %s)", tableMap.Name)
   632  	case event.IsWriteRows():
   633  		ctx.GetLogger().Debugf(" - Inserted Rows (table: %s)", tableMap.Name)
   634  	}
   635  
   636  	writeSession, tableWriter, err := getTableWriter(ctx, engine, tableMap.Name, tableMap.Database, foreignKeyChecksDisabled)
   637  	if err != nil {
   638  		return err
   639  	}
   640  
   641  	for _, row := range rows.Rows {
   642  		var identityRow, dataRow sql.Row
   643  		if len(row.Identify) > 0 {
   644  			identityRow, err = parseRow(ctx, tableMap, schema, rows.IdentifyColumns, row.NullIdentifyColumns, row.Identify)
   645  			if err != nil {
   646  				return err
   647  			}
   648  			ctx.GetLogger().Debugf("     - Identity: %v ", sql.FormatRow(identityRow))
   649  		}
   650  
   651  		if len(row.Data) > 0 {
   652  			dataRow, err = parseRow(ctx, tableMap, schema, rows.DataColumns, row.NullColumns, row.Data)
   653  			if err != nil {
   654  				return err
   655  			}
   656  			ctx.GetLogger().Debugf("     - Data: %v ", sql.FormatRow(dataRow))
   657  		}
   658  
   659  		switch {
   660  		case event.IsDeleteRows():
   661  			err = tableWriter.Delete(ctx, identityRow)
   662  		case event.IsWriteRows():
   663  			err = tableWriter.Insert(ctx, dataRow)
   664  		case event.IsUpdateRows():
   665  			err = tableWriter.Update(ctx, identityRow, dataRow)
   666  		}
   667  		if err != nil {
   668  			return err
   669  		}
   670  
   671  	}
   672  
   673  	err = closeWriteSession(ctx, engine, tableMap.Database, writeSession)
   674  	if err != nil {
   675  		return err
   676  	}
   677  
   678  	return nil
   679  }
   680  
   681  //
   682  // Helper functions
   683  //
   684  
   685  // closeWriteSession flushes and closes the specified |writeSession| and returns an error if anything failed.
   686  func closeWriteSession(ctx *sql.Context, engine *gms.Engine, databaseName string, writeSession writer.WriteSession) error {
   687  	newWorkingSet, err := writeSession.Flush(ctx)
   688  	if err != nil {
   689  		return err
   690  	}
   691  
   692  	database, err := engine.Analyzer.Catalog.Database(ctx, databaseName)
   693  	if err != nil {
   694  		return err
   695  	}
   696  	if privDatabase, ok := database.(mysql_db.PrivilegedDatabase); ok {
   697  		database = privDatabase.Unwrap()
   698  	}
   699  	sqlDatabase, ok := database.(sqle.Database)
   700  	if !ok {
   701  		return fmt.Errorf("unexpected database type: %T", database)
   702  	}
   703  
   704  	hash, err := newWorkingSet.HashOf()
   705  	if err != nil {
   706  		return err
   707  	}
   708  
   709  	return sqlDatabase.DbData().Ddb.UpdateWorkingSet(ctx, newWorkingSet.Ref(), newWorkingSet, hash, newWorkingSet.Meta(), nil)
   710  }
   711  
   712  // getTableSchema returns a sql.Schema for the specified table in the specified database.
   713  func getTableSchema(ctx *sql.Context, engine *gms.Engine, tableName, databaseName string) (sql.Schema, error) {
   714  	database, err := engine.Analyzer.Catalog.Database(ctx, databaseName)
   715  	if err != nil {
   716  		return nil, err
   717  	}
   718  	table, ok, err := database.GetTableInsensitive(ctx, tableName)
   719  	if err != nil {
   720  		return nil, err
   721  	}
   722  	if !ok {
   723  		return nil, fmt.Errorf("unable to find table %q", tableName)
   724  	}
   725  
   726  	return table.Schema(), nil
   727  }
   728  
   729  // getTableWriter returns a WriteSession and a TableWriter for writing to the specified |table| in the specified |database|.
   730  func getTableWriter(ctx *sql.Context, engine *gms.Engine, tableName, databaseName string, foreignKeyChecksDisabled bool) (writer.WriteSession, writer.TableWriter, error) {
   731  	database, err := engine.Analyzer.Catalog.Database(ctx, databaseName)
   732  	if err != nil {
   733  		return nil, nil, err
   734  	}
   735  	if privDatabase, ok := database.(mysql_db.PrivilegedDatabase); ok {
   736  		database = privDatabase.Unwrap()
   737  	}
   738  	sqlDatabase, ok := database.(sqle.Database)
   739  	if !ok {
   740  		return nil, nil, fmt.Errorf("unexpected database type: %T", database)
   741  	}
   742  
   743  	binFormat := sqlDatabase.DbData().Ddb.Format()
   744  
   745  	ws, err := env.WorkingSet(ctx, sqlDatabase.GetDoltDB(), sqlDatabase.DbData().Rsr)
   746  	if err != nil {
   747  		return nil, nil, err
   748  	}
   749  
   750  	tracker, err := dsess.NewAutoIncrementTracker(ctx, sqlDatabase.Name(), ws)
   751  	if err != nil {
   752  		return nil, nil, err
   753  	}
   754  
   755  	options := sqlDatabase.EditOptions()
   756  	options.ForeignKeyChecksDisabled = foreignKeyChecksDisabled
   757  	writeSession := writer.NewWriteSession(binFormat, ws, tracker, options)
   758  
   759  	ds := dsess.DSessFromSess(ctx.Session)
   760  	setter := ds.SetWorkingRoot
   761  	tableWriter, err := writeSession.GetTableWriter(ctx, doltdb.TableName{Name: tableName}, databaseName, setter)
   762  	if err != nil {
   763  		return nil, nil, err
   764  	}
   765  
   766  	return writeSession, tableWriter, nil
   767  }
   768  
   769  // parseRow parses the binary row data from a MySQL binlog event and converts it into a go-mysql-server Row using the
   770  // |schema| information provided. |columnsPresentBitmap| indicates which column values are present in |data| and
   771  // |nullValuesBitmap| indicates which columns have null values and are NOT present in |data|.
   772  func parseRow(ctx *sql.Context, tableMap *mysql.TableMap, schema sql.Schema, columnsPresentBitmap, nullValuesBitmap mysql.Bitmap, data []byte) (sql.Row, error) {
   773  	var parsedRow sql.Row
   774  	pos := 0
   775  
   776  	for i, typ := range tableMap.Types {
   777  		column := schema[i]
   778  
   779  		if columnsPresentBitmap.Bit(i) == false {
   780  			parsedRow = append(parsedRow, nil)
   781  			continue
   782  		}
   783  
   784  		var value sqltypes.Value
   785  		var err error
   786  		if nullValuesBitmap.Bit(i) {
   787  			value, err = sqltypes.NewValue(vquery.Type_NULL_TYPE, nil)
   788  			if err != nil {
   789  				return nil, err
   790  			}
   791  		} else {
   792  			var length int
   793  			value, length, err = mysql.CellValue(data, pos, typ, tableMap.Metadata[i], getSignedType(column))
   794  			if err != nil {
   795  				return nil, err
   796  			}
   797  			pos += length
   798  		}
   799  
   800  		convertedValue, err := convertSqlTypesValue(ctx, value, column)
   801  		if err != nil {
   802  			return nil, err
   803  		}
   804  		parsedRow = append(parsedRow, convertedValue)
   805  	}
   806  
   807  	return parsedRow, nil
   808  }
   809  
   810  // getSignedType returns a Vitess query.Type that can be used with the Vitess mysql.CellValue function to correctly
   811  // parse the value of a signed or unsigned integer value. The mysql.TableMap structure provides information about the
   812  // type, but it doesn't indicate if an integer type is signed or unsigned, so we have to look at the column type in the
   813  // replica's schema and then choose any signed/unsigned query.Type to pass into mysql.CellValue to instruct it whether
   814  // to treat a value as signed or unsigned – the actual type does not matter, only the signed/unsigned property.
   815  func getSignedType(column *sql.Column) vquery.Type {
   816  	switch column.Type.Type() {
   817  	case vquery.Type_UINT8, vquery.Type_UINT16, vquery.Type_UINT24, vquery.Type_UINT32, vquery.Type_UINT64:
   818  		// For any unsigned integer value, we just need to return any unsigned numeric type to signal to Vitess to treat
   819  		// the value as unsigned. The actual type returned doesn't matter – only the signed/unsigned property is used.
   820  		return vquery.Type_UINT64
   821  	default:
   822  		return vquery.Type_INT64
   823  	}
   824  }
   825  
   826  // convertSqlTypesValues converts a sqltypes.Value instance (from vitess) into a sql.Type value (for go-mysql-server).
   827  func convertSqlTypesValue(ctx *sql.Context, value sqltypes.Value, column *sql.Column) (interface{}, error) {
   828  	if value.IsNull() {
   829  		return nil, nil
   830  	}
   831  
   832  	var convertedValue interface{}
   833  	var err error
   834  	switch {
   835  	case types.IsEnum(column.Type), types.IsSet(column.Type):
   836  		atoi, err := strconv.Atoi(value.ToString())
   837  		if err != nil {
   838  			return nil, err
   839  		}
   840  		convertedValue, _, err = column.Type.Convert(atoi)
   841  	case types.IsDecimal(column.Type):
   842  		// Decimal values need to have any leading/trailing whitespace trimmed off
   843  		// TODO: Consider moving this into DecimalType_.Convert; if DecimalType_.Convert handled trimming
   844  		//       leading/trailing whitespace, this special case for Decimal types wouldn't be needed.
   845  		convertedValue, _, err = column.Type.Convert(strings.TrimSpace(value.ToString()))
   846  	case types.IsJSON(column.Type):
   847  		convertedValue, err = convertVitessJsonExpressionString(ctx, value)
   848  	default:
   849  		convertedValue, _, err = column.Type.Convert(value.ToString())
   850  	}
   851  	if err != nil {
   852  		return nil, fmt.Errorf("unable to convert value %q, for column of type %T: %v", value.ToString(), column.Type, err.Error())
   853  	}
   854  
   855  	return convertedValue, nil
   856  }
   857  
   858  // convertVitessJsonExpressionString extracts a JSON value from the specified |value| instance, which Vitess has
   859  // encoded as a SQL expression string. Vitess parses the binary JSON representation from an incoming binlog event,
   860  // and converts it into an expression string containing JSON_OBJECT and JSON_ARRAY function calls. Because we don't
   861  // have access to the raw JSON string or JSON bytes, we have to do extra work to translate from Vitess' SQL
   862  // expression syntax into a raw JSON string value that we can pass to the storage layer. If Vitess kept around the
   863  // raw string representation and returned it from value.ToString, this logic would not be necessary.
   864  func convertVitessJsonExpressionString(ctx *sql.Context, value sqltypes.Value) (interface{}, error) {
   865  	if value.Type() != vquery.Type_EXPRESSION {
   866  		return nil, fmt.Errorf("invalid sqltypes.Value specified; expected a Value instance with an Expression type")
   867  	}
   868  
   869  	strValue := value.String()
   870  	if strings.HasPrefix(strValue, "EXPRESSION(") {
   871  		strValue = strValue[len("EXPRESSION(") : len(strValue)-1]
   872  	}
   873  
   874  	server := sqlserver.GetRunningServer()
   875  	if server == nil {
   876  		return nil, fmt.Errorf("unable to access running SQL server")
   877  	}
   878  
   879  	binder := planbuilder.New(ctx, server.Engine.Analyzer.Catalog, server.Engine.Parser)
   880  	node, _, _, err := binder.Parse("SELECT "+strValue, false)
   881  	if err != nil {
   882  		return nil, err
   883  	}
   884  
   885  	analyze, err := server.Engine.Analyzer.Analyze(ctx, node, nil)
   886  	if err != nil {
   887  		return nil, err
   888  	}
   889  
   890  	rowIter, err := rowexec.DefaultBuilder.Build(ctx, analyze, nil)
   891  	if err != nil {
   892  		return nil, err
   893  	}
   894  	row, err := rowIter.Next(ctx)
   895  	if err != nil {
   896  		return nil, err
   897  	}
   898  
   899  	return row[0], nil
   900  }
   901  
   902  func getAllUserDatabaseNames(ctx *sql.Context, engine *gms.Engine) []string {
   903  	allDatabases := engine.Analyzer.Catalog.AllDatabases(ctx)
   904  	userDatabaseNames := make([]string, 0, len(allDatabases))
   905  	for _, database := range allDatabases {
   906  		switch database.Name() {
   907  		case "information_schema", "mysql":
   908  		default:
   909  			userDatabaseNames = append(userDatabaseNames, database.Name())
   910  		}
   911  	}
   912  	return userDatabaseNames
   913  }
   914  
   915  // loadReplicaServerId loads the @@GLOBAL.server_id system variable needed to register the replica with the source,
   916  // and returns an error specific to replication configuration if the variable is not set to a valid value.
   917  func loadReplicaServerId() (uint32, error) {
   918  	_, value, ok := sql.SystemVariables.GetGlobal("server_id")
   919  	if !ok {
   920  		return 0, fmt.Errorf("no server_id global system variable set")
   921  	}
   922  
   923  	serverId, ok := value.(uint32)
   924  	if !ok || serverId == 0 {
   925  		return 0, fmt.Errorf("invalid server ID configured for @@GLOBAL.server_id (%v); "+
   926  			"must be an integer greater than zero and less than 4,294,967,296", serverId)
   927  	}
   928  
   929  	return serverId, nil
   930  }
   931  
   932  func executeQueryWithEngine(ctx *sql.Context, engine *gms.Engine, query string) {
   933  	// Create a sub-context when running queries against the engine, so that we get an accurate query start time.
   934  	queryCtx := sql.NewContext(ctx, sql.WithSession(ctx.Session))
   935  
   936  	if queryCtx.GetCurrentDatabase() == "" {
   937  		ctx.GetLogger().WithFields(logrus.Fields{
   938  			"query": query,
   939  		}).Warn("No current database selected")
   940  	}
   941  
   942  	_, iter, err := engine.Query(queryCtx, query)
   943  	if err != nil {
   944  		// Log any errors, except for commits with "nothing to commit"
   945  		if err.Error() != "nothing to commit" {
   946  			queryCtx.GetLogger().WithFields(logrus.Fields{
   947  				"error": err.Error(),
   948  				"query": query,
   949  			}).Errorf("Error executing query")
   950  			msg := fmt.Sprintf("Error executing query: %v", err.Error())
   951  			DoltBinlogReplicaController.setSqlError(mysql.ERUnknownError, msg)
   952  		}
   953  		return
   954  	}
   955  	for {
   956  		_, err := iter.Next(queryCtx)
   957  		if err != nil {
   958  			if err != io.EOF {
   959  				queryCtx.GetLogger().Errorf("ERROR reading query results: %v ", err.Error())
   960  			}
   961  			return
   962  		}
   963  	}
   964  }
   965  
   966  //
   967  // Generic util functions...
   968  //
   969  
   970  // convertToHexString returns a lower-case hex string representation of the specified uint16 value |v|.
   971  func convertToHexString(v uint16) string {
   972  	return fmt.Sprintf("%x", v)
   973  }
   974  
   975  // keys returns a slice containing the keys in the specified map |m|.
   976  func keys[K comparable, V any](m map[K]V) []K {
   977  	keys := make([]K, 0, len(m))
   978  	for k := range m {
   979  		keys = append(keys, k)
   980  	}
   981  	return keys
   982  }