@@ -325,6 +325,81 @@ func (s APITestSuite) TestClickHouseMirrorValidation_Pass() {
325325 require .NotNil (s .t , response )
326326}
327327
328+ // TestValidateCDCMirror_ServerIDPeerReuse verifies that validating a CDC mirror whose MySQL source
329+ // peer pins a fixed server_id is rejected when that peer already backs another streaming CDC mirror.
330+ func (s APITestSuite ) TestValidateCDCMirror_ServerIDPeerReuse () {
331+ mySource , ok := s .source .(* MySqlSource )
332+ if ! ok {
333+ s .t .Skip ("server_id peer-reuse validation is MySQL-specific" )
334+ }
335+ ctx := s .t .Context ()
336+
337+ // Source table backing the existing mirror.
338+ require .NoError (s .t , s .source .Exec (ctx ,
339+ fmt .Sprintf ("CREATE TABLE %s(id int primary key, val text)" , AttachSchema (s , "serverid_reuse" ))))
340+
341+ // A MySQL source peer that pins a fixed server_id. It clones the suite's MySQL config, so it
342+ // points at the same (reachable) instance.
343+ serverID := uint32 (4224 )
344+ pinnedConfig := proto .CloneOf (mySource .Config )
345+ pinnedConfig .ServerId = & serverID
346+ peerName := "serverid_reuse_" + s .suffix
347+ _ , err := s .CreatePeer (ctx , & protos.CreatePeerRequest {
348+ Peer : & protos.Peer {
349+ Name : peerName ,
350+ Type : protos .DBType_MYSQL ,
351+ Config : & protos.Peer_MysqlConfig {MysqlConfig : pinnedConfig },
352+ },
353+ })
354+ require .NoError (s .t , err )
355+
356+ // An existing streaming CDC mirror that already uses the pinned-server_id peer as its source.
357+ existingGen := FlowConnectionGenerationConfig {
358+ FlowJobName : "serverid_reuse_existing_" + s .suffix ,
359+ TableNameMapping : map [string ]string {AttachSchema (s , "serverid_reuse" ): "serverid_reuse" },
360+ Destination : s .ch .Peer ().Name ,
361+ }
362+ existingCfg := existingGen .GenerateFlowConnectionConfigs (s )
363+ existingCfg .SourceName = peerName
364+ existingCfg .DoInitialSnapshot = true
365+ createResp , err := s .CreateCDCFlow (ctx , & protos.CreateCDCFlowRequest {ConnectionConfigs : existingCfg })
366+ require .NoError (s .t , err )
367+ require .NotNil (s .t , createResp )
368+
369+ tc := NewTemporalClient (s .t )
370+ env , err := GetPeerflow (ctx , s .catalog , tc , existingCfg .FlowJobName )
371+ require .NoError (s .t , err )
372+
373+ // Validating a second CDC mirror on the same pinned-server_id peer must be rejected.
374+ newGen := FlowConnectionGenerationConfig {
375+ FlowJobName : "serverid_reuse_new_" + s .suffix ,
376+ TableNameMapping : map [string ]string {AttachSchema (s , "serverid_reuse" ): "serverid_reuse" },
377+ Destination : s .ch .Peer ().Name ,
378+ }
379+ newCfg := newGen .GenerateFlowConnectionConfigs (s )
380+ newCfg .SourceName = peerName
381+ newCfg .DoInitialSnapshot = true
382+
383+ res , err := s .ValidateCDCMirror (ctx , & protos.CreateCDCFlowRequest {ConnectionConfigs : newCfg })
384+ require .Nil (s .t , res )
385+ require .Error (s .t , err )
386+ st , ok := status .FromError (err )
387+ require .True (s .t , ok )
388+ require .Equal (s .t , codes .FailedPrecondition , st .Code ())
389+ require .Contains (s .t , st .Message (), "cannot be shared across mirrors" )
390+
391+ // Tear down via gRPC: drop the mirror, then the now-unreferenced peer.
392+ _ , err = s .FlowStateChange (ctx , & protos.FlowStateChangeRequest {
393+ FlowJobName : existingCfg .FlowJobName ,
394+ RequestedFlowState : protos .FlowStatus_STATUS_TERMINATING ,
395+ })
396+ require .NoError (s .t , err )
397+ s .waitForFlowDropped (env , existingCfg .FlowJobName )
398+
399+ _ , err = s .DropPeer (ctx , & protos.DropPeerRequest {PeerName : peerName })
400+ require .NoError (s .t , err )
401+ }
402+
328403func (s APITestSuite ) TestClickHouseMirrorValidation_NoPrimaryKey () {
329404 switch s .source .(type ) {
330405 case * PostgresSource , * MySqlSource :
0 commit comments