Skip to content

Commit 2ad614a

Browse files
Merge branch 'master' into fix-charset
2 parents c93060e + 3f44e04 commit 2ad614a

File tree

18 files changed

+218
-43
lines changed

18 files changed

+218
-43
lines changed

.github/workflows/golangci-lint.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,5 @@ jobs:
1919
- uses: actions/checkout@v3
2020
- name: golangci-lint
2121
uses: golangci/golangci-lint-action@v3
22+
with:
23+
version: v1.46.2

doc/command-line-flags.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ A more in-depth discussion of various `gh-ost` command line flags: implementatio
66

77
Add this flag when executing on Aliyun RDS.
88

9+
### allow-zero-in-date
10+
11+
Allows the user to make schema changes that include a zero date or zero in date (e.g. adding a `datetime default '0000-00-00 00:00:00'` column), even if global `sql_mode` on MySQL has `NO_ZERO_IN_DATE,NO_ZERO_DATE`.
12+
913
### azure
1014

1115
Add this flag when executing on Azure Database for MySQL.
@@ -242,6 +246,14 @@ Provide a command delimited list of replicas; `gh-ost` will throttle when any of
242246

243247
Provide an HTTP endpoint; `gh-ost` will issue `HEAD` requests on given URL and throttle whenever response status code is not `200`. The URL can be queried and updated dynamically via [interactive commands](interactive-commands.md). Empty URL disables the HTTP check.
244248

249+
### throttle-http-interval-millis
250+
251+
Defaults to 100. Configures the HTTP throttle check interval in milliseconds.
252+
253+
### throttle-http-timeout-millis
254+
255+
Defaults to 1000 (1 second). Configures the HTTP throttler check timeout in milliseconds.
256+
245257
### timestamp-old-table
246258

247259
Makes the _old_ table include a timestamp value. The _old_ table is what the original table is renamed to at the end of a successful migration. For example, if the table is `gh_ost_test`, then the _old_ table would normally be `_gh_ost_test_del`. With `--timestamp-old-table` it would be, for example, `_gh_ost_test_20170221103147_del`.

doc/requirements-and-limitations.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ The `SUPER` privilege is required for `STOP SLAVE`, `START SLAVE` operations. Th
2020
- Switching your `binlog_format` to `ROW`, in the case where it is _not_ `ROW` and you explicitly specified `--switch-to-rbr`
2121
- If your replication is already in RBR (`binlog_format=ROW`) you can specify `--assume-rbr` to avoid the `STOP SLAVE/START SLAVE` operations, hence no need for `SUPER`.
2222

23+
- `gh-ost` uses the `REPEATABLE_READ` transaction isolation level for all MySQL connections, regardless of the server default.
24+
2325
- Running `--test-on-replica`: before the cut-over phase, `gh-ost` stops replication so that you can compare the two tables and satisfy that the migration is sound.
2426

2527
### Limitations

