Initial commit
This commit is contained in:
commit
f3961d403c
|
@ -0,0 +1,18 @@
|
||||||
|
Copyright 2018 Gregory Eremin
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,21 @@
|
||||||
|
# BLT
|
||||||
|
|
||||||
|
MySQL binary log parser.
|
||||||
|
|
||||||
|
### WIP
|
||||||
|
|
||||||
|
Work in progress, some events are not fully supported.
|
||||||
|
|
||||||
|
*TODO:*
|
||||||
|
|
||||||
|
[x] FormatDescriptionEvent
|
||||||
|
[x] TableMapEvent
|
||||||
|
[x] RotateEvent
|
||||||
|
[ ] RowsEvent
|
||||||
|
[ ] XIDEvent
|
||||||
|
[ ] GTIDEvent
|
||||||
|
[ ] QueryEvent
|
||||||
|
|
||||||
|
### Licence
|
||||||
|
|
||||||
|
MIT
|
|
@ -0,0 +1,115 @@
|
||||||
|
package blt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
)
|
||||||
|
|
||||||
|
// buffer is a simple wrapper over a slice of bytes with a cursor. It allows for
|
||||||
|
// easy command building and results parsing.
|
||||||
|
type buffer struct {
|
||||||
|
data []byte
|
||||||
|
pos int
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip next n bytes
|
||||||
|
func (b *buffer) skip(n int) {
|
||||||
|
b.pos += n
|
||||||
|
}
|
||||||
|
|
||||||
|
// advance skips next N bytes and returns them
|
||||||
|
func (b *buffer) advance(n int) []byte {
|
||||||
|
b.skip(n)
|
||||||
|
return b.data[b.pos-n:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// cur returns remaining unread buffer.
|
||||||
|
func (b *buffer) cur() []byte {
|
||||||
|
return b.data[b.pos:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// newReadBuffer creates a buffer with command output.
|
||||||
|
func newReadBuffer(data []byte) *buffer {
|
||||||
|
return &buffer{data: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buffer) readUint8() uint8 {
|
||||||
|
return decodeUint8(b.advance(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buffer) readUint16() uint16 {
|
||||||
|
return decodeUint16(b.advance(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buffer) readUint24() uint32 {
|
||||||
|
return decodeUint24(b.advance(3))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buffer) readUint32() uint32 {
|
||||||
|
return decodeUint32(b.advance(4))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buffer) readUint48() uint64 {
|
||||||
|
return decodeUint48(b.advance(6))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buffer) readUint64() uint64 {
|
||||||
|
return decodeUint64(b.advance(8))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buffer) readUintLenEnc() (val uint64, isNull bool) {
|
||||||
|
var size int
|
||||||
|
val, isNull, size = decodeUintLenEnc(b.cur())
|
||||||
|
b.skip(size)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buffer) readStringNullTerm() []byte {
|
||||||
|
str := decodeStringNullTerm(b.cur())
|
||||||
|
b.skip(len(str) + 1)
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buffer) readStringVarLen(n int) []byte {
|
||||||
|
return decodeStringVarLen(b.advance(n), n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buffer) readStringLenEnc() []byte {
|
||||||
|
str, size := decodeStringLenEnc(b.cur())
|
||||||
|
b.skip(size)
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buffer) readStringEOF() []byte {
|
||||||
|
return decodeStringEOF(b.cur())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-allocate command buffer. First four bytes would be used to set command
|
||||||
|
// length and sequence number.
|
||||||
|
func newCommandBuffer(size int) *buffer {
|
||||||
|
return &buffer{data: make([]byte, size+4), pos: 4}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buffer) writeByte(v byte) {
|
||||||
|
b.data[b.pos] = v
|
||||||
|
b.pos++
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buffer) writeUint16(v uint16) {
|
||||||
|
binary.LittleEndian.PutUint16(b.data[b.pos:], v)
|
||||||
|
b.pos += 2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buffer) writeUint32(v uint32) {
|
||||||
|
binary.LittleEndian.PutUint32(b.data[b.pos:], v)
|
||||||
|
b.pos += 4
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buffer) writeString(s string) {
|
||||||
|
b.data[b.pos] = byte(len(s))
|
||||||
|
b.pos++
|
||||||
|
b.pos += copy(b.data[b.pos:], s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *buffer) writeStringEOF(s string) {
|
||||||
|
b.pos += copy(b.data[b.pos:], s)
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/localhots/blt"
|
||||||
|
"github.com/localhots/gobelt/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
dsn := flag.String("dsn", "", "Database source name")
|
||||||
|
id := flag.Uint("id", 1000, "Server ID (arbitrary, unique)")
|
||||||
|
file := flag.String("file", "", "Binary log file name")
|
||||||
|
offset := flag.Uint("offset", 0, "Log offset in bytes")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
validate((*dsn != ""), "Database source name is not set")
|
||||||
|
validate((*id != 0), "Server ID is not set")
|
||||||
|
validate((*file != ""), "Binary log file is not set")
|
||||||
|
conf := blt.Config{
|
||||||
|
ServerID: uint32(*id),
|
||||||
|
File: *file,
|
||||||
|
Offset: uint32(*offset),
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := blt.Connect(*dsn, conf)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf(ctx, "Failed to establish connection: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
off := conf.Offset
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
// for {
|
||||||
|
evt, err := reader.ReadEventHeader(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf(ctx, "Failed to read event: %v", err)
|
||||||
|
}
|
||||||
|
ts := time.Unix(int64(evt.Timestamp), 0).Format(time.RFC3339)
|
||||||
|
log.Info(ctx, "Event received", log.F{
|
||||||
|
"type": evt.Type,
|
||||||
|
"timestamp": ts,
|
||||||
|
"offset": off,
|
||||||
|
})
|
||||||
|
off = evt.NextOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validate(cond bool, msg string) {
|
||||||
|
if !cond {
|
||||||
|
fmt.Println(msg)
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
package blt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type columnType byte
|
||||||
|
|
||||||
|
// Spec: https://dev.mysql.com/doc/internals/en/com-query-response.html#column-type
|
||||||
|
const (
|
||||||
|
colTypeDecimal columnType = 0x00
|
||||||
|
colTypeTiny columnType = 0x01
|
||||||
|
colTypeShort columnType = 0x02
|
||||||
|
colTypeLong columnType = 0x03
|
||||||
|
colTypeFloat columnType = 0x04
|
||||||
|
colTypeDouble columnType = 0x05
|
||||||
|
colTypeNull columnType = 0x06
|
||||||
|
colTypeTimestamp columnType = 0x07
|
||||||
|
colTypeLonglong columnType = 0x08
|
||||||
|
colTypeInt24 columnType = 0x09
|
||||||
|
colTypeDate columnType = 0x0a
|
||||||
|
colTypeTime columnType = 0x0b
|
||||||
|
colTypeDatetime columnType = 0x0c
|
||||||
|
colTypeYear columnType = 0x0d
|
||||||
|
colTypeNewDate columnType = 0x0e // Internal
|
||||||
|
colTypeVarchar columnType = 0x0f
|
||||||
|
colTypeBit columnType = 0x10
|
||||||
|
colTypeTimestamp2 columnType = 0x11 // Internal
|
||||||
|
colTypeDatetime2 columnType = 0x12 // Internal
|
||||||
|
colTypeTime2 columnType = 0x13 // Internal
|
||||||
|
|
||||||
|
colTypeJSON columnType = 0xF5
|
||||||
|
colTypeNewDecimal columnType = 0xF6
|
||||||
|
colTypeEnum columnType = 0xF7
|
||||||
|
colTypeSet columnType = 0xF8
|
||||||
|
colTypeTinyblob columnType = 0xF9
|
||||||
|
colTypeMediumblob columnType = 0xFA
|
||||||
|
colTypeLongblob columnType = 0xFB
|
||||||
|
colTypeBlob columnType = 0xFC
|
||||||
|
colTypeVarstring columnType = 0xFD
|
||||||
|
colTypeString columnType = 0xFE
|
||||||
|
colTypeGeometry columnType = 0xFF
|
||||||
|
)
|
||||||
|
|
||||||
|
func (ct columnType) String() string {
|
||||||
|
switch ct {
|
||||||
|
case colTypeDecimal:
|
||||||
|
return "Decimal"
|
||||||
|
case colTypeTiny:
|
||||||
|
return "Tiny"
|
||||||
|
case colTypeShort:
|
||||||
|
return "Short"
|
||||||
|
case colTypeLong:
|
||||||
|
return "Long"
|
||||||
|
case colTypeFloat:
|
||||||
|
return "Float"
|
||||||
|
case colTypeDouble:
|
||||||
|
return "Double"
|
||||||
|
case colTypeNull:
|
||||||
|
return "Null"
|
||||||
|
case colTypeTimestamp:
|
||||||
|
return "Timestamp"
|
||||||
|
case colTypeLonglong:
|
||||||
|
return "Longlong"
|
||||||
|
case colTypeInt24:
|
||||||
|
return "Int24"
|
||||||
|
case colTypeDate:
|
||||||
|
return "Date"
|
||||||
|
case colTypeTime:
|
||||||
|
return "Time"
|
||||||
|
case colTypeDatetime:
|
||||||
|
return "Datetime"
|
||||||
|
case colTypeYear:
|
||||||
|
return "Year"
|
||||||
|
case colTypeNewDate:
|
||||||
|
return "NewDate"
|
||||||
|
case colTypeVarchar:
|
||||||
|
return "Varchar"
|
||||||
|
case colTypeBit:
|
||||||
|
return "Bit"
|
||||||
|
case colTypeTimestamp2:
|
||||||
|
return "Timestamp2"
|
||||||
|
case colTypeDatetime2:
|
||||||
|
return "Datetime2"
|
||||||
|
case colTypeTime2:
|
||||||
|
return "Time2"
|
||||||
|
case colTypeJSON:
|
||||||
|
return "JSON"
|
||||||
|
case colTypeNewDecimal:
|
||||||
|
return "NewDecimal"
|
||||||
|
case colTypeEnum:
|
||||||
|
return "Enum"
|
||||||
|
case colTypeSet:
|
||||||
|
return "Set"
|
||||||
|
case colTypeTinyblob:
|
||||||
|
return "Tinyblob"
|
||||||
|
case colTypeMediumblob:
|
||||||
|
return "Mediumblob"
|
||||||
|
case colTypeLongblob:
|
||||||
|
return "Longblob"
|
||||||
|
case colTypeBlob:
|
||||||
|
return "Blob"
|
||||||
|
case colTypeVarstring:
|
||||||
|
return "Varstring"
|
||||||
|
case colTypeString:
|
||||||
|
return "String"
|
||||||
|
case colTypeGeometry:
|
||||||
|
return "Geometry"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("Unknown(%d)", ct)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,211 @@
|
||||||
|
package blt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Protocol::FixedLengthInteger
|
||||||
|
// A fixed-length integer stores its value in a series of bytes with the least
|
||||||
|
// significant byte first (little endian).
|
||||||
|
// Spec: https://dev.mysql.com/doc/internals/en/integer.html#fixed-length-integer
|
||||||
|
|
||||||
|
// int<1>
|
||||||
|
|
||||||
|
func encodeUint8(data []byte, v uint8) {
|
||||||
|
data[0] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeUint8(data []byte) uint8 {
|
||||||
|
return uint8(data[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// int<2>
|
||||||
|
|
||||||
|
func encodeUint16(data []byte, v uint16) {
|
||||||
|
binary.LittleEndian.PutUint16(data, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeUint16(data []byte) uint16 {
|
||||||
|
return binary.LittleEndian.Uint16(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// int<3>
|
||||||
|
|
||||||
|
func encodeUint24(data []byte, v uint32) {
|
||||||
|
encodeVarLen64(data, uint64(v), 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeUint24(data []byte) uint32 {
|
||||||
|
return uint32(decodeVarLen64(data, 3))
|
||||||
|
}
|
||||||
|
|
||||||
|
// int<4>
|
||||||
|
|
||||||
|
func encodeUint32(data []byte, v uint32) {
|
||||||
|
binary.LittleEndian.PutUint32(data, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeUint32(data []byte) uint32 {
|
||||||
|
return binary.LittleEndian.Uint32(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// int<6>
|
||||||
|
|
||||||
|
func encodeUint48(data []byte, v uint64) {
|
||||||
|
encodeVarLen64(data, v, 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeUint48(data []byte) uint64 {
|
||||||
|
return decodeVarLen64(data, 6)
|
||||||
|
}
|
||||||
|
|
||||||
|
// int<8>
|
||||||
|
|
||||||
|
func encodeUint64(data []byte, v uint64) {
|
||||||
|
binary.LittleEndian.PutUint64(data, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeUint64(data []byte) uint64 {
|
||||||
|
return binary.LittleEndian.Uint64(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protocol::LengthEncodedInteger
|
||||||
|
// An integer that consumes 1, 3, 4, or 9 bytes, depending on its numeric value.
|
||||||
|
// Spec: https://dev.mysql.com/doc/internals/en/integer.html#length-encoded-integer
|
||||||
|
|
||||||
|
// To convert a number value into a length-encoded integer:
|
||||||
|
// If the value is < 251, it is stored as a 1-byte integer.
|
||||||
|
// If the value is ≥ 251 and < (2^16), it is stored as 0xFC + 2-byte integer.
|
||||||
|
// If the value is ≥ (2^16) and < (2^24), it is stored as 0xFD + 3-byte integer.
|
||||||
|
// If the value is ≥ (2^24) and < (2^64) it is stored as 0xFE + 8-byte integer.
|
||||||
|
// Note: up to MySQL 3.22, 0xFE was followed by a 4-byte integer.
|
||||||
|
func encodeUintLenEnc(data []byte, v uint64, isNull bool) (size int) {
|
||||||
|
switch {
|
||||||
|
case isNull:
|
||||||
|
data[0] = 0xFB
|
||||||
|
return 1
|
||||||
|
case v <= 0xFB:
|
||||||
|
data[0] = byte(v)
|
||||||
|
return 1
|
||||||
|
case v <= 2<<15:
|
||||||
|
data[0] = 0xFC
|
||||||
|
encodeVarLen64(data[1:], v, 2)
|
||||||
|
return 3
|
||||||
|
case v <= 2<<23:
|
||||||
|
data[0] = 0xFD
|
||||||
|
encodeVarLen64(data[1:], v, 3)
|
||||||
|
return 4
|
||||||
|
default:
|
||||||
|
data[0] = 0xFE
|
||||||
|
encodeVarLen64(data[1:], v, 8)
|
||||||
|
return 9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// To convert a length-encoded integer into its numeric value, check the first
|
||||||
|
// byte:
|
||||||
|
// If it is < 0xFB, treat it as a 1-byte integer.
|
||||||
|
// If it is 0xFC, it is followed by a 2-byte integer.
|
||||||
|
// If it is 0xFD, it is followed by a 3-byte integer.
|
||||||
|
// If it is 0xFE, it is followed by a 8-byte integer.
|
||||||
|
// Depending on the context, the first byte may also have other meanings:
|
||||||
|
// If it is 0xFB, it is represents a NULL in a ProtocolText::ResultsetRow.
|
||||||
|
// If it is 0xFF and is the first byte of an ERR_Packet
|
||||||
|
// Caution:
|
||||||
|
// If the first byte of a packet is a length-encoded integer and its byte value
|
||||||
|
// is 0xFE, you must check the length of the packet to verify that it has enough
|
||||||
|
// space for a 8-byte integer.
|
||||||
|
// If not, it may be an EOF_Packet instead.
|
||||||
|
func decodeUintLenEnc(data []byte) (v uint64, isNull bool, size int) {
|
||||||
|
switch data[0] {
|
||||||
|
case 0xFB:
|
||||||
|
return 0xFB, true, 1
|
||||||
|
case 0xFC:
|
||||||
|
return decodeVarLen64(data[1:], 2), false, 3
|
||||||
|
case 0xFD:
|
||||||
|
return decodeVarLen64(data[1:], 3), false, 4
|
||||||
|
case 0xFE:
|
||||||
|
return decodeVarLen64(data[1:], 8), false, 9
|
||||||
|
default:
|
||||||
|
return uint64(data[0]), false, 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Variable length encoding helpers
|
||||||
|
//
|
||||||
|
|
||||||
|
func encodeVarLen64(data []byte, v uint64, s int) {
|
||||||
|
for i := 0; i < s; i++ {
|
||||||
|
data[i] = byte(v >> uint(i*8))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeVarLen64(data []byte, s int) uint64 {
|
||||||
|
v := uint64(data[0])
|
||||||
|
for i := 1; i < s; i++ {
|
||||||
|
v |= uint64(data[i]) << uint(i*8)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protocol::NulTerminatedString
|
||||||
|
// Strings that are terminated by a 0x00 byte.
|
||||||
|
func decodeStringNullTerm(data []byte) []byte {
|
||||||
|
for i, c := range data {
|
||||||
|
if c == 0x00 {
|
||||||
|
s := make([]byte, i+1)
|
||||||
|
copy(s, data[:i])
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s := make([]byte, len(data))
|
||||||
|
copy(s, data)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protocol::VariableLengthString
|
||||||
|
// The length of the string is determined by another field or is calculated at
|
||||||
|
// runtime.
|
||||||
|
// Protocol::FixedLengthString
|
||||||
|
// Fixed-length strings have a known, hardcoded length.
|
||||||
|
func encodeStringVarLen(data, str []byte) {
|
||||||
|
copy(data, str)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeStringVarLen(data []byte, n int) []byte {
|
||||||
|
return decodeStringEOF(data[:n])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protocol::LengthEncodedString
|
||||||
|
// A length encoded string is a string that is prefixed with length encoded
|
||||||
|
// integer describing the length of the string.
|
||||||
|
// It is a special case of Protocol::VariableLengthString
|
||||||
|
func decodeStringLenEnc(data []byte) (str []byte, size int) {
|
||||||
|
strlen, _, size := decodeUintLenEnc(data)
|
||||||
|
strleni := int(strlen)
|
||||||
|
s := make([]byte, strleni)
|
||||||
|
copy(s, data[size:size+strleni])
|
||||||
|
return s, size + strleni
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protocol::RestOfPacketString
|
||||||
|
// If a string is the last component of a packet, its length can be calculated
|
||||||
|
// from the overall packet length minus the current position.
|
||||||
|
func decodeStringEOF(data []byte) []byte {
|
||||||
|
s := make([]byte, len(data))
|
||||||
|
copy(s, data)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimString(str []byte) string {
|
||||||
|
fmt.Println(str, string(str))
|
||||||
|
for i, c := range str {
|
||||||
|
if c == 0x00 {
|
||||||
|
return string(str[:i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(str)
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package blt
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestEncodeUint8(t *testing.T) {
|
||||||
|
buf := make([]byte, 1)
|
||||||
|
encodeUint8(buf, 123)
|
||||||
|
t.Log(buf)
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
package blt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/localhots/pretty"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FormatDescription is a description of binary log format.
|
||||||
|
type FormatDescription struct {
|
||||||
|
Version uint16
|
||||||
|
ServerVersion string
|
||||||
|
CreateTimestamp uint32
|
||||||
|
EventHeaderLength uint8
|
||||||
|
EventTypeHeaderLengths []uint8
|
||||||
|
ServerDetails ServerDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerDetails contains server feature details.
|
||||||
|
type ServerDetails struct {
|
||||||
|
Flavor Flavor
|
||||||
|
Version int
|
||||||
|
ChecksumAlgorithm ChecksumAlgorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flavor defines the specific kind of MySQL-like database.
|
||||||
|
type Flavor string
|
||||||
|
|
||||||
|
// ChecksumAlgorithm is a checksum algorithm is the one used by the server.
|
||||||
|
type ChecksumAlgorithm byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
// FlavorMySQL is the MySQL db flavor.
|
||||||
|
FlavorMySQL = "MySQL"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ChecksumAlgorithmNone means no checksum appened.
|
||||||
|
ChecksumAlgorithmNone ChecksumAlgorithm = 0x00
|
||||||
|
// ChecksumAlgorithmCRC32 used to append a 4 byte checksum at the end.
|
||||||
|
ChecksumAlgorithmCRC32 ChecksumAlgorithm = 0x01
|
||||||
|
// ChecksumAlgorithmUndefined is used when checksum algorithm is not known.
|
||||||
|
ChecksumAlgorithmUndefined ChecksumAlgorithm = 0xFF
|
||||||
|
)
|
||||||
|
|
||||||
|
// Spec: https://dev.mysql.com/doc/internals/en/format-description-event.html
|
||||||
|
func decodeFormatDescription(data []byte) FormatDescription {
|
||||||
|
buf := newReadBuffer(data)
|
||||||
|
fd := FormatDescription{
|
||||||
|
Version: buf.readUint16(),
|
||||||
|
ServerVersion: string(trimString(buf.readStringVarLen(50))),
|
||||||
|
CreateTimestamp: buf.readUint32(),
|
||||||
|
EventHeaderLength: buf.readUint8(),
|
||||||
|
EventTypeHeaderLengths: buf.readStringEOF(),
|
||||||
|
}
|
||||||
|
pretty.Println(fd)
|
||||||
|
fd.ServerDetails = ServerDetails{
|
||||||
|
Flavor: FlavorMySQL,
|
||||||
|
Version: parseVersionNumber(fd.ServerVersion),
|
||||||
|
ChecksumAlgorithm: ChecksumAlgorithmUndefined,
|
||||||
|
}
|
||||||
|
if fd.ServerDetails.Version > 50601 {
|
||||||
|
// Last 5 bytes are:
|
||||||
|
// [1] Checksum algorithm
|
||||||
|
// [4] Checksum
|
||||||
|
fd.ServerDetails.ChecksumAlgorithm = ChecksumAlgorithm(data[len(data)-5])
|
||||||
|
fd.EventTypeHeaderLengths = fd.EventTypeHeaderLengths[:len(fd.EventTypeHeaderLengths)-5]
|
||||||
|
}
|
||||||
|
|
||||||
|
return fd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fd FormatDescription) tableIDSize(et EventType) int {
|
||||||
|
if fd.headerLen(et) == 6 {
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
return 6
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fd FormatDescription) headerLen(et EventType) int {
|
||||||
|
return int(fd.EventTypeHeaderLengths[et-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ca ChecksumAlgorithm) String() string {
|
||||||
|
switch ca {
|
||||||
|
case ChecksumAlgorithmNone:
|
||||||
|
return "None"
|
||||||
|
case ChecksumAlgorithmCRC32:
|
||||||
|
return "CRC32"
|
||||||
|
case ChecksumAlgorithmUndefined:
|
||||||
|
return "Undefined"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("Unknown(%d)", ca)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseVersionNumber turns string version into a number just like the library
|
||||||
|
// mysql_get_server_version function does.
|
||||||
|
// Example: 5.7.19-log gets represented as 50719
|
||||||
|
// Spec: https://dev.mysql.com/doc/refman/8.0/en/mysql-get-server-version.html
|
||||||
|
func parseVersionNumber(v string) int {
|
||||||
|
tokens := strings.Split(v, ".")
|
||||||
|
major, _ := strconv.Atoi(tokens[0])
|
||||||
|
minor, _ := strconv.Atoi(tokens[1])
|
||||||
|
var patch int
|
||||||
|
for i, c := range tokens[2] {
|
||||||
|
if c < '0' || c > '9' {
|
||||||
|
patch, _ = strconv.Atoi(tokens[2][:i])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return major*10000 + minor*100 + patch
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
package blt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/localhots/pretty"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrInvalidHeader is returned when event header cannot be parsed.
|
||||||
|
ErrInvalidHeader = errors.New("Header is invalid")
|
||||||
|
)
|
||||||
|
|
||||||
|
// EventHeader represents binlog event header.
|
||||||
|
type EventHeader struct {
|
||||||
|
Timestamp uint32
|
||||||
|
Type EventType
|
||||||
|
ServerID uint32
|
||||||
|
EventLen uint32
|
||||||
|
NextOffset uint32
|
||||||
|
Flags uint16
|
||||||
|
ExtraHeaders []byte
|
||||||
|
|
||||||
|
eventBody []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spec: https://dev.mysql.com/doc/internals/en/event-header-fields.html
|
||||||
|
func (r *Reader) parseHeader(data []byte) (*EventHeader, error) {
|
||||||
|
headerLen := r.headerLen()
|
||||||
|
if len(data) < headerLen {
|
||||||
|
return nil, ErrInvalidHeader
|
||||||
|
}
|
||||||
|
// pretty.Println(headerLen, data)
|
||||||
|
|
||||||
|
buf := newReadBuffer(data)
|
||||||
|
h := &EventHeader{
|
||||||
|
Timestamp: buf.readUint32(),
|
||||||
|
Type: EventType(buf.readUint8()),
|
||||||
|
ServerID: buf.readUint32(),
|
||||||
|
EventLen: buf.readUint32(),
|
||||||
|
}
|
||||||
|
if r.format.Version == 0 || r.format.Version >= 3 {
|
||||||
|
h.NextOffset = buf.readUint32()
|
||||||
|
h.Flags = buf.readUint16()
|
||||||
|
}
|
||||||
|
if r.format.Version >= 4 {
|
||||||
|
h.ExtraHeaders = buf.readStringVarLen(headerLen - 19)
|
||||||
|
}
|
||||||
|
h.eventBody = buf.cur()
|
||||||
|
|
||||||
|
if h.NextOffset > 0 {
|
||||||
|
r.state.Offset = uint64(h.NextOffset)
|
||||||
|
}
|
||||||
|
|
||||||
|
csa := r.format.ServerDetails.ChecksumAlgorithm
|
||||||
|
if h.Type != FormatDescriptionEvent && csa == ChecksumAlgorithmCRC32 {
|
||||||
|
h.eventBody = h.eventBody[:len(h.eventBody)-4]
|
||||||
|
}
|
||||||
|
|
||||||
|
// pretty.Println(h)
|
||||||
|
|
||||||
|
switch h.Type {
|
||||||
|
case FormatDescriptionEvent:
|
||||||
|
r.format = decodeFormatDescription(h.eventBody)
|
||||||
|
pretty.Println(h.Type.String(), r.format)
|
||||||
|
case RotateEvent:
|
||||||
|
r.state = r.decodeRotateEvent(h.eventBody)
|
||||||
|
pretty.Println(h.Type.String(), r.state)
|
||||||
|
case TableMapEvent:
|
||||||
|
tm := r.decodeTableMap(h.eventBody)
|
||||||
|
r.tableMap[tm.TableID] = tm
|
||||||
|
// pretty.Println(h.Type.String(), tm)
|
||||||
|
case WriteRowsEventV0, WriteRowsEventV1, WriteRowsEventV2,
|
||||||
|
UpdateRowsEventV0, UpdateRowsEventV1, UpdateRowsEventV2,
|
||||||
|
DeleteRowsEventV0, DeleteRowsEventV1, DeleteRowsEventV2:
|
||||||
|
r.decodeRowsEvent(h.eventBody, h.Type)
|
||||||
|
case XIDEvent, GTIDEvent:
|
||||||
|
// TODO: Add support for these too
|
||||||
|
case QueryEvent:
|
||||||
|
// TODO: Handle schema changes
|
||||||
|
}
|
||||||
|
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) headerLen() int {
|
||||||
|
const defaultHeaderLength = 19
|
||||||
|
if r.format.EventHeaderLength > 0 {
|
||||||
|
return int(r.format.EventHeaderLength)
|
||||||
|
}
|
||||||
|
return defaultHeaderLength
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package blt
|
||||||
|
|
||||||
|
func (r *Reader) decodeRotateEvent(data []byte) Position {
|
||||||
|
buf := newReadBuffer(data)
|
||||||
|
var p Position
|
||||||
|
if r.format.Version > 1 {
|
||||||
|
p.Offset = buf.readUint64()
|
||||||
|
} else {
|
||||||
|
p.Offset = 4
|
||||||
|
}
|
||||||
|
p.File = string(buf.readStringEOF())
|
||||||
|
return p
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package blt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/localhots/pretty"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Rows contains a Rows Event.
|
||||||
|
type Rows struct {
|
||||||
|
EventType EventType
|
||||||
|
TableID uint64
|
||||||
|
Flags uint16
|
||||||
|
ExtraData []byte
|
||||||
|
ColumnCount uint64
|
||||||
|
ColumnBitmap1 []byte
|
||||||
|
ColumnBitmap2 []byte
|
||||||
|
Rows [][]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type rowsFlag uint16
|
||||||
|
|
||||||
|
const (
|
||||||
|
rowsFlagEndOfStatement rowsFlag = 0x0001
|
||||||
|
rowsFlagNoForeignKeyChecks rowsFlag = 0x0002
|
||||||
|
rowsFlagNoUniqueKeyChecks rowsFlag = 0x0004
|
||||||
|
rowsFlagRowHasColumns rowsFlag = 0x0008
|
||||||
|
|
||||||
|
freeTableMapID = 0x00FFFFFF
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *Reader) decodeRowsEvent(data []byte, typ EventType) {
|
||||||
|
// pretty.Println(data)
|
||||||
|
buf := newReadBuffer(data)
|
||||||
|
rows := Rows{EventType: typ}
|
||||||
|
idSize := r.format.tableIDSize(typ)
|
||||||
|
if idSize == 6 {
|
||||||
|
rows.TableID = buf.readUint48()
|
||||||
|
} else {
|
||||||
|
rows.TableID = uint64(buf.readUint32())
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.Flags = buf.readUint16()
|
||||||
|
|
||||||
|
if typ.isEither(WriteRowsEventV2, UpdateRowsEventV2, DeleteRowsEventV2) {
|
||||||
|
// Extra data length is part of extra data, deduct 2 bytes as they
|
||||||
|
// already store its length
|
||||||
|
extraLen := buf.readUint16() - 2
|
||||||
|
rows.ExtraData = buf.readStringVarLen(int(extraLen))
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.ColumnCount, _ = buf.readUintLenEnc()
|
||||||
|
rows.ColumnBitmap1 = buf.readStringVarLen(int(rows.ColumnCount+7) / 8)
|
||||||
|
if typ.isEither(UpdateRowsEventV2, UpdateRowsEventV1) {
|
||||||
|
rows.ColumnBitmap2 = buf.readStringVarLen(int(rows.ColumnCount+7) / 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
tm, ok := r.tableMap[rows.TableID]
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Errorf("Out of sync: no table map definition for ID=%d", rows.TableID))
|
||||||
|
}
|
||||||
|
|
||||||
|
pretty.Println(typ.String(), rows, tm, buf.cur())
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package blt
|
||||||
|
|
||||||
|
// TableMap ...
|
||||||
|
type TableMap struct {
|
||||||
|
TableID uint64
|
||||||
|
Flags uint16
|
||||||
|
SchemaName string
|
||||||
|
TableName string
|
||||||
|
ColumnCount uint64
|
||||||
|
ColumnTypes []byte
|
||||||
|
ColumnMeta []uint16
|
||||||
|
NullBitmask []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spec: https://dev.mysql.com/doc/internals/en/table-map-event.html
|
||||||
|
func (r *Reader) decodeTableMap(data []byte) TableMap {
|
||||||
|
buf := newReadBuffer(data)
|
||||||
|
var tm TableMap
|
||||||
|
idSize := r.format.tableIDSize(TableMapEvent)
|
||||||
|
if idSize == 6 {
|
||||||
|
tm.TableID = buf.readUint48()
|
||||||
|
} else {
|
||||||
|
tm.TableID = uint64(buf.readUint32())
|
||||||
|
}
|
||||||
|
|
||||||
|
tm.Flags = buf.readUint16()
|
||||||
|
tm.SchemaName = string(buf.readStringLenEnc())
|
||||||
|
buf.skip(1) // Always 0x00
|
||||||
|
tm.TableName = string(buf.readStringLenEnc())
|
||||||
|
buf.skip(1) // Always 0x00
|
||||||
|
tm.ColumnCount, _ = buf.readUintLenEnc()
|
||||||
|
tm.ColumnTypes = buf.readStringVarLen(int(tm.ColumnCount))
|
||||||
|
tm.ColumnMeta = decodeColumnMeta(buf.readStringLenEnc(), tm.ColumnTypes)
|
||||||
|
tm.NullBitmask = buf.readStringVarLen(int(tm.ColumnCount+8) / 7)
|
||||||
|
|
||||||
|
return tm
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeColumnMeta(data []byte, cols []byte) []uint16 {
|
||||||
|
pos := 0
|
||||||
|
meta := make([]uint16, len(cols))
|
||||||
|
for i, typ := range cols {
|
||||||
|
switch columnType(typ) {
|
||||||
|
case colTypeString, colTypeNewDecimal:
|
||||||
|
// TODO: Is that correct?
|
||||||
|
meta[i] = uint16(data[pos])<<8 | uint16(data[pos+1])
|
||||||
|
pos += 2
|
||||||
|
case colTypeVarchar, colTypeVarstring, colTypeBit:
|
||||||
|
// TODO: Is that correct?
|
||||||
|
meta[i] = decodeUint16(data[pos:])
|
||||||
|
pos += 2
|
||||||
|
case colTypeFloat, colTypeDouble, colTypeBlob, colTypeGeometry, colTypeJSON,
|
||||||
|
colTypeTime2, colTypeDatetime2, colTypeTimestamp2:
|
||||||
|
meta[i] = uint16(data[pos])
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return meta
|
||||||
|
}
|
|
@ -0,0 +1,212 @@
|
||||||
|
package blt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EventType defines a binary log event type.
|
||||||
|
type EventType byte
|
||||||
|
|
||||||
|
// Spec: https://dev.mysql.com/doc/internals/en/event-classes-and-types.html
|
||||||
|
const (
|
||||||
|
// UnknownEvent is an event that should never occur.
|
||||||
|
UnknownEvent EventType = 0
|
||||||
|
// StartEventV3 is the Start_event of binlog format 3.
|
||||||
|
StartEventV3 EventType = 1
|
||||||
|
// QueryEvent is created for each query that modifies the database, unless
|
||||||
|
// the query is logged row-based.
|
||||||
|
QueryEvent EventType = 2
|
||||||
|
// StopEvent is written to the log files under these circumstances:
|
||||||
|
// A master writes the event to the binary log when it shuts down.
|
||||||
|
// A slave writes the event to the relay log when it shuts down or when a
|
||||||
|
// RESET SLAVE statement is executed.
|
||||||
|
StopEvent EventType = 3
|
||||||
|
// RotateEvent is written at the end of the file that points to the next
|
||||||
|
// file in the squence. It is written when a binary log file exceeds a size
|
||||||
|
// limit.
|
||||||
|
RotateEvent EventType = 4
|
||||||
|
// IntvarEvent will be created just before a Query_event, if the query uses
|
||||||
|
// one of the variables LAST_INSERT_ID or INSERT_ID.
|
||||||
|
IntvarEvent EventType = 5
|
||||||
|
// LoadEvent ...
|
||||||
|
LoadEvent EventType = 6
|
||||||
|
// SlaveEvent ...
|
||||||
|
SlaveEvent EventType = 7
|
||||||
|
// CreateFileEvent ...
|
||||||
|
CreateFileEvent EventType = 8
|
||||||
|
// AppendBlockEvent is created to contain the file data.
|
||||||
|
AppendBlockEvent EventType = 9
|
||||||
|
// ExecLoadEvent ...
|
||||||
|
ExecLoadEvent EventType = 10
|
||||||
|
// DeleteFileEvent occurs when the LOAD DATA failed on the master.
|
||||||
|
// This event notifies the slave not to do the load and to delete the
|
||||||
|
// temporary file.
|
||||||
|
DeleteFileEvent EventType = 11
|
||||||
|
// NewLoadEvent ...
|
||||||
|
NewLoadEvent EventType = 12
|
||||||
|
// RandEvent logs random seed used by the next RAND(), and by PASSWORD()
|
||||||
|
// in 4.1.0.
|
||||||
|
RandEvent EventType = 13
|
||||||
|
// UserVarEvent is written every time a statement uses a user variable;
|
||||||
|
// precedes other events for the statement. Indicates the value to use for
|
||||||
|
// the user variable in the next statement. This is written only before a
|
||||||
|
// QUERY_EVENT and is not used with row-based logging.
|
||||||
|
UserVarEvent EventType = 14
|
||||||
|
// FormatDescriptionEvent is saved by threads which read it, as they need it
|
||||||
|
// for future use (to decode the ordinary events).
|
||||||
|
FormatDescriptionEvent EventType = 15
|
||||||
|
// XIDEvent is generated for a commit of a transaction that modifies one or
|
||||||
|
// more tables of an XA-capable storage engine.
|
||||||
|
XIDEvent EventType = 16
|
||||||
|
// BeginLoadQueryEvent is for the first block of file to be loaded, its only
|
||||||
|
// difference from Append_block event is that this event creates or
|
||||||
|
// truncates existing file before writing data.
|
||||||
|
BeginLoadQueryEvent EventType = 17
|
||||||
|
// ExecuteLoadQueryEvent is responsible for LOAD DATA execution, it similar
|
||||||
|
// to Query_event but before executing the query it substitutes original
|
||||||
|
// filename in LOAD DATA query with name of temporary file.
|
||||||
|
ExecuteLoadQueryEvent EventType = 18
|
||||||
|
// TableMapEvent is used in row-based mode where it preceeds every row
|
||||||
|
// operation event and maps a table definition to a number. The table
|
||||||
|
// definition consists of database name, table name, and column definitions.
|
||||||
|
TableMapEvent EventType = 19
|
||||||
|
// WriteRowsEventV0 represents inserted rows. Used in MySQL 5.1.0 to 5.1.15.
|
||||||
|
WriteRowsEventV0 EventType = 20
|
||||||
|
// UpdateRowsEventV0 represents updated rows. It contains both old and new
|
||||||
|
// versions. Used in MySQL 5.1.0 to 5.1.15.
|
||||||
|
UpdateRowsEventV0 EventType = 21
|
||||||
|
// DeleteRowsEventV0 represents deleted rows. Used in MySQL 5.1.0 to 5.1.15.
|
||||||
|
DeleteRowsEventV0 EventType = 22
|
||||||
|
// WriteRowsEventV1 represents inserted rows. Used in MySQL 5.1.15 to 5.6.
|
||||||
|
WriteRowsEventV1 EventType = 23
|
||||||
|
// UpdateRowsEventV1 represents updated rows. It contains both old and new
|
||||||
|
// versions. Used in MySQL 5.1.15 to 5.6.
|
||||||
|
UpdateRowsEventV1 EventType = 24
|
||||||
|
// DeleteRowsEventV1 represents deleted rows. Used in MySQL 5.1.15 to 5.6.
|
||||||
|
DeleteRowsEventV1 EventType = 25
|
||||||
|
// IncidentEvent represents an incident, an occurance out of the ordinary,
|
||||||
|
// that happened on the master. The event is used to inform the slave that
|
||||||
|
// something out of the ordinary happened on the master that might cause the
|
||||||
|
// database to be in an inconsistent state.
|
||||||
|
IncidentEvent EventType = 26
|
||||||
|
// HeartbeetEvent is a replication event used to ensure to slave that master
|
||||||
|
// is alive. The event is originated by master's dump thread and sent
|
||||||
|
// straight to slave without being logged. Slave itself does not store it in
|
||||||
|
// relay log but rather uses a data for immediate checks and throws away the
|
||||||
|
// event.
|
||||||
|
HeartbeetEvent EventType = 27
|
||||||
|
// IgnorableEvent is a kind of event that could be ignored.
|
||||||
|
IgnorableEvent EventType = 28
|
||||||
|
// RowsQueryEvent is a subclass of the IgnorableEvent, to record the
|
||||||
|
// original query for the rows events in RBR.
|
||||||
|
RowsQueryEvent EventType = 29
|
||||||
|
// WriteRowsEventV2 represents inserted rows. Used starting from MySQL 5.6.
|
||||||
|
WriteRowsEventV2 EventType = 30
|
||||||
|
// UpdateRowsEventV2 represents updated rows. It contains both old and new
|
||||||
|
// versions. Used starting from MySQL 5.6.
|
||||||
|
UpdateRowsEventV2 EventType = 31
|
||||||
|
// DeleteRowsEventV2 represents deleted rows. Used starting from MySQL 5.6.
|
||||||
|
DeleteRowsEventV2 EventType = 32
|
||||||
|
// GTIDEvent is an event that contains latest GTID.
|
||||||
|
// GTID stands for Global Transaction IDentifier It is composed of two
|
||||||
|
// parts:
|
||||||
|
// * SID for Source Identifier, and
|
||||||
|
// * GNO for Group Number. The basic idea is to associate an identifier, the
|
||||||
|
// Global Transaction IDentifier or GTID, to every transaction. When a
|
||||||
|
// transaction is copied to a slave, re-executed on the slave, and written
|
||||||
|
// to the slave's binary log, the GTID is preserved. When a slave connects
|
||||||
|
// to a master, the slave uses GTIDs instead of (file, offset).
|
||||||
|
GTIDEvent EventType = 33
|
||||||
|
// AnonymousGTIDEvent is a subclass of GTIDEvent.
|
||||||
|
AnonymousGTIDEvent EventType = 34
|
||||||
|
// PreviousGTIDsEvent is a subclass of GTIDEvent.
|
||||||
|
PreviousGTIDsEvent EventType = 35
|
||||||
|
)
|
||||||
|
|
||||||
|
func (et EventType) isEither(types ...EventType) bool {
|
||||||
|
for _, t := range types {
|
||||||
|
if et == t {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (et EventType) String() string {
|
||||||
|
switch et {
|
||||||
|
case UnknownEvent:
|
||||||
|
return "UnknownEvent"
|
||||||
|
case StartEventV3:
|
||||||
|
return "StartEventV3"
|
||||||
|
case QueryEvent:
|
||||||
|
return "QueryEvent"
|
||||||
|
case StopEvent:
|
||||||
|
return "StopEvent"
|
||||||
|
case RotateEvent:
|
||||||
|
return "RotateEvent"
|
||||||
|
case IntvarEvent:
|
||||||
|
return "IntvarEvent"
|
||||||
|
case LoadEvent:
|
||||||
|
return "LoadEvent"
|
||||||
|
case SlaveEvent:
|
||||||
|
return "SlaveEvent"
|
||||||
|
case CreateFileEvent:
|
||||||
|
return "CreateFileEvent"
|
||||||
|
case AppendBlockEvent:
|
||||||
|
return "AppendBlockEvent"
|
||||||
|
case ExecLoadEvent:
|
||||||
|
return "ExecLoadEvent"
|
||||||
|
case DeleteFileEvent:
|
||||||
|
return "DeleteFileEvent"
|
||||||
|
case NewLoadEvent:
|
||||||
|
return "NewLoadEvent"
|
||||||
|
case RandEvent:
|
||||||
|
return "RandEvent"
|
||||||
|
case UserVarEvent:
|
||||||
|
return "UserVarEvent"
|
||||||
|
case FormatDescriptionEvent:
|
||||||
|
return "FormatDescriptionEvent"
|
||||||
|
case XIDEvent:
|
||||||
|
return "XIDEvent"
|
||||||
|
case BeginLoadQueryEvent:
|
||||||
|
return "BeginLoadQueryEvent"
|
||||||
|
case ExecuteLoadQueryEvent:
|
||||||
|
return "ExecuteLoadQueryEvent"
|
||||||
|
case TableMapEvent:
|
||||||
|
return "TableMapEvent"
|
||||||
|
case WriteRowsEventV0:
|
||||||
|
return "WriteRowsEventV0"
|
||||||
|
case UpdateRowsEventV0:
|
||||||
|
return "UpdateRowsEventV0"
|
||||||
|
case DeleteRowsEventV0:
|
||||||
|
return "DeleteRowsEventV0"
|
||||||
|
case WriteRowsEventV1:
|
||||||
|
return "WriteRowsEventV1"
|
||||||
|
case UpdateRowsEventV1:
|
||||||
|
return "UpdateRowsEventV1"
|
||||||
|
case DeleteRowsEventV1:
|
||||||
|
return "DeleteRowsEventV1"
|
||||||
|
case IncidentEvent:
|
||||||
|
return "IncidentEvent"
|
||||||
|
case HeartbeetEvent:
|
||||||
|
return "HeartbeetEvent"
|
||||||
|
case IgnorableEvent:
|
||||||
|
return "IgnorableEvent"
|
||||||
|
case RowsQueryEvent:
|
||||||
|
return "RowsQueryEvent"
|
||||||
|
case WriteRowsEventV2:
|
||||||
|
return "WriteRowsEventV2"
|
||||||
|
case UpdateRowsEventV2:
|
||||||
|
return "UpdateRowsEventV2"
|
||||||
|
case DeleteRowsEventV2:
|
||||||
|
return "DeleteRowsEventV2"
|
||||||
|
case GTIDEvent:
|
||||||
|
return "GTIDEvent"
|
||||||
|
case AnonymousGTIDEvent:
|
||||||
|
return "AnonymousGTIDEvent"
|
||||||
|
case PreviousGTIDsEvent:
|
||||||
|
return "PreviousGTIDsEvent"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("Unknown(%d)", et)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,197 @@
|
||||||
|
package blt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql/driver"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/juju/errors"
|
||||||
|
"github.com/localhots/gobelt/log"
|
||||||
|
"github.com/localhots/mysql"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reader ...
|
||||||
|
type Reader struct {
|
||||||
|
conn *mysql.ExtendedConn
|
||||||
|
conf Config
|
||||||
|
state Position
|
||||||
|
format FormatDescription
|
||||||
|
tableMap map[uint64]TableMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config ...
|
||||||
|
type Config struct {
|
||||||
|
ServerID uint32
|
||||||
|
File string
|
||||||
|
Offset uint32
|
||||||
|
Hostname string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position ...
|
||||||
|
type Position struct {
|
||||||
|
File string
|
||||||
|
Offset uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Bytes
|
||||||
|
resultOK byte = 0x00
|
||||||
|
resultEOF byte = 0xFE
|
||||||
|
resultERR byte = 0xFF
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewReader ...
|
||||||
|
func NewReader(conn driver.Conn, conf Config) (*Reader, error) {
|
||||||
|
if conf.Hostname == "" {
|
||||||
|
name, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
conf.Hostname = name
|
||||||
|
}
|
||||||
|
|
||||||
|
extconn, err := mysql.ExtendConn(conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r := &Reader{
|
||||||
|
conn: extconn,
|
||||||
|
conf: conf,
|
||||||
|
tableMap: make(map[uint64]TableMap),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.disableChecksum(); err != nil {
|
||||||
|
return nil, errors.Annotate(err, "Failed to disable binlog checksum")
|
||||||
|
}
|
||||||
|
if err := r.registerSlave(); err != nil {
|
||||||
|
return nil, errors.Annotate(err, "Failed to register slave server")
|
||||||
|
}
|
||||||
|
if err := r.binlogDump(); err != nil {
|
||||||
|
return nil, errors.Annotate(err, "Failed to start binlog dump")
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect ...
|
||||||
|
func Connect(dsn string, conf Config) (*Reader, error) {
|
||||||
|
conn, err := (&mysql.MySQLDriver{}).Open(dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewReader(conn, conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadEventHeader reads next event from the log and decodes its header. Header
|
||||||
|
// is then used to decode the event.
|
||||||
|
func (r *Reader) ReadEventHeader(ctx context.Context) (*EventHeader, error) {
|
||||||
|
data, err := r.conn.ReadPacket()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch data[0] {
|
||||||
|
case resultOK:
|
||||||
|
return r.parseHeader(data[1:])
|
||||||
|
case resultERR:
|
||||||
|
return nil, r.conn.HandleErrorPacket(data)
|
||||||
|
case resultEOF:
|
||||||
|
log.Debug(ctx, "EOF received")
|
||||||
|
return nil, nil
|
||||||
|
default:
|
||||||
|
log.Errorf(ctx, "Unexpected header: %x", data[0])
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spec: https://dev.mysql.com/doc/internals/en/com-register-slave.html
|
||||||
|
func (r *Reader) registerSlave() error {
|
||||||
|
const comRegisterSlave byte = 21
|
||||||
|
r.conn.ResetSequence()
|
||||||
|
|
||||||
|
buf := newCommandBuffer(1 + 4 + 1 + len(r.conf.Hostname) + 1 + 1 + 2 + 4 + 4)
|
||||||
|
buf.writeByte(comRegisterSlave)
|
||||||
|
buf.writeUint32(r.conf.ServerID)
|
||||||
|
buf.writeString(r.conf.Hostname)
|
||||||
|
// The rest of the payload would be zeroes, consider following code for
|
||||||
|
// reference:
|
||||||
|
//
|
||||||
|
// buf.writeString(username)
|
||||||
|
// buf.writeString(password)
|
||||||
|
// buf.writeUint16(port)
|
||||||
|
// buf.writeUint32(replicationRank)
|
||||||
|
// buf.writeUint32(masterID)
|
||||||
|
|
||||||
|
return r.runCmd(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spec: https://dev.mysql.com/doc/internals/en/com-binlog-dump.html
|
||||||
|
// TODO: https://dev.mysql.com/doc/internals/en/com-binlog-dump-gtid.html
|
||||||
|
func (r *Reader) binlogDump() error {
|
||||||
|
const comBinlogDump byte = 18
|
||||||
|
r.conn.ResetSequence()
|
||||||
|
|
||||||
|
r.state.File = r.conf.File
|
||||||
|
r.state.Offset = uint64(r.conf.Offset)
|
||||||
|
// First event offset is 4
|
||||||
|
if r.state.Offset < 4 {
|
||||||
|
r.state.Offset = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := newCommandBuffer(1 + 4 + 2 + 4 + len(r.state.File))
|
||||||
|
buf.writeByte(comBinlogDump)
|
||||||
|
buf.writeUint32(uint32(r.state.Offset))
|
||||||
|
buf.skip(2) // Flags
|
||||||
|
buf.writeUint32(r.conf.ServerID)
|
||||||
|
buf.writeStringEOF(r.state.File)
|
||||||
|
|
||||||
|
return r.runCmd(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) runCmd(buf *buffer) error {
|
||||||
|
err := r.conn.WritePacket(buf.data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return r.conn.ReadResultOK()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) disableChecksum() error {
|
||||||
|
cs, err := r.getVar("BINLOG_CHECKSUM")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cs != "NONE" {
|
||||||
|
return r.setVar("@master_binlog_checksum", "NONE")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) getVar(name string) (string, error) {
|
||||||
|
rows, err := r.conn.Query(fmt.Sprintf("SHOW VARIABLES LIKE %q", name), []driver.Value{})
|
||||||
|
if err != nil {
|
||||||
|
return "", notEOF(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
res := make([]driver.Value, len(rows.Columns()))
|
||||||
|
err = rows.Next(res)
|
||||||
|
if err != nil {
|
||||||
|
return "", notEOF(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(res[1].([]byte)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) setVar(name, val string) error {
|
||||||
|
return r.conn.Exec(fmt.Sprintf("SET %s=%q", name, val))
|
||||||
|
}
|
||||||
|
|
||||||
|
func notEOF(err error) error {
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
Loading…
Reference in New Issue