@@ -3,53 +3,119 @@ package connmysql
33import (
44 "context"
55 "fmt"
6+ "slices"
7+ "strings"
68 "testing"
9+ "time"
710
11+ "github.com/go-mysql-org/go-mysql/replication"
12+ "github.com/google/uuid"
813 "github.com/pingcap/tidb/pkg/parser"
914 "github.com/pingcap/tidb/pkg/parser/ast"
15+ tidbmysql "github.com/pingcap/tidb/pkg/parser/mysql"
1016 "github.com/stretchr/testify/require"
1117
1218 "github.com/PeerDB-io/peerdb/flow/generated/protos"
1319 "github.com/PeerDB-io/peerdb/flow/internal"
1420 "github.com/PeerDB-io/peerdb/flow/shared"
1521)
1622
17- func newTestConnector (t * testing.T , ctx context.Context ) * MySqlConnector {
23+ func startBinlogStream (t * testing.T , ctx context.Context , c * MySqlConnector ) * replication. BinlogStreamer {
1824 t .Helper ()
19- flavor := protos .MySqlFlavor_MYSQL_MYSQL
20- if internal .MySQLTestVersionIsMaria () {
21- flavor = protos .MySqlFlavor_MYSQL_MARIA
25+
26+ var syncer * replication.BinlogSyncer
27+ var stream * replication.BinlogStreamer
28+ if internal .MySQLTestVersionIsMySQLPos () {
29+ pos , err := c .GetMasterPos (ctx )
30+ require .NoError (t , err )
31+ syncer , stream , _ , _ , err = c .startCdcStreamingFilePos (ctx , pos , nil )
32+ require .NoError (t , err )
33+ } else {
34+ gset , err := c .GetMasterGTIDSet (ctx )
35+ require .NoError (t , err )
36+ syncer , stream , _ , _ , err = c .startCdcStreamingGtid (ctx , gset , nil )
37+ require .NoError (t , err )
2238 }
23- connector , err := NewMySqlConnector (ctx , & protos.MySqlConfig {
24- Host : internal .MySQLTestHost (),
25- Port : internal .MySQLTestPort (),
26- User : "root" ,
27- Password : internal .MySQLTestRootPasswordWithFallback ("cipass" ),
28- Database : "mysql" ,
29- DisableTls : true ,
30- Flavor : flavor ,
31- })
32- require .NoError (t , err )
33- t .Cleanup (func () {
34- if err := connector .Close (); err != nil {
35- t .Logf ("connector close failed: %v" , err )
36- }
37- })
38- return connector
39+ t .Cleanup (syncer .Close )
40+ return stream
3941}
4042
41- func createTestDB (t * testing.T , ctx context.Context , c * MySqlConnector , dbName string ) {
43+ func waitForQueryEventContaining (
44+ t * testing.T , ctx context.Context , stream * replication.BinlogStreamer , marker string ,
45+ ) ([]byte , string ) {
4246 t .Helper ()
43- quoted := "`" + dbName + "`"
44- _ , err := c .Execute (ctx , "DROP DATABASE IF EXISTS " + quoted )
47+
48+ streamCtx , cancel := context .WithTimeout (ctx , 30 * time .Second )
49+ defer cancel ()
50+ for {
51+ event , err := stream .GetEvent (streamCtx )
52+ require .NoError (t , err , "did not observe QueryEvent containing %q in the binlog" , marker )
53+ if qe , ok := event .Event .(* replication.QueryEvent ); ok && strings .Contains (string (qe .Query ), marker ) {
54+ // status vars alias into the event buffer, which go-mysql may reuse
55+ return slices .Clone (qe .StatusVars ), string (qe .Query )
56+ }
57+ }
58+ }
59+
60+ func TestANSIQuotesDDLParsedFromBinlog (t * testing.T ) {
61+ t .Parallel ()
62+ ctx := t .Context ()
63+ connector := newTestConnector (t , ctx )
64+
65+ dbName := testDBName ("ansi_quotes_binlog" )
66+ createTestDB (t , ctx , connector , dbName )
67+ _ , err := connector .Execute (ctx , "CREATE TABLE `" + dbName + "`.`t` (id INT PRIMARY KEY)" )
4568 require .NoError (t , err )
46- _ , err = c .Execute (ctx , "CREATE DATABASE " + quoted )
69+
70+ stream := startBinlogStream (t , ctx , connector )
71+
72+ // ANSI_QUOTES makes "..." an identifier quote, so the ALTER below only parses
73+ // when the mode is passed to the parser as well
74+ guid := uuid .NewString ()
75+ _ , err = connector .Execute (ctx , `SET SESSION sql_mode='ANSI_QUOTES'` )
4776 require .NoError (t , err )
48- t .Cleanup (func () {
49- if _ , err := c .Execute (context .Background (), "DROP DATABASE IF EXISTS " + quoted ); err != nil {
50- t .Logf ("drop test db %s failed: %v" , dbName , err )
51- }
52- })
77+
78+ ddl := fmt .Sprintf (`/* %s */ ALTER TABLE "%s"."t" ADD COLUMN "c1" INT` , guid , dbName )
79+ _ , err = connector .Execute (ctx , ddl )
80+ require .NoError (t , err )
81+
82+ statusVars , query := waitForQueryEventContaining (t , ctx , stream , guid )
83+
84+ // Validate extraction
85+ mode , ok := sqlModeFromStatusVars (statusVars )
86+ require .True (t , ok )
87+ require .NotZero (t , mode & uint64 (tidbmysql .ModeANSIQuotes ), "ANSI_QUOTES missing from binlog status vars" )
88+
89+ // Validate parsing
90+ mysqlParser := parser .New ()
91+ setParserSQLModeFromStatusVars (mysqlParser , statusVars )
92+ stmts , _ , err := mysqlParser .ParseSQL (query )
93+ require .NoError (t , err )
94+ require .Len (t , stmts , 1 )
95+ require .IsType (t , & ast.AlterTableStmt {}, stmts [0 ])
96+
97+ // Validate parsing fails without the mode
98+ _ , _ , err = parser .New ().ParseSQL (query )
99+ require .Error (t , err )
100+
101+ // Status vars with codes > 1 are written after sql_mode (code 1). Every
102+ // QueryEvent already carries catalog (2) and charset (4); force a few more —
103+ // auto_increment (3), time_zone (5), lc_time_names (7) — to confirm a fuller
104+ // status-var block still doesn't interfere with extracting sql_mode.
105+ _ , err = connector .Execute (ctx ,
106+ `SET SESSION auto_increment_increment=7, auto_increment_offset=3, time_zone='+05:00', lc_time_names='fr_FR'` )
107+ require .NoError (t , err )
108+
109+ guid = uuid .NewString ()
110+ ddl = fmt .Sprintf (`/* %s */ ALTER TABLE "%s"."t" ADD COLUMN "c2" INT` , guid , dbName )
111+ _ , err = connector .Execute (ctx , ddl )
112+ require .NoError (t , err )
113+
114+ statusVars , _ = waitForQueryEventContaining (t , ctx , stream , guid )
115+
116+ mode , ok = sqlModeFromStatusVars (statusVars )
117+ require .True (t , ok )
118+ require .NotZero (t , mode & uint64 (tidbmysql .ModeANSIQuotes ), "ANSI_QUOTES missing from binlog status vars with extra status vars present" )
53119}
54120
55121func TestParseSQLParsesTrailingNull (t * testing.T ) {
@@ -101,8 +167,8 @@ func TestGetTableSchemaCaseSensitiveIdentifiers(t *testing.T) {
101167 require .NoError (t , err )
102168 require .Equal (t , int64 (0 ), lctn )
103169
104- lowerDB := "cs"
105- upperDB := "CS_IDS_TEST"
170+ lowerDB := testDBName ( "cs" )
171+ upperDB := testDBName ( "CS_IDS_TEST" )
106172 createTestDB (t , ctx , connector , lowerDB )
107173 createTestDB (t , ctx , connector , upperDB )
108174
@@ -151,7 +217,7 @@ func TestGetTableSchemaPrimaryKeyVariants(t *testing.T) {
151217 ctx := t .Context ()
152218 connector := newTestConnector (t , ctx )
153219
154- dbName := "pk_variants_test"
220+ dbName := testDBName ( "pk_variants" )
155221 createTestDB (t , ctx , connector , dbName )
156222
157223 exec := func (sql string ) {
0 commit comments