Skip to content

feat: Migrate to bitbucket#1157

Open
sigalikanevsky wants to merge 145 commits into
go-mysql-org:masterfrom
RiveryIO:migrate_to_bitbucket
Open

feat: Migrate to bitbucket#1157
sigalikanevsky wants to merge 145 commits into
go-mysql-org:masterfrom
RiveryIO:migrate_to_bitbucket

Conversation

@sigalikanevsky

Copy link
Copy Markdown

No description provided.

eitamring and others added 30 commits January 6, 2022 15:02
…ests

Removed FlushBinlog which was only used for testing
…deleted_while_reading

Fix/eitam/add skip when table is deleted while reading
…_decode

Added support to convert byte to string dynamically
…_to_date

Change mysql_date default zero based value to nil
…_to_date

Change mysql_MYSQL_TYPE_TIMESTAMP2 and MYSQL_TYPE_TIMESTAMP to be def…
…_to_date

Change decodeDatetime2 default to zero
Change decodeTimestamp2 to return nil in case sec = 0
…p_log

Changed SP is missing to debug level
eitam and others added 15 commits September 1, 2025 10:45
- TestHeartbeatIntervalConversion: Config value conversion
- TestShouldSendHeartbeat: Timer logic with 4 scenarios
- TestSendAsHeartbeat: Event creation and type safety
- TestHeartbeatEventGTIDSet: GTID tracking on heartbeat events
- TestHeartbeatTimerReset: Timer reset behavior

Coverage:
- shouldSendHeartbeat(): 67%
- sendAsHeartbeat(): 71%
- All tests pass in < 0.5s, no MySQL required
…nConnectionSeconds

fix WaitTimeBetweenConnectionSeconds if condition

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces support for column-specific character set decoding, a heartbeat mechanism to maintain connection liveness, and configurable connection retry intervals. However, several critical issues must be addressed: the table charset retrieval logic breaks when regular expressions are used in table filters, and calling it before filter initialization causes highly inefficient database queries. Additionally, a potential out-of-bounds panic exists when slicing hostname runes using byte lengths. Finally, several data corruption risks were identified, including the unintended sanitization of formatting characters (newlines/tabs) in text columns, the forced string conversion of binary BLOB data, and the mutation of user data via smart quote normalization. A test query mismatch in TestGenerateCharsetQuery also needs to be corrected.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread canal/canal.go
Comment on lines +516 to +543
for _, tableRegex := range c.cfg.IncludeTableRegex {
parts := strings.Split(tableRegex, ".")
if len(parts) != 2 {
return fmt.Errorf("invalid tableRegex format, expected db.table")
}
dbName, tableName := parts[0], parts[1]

if !isSafeIdentifier(dbName) || !isSafeIdentifier(tableName) {
return fmt.Errorf("invalid characters in db or table name: %s.%s", dbName, tableName)
}

query, err := c.GenerateCharsetQuery()
if err != nil {
return fmt.Errorf("failed to generate charset query: %w", err)
}
rows, err := db.QueryContext(c.ctx, query, dbName, tableName)
if err != nil {
return fmt.Errorf("error occurred while executing query: %s on db: %s on table: %s. error: %v",
query, dbName, tableName, errors.Trace(err))
}

// Process rows with proper error handling
defer rows.Close()
if err := c.setColumnsCharsetFromRows(tableRegex, rows); err != nil {
return fmt.Errorf("failed to set charset from rows for table %s: %w", tableRegex, err)
}

}

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.

critical

GetColumnsCharsets attempts to split tableRegex by . and expects exactly 2 parts, both of which must be safe identifiers. However, IncludeTableRegex contains regular expressions (e.g., .*\\.canal_test), not literal table names. Splitting a regex by . will produce incorrect parts, and regex characters like * or \\ will fail the isSafeIdentifier check. This completely breaks NewCanal initialization whenever any standard regex is used in IncludeTableRegex.

Additionally, storing the column charsets in c.cfg.ColumnCharset using the tableRegex string as the key (e.g., ".*\\.canal_test") will cause lookups in replication/row_event.go to fail, because the lookup is performed using the actual table name (e.g., "test.canal_test").

