@@ -1522,6 +1522,107 @@ func (this *Applier) buildDMLEventQuery(dmlEvent *binlog.BinlogDMLEvent) []*dmlB
15221522 return []* dmlBuildResult {newDmlBuildResultError (fmt .Errorf ("Unknown dml event type: %+v" , dmlEvent .DML ))}
15231523}
15241524
1525+ // executeBatchWithWarningChecking executes a batch of DML statements with SHOW WARNINGS
1526+ // interleaved after each statement to detect warnings from any statement in the batch.
1527+ // This is used when PanicOnWarnings is enabled to ensure warnings from middle statements
1528+ // are not lost (SHOW WARNINGS only shows warnings from the last statement in a multi-statement batch).
1529+ func (this * Applier ) executeBatchWithWarningChecking (ctx context.Context , tx * gosql.Tx , buildResults []* dmlBuildResult ) (int64 , error ) {
1530+ // Build query with interleaved SHOW WARNINGS: stmt1; SHOW WARNINGS; stmt2; SHOW WARNINGS; ...
1531+ var queryBuilder strings.Builder
1532+ args := make ([]interface {}, 0 )
1533+
1534+ for _ , buildResult := range buildResults {
1535+ queryBuilder .WriteString (buildResult .query )
1536+ queryBuilder .WriteString (";\n SHOW WARNINGS;\n " )
1537+ args = append (args , buildResult .args ... )
1538+ }
1539+
1540+ query := queryBuilder .String ()
1541+
1542+ // Execute the multi-statement query
1543+ rows , err := tx .QueryContext (ctx , query , args ... )
1544+ if err != nil {
1545+ return 0 , fmt .Errorf ("%w; query=%s; args=%+v" , err , query , args )
1546+ }
1547+ defer rows .Close ()
1548+
1549+ var totalDelta int64
1550+
1551+ // QueryContext with multi-statement queries returns rows positioned at the first result set
1552+ // that produces rows (i.e., the first SHOW WARNINGS), automatically skipping DML results.
1553+ // Verify we're at a SHOW WARNINGS result set (should have 3 columns: Level, Code, Message)
1554+ cols , err := rows .Columns ()
1555+ if err != nil {
1556+ return 0 , fmt .Errorf ("failed to get columns: %w" , err )
1557+ }
1558+
1559+ // If somehow we're not at a result set with columns, try to advance
1560+ if len (cols ) == 0 {
1561+ if ! rows .NextResultSet () {
1562+ return 0 , fmt .Errorf ("expected SHOW WARNINGS result set after first statement" )
1563+ }
1564+ }
1565+
1566+ // Compile regex once before loop to avoid performance penalty and handle errors properly
1567+ migrationKeyRegex , err := this .compileMigrationKeyWarningRegex ()
1568+ if err != nil {
1569+ return 0 , err
1570+ }
1571+
1572+ // Iterate through SHOW WARNINGS result sets.
1573+ // DML statements don't create navigable result sets, so we move directly between SHOW WARNINGS.
1574+ // Pattern: [at SHOW WARNINGS #1] -> read warnings -> NextResultSet() -> [at SHOW WARNINGS #2] -> ...
1575+ for i := 0 ; i < len (buildResults ); i ++ {
1576+ // We can't get exact rows affected with QueryContext (needed for reading SHOW WARNINGS).
1577+ // Use the theoretical delta (+1 for INSERT, -1 for DELETE, 0 for UPDATE) as an approximation.
1578+ // This may be inaccurate (e.g., INSERT IGNORE with duplicate affects 0 rows but we count +1).
1579+ totalDelta += buildResults [i ].rowsDelta
1580+
1581+ // Read warnings from this statement's SHOW WARNINGS result set
1582+ var sqlWarnings []string
1583+ for rows .Next () {
1584+ var level , message string
1585+ var code int
1586+ if err := rows .Scan (& level , & code , & message ); err != nil {
1587+ // Scan failure means we cannot reliably read warnings.
1588+ // Since PanicOnWarnings is a safety feature, we must fail hard rather than silently skip.
1589+ return 0 , fmt .Errorf ("failed to scan SHOW WARNINGS for statement %d: %w" , i + 1 , err )
1590+ }
1591+
1592+ if strings .Contains (message , "Duplicate entry" ) && migrationKeyRegex .MatchString (message ) {
1593+ // Duplicate entry on migration unique key is expected during binlog replay
1594+ // (row was already copied during bulk copy phase)
1595+ continue
1596+ }
1597+ sqlWarnings = append (sqlWarnings , fmt .Sprintf ("%s: %s (%d)" , level , message , code ))
1598+ }
1599+
1600+ // Check for errors that occurred while iterating through warnings
1601+ if err := rows .Err (); err != nil {
1602+ return 0 , fmt .Errorf ("error reading SHOW WARNINGS result set for statement %d: %w" , i + 1 , err )
1603+ }
1604+
1605+ if len (sqlWarnings ) > 0 {
1606+ return 0 , fmt .Errorf ("warnings detected in statement %d of %d: %v" , i + 1 , len (buildResults ), sqlWarnings )
1607+ }
1608+
1609+ // Move to the next statement's SHOW WARNINGS result set
1610+ // For the last statement, there's no next result set
1611+ // DML statements don't create result sets, so we only need one NextResultSet call
1612+ // to move from SHOW WARNINGS #N to SHOW WARNINGS #(N+1)
1613+ if i < len (buildResults )- 1 {
1614+ if ! rows .NextResultSet () {
1615+ if err := rows .Err (); err != nil {
1616+ return 0 , fmt .Errorf ("error moving to SHOW WARNINGS for statement %d: %w" , i + 2 , err )
1617+ }
1618+ return 0 , fmt .Errorf ("expected SHOW WARNINGS result set for statement %d" , i + 2 )
1619+ }
1620+ }
1621+ }
1622+
1623+ return totalDelta , nil
1624+ }
1625+
15251626// ApplyDMLEventQueries applies multiple DML queries onto the _ghost_ table
15261627func (this * Applier ) ApplyDMLEventQueries (dmlEvents [](* binlog.BinlogDMLEvent )) error {
15271628 var totalDelta int64
@@ -1561,82 +1662,52 @@ func (this *Applier) ApplyDMLEventQueries(dmlEvents [](*binlog.BinlogDMLEvent))
15611662 }
15621663 }
15631664
1564- // We batch together the DML queries into multi-statements to minimize network trips.
1565- // We have to use the raw driver connection to access the rows affected
1566- // for each statement in the multi-statement.
1567- execErr := conn .Raw (func (driverConn any ) error {
1568- ex := driverConn .(driver.ExecerContext )
1569- nvc := driverConn .(driver.NamedValueChecker )
1570-
1571- multiArgs := make ([]driver.NamedValue , 0 , nArgs )
1572- multiQueryBuilder := strings.Builder {}
1573- for _ , buildResult := range buildResults {
1574- for _ , arg := range buildResult .args {
1575- nv := driver.NamedValue {Value : driver .Value (arg )}
1576- nvc .CheckNamedValue (& nv )
1577- multiArgs = append (multiArgs , nv )
1578- }
1579-
1580- multiQueryBuilder .WriteString (buildResult .query )
1581- multiQueryBuilder .WriteString (";\n " )
1582- }
1583-
1584- res , err := ex .ExecContext (ctx , multiQueryBuilder .String (), multiArgs )
1585- if err != nil {
1586- err = fmt .Errorf ("%w; query=%s; args=%+v" , err , multiQueryBuilder .String (), multiArgs )
1587- return err
1588- }
1589-
1590- mysqlRes := res .(drivermysql.Result )
1591-
1592- // each DML is either a single insert (delta +1), update (delta +0) or delete (delta -1).
1593- // multiplying by the rows actually affected (either 0 or 1) will give an accurate row delta for this DML event
1594- for i , rowsAffected := range mysqlRes .AllRowsAffected () {
1595- totalDelta += buildResults [i ].rowsDelta * rowsAffected
1596- }
1597- return nil
1598- })
1599-
1600- if execErr != nil {
1601- return rollback (execErr )
1602- }
1603-
1604- // Check for warnings when PanicOnWarnings is enabled
1665+ // When PanicOnWarnings is enabled, we need to check warnings after each statement
1666+ // in the batch. SHOW WARNINGS only shows warnings from the last statement in a
1667+ // multi-statement query, so we interleave SHOW WARNINGS after each DML statement.
16051668 if this .migrationContext .PanicOnWarnings {
1606- //nolint:execinquery
1607- rows , err := tx .Query ("SHOW WARNINGS" )
1608- if err != nil {
1609- return rollback (err )
1610- }
1611- defer rows .Close ()
1612- if err = rows .Err (); err != nil {
1613- return rollback (err )
1614- }
1615-
1616- // Compile regex once before loop to avoid performance penalty and handle errors properly
1617- migrationKeyRegex , err := this .compileMigrationKeyWarningRegex ()
1669+ totalDelta , err = this .executeBatchWithWarningChecking (ctx , tx , buildResults )
16181670 if err != nil {
16191671 return rollback (err )
16201672 }
1673+ } else {
1674+ // Fast path: batch together DML queries into multi-statements to minimize network trips.
1675+ // We use the raw driver connection to access the rows affected for each statement.
1676+ execErr := conn .Raw (func (driverConn any ) error {
1677+ ex := driverConn .(driver.ExecerContext )
1678+ nvc := driverConn .(driver.NamedValueChecker )
1679+
1680+ multiArgs := make ([]driver.NamedValue , 0 , nArgs )
1681+ multiQueryBuilder := strings.Builder {}
1682+ for _ , buildResult := range buildResults {
1683+ for _ , arg := range buildResult .args {
1684+ nv := driver.NamedValue {Value : driver .Value (arg )}
1685+ nvc .CheckNamedValue (& nv )
1686+ multiArgs = append (multiArgs , nv )
1687+ }
1688+
1689+ multiQueryBuilder .WriteString (buildResult .query )
1690+ multiQueryBuilder .WriteString (";\n " )
1691+ }
16211692
1622- var sqlWarnings []string
1623- for rows .Next () {
1624- var level , message string
1625- var code int
1626- if err := rows .Scan (& level , & code , & message ); err != nil {
1627- this .migrationContext .Log .Warningf ("Failed to read SHOW WARNINGS row" )
1628- continue
1693+ res , err := ex .ExecContext (ctx , multiQueryBuilder .String (), multiArgs )
1694+ if err != nil {
1695+ err = fmt .Errorf ("%w; query=%s; args=%+v" , err , multiQueryBuilder .String (), multiArgs )
1696+ return err
16291697 }
1630- if strings .Contains (message , "Duplicate entry" ) && migrationKeyRegex .MatchString (message ) {
1631- // Duplicate entry on migration unique key is expected during binlog replay
1632- // (row was already copied during bulk copy phase)
1633- continue
1698+
1699+ mysqlRes := res .(drivermysql.Result )
1700+
1701+ // each DML is either a single insert (delta +1), update (delta +0) or delete (delta -1).
1702+ // multiplying by the rows actually affected (either 0 or 1) will give an accurate row delta for this DML event
1703+ for i , rowsAffected := range mysqlRes .AllRowsAffected () {
1704+ totalDelta += buildResults [i ].rowsDelta * rowsAffected
16341705 }
1635- sqlWarnings = append ( sqlWarnings , fmt . Sprintf ( "%s: %s (%d)" , level , message , code ))
1636- }
1637- if len ( sqlWarnings ) > 0 {
1638- warningMsg := fmt . Sprintf ( "Warnings detected during DML event application: %v" , sqlWarnings )
1639- return rollback (errors . New ( warningMsg ) )
1706+ return nil
1707+ })
1708+
1709+ if execErr != nil {
1710+ return rollback (execErr )
16401711 }
16411712 }
16421713
0 commit comments