diff --git a/mysql/mysql_gtid.go b/mysql/mysql_gtid.go index 20b26ffc9..d83a1f6cf 100644 --- a/mysql/mysql_gtid.go +++ b/mysql/mysql_gtid.go @@ -193,31 +193,61 @@ type UUIDSet struct { Intervals IntervalSlice } -func ParseUUIDSet(str string) (*UUIDSet, error) { - str = strings.TrimSpace(str) - sep := strings.Split(str, ":") - if len(sep) < 2 { - return nil, errors.Errorf("invalid GTID format, must UUID:interval[:interval]") +// ParseUUIDSet parses a GTID set string into a map of UUIDSet structs keyed by their SID. +// Supports multi-UUID sets like "uuid1:1-10,uuid2:5-15". +func ParseUUIDSet(s string) (map[string]*UUIDSet, error) { + if s == "" { + return nil, nil } - var err error - s := new(UUIDSet) - if s.SID, err = uuid.Parse(sep[0]); err != nil { - return nil, errors.Trace(err) + uuidSets := strings.Split(strings.TrimSpace(s), ",") + if len(uuidSets) == 0 { + return nil, fmt.Errorf("empty UUID set") } - // Handle interval - for i := 1; i < len(sep); i++ { - if in, err := parseInterval(sep[i]); err != nil { - return nil, errors.Trace(err) - } else { - s.Intervals = append(s.Intervals, in) + result := make(map[string]*UUIDSet) + for _, set := range uuidSets { + set = strings.TrimSpace(set) + if set == "" { + continue } - } - s.Intervals = s.Intervals.Normalize() + parts := strings.SplitN(set, ":", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid UUID set format: %s", set) + } - return s, nil + sid, err := uuid.Parse(parts[0]) + if err != nil { + return nil, fmt.Errorf("invalid UUID: %s", parts[0]) + } + + // Check if this SID already exists in the map + uuidSet, exists := result[sid.String()] + if !exists { + uuidSet = &UUIDSet{SID: sid} + result[sid.String()] = uuidSet + } + + intervals := strings.Split(parts[1], ":") + for _, intervalStr := range intervals { + interval, err := parseInterval(intervalStr) + if err != nil { + return nil, fmt.Errorf("invalid interval in UUID set %s: %v", set, err) + } + uuidSet.Intervals = append(uuidSet.Intervals, interval) + } + + if len(uuidSet.Intervals) == 0 { + return nil, fmt.Errorf("no valid intervals in UUID set: %s", set) + } + uuidSet.Intervals = uuidSet.Intervals.Normalize() // Normalize intervals after adding + } + + if len(result) == 0 { + return nil, fmt.Errorf("no valid UUID sets parsed from: %s", s) + } + return result, nil } func NewUUIDSet(sid uuid.UUID, in ...Interval) *UUIDSet { @@ -390,15 +420,12 @@ func ParseMysqlGTIDSet(str string) (GTIDSet, error) { return s, nil } - sp := strings.Split(str, ",") - - // todo, handle redundant same uuid - for i := 0; i < len(sp); i++ { - if set, err := ParseUUIDSet(sp[i]); err != nil { - return nil, errors.Trace(err) - } else { - s.AddSet(set) - } + sets, err := ParseUUIDSet(str) // Use the updated ParseUUIDSet + if err != nil { + return nil, errors.Trace(err) + } + for sid, set := range sets { + s.Sets[sid] = set } return s, nil } diff --git a/mysql/mysql_gtid_test.go b/mysql/mysql_gtid_test.go new file mode 100644 index 000000000..8f723c7c6 --- /dev/null +++ b/mysql/mysql_gtid_test.go @@ -0,0 +1,91 @@ +package mysql + +import ( + "reflect" + "testing" + + "github.com/google/uuid" +) + +func TestParseUUIDSet(t *testing.T) { + tests := []struct { + input string + expected map[string]*UUIDSet + wantErr bool + }{ + { + input: "0b8beec9-911e-11e9-9f7b-8a057645f3f6:1-1175877800", + expected: map[string]*UUIDSet{ + "0b8beec9-911e-11e9-9f7b-8a057645f3f6": { + SID: uuid.Must(uuid.Parse("0b8beec9-911e-11e9-9f7b-8a057645f3f6")), + Intervals: []Interval{{Start: 1, Stop: 1175877801}}, // Stop is Start+1 for single intervals + }, + }, + wantErr: false, + }, + { + input: "0b8beec9-911e-11e9-9f7b-8a057645f3f6:1-1175877800,246e88bd-0288-11e8-9cee-230cd2fc765b:1-592884032", + expected: map[string]*UUIDSet{ + "0b8beec9-911e-11e9-9f7b-8a057645f3f6": { + SID: uuid.Must(uuid.Parse("0b8beec9-911e-11e9-9f7b-8a057645f3f6")), + Intervals: []Interval{{Start: 1, Stop: 1175877801}}, + }, + "246e88bd-0288-11e8-9cee-230cd2fc765b": { + SID: uuid.Must(uuid.Parse("246e88bd-0288-11e8-9cee-230cd2fc765b")), + Intervals: []Interval{{Start: 1, Stop: 592884033}}, + }, + }, + wantErr: false, + }, + { + input: "invalid", + wantErr: true, + }, + { + input: "", + expected: nil, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + got, err := ParseUUIDSet(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("ParseUUIDSet() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.expected) { + t.Errorf("ParseUUIDSet() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestParseMysqlGTIDSet(t *testing.T) { + input := "0b8beec9-911e-11e9-9f7b-8a057645f3f6:1-1175877800,246e88bd-0288-11e8-9cee-230cd2fc765b:1-592884032" + expected := &MysqlGTIDSet{ + Sets: map[string]*UUIDSet{ + "0b8beec9-911e-11e9-9f7b-8a057645f3f6": { + SID: uuid.Must(uuid.Parse("0b8beec9-911e-11e9-9f7b-8a057645f3f6")), + Intervals: []Interval{{Start: 1, Stop: 1175877801}}, + }, + "246e88bd-0288-11e8-9cee-230cd2fc765b": { + SID: uuid.Must(uuid.Parse("246e88bd-0288-11e8-9cee-230cd2fc765b")), + Intervals: []Interval{{Start: 1, Stop: 592884033}}, + }, + }, + } + + got, err := ParseMysqlGTIDSet(input) + if err != nil { + t.Fatalf("ParseMysqlGTIDSet() error = %v", err) + } + if !reflect.DeepEqual(got, expected) { + t.Errorf("ParseMysqlGTIDSet() = %v, want %v", got, expected) + } + + if got.String() != input { + t.Errorf("String() = %v, want %v", got.String(), input) + } +} diff --git a/mysql/mysql_test.go b/mysql/mysql_test.go index 2aea1691c..3e0c01c82 100644 --- a/mysql/mysql_test.go +++ b/mysql/mysql_test.go @@ -2,6 +2,7 @@ package mysql import ( "fmt" + "reflect" "strings" "testing" @@ -102,7 +103,6 @@ func TestMysqlGTIDInsertInterval(t *testing.T) { func TestMysqlGTIDCodec(t *testing.T) { us, err := ParseUUIDSet("de278ad0-2106-11e4-9f8e-6edd0ca20947:1-2") require.NoError(t, err) - require.Equal(t, "de278ad0-2106-11e4-9f8e-6edd0ca20947:1-2", us.String()) buf := us.Encode() @@ -124,29 +124,28 @@ func TestMysqlUpdate(t *testing.T) { err = g1.Update("3E11FA47-71CA-11E1-9E33-C80AA9429562:21-58") require.NoError(t, err) - require.Equal(t, "3E11FA47-71CA-11E1-9E33-C80AA9429562:21-58", strings.ToUpper(g1.String())) g1, err = ParseMysqlGTIDSet(` - 519CE70F-A893-11E9-A95A-B32DC65A7026:1-1154661, - 5C9CA52B-9F11-11E9-8EAF-3381EC1CC790:1-244, - 802D69FD-A3B6-11E9-B1EA-50BAB55BA838:1-1221371, - F2B50559-A891-11E9-B646-884FF0CA2043:1-479261 - `) + 519CE70F-A893-11E9-A95A-B32DC65A7026:1-1154661, + 5C9CA52B-9F11-11E9-8EAF-3381EC1CC790:1-244, + 802D69FD-A3B6-11E9-B1EA-50BAB55BA838:1-1221371, + F2B50559-A891-11E9-B646-884FF0CA2043:1-479261 + `) require.NoError(t, err) err = g1.Update(` - 802D69FD-A3B6-11E9-B1EA-50BAB55BA838:1221110-1221371, - F2B50559-A891-11E9-B646-884FF0CA2043:478509-479266 - `) + 802D69FD-A3B6-11E9-B1EA-50BAB55BA838:1221110-1221371, + F2B50559-A891-11E9-B646-884FF0CA2043:478509-479266 + `) require.NoError(t, err) g2, err := ParseMysqlGTIDSet(` - 519CE70F-A893-11E9-A95A-B32DC65A7026:1-1154661, - 5C9CA52B-9F11-11E9-8EAF-3381EC1CC790:1-244, - 802D69FD-A3B6-11E9-B1EA-50BAB55BA838:1-1221371, - F2B50559-A891-11E9-B646-884FF0CA2043:1-479266 - `) + 519CE70F-A893-11E9-A95A-B32DC65A7026:1-1154661, + 5C9CA52B-9F11-11E9-8EAF-3381EC1CC790:1-244, + 802D69FD-A3B6-11E9-B1EA-50BAB55BA838:1-1221371, + F2B50559-A891-11E9-B646-884FF0CA2043:1-479266 + `) require.NoError(t, err) require.True(t, g1.Equal(g2)) } @@ -173,8 +172,8 @@ func TestMysqlAddGTID(t *testing.T) { require.NoError(t, err) g1.AddGTID(u2, 58) g2, err := ParseMysqlGTIDSet(` - 3E11FA47-71CA-11E1-9E33-C80AA9429562:21-60, - 519CE70F-A893-11E9-A95A-B32DC65A7026:58 + 3E11FA47-71CA-11E1-9E33-C80AA9429562:21-60, + 519CE70F-A893-11E9-A95A-B32DC65A7026:58 `) require.NoError(t, err) require.True(t, g2.Equal(g1)) @@ -195,11 +194,8 @@ func TestMysqlGTIDAdd(t *testing.T) { testCases := []struct { left, right, expected string }{ - // simple cases works: {"3E11FA47-71CA-11E1-9E33-C80AA9429562:23", "3E11FA47-71CA-11E1-9E33-C80AA9429562:28-57", "3E11FA47-71CA-11E1-9E33-C80AA9429562:23:28-57"}, - // summ is associative operation {"3E11FA47-71CA-11E1-9E33-C80AA9429562:28-57", "3E11FA47-71CA-11E1-9E33-C80AA9429562:23", "3E11FA47-71CA-11E1-9E33-C80AA9429562:23:28-57"}, - // merge intervals: {"3E11FA47-71CA-11E1-9E33-C80AA9429562:23-27", "3E11FA47-71CA-11E1-9E33-C80AA9429562:28-57", "3E11FA47-71CA-11E1-9E33-C80AA9429562:23-57"}, } @@ -218,12 +214,10 @@ func TestMysqlGTIDMinus(t *testing.T) { testCases := []struct { left, right, expected string }{ - // Minuses that doesn't affect original value: {"3E11FA47-71CA-11E1-9E33-C80AA9429562:23", "3E11FA47-71CA-11E1-9E33-C80AA9429562:28-57", "3E11FA47-71CA-11E1-9E33-C80AA9429562:23"}, {"3E11FA47-71CA-11E1-9E33-C80AA9429562:28-57", "3E11FA47-71CA-11E1-9E33-C80AA9429562:23", "3E11FA47-71CA-11E1-9E33-C80AA9429562:28-57"}, {"3E11FA47-71CA-11E1-9E33-C80AA9429562:23", "3E11FA47-71CA-11E1-9E33-C80AA9429562:1-22:24-57", "3E11FA47-71CA-11E1-9E33-C80AA9429562:23"}, {"3E11FA47-71CA-11E1-9E33-C80AA9429562:23", "ABCDEF12-1234-5678-9012-345678901234:1-1000", "3E11FA47-71CA-11E1-9E33-C80AA9429562:23"}, - // Minuses that change original value: {"3E11FA47-71CA-11E1-9E33-C80AA9429562:20-57:60-90", "3E11FA47-71CA-11E1-9E33-C80AA9429562:23", "3E11FA47-71CA-11E1-9E33-C80AA9429562:20-22:24-57:60-90"}, {"3E11FA47-71CA-11E1-9E33-C80AA9429562:20-57:60-90", "3E11FA47-71CA-11E1-9E33-C80AA9429562:22-70", "3E11FA47-71CA-11E1-9E33-C80AA9429562:20-21:71-90"}, {"3E11FA47-71CA-11E1-9E33-C80AA9429562:28-57", "3E11FA47-71CA-11E1-9E33-C80AA9429562:28-57", ""}, @@ -311,7 +305,6 @@ func TestErrorCode(t *testing.T) { func TestMysqlNullDecode(t *testing.T) { _, isNull, n := LengthEncodedInt([]byte{0xfb}) - require.True(t, isNull) require.Equal(t, 1, n) } @@ -334,7 +327,6 @@ func TestMysqlEmptyDecode(t *testing.T) { func mysqlGTIDfromString(t *testing.T, gtidStr string) MysqlGTIDSet { gtid, err := ParseMysqlGTIDSet(gtidStr) require.NoError(t, err) - return *gtid.(*MysqlGTIDSet) } @@ -350,10 +342,9 @@ func TestValidateFlavor(t *testing.T) { {"msql", false}, {"mArIAdb", true}, } - for _, f := range tbls { err := ValidateFlavor(f.flavor) - if f.valid == true { + if f.valid { require.NoError(t, err) } else { require.Error(t, err) @@ -361,6 +352,81 @@ func TestValidateFlavor(t *testing.T) { } } +func TestParseUUIDSet(t *testing.T) { + tests := []struct { + input string + expected *UUIDSet + wantErr bool + }{ + { + input: "0b8beec9-911e-11e9-9f7b-8a057645f3f6:1-1175877800", + expected: &UUIDSet{ + SID: uuid.Must(uuid.Parse("0b8beec9-911e-11e9-9f7b-8a057645f3f6")), + Intervals: []Interval{{Start: 1, Stop: 1175877801}}, + }, + wantErr: false, + }, + { + input: "246e88bd-0288-11e8-9cee-230cd2fc765b:1-592884032", + expected: &UUIDSet{ + SID: uuid.Must(uuid.Parse("246e88bd-0288-11e8-9cee-230cd2fc765b")), + Intervals: []Interval{{Start: 1, Stop: 592884033}}, + }, + wantErr: false, + }, + { + input: "invalid", + wantErr: true, + }, + { + input: "", + expected: nil, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + got, err := ParseUUIDSet(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("ParseUUIDSet() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.expected) { + t.Errorf("ParseUUIDSet() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestParseMysqlGTIDSet(t *testing.T) { + input := "0b8beec9-911e-11e9-9f7b-8a057645f3f6:1-1175877800,246e88bd-0288-11e8-9cee-230cd2fc765b:1-592884032" + expected := &MysqlGTIDSet{ + Sets: map[string]*UUIDSet{ + "0b8beec9-911e-11e9-9f7b-8a057645f3f6": { + SID: uuid.Must(uuid.Parse("0b8beec9-911e-11e9-9f7b-8a057645f3f6")), + Intervals: []Interval{{Start: 1, Stop: 1175877801}}, + }, + "246e88bd-0288-11e8-9cee-230cd2fc765b": { + SID: uuid.Must(uuid.Parse("246e88bd-0288-11e8-9cee-230cd2fc765b")), + Intervals: []Interval{{Start: 1, Stop: 592884033}}, + }, + }, + } + + got, err := ParseMysqlGTIDSet(input) + if err != nil { + t.Fatalf("ParseMysqlGTIDSet() error = %v", err) + } + if !reflect.DeepEqual(got, expected) { + t.Errorf("ParseMysqlGTIDSet() = %v, want %v", got, expected) + } + + if got.String() != input { + t.Errorf("String() = %v, want %v", got.String(), input) + } +} + func TestMysqlGTIDSetIsEmpty(t *testing.T) { emptyGTIDSet := new(MysqlGTIDSet) emptyGTIDSet.Sets = make(map[string]*UUIDSet)