To fix both issues, query all tables from information_schema.TABLES first, filter them using the already compiled c.checkTableMatch(key) helper, and then query the column charsets only for the matching tables using the actual db.table name as the map key.

	// 1. Query all tables from information_schema
	rows, err := db.QueryContext(c.ctx, "SELECT TABLE_SCHEMA, TABLE_NAME FROM information_schema.TABLES")
	if err != nil {
		return fmt.Errorf("failed to query tables: %w", err)
	}
	defer rows.Close()

	var matchingTables []string
	for rows.Next() {
		var dbName, tableName string
		if err := rows.Scan(&dbName, &tableName); err != nil {
			return fmt.Errorf("failed to scan table: %w", err)
		}
		key := fmt.Sprintf("%s.%s", dbName, tableName)
		if c.checkTableMatch(key) {
			matchingTables = append(matchingTables, key)
		}
	}
	if err := rows.Err(); err != nil {
		return fmt.Errorf("error reading tables: %w", err)
	}
	rows.Close()

	// 2. Query column charsets for each matching table
	query, err := c.GenerateCharsetQuery()
	if err != nil {
		return fmt.Errorf("failed to generate charset query: %w", err)
	}

	for _, tableKey := range matchingTables {
		parts := strings.Split(tableKey, ".")
		dbName, tableName := parts[0], parts[1]

		c.cfg.ColumnCharset[tableKey] = make(map[int]string)
		colRows, err := db.QueryContext(c.ctx, query, dbName, tableName)
		if err != nil {
			return fmt.Errorf("failed to query column charsets for %s: %w", tableKey, err)
		}
		
		for colRows.Next() {
			var ordinal int
			var charset, columnName sql.NullString
			if err := colRows.Scan(&ordinal, &charset, &columnName); err != nil {
				colRows.Close()
				return fmt.Errorf("failed to scan column charset for %s: %w", tableKey, err)
			}
			charsetValue := "utf8mb4"
			if charset.Valid && charset.String != "" {
				charsetValue = charset.String
			}
			c.cfg.ColumnCharset[tableKey][ordinal] = charsetValue
		}
		colRows.Close()
	}

Comment on lines +533 to +536
if len(hostname) > maxHostNameLength {
runes := []rune(hostname)
hostname = string(runes[0:maxHostNameLength])
}

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.

critical

The code checks len(hostname) > maxHostNameLength (which checks the number of bytes), but then slices the runes slice up to maxHostNameLength. If hostname contains multi-byte UTF-8 characters, the number of runes will be less than the number of bytes. If len(hostname) > 60 but len(runes) < 60, slicing runes[0:60] will cause a runtime out-of-bounds panic. Convert to runes first, and then check the length of the runes slice before slicing.

Suggested change
if len(hostname) > maxHostNameLength {
runes := []rune(hostname)
hostname = string(runes[0:maxHostNameLength])
}
runes := []rune(hostname)
if len(runes) > maxHostNameLength {
hostname = string(runes[0:maxHostNameLength])
}

Comment thread replication/row_event.go
Comment on lines +1205 to +1219
func decodeStringByCharSet(data []byte, charset string, length int) (v string, n int) {
enc, err := getDecoderByCharsetName(charset)
if err != nil {
log.Errorf(err.Error())
v, n = decodeString(data, length)
return sanitizeNonPrintable(v), n
}
if enc == nil {
log.Warnf("Falling back to default decoding for charset: %s", charset)
v, n = decodeString(data, length)
return sanitizeNonPrintable(v), n
}
v, n = decodeStringWithEncoder(data, length, enc)
return sanitizeNonPrintable(v), n
}

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.

critical

sanitizeNonPrintable replaces all characters less than 0x20 with spaces. This includes standard formatting characters like newlines (\n / 0x0A), carriage returns (\r / 0x0D), and tabs (\t / 0x09). Applying this to all decoded strings will silently corrupt the data of all VARCHAR, CHAR, TEXT, and JSON columns by replacing all newlines and tabs with spaces. A replication tool must preserve the exact content of the database. Please remove sanitizeNonPrintable entirely.

func decodeStringByCharSet(data []byte, charset string, length int) (v string, n int) {
	enc, err := getDecoderByCharsetName(charset)
	if err != nil {
		log.Errorf(err.Error())
		return decodeString(data, length)
	}
	if enc == nil {
		log.Warnf("Falling back to default decoding for charset: %s", charset)
		return decodeString(data, length)
	}
	return decodeStringWithEncoder(data, length, enc)
}

Comment thread canal/canal.go
Comment on lines +95 to +97
if err := c.GetColumnsCharsets(); err != nil {
return nil, errors.Trace(err)
}

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.

high

