Repository: hidu/mysql-schema-sync

Issue:
MySQL 8.0.19+ 与MySQL 5.7.x对比
mysql 5.7 表中的int和bigint带宽度,如：int(11)，bigint(20)，而8.0移除宽度，变成了int和bigint。涉及到数字类型的，对比的结果总是有差异。

Discussion:

Solution patch:
diff --git a/internal/db.go b/internal/db.go
index af631bb..b55808c 100644
--- a/internal/db.go
+++ b/internal/db.go
@@ -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) {
diff --git a/internal/util.go b/internal/util.go
index a74573f..5a31e8d 100644
--- a/internal/util.go
+++ b/internal/util.go
@@ -83,3 +83,32 @@ func errString(err error) string {
 	}
 	return xcolor.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
+}



Test patch:
diff --git a/internal/field_test.go b/internal/field_test.go
index b30daa6..86e4db5 100644
--- a/internal/field_test.go
+++ b/internal/field_test.go
@@ -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 {
diff --git a/internal/util_test.go b/internal/util_test.go
new file mode 100644
index 0000000..8cf091c
--- /dev/null
+++ b/internal/util_test.go
@@ -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)
+			}
+		})
+	}
+}