Skip to content
This repository was archived by the owner on Aug 10, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
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
46 changes: 28 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,51 @@ jobs:
test:
strategy:
matrix:
go: [ 1.16, 1.15 ]
go: ['1.16', '1.15']
name: Tests Go ${{ matrix.go }}
runs-on: ubuntu-18.04
# ubuntu-18.04 was retired by GitHub (jobs hung "waiting for a runner").
# ubuntu-latest ships MySQL 8.0, but go-mysql's tests were validated against
# MySQL 5.7 (the old 18.04 default), so we run 5.7 via Docker to match — same
# as bitbucket-pipelines.yml. (`docker run -p` is reachable at 127.0.0.1:3306.)
runs-on: ubuntu-latest

steps:
- name: Setup MySQL
- name: Setup MySQL 5.7 (binlog + GTID)
run: |
echo -n "mysql -V: " ; mysql -V
echo -n "mysqldump -V: " ; mysqldump -V

echo -e '[mysqld]\nserver-id=1\nlog-bin=mysql\nbinlog-format=row\ngtid-mode=ON\nenforce_gtid_consistency=ON\n' | sudo tee /etc/mysql/conf.d/replication.cnf
sudo service mysql start
sudo mysql -h 127.0.0.1 -uroot -proot -e "use mysql; update user set authentication_string=PASSWORD('') where User='root'; update user set plugin='mysql_native_password'; FLUSH PRIVILEGES;"
# create ssl/rsa files for mysql ssl support
sudo mysql_ssl_rsa_setup --uid=mysql
mysql -e "CREATE DATABASE IF NOT EXISTS test;" -uroot
mysql -e "SHOW VARIABLES LIKE 'log_bin'" -uroot
docker run -d --name mysql -p 3306:3306 \
-e MYSQL_ALLOW_EMPTY_PASSWORD=yes \
-e MYSQL_DATABASE=test \
mysql:5.7 \
--server-id=1 --log-bin=mysql-bin --binlog-format=ROW \
--gtid-mode=ON --enforce-gtid-consistency=ON
echo "Waiting for MySQL to accept connections..."
for i in $(seq 1 60); do
if mysqladmin ping -h 127.0.0.1 -P 3306 -uroot --silent 2>/dev/null; then echo "MySQL is up"; break; fi
if [ "$i" = "60" ]; then echo "MySQL failed to start"; docker logs mysql; exit 1; fi
sleep 2
done
mysql -h 127.0.0.1 -uroot -e "CREATE DATABASE IF NOT EXISTS test; SHOW VARIABLES LIKE 'log_bin'; SELECT @@gtid_mode;"
- name: Prepare for Go
run: |
sudo apt-get install -y make gcc
- name: Install Go
uses: actions/setup-go@v2
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go }}
- name: Checkout code
uses: actions/checkout@v1
uses: actions/checkout@v4
- name: Run tests
run: go test ./...

golangci:
name: golangci
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
uses: golangci/golangci-lint-action@v6
with:
version: latest
version: v1.64.8
5 changes: 1 addition & 4 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ linters:
disable-all: true
enable:
# All code is ready for:
- deadcode
# deadcode/structcheck/varcheck were removed in golangci-lint v1.49 — folded into `unused`.
- errcheck
- staticcheck
- structcheck
- typecheck
- unused
- varcheck
- misspell
- nolintlint
- goimports
Expand All @@ -24,5 +22,4 @@ linters:
linters-settings:
nolintlint:
allow-unused: false
allow-leading-space: false
require-specific: true
129 changes: 129 additions & 0 deletions bitbucket-pipelines.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Bitbucket Pipelines CI for go-mysql.
# Aligned to mirror .github/workflows/ci.yml as closely as Bitbucket allows:
# * Go matrix 1.15 + 1.16 (parallel test steps).
# * `go test ./...` (no -race / no timeout) — same command as the GH job.
# * MySQL with binlog + GTID, root / empty password / db `test` on 127.0.0.1:3306
# (the defaults in the *_test.go flags). MySQL 5.7 to match the ubuntu-18.04 era.
# * Runs on every branch push AND every pull request (== GH `on: [push, pull_request]`).
# * Tests + golangci-lint run in parallel.
#
# Two unavoidable Bitbucket-isms (can't be byte-identical to GH):
# 1. MySQL is launched with `docker run` rather than a Pipelines `service:` block,
# because services can't take the --log-bin/--gtid-mode flags the tests need.
# A `docker run -p` port is reachable at 127.0.0.1:3306 from the build step.
# 2. golangci-lint is pinned to v1.64.8 (same major as the GitHub job's
# `version: latest` at time of migration) for reproducible CI. .golangci.yml
# has been modernized (deprecated deadcode/structcheck/varcheck removed), so a
# current golangci-lint runs cleanly.

image: golang:1.22 # default — used by the lint step (modern Go needed for golangci-lint
# 1.64.x, mirroring GH's lint job). Test steps override to 1.15/1.16 below.

definitions:
services:
docker:
memory: 3072 # headroom for the MySQL container
caches:
gomodcache: /go/pkg/mod