go/base/context.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ type MigrationContext struct {
9292
AssumeRBR bool
9393
SkipForeignKeyChecks bool
9494
SkipStrictMode bool
95+
AllowZeroInDate bool
9596
NullableUniqueKeyAllowed bool
9697
ApproveRenamedColumns bool
9798
SkipRenamedColumns bool

go/cmd/gh-ost/main.go

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ func main() {
7878
flag.BoolVar(&migrationContext.DiscardForeignKeys, "discard-foreign-keys", false, "DANGER! This flag will migrate a table that has foreign keys and will NOT create foreign keys on the ghost table, thus your altered table will have NO foreign keys. This is useful for intentional dropping of foreign keys")
7979
flag.BoolVar(&migrationContext.SkipForeignKeyChecks, "skip-foreign-key-checks", false, "set to 'true' when you know for certain there are no foreign keys on your table, and wish to skip the time it takes for gh-ost to verify that")
8080
flag.BoolVar(&migrationContext.SkipStrictMode, "skip-strict-mode", false, "explicitly tell gh-ost binlog applier not to enforce strict sql mode")
81+
flag.BoolVar(&migrationContext.AllowZeroInDate, "allow-zero-in-date", false, "explicitly tell gh-ost binlog applier to ignore NO_ZERO_IN_DATE,NO_ZERO_DATE in sql_mode")
8182
flag.BoolVar(&migrationContext.AliyunRDS, "aliyun-rds", false, "set to 'true' when you execute on Aliyun RDS.")
8283
flag.BoolVar(&migrationContext.GoogleCloudPlatform, "gcp", false, "set to 'true' when you execute on a 1st generation Google Cloud Platform (GCP).")
8384
flag.BoolVar(&migrationContext.AzureMySQL, "azure", false, "set to 'true' when you execute on Azure Database on MySQL.")
@@ -180,7 +181,7 @@ func main() {
180181
}
181182

182183
if migrationContext.AlterStatement == "" {
183-
log.Fatalf("--alter must be provided and statement must not be empty")
184+
log.Fatal("--alter must be provided and statement must not be empty")
184185
}
185186
parser := sql.NewParserFromAlterStatement(migrationContext.AlterStatement)
186187
migrationContext.AlterStatementOptions = parser.GetAlterStatementOptions()
@@ -189,7 +190,7 @@ func main() {
189190
if parser.HasExplicitSchema() {
190191
migrationContext.DatabaseName = parser.GetExplicitSchema()
191192
} else {
192-
log.Fatalf("--database must be provided and database name must not be empty, or --alter must specify database name")
193+
log.Fatal("--database must be provided and database name must not be empty, or --alter must specify database name")
193194
}
194195
}
195196

@@ -201,48 +202,48 @@ func main() {
201202
if parser.HasExplicitTable() {
202203
migrationContext.OriginalTableName = parser.GetExplicitTable()
203204
} else {
204-
log.Fatalf("--table must be provided and table name must not be empty, or --alter must specify table name")
205+
log.Fatal("--table must be provided and table name must not be empty, or --alter must specify table name")
205206
}
206207
}
207208
migrationContext.Noop = !(*executeFlag)
208209
if migrationContext.AllowedRunningOnMaster && migrationContext.TestOnReplica {
209-
migrationContext.Log.Fatalf("--allow-on-master and --test-on-replica are mutually exclusive")
210+
migrationContext.Log.Fatal("--allow-on-master and --test-on-replica are mutually exclusive")
210211
}
211212
if migrationContext.AllowedRunningOnMaster && migrationContext.MigrateOnReplica {
212-
migrationContext.Log.Fatalf("--allow-on-master and --migrate-on-replica are mutually exclusive")
213+
migrationContext.Log.Fatal("--allow-on-master and --migrate-on-replica are mutually exclusive")
213214
}
214215
if migrationContext.MigrateOnReplica && migrationContext.TestOnReplica {
215-
migrationContext.Log.Fatalf("--migrate-on-replica and --test-on-replica are mutually exclusive")
216+
migrationContext.Log.Fatal("--migrate-on-replica and --test-on-replica are mutually exclusive")
216217
}
217218
if migrationContext.SwitchToRowBinlogFormat && migrationContext.AssumeRBR {
218-
migrationContext.Log.Fatalf("--switch-to-rbr and --assume-rbr are mutually exclusive")
219+
migrationContext.Log.Fatal("--switch-to-rbr and --assume-rbr are mutually exclusive")
219220
}
220221
if migrationContext.TestOnReplicaSkipReplicaStop {
221222
if !migrationContext.TestOnReplica {
222-
migrationContext.Log.Fatalf("--test-on-replica-skip-replica-stop requires --test-on-replica to be enabled")
223+
migrationContext.Log.Fatal("--test-on-replica-skip-replica-stop requires --test-on-replica to be enabled")
223224
}
224225
migrationContext.Log.Warning("--test-on-replica-skip-replica-stop enabled. We will not stop replication before cut-over. Ensure you have a plugin that does this.")
225226
}
226227
if migrationContext.CliMasterUser != "" && migrationContext.AssumeMasterHostname == "" {
227-
migrationContext.Log.Fatalf("--master-user requires --assume-master-host")
228+
migrationContext.Log.Fatal("--master-user requires --assume-master-host")
228229
}
229230
if migrationContext.CliMasterPassword != "" && migrationContext.AssumeMasterHostname == "" {
230-
migrationContext.Log.Fatalf("--master-password requires --assume-master-host")
231+
migrationContext.Log.Fatal("--master-password requires --assume-master-host")
231232
}
232233
if migrationContext.TLSCACertificate != "" && !migrationContext.UseTLS {
233-
migrationContext.Log.Fatalf("--ssl-ca requires --ssl")
234+
migrationContext.Log.Fatal("--ssl-ca requires --ssl")
234235
}
235236
if migrationContext.TLSCertificate != "" && !migrationContext.UseTLS {
236-
migrationContext.Log.Fatalf("--ssl-cert requires --ssl")
237+
migrationContext.Log.Fatal("--ssl-cert requires --ssl")
237238
}
238239
if migrationContext.TLSKey != "" && !migrationContext.UseTLS {
239-
migrationContext.Log.Fatalf("--ssl-key requires --ssl")
240+
migrationContext.Log.Fatal("--ssl-key requires --ssl")
240241
}
241242
if migrationContext.TLSAllowInsecure && !migrationContext.UseTLS {
242-
migrationContext.Log.Fatalf("--ssl-allow-insecure requires --ssl")
243+
migrationContext.Log.Fatal("--ssl-allow-insecure requires --ssl")
243244
}
244245
if *replicationLagQuery != "" {
245-
migrationContext.Log.Warningf("--replication-lag-query is deprecated")
246+
migrationContext.Log.Warning("--replication-lag-query is deprecated")
246247
}
247248

248249
switch *cutOver {

go/logic/applier.go

Lines changed: 75 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,24 @@ func (this *Applier) validateAndReadTimeZone() error {
117117
return nil
118118
}
119119

120+
// generateSqlModeQuery return a `sql_mode = ...` query, to be wrapped with a `set session` or `set global`,
121+
// based on gh-ost configuration:
122+
// - User may skip strict mode
123+
// - User may allow zero dats or zero in dates
124+
func (this *Applier) generateSqlModeQuery() string {
125+
sqlModeAddendum := `,NO_AUTO_VALUE_ON_ZERO`
126+
if !this.migrationContext.SkipStrictMode {
127+
sqlModeAddendum = fmt.Sprintf("%s,STRICT_ALL_TABLES", sqlModeAddendum)
128+
}
129+
sqlModeQuery := fmt.Sprintf("CONCAT(@@session.sql_mode, ',%s')", sqlModeAddendum)
130+
if this.migrationContext.AllowZeroInDate {
131+
sqlModeQuery = fmt.Sprintf("REPLACE(REPLACE(%s, 'NO_ZERO_IN_DATE', ''), 'NO_ZERO_DATE', '')", sqlModeQuery)
132+
}
133+
sqlModeQuery = fmt.Sprintf("sql_mode = %s", sqlModeQuery)
134+
135+
return sqlModeQuery
136+
}
137+
120138
// readTableColumns reads table columns on applier
121139
func (this *Applier) readTableColumns() (err error) {
122140
this.migrationContext.Log.Infof("Examining table structure on applier")
@@ -182,11 +200,33 @@ func (this *Applier) CreateGhostTable() error {
182200
sql.EscapeName(this.migrationContext.DatabaseName),
183201
sql.EscapeName(this.migrationContext.GetGhostTableName()),
184202
)
185-
if _, err := sqlutils.ExecNoPrepare(this.db, query); err != nil {
186-
return err
187-
}
188-
this.migrationContext.Log.Infof("Ghost table created")
189-
return nil
203+
204+
err := func() error {
205+
tx, err := this.db.Begin()
206+
if err != nil {
207+
return err
208+
}
209+
defer tx.Rollback()
210+
211+
sessionQuery := fmt.Sprintf(`SET SESSION time_zone = '%s'`, this.migrationContext.ApplierTimeZone)
212+
sessionQuery = fmt.Sprintf("%s, %s", sessionQuery, this.generateSqlModeQuery())
213+
214+
if _, err := tx.Exec(sessionQuery); err != nil {
215+
return err
216+
}
217+
if _, err := tx.Exec(query); err != nil {
218+
return err
219+
}
220+
this.migrationContext.Log.Infof("Ghost table created")
221+
if err := tx.Commit(); err != nil {
222+
// Neither SET SESSION nor ALTER are really transactional, so strictly speaking
223+
// there's no need to commit; but let's do this the legit way anyway.
224+
return err
225+
}
226+
return nil
227+
}()
228+
229+
return err
190230
}
191231

192232
// AlterGhost applies `alter` statement on ghost table
@@ -201,11 +241,33 @@ func (this *Applier) AlterGhost() error {
201241
sql.EscapeName(this.migrationContext.GetGhostTableName()),
202242
)
203243
this.migrationContext.Log.Debugf("ALTER statement: %s", query)
204-
if _, err := sqlutils.ExecNoPrepare(this.db, query); err != nil {
205-
return err
206-
}
207-
this.migrationContext.Log.Infof("Ghost table altered")
208-
return nil
244+
245+
err := func() error {
246+
tx, err := this.db.Begin()
247+
if err != nil {
248+
return err
249+
}
250+
defer tx.Rollback()
251+
252+
sessionQuery := fmt.Sprintf(`SET SESSION time_zone = '%s'`, this.migrationContext.ApplierTimeZone)
253+
sessionQuery = fmt.Sprintf("%s, %s", sessionQuery, this.generateSqlModeQuery())
254+
255+
if _, err := tx.Exec(sessionQuery); err != nil {
256+
return err
257+
}
258+
if _, err := tx.Exec(query); err != nil {
259+
return err
260+
}
261+
this.migrationContext.Log.Infof("Ghost table altered")
262+
if err := tx.Commit(); err != nil {
263+
// Neither SET SESSION nor ALTER are really transactional, so strictly speaking
264+
// there's no need to commit; but let's do this the legit way anyway.
265+
return err
266+
}
267+
return nil
268+
}()
269+
270+
return err
209271
}
210272

211273
// AlterGhost applies `alter` statement on ghost table
@@ -539,12 +601,9 @@ func (this *Applier) ApplyIterationInsertQuery() (chunkSize int64, rowsAffected
539601
return nil, err
540602
}
541603
defer tx.Rollback()
604+
542605
sessionQuery := fmt.Sprintf(`SET SESSION time_zone = '%s'`, this.migrationContext.ApplierTimeZone)
543-
sqlModeAddendum := `,NO_AUTO_VALUE_ON_ZERO`
544-
if !this.migrationContext.SkipStrictMode {
545-
sqlModeAddendum = fmt.Sprintf("%s,STRICT_ALL_TABLES", sqlModeAddendum)
546-
}
547-
sessionQuery = fmt.Sprintf("%s, sql_mode = CONCAT(@@session.sql_mode, ',%s')", sessionQuery, sqlModeAddendum)
606+
sessionQuery = fmt.Sprintf("%s, %s", sessionQuery, this.generateSqlModeQuery())
548607

549608
if _, err := tx.Exec(sessionQuery); err != nil {
550609
return nil, err
@@ -1056,12 +1115,7 @@ func (this *Applier) ApplyDMLEventQueries(dmlEvents [](*binlog.BinlogDMLEvent))
10561115
}
10571116

10581117
sessionQuery := "SET SESSION time_zone = '+00:00'"
1059-
1060-
sqlModeAddendum := `,NO_AUTO_VALUE_ON_ZERO`
1061-
if !this.migrationContext.SkipStrictMode {
1062-
sqlModeAddendum = fmt.Sprintf("%s,STRICT_ALL_TABLES", sqlModeAddendum)
1063-
}
1064-
sessionQuery = fmt.Sprintf("%s, sql_mode = CONCAT(@@session.sql_mode, ',%s')", sessionQuery, sqlModeAddendum)
1118+
sessionQuery = fmt.Sprintf("%s, %s", sessionQuery, this.generateSqlModeQuery())
10651119

10661120
if _, err := tx.Exec(sessionQuery); err != nil {
10671121
return rollback(err)

go/mysql/connection.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2016 GitHub Inc.
2+
Copyright 2022 GitHub Inc.
33
See https://github.com/github/gh-ost/blob/master/LICENSE
44
*/
55

@@ -12,12 +12,14 @@ import (
1212
"fmt"
1313
"io/ioutil"
1414
"net"
15+
"strings"
1516

1617
"github.com/go-sql-driver/mysql"
1718
)
1819

1920
const (
20-
TLS_CONFIG_KEY = "ghost"
21+
transactionIsolation = "REPEATABLE-READ"
22+
TLS_CONFIG_KEY = "ghost"
2123
)
2224

2325
// ConnectionConfig is the minimal configuration required to connect to a MySQL server
@@ -112,12 +114,23 @@ func (this *ConnectionConfig) GetDBUri(databaseName string) string {
112114
// Wrap IPv6 literals in square brackets
113115
hostname = fmt.Sprintf("[%s]", hostname)
114116
}
115-
interpolateParams := true
117+
116118
// go-mysql-driver defaults to false if tls param is not provided; explicitly setting here to
117119
// simplify construction of the DSN below.
118120
tlsOption := "false"
119121
if this.tlsConfig != nil {
120122
tlsOption = TLS_CONFIG_KEY
121123
}
122-
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?timeout=%fs&readTimeout=%fs&writeTimeout=%fs&interpolateParams=%t&autocommit=true&charset=utf8mb4,utf8,latin1&tls=%s", this.User, this.Password, hostname, this.Key.Port, databaseName, this.Timeout, this.Timeout, this.Timeout, interpolateParams, tlsOption)
124+
connectionParams := []string{
125+
"autocommit=true",
126+
"charset=utf8mb4,utf8,latin1",
127+
"interpolateParams=true",
128+
fmt.Sprintf("tls=%s", tlsOption),
129+
fmt.Sprintf("transaction_isolation=%q", transactionIsolation),
130+
fmt.Sprintf("timeout=%fs", this.Timeout),
131+
fmt.Sprintf("readTimeout=%fs", this.Timeout),
132+
fmt.Sprintf("writeTimeout=%fs", this.Timeout),
133+
}
134+
135+
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?%s", this.User, this.Password, hostname, this.Key.Port, databaseName, strings.Join(connectionParams, "&"))
123136
}

go/mysql/connection_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2016 GitHub Inc.
2+
Copyright 2022 GitHub Inc.
33
See https://github.com/github/gh-ost/blob/master/LICENSE
44
*/
55

@@ -67,18 +67,20 @@ func TestGetDBUri(t *testing.T) {
6767
c.Key = InstanceKey{Hostname: "myhost", Port: 3306}
6868
c.User = "gromit"
6969
c.Password = "penguin"
70+
c.Timeout = 1.2345
7071

7172
uri := c.GetDBUri("test")
72-
test.S(t).ExpectEquals(uri, "gromit:penguin@tcp(myhost:3306)/test?timeout=0.000000s&readTimeout=0.000000s&writeTimeout=0.000000s&interpolateParams=true&autocommit=true&charset=utf8mb4,utf8,latin1&tls=false")
73+
test.S(t).ExpectEquals(uri, `gromit:penguin@tcp(myhost:3306)/test?autocommit=true&charset=utf8mb4,utf8,latin1&interpolateParams=true&tls=false&transaction_isolation="REPEATABLE-READ"&timeout=1.234500s&readTimeout=1.234500s&writeTimeout=1.234500s`)
7374
}
7475

7576
func TestGetDBUriWithTLSSetup(t *testing.T) {
7677
c := NewConnectionConfig()
7778
c.Key = InstanceKey{Hostname: "myhost", Port: 3306}
7879
c.User = "gromit"
7980
c.Password = "penguin"
81+
c.Timeout = 1.2345
8082
c.tlsConfig = &tls.Config{}
8183

8284
uri := c.GetDBUri("test")
83-
test.S(t).ExpectEquals(uri, "gromit:penguin@tcp(myhost:3306)/test?timeout=0.000000s&readTimeout=0.000000s&writeTimeout=0.000000s&interpolateParams=true&autocommit=true&charset=utf8mb4,utf8,latin1&tls=ghost")
85+
test.S(t).ExpectEquals(uri, `gromit:penguin@tcp(myhost:3306)/test?autocommit=true&charset=utf8mb4,utf8,latin1&interpolateParams=true&tls=ghost&transaction_isolation="REPEATABLE-READ"&timeout=1.234500s&readTimeout=1.234500s&writeTimeout=1.234500s`)
8486
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
drop table if exists gh_ost_test;
2+
create table gh_ost_test (
3+
id int unsigned auto_increment,
4+
i int not null,
5+
dt datetime,
6+
primary key(id)
7+
) auto_increment=1;
8+
9+
drop event if exists gh_ost_test;
10+
delimiter ;;
11+
create event gh_ost_test
12+
on schedule every 1 second
13+
starts current_timestamp
14+
ends current_timestamp + interval 60 second
15+
on completion not preserve
16+
enable
17+
do
18+
begin
19+
insert into gh_ost_test values (null, 7, '2010-10-20 10:20:30');
20+
end ;;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
--allow-zero-in-date --alter="change column dt dt datetime not null default '1970-00-00 00:00:00'"

0 commit comments

Comments
 (0)