github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/dm/pkg/binlog/event/util.go (about)

     1  // Copyright 2019 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  // binlog events generator for MySQL used to generate some binlog events for tests.
    15  // Readability takes precedence over performance.
    16  
    17  package event
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/binary"
    22  	"errors"
    23  	"fmt"
    24  	"hash/crc32"
    25  	"io"
    26  	"math"
    27  	"reflect"
    28  
    29  	gmysql "github.com/go-mysql-org/go-mysql/mysql"
    30  	"github.com/go-mysql-org/go-mysql/replication"
    31  	"github.com/google/uuid"
    32  	"github.com/pingcap/tidb/pkg/parser"
    33  	"github.com/pingcap/tidb/pkg/parser/charset"
    34  	"github.com/pingcap/tidb/pkg/parser/mysql"
    35  	"github.com/pingcap/tiflow/dm/pkg/terror"
    36  	"golang.org/x/text/encoding"
    37  	"golang.org/x/text/encoding/simplifiedchinese"
    38  )
    39  
    40  // encodeTableMapColumnMeta generates the column_meta_def according to the column_type_def.
    41  // NOTE: we should pass more arguments for some type def later, now simply hard-code them.
    42  // ref: https://dev.mysql.com/doc/internals/en/table-map-event.html
    43  // ref: https://github.com/go-mysql-org/go-mysql/blob/88e9cd7f6643b246b4dcc0e3206e9a169dd0ac96/replication/row_event.go#L100
    44  func encodeTableMapColumnMeta(columnType []byte) ([]byte, error) {
    45  	buf := new(bytes.Buffer)
    46  	for _, t := range columnType {
    47  		switch t {
    48  		case gmysql.MYSQL_TYPE_STRING:
    49  			buf.WriteByte(0xfe) // real type
    50  			buf.WriteByte(0xff) // pack or field length
    51  		case gmysql.MYSQL_TYPE_NEWDECIMAL:
    52  			buf.WriteByte(0x12) // precision, 18
    53  			buf.WriteByte(0x09) // decimals, 9
    54  		case gmysql.MYSQL_TYPE_VAR_STRING, gmysql.MYSQL_TYPE_VARCHAR, gmysql.MYSQL_TYPE_BIT:
    55  			buf.WriteByte(0xff)
    56  			buf.WriteByte(0xff)
    57  		case gmysql.MYSQL_TYPE_BLOB, gmysql.MYSQL_TYPE_DOUBLE, gmysql.MYSQL_TYPE_FLOAT, gmysql.MYSQL_TYPE_GEOMETRY, gmysql.MYSQL_TYPE_JSON,
    58  			gmysql.MYSQL_TYPE_TIME2, gmysql.MYSQL_TYPE_DATETIME2, gmysql.MYSQL_TYPE_TIMESTAMP2:
    59  			buf.WriteByte(0xff)
    60  		case gmysql.MYSQL_TYPE_NEWDATE, gmysql.MYSQL_TYPE_ENUM, gmysql.MYSQL_TYPE_SET, gmysql.MYSQL_TYPE_TINY_BLOB, gmysql.MYSQL_TYPE_MEDIUM_BLOB, gmysql.MYSQL_TYPE_LONG_BLOB:
    61  			return nil, terror.ErrBinlogColumnTypeNotSupport.Generate(t)
    62  		}
    63  	}
    64  	return gmysql.PutLengthEncodedString(buf.Bytes()), nil
    65  }
    66  
    67  // decodeTableMapColumnMeta generates the column_meta_def to uint16 slices.
    68  // ref: https://github.com/go-mysql-org/go-mysql/blob/88e9cd7f6643b246b4dcc0e3206e9a169dd0ac96/replication/row_event.go#L100
    69  func decodeTableMapColumnMeta(data []byte, columnType []byte) ([]uint16, error) {
    70  	pos := 0
    71  	columnMeta := make([]uint16, len(columnType))
    72  	for i, t := range columnType {
    73  		switch t {
    74  		case gmysql.MYSQL_TYPE_STRING:
    75  			x := uint16(data[pos]) << 8 // real type
    76  			x += uint16(data[pos+1])    // pack or field length
    77  			columnMeta[i] = x
    78  			pos += 2
    79  		case gmysql.MYSQL_TYPE_NEWDECIMAL:
    80  			x := uint16(data[pos]) << 8 // precision
    81  			x += uint16(data[pos+1])    // decimals
    82  			columnMeta[i] = x
    83  			pos += 2
    84  		case gmysql.MYSQL_TYPE_VAR_STRING, gmysql.MYSQL_TYPE_VARCHAR, gmysql.MYSQL_TYPE_BIT:
    85  			columnMeta[i] = binary.LittleEndian.Uint16(data[pos:])
    86  			pos += 2
    87  		case gmysql.MYSQL_TYPE_BLOB, gmysql.MYSQL_TYPE_DOUBLE, gmysql.MYSQL_TYPE_FLOAT, gmysql.MYSQL_TYPE_GEOMETRY, gmysql.MYSQL_TYPE_JSON,
    88  			gmysql.MYSQL_TYPE_TIME2, gmysql.MYSQL_TYPE_DATETIME2, gmysql.MYSQL_TYPE_TIMESTAMP2:
    89  			columnMeta[i] = uint16(data[pos])
    90  			pos++
    91  		case gmysql.MYSQL_TYPE_NEWDATE, gmysql.MYSQL_TYPE_ENUM, gmysql.MYSQL_TYPE_SET, gmysql.MYSQL_TYPE_TINY_BLOB, gmysql.MYSQL_TYPE_MEDIUM_BLOB, gmysql.MYSQL_TYPE_LONG_BLOB:
    92  			return nil, terror.ErrBinlogColumnTypeNotSupport.Generate(t)
    93  		default:
    94  			columnMeta[i] = 0
    95  		}
    96  	}
    97  
    98  	return columnMeta, nil
    99  }
   100  
   101  // bitmapByteSize returns the byte length of bitmap for columnCount.
   102  func bitmapByteSize(columnCount int) int {
   103  	return (columnCount + 7) / 8
   104  }
   105  
   106  // nullBytes returns a n-length null bytes slice.
   107  func nullBytes(n int) []byte {
   108  	return make([]byte, n)
   109  }
   110  
   111  // fullBytes returns a n-length full bytes slice (all bits are set).
   112  func fullBytes(n int) []byte {
   113  	buf := new(bytes.Buffer)
   114  	for i := 0; i < n; i++ {
   115  		buf.WriteByte(0xff)
   116  	}
   117  	return buf.Bytes()
   118  }
   119  
   120  // assembleEvent assembles header fields, postHeader and payload together to an event.
   121  // header: pass as a struct to make a copy
   122  func assembleEvent(
   123  	buf *bytes.Buffer,
   124  	event replication.Event,
   125  	decodeWithChecksum bool,
   126  	header replication.EventHeader,
   127  	eventType replication.EventType,
   128  	latestPos uint32,
   129  	postHeader, payload []byte,
   130  ) (*replication.BinlogEvent, error) {
   131  	eventSize := uint32(eventHeaderLen) + uint32(len(postHeader)) + uint32(len(payload)) + crc32Len
   132  	// update some fields in header
   133  	header.EventSize = eventSize
   134  	header.LogPos = latestPos + eventSize
   135  	header.EventType = eventType
   136  	headerData, err := GenEventHeader(&header)
   137  	if err != nil {
   138  		return nil, terror.Annotate(err, "generate event header")
   139  	}
   140  
   141  	err = combineHeaderPayload(buf, headerData, postHeader, payload)
   142  	if err != nil {
   143  		return nil, terror.Annotate(err, "combine header, post-header and payload")
   144  	}
   145  
   146  	// CRC32 checksum, 4 bytes
   147  	checksum := crc32.ChecksumIEEE(buf.Bytes())
   148  	err = binary.Write(buf, binary.LittleEndian, checksum)
   149  	if err != nil {
   150  		return nil, terror.ErrBinlogWriteBinaryData.AnnotateDelegate(err, "write CRC32 % X", checksum)
   151  	}
   152  
   153  	if event == nil {
   154  		return nil, nil // not need to decode the event
   155  	}
   156  
   157  	// decode event, some implementations of `Decode` also need checksum
   158  	endIdx := buf.Len()
   159  	if !decodeWithChecksum {
   160  		endIdx -= int(crc32Len)
   161  	}
   162  	err = event.Decode(buf.Bytes()[eventHeaderLen:endIdx])
   163  	if err != nil {
   164  		return nil, terror.ErrBinlogEventDecode.Delegate(err, buf.Bytes())
   165  	}
   166  
   167  	return &replication.BinlogEvent{RawData: buf.Bytes(), Header: &header, Event: event}, nil
   168  }
   169  
   170  // combineHeaderPayload combines header, postHeader and payload together.
   171  func combineHeaderPayload(buf *bytes.Buffer, header, postHeader, payload []byte) error {
   172  	if len(header) != int(eventHeaderLen) {
   173  		return terror.ErrBinlogHeaderLengthNotValid.Generate(eventHeaderLen, len(header))
   174  	}
   175  
   176  	err := binary.Write(buf, binary.LittleEndian, header)
   177  	if err != nil {
   178  		return terror.ErrBinlogWriteBinaryData.AnnotateDelegate(err, "write event header % X", header)
   179  	}
   180  
   181  	if len(postHeader) > 0 { // postHeader maybe empty
   182  		err = binary.Write(buf, binary.LittleEndian, postHeader)
   183  		if err != nil {
   184  			return terror.ErrBinlogWriteBinaryData.AnnotateDelegate(err, "write event post-header % X", postHeader)
   185  		}
   186  	}
   187  
   188  	err = binary.Write(buf, binary.LittleEndian, payload)
   189  	if err != nil {
   190  		return terror.ErrBinlogWriteBinaryData.AnnotateDelegate(err, "write event payload % X", payload)
   191  	}
   192  
   193  	return nil
   194  }
   195  
   196  // encodeColumnValue encodes value to bytes
   197  // ref: https://github.com/go-mysql-org/go-mysql/blob/88e9cd7f6643b246b4dcc0e3206e9a169dd0ac96/replication/row_event.go#L368
   198  // NOTE: we do not generate meaningful `meta` yet.
   199  // nolint:unparam
   200  func encodeColumnValue(v interface{}, tp byte, meta uint16) ([]byte, error) {
   201  	var (
   202  		buf = new(bytes.Buffer)
   203  		err error
   204  	)
   205  	switch tp {
   206  	case gmysql.MYSQL_TYPE_NULL:
   207  		return nil, nil
   208  	case gmysql.MYSQL_TYPE_LONG:
   209  		err = writeIntegerColumnValue(buf, v, reflect.TypeOf(int32(0)))
   210  	case gmysql.MYSQL_TYPE_TINY:
   211  		err = writeIntegerColumnValue(buf, v, reflect.TypeOf(int8(0)))
   212  	case gmysql.MYSQL_TYPE_SHORT:
   213  		err = writeIntegerColumnValue(buf, v, reflect.TypeOf(int16(0)))
   214  	case gmysql.MYSQL_TYPE_INT24:
   215  		err = writeIntegerColumnValue(buf, v, reflect.TypeOf(int32(0)))
   216  		if err == nil {
   217  			buf.Truncate(3)
   218  		}
   219  	case gmysql.MYSQL_TYPE_LONGLONG:
   220  		err = writeIntegerColumnValue(buf, v, reflect.TypeOf(int64(0)))
   221  	case gmysql.MYSQL_TYPE_FLOAT:
   222  		value, ok := v.(float32)
   223  		if !ok {
   224  			err = terror.ErrBinlogColumnTypeMisMatch.Generate(v, reflect.TypeOf(v), reflect.TypeOf(float32(0)))
   225  		} else {
   226  			bits := math.Float32bits(value)
   227  			err = writeIntegerColumnValue(buf, bits, reflect.TypeOf(uint32(0)))
   228  		}
   229  	case gmysql.MYSQL_TYPE_DOUBLE:
   230  		value, ok := v.(float64)
   231  		if !ok {
   232  			err = terror.ErrBinlogColumnTypeMisMatch.Generate(v, reflect.TypeOf(v), reflect.TypeOf(float64(0)))
   233  		} else {
   234  			bits := math.Float64bits(value)
   235  			err = writeIntegerColumnValue(buf, bits, reflect.TypeOf(uint64(0)))
   236  		}
   237  	case gmysql.MYSQL_TYPE_STRING:
   238  		err = writeStringColumnValue(buf, v)
   239  	case gmysql.MYSQL_TYPE_VARCHAR, gmysql.MYSQL_TYPE_VAR_STRING,
   240  		gmysql.MYSQL_TYPE_NEWDECIMAL, gmysql.MYSQL_TYPE_BIT,
   241  		gmysql.MYSQL_TYPE_TIMESTAMP, gmysql.MYSQL_TYPE_TIMESTAMP2,
   242  		gmysql.MYSQL_TYPE_DATETIME, gmysql.MYSQL_TYPE_DATETIME2,
   243  		gmysql.MYSQL_TYPE_TIME, gmysql.MYSQL_TYPE_TIME2,
   244  		gmysql.MYSQL_TYPE_YEAR, gmysql.MYSQL_TYPE_ENUM, gmysql.MYSQL_TYPE_SET,
   245  		gmysql.MYSQL_TYPE_BLOB, gmysql.MYSQL_TYPE_JSON, gmysql.MYSQL_TYPE_GEOMETRY:
   246  		// this generator is used for testing, so some types supporting can be added later.
   247  		err = terror.ErrBinlogGoMySQLTypeNotSupport.Generate(tp)
   248  	default:
   249  		err = terror.ErrBinlogGoMySQLTypeNotSupport.Generate(tp)
   250  	}
   251  	return buf.Bytes(), terror.Annotatef(err, "go-mysql type %d", tp)
   252  }
   253  
   254  // writeIntegerColumnValue writes integer value to bytes buffer.
   255  func writeIntegerColumnValue(buf *bytes.Buffer, value interface{}, valueType reflect.Type) error {
   256  	if reflect.TypeOf(value) != valueType {
   257  		return terror.ErrBinlogColumnTypeMisMatch.Generate(value, reflect.TypeOf(value), valueType)
   258  	}
   259  	return terror.ErrBinlogWriteBinaryData.Delegate(binary.Write(buf, binary.LittleEndian, value))
   260  }
   261  
   262  // writeStringColumnValue writes string value to bytes buffer.
   263  func writeStringColumnValue(buf *bytes.Buffer, value interface{}) error {
   264  	str, ok := value.(string)
   265  	if !ok {
   266  		return terror.ErrBinlogColumnTypeMisMatch.Generate(value, reflect.TypeOf(value), reflect.TypeOf(""))
   267  	}
   268  	var (
   269  		err    error
   270  		length = len(str)
   271  	)
   272  	if length < 256 {
   273  		err = binary.Write(buf, binary.LittleEndian, uint8(length))
   274  		if err == nil {
   275  			err = binary.Write(buf, binary.LittleEndian, []byte(str))
   276  		}
   277  	} else {
   278  		err = binary.Write(buf, binary.LittleEndian, uint16(length))
   279  		if err != nil {
   280  			err = binary.Write(buf, binary.LittleEndian, []byte(str))
   281  		}
   282  	}
   283  	return terror.ErrBinlogWriteBinaryData.Delegate(err)
   284  }
   285  
   286  // https://dev.mysql.com/doc/internals/en/query-event.html
   287  // and after Q_COMMIT_TS could be found in
   288  // https://github.com/mysql/mysql-server/blob/124c7ab1d6f914637521fd4463a993aa73403513/libbinlogevents/include/statement_events.h#L500
   289  // Q_HRNOW (MariaDB) could be found in
   290  // https://github.com/MariaDB/server/blob/09a1f0075a8d5752dd7b2940a20d86a040af1741/sql/log_event.h#L321
   291  const (
   292  	QFlags2Code = iota
   293  	QSqlModeCode
   294  	QCatalog
   295  	QAutoIncrement
   296  	QCharsetCode
   297  	QTimeZoneCode
   298  	QCatalogNzCode
   299  	QLcTimeNamesCode
   300  	QCharsetDatabaseCode
   301  	QTableMapForUpdateCode
   302  	QMasterDataWrittenCode
   303  	QInvokers
   304  	QUpdatedDBNames
   305  	QMicroseconds
   306  	QCommitTS
   307  	QCommitTS2
   308  	QExplicitDefaultsForTimestamp
   309  	QDdlLoggedWithXid
   310  	QDefaultCollationForUtf8mb4
   311  	QSqlRequirePrimaryKey
   312  	QDefaultTableEncryption
   313  	QHrnow = 128
   314  )
   315  
   316  // https://dev.mysql.com/doc/internals/en/query-event.html
   317  var statusVarsFixedLength = map[byte]int{
   318  	QFlags2Code:            4,
   319  	QSqlModeCode:           8,
   320  	QAutoIncrement:         2 + 2,
   321  	QCharsetCode:           2 + 2 + 2,
   322  	QLcTimeNamesCode:       2,
   323  	QCharsetDatabaseCode:   2,
   324  	QTableMapForUpdateCode: 8,
   325  	QMasterDataWrittenCode: 4,
   326  	QMicroseconds:          3,
   327  	QCommitTS:              0, // unused now
   328  	QCommitTS2:             0, // unused now
   329  	// below variables could be find in
   330  	// https://github.com/mysql/mysql-server/blob/7d10c82196c8e45554f27c00681474a9fb86d137/libbinlogevents/src/statement_events.cpp#L312
   331  	QExplicitDefaultsForTimestamp: 1,
   332  	QDdlLoggedWithXid:             8,
   333  	QDefaultCollationForUtf8mb4:   2,
   334  	QSqlRequirePrimaryKey:         1,
   335  	QDefaultTableEncryption:       1,
   336  	// https://github.com/MariaDB/server/blob/94b45787045677c106a25ebb5aaf1273040b2ff6/sql/log_event.cc#L1619
   337  	QHrnow: 3,
   338  }
   339  
   340  // getSQLMode gets SQL mode from binlog statusVars, still could return a reasonable value if found error.
   341  func getSQLMode(statusVars []byte) (mysql.SQLMode, error) {
   342  	vars, err := statusVarsToKV(statusVars)
   343  	b, ok := vars[QSqlModeCode]
   344  
   345  	if !ok {
   346  		if err == nil {
   347  			// only happen when this is a dummy event generated by DM
   348  			err = fmt.Errorf("Q_SQL_MODE_CODE not found in status_vars %v", statusVars)
   349  		}
   350  		return mysql.ModeNone, err
   351  	}
   352  
   353  	r := bytes.NewReader(b)
   354  	var v int64
   355  	_ = binary.Read(r, binary.LittleEndian, &v)
   356  
   357  	return mysql.SQLMode(v), err
   358  }
   359  
   360  // GetParserForStatusVars gets a parser for binlog which is suitable for its sql_mode in statusVars.
   361  func GetParserForStatusVars(statusVars []byte) (*parser.Parser, error) {
   362  	parser2 := parser.New()
   363  	mode, err := getSQLMode(statusVars)
   364  	parser2.SetSQLMode(mode)
   365  	return parser2, err
   366  }
   367  
   368  // GetServerCollationByStatusVars gets server collation by binlog statusVars.
   369  func GetServerCollationByStatusVars(statusVars []byte, idAndCollationMap map[int]string) (string, error) {
   370  	vars, err := statusVarsToKV(statusVars)
   371  	b, ok := vars[QCharsetCode]
   372  
   373  	if !ok {
   374  		if err == nil {
   375  			// only happen when this is a dummy event generated by DM
   376  			err = fmt.Errorf("Q_CHARSET_CODE not found in status_vars %v", statusVars)
   377  		}
   378  		// mysql 5.7.22 default 'latin1_swedish_ci'
   379  		return "latin1_swedish_ci", err
   380  	}
   381  
   382  	// QCharsetCode 2-byte character_set_client + 2-byte collation_connection + 2-byte collation_server
   383  	r := bytes.NewReader(b[4:])
   384  	var v uint16
   385  	_ = binary.Read(r, binary.LittleEndian, &v)
   386  	return idAndCollationMap[int(v)], err
   387  }
   388  
   389  // GetCharsetCodecByStatusVars returns an encoding.Encoding to encode and decode original query if needed.
   390  func GetCharsetCodecByStatusVars(statusVars []byte) (encoding.Encoding, error) {
   391  	vars, err := statusVarsToKV(statusVars)
   392  	b, ok := vars[QCharsetCode]
   393  
   394  	if !ok {
   395  		if err == nil {
   396  			// only happen when this is a dummy event generated by DM
   397  			err = fmt.Errorf("Q_CHARSET_CODE not found in status_vars %v", statusVars)
   398  		}
   399  		return nil, err
   400  	}
   401  
   402  	// QCharsetCode 2-byte character_set_client + 2-byte collation_connection + 2-byte collation_server
   403  	r := bytes.NewReader(b)
   404  	var v uint16
   405  	_ = binary.Read(r, binary.LittleEndian, &v)
   406  
   407  	charsetName, _, err2 := charset.GetCharsetInfoByID(int(v))
   408  
   409  	// only handle GBK to minimize the change
   410  	switch charsetName {
   411  	case charset.CharsetGBK:
   412  		return simplifiedchinese.GBK, nil
   413  	default:
   414  		return nil, err2
   415  	}
   416  }
   417  
   418  // GetTimezoneByStatusVars returns the timezone of upstream for "datetime" type alter table commands.
   419  // For "ALTER TABLE ADD COLUMN x DATETIME", not for "ALTER TABLE ADD COLUMN x TIMESTAMP".
   420  func GetTimezoneByStatusVars(statusVars []byte, upstreamTZStr string) (string, error) {
   421  	vars, err := statusVarsToKV(statusVars)
   422  	b, ok := vars[QTimeZoneCode]
   423  
   424  	// here do not check err first to increase robustness
   425  	// when statusVarsToKV() meets errors like unrecognized key or EOF, caller may still work properly if the wanted key has been parsed
   426  	if !ok {
   427  		if err == nil {
   428  			// happen when this is a dummy event generated by DM
   429  			// or using some DDL commands like "ALTER TABLE ADD COLUMN x TIMESTAMP" which does not contain information of time zone
   430  			err = fmt.Errorf("timezone not found in status_vars %v", statusVars)
   431  		}
   432  		return "", err
   433  	}
   434  
   435  	// QTimeZoneCode 1-byte length + <length> chars of the timezone
   436  	s := string(b[1:])
   437  	if s == "SYSTEM" {
   438  		// replace it with the absolute upstream time zone
   439  		s = upstreamTZStr
   440  	}
   441  	return s, err
   442  }
   443  
   444  // if returned error is `io.EOF`, it means UnexpectedEOF because we handled expected `io.EOF` as success
   445  // returned map should not be nil for other usage.
   446  func statusVarsToKV(statusVars []byte) (map[byte][]byte, error) {
   447  	r := bytes.NewReader(statusVars)
   448  	vars := make(map[byte][]byte)
   449  	var value []byte
   450  
   451  	// NOTE: this closure modifies variable `value`
   452  	appendLengthThenCharsToValue := func() error {
   453  		length, err := r.ReadByte()
   454  		if err != nil {
   455  			return err
   456  		}
   457  		value = append(value, length)
   458  
   459  		buf := make([]byte, length)
   460  		n, err := r.Read(buf)
   461  		if err != nil {
   462  			return err
   463  		}
   464  		if n != int(length) {
   465  			return io.EOF
   466  		}
   467  		value = append(value, buf...)
   468  		return nil
   469  	}
   470  
   471  	generateError := func(err error) (map[byte][]byte, error) {
   472  		offset, _ := r.Seek(0, io.SeekCurrent)
   473  		return vars, terror.ErrBinlogStatusVarsParse.Delegate(err, statusVars, offset)
   474  	}
   475  
   476  	for {
   477  		// reset value
   478  		value = make([]byte, 0)
   479  		key, err := r.ReadByte()
   480  		if err == io.EOF {
   481  			break
   482  		}
   483  		if err != nil {
   484  			return generateError(err)
   485  		}
   486  
   487  		if _, ok := vars[key]; ok {
   488  			return generateError(errors.New("duplicate key"))
   489  		}
   490  
   491  		if length, ok := statusVarsFixedLength[key]; ok {
   492  			value = make([]byte, length)
   493  			n, err2 := r.Read(value)
   494  			if err2 != nil || n != length {
   495  				return generateError(io.EOF)
   496  			}
   497  
   498  			vars[key] = value
   499  			continue
   500  		}
   501  
   502  		// get variable-length value of according key and save it in `value`
   503  		switch key {
   504  		// 1-byte length + <length> chars of the catalog + '0'-char
   505  		case QCatalog:
   506  			if err = appendLengthThenCharsToValue(); err != nil {
   507  				return generateError(err)
   508  			}
   509  
   510  			b, err2 := r.ReadByte()
   511  			if err2 != nil {
   512  				return generateError(err)
   513  			}
   514  			// nolint:makezero
   515  			value = append(value, b)
   516  		// 1-byte length + <length> chars of the timezone/catalog
   517  		case QTimeZoneCode, QCatalogNzCode:
   518  			if err = appendLengthThenCharsToValue(); err != nil {
   519  				return generateError(err)
   520  			}
   521  		// 1-byte length + <length> bytes username and 1-byte length + <length> bytes hostname
   522  		case QInvokers:
   523  			if err = appendLengthThenCharsToValue(); err != nil {
   524  				return generateError(err)
   525  			}
   526  			if err = appendLengthThenCharsToValue(); err != nil {
   527  				return generateError(err)
   528  			}
   529  		// 1-byte count + <count> \0 terminated string
   530  		case QUpdatedDBNames:
   531  			count, err := r.ReadByte()
   532  			if err != nil {
   533  				return generateError(err)
   534  			}
   535  			// nolint:makezero
   536  			value = append(value, count)
   537  			// if count is 254 (OVER_MAX_DBS_IN_EVENT_MTS), there's no following DB names
   538  			// https://github.com/mysql/mysql-server/blob/ee4455a33b10f1b1886044322e4893f587b319ed/libbinlogevents/include/binlog_event.h#L107
   539  			if count == 254 {
   540  				break
   541  			}
   542  
   543  			buf := make([]byte, 0, 128)
   544  			b := byte(1) // initialize to any non-zero value
   545  			for ; count > 0; count-- {
   546  				// read one zero-terminated string
   547  				for b != 0 {
   548  					b, err = r.ReadByte()
   549  					if err != nil {
   550  						return generateError(err)
   551  					}
   552  					buf = append(buf, b)
   553  				}
   554  				b = byte(1) // reset to any non-zero value
   555  			}
   556  			// nolint:makezero
   557  			value = append(value, buf...)
   558  		default:
   559  			return generateError(errors.New("unrecognized key"))
   560  		}
   561  		vars[key] = value
   562  	}
   563  
   564  	return vars, nil
   565  }
   566  
   567  // GetGTIDStr gets GTID string representation from a GTID event or MariaDB GTID evnets.
   568  // learn from: https://github.com/go-mysql-org/go-mysql/blob/c6ab05a85eb86dc51a27ceed6d2f366a32874a24/replication/binlogsyncer.go#L732-L749
   569  func GetGTIDStr(e *replication.BinlogEvent) (string, error) {
   570  	switch ev := e.Event.(type) {
   571  	case *replication.GTIDEvent:
   572  		u, _ := uuid.FromBytes(ev.SID)
   573  		return fmt.Sprintf("%s:%d", u.String(), ev.GNO), nil
   574  	case *replication.MariadbGTIDEvent:
   575  		GTID := ev.GTID
   576  		return fmt.Sprintf("%d-%d-%d", GTID.DomainID, GTID.ServerID, GTID.SequenceNumber), nil
   577  	default:
   578  		return "", fmt.Errorf("unsupported event type %d", e.Header.EventType)
   579  	}
   580  }