@@ -1486,6 +1486,107 @@ func (this *Applier) buildDMLEventQuery(dmlEvent *binlog.BinlogDMLEvent) []*dmlB
14861486 return []* dmlBuildResult {newDmlBuildResultError (fmt .Errorf ("Unknown dml event type: %+v" , dmlEvent .DML ))}
14871487}
14881488
1489+ // executeBatchWithWarningChecking executes a batch of DML statements with SHOW WARNINGS
1490+ // interleaved after each statement to detect warnings from any statement in the batch.
1491+ // This is used when PanicOnWarnings is enabled to ensure warnings from middle statements
1492+ // are not lost (SHOW WARNINGS only shows warnings from the last statement in a multi-statement batch).
1493+ func (this * Applier ) executeBatchWithWarningChecking (ctx context.Context , tx * gosql.Tx , buildResults []* dmlBuildResult ) (int64 , error ) {
1494+ // Build query with interleaved SHOW WARNINGS: stmt1; SHOW WARNINGS; stmt2; SHOW WARNINGS; ...
1495+ var queryBuilder strings.Builder
1496+ args := make ([]interface {}, 0 )
1497+
1498+ for _ , buildResult := range buildResults {
1499+ queryBuilder .WriteString (buildResult .query )
1500+ queryBuilder .WriteString (";\n SHOW WARNINGS;\n " )
1501+ args = append (args , buildResult .args ... )
1502+ }
1503+
1504+ query := queryBuilder .String ()
1505+
1506+ // Execute the multi-statement query
1507+ rows , err := tx .QueryContext (ctx , query , args ... )
1508+ if err != nil {
1509+ return 0 , fmt .Errorf ("%w; query=%s; args=%+v" , err , query , args )
1510+ }
1511+ defer rows .Close ()
1512+
1513+ var totalDelta int64
1514+
1515+ // QueryContext with multi-statement queries returns rows positioned at the first result set
1516+ // that produces rows (i.e., the first SHOW WARNINGS), automatically skipping DML results.
1517+ // Verify we're at a SHOW WARNINGS result set (should have 3 columns: Level, Code, Message)
1518+ cols , err := rows .Columns ()
1519+ if err != nil {
1520+ return 0 , fmt .Errorf ("failed to get columns: %w" , err )
1521+ }
1522+
1523+ // If somehow we're not at a result set with columns, try to advance
1524+ if len (cols ) == 0 {
1525+ if ! rows .NextResultSet () {
1526+ return 0 , fmt .Errorf ("expected SHOW WARNINGS result set after first statement" )
1527+ }
1528+ }
1529+
1530+ // Compile regex once before loop to avoid performance penalty and handle errors properly
1531+ migrationKeyRegex , err := this .compileMigrationKeyWarningRegex ()
1532+ if err != nil {
1533+ return 0 , err
1534+ }
1535+
1536+ // Iterate through SHOW WARNINGS result sets.
1537+ // DML statements don't create navigable result sets, so we move directly between SHOW WARNINGS.
1538+ // Pattern: [at SHOW WARNINGS #1] -> read warnings -> NextResultSet() -> [at SHOW WARNINGS #2] -> ...
1539+ for i := 0 ; i < len (buildResults ); i ++ {
1540+ // We can't get exact rows affected with QueryContext (needed for reading SHOW WARNINGS).
1541+ // Use the theoretical delta (+1 for INSERT, -1 for DELETE, 0 for UPDATE) as an approximation.
1542+ // This may be inaccurate (e.g., INSERT IGNORE with duplicate affects 0 rows but we count +1).
1543+ totalDelta += buildResults [i ].rowsDelta
1544+
1545+ // Read warnings from this statement's SHOW WARNINGS result set
1546+ var sqlWarnings []string
1547+ for rows .Next () {
1548+ var level , message string
1549+ var code int
1550+ if err := rows .Scan (& level , & code , & message ); err != nil {
1551+ // Scan failure means we cannot reliably read warnings.
1552+ // Since PanicOnWarnings is a safety feature, we must fail hard rather than silently skip.
1553+ return 0 , fmt .Errorf ("failed to scan SHOW WARNINGS for statement %d: %w" , i + 1 , err )
1554+ }
1555+
1556+ if strings .Contains (message , "Duplicate entry" ) && migrationKeyRegex .MatchString (message ) {
1557+ // Duplicate entry on migration unique key is expected during binlog replay
1558+ // (row was already copied during bulk copy phase)
1559+ continue
1560+ }
1561+ sqlWarnings = append (sqlWarnings , fmt .Sprintf ("%s: %s (%d)" , level , message , code ))
1562+ }
1563+
1564+ // Check for errors that occurred while iterating through warnings
1565+ if err := rows .Err (); err != nil {
1566+ return 0 , fmt .Errorf ("error reading SHOW WARNINGS result set for statement %d: %w" , i + 1 , err )
1567+ }
1568+
1569+ if len (sqlWarnings ) > 0 {
1570+ return 0 , fmt .Errorf ("warnings detected in statement %d of %d: %v" , i + 1 , len (buildResults ), sqlWarnings )
1571+ }
1572+
1573+ // Move to the next statement's SHOW WARNINGS result set
1574+ // For the last statement, there's no next result set
1575+ // DML statements don't create result sets, so we only need one NextResultSet call
1576+ // to move from SHOW WARNINGS #N to SHOW WARNINGS #(N+1)
1577+ if i < len (buildResults )- 1 {
1578+ if ! rows .NextResultSet () {
1579+ if err := rows .Err (); err != nil {
1580+ return 0 , fmt .Errorf ("error moving to SHOW WARNINGS for statement %d: %w" , i + 2 , err )
1581+ }
1582+ return 0 , fmt .Errorf ("expected SHOW WARNINGS result set for statement %d" , i + 2 )
1583+ }
1584+ }
1585+ }
1586+
1587+ return totalDelta , nil
1588+ }
1589+
14891590// ApplyDMLEventQueries applies multiple DML queries onto the _ghost_ table
14901591func (this * Applier ) ApplyDMLEventQueries (dmlEvents [](* binlog.BinlogDMLEvent )) error {
14911592 var totalDelta int64
@@ -1525,82 +1626,52 @@ func (this *Applier) ApplyDMLEventQueries(dmlEvents [](*binlog.BinlogDMLEvent))
15251626 }
15261627 }
15271628
1528- // We batch together the DML queries into multi-statements to minimize network trips.
1529- // We have to use the raw driver connection to access the rows affected
1530- // for each statement in the multi-statement.
1531- execErr := conn .Raw (func (driverConn any ) error {
1532- ex := driverConn .(driver.ExecerContext )
1533- nvc := driverConn .(driver.NamedValueChecker )
1534-
1535- multiArgs := make ([]driver.NamedValue , 0 , nArgs )
1536- multiQueryBuilder := strings.Builder {}
1537- for _ , buildResult := range buildResults {
1538- for _ , arg := range buildResult .args {
1539- nv := driver.NamedValue {Value : driver .Value (arg )}
1540- nvc .CheckNamedValue (& nv )
1541- multiArgs = append (multiArgs , nv )
1542- }
1543-
1544- multiQueryBuilder .WriteString (buildResult .query )
1545- multiQueryBuilder .WriteString (";\n " )
1546- }
1547-
1548- res , err := ex .ExecContext (ctx , multiQueryBuilder .String (), multiArgs )
1549- if err != nil {
1550- err = fmt .Errorf ("%w; query=%s; args=%+v" , err , multiQueryBuilder .String (), multiArgs )
1551- return err
1552- }
1553-
1554- mysqlRes := res .(drivermysql.Result )
1555-
1556- // each DML is either a single insert (delta +1), update (delta +0) or delete (delta -1).
1557- // multiplying by the rows actually affected (either 0 or 1) will give an accurate row delta for this DML event
1558- for i , rowsAffected := range mysqlRes .AllRowsAffected () {
1559- totalDelta += buildResults [i ].rowsDelta * rowsAffected
1560- }
1561- return nil
1562- })
1563-
1564- if execErr != nil {
1565- return rollback (execErr )
1566- }
1567-
1568- // Check for warnings when PanicOnWarnings is enabled
1629+ // When PanicOnWarnings is enabled, we need to check warnings after each statement
1630+ // in the batch. SHOW WARNINGS only shows warnings from the last statement in a
1631+ // multi-statement query, so we interleave SHOW WARNINGS after each DML statement.
15691632 if this .migrationContext .PanicOnWarnings {
1570- //nolint:execinquery
1571- rows , err := tx .Query ("SHOW WARNINGS" )
1572- if err != nil {
1573- return rollback (err )
1574- }
1575- defer rows .Close ()
1576- if err = rows .Err (); err != nil {
1577- return rollback (err )
1578- }
1579-
1580- // Compile regex once before loop to avoid performance penalty and handle errors properly
1581- migrationKeyRegex , err := this .compileMigrationKeyWarningRegex ()
1633+ totalDelta , err = this .executeBatchWithWarningChecking (ctx , tx , buildResults )
15821634 if err != nil {
15831635 return rollback (err )
15841636 }
1637+ } else {
1638+ // Fast path: batch together DML queries into multi-statements to minimize network trips.
1639+ // We use the raw driver connection to access the rows affected for each statement.
1640+ execErr := conn .Raw (func (driverConn any ) error {
1641+ ex := driverConn .(driver.ExecerContext )
1642+ nvc := driverConn .(driver.NamedValueChecker )
1643+
1644+ multiArgs := make ([]driver.NamedValue , 0 , nArgs )
1645+ multiQueryBuilder := strings.Builder {}
1646+ for _ , buildResult := range buildResults {
1647+ for _ , arg := range buildResult .args {
1648+ nv := driver.NamedValue {Value : driver .Value (arg )}
1649+ nvc .CheckNamedValue (& nv )
1650+ multiArgs = append (multiArgs , nv )
1651+ }
1652+
1653+ multiQueryBuilder .WriteString (buildResult .query )
1654+ multiQueryBuilder .WriteString (";\n " )
1655+ }
15851656
1586- var sqlWarnings []string
1587- for rows .Next () {
1588- var level , message string
1589- var code int
1590- if err := rows .Scan (& level , & code , & message ); err != nil {
1591- this .migrationContext .Log .Warningf ("Failed to read SHOW WARNINGS row" )
1592- continue
1657+ res , err := ex .ExecContext (ctx , multiQueryBuilder .String (), multiArgs )
1658+ if err != nil {
1659+ err = fmt .Errorf ("%w; query=%s; args=%+v" , err , multiQueryBuilder .String (), multiArgs )
1660+ return err
15931661 }
1594- if strings .Contains (message , "Duplicate entry" ) && migrationKeyRegex .MatchString (message ) {
1595- // Duplicate entry on migration unique key is expected during binlog replay
1596- // (row was already copied during bulk copy phase)
1597- continue
1662+
1663+ mysqlRes := res .(drivermysql.Result )
1664+
1665+ // each DML is either a single insert (delta +1), update (delta +0) or delete (delta -1).
1666+ // multiplying by the rows actually affected (either 0 or 1) will give an accurate row delta for this DML event
1667+ for i , rowsAffected := range mysqlRes .AllRowsAffected () {
1668+ totalDelta += buildResults [i ].rowsDelta * rowsAffected
15981669 }
1599- sqlWarnings = append ( sqlWarnings , fmt . Sprintf ( "%s: %s (%d)" , level , message , code ))
1600- }
1601- if len ( sqlWarnings ) > 0 {
1602- warningMsg := fmt . Sprintf ( "Warnings detected during DML event application: %v" , sqlWarnings )
1603- return rollback (errors . New ( warningMsg ) )
1670+ return nil
1671+ })
1672+
1673+ if execErr != nil {
1674+ return rollback (execErr )
16041675 }
16051676 }
16061677
0 commit comments