steps:
- step: &test-go116
name: Tests Go 1.16 (MySQL 5.7 binlog+GTID)
image: golang:1.16
size: 2x
services:
- docker
caches:
- gomodcache
script: &test-script
- apt-get update >/dev/null && apt-get install -y --no-install-recommends default-mysql-client >/dev/null
- |
docker run -d --name mysql -p 3306:3306 \
-e MYSQL_ALLOW_EMPTY_PASSWORD=yes \
-e MYSQL_DATABASE=test \
mysql:5.7 \
--server-id=1 --log-bin=mysql-bin --binlog-format=ROW \
--gtid-mode=ON --enforce-gtid-consistency=ON
- |
echo "Waiting for MySQL to accept connections..."
for i in $(seq 1 60); do
if mysqladmin ping -h 127.0.0.1 -P 3306 -uroot --silent 2>/dev/null; then echo "MySQL is up"; break; fi
if [ "$i" = "60" ]; then echo "MySQL failed to start"; docker logs mysql; exit 1; fi
sleep 2
done
- mysql -h 127.0.0.1 -uroot -e "CREATE DATABASE IF NOT EXISTS test; SHOW VARIABLES LIKE 'log_bin'; SELECT @@gtid_mode;"
- go test ./...

- step: &test-go115
name: Tests Go 1.15 (MySQL 5.7 binlog+GTID)
image: golang:1.15
size: 2x
services:
- docker
caches:
- gomodcache
script: *test-script

- step: &lint
name: golangci-lint
caches:
- gomodcache
script:
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b "$(go env GOPATH)/bin" v1.64.8
- "$(go env GOPATH)/bin/golangci-lint run --timeout 5m"

