Repository: ariga/atlas (go)

Issue:
MySQL - Atlas fails to inpect schema when a column is specified as an unsigned float
It seems that when you create a column in mysql with an unsigned float, Atlas cannot parse its precision in sql/mysql/convert.go and throws a lovely error message like below:

Error: mysql: parse precision "unsigned"



Pull request description:
sql/mysql: add support for float and decimal unsigned attribute
Fix #545 

Patch:
diff --git a/sql/mysql/convert.go b/sql/mysql/convert.go
index 6c420780..46a1e443 100644
--- a/sql/mysql/convert.go
+++ b/sql/mysql/convert.go
@@ -51,6 +51,9 @@ func FormatType(t schema.Type) (string, error) {
 		default:
 			f = fmt.Sprintf("decimal(%d,%d)", p, s)
 		}
+		if t.Unsigned {
+			f += " unsigned"
+		}
 	case *schema.EnumType:
 		f = fmt.Sprintf("enum(%s)", formatValues(t.Values))
 	case *schema.FloatType:
@@ -60,6 +63,9 @@ func FormatType(t schema.Type) (string, error) {
 		if f == TypeFloat && t.Precision > 24 || f == TypeReal {
 			f = TypeDouble
 		}
+		if t.Unsigned {
+			f += " unsigned"
+		}
 	case *schema.IntegerType:
 		f = strings.ToLower(t.T)
 		if t.Unsigned {
@@ -142,16 +148,17 @@ func ParseType(raw string) (schema.Type, error) {
 		return ft, nil
 	case TypeNumeric, TypeDecimal:
 		dt := &schema.DecimalType{
-			T: t,
+			T:        t,
+			Unsigned: unsigned,
 		}
-		if len(parts) > 1 {
+		if len(parts) > 1 && parts[1] != "unsigned" {
 			p, err := strconv.ParseInt(parts[1], 10, 64)
 			if err != nil {
 				return nil, fmt.Errorf("parse precision %q", parts[1])
 			}
 			dt.Precision = int(p)
 		}
-		if len(parts) > 2 {
+		if len(parts) > 2 && parts[2] != "unsigned" {
 			s, err := strconv.ParseInt(parts[2], 10, 64)
 			if err != nil {
 				return nil, fmt.Errorf("parse scale %q", parts[1])
@@ -161,9 +168,10 @@ func ParseType(raw string) (schema.Type, error) {
 		return dt, nil
 	case TypeFloat, TypeDouble, TypeReal:
 		ft := &schema.FloatType{
-			T: t,
+			T:        t,
+			Unsigned: unsigned,
 		}
-		if len(parts) > 1 {
+		if len(parts) > 1 && parts[1] != "unsigned" {
 			p, err := strconv.ParseInt(parts[1], 10, 64)
 			if err != nil {
 				return nil, fmt.Errorf("parse precision %q", parts[1])
diff --git a/sql/mysql/inspect.go b/sql/mysql/inspect.go
index 232f10a7..a17c4e10 100644
--- a/sql/mysql/inspect.go
+++ b/sql/mysql/inspect.go
@@ -552,7 +552,8 @@ func parseColumn(typ string) (parts []string, size int64, unsigned bool, err err
 	switch parts = strings.FieldsFunc(typ, func(r rune) bool {
 		return r == '(' || r == ')' || r == ' ' || r == ','
 	}); parts[0] {
-	case TypeTinyInt, TypeSmallInt, TypeMediumInt, TypeInt, TypeBigInt:
+	case TypeTinyInt, TypeSmallInt, TypeMediumInt, TypeInt, TypeBigInt,
+		TypeDecimal, TypeNumeric, TypeFloat, TypeDouble, TypeReal:
 		if attr := parts[len(parts)-1]; attr == "unsigned" || attr == "zerofill" {
 			unsigned = true
 		}
diff --git a/sql/mysql/sqlspec.go b/sql/mysql/sqlspec.go
index 9b544bd0..4e594245 100644
--- a/sql/mysql/sqlspec.go
+++ b/sql/mysql/sqlspec.go
@@ -279,11 +279,11 @@ var TypeRegistry = specutil.NewRegistry(
 		specutil.TypeSpec(TypeSmallInt, specutil.WithAttributes(unsignedTypeAttr(), specutil.SizeTypeAttr(false))),
 		specutil.TypeSpec(TypeMediumInt, specutil.WithAttributes(unsignedTypeAttr(), specutil.SizeTypeAttr(false))),
 		specutil.TypeSpec(TypeBigInt, specutil.WithAttributes(unsignedTypeAttr(), specutil.SizeTypeAttr(false))),
-		specutil.TypeSpec(TypeDecimal, specutil.WithAttributes(&schemaspec.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false}, &schemaspec.TypeAttr{Name: "scale", Kind: reflect.Int, Required: false})),
-		specutil.TypeSpec(TypeNumeric, specutil.WithAttributes(&schemaspec.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false}, &schemaspec.TypeAttr{Name: "scale", Kind: reflect.Int, Required: false})),
-		specutil.TypeSpec(TypeFloat, specutil.WithAttributes(&schemaspec.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false}, &schemaspec.TypeAttr{Name: "scale", Kind: reflect.Int, Required: false})),
-		specutil.TypeSpec(TypeDouble, specutil.WithAttributes(&schemaspec.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false}, &schemaspec.TypeAttr{Name: "scale", Kind: reflect.Int, Required: false})),
-		specutil.TypeSpec(TypeReal, specutil.WithAttributes(&schemaspec.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false}, &schemaspec.TypeAttr{Name: "scale", Kind: reflect.Int, Required: false})),
+		specutil.TypeSpec(TypeDecimal, specutil.WithAttributes(unsignedTypeAttr(), &schemaspec.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false}, &schemaspec.TypeAttr{Name: "scale", Kind: reflect.Int, Required: false})),
+		specutil.TypeSpec(TypeNumeric, specutil.WithAttributes(unsignedTypeAttr(), &schemaspec.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false}, &schemaspec.TypeAttr{Name: "scale", Kind: reflect.Int, Required: false})),
+		specutil.TypeSpec(TypeFloat, specutil.WithAttributes(unsignedTypeAttr(), &schemaspec.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false}, &schemaspec.TypeAttr{Name: "scale", Kind: reflect.Int, Required: false})),
+		specutil.TypeSpec(TypeDouble, specutil.WithAttributes(unsignedTypeAttr(), &schemaspec.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false}, &schemaspec.TypeAttr{Name: "scale", Kind: reflect.Int, Required: false})),
+		specutil.TypeSpec(TypeReal, specutil.WithAttributes(unsignedTypeAttr(), &schemaspec.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false}, &schemaspec.TypeAttr{Name: "scale", Kind: reflect.Int, Required: false})),
 		specutil.TypeSpec(TypeTimestamp, specutil.WithAttributes(&schemaspec.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false})),
 		specutil.TypeSpec(TypeDate, specutil.WithAttributes(&schemaspec.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false})),
 		specutil.TypeSpec(TypeTime, specutil.WithAttributes(&schemaspec.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false})),
diff --git a/sql/schema/dsl.go b/sql/schema/dsl.go
index 7207108e..b28acfba 100644
--- a/sql/schema/dsl.go
+++ b/sql/schema/dsl.go
@@ -350,6 +350,13 @@ func DecimalScale(scale int) DecimalOption {
 	}
 }
 
