├── .gitignore ├── src ├── main.go ├── user.go ├── binlog.go ├── parser.go └── parser_test.go ├── migration └── user.sql ├── go.mod ├── README.md └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | .idea/ 3 | config/config.yml 4 | logs/ 5 | bin/ 6 | build/ 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /src/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func main() { 9 | go binlogListener() 10 | 11 | time.Sleep(2 * time.Minute) 12 | fmt.Print("Thx for watching, goodbuy") 13 | } 14 | -------------------------------------------------------------------------------- /src/user.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "time" 4 | 5 | type User struct { 6 | Id int `gorm:"column:id"` 7 | Name string `gorm:"column:name"` 8 | Status string `gorm:"column:status"` 9 | Created time.Time `gorm:"column:created"` 10 | } 11 | 12 | func (User) TableName() string { 13 | return "User" 14 | } 15 | 16 | func (User) SchemaName() string { 17 | return "Test" 18 | } 19 | -------------------------------------------------------------------------------- /migration/user.sql: -------------------------------------------------------------------------------- 1 | create table Test.User 2 | ( 3 | id int auto_increment primary key, 4 | name varchar(40) null, 5 | status enum("active","deleted") DEFAULT "active", 6 | created timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP 7 | ) 8 | engine=InnoDB; 9 | 10 | 11 | INSERT Into Test.User (`id`,`name`) VALUE (1,"Jack"); 12 | UPDATE Test.User SET name="Jonh" WHERE id=1; 13 | DELETE FROM Test.User WHERE id=1; -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/JackShadow/go-binlog-example 2 | 3 | require ( 4 | github.com/jmoiron/sqlx v1.2.0 // indirect 5 | github.com/json-iterator/go v1.1.6 6 | github.com/kr/pretty v0.1.0 // indirect 7 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 8 | github.com/modern-go/reflect2 v1.0.1 // indirect 9 | github.com/pingcap/check v0.0.0-20190102082844-67f458068fc8 // indirect 10 | github.com/pkg/errors v0.8.1 // indirect 11 | github.com/siddontang/go-mysql v0.0.0-20190312052122-c6ab05a85eb8 12 | github.com/stretchr/testify v1.3.0 // indirect 13 | google.golang.org/appengine v1.5.0 // indirect 14 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Golang binglog example project 2 | 3 | tested on go1.11.2 and mariadb10.2 (binlog format must be 'raw') 4 | In binlog.go change with your connection data 5 | ``` 6 | cfg.Addr = fmt.Sprintf("%s:%d", "127.0.0.1", 3306) //host,port 7 | cfg.User = "root" 8 | cfg.Password = "root" 9 | ``` 10 | 11 | Create database with sql 12 | ``` 13 | create table Test.User 14 | ( 15 | id int auto_increment primary key, 16 | name varchar(40) null, 17 | status enum("active","deleted") DEFAULT "active", 18 | created timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP 19 | ) 20 | engine=InnoDB; 21 | ``` 22 | 23 | ``` 24 | go get 25 | go build src/* 26 | ./binlog 27 | ``` 28 | than execute sql 29 | ``` 30 | INSERT Into Test.User (`id`,`name`) VALUE (1,"Jack"); 31 | UPDATE Test.User SET name="Jonh" WHERE id=1; 32 | DELETE FROM Test.User WHERE id=1; 33 | 34 | ``` 35 | 36 | and you'll see 37 | ``` 38 | User 1 is created with name Jack 39 | User 1 is updated from name Jack to name Jonh 40 | User 1 is deleted with name Jonh 41 | ``` 42 | -------------------------------------------------------------------------------- /src/binlog.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/siddontang/go-mysql/canal" 6 | "runtime/debug" 7 | ) 8 | 9 | type binlogHandler struct { 10 | canal.DummyEventHandler 11 | BinlogParser 12 | } 13 | 14 | func (h *binlogHandler) OnRow(e *canal.RowsEvent) error { 15 | defer func() { 16 | if r := recover(); r != nil { 17 | fmt.Print(r, " ", string(debug.Stack())) 18 | } 19 | }() 20 | 21 | // base value for canal.DeleteAction or canal.InsertAction 22 | var n = 0 23 | var k = 1 24 | 25 | if e.Action == canal.UpdateAction { 26 | n = 1 27 | k = 2 28 | } 29 | 30 | for i := n; i < len(e.Rows); i += k { 31 | 32 | key := e.Table.Schema + "." + e.Table.Name 33 | 34 | switch key { 35 | case User{}.SchemaName() + "." + User{}.TableName(): 36 | user := User{} 37 | h.GetBinLogData(&user, e, i) 38 | switch e.Action { 39 | case canal.UpdateAction: 40 | oldUser := User{} 41 | h.GetBinLogData(&oldUser, e, i-1) 42 | fmt.Printf("User %d name changed from %s to %s\n", user.Id, oldUser.Name, user.Name) 43 | case canal.InsertAction: 44 | fmt.Printf("User %d is created with name %s\n", user.Id, user.Name) 45 | case canal.DeleteAction: 46 | fmt.Printf("User %d is deleted with name %s\n", user.Id, user.Name) 47 | default: 48 | fmt.Printf("Unknown action") 49 | } 50 | } 51 | 52 | } 53 | return nil 54 | } 55 | 56 | func (h *binlogHandler) String() string { 57 | return "binlogHandler" 58 | } 59 | 60 | func binlogListener() { 61 | c, err := getDefaultCanal() 62 | if err == nil { 63 | coords, err := c.GetMasterPos() 64 | if err == nil { 65 | c.SetEventHandler(&binlogHandler{}) 66 | c.RunFrom(coords) 67 | } 68 | } 69 | } 70 | 71 | func getDefaultCanal() (*canal.Canal, error) { 72 | cfg := canal.NewDefaultConfig() 73 | cfg.Addr = fmt.Sprintf("%s:%d", "mariadb", 3307) 74 | cfg.User = "root" 75 | cfg.Password = "root" 76 | cfg.Flavor = "mysql" 77 | 78 | cfg.Dump.ExecutionPath = "" 79 | 80 | return canal.NewCanal(cfg) 81 | } 82 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= 6 | github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 7 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 8 | github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= 9 | github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= 10 | github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= 11 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 12 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 13 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 14 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 15 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 16 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 17 | github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= 18 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 19 | github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4= 20 | github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 21 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 22 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 23 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 24 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 25 | github.com/pingcap/check v0.0.0-20190102082844-67f458068fc8 h1:USx2/E1bX46VG32FIw034Au6seQ2fY9NEILmNh/UlQg= 26 | github.com/pingcap/check v0.0.0-20190102082844-67f458068fc8/go.mod h1:B1+S9LNcuMyLH/4HMTViQOJevkGiik3wW2AN9zb2fNQ= 27 | github.com/pingcap/errors v0.11.0 h1:DCJQB8jrHbQ1VVlMFIrbj2ApScNNotVmkSNplu2yUt4= 28 | github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= 29 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 30 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 31 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 32 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 33 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 34 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 35 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE= 36 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 37 | github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726 h1:xT+JlYxNGqyT+XcU8iUrN18JYed2TvG9yN5ULG2jATM= 38 | github.com/siddontang/go v0.0.0-20180604090527-bdc77568d726/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw= 39 | github.com/siddontang/go-log v0.0.0-20180807004314-8d05993dda07 h1:oI+RNwuC9jF2g2lP0u0cVEEZrc/AYBCuFdvwrLWM/6Q= 40 | github.com/siddontang/go-log v0.0.0-20180807004314-8d05993dda07/go.mod h1:yFdBgwXP24JziuRl2NMUahT7nGLNOKi1SIiFxMttVD4= 41 | github.com/siddontang/go-mysql v0.0.0-20190312052122-c6ab05a85eb8 h1:8puKTg/UOIQ+ZiowY1ywmGsI08sWqrKD7HJ/j165CUM= 42 | github.com/siddontang/go-mysql v0.0.0-20190312052122-c6ab05a85eb8/go.mod h1:/b8ZcWjAShCcHp2dWpjb1vTlNyiG03UeHEQr2jteOpI= 43 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 44 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 45 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 46 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 47 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 48 | google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= 49 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 50 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 51 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 52 | -------------------------------------------------------------------------------- /src/parser.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/json-iterator/go" 6 | "github.com/siddontang/go-mysql/canal" 7 | "github.com/siddontang/go-mysql/schema" 8 | "reflect" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | type BinlogParser struct{} 14 | 15 | func (m *BinlogParser) GetBinLogData(element interface{}, e *canal.RowsEvent, n int) error { 16 | var columnName string 17 | var ok bool 18 | v := reflect.ValueOf(element) 19 | s := reflect.Indirect(v) 20 | t := s.Type() 21 | num := t.NumField() 22 | for k := 0; k < num; k++ { 23 | parsedTag := parseTagSetting(t.Field(k).Tag) 24 | name := s.Field(k).Type().Name() 25 | 26 | if columnName, ok = parsedTag["COLUMN"]; !ok || columnName == "COLUMN" { 27 | continue 28 | } 29 | 30 | switch name { 31 | case "bool": 32 | s.Field(k).SetBool(m.boolHelper(e, n, columnName)) 33 | case "int": 34 | s.Field(k).SetInt(m.intHelper(e, n, columnName)) 35 | case "string": 36 | s.Field(k).SetString(m.stringHelper(e, n, columnName)) 37 | case "Time": 38 | timeVal := m.dateTimeHelper(e, n, columnName) 39 | s.Field(k).Set(reflect.ValueOf(timeVal)) 40 | case "float64": 41 | s.Field(k).SetFloat(m.floatHelper(e, n, columnName)) 42 | default: 43 | if _, ok := parsedTag["FROMJSON"]; ok { 44 | 45 | newObject := reflect.New(s.Field(k).Type()).Interface() 46 | json := m.stringHelper(e, n, columnName) 47 | 48 | jsoniter.Unmarshal([]byte(json), &newObject) 49 | 50 | s.Field(k).Set(reflect.ValueOf(newObject).Elem().Convert(s.Field(k).Type())) 51 | } 52 | } 53 | } 54 | return nil 55 | } 56 | func (m *BinlogParser) dateTimeHelper(e *canal.RowsEvent, n int, columnName string) time.Time { 57 | 58 | columnId := m.getBinlogIdByName(e, columnName) 59 | if e.Table.Columns[columnId].Type != schema.TYPE_TIMESTAMP { 60 | panic("Not dateTime type") 61 | } 62 | t, _ := time.Parse("2006-01-02 15:04:05", e.Rows[n][columnId].(string)) 63 | 64 | return t 65 | } 66 | 67 | func (m *BinlogParser) intHelper(e *canal.RowsEvent, n int, columnName string) int64 { 68 | 69 | columnId := m.getBinlogIdByName(e, columnName) 70 | if e.Table.Columns[columnId].Type != schema.TYPE_NUMBER { 71 | return 0 72 | } 73 | 74 | switch e.Rows[n][columnId].(type) { 75 | case int8: 76 | return int64(e.Rows[n][columnId].(int8)) 77 | case int32: 78 | return int64(e.Rows[n][columnId].(int32)) 79 | case int64: 80 | return e.Rows[n][columnId].(int64) 81 | case int: 82 | return int64(e.Rows[n][columnId].(int)) 83 | case uint8: 84 | return int64(e.Rows[n][columnId].(uint8)) 85 | case uint16: 86 | return int64(e.Rows[n][columnId].(uint16)) 87 | case uint32: 88 | return int64(e.Rows[n][columnId].(uint32)) 89 | case uint64: 90 | return int64(e.Rows[n][columnId].(uint64)) 91 | case uint: 92 | return int64(e.Rows[n][columnId].(uint)) 93 | } 94 | return 0 95 | } 96 | 97 | func (m *BinlogParser) floatHelper(e *canal.RowsEvent, n int, columnName string) float64 { 98 | 99 | columnId := m.getBinlogIdByName(e, columnName) 100 | if e.Table.Columns[columnId].Type != schema.TYPE_FLOAT { 101 | panic("Not float type") 102 | } 103 | 104 | switch e.Rows[n][columnId].(type) { 105 | case float32: 106 | return float64(e.Rows[n][columnId].(float32)) 107 | case float64: 108 | return float64(e.Rows[n][columnId].(float64)) 109 | } 110 | return float64(0) 111 | } 112 | 113 | func (m *BinlogParser) boolHelper(e *canal.RowsEvent, n int, columnName string) bool { 114 | 115 | val := m.intHelper(e, n, columnName) 116 | if val == 1 { 117 | return true 118 | } 119 | return false 120 | } 121 | 122 | func (m *BinlogParser) stringHelper(e *canal.RowsEvent, n int, columnName string) string { 123 | 124 | columnId := m.getBinlogIdByName(e, columnName) 125 | if e.Table.Columns[columnId].Type == schema.TYPE_ENUM { 126 | 127 | values := e.Table.Columns[columnId].EnumValues 128 | if len(values) == 0 { 129 | return "" 130 | } 131 | if e.Rows[n][columnId] == nil { 132 | //Если в енум лежит нуул ставим пустую строку 133 | return "" 134 | } 135 | 136 | return values[e.Rows[n][columnId].(int64)-1] 137 | } 138 | 139 | value := e.Rows[n][columnId] 140 | 141 | switch value := value.(type) { 142 | case []byte: 143 | return string(value) 144 | case string: 145 | return value 146 | } 147 | return "" 148 | } 149 | 150 | func (m *BinlogParser) getBinlogIdByName(e *canal.RowsEvent, name string) int { 151 | for id, value := range e.Table.Columns { 152 | if value.Name == name { 153 | return id 154 | } 155 | } 156 | panic(fmt.Sprintf("There is no column %s in table %s.%s", name, e.Table.Schema, e.Table.Name)) 157 | } 158 | 159 | func parseTagSetting(tags reflect.StructTag) map[string]string { 160 | settings := map[string]string{} 161 | for _, str := range []string{tags.Get("sql"), tags.Get("gorm")} { 162 | tags := strings.Split(str, ";") 163 | for _, value := range tags { 164 | v := strings.Split(value, ":") 165 | k := strings.TrimSpace(strings.ToUpper(v[0])) 166 | if len(v) >= 2 { 167 | settings[k] = strings.Join(v[1:], ":") 168 | } else { 169 | settings[k] = k 170 | } 171 | } 172 | } 173 | return settings 174 | } 175 | -------------------------------------------------------------------------------- /src/parser_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/siddontang/go-mysql/canal" 5 | "github.com/siddontang/go-mysql/schema" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestCommonHandler_GetBinLogDataGetBinLogData_Insert(t *testing.T) { 11 | 12 | model := binlogTestStruct{} 13 | handler := BinlogParser{} 14 | rows := make([][]interface{}, 1) 15 | insertRows := make([]interface{}, 8) 16 | insertRows[0] = 1 17 | insertRows[1] = 1 18 | insertRows[2] = 1.123 19 | insertRows[3] = int64(1) 20 | insertRows[4] = "test text" 21 | insertRows[5] = "2018-02-16 14:28:09" 22 | insertRows[6] = nil 23 | insertRows[7] = []byte("test text") 24 | rows[0] = insertRows 25 | 26 | columns := make([]schema.TableColumn, 8) 27 | 28 | columns[0] = schema.TableColumn{Name: "int", Type: schema.TYPE_NUMBER} 29 | columns[1] = schema.TableColumn{Name: "bool", Type: schema.TYPE_NUMBER} 30 | columns[2] = schema.TableColumn{Name: "float", Type: schema.TYPE_FLOAT} 31 | columns[3] = schema.TableColumn{Name: "enum", Type: schema.TYPE_ENUM, EnumValues: []string{"Active", "Deleted"}} 32 | columns[4] = schema.TableColumn{Name: "string", Type: schema.TYPE_STRING} 33 | columns[5] = schema.TableColumn{Name: "time", Type: schema.TYPE_TIMESTAMP} 34 | columns[6] = schema.TableColumn{Name: "enum_null", Type: schema.TYPE_ENUM, EnumValues: []string{"Active", "Deleted"}} 35 | columns[7] = schema.TableColumn{Name: "byte_text", Type: schema.TYPE_STRING} 36 | table := schema.Table{Schema: "test", Name: "test", Columns: columns} 37 | 38 | e := canal.RowsEvent{Table: &table, Action: canal.InsertAction, Rows: rows} 39 | handler.GetBinLogData(&model, &e, 0) 40 | 41 | if model.Int != insertRows[0] { 42 | t.Errorf("Int value did not update.") 43 | } 44 | if model.Bool != true { 45 | t.Errorf("Bool value did not update.") 46 | } 47 | if model.Float != insertRows[2] { 48 | t.Errorf("Float value did not update.") 49 | } 50 | if model.Enum != "Active" { 51 | t.Errorf("Enum value did not update.") 52 | } 53 | if model.String != insertRows[4] { 54 | t.Errorf("String value did not update.") 55 | } 56 | timeValue, _ := time.Parse("2006-01-02 15:04:05", insertRows[5].(string)) 57 | if model.Time.Unix() != timeValue.Unix() { 58 | t.Errorf("DateTime value did not update.") 59 | } 60 | if model.EnumNull != "" { 61 | t.Errorf("Null enum was not parsed as string.") 62 | } 63 | 64 | } 65 | 66 | func TestCommonHandler_GetBinLogDataGetBinLogData_Update(t *testing.T) { 67 | 68 | model := binlogTestStruct{} 69 | handler := BinlogParser{} 70 | rows := make([][]interface{}, 2) 71 | insertRows := make([]interface{}, 8) 72 | insertRows[0] = 1 73 | insertRows[1] = 0 74 | insertRows[2] = 1.123 75 | insertRows[3] = int64(1) 76 | insertRows[4] = "test text" 77 | insertRows[5] = "2018-02-16 14:28:09" 78 | insertRows[6] = int64(1) 79 | insertRows[7] = []byte("test text") 80 | rows[0] = insertRows 81 | updateRows := make([]interface{}, 8) 82 | updateRows[0] = 3 83 | updateRows[1] = 1 84 | updateRows[2] = 2.234 85 | updateRows[3] = int64(2) 86 | updateRows[4] = "test2 text2" 87 | updateRows[5] = "2018-02-16 15:28:09" 88 | updateRows[6] = nil 89 | updateRows[7] = []byte("test2 text2") 90 | rows[1] = updateRows 91 | columns := make([]schema.TableColumn, 8) 92 | 93 | columns[0] = schema.TableColumn{Name: "int", Type: schema.TYPE_NUMBER} 94 | columns[1] = schema.TableColumn{Name: "bool", Type: schema.TYPE_NUMBER} 95 | columns[2] = schema.TableColumn{Name: "float", Type: schema.TYPE_FLOAT} 96 | columns[3] = schema.TableColumn{Name: "enum", Type: schema.TYPE_ENUM, EnumValues: []string{"Active", "Deleted"}} 97 | columns[4] = schema.TableColumn{Name: "string", Type: schema.TYPE_STRING} 98 | columns[5] = schema.TableColumn{Name: "time", Type: schema.TYPE_TIMESTAMP} 99 | columns[6] = schema.TableColumn{Name: "enum_null", Type: schema.TYPE_ENUM, EnumValues: []string{"Active", "Deleted"}} 100 | columns[7] = schema.TableColumn{Name: "byte_text", Type: schema.TYPE_STRING} 101 | table := schema.Table{Schema: "test", Name: "test", Columns: columns} 102 | 103 | e := canal.RowsEvent{Table: &table, Action: canal.UpdateAction, Rows: rows} 104 | handler.GetBinLogData(&model, &e, 1) 105 | 106 | if model.Int != updateRows[0] { 107 | t.Errorf("Int value did not update.") 108 | } 109 | if model.Bool != true { 110 | t.Errorf("Bool value did not update.") 111 | } 112 | if model.Float != updateRows[2] { 113 | t.Errorf("Float value did not update.") 114 | } 115 | if model.Enum != "Deleted" { 116 | t.Errorf("Enum value did not update.") 117 | } 118 | if model.String != updateRows[4] { 119 | t.Errorf("String value did not update.") 120 | } 121 | timeValue, _ := time.Parse("2006-01-02 15:04:05", updateRows[5].(string)) 122 | if model.Time.Unix() != timeValue.Unix() { 123 | t.Errorf("DateTime value did not update.") 124 | } 125 | if model.String != updateRows[4].(string) { 126 | t.Errorf("String value did not update.") 127 | } 128 | if model.EnumNull != "" { 129 | t.Errorf("Enum nulled did not update.") 130 | } 131 | 132 | } 133 | 134 | func TestPanic(t *testing.T) { 135 | defer func() { 136 | if r := recover(); r == nil { 137 | t.Errorf("The code did not panic") 138 | } 139 | }() 140 | 141 | model := binlogInvalidStruct{} 142 | handler := BinlogParser{} 143 | rows := make([][]interface{}, 1) 144 | insertRows := make([]interface{}, 6) 145 | insertRows[0] = 1 146 | 147 | rows[0] = insertRows 148 | 149 | columns := make([]schema.TableColumn, 6) 150 | 151 | columns[0] = schema.TableColumn{Name: "int", Type: schema.TYPE_NUMBER} 152 | 153 | table := schema.Table{Schema: "test", Name: "test", Columns: columns} 154 | 155 | e := canal.RowsEvent{Table: &table, Action: canal.InsertAction, Rows: rows} 156 | handler.GetBinLogData(&model, &e, 0) 157 | } 158 | 159 | func TestJson(t *testing.T) { 160 | model := JsonData{} 161 | handler := BinlogParser{} 162 | rows := make([][]interface{}, 1) 163 | insertRows := make([]interface{}, 6) 164 | insertRows[0] = 1 165 | insertRows[1] = `{"int":1,"test":"test"}` 166 | insertRows[2] = `{"a":"a","b":"b"}` 167 | insertRows[3] = `[2,4,6}` 168 | rows[0] = insertRows 169 | 170 | columns := make([]schema.TableColumn, 6) 171 | 172 | columns[0] = schema.TableColumn{Name: "int", Type: schema.TYPE_NUMBER} 173 | columns[1] = schema.TableColumn{Name: "struct_data", Type: schema.TYPE_STRING} 174 | columns[2] = schema.TableColumn{Name: "map_data", Type: schema.TYPE_STRING} 175 | columns[3] = schema.TableColumn{Name: "slice_data", Type: schema.TYPE_STRING} 176 | table := schema.Table{Schema: "test", Name: "test", Columns: columns} 177 | 178 | e := canal.RowsEvent{Table: &table, Action: canal.InsertAction, Rows: rows} 179 | handler.GetBinLogData(&model, &e, 0) 180 | if model.StructData.Test != "test" || model.StructData.Int != 1 { 181 | t.Errorf("Struct from json parsing failed.") 182 | } 183 | if len(model.SliceData) != 3 || model.SliceData[0] != 2 || model.SliceData[2] != 6 { 184 | t.Errorf("Sliced json parsing failed.") 185 | } 186 | 187 | if val, ok := model.MapData["a"]; ok && val != "a" && len(model.MapData) != 2 { 188 | t.Errorf("Map json parsing failed.") 189 | } 190 | } 191 | 192 | type binlogTestStruct struct { 193 | Int int `gorm:"column:int"` 194 | Bool bool `gorm:"column:bool"` 195 | Float float64 `gorm:"column:float"` 196 | Enum string `gorm:"column:enum"` 197 | String string `gorm:"column:string"` 198 | Time time.Time `gorm:"column:time"` 199 | EnumNull string `gorm:"column:enum_null"` 200 | ByteText string `gorm:"column:byte_text"` 201 | WillNotParse int 202 | WillNotParseAlt int `gorm:"column"` 203 | } 204 | 205 | type binlogInvalidStruct struct { 206 | Int int `gorm:"column:id"` 207 | } 208 | 209 | type JsonData struct { 210 | Int int `gorm:"column:int"` 211 | StructData TestData `gorm:"column:struct_data;fromJson"` 212 | MapData map[string]string `gorm:"column:map_data;fromJson"` 213 | SliceData []int `gorm:"column:slice_data;fromJson"` 214 | } 215 | 216 | type TestData struct { 217 | Test string `json:"test"` 218 | Int int `json:"int"` 219 | } 220 | --------------------------------------------------------------------------------