vitess.io/vitess@v0.16.2/go/mysql/mysql56_gtid_set.go (about)

     1  /*
     2  Copyright 2019 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package mysql
    18  
    19  import (
    20  	"bytes"
    21  	"encoding/binary"
    22  	"strconv"
    23  	"strings"
    24  
    25  	"golang.org/x/exp/slices"
    26  
    27  	"vitess.io/vitess/go/vt/proto/vtrpc"
    28  	"vitess.io/vitess/go/vt/vterrors"
    29  )
    30  
    31  type interval struct {
    32  	start, end int64
    33  }
    34  
    35  func (iv interval) contains(other interval) bool {
    36  	return iv.start <= other.start && other.end <= iv.end
    37  }
    38  
    39  func parseInterval(s string) (interval, error) {
    40  	part0, part1, twoParts := strings.Cut(s, "-")
    41  
    42  	start, err := strconv.ParseUint(part0, 10, 63)
    43  	if err != nil {
    44  		return interval{}, vterrors.Wrapf(err, "invalid interval (%q)", s)
    45  	}
    46  	if start < 1 {
    47  		return interval{}, vterrors.Errorf(vtrpc.Code_INTERNAL, "invalid interval (%q): start must be > 0", s)
    48  	}
    49  
    50  	if twoParts {
    51  		end, err := strconv.ParseUint(part1, 10, 63)
    52  		if err != nil {
    53  			return interval{}, vterrors.Wrapf(err, "invalid interval (%q)", s)
    54  		}
    55  		return interval{start: int64(start), end: int64(end)}, nil
    56  	}
    57  	return interval{start: int64(start), end: int64(start)}, nil
    58  }
    59  
    60  // ParseMysql56GTIDSet is registered as a GTIDSet parser.
    61  //
    62  // https://dev.mysql.com/doc/refman/5.6/en/replication-gtids-concepts.html
    63  func ParseMysql56GTIDSet(s string) (Mysql56GTIDSet, error) {
    64  	set := make(Mysql56GTIDSet)
    65  	input := s
    66  
    67  	// gtid_set: uuid_set [, uuid_set] ...
    68  	for len(input) > 0 {
    69  		var uuidSet string
    70  		if idx := strings.IndexByte(input, ','); idx >= 0 {
    71  			uuidSet = input[:idx]
    72  			input = input[idx+1:]
    73  		} else {
    74  			uuidSet = input
    75  			input = ""
    76  		}
    77  
    78  		uuidSet = strings.TrimSpace(uuidSet)
    79  		if uuidSet == "" {
    80  			continue
    81  		}
    82  
    83  		// uuid_set: uuid:interval[:interval]...
    84  		head, tail, ok := strings.Cut(uuidSet, ":")
    85  		if !ok {
    86  			return nil, vterrors.Errorf(vtrpc.Code_INTERNAL, "invalid MySQL 5.6 GTID set (%q): expected uuid:interval", s)
    87  		}
    88  
    89  		// Parse Server ID.
    90  		sid, err := ParseSID(head)
    91  		if err != nil {
    92  			return nil, vterrors.Wrapf(err, "invalid MySQL 5.6 GTID set (%q)", s)
    93  		}
    94  
    95  		intervals := make([]interval, 0, strings.Count(tail, ":")+1)
    96  		for len(tail) > 0 {
    97  			if idx := strings.IndexByte(tail, ':'); idx >= 0 {
    98  				head = tail[:idx]
    99  				tail = tail[idx+1:]
   100  			} else {
   101  				head = tail
   102  				tail = ""
   103  			}
   104  
   105  			iv, err := parseInterval(head)
   106  			if err != nil {
   107  				return nil, vterrors.Wrapf(err, "invalid MySQL 5.6 GTID set (%q)", s)
   108  			}
   109  			if iv.end < iv.start {
   110  				// According to MySQL 5.6 code:
   111  				//   "The end of an interval may be 0, but any interval that has an
   112  				//    endpoint that is smaller than the start is discarded."
   113  				continue
   114  			}
   115  			intervals = append(intervals, iv)
   116  		}
   117  		if len(intervals) == 0 {
   118  			// We might have discarded all the intervals.
   119  			continue
   120  		}
   121  
   122  		if sidIntervals, ok := set[sid]; ok {
   123  			// SID already exists, we append
   124  			// Example:  "00010203-0405-0607-0809-0a0b0c0d0e0f:1-5,00010203-0405-0607-0809-0a0b0c0d0e0f:10-20"
   125  			// turns to: "00010203-0405-0607-0809-0a0b0c0d0e0f:1-5:10-20"
   126  			intervals = append(sidIntervals, intervals...)
   127  		}
   128  		// Internally we expect intervals to be stored in order.
   129  		slices.SortFunc(intervals, func(a, b interval) bool {
   130  			return a.start < b.start
   131  		})
   132  		set[sid] = intervals
   133  	}
   134  
   135  	return set, nil
   136  }
   137  
   138  // Mysql56GTIDSet implements GTIDSet for MySQL 5.6.
   139  type Mysql56GTIDSet map[SID][]interval
   140  
   141  // SIDs returns a sorted list of SIDs in the set.
   142  func (set Mysql56GTIDSet) SIDs() []SID {
   143  	sids := make([]SID, 0, len(set))
   144  	for sid := range set {
   145  		sids = append(sids, sid)
   146  	}
   147  	sortSIDs(sids)
   148  	return sids
   149  }
   150  
   151  func sortSIDs(sids []SID) {
   152  	slices.SortFunc(sids, func(a, b SID) bool {
   153  		return bytes.Compare(a[:], b[:]) < 0
   154  	})
   155  }
   156  
   157  // String implements GTIDSet.
   158  func (set Mysql56GTIDSet) String() string {
   159  	var buf strings.Builder
   160  	for i, sid := range set.SIDs() {
   161  		if i != 0 {
   162  			buf.WriteByte(',')
   163  		}
   164  		buf.WriteString(sid.String())
   165  
   166  		for _, interval := range set[sid] {
   167  			buf.WriteByte(':')
   168  			buf.WriteString(strconv.FormatInt(interval.start, 10))
   169  
   170  			if interval.end != interval.start {
   171  				buf.WriteByte('-')
   172  				buf.WriteString(strconv.FormatInt(interval.end, 10))
   173  			}
   174  		}
   175  	}
   176  	return buf.String()
   177  }
   178  
   179  // Last returns the last gtid as string
   180  // For gtidset having multiple SIDs or multiple intervals
   181  // it just returns the last SID with last interval
   182  func (set Mysql56GTIDSet) Last() string {
   183  	var buf strings.Builder
   184  	if len(set.SIDs()) > 0 {
   185  		sid := set.SIDs()[len(set.SIDs())-1]
   186  		buf.WriteString(sid.String())
   187  		sequences := set[sid]
   188  		if len(sequences) > 0 {
   189  			buf.WriteByte(':')
   190  			lastInterval := sequences[len(sequences)-1]
   191  			buf.WriteString(strconv.FormatInt(lastInterval.end, 10))
   192  		}
   193  	}
   194  	return buf.String()
   195  }
   196  
   197  // Flavor implements GTIDSet.
   198  func (Mysql56GTIDSet) Flavor() string { return Mysql56FlavorID }
   199  
   200  // ContainsGTID implements GTIDSet.
   201  func (set Mysql56GTIDSet) ContainsGTID(gtid GTID) bool {
   202  	gtid56, ok := gtid.(Mysql56GTID)
   203  	if !ok {
   204  		return false
   205  	}
   206  
   207  	for _, iv := range set[gtid56.Server] {
   208  		if iv.start > gtid56.Sequence {
   209  			// We assume intervals are sorted, so we can skip the rest.
   210  			return false
   211  		}
   212  		if gtid56.Sequence <= iv.end {
   213  			// Now we know that: start <= Sequence <= end.
   214  			return true
   215  		}
   216  	}
   217  	// Server wasn't in the set, or no interval contained gtid.
   218  	return false
   219  }
   220  
   221  // Contains implements GTIDSet.
   222  func (set Mysql56GTIDSet) Contains(other GTIDSet) bool {
   223  	if other == nil {
   224  		return false
   225  	}
   226  
   227  	other56, ok := other.(Mysql56GTIDSet)
   228  	if !ok {
   229  		return false
   230  	}
   231  
   232  	// Check each SID in the other set.
   233  	for sid, otherIntervals := range other56 {
   234  		i := 0
   235  		intervals := set[sid]
   236  		count := len(intervals)
   237  
   238  		// Check each interval for this SID in the other set.
   239  		for _, iv := range otherIntervals {
   240  			// Check that interval against each of our intervals.
   241  			// Intervals are monotonically increasing,
   242  			// so we don't need to reset the index each time.
   243  			for {
   244  				if i >= count {
   245  					// We ran out of intervals to check against.
   246  					return false
   247  				}
   248  				if intervals[i].contains(iv) {
   249  					// Yes it's covered. Go on to the next one.
   250  					break
   251  				}
   252  				i++
   253  			}
   254  		}
   255  	}
   256  
   257  	// No uncovered intervals were found.
   258  	return true
   259  }
   260  
   261  // Equal implements GTIDSet.
   262  func (set Mysql56GTIDSet) Equal(other GTIDSet) bool {
   263  	other56, ok := other.(Mysql56GTIDSet)
   264  	if !ok {
   265  		return false
   266  	}
   267  
   268  	// Check for same number of SIDs.
   269  	if len(set) != len(other56) {
   270  		return false
   271  	}
   272  
   273  	// Compare each SID.
   274  	for sid, intervals := range set {
   275  		otherIntervals := other56[sid]
   276  
   277  		// Check for same number of intervals.
   278  		if len(intervals) != len(otherIntervals) {
   279  			return false
   280  		}
   281  
   282  		// Compare each interval.
   283  		// Since intervals are sorted, they have to be in the same order.
   284  		for i, iv := range intervals {
   285  			if iv != otherIntervals[i] {
   286  				return false
   287  			}
   288  		}
   289  	}
   290  
   291  	// No discrepancies were found.
   292  	return true
   293  }
   294  
   295  // AddGTID implements GTIDSet.
   296  func (set Mysql56GTIDSet) AddGTID(gtid GTID) GTIDSet {
   297  	gtid56, ok := gtid.(Mysql56GTID)
   298  	if !ok {
   299  		return set
   300  	}
   301  
   302  	// If it's already in the set, we can return the same instance.
   303  	// This is safe because GTIDSets are immutable.
   304  	if set.ContainsGTID(gtid) {
   305  		return set
   306  	}
   307  
   308  	// Make a copy and add the new GTID in the proper place.
   309  	// This function is not supposed to modify the original set.
   310  	newSet := make(Mysql56GTIDSet)
   311  
   312  	added := false
   313  
   314  	for sid, intervals := range set {
   315  		newIntervals := make([]interval, 0, len(intervals))
   316  
   317  		if sid == gtid56.Server {
   318  			// Look for the right place to add this GTID.
   319  			for _, iv := range intervals {
   320  				if !added {
   321  					switch {
   322  					case gtid56.Sequence == iv.start-1:
   323  						// Expand the interval at the beginning.
   324  						iv.start = gtid56.Sequence
   325  						added = true
   326  					case gtid56.Sequence == iv.end+1:
   327  						// Expand the interval at the end.
   328  						iv.end = gtid56.Sequence
   329  						added = true
   330  					case gtid56.Sequence < iv.start-1:
   331  						// The next interval is beyond the new GTID, but it can't
   332  						// be expanded, so we have to insert a new interval.
   333  						newIntervals = append(newIntervals, interval{start: gtid56.Sequence, end: gtid56.Sequence})
   334  						added = true
   335  					}
   336  				}
   337  				// Check if this interval can be merged with the previous one.
   338  				count := len(newIntervals)
   339  				if count != 0 && iv.start == newIntervals[count-1].end+1 {
   340  					// Merge instead of appending.
   341  					newIntervals[count-1].end = iv.end
   342  				} else {
   343  					// Can't be merged.
   344  					newIntervals = append(newIntervals, iv)
   345  				}
   346  			}
   347  		} else {
   348  			// Just copy everything.
   349  			newIntervals = append(newIntervals, intervals...)
   350  		}
   351  
   352  		newSet[sid] = newIntervals
   353  	}
   354  
   355  	if !added {
   356  		// There wasn't any place to insert the new GTID, so just append it
   357  		// as a new interval.
   358  		newSet[gtid56.Server] = append(newSet[gtid56.Server], interval{start: gtid56.Sequence, end: gtid56.Sequence})
   359  	}
   360  
   361  	return newSet
   362  }
   363  
   364  // Union implements GTIDSet.Union().
   365  func (set Mysql56GTIDSet) Union(other GTIDSet) GTIDSet {
   366  	if set == nil && other != nil {
   367  		return other
   368  	}
   369  	if set == nil || other == nil {
   370  		return set
   371  	}
   372  	mydbOther, ok := other.(Mysql56GTIDSet)
   373  	if !ok {
   374  		return set
   375  	}
   376  
   377  	// Make a copy and add the new GTID in the proper place.
   378  	// This function is not supposed to modify the original set.
   379  	newSet := make(Mysql56GTIDSet)
   380  
   381  	for otherSID, otherIntervals := range mydbOther {
   382  		intervals, ok := set[otherSID]
   383  		if !ok {
   384  			// No matching server id, so we must add it from other set.
   385  			newSet[otherSID] = otherIntervals
   386  			continue
   387  		}
   388  
   389  		// Found server id match between sets, so now we need to add each interval.
   390  		s1 := intervals
   391  		s2 := otherIntervals
   392  		var nextInterval interval
   393  		var newIntervals []interval
   394  
   395  		// While our stacks have intervals to process, do work.
   396  		for popInterval(&nextInterval, &s1, &s2) {
   397  			if len(newIntervals) == 0 {
   398  				newIntervals = append(newIntervals, nextInterval)
   399  				continue
   400  			}
   401  
   402  			activeInterval := &newIntervals[len(newIntervals)-1]
   403  
   404  			if nextInterval.end <= activeInterval.end {
   405  				// We hit an interval whose start was after or equal to the previous interval's start, but whose
   406  				// end is prior to the active intervals end. Skip to next interval.
   407  				continue
   408  			}
   409  
   410  			if nextInterval.start > activeInterval.end+1 {
   411  				// We found a gap, so we need to start a new interval.
   412  				newIntervals = append(newIntervals, nextInterval)
   413  				continue
   414  			}
   415  
   416  			// Extend our active interval.
   417  			activeInterval.end = nextInterval.end
   418  		}
   419  
   420  		newSet[otherSID] = newIntervals
   421  	}
   422  
   423  	// Add any intervals from SIDs that exist in caller set, but don't exist in other set.
   424  	for sid, intervals := range set {
   425  		if _, ok := newSet[sid]; !ok {
   426  			newSet[sid] = intervals
   427  		}
   428  	}
   429  
   430  	return newSet
   431  }
   432  
   433  // SIDBlock returns the binary encoding of a MySQL 5.6 GTID set as expected
   434  // by internal commands that refer to an "SID block".
   435  //
   436  // e.g. https://dev.mysql.com/doc/internals/en/com-binlog-dump-gtid.html
   437  func (set Mysql56GTIDSet) SIDBlock() []byte {
   438  	buf := &bytes.Buffer{}
   439  
   440  	// Number of SIDs.
   441  	binary.Write(buf, binary.LittleEndian, uint64(len(set)))
   442  
   443  	for _, sid := range set.SIDs() {
   444  		buf.Write(sid[:])
   445  
   446  		// Number of intervals.
   447  		intervals := set[sid]
   448  		binary.Write(buf, binary.LittleEndian, uint64(len(intervals)))
   449  
   450  		for _, iv := range intervals {
   451  			binary.Write(buf, binary.LittleEndian, iv.start)
   452  			// MySQL's internal form for intervals adds 1 to the end value.
   453  			// See Gtid_set::add_gtid_text() in rpl_gtid_set.cc for example.
   454  			binary.Write(buf, binary.LittleEndian, iv.end+1)
   455  		}
   456  	}
   457  
   458  	return buf.Bytes()
   459  }
   460  
   461  // Difference will supply the difference between the receiver and supplied Mysql56GTIDSets, and supply the result
   462  // as a Mysql56GTIDSet.
   463  func (set Mysql56GTIDSet) Difference(other Mysql56GTIDSet) Mysql56GTIDSet {
   464  	if other == nil || set == nil {
   465  		return set
   466  	}
   467  
   468  	// Make a fresh, empty set to hold the new value.
   469  	// This function is not supposed to modify the original set.
   470  	differenceSet := make(Mysql56GTIDSet)
   471  
   472  	for sid, intervals := range set {
   473  		otherIntervals, ok := other[sid]
   474  		if !ok {
   475  			// We didn't find SID in other set, so diff should include all intervals for sid unique to receiver.
   476  			differenceSet[sid] = intervals
   477  			continue
   478  		}
   479  
   480  		// Found server id match between sets, so now we need to subtract each interval.
   481  		var diffIntervals []interval
   482  		advance := func() bool {
   483  			if len(intervals) == 0 {
   484  				return false
   485  			}
   486  			diffIntervals = append(diffIntervals, intervals[0])
   487  			intervals = intervals[1:]
   488  			return true
   489  		}
   490  
   491  		var otherInterval interval
   492  		advanceOther := func() bool {
   493  			if len(otherIntervals) == 0 {
   494  				return false
   495  			}
   496  			otherInterval = otherIntervals[0]
   497  			otherIntervals = otherIntervals[1:]
   498  			return true
   499  		}
   500  
   501  		if !advance() {
   502  			continue
   503  		}
   504  		if !advanceOther() {
   505  			differenceSet[sid] = intervals
   506  			continue
   507  		}
   508  
   509  	diffLoop:
   510  		for {
   511  			iv := diffIntervals[len(diffIntervals)-1]
   512  
   513  			switch {
   514  			case iv.end < otherInterval.start:
   515  				// [1, 2] - [3, 5]
   516  				// Need to skip to next s1 interval. This one is completely before otherInterval even starts. It's a diff in whole.
   517  				if !advance() {
   518  					break diffLoop
   519  				}
   520  
   521  			case iv.start > otherInterval.end:
   522  				// [3, 5] - [1, 2]
   523  				// Interval is completely past other interval. We need a valid other to compare against.
   524  				if !advanceOther() {
   525  					break diffLoop
   526  				}
   527  
   528  			case iv.start >= otherInterval.start && iv.end <= otherInterval.end:
   529  				// [3, 4] - [1, 5]
   530  				// Interval is completed contained. Pop off diffIntervals, and advance to next s1.
   531  				diffIntervals = diffIntervals[:len(diffIntervals)-1]
   532  				if !advance() {
   533  					break diffLoop
   534  				}
   535  
   536  			case iv.start < otherInterval.start && iv.end >= otherInterval.start && iv.end <= otherInterval.end:
   537  				// [1, 4] - [3, 5]
   538  				// We have a unique interval prior to where otherInterval starts and should adjust end to match this piece.
   539  				diffIntervals[len(diffIntervals)-1].end = otherInterval.start - 1
   540  
   541  				if !advance() {
   542  					break diffLoop
   543  				}
   544  
   545  			case iv.start >= otherInterval.start && iv.start <= otherInterval.end && iv.end > otherInterval.end:
   546  				// [3, 7] - [1, 5]
   547  				// We have an end piece to deal with.
   548  				diffIntervals[len(diffIntervals)-1].start = otherInterval.end + 1
   549  
   550  				// We need to pop s2 at this point. s1's new interval is fully past otherInterval, so no point in comparing
   551  				// this one next round.
   552  				if !advanceOther() {
   553  					break diffLoop
   554  				}
   555  
   556  			case iv.start < otherInterval.start && iv.end > otherInterval.end:
   557  				// [1, 7] - [3, 4]
   558  				// End is strictly greater. In this case we need to create an extra diff interval. We'll deal with any necessary trimming of it next round.
   559  				diffIntervals[len(diffIntervals)-1].end = otherInterval.start - 1
   560  				diffIntervals = append(diffIntervals, interval{start: otherInterval.end + 1, end: iv.end})
   561  
   562  				// We need to pop s2 at this point. s1's new interval is fully past otherInterval, so no point in comparing
   563  				// this one next round.
   564  				if !advanceOther() {
   565  					break diffLoop
   566  				}
   567  
   568  			default:
   569  				panic("This should never happen.")
   570  			}
   571  		}
   572  
   573  		if len(intervals) != 0 {
   574  			// If we've gotten to this point, then we have intervals that exist beyond the bounds of any intervals in otherIntervals, and they
   575  			// are all diffs and should be added in whole.
   576  			diffIntervals = append(diffIntervals, intervals...)
   577  		}
   578  
   579  		if len(diffIntervals) == 0 {
   580  			delete(differenceSet, sid)
   581  		} else {
   582  			differenceSet[sid] = diffIntervals
   583  		}
   584  	}
   585  
   586  	return differenceSet
   587  }
   588  
   589  // NewMysql56GTIDSetFromSIDBlock builds a Mysql56GTIDSet from parsing a SID Block.
   590  // This is the reverse of the SIDBlock method.
   591  //
   592  // Expected format:
   593  //
   594  //	# bytes field
   595  //	8       nSIDs
   596  //
   597  // (nSIDs times)
   598  //
   599  //	16      SID
   600  //	8       nIntervals
   601  //
   602  // (nIntervals times)
   603  //
   604  //	8       start
   605  //	8       end
   606  func NewMysql56GTIDSetFromSIDBlock(data []byte) (Mysql56GTIDSet, error) {
   607  	buf := bytes.NewReader(data)
   608  	var set Mysql56GTIDSet = make(map[SID][]interval)
   609  	var nSIDs uint64
   610  	if err := binary.Read(buf, binary.LittleEndian, &nSIDs); err != nil {
   611  		return nil, vterrors.Wrapf(err, "cannot read nSIDs")
   612  	}
   613  	for i := uint64(0); i < nSIDs; i++ {
   614  		var sid SID
   615  		if c, err := buf.Read(sid[:]); err != nil || c != 16 {
   616  			return nil, vterrors.Errorf(vtrpc.Code_INTERNAL, "cannot read SID %v: %v %v", i, err, c)
   617  		}
   618  		var nIntervals uint64
   619  		if err := binary.Read(buf, binary.LittleEndian, &nIntervals); err != nil {
   620  			return nil, vterrors.Wrapf(err, "cannot read nIntervals %v", i)
   621  		}
   622  		for j := uint64(0); j < nIntervals; j++ {
   623  			var start, end uint64
   624  			if err := binary.Read(buf, binary.LittleEndian, &start); err != nil {
   625  				return nil, vterrors.Wrapf(err, "cannot read start %v/%v", i, j)
   626  			}
   627  			if err := binary.Read(buf, binary.LittleEndian, &end); err != nil {
   628  				return nil, vterrors.Wrapf(err, "cannot read end %v/%v", i, j)
   629  			}
   630  			set[sid] = append(set[sid], interval{
   631  				start: int64(start),
   632  				end:   int64(end - 1),
   633  			})
   634  		}
   635  	}
   636  	return set, nil
   637  }
   638  
   639  // popInterval will look at the two pre-sorted interval stacks supplied, and if at least one of the stacks is non-empty
   640  // will mutate the destination interval with the next earliest interval based on start sequence.
   641  // popInterval will return true if we were able to pop, or false if both stacks are now empty.
   642  func popInterval(dst *interval, s1, s2 *[]interval) bool {
   643  	if len(*s1) == 0 && len(*s2) == 0 {
   644  		return false
   645  	}
   646  
   647  	// Find which intervals list has earliest start.
   648  	if len(*s2) == 0 || (len(*s1) != 0 && (*s1)[0].start <= (*s2)[0].start) {
   649  		*dst = (*s1)[0]
   650  		// Progress pointer since this stack has earliest.
   651  		*s1 = (*s1)[1:]
   652  	} else {
   653  		*dst = (*s2)[0]
   654  		// Progress pointer since this stack has earliest.
   655  		*s2 = (*s2)[1:]
   656  	}
   657  
   658  	return true
   659  }
   660  
   661  func init() {
   662  	gtidSetParsers[Mysql56FlavorID] = func(s string) (GTIDSet, error) {
   663  		return ParseMysql56GTIDSet(s)
   664  	}
   665  }
   666  
   667  // Subtract takes in two Mysql56GTIDSets as strings and subtracts the second from the first
   668  // The result is also a string.
   669  // An error is thrown if parsing is not possible for either GTIDSets
   670  func Subtract(lhs, rhs string) (string, error) {
   671  	lhsSet, err := ParseMysql56GTIDSet(lhs)
   672  	if err != nil {
   673  		return "", err
   674  	}
   675  	rhsSet, err := ParseMysql56GTIDSet(rhs)
   676  	if err != nil {
   677  		return "", err
   678  	}
   679  	diffSet := lhsSet.Difference(rhsSet)
   680  	return diffSet.String(), nil
   681  }