@@ -3,6 +3,7 @@ package connmysql
33import (
44 "context"
55 "fmt"
6+ "log/slog"
67 "slices"
78 "strings"
89 "testing"
@@ -14,9 +15,15 @@ import (
1415 "github.com/pingcap/tidb/pkg/parser/ast"
1516 tidbmysql "github.com/pingcap/tidb/pkg/parser/mysql"
1617 "github.com/stretchr/testify/require"
18+ "go.opentelemetry.io/otel/metric"
19+ sdkmetric "go.opentelemetry.io/otel/sdk/metric"
20+ "go.opentelemetry.io/otel/sdk/metric/metricdata"
21+ "go.temporal.io/sdk/log"
1722
1823 "github.com/PeerDB-io/peerdb/flow/generated/protos"
1924 "github.com/PeerDB-io/peerdb/flow/internal"
25+ "github.com/PeerDB-io/peerdb/flow/model"
26+ "github.com/PeerDB-io/peerdb/flow/otel_metrics"
2027 "github.com/PeerDB-io/peerdb/flow/shared"
2128)
2229
@@ -155,6 +162,145 @@ func TestParseSQLParsesTrailingNull(t *testing.T) {
155162 }
156163}
157164
165+ func TestClassifyOnlineSchemaMigrationTool (t * testing.T ) {
166+ for _ , tc := range []struct {
167+ name string
168+ oldTable string
169+ newTable string
170+ want string
171+ }{
172+ {"gh-ost" , "_users_gho" , "users" , OnlineSchemaMigrationToolGhOst },
173+ {"pt-osc" , "_users_new" , "users" , OnlineSchemaMigrationToolPtOsc },
174+ {"plain rename" , "users_backup" , "users" , OnlineSchemaMigrationToolOther },
175+ {"gh-ost del table is not the target" , "_users_del" , "users" , OnlineSchemaMigrationToolOther },
176+ } {
177+ t .Run (tc .name , func (t * testing.T ) {
178+ require .Equal (t , tc .want , classifyOnlineSchemaMigrationTool (tc .oldTable , tc .newTable ))
179+ })
180+ }
181+ }
182+
183+ func TestParseSQLParsesGhOstRename (t * testing.T ) {
184+ mysqlParser := parser .New ()
185+ // the atomic swap gh-ost issues at the end of a migration
186+ query := []byte ("RENAME TABLE `mydb`.`users` TO `mydb`.`_users_del`, `mydb`.`_users_gho` TO `mydb`.`users`" )
187+ stmts , warns , err := parseSQL (mysqlParser , query )
188+ require .NoError (t , err )
189+ require .Empty (t , warns )
190+ require .Len (t , stmts , 1 )
191+
192+ rename , ok := stmts [0 ].(* ast.RenameTableStmt )
193+ require .True (t , ok )
194+ require .Len (t , rename .TableToTables , 2 )
195+
196+ // the rename that lands on the tracked table is the ghost table swap
197+ swap := rename .TableToTables [1 ]
198+ require .Equal (t , "users" , swap .NewTable .Name .String ())
199+ require .Equal (t , "_users_gho" , swap .OldTable .Name .String ())
200+ require .Equal (t , OnlineSchemaMigrationToolGhOst ,
201+ classifyOnlineSchemaMigrationTool (swap .OldTable .Name .String (), swap .NewTable .Name .String ()))
202+ }
203+
204+ // newMetricsTestOtelManager builds an OtelManager backed by a ManualReader so tests
205+ // can collect and assert recorded metric values without a real exporter.
206+ func newMetricsTestOtelManager (t * testing.T ) (* otel_metrics.OtelManager , * sdkmetric.ManualReader ) {
207+ t .Helper ()
208+ reader := sdkmetric .NewManualReader ()
209+ provider := sdkmetric .NewMeterProvider (sdkmetric .WithReader (reader ))
210+ om := & otel_metrics.OtelManager {
211+ MetricsProvider : provider ,
212+ Meter : provider .Meter ("test" ),
213+ Int64CountersCache : make (map [string ]metric.Int64Counter ),
214+ }
215+ counter , err := om .GetOrInitInt64Counter (otel_metrics .BuildMetricName (otel_metrics .OnlineSchemaMigrationsName ))
216+ require .NoError (t , err )
217+ om .Metrics .OnlineSchemaMigrationsCounter = counter
218+ t .Cleanup (func () { _ = provider .Shutdown (context .Background ()) })
219+ return om , reader
220+ }
221+
222+ // collectOnlineSchemaMigrationCounts collects the online_schema_migrations counter and
223+ // returns its value bucketed by the `tool` attribute.
224+ func collectOnlineSchemaMigrationCounts (t * testing.T , ctx context.Context , reader * sdkmetric.ManualReader ) map [string ]int64 {
225+ t .Helper ()
226+ var rm metricdata.ResourceMetrics
227+ require .NoError (t , reader .Collect (ctx , & rm ))
228+
229+ wantName := otel_metrics .BuildMetricName (otel_metrics .OnlineSchemaMigrationsName )
230+ counts := make (map [string ]int64 )
231+ for _ , sm := range rm .ScopeMetrics {
232+ for _ , m := range sm .Metrics {
233+ if m .Name != wantName {
234+ continue
235+ }
236+ sum , ok := m .Data .(metricdata.Sum [int64 ])
237+ require .True (t , ok , "expected Int64 sum for %s" , wantName )
238+ for _ , dp := range sum .DataPoints {
239+ tool , _ := dp .Attributes .Value (otel_metrics .OnlineSchemaMigrationTool )
240+ source , _ := dp .Attributes .Value (otel_metrics .SourcePeerType )
241+ require .Equal (t , "mysql" , source .AsString ())
242+ counts [tool .AsString ()] += dp .Value
243+ }
244+ }
245+ }
246+ return counts
247+ }
248+
249+ func TestProcessRenameTableQueryMetric (t * testing.T ) {
250+ ctx := t .Context ()
251+ c := & MySqlConnector {logger : log .NewStructuredLogger (slog .Default ())}
252+
253+ parseRename := func (query string ) * ast.RenameTableStmt {
254+ t .Helper ()
255+ stmts , _ , err := parseSQL (parser .New (), []byte (query ))
256+ require .NoError (t , err )
257+ require .Len (t , stmts , 1 )
258+ rename , ok := stmts [0 ].(* ast.RenameTableStmt )
259+ require .True (t , ok )
260+ return rename
261+ }
262+
263+ for _ , tc := range []struct {
264+ name string
265+ query string
266+ want map [string ]int64
267+ }{
268+ {
269+ name : "gh-ost cutover on tracked table increments once" ,
270+ // full gh-ost atomic swap: only the `_users_gho -> users` pair lands on a tracked table
271+ query : "RENAME TABLE `mydb`.`users` TO `mydb`.`_users_del`, `mydb`.`_users_gho` TO `mydb`.`users`" ,
272+ want : map [string ]int64 {OnlineSchemaMigrationToolGhOst : 1 },
273+ },
274+ {
275+ name : "pt-online-schema-change cutover" ,
276+ query : "RENAME TABLE `mydb`.`users` TO `mydb`.`_users_old`, `mydb`.`_users_new` TO `mydb`.`users`" ,
277+ want : map [string ]int64 {OnlineSchemaMigrationToolPtOsc : 1 },
278+ },
279+ {
280+ name : "schema falls back to event schema" ,
281+ // no schema qualifier on the statement; resolved from the event's schema
282+ query : "RENAME TABLE `users` TO `_users_del`, `_users_gho` TO `users`" ,
283+ want : map [string ]int64 {OnlineSchemaMigrationToolGhOst : 1 },
284+ },
285+ {
286+ name : "rename into untracked table is ignored" ,
287+ // neither target is a table we replicate
288+ query : "RENAME TABLE `mydb`.`orders` TO `mydb`.`orders_archive`" ,
289+ want : map [string ]int64 {},
290+ },
291+ } {
292+ t .Run (tc .name , func (t * testing.T ) {
293+ om , reader := newMetricsTestOtelManager (t )
294+ req := & model.PullRecordsRequest [model.RecordItems ]{
295+ TableNameMapping : map [string ]model.NameAndExclude {"mydb.users" : {Name : "users_dst" }},
296+ }
297+ c .processRenameTableQuery (ctx , om , req , parseRename (tc .query ), "mydb" )
298+
299+ require .Equal (t , tc .want , collectOnlineSchemaMigrationCounts (t , ctx , reader ))
300+ })
301+ }
302+ }
303+
158304func TestGetTableSchemaCaseSensitiveIdentifiers (t * testing.T ) {
159305 t .Parallel ()
160306 ctx := t .Context ()
0 commit comments