In NewCanal, c.GetColumnsCharsets() is called before the table filters (c.includeTableRegex, c.excludeTableRegex, and c.tableMatchCache) are initialized. Because c.tableMatchCache is nil at this point, c.checkTableMatch() will always return true for every table in the database. This causes GetColumnsCharsets to fetch column charsets for all tables in the database, which is extremely inefficient. Please move the table filter initialization block above the GetColumnsCharsets() call.

Comment thread replication/row_event.go
Comment on lines 1155 to +1160
case MYSQL_TYPE_BLOB:
v, n, err = decodeBlob(data, meta)
newValue, ok := convertToString(v)
if ok {
v = newValue
}

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.

high

For MYSQL_TYPE_BLOB, the code automatically calls convertToString(v) which converts the []byte to a string and sanitizes it. However, BLOB columns are designed to store arbitrary binary data (e.g., images, encrypted data). Converting binary data to a UTF-8 string and replacing non-printable bytes with spaces will completely corrupt any binary data stored in BLOB columns. Only convert MYSQL_TYPE_BLOB to string if the column is actually a text column (i.e., its charset is not "binary").

Suggested change
case MYSQL_TYPE_BLOB:
v, n, err = decodeBlob(data, meta)
newValue, ok := convertToString(v)
if ok {
v = newValue
}
case MYSQL_TYPE_BLOB:
v, n, err = decodeBlob(data, meta)
if charset != "binary" {
newValue, ok := convertToString(v)
if ok {
v = newValue
}
}

Comment thread replication/row_event.go
Comment on lines +1337 to +1344
// Replaces smart quotes with ASCII equivalents
func normalizeSmartQuotes(content []byte) []byte {
content = bytes.ReplaceAll(content, []byte("‘"), []byte("'"))
content = bytes.ReplaceAll(content, []byte("’"), []byte("'"))
content = bytes.ReplaceAll(content, []byte("“"), []byte("\""))
content = bytes.ReplaceAll(content, []byte("”"), []byte("\""))
return content
}

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.

medium

