Repository: ariga/atlas
Pull request #311 (2021-12-15 07:39:12)

sql/sqlite: type registry


Patch:
diff --git a/sql/internal/specutil/types.go b/sql/internal/specutil/types.go
index ae14e541..5f29ecfa 100644
--- a/sql/internal/specutil/types.go
+++ b/sql/internal/specutil/types.go
@@ -135,7 +135,9 @@ func (r *TypeRegistry) Convert(typ schema.Type) (*schemaspec.Type, error) {
 	if !ok {
 		return nil, fmt.Errorf("specutil: type %q not found in registry", s.T)
 	}
-	for _, attr := range typeSpec.Attributes {
+	// Iterate the attributes in reverse order, so we can skip zero value and optional attrs.
+	for i := len(typeSpec.Attributes) - 1; i >= 0; i-- {
+		attr := typeSpec.Attributes[i]
 		n := inflect.Camelize(attr.Name)
 		field := rv.FieldByName(n)
 		if !field.IsValid() {
@@ -146,11 +148,19 @@ func (r *TypeRegistry) Convert(typ schema.Type) (*schemaspec.Type, error) {
 		}
 		switch attr.Kind {
 		case reflect.Int:
-			i := strconv.Itoa(int(field.Int()))
-			s.Attrs = append(s.Attrs, LitAttr(attr.Name, i))
+			v := int(field.Int())
+			if v == 0 && len(s.Attrs) == 0 {
+				break
+			}
+			i := strconv.Itoa(v)
+			s.Attrs = append([]*schemaspec.Attr{LitAttr(attr.Name, i)}, s.Attrs...)
 		case reflect.Bool:
-			b := strconv.FormatBool(field.Bool())
-			s.Attrs = append(s.Attrs, LitAttr(attr.Name, b))
+			v := field.Bool()
+			if !v && len(s.Attrs) == 0 {
+				break
+			}
+			b := strconv.FormatBool(v)
+			s.Attrs = append([]*schemaspec.Attr{LitAttr(attr.Name, b)}, s.Attrs...)
 		case reflect.Slice:
 			lits := make([]string, 0, field.Len())
 			for i := 0; i < field.Len(); i++ {
@@ -160,7 +170,7 @@ func (r *TypeRegistry) Convert(typ schema.Type) (*schemaspec.Type, error) {
 				}
 				lits = append(lits, strconv.Quote(fi.String()))
 			}
-			s.Attrs = append(s.Attrs, ListAttr(attr.Name, lits...))
+			s.Attrs = append([]*schemaspec.Attr{ListAttr(attr.Name, lits...)}, s.Attrs...)
 		default:
 			return nil, fmt.Errorf("specutil: unsupported attr kind %s for attribute %q of %q", attr.Kind, attr.Name, typeSpec.Name)
 		}
diff --git a/sql/internal/sqlx/diff.go b/sql/internal/sqlx/diff.go
index 84b439b3..f3abf477 100644
--- a/sql/internal/sqlx/diff.go
+++ b/sql/internal/sqlx/diff.go
@@ -308,17 +308,27 @@ func CommentChange(from, to []schema.Attr) schema.ChangeKind {
 	return schema.NoChange
 }
 
-// Has finds the first attribute in the attribute list that
+var (
+	attrsType   = reflect.TypeOf(([]schema.Attr)(nil))
+	clausesType = reflect.TypeOf(([]schema.Clause)(nil))
+	exprsType   = reflect.TypeOf(([]schema.Expr)(nil))
+)
+
+// Has finds the first element in the elements list that
 // matches target, and if so, sets target to that attribute
 // value and returns true.
-func Has(attrs []schema.Attr, target interface{}) bool {
-	rv := reflect.ValueOf(target)
-	if rv.Kind() != reflect.Ptr || rv.IsNil() {
+func Has(elements, target interface{}) bool {
+	ev := reflect.ValueOf(elements)
+	if t := ev.Type(); t != attrsType && t != clausesType && t != exprsType {
+		panic(fmt.Sprintf("unexpected elements type: %T", t))
+	}
+	tv := reflect.ValueOf(target)
+	if tv.Kind() != reflect.Ptr || tv.IsNil() {
 		panic("target must be a non-nil pointer")
 	}
-	for _, attr := range attrs {
-		if reflect.TypeOf(attr).AssignableTo(rv.Type()) {
-			rv.Elem().Set(reflect.ValueOf(attr).Elem())
+	for i := 0; i < ev.Len(); i++ {
+		if e := ev.Index(i).Elem(); e.Type().AssignableTo(tv.Type()) {
+			tv.Elem().Set(e.Elem())
 			return true
 		}
 	}
diff --git a/sql/mysql/migrate.go b/sql/mysql/migrate.go
index 410164d3..b742746b 100644
--- a/sql/mysql/migrate.go
+++ b/sql/mysql/migrate.go
@@ -53,6 +53,9 @@ func (m *migrate) topLevel(ctx context.Context, changes []schema.Change) ([]sche
 		switch c := c.(type) {
 		case *schema.AddSchema:
 			b := Build("CREATE DATABASE").Ident(c.S.Name)
+			if sqlx.Has(c.Extra, &schema.IfNotExists{}) {
+				b.P("IF NOT EXISTS")
+			}
 			if a := (schema.Charset{}); sqlx.Has(c.S.Attrs, &a) {
 				b.P("CHARACTER SET", a.V)
 			}
@@ -76,6 +79,9 @@ func (m *migrate) topLevel(ctx context.Context, changes []schema.Change) ([]sche
 // addTable builds and executes the query for creating a table in a schema.
 func (m *migrate) addTable(ctx context.Context, add *schema.AddTable) error {
 	b := Build("CREATE TABLE").Table(add.T)
+	if sqlx.Has(add.Extra, &schema.IfNotExists{}) {
+		b.P("IF NOT EXISTS")
+	}
 	b.Wrap(func(b *sqlx.Builder) {
 		b.MapComma(add.T.Columns, func(i int, b *sqlx.Builder) {
 			m.column(b, add.T, add.T.Columns[i])
@@ -112,6 +118,9 @@ func (m *migrate) addTable(ctx context.Context, add *schema.AddTable) error {
 // dropTable builds and executes the query for dropping a table from a schema.
 func (m *migrate) dropTable(ctx context.Context, drop *schema.DropTable) error {
 	b := Build("DROP TABLE").Table(drop.T)
+	if sqlx.Has(drop.Extra, &schema.IfExists{}) {
+		b.P("IF EXISTS")
+	}
 	if _, err := m.ExecContext(ctx, b.String()); err != nil {
 		return fmt.Errorf("drop table: %w", err)
 	}
diff --git a/sql/postgres/migrate.go b/sql/postgres/migrate.go
index e8cd3557..4e35de99 100644
--- a/sql/postgres/migrate.go
+++ b/sql/postgres/migrate.go
@@ -51,7 +51,11 @@ func (m *migrate) topLevel(ctx context.Context, changes []schema.Change) ([]sche
 	for _, c := range changes {
 		switch c := c.(type) {
 		case *schema.AddSchema:
-			if _, err := m.ExecContext(ctx, Build("CREATE SCHEMA").Ident(c.S.Name).String()); err != nil {
+			b := Build("CREATE SCHEMA").Ident(c.S.Name)
+			if sqlx.Has(c.Extra, &schema.IfNotExists{}) {
+				b.P("IF NOT EXISTS")
+			}
+			if _, err := m.ExecContext(ctx, b.String()); err != nil {
 				return nil, fmt.Errorf("add schema: %w", err)
 			}
 		case *schema.DropSchema:
@@ -72,6 +76,9 @@ func (m *migrate) addTable(ctx context.Context, add *schema.AddTable) error {
 		return err
 	}
 	b := Build("CREATE TABLE").Table(add.T)
+	if sqlx.Has(add.Extra, &schema.IfNotExists{}) {
+		b.P("IF NOT EXISTS")
+	}
 	b.Wrap(func(b *sqlx.Builder) {
 		b.MapComma(add.T.Columns, func(i int, b *sqlx.Builder) {
 			m.column(b, add.T.Columns[i])
@@ -97,6 +104,9 @@ func (m *migrate) addTable(ctx context.Context, add *schema.AddTable) error {
 // dropTable builds and executes the query for dropping a table from a schema.
 func (m *migrate) dropTable(ctx context.Context, drop *schema.DropTable) error {
 	b := Build("DROP TABLE").Table(drop.T)
+	if sqlx.Has(drop.Extra, &schema.IfExists{}) {
+		b.P("IF EXISTS")
+	}
 	if _, err := m.ExecContext(ctx, b.String()); err != nil {
 		return fmt.Errorf("drop table: %w", err)
 	}
diff --git a/sql/schema/migrate.go b/sql/schema/migrate.go
index 24a23f31..c919e574 100644
--- a/sql/schema/migrate.go
+++ b/sql/schema/migrate.go
@@ -24,18 +24,33 @@ type (
 		change()
 	}
 
+	// Clause carries additional information that can be added
+	// to schema changes. The Clause interface can be implemented
+	// outside this package as follows:
+	//
+	//	type Authorization struct {
+	//		schema.Clause
+	//		UserName string
+	//	}
+	//
+	//	var c schema.Clause = &Authorization{UserName: "a8m"}
+	//
+	Clause interface {
+		clause()
+	}
+
 	// AddSchema describes a schema (named database) creation change.
 	// Unlike table creation, schemas and their elements are described
 	// with separate changes. For example, "AddSchema" and "AddTable"
 	AddSchema struct {
 		S     *Schema
-		Extra []Expr // Extra clauses and options.
+		Extra []Clause // Extra clauses and options.
 	}
 
 	// DropSchema describes a schema (named database) removal change.
 	DropSchema struct {
 		S     *Schema
-		Extra []Expr // Extra clauses and options.
+		Extra []Clause // Extra clauses and options.
 	}
 
 	// ModifySchema describes a modification change for schema attributes.
@@ -47,13 +62,13 @@ type (
 	// AddTable describes a table creation change.
 	AddTable struct {
 		T     *Table
-		Extra []Expr // Extra clauses and options.
+		Extra []Clause // Extra clauses and options.
 	}
 
 	// DropTable describes a table removal change.
 	DropTable struct {
 		T     *Table
-		Extra []Expr // Extra clauses.
+		Extra []Clause // Extra clauses.
 	}
 
 	// ModifyTable describes a table modification change.
@@ -124,6 +139,14 @@ type (
 	ModifyAttr struct {
 		From, To Attr
 	}
+
+	// IfExists represents a clause in a schema change that is commonly
+	// supported by multiple statements (e.g. DROP TABLE or DROP SCHEMA).
+	IfExists struct{}
+
+	// IfNotExists represents a clause in a schema change that is commonly
+	// supported by multiple statements (e.g. CREATE TABLE or CREATE SCHEMA).
+	IfNotExists struct{}
 )
 
 // A ChangeKind describes a change kind that can be combined
@@ -233,3 +256,7 @@ func (*ModifyColumn) change()     {}
 func (*AddForeignKey) change()    {}
 func (*DropForeignKey) change()   {}
 func (*ModifyForeignKey) change() {}
+
+// clauses.
+func (*IfExists) clause()    {}
+func (*IfNotExists) clause() {}
diff --git a/sql/sqlite/migrate.go b/sql/sqlite/migrate.go
index 24431d62..46d73e10 100644
--- a/sql/sqlite/migrate.go
+++ b/sql/sqlite/migrate.go
@@ -40,6 +40,9 @@ func (m *migrate) Exec(ctx context.Context, changes []schema.Change) (err error)
 // addTable builds and executes the query for creating a table in a schema.
 func (m *migrate) addTable(ctx context.Context, add *schema.AddTable) error {
 	b := Build("CREATE TABLE").Ident(add.T.Name)
+	if sqlx.Has(add.Extra, &schema.IfNotExists{}) {
+		b.P("IF NOT EXISTS")
+	}
 	b.Wrap(func(b *sqlx.Builder) {
 		b.MapComma(add.T.Columns, func(i int, b *sqlx.Builder) {
 			m.column(b, add.T.Columns[i])
@@ -66,7 +69,11 @@ func (m *migrate) addTable(ctx context.Context, add *schema.AddTable) error {
 // dropTable builds and executes the query for dropping a table from a schema.
 func (m *migrate) dropTable(ctx context.Context, drop *schema.DropTable) error {
 	if err := m.skipConstraints(ctx, func(ctx context.Context) error {
-		_, err := m.ExecContext(ctx, Build("DROP TABLE").Ident(drop.T.Name).String())
+		b := Build("DROP TABLE").Ident(drop.T.Name)
+		if sqlx.Has(drop.Extra, &schema.IfExists{}) {
+			b.P("IF EXISTS")
+		}
+		_, err := m.ExecContext(ctx, b.String())
 		return err
 	}); err != nil {
 		return fmt.Errorf("drop table: %w", err)
diff --git a/sql/sqlite/sqlspec.go b/sql/sqlite/sqlspec.go
index 09e7d817..5b1183a4 100644
--- a/sql/sqlite/sqlspec.go
+++ b/sql/sqlite/sqlspec.go
@@ -3,6 +3,7 @@ package sqlite
 import (
 	"errors"
 	"fmt"
+	"reflect"
 	"strconv"
 	"strings"
 
@@ -275,3 +276,36 @@ func enumSpec(t *schema.EnumType) (*sqlspec.Column, error) {
 	}
 	return specutil.NewCol("", "enum", specutil.ListAttr("values", quoted...)), nil
 }
+
+// TypeRegistry contains the supported TypeSpecs for the sqlite driver.
+var TypeRegistry = specutil.NewRegistry(
+	specutil.TypeSpec(tReal, &schemaspec.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false}, &schemaspec.TypeAttr{Name: "scale", Kind: reflect.Int, Required: false}),
+	specutil.TypeSpec(tBlob, specutil.SizeTypeAttr(false)),
+	specutil.TypeSpec(tText, specutil.SizeTypeAttr(false)),
+	specutil.TypeSpec("integer", specutil.SizeTypeAttr(false)),
+	specutil.TypeSpec("int", specutil.SizeTypeAttr(false)),
+	specutil.TypeSpec("tinyint", specutil.SizeTypeAttr(false)),
+	specutil.TypeSpec("smallint", specutil.SizeTypeAttr(false)),
+	specutil.TypeSpec("mediumint", specutil.SizeTypeAttr(false)),
+	specutil.TypeSpec("bigint", specutil.SizeTypeAttr(false)),
+	specutil.AliasTypeSpec("unsigned_big_int", "unsigned big int", specutil.SizeTypeAttr(false)),
+	specutil.TypeSpec("int2", specutil.SizeTypeAttr(false)),
+	specutil.TypeSpec("int8", specutil.SizeTypeAttr(false)),
+	specutil.TypeSpec("double", specutil.SizeTypeAttr(false)),
+	specutil.AliasTypeSpec("double_precision", "double precision", specutil.SizeTypeAttr(false)),
+	specutil.TypeSpec("float", specutil.SizeTypeAttr(false)),
+	specutil.TypeSpec("character", specutil.SizeTypeAttr(false)),
+	specutil.TypeSpec("varchar", specutil.SizeTypeAttr(false)),
+	specutil.AliasTypeSpec("varying_character", "varying character", specutil.SizeTypeAttr(false)),
+	specutil.TypeSpec("nchar", specutil.SizeTypeAttr(false)),
+	specutil.AliasTypeSpec("native_character", "native character", specutil.SizeTypeAttr(false)),
+	specutil.TypeSpec("nvarchar", specutil.SizeTypeAttr(false)),
+	specutil.TypeSpec("clob", specutil.SizeTypeAttr(false)),
+	specutil.TypeSpec("numeric", specutil.SizeTypeAttr(false)),
+	specutil.TypeSpec("decimal", &schemaspec.TypeAttr{Name: "precision", Kind: reflect.Int, Required: false}, &schemaspec.TypeAttr{Name: "scale", Kind: reflect.Int, Required: false}),
+	specutil.TypeSpec("boolean"),
+	specutil.TypeSpec("date"),
+	specutil.TypeSpec("datetime"),
+	specutil.TypeSpec("json"),
+	specutil.TypeSpec("uuid"),
+)


Test patch:
diff --git a/sql/internal/specutil/types_test.go b/sql/internal/specutil/types_test.go
index 4ee551c4..dc51d555 100644
--- a/sql/internal/specutil/types_test.go
+++ b/sql/internal/specutil/types_test.go
@@ -110,12 +110,29 @@ func TestRegistryConvert(t *testing.T) {
 			expected: &schemaspec.Type{T: "int", Attrs: []*schemaspec.Attr{LitAttr("unsigned", "true")}},
 		},
 		{
-			typ: &schema.DecimalType{T: "decimal", Precision: 10, Scale: 2},
+			typ: &schema.DecimalType{T: "decimal", Precision: 10, Scale: 2}, // decimal(10,2)
 			expected: &schemaspec.Type{T: "decimal", Attrs: []*schemaspec.Attr{
 				LitAttr("precision", "10"),
 				LitAttr("scale", "2"),
 			}},
 		},
+		{
+			typ: &schema.DecimalType{T: "decimal", Precision: 10}, // decimal(10)
+			expected: &schemaspec.Type{T: "decimal", Attrs: []*schemaspec.Attr{
+				LitAttr("precision", "10"),
+			}},
+		},
+		{
+			typ: &schema.DecimalType{T: "decimal", Scale: 2}, // decimal(0,2)
+			expected: &schemaspec.Type{T: "decimal", Attrs: []*schemaspec.Attr{
+				LitAttr("precision", "0"),
+				LitAttr("scale", "2"),
+			}},
+		},
+		{
+			typ:      &schema.DecimalType{T: "decimal"}, // decimal
+			expected: &schemaspec.Type{T: "decimal"},
+		},
 		{
 			typ: &schema.EnumType{T: "enum", Values: []string{"on", "off"}},
 			expected: &schemaspec.Type{T: "enum", Attrs: []*schemaspec.Attr{
diff --git a/sql/mysql/migrate_test.go b/sql/mysql/migrate_test.go
index 72df312b..8679ce97 100644
--- a/sql/mysql/migrate_test.go
+++ b/sql/mysql/migrate_test.go
@@ -24,9 +24,9 @@ func TestMigrate_Exec(t *testing.T) {
 		WillReturnResult(sqlmock.NewResult(0, 1))
 	mk.ExpectExec(sqltest.Escape("DROP TABLE `users`")).
 		WillReturnResult(sqlmock.NewResult(0, 0))
-	mk.ExpectExec(sqltest.Escape("DROP TABLE `public`.`pets`")).
+	mk.ExpectExec(sqltest.Escape("DROP TABLE `public`.`pets` IF EXISTS")).
 		WillReturnResult(sqlmock.NewResult(0, 0))
-	mk.ExpectExec(sqltest.Escape("CREATE TABLE `pets` (`a` int NOT NULL, `b` bigint NOT NULL, `c` bigint NULL, PRIMARY KEY (`a`, `b`), UNIQUE INDEX `b_c_unique` (`b`, `c`) COMMENT \"comment\")")).
+	mk.ExpectExec(sqltest.Escape("CREATE TABLE `pets` IF NOT EXISTS (`a` int NOT NULL, `b` bigint NOT NULL, `c` bigint NULL, PRIMARY KEY (`a`, `b`), UNIQUE INDEX `b_c_unique` (`b`, `c`) COMMENT \"comment\")")).
 		WillReturnResult(sqlmock.NewResult(0, 0))
 	mk.ExpectExec(sqltest.Escape("ALTER TABLE `users` DROP INDEX `id_spouse_id`")).
 		WillReturnResult(sqlmock.NewResult(0, 0))
@@ -40,7 +40,7 @@ func TestMigrate_Exec(t *testing.T) {
 		&schema.AddSchema{S: &schema.Schema{Name: "test", Attrs: []schema.Attr{&schema.Charset{V: "latin"}}}},
 		&schema.DropSchema{S: &schema.Schema{Name: "atlas", Attrs: []schema.Attr{&schema.Charset{V: "latin"}}}},
 		&schema.DropTable{T: &schema.Table{Name: "users"}},
-		&schema.DropTable{T: &schema.Table{Name: "pets", Schema: &schema.Schema{Name: "public"}}},
+		&schema.DropTable{T: &schema.Table{Name: "pets", Schema: &schema.Schema{Name: "public"}}, Extra: []schema.Clause{&schema.IfExists{}}},
 		&schema.AddTable{
 			T: func() *schema.Table {
 				t := &schema.Table{
@@ -59,6 +59,9 @@ func TestMigrate_Exec(t *testing.T) {
 				}
 				return t
 			}(),
+			Extra: []schema.Clause{
+				&schema.IfNotExists{},
+			},
 		},
 	})
 	require.NoError(t, err)
diff --git a/sql/sqlite/sqlspec_test.go b/sql/sqlite/sqlspec_test.go
index 5aed31e0..61f976c0 100644
--- a/sql/sqlite/sqlspec_test.go
+++ b/sql/sqlite/sqlspec_test.go
@@ -8,14 +8,18 @@ import (
 	"fmt"
 	"testing"
 
+	"ariga.io/atlas/schema/schemaspec"
 	"ariga.io/atlas/schema/schemaspec/schemahcl"
 	"ariga.io/atlas/sql/internal/specutil"
 	"ariga.io/atlas/sql/schema"
 	"ariga.io/atlas/sql/sqlspec"
 
+	"github.com/hashicorp/hcl/v2/hclwrite"
 	"github.com/stretchr/testify/require"
 )
 
+var hclState = schemahcl.New(schemahcl.WithTypes(TypeRegistry.Specs()))
+
 func TestSQLSpec(t *testing.T) {
 	f := `
 schema "schema" {
@@ -386,3 +390,151 @@ func TestNotSupportedMarshalSpecColumnType(t *testing.T) {
 		})
 	}
 }
+
+func TestTypes(t *testing.T) {
+	for _, tt := range []struct {
+		typeExpr string
+		expected schema.Type
+	}{
+		{
+			typeExpr: "integer(10)",
+			expected: &schema.IntegerType{T: "integer"},
+		},
+		{
+			typeExpr: "int(10)",
+			expected: &schema.IntegerType{T: "int"},
+		},
+		{
+			typeExpr: "tinyint(10)",
+			expected: &schema.IntegerType{T: "tinyint"},
+		},
+		{
+			typeExpr: "smallint(10)",
+			expected: &schema.IntegerType{T: "smallint"},
+		},
+		{
+			typeExpr: "mediumint(10)",
+			expected: &schema.IntegerType{T: "mediumint"},
+		},
+		{
+			typeExpr: "bigint(10)",
+			expected: &schema.IntegerType{T: "bigint"},
+		},
+		{
+			typeExpr: "unsigned_big_int(10)",
+			expected: &schema.IntegerType{T: "unsigned big int"},
+		},
+		{
+			typeExpr: "int2(10)",
+			expected: &schema.IntegerType{T: "int2"},
+		},
+		{
+			typeExpr: "int8(10)",
+			expected: &schema.IntegerType{T: "int8"},
+		},
+		{
+			typeExpr: "real",
+			expected: &schema.FloatType{T: "real"},
+		},
+		{
+			typeExpr: "double",
+			expected: &schema.FloatType{T: "double"},
+		},
+		{
+			typeExpr: "double_precision",
+			expected: &schema.FloatType{T: "double precision"},
+		},
+		{
+			typeExpr: "float(10)",
+			expected: &schema.FloatType{T: "float"},
+		},
+		{
+			typeExpr: "text(10)",
+			expected: &schema.StringType{T: "text", Size: 10},
+		},
+		{
+			typeExpr: "character(10)",
+			expected: &schema.StringType{T: "character", Size: 10},
+		},
+		{
+			typeExpr: "varchar(10)",
+			expected: &schema.StringType{T: "varchar", Size: 10},
+		},
+		{
+			typeExpr: "varying_character",
+			expected: &schema.StringType{T: "varying character"},
+		},
+		{
+			typeExpr: "nchar(10)",
+			expected: &schema.StringType{T: "nchar", Size: 10},
+		},
+		{
+			typeExpr: "native_character",
+			expected: &schema.StringType{T: "native character"},
+		},
+		{
+			typeExpr: "nvarchar(10)",
+			expected: &schema.StringType{T: "nvarchar", Size: 10},
+		},
+		{
+			typeExpr: "clob(10)",
+			expected: &schema.StringType{T: "clob", Size: 10},
+		},
+		{
+			typeExpr: "blob(10)",
+			expected: &schema.BinaryType{T: "blob"},
+		},
+		{
+			typeExpr: "numeric(10)",
+			expected: &schema.DecimalType{T: "numeric", Precision: 10},
+		},
+		{
+			typeExpr: "decimal(10,5)",
+			expected: &schema.DecimalType{T: "decimal", Precision: 10, Scale: 5},
+		},
+		{
+			typeExpr: "boolean",
+			expected: &schema.BoolType{T: "boolean"},
+		},
+		{
+			typeExpr: "date",
+			expected: &schema.TimeType{T: "date"},
+		},
+		{
+			typeExpr: "datetime",
+			expected: &schema.TimeType{T: "datetime"},
+		},
+		{
+			typeExpr: "json",
+			expected: &schema.JSONType{T: "json"},
+		},
+		{
+			typeExpr: "uuid",
+			expected: &UUIDType{T: "uuid"},
+		},
+	} {
+		t.Run(tt.typeExpr, func(t *testing.T) {
+			// Simulates sqlspec.Column until we change its Type field.
+			type col struct {
+				Type *schemaspec.Type `spec:"type"`
+				schemaspec.DefaultExtension
+			}
+			var test struct {
+				Columns []*col `spec:"column"`
+			}
+			doc := fmt.Sprintf(`column {
+	type = %s
+}
+`, tt.typeExpr)
+			err := hclState.UnmarshalSpec([]byte(doc), &test)
+			require.NoError(t, err)
+			column := test.Columns[0]
+			typ, err := TypeRegistry.Type(column.Type, column.Extra.Attrs, parseRawType)
+			require.NoError(t, err)
+			require.EqualValues(t, tt.expected, typ)
+			spec, err := hclState.MarshalSpec(&test)
+			require.NoError(t, err)
+			require.EqualValues(t, string(hclwrite.Format([]byte(doc))), string(hclwrite.Format(spec)))
+		})
+	}
+}