Skip to content

Commit a284701

Browse files
author
Shlomi Noach
authored
Merge branch 'master' into nm-refactor-migration-context
2 parents 0a7e713 + 3fc3446 commit a284701

31 files changed

Lines changed: 225 additions & 48 deletions

File tree

doc/requirements-and-limitations.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ The `SUPER` privilege is required for `STOP SLAVE`, `START SLAVE` operations. Th
2828

2929
- MySQL 5.7 generated columns are not supported. They may be supported in the future.
3030

31-
- MySQL 5.7 `JSON` columns are not supported. They are likely to be supported shortly.
31+
- MySQL 5.7 `POINT` column type is not supported.
32+
33+
- MySQL 5.7 `JSON` columns are supported but not as part of `PRIMARY KEY`
3234

3335
- The two _before_ & _after_ tables must share a `PRIMARY KEY` or other `UNIQUE KEY`. This key will be used by `gh-ost` to iterate through the table rows when copying. [Read more](shared-key.md)
3436
- The migration key must not include columns with NULL values. This means either:

go/cmd/gh-ost/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func main() {
6363
flag.BoolVar(&migrationContext.AllowedRunningOnMaster, "allow-on-master", false, "allow this migration to run directly on master. Preferably it would run on a replica")
6464
flag.BoolVar(&migrationContext.AllowedMasterMaster, "allow-master-master", false, "explicitly allow running in a master-master setup")
6565
flag.BoolVar(&migrationContext.NullableUniqueKeyAllowed, "allow-nullable-unique-key", false, "allow gh-ost to migrate based on a unique key with nullable columns. As long as no NULL values exist, this should be OK. If NULL values exist in chosen key, data may be corrupted. Use at your own risk!")
66-
flag.BoolVar(&migrationContext.ApproveRenamedColumns, "approve-renamed-columns", false, "in case your `ALTER` statement renames columns, gh-ost will note that and offer its interpretation of the rename. By default gh-ost does not proceed to execute. This flag approves that gh-ost's interpretation si correct")
66+
flag.BoolVar(&migrationContext.ApproveRenamedColumns, "approve-renamed-columns", false, "in case your `ALTER` statement renames columns, gh-ost will note that and offer its interpretation of the rename. By default gh-ost does not proceed to execute. This flag approves that gh-ost's interpretation is correct")
6767
flag.BoolVar(&migrationContext.SkipRenamedColumns, "skip-renamed-columns", false, "in case your `ALTER` statement renames columns, gh-ost will note that and offer its interpretation of the rename. By default gh-ost does not proceed to execute. This flag tells gh-ost to skip the renamed columns, i.e. to treat what gh-ost thinks are renamed columns as unrelated columns. NOTE: you may lose column data")
6868
flag.BoolVar(&migrationContext.IsTungsten, "tungsten", false, "explicitly let gh-ost know that you are running on a tungsten-replication based topology (you are likely to also provide --assume-master-host)")
6969
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")

go/logic/applier.go

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ func (this *Applier) CreateChangelogTable() error {
202202
id bigint auto_increment,
203203
last_update timestamp not null DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
204204
hint varchar(64) charset ascii not null,
205-
value varchar(255) charset ascii not null,
205+
value varchar(4096) charset ascii not null,
206206
primary key(id),
207207
unique key hint_uidx(hint)
208208
) auto_increment=256
@@ -403,35 +403,41 @@ func (this *Applier) CalculateNextIterationRangeEndValues() (hasFurtherRange boo
403403
if this.migrationContext.MigrationIterationRangeMinValues == nil {
404404
this.migrationContext.MigrationIterationRangeMinValues = this.migrationContext.MigrationRangeMinValues
405405
}
406-
query, explodedArgs, err := sql.BuildUniqueKeyRangeEndPreparedQuery(
407-
this.migrationContext.DatabaseName,
408-
this.migrationContext.OriginalTableName,
409-
&this.migrationContext.UniqueKey.Columns,
410-
this.migrationContext.MigrationIterationRangeMinValues.AbstractValues(),
411-
this.migrationContext.MigrationRangeMaxValues.AbstractValues(),
412-
atomic.LoadInt64(&this.migrationContext.ChunkSize),
413-
this.migrationContext.GetIteration() == 0,
414-
fmt.Sprintf("iteration:%d", this.migrationContext.GetIteration()),
415-
)
416-
if err != nil {
417-
return hasFurtherRange, err
418-
}
419-
rows, err := this.db.Query(query, explodedArgs...)
420-
if err != nil {
421-
return hasFurtherRange, err
422-
}
423-
iterationRangeMaxValues := sql.NewColumnValues(this.migrationContext.UniqueKey.Len())
424-
for rows.Next() {
425-
if err = rows.Scan(iterationRangeMaxValues.ValuesPointers...); err != nil {
406+
for i := 0; i < 2; i++ {
407+
buildFunc := sql.BuildUniqueKeyRangeEndPreparedQueryViaOffset
408+
if i == 1 {
409+
buildFunc = sql.BuildUniqueKeyRangeEndPreparedQueryViaTemptable
410+
}
411+
query, explodedArgs, err := buildFunc(
412+
this.migrationContext.DatabaseName,
413+
this.migrationContext.OriginalTableName,
414+
&this.migrationContext.UniqueKey.Columns,
415+
this.migrationContext.MigrationIterationRangeMinValues.AbstractValues(),
416+
this.migrationContext.MigrationRangeMaxValues.AbstractValues(),
417+
atomic.LoadInt64(&this.migrationContext.ChunkSize),
418+
this.migrationContext.GetIteration() == 0,
419+
fmt.Sprintf("iteration:%d", this.migrationContext.GetIteration()),
420+
)
421+
if err != nil {
426422
return hasFurtherRange, err
427423
}
428-
hasFurtherRange = true
429-
}
430-
if !hasFurtherRange {
431-
log.Debugf("Iteration complete: no further range to iterate")
432-
return hasFurtherRange, nil
424+
rows, err := this.db.Query(query, explodedArgs...)
425+
if err != nil {
426+
return hasFurtherRange, err
427+
}
428+
iterationRangeMaxValues := sql.NewColumnValues(this.migrationContext.UniqueKey.Len())
429+
for rows.Next() {
430+
if err = rows.Scan(iterationRangeMaxValues.ValuesPointers...); err != nil {
431+
return hasFurtherRange, err
432+
}
433+
hasFurtherRange = true
434+
}
435+
if hasFurtherRange {
436+
this.migrationContext.MigrationIterationRangeMaxValues = iterationRangeMaxValues
437+
return hasFurtherRange, nil
438+
}
433439
}
434-
this.migrationContext.MigrationIterationRangeMaxValues = iterationRangeMaxValues
440+
log.Debugf("Iteration complete: no further range to iterate")
435441
return hasFurtherRange, nil
436442
}
437443

go/logic/inspect.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,33 @@ func (this *Inspector) inspectOriginalAndGhostTables() (err error) {
121121
if err != nil {
122122
return err
123123
}
124-
if len(sharedUniqueKeys) == 0 {
124+
for i, sharedUniqueKey := range sharedUniqueKeys {
125+
this.applyColumnTypes(this.migrationContext.DatabaseName, this.migrationContext.OriginalTableName, &sharedUniqueKey.Columns)
126+
uniqueKeyIsValid := true
127+
for _, column := range sharedUniqueKey.Columns.Columns() {
128+
switch column.Type {
129+
case sql.FloatColumnType:
130+
{
131+
log.Warning("Will not use %+v as shared key due to FLOAT data type", sharedUniqueKey.Name)
132+
uniqueKeyIsValid = false
133+
}
134+
case sql.JSONColumnType:
135+
{
136+
// Noteworthy that at this time MySQL does not allow JSON indexing anyhow, but this code
137+
// will remain in place to potentially handle the future case where JSON is supported in indexes.
138+
log.Warning("Will not use %+v as shared key due to JSON data type", sharedUniqueKey.Name)
139+
uniqueKeyIsValid = false
140+
}
141+
}
142+
}
143+
if uniqueKeyIsValid {
144+
this.migrationContext.UniqueKey = sharedUniqueKeys[i]
145+
break
146+
}
147+
}
148+
if this.migrationContext.UniqueKey == nil {
125149
return fmt.Errorf("No shared unique key can be found after ALTER! Bailing out")
126150
}
127-
this.migrationContext.UniqueKey = sharedUniqueKeys[0]
128151
log.Infof("Chosen shared unique key is %s", this.migrationContext.UniqueKey.Name)
129152
if this.migrationContext.UniqueKey.HasNullable {
130153
if this.migrationContext.NullableUniqueKeyAllowed {
@@ -169,6 +192,9 @@ func (this *Inspector) inspectOriginalAndGhostTables() (err error) {
169192

170193
// validateConnection issues a simple can-connect to MySQL
171194
func (this *Inspector) validateConnection() error {
195+
if len(this.connectionConfig.Password) > mysql.MaxReplicationPasswordLength {
196+
return fmt.Errorf("MySQL replication length limited to 32 characters. See https://dev.mysql.com/doc/refman/5.7/en/assigning-passwords.html")
197+
}
172198
query := `select @@global.port, @@global.version`
173199
var port int
174200
if err := this.db.QueryRow(query).Scan(&port, &this.migrationContext.InspectorMySQLVersion); err != nil {
@@ -545,6 +571,16 @@ func (this *Inspector) applyColumnTypes(databaseName, tableName string, columnsL
545571
columnsList.GetColumn(columnName).Type = sql.DateTimeColumnType
546572
}
547573
}
574+
if strings.Contains(columnType, "json") {
575+
for _, columnsList := range columnsLists {
576+
columnsList.GetColumn(columnName).Type = sql.JSONColumnType
577+
}
578+
}
579+
if strings.Contains(columnType, "float") {
580+
for _, columnsList := range columnsLists {
581+
columnsList.GetColumn(columnName).Type = sql.FloatColumnType
582+
}
583+
}
548584
if strings.HasPrefix(columnType, "enum") {
549585
for _, columnsList := range columnsLists {
550586
columnsList.GetColumn(columnName).Type = sql.EnumColumnType

go/mysql/connection.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,5 @@ func (this *ConnectionConfig) GetDBUri(databaseName string) string {
5656
// Wrap IPv6 literals in square brackets
5757
hostname = fmt.Sprintf("[%s]", hostname)
5858
}
59-
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4,utf8,latin1", this.User, this.Password, hostname, this.Key.Port, databaseName)
59+
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?interpolateParams=true&autocommit=true&charset=utf8mb4,utf8,latin1", this.User, this.Password, hostname, this.Key.Port, databaseName)
6060
}

go/mysql/utils.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
)
1818

1919
const MaxTableNameLength = 64
20+
const MaxReplicationPasswordLength = 32
2021

2122
type ReplicationLagResult struct {
2223
Key InstanceKey

go/sql/builder.go

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ func buildColumnsPreparedValues(columns *ColumnList) []string {
3838
var token string
3939
if column.timezoneConversion != nil {
4040
token = fmt.Sprintf("convert_tz(?, '%s', '%s')", column.timezoneConversion.ToTimezone, "+00:00")
41+
} else if column.Type == JSONColumnType {
42+
token = "convert(? using utf8mb4)"
4143
} else {
4244
token = "?"
4345
}
@@ -106,6 +108,8 @@ func BuildSetPreparedClause(columns *ColumnList) (result string, err error) {
106108
var setToken string
107109
if column.timezoneConversion != nil {
108110
setToken = fmt.Sprintf("%s=convert_tz(?, '%s', '%s')", EscapeName(column.Name), column.timezoneConversion.ToTimezone, "+00:00")
111+
} else if column.Type == JSONColumnType {
112+
setToken = fmt.Sprintf("%s=convert(? using utf8mb4)", EscapeName(column.Name))
109113
} else {
110114
setToken = fmt.Sprintf("%s=?", EscapeName(column.Name))
111115
}
@@ -231,7 +235,62 @@ func BuildRangeInsertPreparedQuery(databaseName, originalTableName, ghostTableNa
231235
return BuildRangeInsertQuery(databaseName, originalTableName, ghostTableName, sharedColumns, mappedSharedColumns, uniqueKey, uniqueKeyColumns, rangeStartValues, rangeEndValues, rangeStartArgs, rangeEndArgs, includeRangeStartValues, transactionalTable)
232236
}
233237

234-
func BuildUniqueKeyRangeEndPreparedQuery(databaseName, tableName string, uniqueKeyColumns *ColumnList, rangeStartArgs, rangeEndArgs []interface{}, chunkSize int64, includeRangeStartValues bool, hint string) (result string, explodedArgs []interface{}, err error) {
238+
func BuildUniqueKeyRangeEndPreparedQueryViaOffset(databaseName, tableName string, uniqueKeyColumns *ColumnList, rangeStartArgs, rangeEndArgs []interface{}, chunkSize int64, includeRangeStartValues bool, hint string) (result string, explodedArgs []interface{}, err error) {
239+
if uniqueKeyColumns.Len() == 0 {
240+
return "", explodedArgs, fmt.Errorf("Got 0 columns in BuildUniqueKeyRangeEndPreparedQuery")
241+
}
242+
databaseName = EscapeName(databaseName)
243+
tableName = EscapeName(tableName)
244+
245+
var startRangeComparisonSign ValueComparisonSign = GreaterThanComparisonSign
246+
if includeRangeStartValues {
247+
startRangeComparisonSign = GreaterThanOrEqualsComparisonSign
248+
}
249+
rangeStartComparison, rangeExplodedArgs, err := BuildRangePreparedComparison(uniqueKeyColumns, rangeStartArgs, startRangeComparisonSign)
250+
if err != nil {
251+
return "", explodedArgs, err
252+
}
253+
explodedArgs = append(explodedArgs, rangeExplodedArgs...)
254+
rangeEndComparison, rangeExplodedArgs, err := BuildRangePreparedComparison(uniqueKeyColumns, rangeEndArgs, LessThanOrEqualsComparisonSign)
255+
if err != nil {
256+
return "", explodedArgs, err
257+
}
258+
explodedArgs = append(explodedArgs, rangeExplodedArgs...)
259+
260+
uniqueKeyColumnNames := duplicateNames(uniqueKeyColumns.Names())
261+
uniqueKeyColumnAscending := make([]string, len(uniqueKeyColumnNames), len(uniqueKeyColumnNames))
262+
uniqueKeyColumnDescending := make([]string, len(uniqueKeyColumnNames), len(uniqueKeyColumnNames))
263+
for i, column := range uniqueKeyColumns.Columns() {
264+
uniqueKeyColumnNames[i] = EscapeName(uniqueKeyColumnNames[i])
265+
if column.Type == EnumColumnType {
266+
uniqueKeyColumnAscending[i] = fmt.Sprintf("concat(%s) asc", uniqueKeyColumnNames[i])
267+
uniqueKeyColumnDescending[i] = fmt.Sprintf("concat(%s) desc", uniqueKeyColumnNames[i])
268+
} else {
269+
uniqueKeyColumnAscending[i] = fmt.Sprintf("%s asc", uniqueKeyColumnNames[i])
270+
uniqueKeyColumnDescending[i] = fmt.Sprintf("%s desc", uniqueKeyColumnNames[i])
271+
}
272+
}
273+
result = fmt.Sprintf(`
274+
select /* gh-ost %s.%s %s */
275+
%s
276+
from
277+
%s.%s
278+
where %s and %s
279+
order by
280+
%s
281+
limit 1
282+
offset %d
283+
`, databaseName, tableName, hint,
284+
strings.Join(uniqueKeyColumnNames, ", "),
285+
databaseName, tableName,
286+
rangeStartComparison, rangeEndComparison,
287+
strings.Join(uniqueKeyColumnAscending, ", "),
288+
(chunkSize - 1),
289+
)
290+
return result, explodedArgs, nil
291+
}
292+
293+
func BuildUniqueKeyRangeEndPreparedQueryViaTemptable(databaseName, tableName string, uniqueKeyColumns *ColumnList, rangeStartArgs, rangeEndArgs []interface{}, chunkSize int64, includeRangeStartValues bool, hint string) (result string, explodedArgs []interface{}, err error) {
235294
if uniqueKeyColumns.Len() == 0 {
236295
return "", explodedArgs, fmt.Errorf("Got 0 columns in BuildUniqueKeyRangeEndPreparedQuery")
237296
}

go/sql/builder_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ func TestBuildUniqueKeyRangeEndPreparedQuery(t *testing.T) {
283283
rangeStartArgs := []interface{}{3, 17}
284284
rangeEndArgs := []interface{}{103, 117}
285285

286-
query, explodedArgs, err := BuildUniqueKeyRangeEndPreparedQuery(databaseName, originalTableName, uniqueKeyColumns, rangeStartArgs, rangeEndArgs, chunkSize, false, "test")
286+
query, explodedArgs, err := BuildUniqueKeyRangeEndPreparedQueryViaTemptable(databaseName, originalTableName, uniqueKeyColumns, rangeStartArgs, rangeEndArgs, chunkSize, false, "test")
287287
test.S(t).ExpectNil(err)
288288
expected := `
289289
select /* gh-ost mydb.tbl test */ name, position

go/sql/types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ const (
2020
DateTimeColumnType = iota
2121
EnumColumnType = iota
2222
MediumIntColumnType = iota
23+
JSONColumnType = iota
24+
FloatColumnType = iota
2325
)
2426

2527
const maxMediumintUnsigned int32 = 16777215

localtests/datetime-to-timestamp-pk-fail/create.sql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ create table gh_ost_test (
33
id int unsigned auto_increment,
44
i int not null,
55
ts0 timestamp default current_timestamp,
6-
ts1 timestamp,
6+
ts1 timestamp null,
77
dt2 datetime,
8-
t datetime,
8+
t datetime default current_timestamp,
99
updated tinyint unsigned default 0,
1010
primary key(id, t),
1111
key i_idx(i)

0 commit comments

Comments
 (0)