Repository: ariga/atlas
Pull request #1756 (2023-06-18 16:15:38)

sql/schema: added Scale attribute for TimeType


Patch:
diff --git a/schemahcl/types.go b/schemahcl/types.go
index c4c5272e..6651882f 100644
--- a/schemahcl/types.go
+++ b/schemahcl/types.go
@@ -343,6 +343,22 @@ func SizeTypeAttr(required bool) *TypeAttr {
 	}
 }
 
+// PrecisionTypeAttr returns a TypeAttr for a precision attribute.
+func PrecisionTypeAttr() *TypeAttr {
+	return &TypeAttr{
+		Name: "precision",
+		Kind: reflect.Int,
+	}
+}
+
+// ScaleTypeAttr returns a TypeAttr for a scale attribute.
+func ScaleTypeAttr() *TypeAttr {
+	return &TypeAttr{
+		Name: "scale",
+		Kind: reflect.Int,
+	}
+}
+
 // typeNonFuncArgs returns the type attributes that are NOT configured via arguments to the
 // type definition, `int unsigned`.
 func typeNonFuncArgs(spec *TypeSpec) []*TypeAttr {
diff --git a/sql/internal/sqlx/diff.go b/sql/internal/sqlx/diff.go
index d92809c7..40d759ea 100644
--- a/sql/internal/sqlx/diff.go
+++ b/sql/internal/sqlx/diff.go
@@ -445,6 +445,28 @@ func CommentChange(from, to []schema.Attr) schema.ChangeKind {
 	return schema.NoChange
 }
 
+// Charset reports if the attribute contains the "charset" attribute,
+// and it needs to be defined explicitly on the schema. This is true, in
+// case the element charset is different from its parent charset.
+func Charset(attr, parent []schema.Attr) (string, bool) {
+	var c, p schema.Charset
+	if Has(attr, &c) && (parent == nil || Has(parent, &p) && c.V != p.V) {
+		return c.V, true
+	}
+	return "", false
+}
+
+// Collate reports if the attribute contains the "collation"/"collate" attribute,
+// and it needs to be defined explicitly on the schema. This is true, in
+// case the element collation is different from its parent collation.
+func Collate(attr, parent []schema.Attr) (string, bool) {
+	var c, p schema.Collation
+	if Has(attr, &c) && (parent == nil || Has(parent, &p) && c.V != p.V) {
+		return c.V, true
+	}
+	return "", false
+}
+
 var (
 	attrsType   = reflect.TypeOf(([]schema.Attr)(nil))
 	clausesType = reflect.TypeOf(([]schema.Clause)(nil))
diff --git a/sql/mysql/sqlspec.go b/sql/mysql/sqlspec.go
index f050292f..ee85390f 100644
--- a/sql/mysql/sqlspec.go
+++ b/sql/mysql/sqlspec.go
@@ -235,10 +235,10 @@ func schemaSpec(s *schema.Schema) (*specutil.SchemaSpec, error) {
 	if err != nil {
 		return nil, err
 	}
-	if c, ok := hasCharset(s.Attrs, nil); ok {
+	if c, ok := sqlx.Charset(s.Attrs, nil); ok {
 		spec.Schema.Extra.Attrs = append(spec.Schema.Extra.Attrs, schemahcl.StringAttr("charset", c))
 	}
-	if c, ok := hasCollate(s.Attrs, nil); ok {
+	if c, ok := sqlx.Collate(s.Attrs, nil); ok {
 		spec.Schema.Extra.Attrs = append(spec.Schema.Extra.Attrs, schemahcl.StringAttr("collate", c))
 	}
 	return spec, nil
@@ -257,10 +257,10 @@ func tableSpec(t *schema.Table) (*sqlspec.Table, error) {
 	if err != nil {
 		return nil, err
 	}
-	if c, ok := hasCharset(t.Attrs, t.Schema.Attrs); ok {
+	if c, ok := sqlx.Charset(t.Attrs, t.Schema.Attrs); ok {
 		ts.Extra.Attrs = append(ts.Extra.Attrs, schemahcl.StringAttr("charset", c))
 	}
-	if c, ok := hasCollate(t.Attrs, t.Schema.Attrs); ok {
+	if c, ok := sqlx.Collate(t.Attrs, t.Schema.Attrs); ok {
 		ts.Extra.Attrs = append(ts.Extra.Attrs, schemahcl.StringAttr("collate", c))
 	}
 	// Marshal the engine attribute only if it is not InnoDB (default).
@@ -327,10 +327,10 @@ func columnSpec(c *schema.Column, t *schema.Table) (*sqlspec.Column, error) {
 	if err != nil {
 		return nil, err
 	}
-	if c, ok := hasCharset(c.Attrs, t.Attrs); ok {
+	if c, ok := sqlx.Charset(c.Attrs, t.Attrs); ok {
 		spec.Extra.Attrs = append(spec.Extra.Attrs, schemahcl.StringAttr("charset", c))
 	}
-	if c, ok := hasCollate(c.Attrs, t.Attrs); ok {
+	if c, ok := sqlx.Collate(c.Attrs, t.Attrs); ok {
 		spec.Extra.Attrs = append(spec.Extra.Attrs, schemahcl.StringAttr("collate", c))
 	}
 	if o := (OnUpdate{}); sqlx.Has(c.Attrs, &o) {
@@ -409,28 +409,6 @@ func convertCharset(spec specutil.Attrer, attrs *[]schema.Attr) error {
 	return nil
 }
 
-// hasCharset reports if the attribute contains the "charset" attribute,
-// and it needs to be defined explicitly on the schema. This is true, in
-// case the element charset is different from its parent charset.
-func hasCharset(attr []schema.Attr, parent []schema.Attr) (string, bool) {
-	var c, p schema.Charset
-	if sqlx.Has(attr, &c) && (parent == nil || sqlx.Has(parent, &p) && c.V != p.V) {
-		return c.V, true
-	}
-	return "", false
-}
-
-// hasCollate reports if the attribute contains the "collation"/"collate" attribute,
-// and it needs to be defined explicitly on the schema. This is true, in
-// case the element collation is different from its parent collation.
-func hasCollate(attr []schema.Attr, parent []schema.Attr) (string, bool) {
-	var c, p schema.Collation
-	if sqlx.Has(attr, &c) && (parent == nil || sqlx.Has(parent, &p) && c.V != p.V) {
-		return c.V, true
-	}
-	return "", false
-}
-
 // TypeRegistry contains the supported TypeSpecs for the mysql driver.
 var TypeRegistry = schemahcl.NewRegistry(
 	schemahcl.WithFormatter(FormatType),
@@ -480,16 +458,16 @@ var TypeRegistry = schemahcl.NewRegistry(
 		schemahcl.NewTypeSpec(TypeSmallInt, schemahcl.WithAttributes(unsignedTypeAttr(), schemahcl.SizeTypeAttr(false))),
 		schemahcl.NewTypeSpec(TypeMediumInt, schemahcl.WithAttributes(unsignedTypeAttr(), schemahcl.SizeTypeAttr(false))),
 		schemahcl.NewTypeSpec(TypeBigInt, schemahcl.WithAttributes(unsignedTypeAttr(), schemahcl.SizeTypeAttr(false))),
-		schemahcl.NewTypeSpec(TypeDecimal, schemahcl.WithAttributes(unsignedTypeAttr(), &schemahcl.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false}, &schemahcl.TypeAttr{Name: "scale", Kind: reflect.Int, Required: false})),
-		schemahcl.NewTypeSpec(TypeNumeric, schemahcl.WithAttributes(unsignedTypeAttr(), &schemahcl.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false}, &schemahcl.TypeAttr{Name: "scale", Kind: reflect.Int, Required: false})),
-		schemahcl.NewTypeSpec(TypeFloat, schemahcl.WithAttributes(unsignedTypeAttr(), &schemahcl.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false}, &schemahcl.TypeAttr{Name: "scale", Kind: reflect.Int, Required: false})),
-		schemahcl.NewTypeSpec(TypeDouble, schemahcl.WithAttributes(unsignedTypeAttr(), &schemahcl.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false}, &schemahcl.TypeAttr{Name: "scale", Kind: reflect.Int, Required: false})),
-		schemahcl.NewTypeSpec(TypeReal, schemahcl.WithAttributes(unsignedTypeAttr(), &schemahcl.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false}, &schemahcl.TypeAttr{Name: "scale", Kind: reflect.Int, Required: false})),
-		schemahcl.NewTypeSpec(TypeTimestamp, schemahcl.WithAttributes(&schemahcl.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false})),
+		schemahcl.NewTypeSpec(TypeDecimal, schemahcl.WithAttributes(unsignedTypeAttr(), schemahcl.PrecisionTypeAttr(), schemahcl.ScaleTypeAttr())),
+		schemahcl.NewTypeSpec(TypeNumeric, schemahcl.WithAttributes(unsignedTypeAttr(), schemahcl.PrecisionTypeAttr(), schemahcl.ScaleTypeAttr())),
+		schemahcl.NewTypeSpec(TypeFloat, schemahcl.WithAttributes(unsignedTypeAttr(), schemahcl.PrecisionTypeAttr(), schemahcl.ScaleTypeAttr())),
+		schemahcl.NewTypeSpec(TypeDouble, schemahcl.WithAttributes(unsignedTypeAttr(), schemahcl.PrecisionTypeAttr(), schemahcl.ScaleTypeAttr())),
+		schemahcl.NewTypeSpec(TypeReal, schemahcl.WithAttributes(unsignedTypeAttr(), schemahcl.PrecisionTypeAttr(), schemahcl.ScaleTypeAttr())),
+		schemahcl.NewTypeSpec(TypeTimestamp, schemahcl.WithAttributes(schemahcl.PrecisionTypeAttr())),
 		schemahcl.NewTypeSpec(TypeDate),
-		schemahcl.NewTypeSpec(TypeTime, schemahcl.WithAttributes(&schemahcl.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false})),
-		schemahcl.NewTypeSpec(TypeDateTime, schemahcl.WithAttributes(&schemahcl.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false})),
-		schemahcl.NewTypeSpec(TypeYear, schemahcl.WithAttributes(&schemahcl.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false})),
+		schemahcl.NewTypeSpec(TypeTime, schemahcl.WithAttributes(schemahcl.PrecisionTypeAttr())),
+		schemahcl.NewTypeSpec(TypeDateTime, schemahcl.WithAttributes(schemahcl.PrecisionTypeAttr())),
+		schemahcl.NewTypeSpec(TypeYear, schemahcl.WithAttributes(schemahcl.PrecisionTypeAttr())),
 		schemahcl.NewTypeSpec(TypeVarchar, schemahcl.WithAttributes(schemahcl.SizeTypeAttr(true))),
 		schemahcl.NewTypeSpec(TypeChar, schemahcl.WithAttributes(schemahcl.SizeTypeAttr(false))),
 		schemahcl.NewTypeSpec(TypeVarBinary, schemahcl.WithAttributes(schemahcl.SizeTypeAttr(true))),
diff --git a/sql/postgres/sqlspec.go b/sql/postgres/sqlspec.go
index 211118cc..a3a6d6a5 100644
--- a/sql/postgres/sqlspec.go
+++ b/sql/postgres/sqlspec.go
@@ -718,17 +718,17 @@ var TypeRegistry = schemahcl.NewRegistry(
 		schemahcl.NewTypeSpec(TypePoint),
 		schemahcl.NewTypeSpec(TypePolygon),
 		schemahcl.NewTypeSpec(TypeDate),
-		schemahcl.NewTypeSpec(TypeTime, schemahcl.WithAttributes(precisionTypeAttr()), formatTime()),
-		schemahcl.NewTypeSpec(TypeTimeTZ, schemahcl.WithAttributes(precisionTypeAttr()), formatTime()),
-		schemahcl.NewTypeSpec(TypeTimestampTZ, schemahcl.WithAttributes(precisionTypeAttr()), formatTime()),
-		schemahcl.NewTypeSpec(TypeTimestamp, schemahcl.WithAttributes(precisionTypeAttr()), formatTime()),
+		schemahcl.NewTypeSpec(TypeTime, schemahcl.WithAttributes(schemahcl.PrecisionTypeAttr()), formatTime()),
+		schemahcl.NewTypeSpec(TypeTimeTZ, schemahcl.WithAttributes(schemahcl.PrecisionTypeAttr()), formatTime()),
+		schemahcl.NewTypeSpec(TypeTimestampTZ, schemahcl.WithAttributes(schemahcl.PrecisionTypeAttr()), formatTime()),
+		schemahcl.NewTypeSpec(TypeTimestamp, schemahcl.WithAttributes(schemahcl.PrecisionTypeAttr()), formatTime()),
 		schemahcl.AliasTypeSpec("double_precision", TypeDouble),
 		schemahcl.NewTypeSpec(TypeReal),
-		schemahcl.NewTypeSpec(TypeFloat, schemahcl.WithAttributes(precisionTypeAttr())),
+		schemahcl.NewTypeSpec(TypeFloat, schemahcl.WithAttributes(schemahcl.PrecisionTypeAttr())),
 		schemahcl.NewTypeSpec(TypeFloat8),
 		schemahcl.NewTypeSpec(TypeFloat4),
-		schemahcl.NewTypeSpec(TypeNumeric, schemahcl.WithAttributes(precisionTypeAttr(), &schemahcl.TypeAttr{Name: "scale", Kind: reflect.Int, Required: false})),
-		schemahcl.NewTypeSpec(TypeDecimal, schemahcl.WithAttributes(precisionTypeAttr(), &schemahcl.TypeAttr{Name: "scale", Kind: reflect.Int, Required: false})),
+		schemahcl.NewTypeSpec(TypeNumeric, schemahcl.WithAttributes(schemahcl.PrecisionTypeAttr(), schemahcl.ScaleTypeAttr())),
+		schemahcl.NewTypeSpec(TypeDecimal, schemahcl.WithAttributes(schemahcl.PrecisionTypeAttr(), schemahcl.ScaleTypeAttr())),
 		schemahcl.NewTypeSpec(TypeSmallSerial),
 		schemahcl.NewTypeSpec(TypeSerial),
 		schemahcl.NewTypeSpec(TypeBigSerial),
@@ -801,7 +801,7 @@ var TypeRegistry = schemahcl.NewRegistry(
 			}),
 		}
 		for _, f := range []string{"interval", "second", "day to second", "hour to second", "minute to second"} {
-			specs = append(specs, schemahcl.NewTypeSpec(specutil.Var(f), append(opts, schemahcl.WithAttributes(precisionTypeAttr()))...))
+			specs = append(specs, schemahcl.NewTypeSpec(specutil.Var(f), append(opts, schemahcl.WithAttributes(schemahcl.PrecisionTypeAttr()))...))
 		}
 		for _, f := range []string{"year", "month", "day", "hour", "minute", "year to month", "day to hour", "day to minute", "hour to minute"} {
 			specs = append(specs, schemahcl.NewTypeSpec(specutil.Var(f), opts...))
@@ -810,14 +810,6 @@ var TypeRegistry = schemahcl.NewRegistry(
 	}()...),
 )
 
-func precisionTypeAttr() *schemahcl.TypeAttr {
-	return &schemahcl.TypeAttr{
-		Name:     "precision",
-		Kind:     reflect.Int,
-		Required: false,
-	}
-}
-
 func attr(typ *schemahcl.Type, key string) (*schemahcl.Attr, bool) {
 	for _, a := range typ.Attrs {
 		if a.K == key {
diff --git a/sql/schema/dsl.go b/sql/schema/dsl.go
index d8485a3e..e9ae9e52 100644
--- a/sql/schema/dsl.go
+++ b/sql/schema/dsl.go
@@ -469,6 +469,13 @@ func TimePrecision(precision int) TimeOption {
 	}
 }
 
+// TimeScale configures the scale of the time type.
+func TimeScale(scale int) TimeOption {
+	return func(b *TimeType) {
+		b.Scale = &scale
+	}
+}
+
 // NewTimeColumn creates a new TimeType column.
 func NewTimeColumn(name, typ string, opts ...TimeOption) *Column {
 	t := &TimeType{T: typ}
diff --git a/sql/schema/schema.go b/sql/schema/schema.go
index 7edb34ef..9e6d70c7 100644
--- a/sql/schema/schema.go
+++ b/sql/schema/schema.go
@@ -265,6 +265,7 @@ type (
 	TimeType struct {
 		T         string
 		Precision *int
+		Scale     *int
 	}
 
 	// JSONType represents a JSON type.
diff --git a/sql/sqlite/sqlspec.go b/sql/sqlite/sqlspec.go
index f3d965a9..1146820e 100644
--- a/sql/sqlite/sqlspec.go
+++ b/sql/sqlite/sqlspec.go
@@ -6,7 +6,6 @@ package sqlite
 
 import (
 	"fmt"
-	"reflect"
 	"strconv"
 	"strings"
 
@@ -176,7 +175,7 @@ var TypeRegistry = schemahcl.NewRegistry(
 	schemahcl.WithFormatter(FormatType),
 	schemahcl.WithParser(ParseType),
 	schemahcl.WithSpecs(
-		schemahcl.NewTypeSpec(TypeReal, schemahcl.WithAttributes(&schemahcl.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false}, &schemahcl.TypeAttr{Name: "scale", Kind: reflect.Int, Required: false})),
+		schemahcl.NewTypeSpec(TypeReal, schemahcl.WithAttributes(schemahcl.PrecisionTypeAttr(), schemahcl.ScaleTypeAttr())),
 		schemahcl.NewTypeSpec(TypeBlob, schemahcl.WithAttributes(schemahcl.SizeTypeAttr(false))),
 		schemahcl.NewTypeSpec(TypeText, schemahcl.WithAttributes(schemahcl.SizeTypeAttr(false))),
 		schemahcl.NewTypeSpec(TypeInteger, schemahcl.WithAttributes(schemahcl.SizeTypeAttr(false))),
@@ -199,8 +198,8 @@ var TypeRegistry = schemahcl.NewRegistry(
 		schemahcl.AliasTypeSpec("native_character", "native character", schemahcl.WithAttributes(schemahcl.SizeTypeAttr(false))),
 		schemahcl.NewTypeSpec("nvarchar", schemahcl.WithAttributes(schemahcl.SizeTypeAttr(false))),
 		schemahcl.NewTypeSpec("clob", schemahcl.WithAttributes(schemahcl.SizeTypeAttr(false))),
-		schemahcl.NewTypeSpec("numeric", schemahcl.WithAttributes(&schemahcl.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false}, &schemahcl.TypeAttr{Name: "scale", Kind: reflect.Int, Required: false})),
-		schemahcl.NewTypeSpec("decimal", schemahcl.WithAttributes(&schemahcl.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false}, &schemahcl.TypeAttr{Name: "scale", Kind: reflect.Int, Required: false})),
+		schemahcl.NewTypeSpec("numeric", schemahcl.WithAttributes(schemahcl.PrecisionTypeAttr(), schemahcl.ScaleTypeAttr())),
+		schemahcl.NewTypeSpec("decimal", schemahcl.WithAttributes(schemahcl.PrecisionTypeAttr(), schemahcl.ScaleTypeAttr())),
 		schemahcl.NewTypeSpec("bool"),
 		schemahcl.NewTypeSpec("boolean"),
 		schemahcl.NewTypeSpec("date"),


Test patch:
diff --git a/schemahcl/context_test.go b/schemahcl/context_test.go
index f174f85f..4e3c5eb0 100644
--- a/schemahcl/context_test.go
+++ b/schemahcl/context_test.go
@@ -432,8 +432,8 @@ func TestOptionalArgs(t *testing.T) {
 				T:    "float",
 				Name: "float",
 				Attributes: []*TypeAttr{
-					{Name: "precision", Kind: reflect.Int, Required: false},
-					{Name: "scale", Kind: reflect.Int, Required: false},
+					PrecisionTypeAttr(),
+					ScaleTypeAttr(),
 				},
 			},
 		}),
diff --git a/schemahcl/types_test.go b/schemahcl/types_test.go
index d714c28f..113768fa 100644
--- a/schemahcl/types_test.go
+++ b/schemahcl/types_test.go
@@ -145,16 +145,8 @@ func TestRegistryConvert(t *testing.T) {
 		NewTypeSpec(
 			"decimal",
 			WithAttributes(
-				&TypeAttr{
-					Name:     "precision",
-					Kind:     reflect.Int,
-					Required: false,
-				},
-				&TypeAttr{
-					Name:     "scale",
-					Kind:     reflect.Int,
-					Required: false,
-				},
+				PrecisionTypeAttr(),
+				ScaleTypeAttr(),
 			),
 		),
 		NewTypeSpec("enum", WithAttributes(&TypeAttr{
diff --git a/sql/schema/dsl_test.go b/sql/schema/dsl_test.go
index 8a74ae48..e4b52ff9 100644
--- a/sql/schema/dsl_test.go
+++ b/sql/schema/dsl_test.go
@@ -43,6 +43,7 @@ func TestSchema_AddTables(t *testing.T) {
 		schema.NewBoolColumn("active", "boolean"),
 		schema.NewNullStringColumn("name", "varchar", schema.StringSize(255)),
 		schema.NewTimeColumn("registered_at", "timestamp", schema.TimePrecision(6)),
+		schema.NewTimeColumn("open_time", "time", schema.TimeScale(2)),
 	}
 	users := schema.NewTable("users").
 		AddColumns(userColumns...).
@@ -74,6 +75,7 @@ func TestSchema_AddTables(t *testing.T) {
 		t,
 		func() *schema.Schema {
 			p := 6
+			scale := 2
 			s := &schema.Schema{Name: "public"}
 			users := &schema.Table{
 				Name:   "users",
@@ -86,6 +88,7 @@ func TestSchema_AddTables(t *testing.T) {
 					{Name: "active", Type: &schema.ColumnType{Type: &schema.BoolType{T: "boolean"}}},
 					{Name: "name", Type: &schema.ColumnType{Null: true, Type: &schema.StringType{T: "varchar", Size: 255}}},
 					{Name: "registered_at", Type: &schema.ColumnType{Null: false, Type: &schema.TimeType{T: "timestamp", Precision: &p}}},
+					{Name: "open_time", Type: &schema.ColumnType{Null: false, Type: &schema.TimeType{T: "time", Precision: nil, Scale: &scale}}},
 				},
 			}
 			s.Tables = append(s.Tables, users)