normalizeSmartQuotes replaces smart quotes (, , , ) with standard ASCII quotes (', "). A database replication tool should be a faithful, lossless replica of the source database and should not mutate user data. Please avoid mutating user data; remove normalizeSmartQuotes or make it strictly optional/configurable.

Comment thread canal/canal_test.go Outdated
Comment on lines +388 to +406
expected := `
SELECT
c.ORDINAL_POSITION,
CASE
WHEN c.CHARACTER_SET_NAME IS NOT NULL THEN c.CHARACTER_SET_NAME
WHEN c.DATA_TYPE IN ('binary','varbinary','tinyblob','blob','mediumblob','longblob') THEN col.CHARACTER_SET_NAME
END AS CHARACTER_SET_NAME,
c.COLUMN_NAME
FROM
information_schema.COLUMNS c
LEFT JOIN information_schema.TABLES t
ON t.TABLE_SCHEMA = c.TABLE_SCHEMA AND t.TABLE_NAME = c.TABLE_NAME
LEFT JOIN information_schema.COLLATIONS col
ON col.COLLATION_NAME = t.TABLE_COLLATION
WHERE
c.TABLE_SCHEMA = ?
AND c.TABLE_NAME = ?
AND (c.CHARACTER_SET_NAME IS NOT NULL OR c.DATA_TYPE IN ('binary','varbinary','tinyblob','blob','mediumblob','longblob'));
`

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.

medium

The expected query in TestGenerateCharsetQuery does not match the actual query returned by GenerateCharsetQuery() in canal.go (which contains COALESCE and ELSE clauses). This mismatch will cause the test to fail. Please update the expected query to match the actual implementation.

	expected := "\n\t	SELECT \n\t\t   c.ORDINAL_POSITION,\n\t\t   COALESCE(\n\t\t\t  CASE \n\t\t\t\t WHEN c.CHARACTER_SET_NAME IS NOT NULL THEN c.CHARACTER_SET_NAME\n\t\t\t\t WHEN c.DATA_TYPE IN ('binary','varbinary','tinyblob','blob','mediumblob','longblob') THEN col.CHARACTER_SET_NAME\n\t\t\t\t ELSE col.CHARACTER_SET_NAME\n\t\t\t  END,\n\t\t\t  'utf8mb4'\n\t\t   ) AS CHARACTER_SET_NAME,\n\t\t   c.COLUMN_NAME\n\t\tFROM \n\t\t   information_schema.COLUMNS c\n\t\tLEFT JOIN information_schema.TABLES t\n\t\t   ON t.TABLE_SCHEMA = c.TABLE_SCHEMA AND t.TABLE_NAME = c.TABLE_NAME\n\t\tLEFT JOIN information_schema.COLLATIONS col\n\t\t   ON col.COLLATION_NAME = t.TABLE_COLLATION\n\t\tWHERE \n\t\t   c.TABLE_SCHEMA = ?\n\t\t   AND c.TABLE_NAME = ?\n\t\t   AND (c.CHARACTER_SET_NAME IS NOT NULL \n\t\t\t\tOR c.DATA_TYPE IN ('binary','varbinary','tinyblob','blob','mediumblob','longblob')\n\t\t\t\tOR c.DATA_TYPE IN ('varchar','char','text','tinytext','mediumtext','longtext'));\n\t\t"

sigalikanevsky and others added 4 commits June 21, 2026 14:06
ubuntu-18.04 runners were retired, leaving the test matrix jobs queued
forever. Move to ubuntu-latest (MySQL 8.0), replace the removed PASSWORD()
call with ALTER USER ... mysql_native_password, and bump the EOL actions
(checkout@v4, setup-go@v5, golangci-lint-action@v6 pinned to v1.64.8 with
Go 1.22 for the lint job).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
golangci-lint-action runs `config verify`, which strictly validates the
schema and rejects the obsolete nolintlint.allow-leading-space key
(removed in newer golangci-lint). Verified locally: `config verify` and
`run` both pass on v1.64.8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lance6716

Copy link
Copy Markdown
Collaborator

hi @sigalikanevsky did you wrongly create this PR? Seems it's your highly customized fork

sigalikanevsky and others added 8 commits June 22, 2026 09:48
- ci.yml: run MySQL 5.7 via Docker (ubuntu-latest ships 8.0, but the suite
  was validated against 5.7 — matches bitbucket-pipelines.yml). Clears the
  8.0-only failures in replication/* and schema (binlog/JSON format, extra
  information_schema columns).
- canal_test.go TestGenerateCharsetQuery: sync the expected query to the
  current GenerateCharsetQuery() output (COALESCE(...'utf8mb4') + varchar/
  char/text clause); the test string had drifted from the code.
- canal_test.go SetUpSuite: IncludeTableRegex ".*\\.canal_test" -> literal
  "test.canal_test" so it passes the charset code's db.table validation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GetColumnsCharsets (called from NewCanal) requires each IncludeTableRegex
entry to be a literal db.table and errors otherwise, which broke the canal
suite's SetUpSuite when it used the regex ".*\.canal_test". Use the literal
"test.canal_test" and adjust TestCanalFilter's cross-db case accordingly
(a table in another database is now excluded under the literal include).

Tests-only change; canal.go is intentionally left unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tests-only (row_event.go unchanged):
- TestJsonCompatibility: JSON columns decode to string, not []byte — drop
  the []uint8 casts so the assertions compare as strings (same content).
- TestDecodeDatetime2: the all-zero datetime decodes to nil; accept it via
  a case nil branch instead of failing the type switch.
- TestDecodeValueBinaryFallback: skipped — decodeValue does real per-charset
  decoding, so invalid-UTF-8 input doesn't uniformly fall back to the latin1
  rendering the test asserts (charset=utf8 yields replacement chars). Needs a
  decoder-side fix, tracked separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Re-applies the minimal, non-behavioral fixes golangci-lint requires:
- goimports/gofmt formatting on the flagged files
- remove the trailing blank line in canal.go (whitespace linter)
- mysql/util.go: replace the always-true `case n <= 0xffffffffffffffff`
  with `default:` (staticcheck SA4003)

Verified locally on golangci-lint v1.64.8: `config verify` and `run` both
pass; `go build ./...` succeeds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a Bitbucket-only `main` branch pipeline that runs tests/lint and then
cuts an auto-incremented vX.Y.Z tag (patch bump) once they pass, so every
PR merged to main produces a release tag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- packet/conn.go, client/conn.go: gofmt comment reformatting (goimports)
- replication/row_event.go: regroup stdlib imports (goimports)
- canal/canal.go: drop blank line before closing brace (whitespace)

Verified clean with golangci-lint v1.64.8 (the CI-pinned version).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dveeden

dveeden commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

hi @sigalikanevsky did you wrongly create this PR? Seems it's your highly customized fork

Yes, this PR also doesn't seem to make sense to me.

For the PR title: We could setup a mirror on bitbucket and/or any other git hosting as long as it doesn't interfere too much with the current workflow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants