Skip to content
Merged
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
9 changes: 8 additions & 1 deletion internal/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,18 @@ func (f *FieldInfo) Equals(other *FieldInfo) bool {
if f.ColumnName != other.ColumnName ||
f.IsNullAble != other.IsNullAble ||
f.DataType != other.DataType ||
f.ColumnType != other.ColumnType ||
f.Extra != other.Extra {
return false
}

// Compare ColumnType with normalization for integer display width
// MySQL 8.0.19+ removed display width for integer types (int(11) -> int)
normalizedSourceType := normalizeIntegerType(f.ColumnType)
normalizedDestType := normalizeIntegerType(other.ColumnType)
if normalizedSourceType != normalizedDestType {
return false
}

// Compare default values
if (f.ColumnDefault == nil && other.ColumnDefault != nil) ||
(f.ColumnDefault != nil && other.ColumnDefault == nil) {
Expand Down
161 changes: 161 additions & 0 deletions internal/field_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,167 @@ func TestFieldInfo_Equals(t *testing.T) {
},
equal: true,
},
// Integer type display width tests (MySQL 5.7 vs 8.0 compatibility)
{
name: "int(11) vs int should be equal",
field1: &FieldInfo{
ColumnName: "id",
ColumnType: "int(11)",
DataType: "int",
IsNullAble: "NO",
},
field2: &FieldInfo{
ColumnName: "id",
ColumnType: "int",
DataType: "int",
IsNullAble: "NO",
},
equal: true,
},
{
name: "bigint(20) vs bigint should be equal",
field1: &FieldInfo{
ColumnName: "user_id",
ColumnType: "bigint(20)",
DataType: "bigint",
IsNullAble: "NO",
},
field2: &FieldInfo{
ColumnName: "user_id",
ColumnType: "bigint",
DataType: "bigint",
IsNullAble: "NO",
},
equal: true,
},
{
name: "tinyint(1) vs tinyint should be equal",
field1: &FieldInfo{
ColumnName: "is_active",
ColumnType: "tinyint(1)",
DataType: "tinyint",
IsNullAble: "NO",
},
field2: &FieldInfo{
ColumnName: "is_active",
ColumnType: "tinyint",
DataType: "tinyint",
IsNullAble: "NO",
},
equal: true,
},
{
name: "tinyint(4) vs tinyint should be equal",
field1: &FieldInfo{
ColumnName: "status",
ColumnType: "tinyint(4)",
DataType: "tinyint",
IsNullAble: "NO",
},
field2: &FieldInfo{
ColumnName: "status",
ColumnType: "tinyint",
DataType: "tinyint",
IsNullAble: "NO",
},
equal: true,
},
{
name: "int(11) unsigned vs int unsigned should be equal",
field1: &FieldInfo{
ColumnName: "count",
ColumnType: "int(11) unsigned",
DataType: "int",
IsNullAble: "NO",
},
field2: &FieldInfo{
ColumnName: "count",
ColumnType: "int unsigned",
DataType: "int",
IsNullAble: "NO",
},
equal: true,
},
{
name: "bigint(20) unsigned vs bigint unsigned should be equal",
field1: &FieldInfo{
ColumnName: "total",
ColumnType: "bigint(20) unsigned",
DataType: "bigint",
IsNullAble: "NO",
},
field2: &FieldInfo{
ColumnName: "total",
ColumnType: "bigint unsigned",
DataType: "bigint",
IsNullAble: "NO",
},
equal: true,
},
{
name: "int(10) zerofill vs int zerofill should be equal",
field1: &FieldInfo{
ColumnName: "order_id",
ColumnType: "int(10) zerofill",
DataType: "int",
IsNullAble: "NO",
},
field2: &FieldInfo{
ColumnName: "order_id",
ColumnType: "int zerofill",
DataType: "int",
IsNullAble: "NO",
},
equal: true,
},
{
name: "int(10) unsigned zerofill vs int unsigned zerofill should be equal",
field1: &FieldInfo{
ColumnName: "code",
ColumnType: "int(10) unsigned zerofill",
DataType: "int",
IsNullAble: "NO",
},
field2: &FieldInfo{
ColumnName: "code",
ColumnType: "int unsigned zerofill",
DataType: "int",
IsNullAble: "NO",
},
equal: true,
},
{
name: "int vs bigint should not be equal",
field1: &FieldInfo{
ColumnName: "value",
ColumnType: "int",
DataType: "int",
IsNullAble: "NO",
},
field2: &FieldInfo{
ColumnName: "value",
ColumnType: "bigint",
DataType: "bigint",
IsNullAble: "NO",
},
equal: false,
},
{
name: "int unsigned vs int should not be equal (unsigned modifier difference)",
field1: &FieldInfo{
ColumnName: "amount",
ColumnType: "int unsigned",
DataType: "int",
IsNullAble: "NO",
},
field2: &FieldInfo{
ColumnName: "amount",
ColumnType: "int",
DataType: "int",
IsNullAble: "NO",
},
equal: false,
},
}

