-
Notifications
You must be signed in to change notification settings - Fork 104
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft: add mysql validations #2594
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,14 +1,172 @@ | ||||||||||||||||||
package connmysql | ||||||||||||||||||
|
||||||||||||||||||
|
||||||||||||||||||
import ( | ||||||||||||||||||
"context" | ||||||||||||||||||
"database/sql" | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. have been avoiding |
||||||||||||||||||
"errors" | ||||||||||||||||||
"fmt" | ||||||||||||||||||
"strings" | ||||||||||||||||||
|
||||||||||||||||||
"github.com/go-mysql-org/go-mysql/mysql" | ||||||||||||||||||
_ "github.com/go-sql-driver/mysql" | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we're using go-mysql-org/go-mysql, no need to mix in go-sql-driver/mysql |
||||||||||||||||||
|
||||||||||||||||||
"github.com/PeerDB-io/peerdb/flow/connectors/utils" | ||||||||||||||||||
"github.com/PeerDB-io/peerdb/flow/generated/protos" | ||||||||||||||||||
"github.com/PeerDB-io/peerdb/flow/shared" | ||||||||||||||||||
) | ||||||||||||||||||
|
||||||||||||||||||
type MySQLConnector struct { | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. MySqlConnector already exists in mysql.go |
||||||||||||||||||
conn *sql.DB | ||||||||||||||||||
config *MySQLConfig | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
func (c *MySQLConnector) CheckSourceTables(ctx context.Context, tableNames []*utils.SchemaTable) error { | ||||||||||||||||||
if c.conn == nil { | ||||||||||||||||||
return errors.New("check tables: conn is nil") | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. MySql connector doesn't maintain a single long lived connection, let Execute manage this |
||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
for _, parsedTable := range tableNames { | ||||||||||||||||||
query := fmt.Sprintf("SELECT 1 FROM `%s`.`%s` LIMIT 1", parsedTable.Schema, parsedTable.Table) | ||||||||||||||||||
_, err := c.conn.QueryContext(ctx, query) | ||||||||||||||||||
if err != nil { | ||||||||||||||||||
return fmt.Errorf("error checking table %s.%s: %v", parsedTable.Schema, parsedTable.Table, err) | ||||||||||||||||||
} | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
} | ||||||||||||||||||
return nil | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
func (c *MySQLConnector) CheckReplicationPermissions(ctx context.Context) error { | ||||||||||||||||||
if c.conn == nil { | ||||||||||||||||||
return errors.New("check replication permissions: conn is nil") | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
var replicationPrivilege string | ||||||||||||||||||
err := c.conn.QueryRowContext(ctx, "SHOW GRANTS FOR CURRENT_USER()").Scan(&replicationPrivilege) | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it's more robust to parse this into a string array and then check to avoid matching partial permission names later |
||||||||||||||||||
if err != nil { | ||||||||||||||||||
return fmt.Errorf("failed to check replication privileges: %v", err) | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
if !strings.Contains(replicationPrivilege, "REPLICATION SLAVE") && !strings.Contains(replicationPrivilege, "REPLICATION CLIENT") { | ||||||||||||||||||
return errors.New("MySQL user does not have replication privileges") | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
return nil | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
func (c *MySQLConnector) CheckReplicationConnectivity(ctx context.Context) error { | ||||||||||||||||||
if c.conn == nil { | ||||||||||||||||||
return errors.New("check replication connectivity: conn is nil") | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
var masterLogFile string | ||||||||||||||||||
var masterLogPos int | ||||||||||||||||||
|
||||||||||||||||||
err := c.conn.QueryRowContext(ctx, "SHOW MASTER STATUS").Scan(&masterLogFile, &masterLogPos) | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this has flavor/versioning considerations, see There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. to check binlog is enabled should only need to |
||||||||||||||||||
if err != nil { | ||||||||||||||||||
// Handle case where SHOW MASTER STATUS returns no rows (binary logging disabled) | ||||||||||||||||||
if errors.Is(err, sql.ErrNoRows) { | ||||||||||||||||||
return errors.New("binary logging is disabled on this MySQL server") | ||||||||||||||||||
} | ||||||||||||||||||
return fmt.Errorf("failed to check replication status: %v", err) | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
// Additional validation: Check if the values are valid | ||||||||||||||||||
if masterLogFile == "" || masterLogPos <= 0 { | ||||||||||||||||||
return errors.New("invalid replication status: missing log file or position") | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
return nil | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
func (c *MySQLConnector) CheckBinlogSettings(ctx context.Context) error { | ||||||||||||||||||
if c.conn == nil { | ||||||||||||||||||
return errors.New("check binlog settings: conn is nil") | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
// Check binlog_expire_logs_seconds | ||||||||||||||||||
var expireSeconds int | ||||||||||||||||||
err := c.conn.QueryRowContext(ctx, "SELECT @@binlog_expire_logs_seconds").Scan(&expireSeconds) | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Settings are flavor specific. Fine to return nil when flavor is not MySQL for now |
||||||||||||||||||
if err != nil { | ||||||||||||||||||
return fmt.Errorf("failed to retrieve binlog_expire_logs_seconds: %v", err) | ||||||||||||||||||
} | ||||||||||||||||||
if expireSeconds <= 86400 { | ||||||||||||||||||
return errors.New("binlog_expire_logs_seconds is too low. Must be greater than 1 day") | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
// Check binlog_format | ||||||||||||||||||
var binlogFormat string | ||||||||||||||||||
err = c.conn.QueryRowContext(ctx, "SELECT @@binlog_format").Scan(&binlogFormat) | ||||||||||||||||||
if err != nil { | ||||||||||||||||||
return fmt.Errorf("failed to retrieve binlog_format: %v", err) | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
been trying to have consistent error scoping, this decision has avoided a couple issues %w over %v in |
||||||||||||||||||
} | ||||||||||||||||||
if binlogFormat != "ROW" { | ||||||||||||||||||
return errors.New("binlog_format must be set to 'ROW'") | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
// Check binlog_row_metadata | ||||||||||||||||||
var binlogRowMetadata string | ||||||||||||||||||
err = c.conn.QueryRowContext(ctx, "SELECT @@binlog_row_metadata").Scan(&binlogRowMetadata) | ||||||||||||||||||
if err != nil { | ||||||||||||||||||
return fmt.Errorf("failed to retrieve binlog_row_metadata: %v", err) | ||||||||||||||||||
} | ||||||||||||||||||
if binlogRowMetadata != "FULL" { | ||||||||||||||||||
return errors.New("binlog_row_metadata must be set to 'FULL' for column exclusion support") | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should only be warning, not error. might be best to defer until mirror validation |
||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
// Check binlog_row_image | ||||||||||||||||||
var binlogRowImage string | ||||||||||||||||||
err = c.conn.QueryRowContext(ctx, "SELECT @@binlog_row_image").Scan(&binlogRowImage) | ||||||||||||||||||
if err != nil { | ||||||||||||||||||
return fmt.Errorf("failed to retrieve binlog_row_image: %v", err) | ||||||||||||||||||
} | ||||||||||||||||||
if binlogRowImage != "FULL" { | ||||||||||||||||||
return errors.New("binlog_row_image must be set to 'FULL' (equivalent to PostgreSQL's REPLICA IDENTITY FULL)") | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would prefer to avoid |
||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
// Check binlog_row_value_options | ||||||||||||||||||
var binlogRowValueOptions string | ||||||||||||||||||
err = c.conn.QueryRowContext(ctx, "SELECT @@binlog_row_value_options").Scan(&binlogRowValueOptions) | ||||||||||||||||||
if err != nil { | ||||||||||||||||||
return fmt.Errorf("failed to retrieve binlog_row_value_options: %v", err) | ||||||||||||||||||
} | ||||||||||||||||||
if binlogRowValueOptions != "" { | ||||||||||||||||||
return errors.New("binlog_row_value_options must be disabled to prevent JSON change deltas") | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
return nil | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
func (c *MySQLConnector) ValidateMirrorSource(ctx context.Context, cfg *protos.FlowConnectionConfigs) error { | ||||||||||||||||||
sourceTables := make([]*utils.SchemaTable, 0, len(cfg.TableMappings)) | ||||||||||||||||||
for _, tableMapping := range cfg.TableMappings { | ||||||||||||||||||
parsedTable, parseErr := utils.ParseSchemaTable(tableMapping.SourceTableIdentifier) | ||||||||||||||||||
if parseErr != nil { | ||||||||||||||||||
return fmt.Errorf("invalid source table identifier: %s", parseErr) | ||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
} | ||||||||||||||||||
sourceTables = append(sourceTables, parsedTable) | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
if err := c.CheckReplicationConnectivity(ctx); err != nil { | ||||||||||||||||||
return fmt.Errorf("unable to establish replication connectivity: %v", err) | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
if err := c.CheckReplicationPermissions(ctx); err != nil { | ||||||||||||||||||
return fmt.Errorf("failed to check replication permissions: %v", err) | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
if err := c.CheckSourceTables(ctx, sourceTables); err != nil { | ||||||||||||||||||
return fmt.Errorf("provided source tables invalidated: %v", err) | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
if err := c.CheckBinlogSettings(ctx); err != nil { | ||||||||||||||||||
return fmt.Errorf("binlog configuration error: %v", err) | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
return nil | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
|
||||||||||||||||||
func (c *MySqlConnector) ValidateCheck(ctx context.Context) error { | ||||||||||||||||||
if _, err := c.Execute(ctx, "select @@gtid_mode"); err != nil { | ||||||||||||||||||
var mErr *mysql.MyError | ||||||||||||||||||
|
@@ -24,6 +182,19 @@ func (c *MySqlConnector) ValidateCheck(ctx context.Context) error { | |||||||||||||||||
} else if c.config.Flavor == protos.MySqlFlavor_MYSQL_UNKNOWN { | ||||||||||||||||||
return errors.New("flavor is set to unknown") | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
if err := c.CheckReplicationConnectivity(ctx); err != nil { | ||||||||||||||||||
return fmt.Errorf("unable to establish replication connectivity: %v", err) | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
if err := c.CheckReplicationPermissions(ctx); err != nil { | ||||||||||||||||||
return fmt.Errorf("failed to check replication permissions: %v", err) | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
if err := c.CheckBinlogSettings(ctx); err != nil { | ||||||||||||||||||
return fmt.Errorf("binlog configuration error: %v", err) | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
return nil | ||||||||||||||||||
} | ||||||||||||||||||
|
||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.