+// DecimalUnsigned configures the unsigned of the float type.
+func DecimalUnsigned(unsigned bool) DecimalOption {
+	return func(b *DecimalType) {
+		b.Unsigned = unsigned
+	}
+}
+
 // NewDecimalColumn creates a new DecimalType column.
 func NewDecimalColumn(name, typ string, opts ...DecimalOption) *Column {
 	t := &DecimalType{T: typ}
@@ -375,6 +382,13 @@ func FloatPrecision(precision int) FloatOption {
 	}
 }
 
+// FloatUnsigned configures the unsigned of the float type.
+func FloatUnsigned(unsigned bool) FloatOption {
+	return func(b *FloatType) {
+		b.Unsigned = unsigned
+	}
+}
+
 // NewFloatColumn creates a new FloatType column.
 func NewFloatColumn(name, typ string, opts ...FloatOption) *Column {
 	t := &FloatType{T: typ}
diff --git a/sql/schema/schema.go b/sql/schema/schema.go
index 15ce189c..7793fa04 100644
--- a/sql/schema/schema.go
+++ b/sql/schema/schema.go
@@ -220,11 +220,13 @@ type (
 		T         string
 		Precision int
 		Scale     int
+		Unsigned  bool
 	}
 
 	// FloatType represents a floating-point type that stores approximate numeric values.
 	FloatType struct {
 		T         string
+		Unsigned  bool
 		Precision int
 	}
 


Test patch:
diff --git a/sql/internal/specutil/types_test.go b/sql/internal/specutil/types_test.go
index 6b54e247..ed9bae7a 100644
--- a/sql/internal/specutil/types_test.go
+++ b/sql/internal/specutil/types_test.go
@@ -34,6 +34,15 @@ func TestTypePrint(t *testing.T) {
 			typ:      &schemaspec.Type{T: "int", Attrs: []*schemaspec.Attr{LitAttr("unsigned", "true")}},
 			expected: "int unsigned",
 		},
+		{
+			spec: &schemaspec.TypeSpec{
+				Name:       "float",
+				T:          "float",
+				Attributes: []*schemaspec.TypeAttr{unsignedTypeAttr()},
+			},
+			typ:      &schemaspec.Type{T: "float", Attrs: []*schemaspec.Attr{LitAttr("unsigned", "true")}},
+			expected: "float unsigned",
+		},
 		{
 			spec: &schemaspec.TypeSpec{
 				T:    "varchar",
diff --git a/sql/mysql/inspect_test.go b/sql/mysql/inspect_test.go
index 05f6b690..342bd79a 100644
--- a/sql/mysql/inspect_test.go
+++ b/sql/mysql/inspect_test.go
@@ -207,12 +207,14 @@ func TestDriver_InspectTable(t *testing.T) {
 				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 |
-+------------+--------------+---------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
-| users      |  d1          | decimal(10,2) |                | NO          |            | 10.20          |       | NULL               | NULL           |
-| users      |  d2          | decimal(10,0) |                | NO          |            | 10             |       | NULL               | NULL           |
-+------------+-------------+---------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
++------------+--------------+------------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
+| table_name |  column_name |      column_type       | column_comment | is_nullable | column_key | column_default | extra | character_set_name | collation_name |
++------------+--------------+------------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
+| users      |  d1          | decimal(10,2)          |                | NO          |            | 10.20          |       | NULL               | NULL           |
+| users      |  d2          | decimal(10,0)          |                | NO          |            | 10             |       | NULL               | NULL           |
+| users      |  d3          | decimal(10,2) unsigned |                | NO          |            | 10.20          |       | NULL               | NULL           |
+| users      |  d4          | decimal(10,0) unsigned |                | NO          |            | 10             |       | NULL               | NULL           |
++------------+-------------+-------------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
 `))
 				m.noIndexes()
 				m.noFKs()
@@ -223,6 +225,8 @@ func TestDriver_InspectTable(t *testing.T) {
 				require.EqualValues([]*schema.Column{
 					{Name: "d1", Type: &schema.ColumnType{Raw: "decimal(10,2)", Type: &schema.DecimalType{T: "decimal", Precision: 10, Scale: 2}}, Default: &schema.Literal{V: "10.20"}},
 					{Name: "d2", Type: &schema.ColumnType{Raw: "decimal(10,0)", Type: &schema.DecimalType{T: "decimal", Precision: 10, Scale: 0}}, Default: &schema.Literal{V: "10"}},
+					{Name: "d3", Type: &schema.ColumnType{Raw: "decimal(10,2) unsigned", Type: &schema.DecimalType{T: "decimal", Precision: 10, Scale: 2, Unsigned: true}}, Default: &schema.Literal{V: "10.20"}},
+					{Name: "d4", Type: &schema.ColumnType{Raw: "decimal(10,0) unsigned", Type: &schema.DecimalType{T: "decimal", Precision: 10, Scale: 0, Unsigned: true}}, Default: &schema.Literal{V: "10"}},
 				}, t.Columns)
 			},
 		},
@@ -233,12 +237,15 @@ func TestDriver_InspectTable(t *testing.T) {
 				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 |
-+------------+--------------+--------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
-| users      |  float       | float        |                | NO          |            |                |       | NULL               | NULL           |
-| users      |  double      | double       |                | NO          |            |                |       | NULL               | NULL           |
-+------------+-------------+--------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
++------------+-------------------+--------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
+| table_name |    column_name    | column_type        | column_comment | is_nullable | column_key | column_default | extra | character_set_name | collation_name |
++------------+-------------------+--------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
+| users      |  float            | float              |                | NO          |            |                |       | NULL               | NULL           |
+| users      |  double           | double             |                | NO          |            |                |       | NULL               | NULL           |
+| users      |  float_unsigned   | float unsigned     |                | NO          |            |                |       | NULL               | NULL           |
+| users      |  double_unsigned  | double unsigned    |                | NO          |            |                |       | NULL               | NULL           |
+| users      |  float_unsigned_p | float(10) unsigned |                | NO          |            |                |       | NULL               | NULL           |
++------------+-------------------+--------------------+----------------+-------------+------------+----------------+-------+--------------------+----------------+
 `))
 				m.noIndexes()
 				m.noFKs()
@@ -249,6 +256,9 @@ func TestDriver_InspectTable(t *testing.T) {
 				require.EqualValues([]*schema.Column{
 					{Name: "float", Type: &schema.ColumnType{Raw: "float", Type: &schema.FloatType{T: "float"}}},
 					{Name: "double", Type: &schema.ColumnType{Raw: "double", Type: &schema.FloatType{T: "double"}}},
+					{Name: "float_unsigned", Type: &schema.ColumnType{Raw: "float unsigned", Type: &schema.FloatType{T: "float", Unsigned: true}}},
+					{Name: "double_unsigned", Type: &schema.ColumnType{Raw: "double unsigned", Type: &schema.FloatType{T: "double", Unsigned: true}}},
+					{Name: "float_unsigned_p", Type: &schema.ColumnType{Raw: "float(10) unsigned", Type: &schema.FloatType{T: "float", Precision: 10, Unsigned: true}}},
 				}, t.Columns)
 			},
 		},
diff --git a/sql/mysql/sqlspec_test.go b/sql/mysql/sqlspec_test.go
index 951d7072..229ea1d5 100644
--- a/sql/mysql/sqlspec_test.go
+++ b/sql/mysql/sqlspec_test.go
@@ -77,6 +77,14 @@ table "accounts" {
 	column "name" {
 		type = varchar(32)
 	}
+	column "unsigned_float" {
+		type     = float(10)
+		unsigned = true
+	}
+	column "unsigned_decimal" {
+		type     = decimal(10,2)
+		unsigned = true
+	}
 	primary_key {
 		columns = [table.accounts.column.name]
 	}
@@ -189,6 +197,27 @@ table "accounts" {
 						},
 					},
 				},
