Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions flow/connectors/mysql/cdc.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import (
"github.com/PeerDB-io/peerdb/flow/model"
"github.com/PeerDB-io/peerdb/flow/otel_metrics"
"github.com/PeerDB-io/peerdb/flow/pkg/common"
mysql_validation "github.com/PeerDB-io/peerdb/flow/pkg/mysql"
"github.com/PeerDB-io/peerdb/flow/shared"
"github.com/PeerDB-io/peerdb/flow/shared/datatypes"
"github.com/PeerDB-io/peerdb/flow/shared/exceptions"
Expand Down Expand Up @@ -320,15 +319,10 @@ func (c *MySqlConnector) PullRecords(
return err
}

versionToCmp := mysql_validation.MySQLMinVersionForBinlogRowMetadata
if c.config.Flavor == protos.MySqlFlavor_MYSQL_MARIA {
versionToCmp = mysql_validation.MariaDBMinVersionForBinlogRowMetadata
}
cmp, err := c.CompareServerVersion(ctx, versionToCmp)
binlogRowMetadataSupported, err := c.IsBinlogMetadataSupported(ctx)
if err != nil {
return fmt.Errorf("failed to get server version: %w", err)
return fmt.Errorf("failed to determine if binlog row metadata is supported: %w", err)
}
binlogRowMetadataSupported := cmp >= 0

syncer, mystream, gset, pos, err := c.startStreaming(ctx, req.LastOffset.Text)
if err != nil {
Expand Down
13 changes: 13 additions & 0 deletions flow/connectors/mysql/mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/PeerDB-io/peerdb/flow/generated/protos"
"github.com/PeerDB-io/peerdb/flow/internal"
"github.com/PeerDB-io/peerdb/flow/pkg/common"
mysql_validation "github.com/PeerDB-io/peerdb/flow/pkg/mysql"
"github.com/PeerDB-io/peerdb/flow/shared"
)

Expand Down Expand Up @@ -549,3 +550,15 @@ func (c *MySqlConnector) GetTableSizeEstimatedBytes(ctx context.Context, tableId
defer rs.Close()
return rs.GetInt(0, 0)
}

func (c *MySqlConnector) IsBinlogMetadataSupported(ctx context.Context) (bool, error) {
versionToCmp := mysql_validation.MySQLMinVersionForBinlogRowMetadata
if c.config.Flavor == protos.MySqlFlavor_MYSQL_MARIA {
versionToCmp = mysql_validation.MariaDBMinVersionForBinlogRowMetadata
}
cmp, err := c.CompareServerVersion(ctx, versionToCmp)
if err != nil {
return false, fmt.Errorf("failed to get server version: %w", err)
}
return cmp >= 0, nil
}
39 changes: 30 additions & 9 deletions flow/connectors/mysql/qrep.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,32 @@
}, nil
}

func buildSelectedColumns(cols []*protos.FieldDescription, exclude []string, isBinlogMetadataSupported bool) string {
columns := []string{}
selectAsterisk := true

@dtunikov dtunikov Mar 14, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not really necessary (we could always query columns explicitly).
But I decided that this way it will be consistent with the current behaviour (no transformations -> select *)

for _, col := range cols {
if slices.Contains(exclude, col.Name) {
selectAsterisk = false
continue
}

converted := common.QuoteMySQLIdentifier(col.Name)
if !isBinlogMetadataSupported && col.Type == string(types.QValueKindEnum) {
// if binlog metadata is not supported, we need to cast enum columns to integers to align it with cdc stream
converted = fmt.Sprintf("%s + 0", converted)

Check failure on line 192 in flow/connectors/mysql/qrep.go

View workflow job for this annotation

GitHub Actions / lint

string-format: fmt.Sprintf can be replaced with string concatenation (perfsprint)
selectAsterisk = false
}
columns = append(columns, converted)
}

selectedColumns := "*"
if !selectAsterisk {
selectedColumns = strings.Join(columns, ", ")
}

return selectedColumns
Comment thread
dtunikov marked this conversation as resolved.
}

func (c *MySqlConnector) PullQRepRecords(
ctx context.Context,
catalogPool shared.CatalogPool,
Expand All @@ -192,17 +218,12 @@
return 0, 0, fmt.Errorf("failed to get schema for watermark table %s: %w", config.WatermarkTable, err)
}

