@@ -1819,20 +1819,16 @@ func (s ClickHouseSuite) Test_MySQL_String_Partition_Key_UUID_Parallel_Snapshot(
18191819 EnvWaitForEqualTablesWithNames (env , s , "waiting on initial" , srcTable , dstTable , "id,val" )
18201820
18211821 partitionRows , err := s .catalog .Query (s .t .Context (),
1822- `SELECT partition_start, partition_end, COALESCE(rows_in_partition, 0)
1823- FROM peerdb_stats.qrep_partitions WHERE parent_mirror_name = $1` ,
1822+ `SELECT COALESCE(rows_in_partition, 0) FROM peerdb_stats.qrep_partitions WHERE parent_mirror_name = $1` ,
18241823 flowConnConfig .FlowJobName )
18251824 require .NoError (s .t , err )
18261825 defer partitionRows .Close ()
18271826
18281827 var partitionCount int32
18291828 var totalRows int64
18301829 for partitionRows .Next () {
1831- var partitionStart , partitionEnd * string
18321830 var rowsInPartition int64
1833- require .NoError (s .t , partitionRows .Scan (& partitionStart , & partitionEnd , & rowsInPartition ))
1834- require .NotNil (s .t , partitionStart )
1835- require .NotNil (s .t , partitionEnd )
1831+ require .NoError (s .t , partitionRows .Scan (& rowsInPartition ))
18361832 totalRows += rowsInPartition
18371833 partitionCount ++
18381834 }
@@ -1910,3 +1906,103 @@ func (s ClickHouseSuite) Test_MySQL_String_Partition_Key_Arbitrary_FullTable() {
19101906 env .Cancel (s .t .Context ())
19111907 RequireEnvCanceled (s .t , env )
19121908}
1909+
1910+ // Test_MySQL_String_Partition_Key_Sensitive_Data_Handling verifies that string partition ranges,
1911+ // which may carry sensitive watermark values, are never stored in plaintext in the catalog: they are
1912+ // stripped from peerdb_stats.qrep_partitions and persisted encrypted in metadata_qrep_partition_ranges.
1913+ func (s ClickHouseSuite ) Test_MySQL_String_Partition_Key_Sensitive_Range_Handling () {
1914+ if _ , ok := s .source .(* MySqlSource ); ! ok {
1915+ s .t .Skip ("only applies to mysql" )
1916+ }
1917+
1918+ srcTable := "test_string_pk_sensitive"
1919+ srcFullName := s .attachSchemaSuffix (srcTable )
1920+ dstTable := "test_string_pk_sensitive_dst"
1921+
1922+ const numRows = 8
1923+ const numPartitions = 4
1924+ require .NoError (s .t , s .source .Exec (s .t .Context (),
1925+ fmt .Sprintf ("CREATE TABLE %s (id CHAR(36) PRIMARY KEY, val INT NOT NULL)" , srcFullName )))
1926+ insertedUUIDs := make ([]string , 0 , numRows )
1927+ for i := 1 ; i <= numRows ; i ++ {
1928+ id := uuid .NewString ()
1929+ insertedUUIDs = append (insertedUUIDs , id )
1930+ require .NoError (s .t , s .source .Exec (s .t .Context (),
1931+ fmt .Sprintf ("INSERT INTO %s (id, val) VALUES ('%s', %d)" , srcFullName , id , i )))
1932+ }
1933+
1934+ tableMappings := TableMappings (s , srcTable , dstTable )
1935+ // a String column can't be used directly as a Distributed sharding key (ClickHouse
1936+ // requires an integer-typed sharding expression), so hash it for the cluster suite
1937+ for _ , tm := range tableMappings {
1938+ tm .ShardingKey = "cityHash64(id)"
1939+ }
1940+ connectionGen := FlowConnectionGenerationConfig {
1941+ FlowJobName : s .attachSuffix ("string_pk_sensitive" ),
1942+ TableMappings : tableMappings ,
1943+ Destination : s .Peer ().Name ,
1944+ }
1945+ flowConnConfig := connectionGen .GenerateFlowConnectionConfigs (s )
1946+ flowConnConfig .DoInitialSnapshot = true
1947+ flowConnConfig .SnapshotMaxParallelWorkers = 4
1948+ flowConnConfig .SnapshotNumPartitionsOverride = numPartitions
1949+
1950+ tc := NewTemporalClient (s .t )
1951+ env := ExecutePeerflow (s .t , tc , flowConnConfig )
1952+ SetupCDCFlowStatusQuery (s .t , env , flowConnConfig )
1953+
1954+ EnvWaitForEqualTablesWithNames (env , s , "waiting on initial" , srcTable , dstTable , "id,val" )
1955+
1956+ partitionRows , err := s .catalog .Query (s .t .Context (),
1957+ `SELECT partition_start, partition_end FROM peerdb_stats.qrep_partitions WHERE parent_mirror_name = $1` ,
1958+ flowConnConfig .FlowJobName )
1959+ require .NoError (s .t , err )
1960+ defer partitionRows .Close ()
1961+
1962+ var partitionCount int32
1963+ for partitionRows .Next () {
1964+ var partitionStart , partitionEnd * string
1965+ var rowsInPartition int64
1966+ require .NoError (s .t , partitionRows .Scan (& partitionStart , & partitionEnd , & rowsInPartition ))
1967+ require .Empty (s .t , partitionStart )
1968+ require .Empty (s .t , partitionEnd )
1969+ partitionCount ++
1970+ }
1971+ require .NoError (s .t , partitionRows .Err ())
1972+ require .EqualValues (s .t , numPartitions , partitionCount )
1973+
1974+ rangeRows , err := s .catalog .Query (s .t .Context (),
1975+ `SELECT enc_key_id, range_payload FROM metadata_qrep_partition_ranges WHERE parent_mirror_name = $1` ,
1976+ flowConnConfig .FlowJobName )
1977+ require .NoError (s .t , err )
1978+ defer rangeRows .Close ()
1979+
1980+ var encryptedCount int32
1981+ for rangeRows .Next () {
1982+ var encKeyID string
1983+ var rangePayload []byte
1984+ require .NoError (s .t , rangeRows .Scan (& encKeyID , & rangePayload ))
1985+ require .NotEmpty (s .t , encKeyID )
1986+ require .NotEmpty (s .t , rangePayload )
1987+ // payload must be ciphertext: none of the plaintext watermark UUIDs should appear in it
1988+ for _ , id := range insertedUUIDs {
1989+ require .NotContains (s .t , string (rangePayload ), id )
1990+ }
1991+ encryptedCount ++
1992+ }
1993+ require .NoError (s .t , rangeRows .Err ())
1994+ require .EqualValues (s .t , numPartitions , encryptedCount )
1995+
1996+ env .Cancel (s .t .Context ())
1997+ RequireEnvCanceled (s .t , env )
1998+
1999+ // dropping the mirror should delete metadata_qrep_partition_ranges entries from the catalog for this mirror.
2000+ dropEnv := ExecuteDropFlow (s .t .Context (), tc , flowConnConfig , 0 )
2001+ EnvWaitForFinished (s .t , dropEnv , 3 * time .Minute )
2002+
2003+ var remainingRanges int64
2004+ require .NoError (s .t , s .catalog .QueryRow (s .t .Context (),
2005+ `SELECT COUNT(*) FROM metadata_qrep_partition_ranges WHERE parent_mirror_name = $1` ,
2006+ flowConnConfig .FlowJobName ).Scan (& remainingRanges ))
2007+ require .Zero (s .t , remainingRanges )
2008+ }
0 commit comments