+				{
+					Name: "unsigned_float",
+					Type: &schema.ColumnType{
+						Type: &schema.FloatType{
+							T:         TypeFloat,
+							Precision: 10,
+							Unsigned:  true,
+						},
+					},
+				},
+				{
+					Name: "unsigned_decimal",
+					Type: &schema.ColumnType{
+						Type: &schema.DecimalType{
+							T:         TypeDecimal,
+							Precision: 10,
+							Scale:     2,
+							Unsigned:  true,
+						},
+					},
+				},
 			},
 		},
 	}
@@ -602,6 +631,47 @@ schema "test" {
 	require.EqualValues(t, expected, string(buf))
 }
 
+func TestMarshalSpec_FloatUnsigned(t *testing.T) {
+	s := schema.New("test").
+		AddTables(
+			schema.NewTable("test").
+				AddColumns(
+					schema.NewFloatColumn(
+						"float_col",
+						TypeFloat,
+						schema.FloatPrecision(10),
+						schema.FloatUnsigned(true),
+					),
+					schema.NewDecimalColumn(
+						"decimal_col",
+						TypeDecimal,
+						schema.DecimalPrecision(10),
+						schema.DecimalScale(2),
+						schema.DecimalUnsigned(true),
+					),
+				),
+		)
+	buf, err := MarshalSpec(s, hclState)
+	require.NoError(t, err)
+	const expected = `table "test" {
+  schema = schema.test
+  column "float_col" {
+    null     = false
+    type     = float(10)
+    unsigned = true
+  }
+  column "decimal_col" {
+    null     = false
+    type     = decimal(10,2)
+    unsigned = true
+  }
+}
+schema "test" {
+}
+`
+	require.EqualValues(t, expected, string(buf))
+}
+
 func TestTypes(t *testing.T) {
 	tests := []struct {
 		typeExpr  string
@@ -733,10 +803,20 @@ func TestTypes(t *testing.T) {
 			typeExpr: "decimal(10,2)",
 			expected: &schema.DecimalType{T: TypeDecimal, Precision: 10, Scale: 2},
 		},
+		{
+			typeExpr:  "decimal(10,2)",
+			extraAttr: "unsigned=true",
+			expected:  &schema.DecimalType{T: TypeDecimal, Precision: 10, Scale: 2, Unsigned: true},
+		},
 		{
 			typeExpr: "numeric",
 			expected: &schema.DecimalType{T: TypeNumeric},
 		},
+		{
+			typeExpr:  "numeric",
+			extraAttr: "unsigned=true",
+			expected:  &schema.DecimalType{T: TypeNumeric, Unsigned: true},
+		},
 		{
 			typeExpr: "numeric(10)",
 			expected: &schema.DecimalType{T: TypeNumeric, Precision: 10},
@@ -749,6 +829,11 @@ func TestTypes(t *testing.T) {
 			typeExpr: "float(10,0)",
 			expected: &schema.FloatType{T: TypeFloat, Precision: 10},
 		},
+		{
+			typeExpr:  "float(10)",
+			extraAttr: "unsigned=true",
+			expected:  &schema.FloatType{T: TypeFloat, Precision: 10, Unsigned: true},
+		},
 		{
 			typeExpr: "double(10,0)",
 			expected: &schema.FloatType{T: TypeDouble, Precision: 10},
@@ -757,6 +842,11 @@ func TestTypes(t *testing.T) {
 			typeExpr: "real",
 			expected: &schema.FloatType{T: TypeReal},
 		},
+		{
+			typeExpr:  "real",
+			extraAttr: "unsigned=true",
+			expected:  &schema.FloatType{T: TypeReal, Unsigned: true},
+		},
 		{
 			typeExpr: "timestamp",
 			expected: &schema.TimeType{T: TypeTimestamp},