Skip to content

Commit bee009b

Browse files
Allow zero in dates (#1161)
* Merge pull request #31 from openark/zero-date Support zero date and zero in date, via dedicated command line flag * Merge pull request #32 from openark/existing-date-with-zero Support tables with existing zero dates * Remove un-needed ignore_versions file * Fix new lint errors from golang-ci update Co-authored-by: Shlomi Noach <[email protected]>
1 parent 7d8e4e8 commit bee009b

File tree

16 files changed

+172
-21
lines changed

16 files changed

+172
-21
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: 4 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.

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: 1 addition & 0 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.")

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)
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'"
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
set session sql_mode='';
2+
drop table if exists gh_ost_test;
3+
create table gh_ost_test (
4+
id int unsigned auto_increment,
5+
i int not null,
6+
dt datetime not null default '1970-00-00 00:00:00',
7+
primary key(id)
8+
) auto_increment=1;
9+
10+
drop event if exists gh_ost_test;
11+
delimiter ;;
12+
create event gh_ost_test
13+
on schedule every 1 second
14+
starts current_timestamp
15+
ends current_timestamp + interval 60 second
16+
on completion not preserve
17+
enable
18+
do
19+
begin
20+
insert into gh_ost_test values (null, 7, '2010-10-20 10:20:30');
21+
end ;;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
--allow-zero-in-date --alter="engine=innodb"
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+
Invalid default value for 'dt'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
--alter="change column dt dt datetime not null default '1970-00-00 00:00:00'"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
(5.5|5.6)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
set session sql_mode='';
2+
drop table if exists gh_ost_test;
3+
create table gh_ost_test (
4+
id int unsigned auto_increment,
5+
i int not null,
6+
dt datetime not null default '1970-00-00 00:00:00',
7+
primary key(id)
8+
) auto_increment=1;
9+
10+
drop event if exists gh_ost_test;
11+
delimiter ;;
12+
create event gh_ost_test
13+
on schedule every 1 second
14+
starts current_timestamp
15+
ends current_timestamp + interval 60 second
16+
on completion not preserve
17+
enable
18+
do
19+
begin
20+
insert into gh_ost_test values (null, 7, '2010-10-20 10:20:30');
21+
end ;;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Invalid default value for 'dt'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
--alter="engine=innodb"

0 commit comments

Comments
 (0)