@@ -9,68 +9,197 @@ import (
99 "github.com/PeerDB-io/peerdb/flow/generated/protos"
1010)
1111
12+ // parseOffsetTestCase describes the validation case, separating gtid checks from fpos checks
13+ // ussing `offsetExpecation` interface.
14+ type parseOffsetTestCase struct {
15+ name string
16+ flavor string
17+ offsetText string
18+
19+ // If non-empty, parsing is expected to fail and the error must contain this substring.
20+ maybeErrContains string
21+
22+ expect offsetExpectation
23+ }
24+
25+ // offsetExpectation owns the mechanism-specific success assertions> gtid vs fpos.
26+ type offsetExpectation interface {
27+ // wantMechanism is the mechanism a successful parse must resolve to.
28+ wantMechanism () protos.MySqlReplicationMechanism
29+ // verifyParse asserts the parsed payload.
30+ verifyParse (t * testing.T , parsed parsedReplicationOffset )
31+ }
32+
33+ type gtidExpectation struct {
34+ gtidString string
35+ gtidEmpty bool
36+ }
37+
38+ func (gtidExpectation ) wantMechanism () protos.MySqlReplicationMechanism {
39+ return protos .MySqlReplicationMechanism_MYSQL_GTID
40+ }
41+
42+ func (e gtidExpectation ) verifyParse (t * testing.T , parsed parsedReplicationOffset ) {
43+ t .Helper ()
44+ require .Equal (t , e .wantMechanism ().String (), parsed .mechanism )
45+ require .Equal (t , mysql.Position {}, parsed .pos )
46+ require .NotNil (t , parsed .gset )
47+ require .Equal (t , e .gtidString , parsed .gset .String ())
48+ if e .gtidEmpty {
49+ require .True (t , parsed .gset .IsEmpty ())
50+ }
51+ }
52+
53+ type fposExpectation struct {
54+ pos mysql.Position
55+ }
56+
57+ func (fposExpectation ) wantMechanism () protos.MySqlReplicationMechanism {
58+ return protos .MySqlReplicationMechanism_MYSQL_FILEPOS
59+ }
60+
61+ func (e fposExpectation ) verifyParse (t * testing.T , parsed parsedReplicationOffset ) {
62+ t .Helper ()
63+ require .Equal (t , e .wantMechanism ().String (), parsed .mechanism )
64+ require .Nil (t , parsed .gset )
65+ require .Equal (t , e .pos , parsed .pos )
66+ }
67+
68+ // parseOffsetTestCases drives both TestParseReplicationOffsetText and
69+ // TestReplicationMechanismInUseFromOffsetText.
70+ var parseOffsetTestCases = []parseOffsetTestCase {
71+ {
72+ name : "mysql filepos" ,
73+ flavor : mysql .MySQLFlavor ,
74+ offsetText : "!f:mysql-bin.000001,4d2" ,
75+ expect : fposExpectation {pos : mysql.Position {Name : "mysql-bin.000001" , Pos : 0x4d2 }},
76+ },
77+ {
78+ name : "mysql filepos missing comma" ,
79+ flavor : mysql .MySQLFlavor ,
80+ offsetText : "!f:mysql-bin.000001" ,
81+ maybeErrContains : "no comma in file/pos offset" ,
82+ },
83+ {
84+ name : "mysql filepos invalid offset" ,
85+ flavor : mysql .MySQLFlavor ,
86+ offsetText : "!f:mysql-bin.000001,zzz" ,
87+ maybeErrContains : "invalid offset in file/pos offset" ,
88+ },
89+ {
90+ name : "mysql gtid single uuid" ,
91+ flavor : mysql .MySQLFlavor ,
92+ offsetText : "3E11FA47-71CA-11E1-9E33-C80AA9429562:23" ,
93+ expect : gtidExpectation {gtidString : "3e11fa47-71ca-11e1-9e33-c80aa9429562:23" },
94+ },
95+ {
96+ name : "mysql empty" ,
97+ flavor : mysql .MySQLFlavor ,
98+ offsetText : "" ,
99+ expect : gtidExpectation {gtidEmpty : true },
100+ },
101+ {
102+ name : "mysql invalid gtid" ,
103+ flavor : mysql .MySQLFlavor ,
104+ offsetText : "not-a-valid-offset" ,
105+ maybeErrContains : `failed to parse mysql offset text "not-a-valid-offset" as GTID set` ,
106+ },
107+ {
108+ name : "mariadb gtid single domain" ,
109+ flavor : mysql .MariaDBFlavor ,
110+ offsetText : "0-1-23" ,
111+ expect : gtidExpectation {gtidString : "0-1-23" },
112+ },
113+ {
114+ name : "mariadb gtid non-default domain and multi-digit fields" ,
115+ flavor : mysql .MariaDBFlavor ,
116+ offsetText : "5-100-9999999" ,
117+ expect : gtidExpectation {gtidString : "5-100-9999999" },
118+ },
119+ {
120+ name : "mariadb gtid max uint64 sequence" ,
121+ flavor : mysql .MariaDBFlavor ,
122+ offsetText : "5-100-18446744073709551615" ,
123+ expect : gtidExpectation {gtidString : "5-100-18446744073709551615" },
124+ },
125+ {
126+ name : "mariadb gtid multi-domain already sorted" ,
127+ flavor : mysql .MariaDBFlavor ,
128+ offsetText : "0-1-23,1-2-45" ,
129+ expect : gtidExpectation {gtidString : "0-1-23,1-2-45" },
130+ },
131+ {
132+ name : "mariadb gtid multi-domain canonicalizes order" ,
133+ flavor : mysql .MariaDBFlavor ,
134+ offsetText : "1-2-45,0-1-23" ,
135+ expect : gtidExpectation {gtidString : "0-1-23,1-2-45" },
136+ },
137+ {
138+ // A MariaDB position is one GTID per domain: the same domain twice collapses
139+ // to the newest sequence.
140+ name : "mariadb gtid same domain collapses to newest sequence" ,
141+ flavor : mysql .MariaDBFlavor ,
142+ offsetText : "0-1-23,0-1-50" ,
143+ expect : gtidExpectation {gtidString : "0-1-50" },
144+ },
145+ {
146+ // Failover within a domain: server_id changes, the newest entry wins.
147+ name : "mariadb gtid same domain failover keeps newest server" ,
148+ flavor : mysql .MariaDBFlavor ,
149+ offsetText : "0-1-23,0-2-50" ,
150+ expect : gtidExpectation {gtidString : "0-2-50" },
151+ },
152+ {
153+ name : "mariadb empty" ,
154+ flavor : mysql .MariaDBFlavor ,
155+ offsetText : "" ,
156+ expect : gtidExpectation {gtidEmpty : true },
157+ },
158+ {
159+ // MySQL uuid:interval grammar must not parse under the MariaDB flavor.
160+ name : "mariadb rejects mysql-style uuid gtid" ,
161+ flavor : mysql .MariaDBFlavor ,
162+ offsetText : "3E11FA47-71CA-11E1-9E33-C80AA9429562:23" ,
163+ maybeErrContains : "must domain-server-sequence" ,
164+ },
165+ {
166+ name : "mariadb gtid too few parts" ,
167+ flavor : mysql .MariaDBFlavor ,
168+ offsetText : "0-1" ,
169+ maybeErrContains : "must domain-server-sequence" ,
170+ },
171+ {
172+ name : "mariadb gtid non-numeric domain" ,
173+ flavor : mysql .MariaDBFlavor ,
174+ offsetText : "a-1-23" ,
175+ maybeErrContains : "invalid MariaDB GTID Domain ID" ,
176+ },
177+ }
178+
12179func TestParseReplicationOffsetText (t * testing.T ) {
13- t .Run ("filepos" , func (t * testing.T ) {
14- parsedOffset , err := parseReplicationOffsetText ("mysql" , "!f:mysql-bin.000001,4d2" )
15- require .NoError (t , err )
16- require .Equal (t , protos .MySqlReplicationMechanism_MYSQL_FILEPOS .String (), parsedOffset .mechanism )
17- require .Nil (t , parsedOffset .gset )
18- require .Equal (t , mysql.Position {Name : "mysql-bin.000001" , Pos : 0x4d2 }, parsedOffset .pos )
19- })
20-
21- t .Run ("filepos missing comma" , func (t * testing.T ) {
22- _ , err := parseReplicationOffsetText ("mysql" , "!f:mysql-bin.000001" )
23- require .ErrorContains (t , err , "no comma in file/pos offset" )
24- })
25-
26- t .Run ("filepos invalid offset" , func (t * testing.T ) {
27- _ , err := parseReplicationOffsetText ("mysql" , "!f:mysql-bin.000001,zzz" )
28- require .ErrorContains (t , err , "invalid offset in file/pos offset" )
29- })
30-
31- t .Run ("gtid" , func (t * testing.T ) {
32- parsedOffset , err := parseReplicationOffsetText ("mysql" , "3E11FA47-71CA-11E1-9E33-C80AA9429562:23" )
33- require .NoError (t , err )
34- require .Equal (t , protos .MySqlReplicationMechanism_MYSQL_GTID .String (), parsedOffset .mechanism )
35- require .Equal (t , "3e11fa47-71ca-11e1-9e33-c80aa9429562:23" , parsedOffset .gset .String ())
36- require .Equal (t , mysql.Position {}, parsedOffset .pos )
37- })
38-
39- t .Run ("empty" , func (t * testing.T ) {
40- parsedOffset , err := parseReplicationOffsetText ("mysql" , "" )
41- require .NoError (t , err )
42- require .Equal (t , protos .MySqlReplicationMechanism_MYSQL_GTID .String (), parsedOffset .mechanism )
43- require .True (t , parsedOffset .gset .IsEmpty ())
44- require .Equal (t , mysql.Position {}, parsedOffset .pos )
45- })
46-
47- t .Run ("invalid gtid" , func (t * testing.T ) {
48- _ , err := parseReplicationOffsetText ("mysql" , "not-a-valid-offset" )
49- require .ErrorContains (t , err , `failed to parse mysql offset text "not-a-valid-offset" as GTID set` )
50- })
180+ for _ , tc := range parseOffsetTestCases {
181+ t .Run (tc .name , func (t * testing.T ) {
182+ parsed , err := parseReplicationOffsetText (tc .flavor , tc .offsetText )
183+ if tc .maybeErrContains != "" {
184+ require .ErrorContains (t , err , tc .maybeErrContains )
185+ return
186+ }
187+ require .NoError (t , err )
188+ tc .expect .verifyParse (t , parsed )
189+ })
190+ }
51191}
52192
53193func TestReplicationMechanismInUseFromOffsetText (t * testing.T ) {
54- t .Run ("filepos" , func (t * testing.T ) {
55- mechanism , err := replicationMechanismInUseFromOffsetText ("mysql" , "!f:mysql-bin.000001,4d2" )
56- require .NoError (t , err )
57- require .Equal (t , protos .MySqlReplicationMechanism_MYSQL_FILEPOS .String (), mechanism )
58- })
59-
60- t .Run ("gtid" , func (t * testing.T ) {
61- mechanism , err := replicationMechanismInUseFromOffsetText ("mysql" , "3E11FA47-71CA-11E1-9E33-C80AA9429562:23" )
62- require .NoError (t , err )
63- require .Equal (t , protos .MySqlReplicationMechanism_MYSQL_GTID .String (), mechanism )
64- })
65-
66- t .Run ("empty" , func (t * testing.T ) {
67- mechanism , err := replicationMechanismInUseFromOffsetText ("mysql" , "" )
68- require .NoError (t , err )
69- require .Equal (t , protos .MySqlReplicationMechanism_MYSQL_GTID .String (), mechanism )
70- })
71-
72- t .Run ("invalid" , func (t * testing.T ) {
73- _ , err := replicationMechanismInUseFromOffsetText ("mysql" , "not-a-valid-offset" )
74- require .ErrorContains (t , err , `failed to parse mysql offset text "not-a-valid-offset" as GTID set` )
75- })
194+ for _ , tc := range parseOffsetTestCases {
195+ t .Run (tc .name , func (t * testing.T ) {
196+ mechanism , err := replicationMechanismInUseFromOffsetText (tc .flavor , tc .offsetText )
197+ if tc .maybeErrContains != "" {
198+ require .ErrorContains (t , err , tc .maybeErrContains )
199+ return
200+ }
201+ require .NoError (t , err )
202+ require .Equal (t , tc .expect .wantMechanism ().String (), mechanism )
203+ })
204+ }
76205}
0 commit comments