@@ -927,6 +927,115 @@ func (s ClickHouseSuite) Test_MySQL_MariaDB_UUID_INET() {
927927 RequireEnvCanceled (s .t , env )
928928}
929929
930+ // Test_MySQL_AlterTableAddColumnTypes exercises the binlog ALTER TABLE ADD COLUMN path
931+ // (processAlterTableQuery -> QkindFromMysqlColumnType) together with the row-metadata decode
932+ // path for a broad spectrum of MySQL/MariaDB column types. Every type is added in a single
933+ // multi-column ALTER while the mirror is running, then one row carries every value end-to-end
934+ // into ClickHouse. Integer cases use the signed minimum and unsigned maximum to pin the
935+ // width/signedness mapping, which no other MySQL e2e test covers.
936+ //
937+ // The type list mirrors the spellings the parser-level TestParseSQLAlterTableAddColumnTypes
938+ // checks, so the unit test (parse -> qkind) and this test (qkind -> value over the wire) stay
939+ // aligned. Types the TiDB parser rejects in ADD COLUMN (spatial types and MariaDB's native
940+ // UUID/INET4/INET6) cannot travel this path and are covered separately by snapshot-based tests
941+ // (Test_MySQL_Geometric_Types, Test_MySQL_MariaDB_UUID_INET).
942+ func (s ClickHouseSuite ) Test_MySQL_AlterTableAddColumnTypes () {
943+ mysource , ok := s .source .(* MySqlSource )
944+ if ! ok {
945+ s .t .Skip ("only applies to mysql" )
946+ }
947+ // ENUM/SET columns added during CDC need binlog row metadata to resolve their member names.
948+ cmp , err := mysource .CompareServerVersion (s .t .Context (), mysql_validation .MySQLMinVersionForBinlogRowMetadata )
949+ require .NoError (s .t , err )
950+ if cmp < 0 {
951+ s .t .Skip ("ALTER ADD COLUMN enum/set decoding requires binlog row metadata" )
952+ }
953+
954+ srcTableName := "test_add_col_types"
955+ srcFullName := s .attachSchemaSuffix (srcTableName )
956+ dstTableName := "test_add_col_types_dst"
957+
958+ require .NoError (s .t , s .source .Exec (s .t .Context (),
959+ fmt .Sprintf ("CREATE TABLE IF NOT EXISTS %s (id INT PRIMARY KEY)" , srcFullName )))
960+
961+ connectionGen := FlowConnectionGenerationConfig {
962+ FlowJobName : s .attachSuffix (srcTableName ),
963+ TableNameMapping : map [string ]string {srcFullName : dstTableName },
964+ Destination : s .Peer ().Name ,
965+ }
966+ flowConnConfig := connectionGen .GenerateFlowConnectionConfigs (s )
967+ flowConnConfig .DoInitialSnapshot = true
968+
969+ tc := NewTemporalClient (s .t )
970+ env := ExecutePeerflow (s .t , tc , flowConnConfig )
971+ SetupCDCFlowStatusQuery (s .t , env , flowConnConfig )
972+
973+ cols := []struct { name , def , val string }{
974+ // Integer widths: the signed minimum and unsigned maximum pin width + signedness.
975+ {"c_tinyint" , "TINYINT" , "-128" },
976+ {"c_tinyint_u" , "TINYINT UNSIGNED" , "255" },
977+ {"c_bool" , "TINYINT(1)" , "1" },
978+ {"c_smallint" , "SMALLINT" , "-32768" },
979+ {"c_smallint_u" , "SMALLINT UNSIGNED" , "65535" },
980+ {"c_mediumint" , "MEDIUMINT" , "-8388608" },
981+ {"c_mediumint_u" , "MEDIUMINT UNSIGNED" , "16777215" },
982+ {"c_int" , "INT" , "-2147483648" },
983+ {"c_int_u" , "INT UNSIGNED" , "4294967295" },
984+ {"c_bigint" , "BIGINT" , "-9223372036854775808" },
985+ {"c_bigint_u" , "BIGINT UNSIGNED" , "18446744073709551615" },
986+ {"c_integer" , "INTEGER" , "7" }, // synonym; normalizes to int through the parser
987+ // Approximate + exact numeric.
988+ {"c_float" , "FLOAT" , "1.5" },
989+ {"c_double" , "DOUBLE PRECISION" , "2.5" }, // synonym; normalizes to double
990+ {"c_decimal" , "DECIMAL(10,2)" , "123.45" },
991+ {"c_decimal_big" , "DECIMAL(60,3)" , "780780780.780" },
992+ // Bit.
993+ {"c_bit" , "BIT(20)" , "b'11100011100011100011'" },
994+ // Date and time.
995+ {"c_date" , "DATE" , "'2020-01-02'" },
996+ {"c_datetime" , "DATETIME(3)" , "'2020-01-02 03:04:05.678'" },
997+ {"c_timestamp" , "TIMESTAMP(6)" , "'2020-01-02 03:04:05.678901'" },
998+ {"c_time" , "TIME" , "'13:14:15'" },
999+ {"c_year" , "YEAR" , "2021" },
1000+ // Strings.
1001+ {"c_char" , "CHAR(10)" , "'abc'" },
1002+ {"c_varchar" , "VARCHAR(20)" , "'hello world'" },
1003+ {"c_text" , "TEXT" , "'some text'" },
1004+ {"c_enum" , "ENUM('a','b','c')" , "'b'" },
1005+ {"c_set" , "SET('x','y','z')" , "'x,z'" },
1006+ // Binary. BINARY(4) holds exactly 4 bytes, so there is no trailing-zero padding to
1007+ // reconcile (that case has its own test, Test_MySQL_Binary_Trailing_Zeros).
1008+ {"c_binary" , "BINARY(4)" , "'abcd'" },
1009+ {"c_varbinary" , "VARBINARY(10)" , "'binblob'" },
1010+ {"c_blob" , "BLOB" , "'someblob'" },
1011+ // JSON.
1012+ {"c_json" , "JSON" , `'{"a":2}'` },
1013+ }
1014+
1015+ adds := make ([]string , len (cols ))
1016+ names := make ([]string , 0 , len (cols )+ 1 )
1017+ vals := make ([]string , len (cols ))
1018+ names = append (names , "id" )
1019+ for i , c := range cols {
1020+ adds [i ] = "ADD COLUMN " + c .name + " " + c .def
1021+ names = append (names , c .name )
1022+ vals [i ] = c .val
1023+ }
1024+
1025+ // Single multi-column ALTER: processAlterTableQuery iterates every ADD COLUMN spec, so this
1026+ // exercises the DDL parse for all types at once.
1027+ require .NoError (s .t , s .source .Exec (s .t .Context (),
1028+ fmt .Sprintf ("ALTER TABLE %s %s" , srcFullName , strings .Join (adds , ", " ))))
1029+
1030+ require .NoError (s .t , s .source .Exec (s .t .Context (),
1031+ fmt .Sprintf ("INSERT INTO %s (%s) VALUES (1, %s)" , srcFullName , strings .Join (names , ", " ), strings .Join (vals , ", " ))))
1032+
1033+ EnvWaitForEqualTablesWithNames (env , s , "waiting on cdc add column" , srcTableName , dstTableName , strings .Join (names , "," ))
1034+
1035+ env .Cancel (s .t .Context ())
1036+ RequireEnvCanceled (s .t , env )
1037+ }
1038+
9301039func (s ClickHouseSuite ) Test_MySQL_Numbers () {
9311040 if mysource , ok := s .source .(* MySqlSource ); ! ok || mysource .Config .Flavor != protos .MySqlFlavor_MYSQL_MYSQL {
9321041 s .t .Skip ("only applies to mysql" )
0 commit comments