@@ -219,6 +219,21 @@ func (s APITestSuite) waitForFlowDropped(env WorkflowRun, flowJobName string) {
219219 })
220220}
221221
222+ func (s APITestSuite ) waitForDestinationRowCount (env WorkflowRun , dstTable string , expected int64 ) {
223+ s .t .Helper ()
224+
225+ EnvWaitFor (s .t , env , 3 * time .Minute , fmt .Sprintf ("wait for %d rows in %s" , expected , dstTable ), func () bool {
226+ var count int64
227+ if err := s .pg .Conn ().QueryRow (
228+ s .t .Context (), "SELECT COUNT(*) FROM " + dstTable ,
229+ ).Scan (& count ); err != nil {
230+ s .t .Log (err )
231+ return false
232+ }
233+ return count == expected
234+ })
235+ }
236+
222237func testApi [TSource SuiteSource ](
223238 t * testing.T ,
224239 setup func (* testing.T , string ) (TSource , error ),
@@ -3728,3 +3743,146 @@ func (s APITestSuite) TestSnapshotNullPartitionKey() {
37283743 EnvWaitForFinished (s .t , env , 3 * time .Minute )
37293744 EnvWaitForCount (env , s .ch , "all 4 rows including nulls should be snapshotted" , tableName , "*" , 4 )
37303745}
3746+
3747+ func (s APITestSuite ) TestGetMirrorRowCounts () {
3748+ _ , ok := s .source .(* PostgresSource )
3749+ if ! ok {
3750+ s .t .Skip ("row count validation is only supported for PostgreSQL source and destination" )
3751+ }
3752+
3753+ srcTable1 := AttachSchema (s , "rowcount_src1" )
3754+ dstTable1 := AttachSchema (s , "rowcount_dst1" )
3755+ srcTable2 := AttachSchema (s , "rowcount_src2" )
3756+ dstTable2 := AttachSchema (s , "rowcount_dst2" )
3757+ srcTable3 := AttachSchema (s , "rowcount_src3" )
3758+ dstTable3 := AttachSchema (s , "rowcount_dst3" )
3759+
3760+ for _ , tbl := range []string {srcTable1 , srcTable2 , srcTable3 } {
3761+ require .NoError (s .t , s .source .Exec (s .t .Context (),
3762+ fmt .Sprintf ("CREATE TABLE %s(id int primary key, val text)" , tbl )))
3763+ }
3764+ for _ , tbl := range []string {dstTable1 , dstTable2 , dstTable3 } {
3765+ _ , err := s .pg .Conn ().Exec (s .t .Context (),
3766+ fmt .Sprintf ("CREATE TABLE %s(id int primary key, val text)" , tbl ))
3767+ require .NoError (s .t , err )
3768+ }
3769+
3770+ require .NoError (s .t , s .source .Exec (s .t .Context (),
3771+ fmt .Sprintf ("INSERT INTO %s(id, val) SELECT g, 'v'||g FROM generate_series(1,3) g" , srcTable1 )))
3772+ require .NoError (s .t , s .source .Exec (s .t .Context (),
3773+ fmt .Sprintf ("INSERT INTO %s(id, val) SELECT g, 'v'||g FROM generate_series(1,5) g" , srcTable2 )))
3774+
3775+ flowJobName := "get_row_counts_" + s .suffix
3776+ flowConnConfig := & protos.FlowConnectionConfigs {
3777+ FlowJobName : flowJobName ,
3778+ SourceName : s .source .GeneratePeer (s .t ).Name ,
3779+ DestinationName : s .pg .GeneratePeer (s .t ).Name ,
3780+ TableMappings : []* protos.TableMapping {
3781+ {SourceTableIdentifier : srcTable1 , DestinationTableIdentifier : dstTable1 },
3782+ {SourceTableIdentifier : srcTable2 , DestinationTableIdentifier : dstTable2 },
3783+ {SourceTableIdentifier : srcTable3 , DestinationTableIdentifier : dstTable3 },
3784+ },
3785+ DoInitialSnapshot : true ,
3786+ System : protos .TypeSystem_PG ,
3787+ IdleTimeoutSeconds : 15 ,
3788+ Version : shared .InternalVersion_Latest ,
3789+ }
3790+
3791+ response , err := s .CreateCDCFlow (s .t .Context (), & protos.CreateCDCFlowRequest {ConnectionConfigs : flowConnConfig })
3792+ require .NoError (s .t , err )
3793+ require .NotNil (s .t , response )
3794+
3795+ tc := NewTemporalClient (s .t )
3796+ env , err := GetPeerflow (s .t .Context (), s .catalog , tc , flowJobName )
3797+ require .NoError (s .t , err )
3798+ SetupCDCFlowStatusQuery (s .t , env , flowConnConfig )
3799+ EnvWaitFor (s .t , env , 3 * time .Minute , "wait for initial load to finish" , func () bool {
3800+ return env .GetFlowStatus (s .t ) == protos .FlowStatus_STATUS_RUNNING
3801+ })
3802+ s .waitForDestinationRowCount (env , dstTable1 , 3 )
3803+ s .waitForDestinationRowCount (env , dstTable2 , 5 )
3804+ s .waitForDestinationRowCount (env , dstTable3 , 0 )
3805+
3806+ rowCounts , err := s .GetMirrorRowCounts (s .t .Context (), & protos.RowCountRequest {FlowJobName : flowJobName })
3807+ require .NoError (s .t , err )
3808+ require .NotNil (s .t , rowCounts )
3809+ require .Len (s .t , rowCounts .TableCounts , 3 )
3810+
3811+ bySrc := make (map [string ]* protos.TableRowCount , len (rowCounts .TableCounts ))
3812+ for _ , tc := range rowCounts .TableCounts {
3813+ bySrc [tc .SourceTable ] = tc
3814+ }
3815+ require .Contains (s .t , bySrc , srcTable1 )
3816+ require .Contains (s .t , bySrc , srcTable2 )
3817+ require .Contains (s .t , bySrc , srcTable3 )
3818+
3819+ require .Equal (s .t , dstTable1 , bySrc [srcTable1 ].DestinationTable )
3820+ require .Equal (s .t , int64 (3 ), bySrc [srcTable1 ].SourceCount )
3821+ require .Equal (s .t , int64 (3 ), bySrc [srcTable1 ].DestinationCount )
3822+ require .False (s .t , bySrc [srcTable1 ].SourceIsApproximate )
3823+ require .False (s .t , bySrc [srcTable1 ].DestinationIsApproximate )
3824+
3825+ require .Equal (s .t , dstTable2 , bySrc [srcTable2 ].DestinationTable )
3826+ require .Equal (s .t , int64 (5 ), bySrc [srcTable2 ].SourceCount )
3827+ require .Equal (s .t , int64 (5 ), bySrc [srcTable2 ].DestinationCount )
3828+
3829+ require .Equal (s .t , dstTable3 , bySrc [srcTable3 ].DestinationTable )
3830+ require .Equal (s .t , int64 (0 ), bySrc [srcTable3 ].SourceCount )
3831+ require .Equal (s .t , int64 (0 ), bySrc [srcTable3 ].DestinationCount )
3832+ require .False (s .t , bySrc [srcTable3 ].SourceIsApproximate )
3833+ require .False (s .t , bySrc [srcTable3 ].DestinationIsApproximate )
3834+
3835+ filtered , err := s .GetMirrorRowCounts (s .t .Context (), & protos.RowCountRequest {
3836+ FlowJobName : flowJobName ,
3837+ SourceTables : []string {srcTable2 },
3838+ })
3839+ require .NoError (s .t , err )
3840+ require .Len (s .t , filtered .TableCounts , 1 )
3841+ require .Equal (s .t , srcTable2 , filtered .TableCounts [0 ].SourceTable )
3842+ require .Equal (s .t , int64 (5 ), filtered .TableCounts [0 ].SourceCount )
3843+ require .Equal (s .t , int64 (5 ), filtered .TableCounts [0 ].DestinationCount )
3844+
3845+ require .NoError (s .t , s .source .Exec (s .t .Context (),
3846+ fmt .Sprintf ("INSERT INTO %s(id, val) VALUES (4,'v4'),(5,'v5')" , srcTable1 )))
3847+ s .waitForDestinationRowCount (env , dstTable1 , 5 )
3848+
3849+ afterCdc , err := s .GetMirrorRowCounts (s .t .Context (), & protos.RowCountRequest {
3850+ FlowJobName : flowJobName ,
3851+ SourceTables : []string {srcTable1 },
3852+ })
3853+ require .NoError (s .t , err )
3854+ require .Len (s .t , afterCdc .TableCounts , 1 )
3855+ require .Equal (s .t , int64 (5 ), afterCdc .TableCounts [0 ].SourceCount )
3856+ require .Equal (s .t , int64 (5 ), afterCdc .TableCounts [0 ].DestinationCount )
3857+
3858+ env .Cancel (s .t .Context ())
3859+ RequireEnvCanceled (s .t , env )
3860+
3861+ require .NoError (s .t , s .source .Exec (s .t .Context (), "DROP TABLE " + srcTable3 ))
3862+
3863+ missingCounts , err := s .GetMirrorRowCounts (s .t .Context (), & protos.RowCountRequest {
3864+ FlowJobName : flowJobName ,
3865+ SourceTables : []string {srcTable3 },
3866+ })
3867+ require .NoError (s .t , err )
3868+ require .Len (s .t , missingCounts .TableCounts , 1 )
3869+ require .Equal (s .t , srcTable3 , missingCounts .TableCounts [0 ].SourceTable )
3870+ require .Equal (s .t , int64 (- 1 ), missingCounts .TableCounts [0 ].SourceCount )
3871+ require .Equal (s .t , int64 (0 ), missingCounts .TableCounts [0 ].DestinationCount )
3872+ }
3873+
3874+ func (s APITestSuite ) TestGetMirrorRowCountsNonCDC () {
3875+ _ , ok := s .source .(* PostgresSource )
3876+ if ! ok {
3877+ s .t .Skip ("row count validation is only supported for PostgreSQL source and destination" )
3878+ }
3879+
3880+ _ , err := s .GetMirrorRowCounts (s .t .Context (), & protos.RowCountRequest {
3881+ FlowJobName : "nonexistent_flow_" + s .suffix ,
3882+ })
3883+ require .Error (s .t , err )
3884+ grpcStatus , ok := status .FromError (err )
3885+ require .True (s .t , ok )
3886+ require .Equal (s .t , codes .InvalidArgument , grpcStatus .Code ())
3887+ require .Contains (s .t , grpcStatus .Message (), "not supported for query replication mirrors" )
3888+ }
0 commit comments