for _, tt := range tests {
Expand Down
29 changes: 29 additions & 0 deletions internal/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,32 @@ func errString(err error) string {
}
return color.RedString("%s", err.Error())
}

// normalizeIntegerType removes display width from integer types for MySQL 8.0.19+ compatibility.
// MySQL 8.0.19+ deprecated display width for integer types (TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT).
// This function normalizes types like "int(11)" to "int" while preserving modifiers like "unsigned" and "zerofill".
//
// Examples:
// - "int(11)" -> "int"
// - "int(11) unsigned" -> "int unsigned"
// - "bigint(20)" -> "bigint"
// - "tinyint(1)" -> "tinyint"
// - "varchar(255)" -> "varchar(255)" (unchanged, not an integer type)
func normalizeIntegerType(columnType string) string {
// Pattern matches: (tinyint|smallint|mediumint|int|bigint) followed by optional (digits)
// Captures the type name and everything after the display width
re := regexp.MustCompile(`(?i)^(tinyint|smallint|mediumint|int|bigint)\(\d+\)(\s+.+)?$`)

matches := re.FindStringSubmatch(columnType)
if len(matches) > 0 {
// matches[1] is the type name (e.g., "int")
// matches[2] is the modifiers (e.g., " unsigned", " zerofill"), may be empty
if len(matches) > 2 && matches[2] != "" {
return matches[1] + matches[2] // e.g., "int unsigned"
}
return matches[1] // e.g., "int"
}

// Not an integer type with display width, return as-is
return columnType
}
144 changes: 144 additions & 0 deletions internal/util_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package internal

import (
"testing"
)

func TestNormalizeIntegerType(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
// Basic integer types with display width
{
name: "int with display width",
input: "int(11)",
expected: "int",
},
{
name: "bigint with display width",
input: "bigint(20)",
expected: "bigint",
},
{
name: "tinyint with display width",
input: "tinyint(1)",
expected: "tinyint",
},
{
name: "tinyint(4) with display width",
input: "tinyint(4)",
expected: "tinyint",
},
{
name: "smallint with display width",
input: "smallint(5)",
expected: "smallint",
},
{
name: "mediumint with display width",
input: "mediumint(8)",
expected: "mediumint",
},

// Integer types with unsigned modifier
{
name: "int(11) unsigned",
input: "int(11) unsigned",
expected: "int unsigned",
},
{
name: "bigint(20) unsigned",
input: "bigint(20) unsigned",
expected: "bigint unsigned",
},
{
name: "tinyint(1) unsigned",
input: "tinyint(1) unsigned",
expected: "tinyint unsigned",
},

// Integer types with zerofill modifier
{
name: "int(11) zerofill",
input: "int(11) zerofill",
expected: "int zerofill",
},
{
name: "int(10) unsigned zerofill",
input: "int(10) unsigned zerofill",
expected: "int unsigned zerofill",
},

// Integer types without display width (already normalized)
{
name: "int without display width",
input: "int",
expected: "int",
},
{
name: "bigint without display width",
input: "bigint",
expected: "bigint",
},
{
name: "int unsigned without display width",
input: "int unsigned",
expected: "int unsigned",
},

// Non-integer types (should not be affected)
{
name: "varchar with length",
input: "varchar(255)",
expected: "varchar(255)",
},
{
name: "char with length",
input: "char(10)",
expected: "char(10)",
},
{
name: "decimal with precision",
input: "decimal(10,2)",
expected: "decimal(10,2)",
},
{
name: "text type",
input: "text",
expected: "text",
},
{
name: "timestamp",
input: "timestamp",
expected: "timestamp",
},

// Case insensitive matching
{
name: "INT(11) uppercase",
input: "INT(11)",
expected: "INT",
},
{
name: "BIGINT(20) UNSIGNED uppercase",
input: "BIGINT(20) UNSIGNED",
expected: "BIGINT UNSIGNED",
},
{
name: "TinyInt(1) mixed case",
input: "TinyInt(1)",
expected: "TinyInt",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := normalizeIntegerType(tt.input)
if result != tt.expected {
t.Errorf("normalizeIntegerType(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}