Skip to content

Commit e7499e6

Browse files
dtunikovclaude
andcommitted
fix(mysql): fail fast on partial JSON binlog events
When binlog_row_value_options=PARTIAL_JSON is enabled at runtime on the source, MySQL emits PARTIAL_UPDATE_ROWS_EVENT carrying JSON diffs instead of full values. go-mysql parses these as a *RowsEvent, but PeerDB's inner event-type switch had no case for it and no default, so the entire update (all columns) was silently dropped after its bytes were counted as consumed — CDC advanced past it, causing silent data loss. Add a PARTIAL_UPDATE_ROWS_EVENT case that fails fast with a classified error (NOTIFY_BINLOG_PARTIAL_JSON_UNSUPPORTED) telling the user to disable binlog_row_value_options and resync, plus a default branch so any future rows-event subtype errors loudly instead of dropping data. The JsonDiff value conversion now errors as a defensive backstop. Scoped to tables in the pipe via the existing `schema != nil` guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent cd189a0 commit e7499e6

5 files changed

Lines changed: 48 additions & 2 deletions

File tree

flow/alerting/classifier.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ var (
145145
ErrorNotifyBinlogEventExceededMaxAllowedPacket = ErrorClass{
146146
Class: "NOTIFY_BINLOG_EVENT_EXCEEDED_MAX_ALLOWED_PACKET", action: NotifyUser,
147147
}
148+
ErrorNotifyBinlogPartialJsonUnsupported = ErrorClass{
149+
Class: "NOTIFY_BINLOG_PARTIAL_JSON_UNSUPPORTED", action: NotifyUser,
150+
}
148151
ErrorNotifyBinlogRowMetadataInvalid = ErrorClass{
149152
Class: "NOTIFY_BINLOG_ROW_METADATA_INVALID", action: NotifyUser,
150153
}
@@ -1119,6 +1122,13 @@ func GetErrorClass(ctx context.Context, err error) (ErrorClass, ErrorInfo) {
11191122
}
11201123
}
11211124

1125+
if _, ok := errors.AsType[*exceptions.MySQLUnsupportedBinlogRowValueOptionsError](err); ok {
1126+
return ErrorNotifyBinlogPartialJsonUnsupported, ErrorInfo{
1127+
Source: ErrorSourceMySQL,
1128+
Code: "UNSUPPORTED_BINLOG_ROW_VALUE_OPTIONS",
1129+
}
1130+
}
1131+
11221132
if unsupportedDDLError, ok := errors.AsType[*exceptions.MySQLUnsupportedDDLError](err); ok {
11231133
return ErrorNotifyBinlogRowMetadataInvalid, ErrorInfo{
11241134
Source: ErrorSourceMySQL,

flow/alerting/classifier_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,3 +1243,15 @@ func TestMySQLBinlogIncidentErrorShouldBeNotifyBinlogInvalid(t *testing.T) {
12431243
Code: "BINLOG_INCIDENT",
12441244
}, errInfo)
12451245
}
1246+
1247+
func TestMySQLUnsupportedBinlogRowValueOptionsErrorShouldBeNotifyBinlogPartialJsonUnsupported(t *testing.T) {
1248+
t.Parallel()
1249+
1250+
err := exceptions.NewMySQLUnsupportedBinlogRowValueOptionsError("mydb", "mytable")
1251+
errorClass, errInfo := GetErrorClass(t.Context(), fmt.Errorf("pulling records failed: %w", err))
1252+
assert.Equal(t, ErrorNotifyBinlogPartialJsonUnsupported, errorClass)
1253+
assert.Equal(t, ErrorInfo{
1254+
Source: ErrorSourceMySQL,
1255+
Code: "UNSUPPORTED_BINLOG_ROW_VALUE_OPTIONS",
1256+
}, errInfo)
1257+
}

flow/connectors/mysql/cdc.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,13 @@ func (c *MySqlConnector) PullRecords(
813813
}
814814
case replication.WRITE_ROWS_EVENTv0, replication.UPDATE_ROWS_EVENTv0, replication.DELETE_ROWS_EVENTv0:
815815
return fmt.Errorf("mysql v0 replication protocol not supported")
816+
case replication.PARTIAL_UPDATE_ROWS_EVENT:
817+
// Emitted only when binlog_row_value_options=PARTIAL_JSON is enabled at runtime.
818+
e := exceptions.NewMySQLUnsupportedBinlogRowValueOptionsError(string(ev.Table.Schema), string(ev.Table.Table))
819+
c.logger.Error(e.Error())
820+
return e
821+
default:
822+
return fmt.Errorf("unhandled mysql rows event type: %s", event.Header.EventType)
816823
}
817824
}
818825
if event.Header.Timestamp > 0 {

flow/connectors/mysql/qvalue_convert.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package connmysql
22

33
import (
44
"encoding/binary"
5+
"errors"
56
"fmt"
67
"log/slog"
78
"math"
@@ -547,8 +548,9 @@ func QValueFromMysqlRowEvent(
547548
case time.Time:
548549
return types.QValueTimestamp{Val: val}, nil
549550
case *replication.JsonDiff:
550-
// TODO support somehow??
551-
return types.QValueNull(types.QValueKindJSON), nil
551+
// Partial JSON updates (binlog_row_value_options=PARTIAL_JSON) cannot be applied; the caller
552+
// fails fast on PARTIAL_UPDATE_ROWS_EVENT, so this is a defensive backstop against silent data loss.
553+
return nil, errors.New("partial JSON update value is not supported; binlog_row_value_options must be disabled")
552554
case []byte:
553555
switch qkind {
554556
case types.QValueKindBytes:

flow/shared/exceptions/mysql.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,21 @@ func (e *MySQLUnsupportedBinlogRowMetadataError) Error() string {
5555
e.SchemaName, e.TableName)
5656
}
5757

58+
type MySQLUnsupportedBinlogRowValueOptionsError struct {
59+
SchemaName string
60+
TableName string
61+
}
62+
63+
func NewMySQLUnsupportedBinlogRowValueOptionsError(schema string, table string) *MySQLUnsupportedBinlogRowValueOptionsError {
64+
return &MySQLUnsupportedBinlogRowValueOptionsError{SchemaName: schema, TableName: table}
65+
}
66+
67+
func (e *MySQLUnsupportedBinlogRowValueOptionsError) Error() string {
68+
return fmt.Sprintf(
69+
"Received a partial JSON update event while processing %s.%s; binlog_row_value_options must be disabled (set to '')",
70+
e.SchemaName, e.TableName)
71+
}
72+
5873
type MySQLUnsupportedDDLError struct {
5974
TableName string
6075
}

0 commit comments

Comments
 (0)