Repository: ariga/atlas
Pull request #960 (2022-07-18 18:18:45)

sql/mysql: add support for sized bit types


Patch:
diff --git a/sql/mysql/convert.go b/sql/mysql/convert.go
index 4e8359eb..0c0edec7 100644
--- a/sql/mysql/convert.go
+++ b/sql/mysql/convert.go
@@ -20,6 +20,11 @@ func FormatType(t schema.Type) (string, error) {
 	switch t := t.(type) {
 	case *BitType:
 		f = strings.ToLower(t.T)
+		if t.Size > 1 {
+			// The default size is 1. Thus, both
+			// BIT and BIT(1) are formatted as bit.
+			f += fmt.Sprintf("(%d)", t.Size)
+		}
 	case *schema.BoolType:
 		// Map all flavors to a single form.
 		switch f = strings.ToLower(t.T); f {
@@ -74,7 +79,7 @@ func FormatType(t schema.Type) (string, error) {
 	case *schema.JSONType:
 		f = strings.ToLower(t.T)
 	case *SetType:
-		f = fmt.Sprintf("enum(%s)", formatValues(t.Values))
+		f = fmt.Sprintf("set(%s)", formatValues(t.Values))
 	case *schema.StringType:
 		f = strings.ToLower(t.T)
 		switch f {
@@ -113,7 +118,8 @@ func ParseType(raw string) (schema.Type, error) {
 	switch t := parts[0]; t {
 	case TypeBit:
 		return &BitType{
-			T: t,
+			T:    t,
+			Size: int(size),
 		}, nil
 	// bool and booleans are synonyms for
 	// tinyint with display-width set to 1.
@@ -244,15 +250,6 @@ func ParseType(raw string) (schema.Type, error) {
 	}
 }
 
-// mustFormat calls to FormatType and panics in case of error.
-func mustFormat(t schema.Type) string {
-	s, err := FormatType(t)
-	if err != nil {
-		panic(err)
-	}
-	return s
-}
-
 // formatValues formats ENUM and SET values.
 func formatValues(vs []string) string {
 	values := make([]string, len(vs))
diff --git a/sql/mysql/diff.go b/sql/mysql/diff.go
index 79456181..043eaf1a 100644
--- a/sql/mysql/diff.go
+++ b/sql/mysql/diff.go
@@ -358,8 +358,17 @@ func (d *diff) typeChanged(from, to *schema.Column) (bool, error) {
 	}
 	var changed bool
 	switch fromT := fromT.(type) {
-	case *schema.BinaryType, *schema.BoolType, *schema.DecimalType, *schema.FloatType:
-		changed = mustFormat(fromT) != mustFormat(toT)
+	case *BitType, *schema.BinaryType, *schema.BoolType, *schema.DecimalType, *schema.FloatType,
+		*schema.JSONType, *schema.StringType, *schema.SpatialType, *schema.TimeType:
+		ft, err := FormatType(fromT)
+		if err != nil {
+			return false, err
+		}
+		tt, err := FormatType(toT)
+		if err != nil {
+			return false, err
+		}
+		changed = ft != tt
 	case *schema.EnumType:
 		toT := toT.(*schema.EnumType)
 		changed = !sqlx.ValuesEqual(fromT.Values, toT.Values)
@@ -379,20 +388,6 @@ func (d *diff) typeChanged(from, to *schema.Column) (bool, error) {
 			fromT.T, toT.T = ft[0], tt[0]
 		}
 		changed = fromT.T != toT.T || fromT.Unsigned != toT.Unsigned
-	case *schema.JSONType:
-		toT := toT.(*schema.JSONType)
-		changed = fromT.T != toT.T
-	case *schema.StringType:
-		changed = mustFormat(fromT) != mustFormat(toT)
-	case *schema.SpatialType:
-		toT := toT.(*schema.SpatialType)
-		changed = fromT.T != toT.T
-	case *schema.TimeType:
-		toT := toT.(*schema.TimeType)
-		changed = fromT.T != toT.T
-	case *BitType:
-		toT := toT.(*BitType)
-		changed = fromT.T != toT.T
 	case *SetType:
 		toT := toT.(*SetType)
 		changed = !sqlx.ValuesEqual(fromT.Values, toT.Values)
diff --git a/sql/mysql/inspect.go b/sql/mysql/inspect.go
index 966f10d7..37162efa 100644
--- a/sql/mysql/inspect.go
+++ b/sql/mysql/inspect.go
@@ -601,8 +601,10 @@ func parseColumn(typ string) (parts []string, size int64, unsigned bool, err err
 		if len(parts) > 2 || len(parts) == 2 && !unsigned {
 			size, err = strconv.ParseInt(parts[1], 10, 64)
 		}
-	case TypeBinary, TypeVarBinary, TypeChar, TypeVarchar:
-		size, err = strconv.ParseInt(parts[1], 10, 64)
+	case TypeBit, TypeBinary, TypeVarBinary, TypeChar, TypeVarchar:
+		if len(parts) > 1 {
+			size, err = strconv.ParseInt(parts[1], 10, 64)
+		}
 	}
 	if err != nil {
 		return nil, 0, false, fmt.Errorf("parse %q to int: %w", parts[1], err)
@@ -828,7 +830,8 @@ type (
 	// BitType represents a bit type.
 	BitType struct {
 		schema.Type
-		T string
+		T    string
+		Size int
 	}
 
 	// SetType represents a set type.


Test patch:
diff --git a/internal/integration/mysql_test.go b/internal/integration/mysql_test.go
index ab76225d..c16ecc50 100644
--- a/internal/integration/mysql_test.go
+++ b/internal/integration/mysql_test.go
@@ -827,7 +827,7 @@ create table atlas_types_sanity
 				Columns: []*schema.Column{
 					{
 						Name:    "tBit",
-						Type:    &schema.ColumnType{Type: &mysql.BitType{T: "bit"}, Raw: "bit(10)", Null: true},
+						Type:    &schema.ColumnType{Type: &mysql.BitType{T: "bit", Size: 10}, Raw: "bit(10)", Null: true},
 						Default: &schema.Literal{V: "b'100'"},
 					},
 					{
diff --git a/internal/integration/testdata/mysql/column-bit.txt b/internal/integration/testdata/mysql/column-bit.txt
new file mode 100644
index 00000000..e666bb6f
--- /dev/null
+++ b/internal/integration/testdata/mysql/column-bit.txt
@@ -0,0 +1,25 @@
+apply 1.hcl
+cmpshow t 1.sql
+
+-- 1.hcl --
+schema "$db" {}
+
+table "t" {
+  schema = schema.$db
+  column "c1" {
+    type = bit
+  }
+  column "c2" {
+    type = bit(1)
+  }
+  column "c3" {
+    type = bit(64)
+  }
+}
+
+-- 1.sql --
+CREATE TABLE `t` (
+  `c1` bit(1) NOT NULL,
+  `c2` bit(1) NOT NULL,
+  `c3` bit(64) NOT NULL
+)
diff --git a/internal/integration/tidb_test.go b/internal/integration/tidb_test.go
index f50275ca..b20483d7 100644
--- a/internal/integration/tidb_test.go
+++ b/internal/integration/tidb_test.go
@@ -770,7 +770,7 @@ create table atlas_types_sanity
 				Columns: []*schema.Column{
 					{
 						Name:    "tBit",
-						Type:    &schema.ColumnType{Type: &mysql.BitType{T: "bit"}, Raw: "bit(10) unsigned", Null: true},
+						Type:    &schema.ColumnType{Type: &mysql.BitType{T: "bit", Size: 10}, Raw: "bit(10) unsigned", Null: true},
 						Default: &schema.Literal{V: "b'1000000001'"},
 					},
 					{
diff --git a/sql/mysql/inspect_test.go b/sql/mysql/inspect_test.go
index d3a37d23..e50d2939 100644
--- a/sql/mysql/inspect_test.go
+++ b/sql/mysql/inspect_test.go
@@ -295,6 +295,34 @@ func TestDriver_InspectTable(t *testing.T) {
 				}, t.Columns)
 			},
 		},
+		{
+			name: "bit type",
+			before: func(m mock) {
+				m.tableExists("public", "users", true)
+				m.ExpectQuery(queryColumns).
+					WithArgs("public", "users").
+					WillReturnRows(sqltest.Rows(`
++------------+------------+-------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+---------------------------+
+| TABLE_NAME |COLUMN_NAME | COLUMN_TYPE | COLUMN_COMMENT | IS_NULLABLE | COLUMN_KEY | COLUMN_DEFAULT | EXTRA | CHARACTER_SET_NAME | COLLATION_NAME | GENERATION_EXPRESSION     |
++------------+------------+-------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+---------------------------+
+| users      |c1          | bit        |                | NO          |            | NULL           |       | NULL               | NULL           | NULL                       |
+| users      |c2          | bit(1)     |                | NO          |            | NULL           |       | NULL               | NULL           | NULL                       |
+| users      |c3          | bit(2)     |                | NO          |            | NULL           |       | NULL               | NULL           | NULL                       |
++------------+------------+-------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+---------------------------+
+`))
+				m.noIndexes()
+				m.noFKs()
+			},
+			expect: func(require *require.Assertions, t *schema.Table, err error) {
+				require.NoError(err)
+				require.Equal("users", t.Name)
+				require.EqualValues([]*schema.Column{
+					{Name: "c1", Type: &schema.ColumnType{Raw: "bit", Type: &BitType{T: "bit"}}},
+					{Name: "c2", Type: &schema.ColumnType{Raw: "bit(1)", Type: &BitType{T: "bit", Size: 1}}},
+					{Name: "c3", Type: &schema.ColumnType{Raw: "bit(2)", Type: &BitType{T: "bit", Size: 2}}},
+				}, t.Columns)
+			},
+		},
 		{
 			name: "string types",
 			before: func(m mock) {
diff --git a/sql/mysql/sqlspec_test.go b/sql/mysql/sqlspec_test.go
index 410e155a..e988d569 100644
--- a/sql/mysql/sqlspec_test.go
+++ b/sql/mysql/sqlspec_test.go
@@ -909,9 +909,13 @@ func TestTypes(t *testing.T) {
 			expected: &schema.EnumType{T: TypeEnum, Values: []string{"on", "off"}},
 		},
 		{
-			typeExpr: "bit(10)",
+			typeExpr: "bit",
 			expected: &BitType{T: TypeBit},
 		},
+		{
+			typeExpr: "bit(10)",
+			expected: &BitType{T: TypeBit, Size: 10},
+		},
 		{
 			typeExpr: "int(10)",
 			expected: &schema.IntegerType{T: TypeInt},