@@ -1931,20 +1931,16 @@ func (s ClickHouseSuite) Test_MySQL_String_Partition_Key_UUID_Parallel_Snapshot(
19311931 EnvWaitForEqualTablesWithNames (env , s , "waiting on initial" , srcTable , dstTable , "id,val" )
19321932
19331933 partitionRows , err := s .catalog .Query (s .t .Context (),
1934- `SELECT partition_start, partition_end, COALESCE(rows_in_partition, 0)
1935- FROM peerdb_stats.qrep_partitions WHERE parent_mirror_name = $1` ,
1934+ `SELECT COALESCE(rows_in_partition, 0) FROM peerdb_stats.qrep_partitions WHERE parent_mirror_name = $1` ,
19361935 flowConnConfig .FlowJobName )
19371936 require .NoError (s .t , err )
19381937 defer partitionRows .Close ()
19391938
19401939 var partitionCount int32
19411940 var totalRows int64
19421941 for partitionRows .Next () {
1943- var partitionStart , partitionEnd * string
19441942 var rowsInPartition int64
1945- require .NoError (s .t , partitionRows .Scan (& partitionStart , & partitionEnd , & rowsInPartition ))
1946- require .NotNil (s .t , partitionStart )
1947- require .NotNil (s .t , partitionEnd )
1943+ require .NoError (s .t , partitionRows .Scan (& rowsInPartition ))
19481944 totalRows += rowsInPartition
19491945 partitionCount ++
19501946 }
@@ -2022,3 +2018,102 @@ func (s ClickHouseSuite) Test_MySQL_String_Partition_Key_Arbitrary_FullTable() {
20222018 env .Cancel (s .t .Context ())
20232019 RequireEnvCanceled (s .t , env )
20242020}
2021+
2022+ // Test_MySQL_String_Partition_Key_Sensitive_Data_Handling verifies that string partition ranges,
2023+ // which may carry sensitive watermark values, are never stored in plaintext in the catalog: they are
2024+ // stripped from peerdb_stats.qrep_partitions and persisted encrypted in metadata_qrep_partition_ranges.
2025+ func (s ClickHouseSuite ) Test_MySQL_String_Partition_Key_Sensitive_Range_Handling () {
2026+ if _ , ok := s .source .(* MySqlSource ); ! ok {
2027+ s .t .Skip ("only applies to mysql" )
2028+ }
2029+
2030+ srcTable := "test_string_pk_sensitive"
2031+ srcFullName := s .attachSchemaSuffix (srcTable )
2032+ dstTable := "test_string_pk_sensitive_dst"
2033+
2034+ const numRows = 8
2035+ const numPartitions = 4
2036+ require .NoError (s .t , s .source .Exec (s .t .Context (),
2037+ fmt .Sprintf ("CREATE TABLE %s (id CHAR(36) PRIMARY KEY, val INT NOT NULL)" , srcFullName )))
2038+ insertedUUIDs := make ([]string , 0 , numRows )
2039+ for i := 1 ; i <= numRows ; i ++ {
2040+ id := uuid .NewString ()
2041+ insertedUUIDs = append (insertedUUIDs , id )
2042+ require .NoError (s .t , s .source .Exec (s .t .Context (),
2043+ fmt .Sprintf ("INSERT INTO %s (id, val) VALUES ('%s', %d)" , srcFullName , id , i )))
2044+ }
2045+
2046+ tableMappings := TableMappings (s , srcTable , dstTable )
2047+ // a String column can't be used directly as a Distributed sharding key (ClickHouse
2048+ // requires an integer-typed sharding expression), so hash it for the cluster suite
2049+ for _ , tm := range tableMappings {
2050+ tm .ShardingKey = "cityHash64(id)"
2051+ }
2052+ connectionGen := FlowConnectionGenerationConfig {
2053+ FlowJobName : s .attachSuffix ("string_pk_sensitive" ),
2054+ TableMappings : tableMappings ,
2055+ Destination : s .Peer ().Name ,
2056+ }
2057+ flowConnConfig := connectionGen .GenerateFlowConnectionConfigs (s )
2058+ flowConnConfig .DoInitialSnapshot = true
2059+ flowConnConfig .SnapshotMaxParallelWorkers = 4
2060+ flowConnConfig .SnapshotNumPartitionsOverride = numPartitions
2061+
2062+ tc := NewTemporalClient (s .t )
2063+ env := ExecutePeerflow (s .t , tc , flowConnConfig )
2064+ SetupCDCFlowStatusQuery (s .t , env , flowConnConfig )
2065+
2066+ EnvWaitForEqualTablesWithNames (env , s , "waiting on initial" , srcTable , dstTable , "id,val" )
2067+
2068+ partitionRows , err := s .catalog .Query (s .t .Context (),
2069+ `SELECT partition_start, partition_end FROM peerdb_stats.qrep_partitions WHERE parent_mirror_name = $1` ,
2070+ flowConnConfig .FlowJobName )
2071+ require .NoError (s .t , err )
2072+ defer partitionRows .Close ()
2073+
2074+ var partitionCount int32
2075+ for partitionRows .Next () {
2076+ var partitionStart , partitionEnd * string
2077+ require .NoError (s .t , partitionRows .Scan (& partitionStart , & partitionEnd ))
2078+ require .Empty (s .t , partitionStart )
2079+ require .Empty (s .t , partitionEnd )
2080+ partitionCount ++
2081+ }
2082+ require .NoError (s .t , partitionRows .Err ())
2083+ require .EqualValues (s .t , numPartitions , partitionCount )
2084+
2085+ rangeRows , err := s .catalog .Query (s .t .Context (),
2086+ `SELECT enc_key_id, range_payload FROM metadata_qrep_partition_ranges WHERE parent_mirror_name = $1` ,
2087+ flowConnConfig .FlowJobName )
2088+ require .NoError (s .t , err )
2089+ defer rangeRows .Close ()
2090+
2091+ var encryptedCount int32
2092+ for rangeRows .Next () {
2093+ var encKeyID string
2094+ var rangePayload []byte
2095+ require .NoError (s .t , rangeRows .Scan (& encKeyID , & rangePayload ))
2096+ require .NotEmpty (s .t , encKeyID )
2097+ require .NotEmpty (s .t , rangePayload )
2098+ // payload must be ciphertext: none of the plaintext watermark UUIDs should appear in it
2099+ for _ , id := range insertedUUIDs {
2100+ require .NotContains (s .t , string (rangePayload ), id )
2101+ }
2102+ encryptedCount ++
2103+ }
2104+ require .NoError (s .t , rangeRows .Err ())
2105+ require .EqualValues (s .t , numPartitions , encryptedCount )
2106+
2107+ env .Cancel (s .t .Context ())
2108+ RequireEnvCanceled (s .t , env )
2109+
2110+ // dropping the mirror should delete metadata_qrep_partition_ranges entries from the catalog for this mirror.
2111+ dropEnv := ExecuteDropFlow (s .t .Context (), tc , flowConnConfig , 0 )
2112+ EnvWaitForFinished (s .t , dropEnv , 3 * time .Minute )
2113+
2114+ var remainingRanges int64
2115+ require .NoError (s .t , s .catalog .QueryRow (s .t .Context (),
2116+ `SELECT COUNT(*) FROM metadata_qrep_partition_ranges WHERE parent_mirror_name = $1` ,
2117+ flowConnConfig .FlowJobName ).Scan (& remainingRanges ))
2118+ require .Zero (s .t , remainingRanges )
2119+ }
0 commit comments