Skip to content
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
17 changes: 15 additions & 2 deletions go/logic/applier.go
Original file line number Diff line number Diff line change
Expand Up @@ -801,9 +801,15 @@ func (this *Applier) readMigrationMinValues(tx *gosql.Tx, uniqueKey *sql.UniqueK
return err
}
}
if err := rows.Err(); err != nil {
return err
}
this.migrationContext.Log.Infof("Migration min values: [%s]", this.migrationContext.MigrationRangeMinValues)
if this.migrationContext.MigrationRangeMinValues != nil {
this.migrationContext.MigrationRangeMinValues.NormalizeValues(uniqueKey.Columns)
}
Comment on lines 807 to +810
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BIT normalization is applied for min/max range reads, but the resume-from-checkpoint path populates MigrationIterationRangeMinValues/MaxValues from _ghk via ReadLastCheckpoint() and those values are never normalized. Since the checkpoint table uses the original column types (including BIT), scanned values will still be []uint8, which can reintroduce the incorrect range-comparison behavior (infinite loop / missing rows) after resume. Consider normalizing the checkpoint-derived IterationRangeMin/Max (e.g., inside ReadLastCheckpoint() before returning, or immediately after assigning them during resume).

Copilot uses AI. Check for mistakes.

return rows.Err()
return nil
}

// readMigrationMaxValues returns the maximum values to be iterated on rowcopy
Expand All @@ -826,9 +832,15 @@ func (this *Applier) readMigrationMaxValues(tx *gosql.Tx, uniqueKey *sql.UniqueK
return err
}
}
if err := rows.Err(); err != nil {
return err
}
this.migrationContext.Log.Infof("Migration max values: [%s]", this.migrationContext.MigrationRangeMaxValues)
if this.migrationContext.MigrationRangeMaxValues != nil {
this.migrationContext.MigrationRangeMaxValues.NormalizeValues(uniqueKey.Columns)
}

return rows.Err()
return nil
}

// ReadMigrationRangeValues reads min/max values that will be used for rowcopy.
Expand Down Expand Up @@ -911,6 +923,7 @@ func (this *Applier) CalculateNextIterationRangeEndValues() (hasFurtherRange boo
}
if hasFurtherRange {
this.migrationContext.MigrationIterationRangeMaxValues = iterationRangeMaxValues
this.migrationContext.MigrationIterationRangeMaxValues.NormalizeValues(this.migrationContext.UniqueKey.Columns)
return hasFurtherRange, nil
}
}
Expand Down
3 changes: 3 additions & 0 deletions go/logic/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,9 @@ func (this *Inspector) applyColumnTypes(databaseName, tableName string, columnsL
column.Type = sql.BinaryColumnType
column.BinaryOctetLength = columnOctetLength
}
if strings.HasPrefix(columnType, "bit") {
column.Type = sql.BitColumnType
}
if strings.Contains(extra, " GENERATED") {
column.IsVirtual = true
}
Expand Down
3 changes: 3 additions & 0 deletions go/sql/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ func buildColumnsPreparedValues(columns *ColumnList) []string {
token = fmt.Sprintf("ELT(?, %s)", column.EnumValues)
} else if column.Type == JSONColumnType {
token = "convert(? using utf8mb4)"
} else if column.Type == BitColumnType {
token = "cast(? as unsigned)"
} else {
Comment on lines +61 to 63
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new BitColumnType placeholder rendering (cast(? as unsigned)) in buildColumnsPreparedValues isn't covered by existing unit tests in builder_test.go. Adding a small test that builds a range/ checkpoint query with a BIT column and asserts the generated SQL contains the cast would help prevent regressions in this comparison fix.

Copilot uses AI. Check for mistakes.
token = "?"
}
Expand Down Expand Up @@ -340,6 +342,7 @@ func BuildUniqueKeyRangeEndPreparedQueryViaOffset(databaseName, tableName string
if includeRangeStartValues {
startRangeComparisonSign = GreaterThanOrEqualsComparisonSign
}

rangeStartComparison, rangeExplodedArgs, err := BuildRangePreparedComparison(uniqueKeyColumns, rangeStartArgs, startRangeComparisonSign)
if err != nil {
return "", explodedArgs, err
Expand Down
16 changes: 16 additions & 0 deletions go/sql/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const (
JSONColumnType
FloatColumnType
BinaryColumnType
BitColumnType
)

const maxMediumintUnsigned int32 = 16777215
Expand Down Expand Up @@ -95,6 +96,15 @@ func (this *Column) convertArg(arg interface{}) interface{} {
}
}

// We convert BIT col to uint64 to force correct value comparison.
if this.Type == BitColumnType {
var n uint64
for _, b := range arg2Bytes {
n = (n << 8) | uint64(b)
}
arg = n
}

return arg
}

Expand Down Expand Up @@ -342,6 +352,12 @@ func (this *ColumnValues) AbstractValues() []interface{} {
return this.abstractValues
}

func (this *ColumnValues) NormalizeValues(columns ColumnList) {
for i, col := range columns.Columns() {
this.abstractValues[i] = col.convertArg(this.abstractValues[i])
}
Comment on lines +356 to +358
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NormalizeValues assumes len(this.abstractValues) >= columns.Len() and will panic with an index-out-of-range if called with a mismatched ColumnList (or a ColumnValues created with the wrong length). Consider adding a length check (and returning an error) or iterating up to min(len(values), columns.Len()) with a clear failure path when they differ, so this can't crash the process unexpectedly.

Suggested change
for i, col := range columns.Columns() {
this.abstractValues[i] = col.convertArg(this.abstractValues[i])
}
cols := columns.Columns()
limit := len(cols)
if len(this.abstractValues) < limit {
limit = len(this.abstractValues)
}
for i := 0; i < limit; i++ {
this.abstractValues[i] = cols[i].convertArg(this.abstractValues[i])
}

Copilot uses AI. Check for mistakes.
}

func (this *ColumnValues) StringColumn(index int) string {
val := this.AbstractValues()[index]
if ints, ok := val.([]uint8); ok {
Expand Down
10 changes: 10 additions & 0 deletions go/sql/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,13 @@ func TestConvertArgBinaryColumnNoPaddingWhenFull(t *testing.T) {
require.Equal(t, 20, len(resultBytes))
require.Equal(t, fullValue, resultBytes)
}

func TestConvertArgBitColumn(t *testing.T) {
b := []uint8{0x00, 0x00, 0xa3}
col := Column{
Name: "bit_col",
Type: BitColumnType,
}
result := col.convertArg(b)
require.Equal(t, uint64(163), result)
}
9 changes: 9 additions & 0 deletions localtests/bit-unique-key/create.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
drop table if exists gh_ost_test;
create table gh_ost_test (
`id` bigint not null,
`bit_col` bit not null,
primary key (`id`, `bit_col`)
) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
insert into gh_ost_test values (1, b'1');
insert into gh_ost_test values (2, b'1');
insert into gh_ost_test values (3, b'1');
Loading