# Cuts an auto-incremented semver tag (patch bump) when a commit lands on main —
# i.e. when a PR is merged. Only runs after the test/lint steps pass (see the
# `branches.main` pipeline below). Bitbucket-only: there is no equivalent in the
# GitHub workflow this file otherwise mirrors.
- step: &tag-release
name: Tag release (semver patch bump)
image: alpine/git:latest
clone:
depth: full # need full history so all existing tags are visible
script:
- git config user.name "bitbucket-pipelines"
- git config user.email "pipelines@bitbucket.org"
- git fetch --tags --quiet
- |
latest=$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n1)
if [ -z "$latest" ]; then
new="v0.1.0"
echo "No existing semver tag found; starting at $new"
else
ver=${latest#v}
major=$(echo "$ver" | cut -d. -f1)
minor=$(echo "$ver" | cut -d. -f2)
patch=$(echo "$ver" | cut -d. -f3)
new="v${major}.${minor}.$((patch + 1))"
echo "Latest tag $latest -> new tag $new"
fi
# No-op if HEAD is already tagged (e.g. a re-run of the same commit).
if git describe --exact-match --tags HEAD >/dev/null 2>&1; then
echo "HEAD already tagged; skipping."
exit 0
fi
git tag -a "$new" -m "Release $new (PR merged to main)"
git push origin "$new"

pipelines:
branches:
# Explicit, more-specific glob: main pushes (= PR merges) run this instead of '**'.
# Tests/lint run first; the tag step only executes if they all pass.
main:
- parallel:
- step: *test-go116
- step: *test-go115
- step: *lint
- step: *tag-release
'**':
- parallel:
- step: *test-go116
- step: *test-go115
- step: *lint
pull-requests:
'**':
- parallel:
- step: *test-go116
- step: *test-go115
- step: *lint
1 change: 0 additions & 1 deletion canal/canal.go
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,6 @@ func (c *Canal) GetColumnsCharsets() error {
if err := c.setColumnsCharsetFromRows(tableRegex, rows); err != nil {
return fmt.Errorf("failed to set charset from rows for table %s: %w", tableRegex, err)
}

}

return nil
Expand Down
37 changes: 13 additions & 24 deletions canal/canal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ package canal
import (
"flag"
"fmt"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
"strings"
"testing"
"time"

"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"

"github.com/go-mysql-org/go-mysql/mysql"
"github.com/go-mysql-org/go-mysql/replication"
. "github.com/pingcap/check"
Expand Down Expand Up @@ -51,8 +52,11 @@ func (s *canalTestSuite) SetUpSuite(c *C) {
cfg.Dump.Where = "id>0"

// include & exclude config
// NOTE: GetColumnsCharsets (called from NewCanal) requires each IncludeTableRegex
// entry to be a literal db.table, so this uses "test.canal_test" rather than a
// cross-db regex like ".*\\.canal_test".
cfg.IncludeTableRegex = make([]string, 1)
cfg.IncludeTableRegex[0] = ".*\\.canal_test"
cfg.IncludeTableRegex[0] = "test.canal_test"
cfg.ExcludeTableRegex = make([]string, 2)
cfg.ExcludeTableRegex[0] = "mysql\\..*"
cfg.ExcludeTableRegex[1] = ".*\\..*_inner"
Expand Down Expand Up @@ -191,9 +195,12 @@ func (s *canalTestSuite) TestCanalFilter(c *C) {
sch, err := s.c.GetTable("test", "canal_test")
c.Assert(err, IsNil)
c.Assert(sch, NotNil)
_, err = s.c.GetTable("not_exist_db", "canal_test")
c.Assert(errors.Trace(err), Not(Equals), ErrExcludedTable)
// excluded
// IncludeTableRegex is the literal "test.canal_test", so a table in another
// database is not matched and is therefore excluded.
sch, err = s.c.GetTable("not_exist_db", "canal_test")
c.Assert(errors.Cause(err), Equals, ErrExcludedTable)
c.Assert(sch, IsNil)
sch, err = s.c.GetTable("test", "canal_test_inner")
c.Assert(errors.Cause(err), Equals, ErrExcludedTable)
c.Assert(sch, IsNil)
Expand Down Expand Up @@ -385,25 +392,7 @@ func TestWithoutSchemeExp(t *testing.T) {
func TestGenerateCharsetQuery(t *testing.T) {
c := &Canal{}

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'));
`
expected := `SELECT c.ORDINAL_POSITION, COALESCE( 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 ELSE col.CHARACTER_SET_NAME END, 'utf8mb4' ) 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') OR c.DATA_TYPE IN ('varchar','char','text','tinytext','mediumtext','longtext'));`

actual, err := c.GenerateCharsetQuery()
assert.NoError(t, err)
Expand Down
41 changes: 20 additions & 21 deletions canal/heartbeat_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,39 +49,39 @@ func TestHeartbeatIntervalConversion(t *testing.T) {
// TestShouldSendHeartbeat tests the heartbeat timing logic
func TestShouldSendHeartbeat(t *testing.T) {
tests := []struct {
name string
name string
heartbeatInterval time.Duration
lastEventTime time.Time
currentTime time.Time
expected bool
lastEventTime time.Time
currentTime time.Time
expected bool
}{
{
name: "Disabled (zero interval)",
name: "Disabled (zero interval)",
heartbeatInterval: 0,
lastEventTime: time.Now().Add(-100 * time.Second),
currentTime: time.Now(),
expected: false,
lastEventTime: time.Now().Add(-100 * time.Second),
currentTime: time.Now(),
expected: false,
},
{
name: "Not enough time passed",
name: "Not enough time passed",
heartbeatInterval: 60 * time.Second,
lastEventTime: time.Now().Add(-30 * time.Second),
currentTime: time.Now(),
expected: false,
lastEventTime: time.Now().Add(-30 * time.Second),
currentTime: time.Now(),
expected: false,
},
{
name: "Exactly at interval",
name: "Exactly at interval",
heartbeatInterval: 60 * time.Second,
lastEventTime: time.Now().Add(-60 * time.Second),
currentTime: time.Now(),
expected: true,
lastEventTime: time.Now().Add(-60 * time.Second),
currentTime: time.Now(),
expected: true,
},
{
name: "Past interval",
name: "Past interval",
heartbeatInterval: 60 * time.Second,
lastEventTime: time.Now().Add(-120 * time.Second),
currentTime: time.Now(),
expected: true,
lastEventTime: time.Now().Add(-120 * time.Second),
currentTime: time.Now(),
expected: true,
},
}

Expand Down Expand Up @@ -275,4 +275,3 @@ func (h *mockHeartbeatEventHandler) OnRow(e *RowsEvent) error {
func (h *mockHeartbeatEventHandler) String() string {
return "mockHeartbeatEventHandler"
}

7 changes: 4 additions & 3 deletions client/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,10 @@ func (c *Conn) readInitialHandshake() error {
// generate auth response data according to auth plugin
//
// NOTE: the returned boolean value indicates whether to add a \NUL to the end of data.
// it is quite tricky because MySQL server expects different formats of responses in different auth situations.
// here the \NUL needs to be added when sending back the empty password or cleartext password in 'sha256_password'
// authentication.
//
// it is quite tricky because MySQL server expects different formats of responses in different auth situations.
// here the \NUL needs to be added when sending back the empty password or cleartext password in 'sha256_password'
// authentication.
func (c *Conn) genAuthResponse(authData []byte) ([]byte, bool, error) {
// password hashing
switch c.authPluginName {
Expand Down
5 changes: 3 additions & 2 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,9 @@ func (s *clientTestSuite) TestConn_SetCapability(c *C) {
}

// NOTE for MySQL 5.5 and 5.6, server side has to config SSL to pass the TLS test, otherwise, it will throw error that
// MySQL server does not support TLS required by the client. However, for MySQL 5.7 and above, auto generated certificates
// are used by default so that manual config is no longer necessary.
//
// MySQL server does not support TLS required by the client. However, for MySQL 5.7 and above, auto generated certificates
// are used by default so that manual config is no longer necessary.
func (s *clientTestSuite) TestConn_TLS_Verify(c *C) {
// Verify that the provided tls.Config is used when attempting to connect to mysql.
// An empty tls.Config will result in a connection error.
Expand Down
Loading
Loading