selectedColumns := "*"
if len(config.Exclude) != 0 {
quotedColumns := make([]string, 0, len(tableSchema.Columns))
for _, col := range tableSchema.Columns {
if !slices.Contains(config.Exclude, col.Name) {
quotedColumns = append(quotedColumns, common.QuoteMySQLIdentifier(col.Name))
}
}
selectedColumns = strings.Join(quotedColumns, ",")
isBinlogMetadataSupported, err := c.IsBinlogMetadataSupported(ctx)
if err != nil {
return 0, 0, fmt.Errorf("failed to determine if binlog metadata is supported: %w", err)
}

selectedColumns := buildSelectedColumns(tableSchema.Columns, config.Exclude, isBinlogMetadataSupported)
parsedSrcTable, err := common.ParseTableIdentifier(config.WatermarkTable)
if err != nil {
c.logger.Error("unable to parse source table", slog.Any("error", err))
Expand Down
70 changes: 70 additions & 0 deletions flow/connectors/mysql/qrep_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package connmysql

import (
"testing"

"github.com/PeerDB-io/peerdb/flow/generated/protos"
"github.com/PeerDB-io/peerdb/flow/shared/types"
)

func TestBuildSelectedColumns(t *testing.T) {
testCases := []struct {

Check failure on line 11 in flow/connectors/mysql/qrep_test.go

View workflow job for this annotation

GitHub Actions / lint

fieldalignment: struct with 80 pointer bytes could be 64 (govet)
name string
cols []*protos.FieldDescription
exclude []string
isBinlogMetadataSupported bool
expectedSelectedColumns string
}{
{
name: "no excluded columns, binlog metadata supported",
cols: []*protos.FieldDescription{
{Name: "id", Type: string(types.QValueKindInt32)},
{Name: "name", Type: string(types.QValueKindString)},
{Name: "status", Type: string(types.QValueKindEnum)},
},
exclude: []string{},
isBinlogMetadataSupported: true,
expectedSelectedColumns: "*",
},
{
name: "one excluded column, binlog metadata supported",
cols: []*protos.FieldDescription{
{Name: "id", Type: string(types.QValueKindInt32)},
{Name: "name", Type: string(types.QValueKindString)},
},
exclude: []string{"name"},
isBinlogMetadataSupported: true,
expectedSelectedColumns: "`id`",
},
{
name: "one enum column, binlog metadata not supported",
cols: []*protos.FieldDescription{
{Name: "id", Type: string(types.QValueKindInt32)},
{Name: "status", Type: string(types.QValueKindEnum)},
},
exclude: []string{},
isBinlogMetadataSupported: false,
expectedSelectedColumns: "`id`, `status` + 0",
},
{
name: "one enum column, one excluded non enum column, binlog metadata supported",
cols: []*protos.FieldDescription{
{Name: "id", Type: string(types.QValueKindInt32)},
{Name: "status", Type: string(types.QValueKindEnum)},
{Name: "created_at", Type: string(types.QValueKindTimestamp)},
},
exclude: []string{"created_at"},
isBinlogMetadataSupported: true,
expectedSelectedColumns: "`id`, `status`",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
selectedColumns := buildSelectedColumns(tc.cols, tc.exclude, tc.isBinlogMetadataSupported)
if selectedColumns != tc.expectedSelectedColumns {
t.Errorf("expected selected columns to be %s, but got %s", tc.expectedSelectedColumns, selectedColumns)
}
})
}
}
53 changes: 53 additions & 0 deletions flow/e2e/clickhouse_mysql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,59 @@ func (s ClickHouseSuite) Test_MySQL_Enum() {
RequireEnvCanceled(s.t, env)
}

func (s ClickHouseSuite) Test_MySQL_Enum_Consistency() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a test for the previous version as well (existing mirrors/pipes). Example:

func (s ClickHouseSuite) Test_PgVector_Version0() {

Test itself would be basically the same except for asserting the "active" and "1" depending on the row

if _, ok := s.source.(*MySqlSource); !ok {
s.t.Skip("only applies to mysql")
}

srcTableName := "test_my_enum_consistency"
srcFullName := s.attachSchemaSuffix(srcTableName)
dstTableName := "test_my_enum_consistency_dst"

require.NoError(s.t, s.source.Exec(s.t.Context(), fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS %s (
id SERIAL PRIMARY KEY,
status ENUM('active', 'inactive', 'pending') NOT NULL
)
`, srcFullName)))

// Insert row before snapshot
require.NoError(s.t, s.source.Exec(s.t.Context(), fmt.Sprintf(
`INSERT INTO %s (status) VALUES ('active')`, srcFullName)))

connectionGen := FlowConnectionGenerationConfig{
FlowJobName: s.attachSuffix(srcTableName),
TableNameMapping: map[string]string{srcFullName: dstTableName},
Destination: s.Peer().Name,
}
flowConnConfig := connectionGen.GenerateFlowConnectionConfigs(s)
flowConnConfig.DoInitialSnapshot = true

tc := NewTemporalClient(s.t)
env := ExecutePeerflow(s.t, tc, flowConnConfig)
SetupCDCFlowStatusQuery(s.t, env, flowConnConfig)

// Wait for snapshot row to appear in destination
EnvWaitForCount(env, s, "waiting on snapshot", dstTableName, "id,status", 1)

// Insert row via CDC — on old MySQL this comes as integer from binlog
require.NoError(s.t, s.source.Exec(s.t.Context(), fmt.Sprintf(
`INSERT INTO %s (status) VALUES ('active')`, srcFullName)))

// Wait for CDC row
EnvWaitForCount(env, s, "waiting on cdc", dstTableName, "id,status", 2)

// Verify both rows have the same status value (consistency between snapshot and CDC)
rows, err := s.GetRows(dstTableName, "status")
require.NoError(s.t, err)
require.Len(s.t, rows.Records, 2)
require.Equal(s.t, rows.Records[0][0].Value(), rows.Records[1][0].Value(),
"snapshot and CDC enum values should be consistent")
Comment thread
dtunikov marked this conversation as resolved.
Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add an assert for the actual value. Can do

if os.Getenv("CI_MYSQL_VERSION") == "mysql-pos" {
	require.Equal(s.t, 1, rows.Records[0][0].Value())
} else {
	require.Equal(s.t, 'active', rows.Records[0][0].Value())
}


env.Cancel(s.t.Context())
RequireEnvCanceled(s.t, env)
}

func (s ClickHouseSuite) Test_MySQL_Vector() {
mysource, ok := s.source.(*MySqlSource)
if !ok || mysource.Config.Flavor != protos.MySqlFlavor_MYSQL_MYSQL {
Expand Down
Loading