Repository: ariga/atlas
Pull request #481 (2022-01-24 16:50:43)

sql/postgres: support marshal time types precision in HCL


Patch:
diff --git a/cmd/action/apply.go b/cmd/action/apply.go
index 5521f602..e9f8152a 100644
--- a/cmd/action/apply.go
+++ b/cmd/action/apply.go
@@ -19,6 +19,7 @@ var (
 		Web    bool
 		Addr   string
 		DryRun bool
+		Schema []string
 	}
 	// ApplyCmd represents the apply command.
 	ApplyCmd = &cobra.Command{
@@ -31,6 +32,7 @@ plan and prompt the user for approval.
 If run with the "--dry-run" flag, atlas will exit after printing out the planned migration.`,
 		Run: CmdApplyRun,
 		Example: `atlas schema apply -d "mysql://user:pass@tcp(localhost:3306)/dbname" -f atlas.hcl
+atlas schema apply -d "mysql://user:pass@tcp(localhost:3306)/" -f atlas.hcl --schema prod --schema staging
 atlas schema apply -d "mysql://user:pass@tcp(localhost:3306)/dbname" -f atlas.hcl --dry-run 
 atlas schema apply -d "mariadb://user:pass@tcp(localhost:3306)/dbname" -f atlas.hcl
 atlas schema apply --dsn "postgres://user:pass@host:port/dbname" -f atlas.hcl
@@ -50,6 +52,7 @@ func init() {
 	ApplyCmd.Flags().BoolVarP(&ApplyFlags.Web, "web", "w", false, "Open in a local Atlas UI")
 	ApplyCmd.Flags().BoolVarP(&ApplyFlags.DryRun, "dry-run", "", false, "Dry-run. Print SQL plan without prompting for execution")
 	ApplyCmd.Flags().StringVarP(&ApplyFlags.Addr, "addr", "", "127.0.0.1:5800", "used with -w, local address to bind the server to")
+	ApplyCmd.Flags().StringSliceVarP(&ApplyFlags.Schema, "schema", "s", nil, "Set schema name")
 	cobra.CheckErr(ApplyCmd.MarkFlagRequired("dsn"))
 	cobra.CheckErr(ApplyCmd.MarkFlagRequired("file"))
 }
@@ -67,16 +70,34 @@ func CmdApplyRun(cmd *cobra.Command, args []string) {
 
 func applyRun(d *Driver, dsn string, file string, dryRun bool) {
 	ctx := context.Background()
-	name, err := SchemaNameFromDSN(dsn)
-	cobra.CheckErr(err)
-	s, err := d.InspectSchema(ctx, name, nil)
+	schemas := ApplyFlags.Schema
+	if n, err := SchemaNameFromDSN(dsn); n != "" {
+		cobra.CheckErr(err)
+		schemas = append(schemas, n)
+	}
+	realm, err := d.InspectRealm(ctx, &schema.InspectRealmOption{
+		Schemas: schemas,
+	})
 	cobra.CheckErr(err)
 	f, err := ioutil.ReadFile(file)
 	cobra.CheckErr(err)
-	var desired schema.Schema
+	var desired schema.Realm
 	err = d.UnmarshalSpec(f, &desired)
+	if len(schemas) > 0 {
+		// Validate all schemas in file were selected by user.
+		sm := make(map[string]bool, len(schemas))
+		for _, s := range schemas {
+			sm[s] = true
+		}
+		for _, s := range desired.Schemas {
+			if !sm[s.Name] {
+				schemaCmd.Printf("schema %q from file %q was not selected %q, all schemas defined in file must be selected\n", s.Name, file, schemas)
+				return
+			}
+		}
+	}
 	cobra.CheckErr(err)
-	changes, err := d.SchemaDiff(s, &desired)
+	changes, err := d.RealmDiff(realm, &desired)
 	cobra.CheckErr(err)
 	if len(changes) == 0 {
 		schemaCmd.Println("Schema is synced, no changes to be made")
diff --git a/cmd/action/diff.go b/cmd/action/diff.go
index e4345b18..df2fb77c 100644
--- a/cmd/action/diff.go
+++ b/cmd/action/diff.go
@@ -52,6 +52,13 @@ func CmdDiffRun(cmd *cobra.Command, args []string) {
 	cobra.CheckErr(err)
 	toSchema, err := toDriver.InspectSchema(ctx, toName, nil)
 	cobra.CheckErr(err)
+	// SchemaDiff checks for name equality which is irrelevant in the case
+	// the user wants to compare their contents, if the names are different
+	// we reset them to allow the comparison.
+	if fromName != toName {
+		toSchema.Name = ""
+		fromSchema.Name = ""
+	}
 	diff, err := toDriver.SchemaDiff(fromSchema, toSchema)
 	cobra.CheckErr(err)
 	p, err := toDriver.PlanChanges(ctx, "plan", diff)
diff --git a/cmd/action/fmt.go b/cmd/action/fmt.go
index 9f5afe5f..27b3f7bc 100644
--- a/cmd/action/fmt.go
+++ b/cmd/action/fmt.go
@@ -19,7 +19,7 @@ import (
 var (
 	// FmtCmd represents the fmt command.
 	FmtCmd = &cobra.Command{
-		Use:   "fmt [path]",
+		Use:   "fmt [path ...]",
 		Short: "Formats Atlas HCL files",
 		Long: "`atlas schema fmt`" + ` formats all ".hcl" files under the given path using
 cannonical HCL layout style as defined by the github.com/hashicorp/hcl/v2/hclwrite package. 
diff --git a/cmd/action/inspect.go b/cmd/action/inspect.go
index 2a133a3f..4dd48c55 100644
--- a/cmd/action/inspect.go
+++ b/cmd/action/inspect.go
@@ -26,7 +26,11 @@ saved to a file, commonly by redirecting the output to a file named with a ".hcl
 	atlas schema inspect -d "mysql://user:pass@tcp(localhost:3306)/dbname" > atlas.hcl
 
 This file can then be edited and used with the` + " `atlas schema apply` " + `command to plan
-and execute schema migrations against the given database. 
+and execute schema migrations against the given database. In cases where users wish to inspect
+all multiple schemas in a given database (for instance a MySQL server may contain multiple named
+databases), omit the relevant part from the dsn, e.g. "mysql://user:pass@tcp(localhost:3306)/".
+To select specific schemas from the databases, users may use the "--schema" (or "-s" shorthand)
+flag.
 	`,
 		Run: CmdInspectRun,
 		Example: `
diff --git a/doc/md/cli.md b/doc/md/cli.md
index b0a9a032..189e2fb7 100644
--- a/doc/md/cli.md
+++ b/doc/md/cli.md
@@ -79,6 +79,7 @@ If run with the "--dry-run" flag, atlas will exit after printing out the planned
 
 ```
 atlas schema apply -d "mysql://user:pass@tcp(localhost:3306)/dbname" -f atlas.hcl
+atlas schema apply -d "mysql://user:pass@tcp(localhost:3306)/" -f atlas.hcl --schema prod --schema staging
 atlas schema apply -d "mysql://user:pass@tcp(localhost:3306)/dbname" -f atlas.hcl --dry-run 
 atlas schema apply -d "mariadb://user:pass@tcp(localhost:3306)/dbname" -f atlas.hcl
 atlas schema apply --dsn "postgres://user:pass@host:port/dbname" -f atlas.hcl
@@ -86,11 +87,12 @@ atlas schema apply -d "sqlite://file:ex1.db?_fk=1" -f atlas.hcl
 ```
 #### Flags
 ```
-      --addr string   used with -w, local address to bind the server to (default "127.0.0.1:5800")
-      --dry-run       Dry-run. Print SQL plan without prompting for execution
-  -d, --dsn string    [driver://username:password@protocol(address)/dbname?param=value] Select data source using the dsn format
-  -f, --file string   [/path/to/file] file containing schema
-  -w, --web           Open in a local Atlas UI
+      --addr string      used with -w, local address to bind the server to (default "127.0.0.1:5800")
+      --dry-run          Dry-run. Print SQL plan without prompting for execution
+  -d, --dsn string       [driver://username:password@protocol(address)/dbname?param=value] Select data source using the dsn format
+  -f, --file string      [/path/to/file] file containing schema
+  -s, --schema strings   Set schema name
+  -w, --web              Open in a local Atlas UI
 
 ```
 
@@ -123,7 +125,7 @@ Formats Atlas HCL files
 
 #### Usage
 ```
-atlas schema fmt [path]
+atlas schema fmt [path ...]
 ```
 
 #### Details
@@ -153,7 +155,11 @@ saved to a file, commonly by redirecting the output to a file named with a ".hcl
 	atlas schema inspect -d "mysql://user:pass@tcp(localhost:3306)/dbname" > atlas.hcl
 
 This file can then be edited and used with the `atlas schema apply` command to plan
-and execute schema migrations against the given database. 
+and execute schema migrations against the given database. In cases where users wish to inspect
+all multiple schemas in a given database (for instance a MySQL server may contain multiple named
+databases), omit the relevant part from the dsn, e.g. "mysql://user:pass@tcp(localhost:3306)/".
+To select specific schemas from the databases, users may use the "--schema" (or "-s" shorthand)
+flag.
 	
 
 #### Example
diff --git a/schema/schemaspec/extension.go b/schema/schemaspec/extension.go
index f1f713f0..c247a434 100644
--- a/schema/schemaspec/extension.go
+++ b/schema/schemaspec/extension.go
@@ -291,7 +291,26 @@ func setPtr(field reflect.Value, val Value) error {
 			return nil
 		}
 	}
-	return fmt.Errorf("unhandled pointer type %T", val)
+	if field.IsNil() {
+		field.Set(reflect.New(field.Type().Elem()))
+	}
+	switch e := field.Elem(); e.Kind() {
+	case reflect.Bool:
+		b, err := BoolVal(val)
+		if err != nil {
+			return err
+		}
+		e.SetBool(b)
+	case reflect.String:
+		s, err := StrVal(val)
+		if err != nil {
+			return err
+		}
+		e.SetString(s)
+	default:
+		return fmt.Errorf("unhandled pointer type %T", val)
+	}
+	return nil
 }
 
 // setSliceAttr sets the value of attr to the slice field. This function expects both the target field
@@ -398,14 +417,17 @@ func (r *Resource) Scan(ext interface{}) error {
 
 func scanPtr(key string, r *Resource, field reflect.Value) error {
 	attr := &Attr{K: key}
-	switch e := field.Elem(); e.Type() {
-	case reflect.TypeOf(LiteralValue{}):
-		attr.V = &LiteralValue{V: e.FieldByName("V").String()}
-	case reflect.TypeOf(Ref{}):
-		attr.V = &Ref{V: e.FieldByName("V").String()}
-	case reflect.TypeOf(Type{}):
-		t := e.Interface().(Type)
-		attr.V = &t
+	switch e := field.Interface().(type) {
+	case *LiteralValue:
+		attr.V = e
+	case *Ref:
+		attr.V = e
+	case *Type:
+		attr.V = e
+	case *bool:
+		attr.V = &LiteralValue{V: strconv.FormatBool(*e)}
+	case *string:
+		attr.V = &LiteralValue{V: strconv.Quote(*e)}
 	default:
 		return fmt.Errorf("schemaspec: unsupported pointer to %s", e)
 	}
diff --git a/schema/schemaspec/schemahcl/hcl.go b/schema/schemaspec/schemahcl/hcl.go
index 21e6b6be..943db72a 100644
--- a/schema/schemaspec/schemahcl/hcl.go
+++ b/schema/schemaspec/schemahcl/hcl.go
@@ -278,6 +278,11 @@ func (s *state) writeAttr(attr *schemaspec.Attr, body *hclwrite.Body) error {
 		fnc := fmt.Sprintf("sql(%q)", v.X)
 		body.SetAttributeRaw(attr.K, hclRawTokens(fnc))
 	case *schemaspec.ListValue:
+		// Skip scanning nil slices ([]T(nil)) by default. Users that
+		// want to print empty lists, should use make([]T, 0) instead.
+		if v.V == nil {
+			return nil
+		}
 		lst := make([]string, 0, len(v.V))
 		for _, item := range v.V {
 			switch v := item.(type) {
@@ -374,14 +379,11 @@ func hclRawList(items []string) hclwrite.Tokens {
 		Type:  hclsyntax.TokenOBrack,
 		Bytes: []byte("["),
 	}}
-	for _, item := range items {
-		t = append(t, &hclwrite.Token{
-			Type:  hclsyntax.TokenIdent,
-			Bytes: []byte(item),
-		}, &hclwrite.Token{
-			Type:  hclsyntax.TokenComma,
-			Bytes: []byte(","),
-		})
+	for i, item := range items {
+		if i > 0 {
+			t = append(t, &hclwrite.Token{Type: hclsyntax.TokenComma, Bytes: []byte(",")})
+		}
+		t = append(t, &hclwrite.Token{Type: hclsyntax.TokenIdent, Bytes: []byte(item)})
 	}
 	t = append(t, &hclwrite.Token{
 		Type:  hclsyntax.TokenCBrack,
diff --git a/sql/internal/specutil/convert.go b/sql/internal/specutil/convert.go
index 67b14206..83ad0e3a 100644
--- a/sql/internal/specutil/convert.go
+++ b/sql/internal/specutil/convert.go
@@ -172,14 +172,17 @@ func Index(spec *sqlspec.Index, parent *schema.Table) (*schema.Index, error) {
 		}
 	case m > 0:
 		for i, p := range spec.Parts {
-			part := &schema.IndexPart{SeqNo: i, Desc: p.Desc}
+			part := &schema.IndexPart{SeqNo: i}
+			if p.Desc != nil {
+				part.Desc = *p.Desc
+			}
 			switch {
-			case p.Column == nil && p.Expr == "":
+			case p.Column == nil && p.Expr == nil:
 				return nil, fmt.Errorf(`"column" or "expr" are required for index %q at position %d`, spec.Name, i)
-			case p.Column != nil && p.Expr != "":
+			case p.Column != nil && p.Expr != nil:
 				return nil, fmt.Errorf(`cannot use both "column" and "expr" in index %q at position %d`, spec.Name, i)
-			case p.Expr != "":
-				part.X = &schema.RawExpr{X: p.Expr}
+			case p.Expr != nil:
+				part.X = &schema.RawExpr{X: *p.Expr}
 			case p.Column != nil:
 				c, err := column(parent, p.Column)
 				if err != nil {
@@ -431,20 +434,43 @@ func normalizeQuotes(s string) (string, error) {
 
 // FromIndex converts schema.Index to sqlspec.Index.
 func FromIndex(idx *schema.Index) (*sqlspec.Index, error) {
-	c := make([]*schemaspec.Ref, 0, len(idx.Parts))
-	for _, p := range idx.Parts {
-		if p.C == nil {
-			return nil, errors.New("index expression is not supported")
+	spec := &sqlspec.Index{Name: idx.Name, Unique: idx.Unique}
+	convertCommentFromSchema(idx.Attrs, &spec.Extra.Attrs)
+	if parts, ok := columnsOnly(idx); ok {
+		spec.Columns = parts
+		return spec, nil
+	}
+	spec.Parts = make([]*sqlspec.IndexPart, len(idx.Parts))
+	for i, p := range idx.Parts {
+		part := &sqlspec.IndexPart{Desc: &p.Desc}
+		switch {
+		case p.C == nil && p.X == nil:
+			return nil, fmt.Errorf("missing column or expression for key part of index %q", idx.Name)
+		case p.C != nil && p.X != nil:
+			return nil, fmt.Errorf("multiple key part definitions for index %q", idx.Name)
+		case p.C != nil:
+			part.Column = colRef(p.C.Name, idx.Table.Name)
+		case p.X != nil:
+			x, ok := p.X.(*schema.RawExpr)
+			if !ok {
+				return nil, fmt.Errorf("unexpected expression %T for index %q", p.X, idx.Name)
+			}
+			part.Expr = &x.X
 		}
-		c = append(c, colRef(p.C.Name, idx.Table.Name))
+		spec.Parts[i] = part
 	}
-	i := &sqlspec.Index{
-		Name:    idx.Name,
-		Unique:  idx.Unique,
-		Columns: c,
+	return spec, nil
+}
+
+func columnsOnly(idx *schema.Index) ([]*schemaspec.Ref, bool) {
+	parts := make([]*schemaspec.Ref, len(idx.Parts))
+	for i, p := range idx.Parts {
+		if p.C == nil || p.Desc {
+			return nil, false
+		}
+		parts[i] = colRef(p.C.Name, idx.Table.Name)
 	}
-	convertCommentFromSchema(idx.Attrs, &i.Extra.Attrs)
-	return i, nil
+	return parts, true
 }
 
 // FromForeignKey converts schema.ForeignKey to sqlspec.ForeignKey.
diff --git a/sql/mysql/migrate.go b/sql/mysql/migrate.go
index c116aa01..26342cc9 100644
--- a/sql/mysql/migrate.go
+++ b/sql/mysql/migrate.go
@@ -377,7 +377,10 @@ func (s *state) alterTable(t *schema.Table, changes []schema.Change) error {
 			reverse.Comma().P("DROP FOREIGN KEY").Ident(change.F.Symbol)
 		case *schema.DropForeignKey:
 			b.P("DROP FOREIGN KEY").Ident(change.F.Symbol)
-			reversible = false
+			reverse.Comma().P("ADD")
+			if err := s.fks(reverse, change.F); err != nil {
+				errors = append(errors, err.Error())
+			}
 		case *schema.AddAttr:
 			s.tableAttr(b, change, change.A)
 			// Unsupported reverse operation.
diff --git a/sql/postgres/convert.go b/sql/postgres/convert.go
index 5e3fba04..1e619004 100644
--- a/sql/postgres/convert.go
+++ b/sql/postgres/convert.go
@@ -195,6 +195,8 @@ type columnDesc struct {
 	parts         []string
 }
 
+var reDigits = regexp.MustCompile(`\d`)
+
 func parseColumn(s string) (*columnDesc, error) {
 	parts := strings.FieldsFunc(s, func(r rune) bool {
 		return r == '(' || r == ')' || r == ' ' || r == ','
@@ -232,6 +234,21 @@ func parseColumn(s string) (*columnDesc, error) {
 		c.precision = 53
 	case TypeReal, TypeFloat4:
 		c.precision = 24
+	case TypeTime, TypeTimestamp, TypeTimestampTZ:
+		// If the second part is only one digit it is the precision argument.
+		// For cases like "timestamp(4) with time zone" make sure to not drop the rest of the type definition.
+		offset := 1
+		if len(parts) > 1 && reDigits.MatchString(parts[1]) {
+			offset = 2
+			c.timePrecision, err = strconv.ParseInt(parts[1], 10, 64)
+			if err != nil {
+				return nil, fmt.Errorf("postgres: parse time precision %q: %w", parts[1], err)
+			}
+		}
+		// Append time zone part (if present).
+		if len(parts) > offset {
+			c.typ = fmt.Sprintf("%s %s", c.typ, strings.Join(parts[offset:], " "))
+		}
 	default:
 		c.typ = s
 	}
diff --git a/sql/postgres/migrate.go b/sql/postgres/migrate.go
index 67db315f..b5e6a08d 100644
--- a/sql/postgres/migrate.go
+++ b/sql/postgres/migrate.go
@@ -300,7 +300,8 @@ func (s *state) alterTable(t *schema.Table, changes []schema.Change) error {
 			reverse.Comma().P("DROP CONSTRAINT").Ident(change.F.Symbol)
 		case *schema.DropForeignKey:
 			b.P("DROP CONSTRAINT").Ident(change.F.Symbol)
-			reversible = false
+			reverse.P("ADD")
+			s.fks(reverse, change.F)
 		case *schema.AddCheck:
 			check(b.P("ADD"), change.C)
 			// Reverse operation is supported if
@@ -398,10 +399,13 @@ func (*state) indexComment(t *schema.Table, idx *schema.Index, to, from string)
 }
 
 func (s *state) dropIndexes(t *schema.Table, indexes ...*schema.Index) {
-	for _, idx := range indexes {
+	rs := &state{conn: s.conn}
+	rs.addIndexes(t, indexes...)
+	for i, idx := range indexes {
 		s.append(&migrate.Change{
-			Cmd:     Build("DROP INDEX").Ident(idx.Name).String(),
-			Comment: fmt.Sprintf("Drop index %q to table: %q", idx.Name, t.Name),
+			Cmd:     rs.Changes[i].Reverse,
+			Comment: fmt.Sprintf("Drop index %q from table: %q", idx.Name, t.Name),
+			Reverse: rs.Changes[i].Cmd,
 		})
 	}
 }
@@ -563,7 +567,7 @@ func (s *state) alterColumn(b *sqlx.Builder, k schema.ChangeKind, c *schema.Colu
 		case k.Is(schema.ChangeAttr):
 			id, ok := identity(c.Attrs)
 			if !ok {
-				return fmt.Errorf("unexpected attribute change: %v", c.Attrs)
+				return fmt.Errorf("unexpected attribute change (expect IDENTITY): %v", c.Attrs)
 			}
 			// The syntax for altering identity columns is identical to sequence_options.
 			// https://www.postgresql.org/docs/current/sql-altersequence.html
diff --git a/sql/postgres/sqlspec.go b/sql/postgres/sqlspec.go
index d7005f38..c6132190 100644
--- a/sql/postgres/sqlspec.go
+++ b/sql/postgres/sqlspec.go
@@ -90,6 +90,18 @@ func MarshalSpec(v interface{}, marshaler schemaspec.Marshaler) ([]byte, error)
 	return marshaler.MarshalSpec(&d)
 }
 
+var (
+	hclState = schemahcl.New(schemahcl.WithTypes(TypeRegistry.Specs()))
+	// UnmarshalHCL unmarshals an Atlas HCL DDL document into v.
+	UnmarshalHCL = schemaspec.UnmarshalerFunc(func(bytes []byte, i interface{}) error {
+		return UnmarshalSpec(bytes, hclState, i)
+	})
+	// MarshalHCL marshals v into an Atlas HCL DDL document.
+	MarshalHCL = schemaspec.MarshalerFunc(func(v interface{}) ([]byte, error) {
+		return MarshalSpec(v, hclState)
+	})
+)
+
 // Realm converts the schemas and tables of the doc into a schema.Realm.
 func Realm(schemas []*sqlspec.Schema, tables []*sqlspec.Table, enums []*Enum) (*schema.Realm, error) {
 	r := &schema.Realm{}
@@ -186,7 +198,17 @@ func fixDefaultQuotes(value schemaspec.Value) error {
 
 // convertColumnType converts a sqlspec.Column into a concrete Postgres schema.Type.
 func convertColumnType(spec *sqlspec.Column) (schema.Type, error) {
-	return TypeRegistry.Type(spec.Type, spec.Extra.Attrs)
+	typ, err := TypeRegistry.Type(spec.Type, spec.Extra.Attrs)
+	if err != nil {
+		return nil, err
+	}
+	// Handle default values for time precision types.
+	if t, ok := typ.(*schema.TimeType); ok && strings.HasPrefix(t.T, "time") {
+		if _, ok := attr(spec.Type, "precision"); !ok {
+			t.Precision = 6
+		}
+	}
+	return typ, nil
 }
 
 // convertEnums converts possibly referenced column types (like enums) to
@@ -336,13 +358,33 @@ var TypeRegistry = specutil.NewRegistry(
 		specutil.TypeSpec(TypePath),
 		specutil.TypeSpec(TypePoint),
 		specutil.TypeSpec(TypeDate),
-		specutil.TypeSpec(TypeTime),
-		specutil.AliasTypeSpec("time_with_time_zone", TypeTimeWTZ),
-		specutil.AliasTypeSpec("time_without_time_zone", TypeTimeWOTZ),
-		specutil.TypeSpec(TypeTimestampTZ),
-		specutil.TypeSpec(TypeTimestamp),
-		specutil.AliasTypeSpec("timestamp_with_time_zone", TypeTimestampWTZ),
-		specutil.AliasTypeSpec("timestamp_without_time_zone", TypeTimestampWOTZ),
+		specutil.TypeSpec(TypeTime, specutil.WithAttributes(precisionTypeAttr())),
+		specutil.AliasTypeSpec(
+			"time_with_time_zone",
+			TypeTimeWTZ,
+			specutil.WithAttributes(precisionTypeAttr()),
+			specutil.WithPrinter(timePrinter),
+		),
+		specutil.AliasTypeSpec(
+			"time_without_time_zone",
+			TypeTimeWOTZ,
+			specutil.WithAttributes(precisionTypeAttr()),
+			specutil.WithPrinter(timePrinter),
+		),
+		specutil.TypeSpec(TypeTimestampTZ, specutil.WithAttributes(precisionTypeAttr())),
+		specutil.TypeSpec(TypeTimestamp, specutil.WithAttributes(precisionTypeAttr())),
+		specutil.AliasTypeSpec(
+			"timestamp_with_time_zone",
+			TypeTimestampWTZ,
+			specutil.WithAttributes(precisionTypeAttr()),
+			specutil.WithPrinter(timePrinter),
+		),
+		specutil.AliasTypeSpec(
+			"timestamp_without_time_zone",
+			TypeTimestampWOTZ,
+			specutil.WithAttributes(precisionTypeAttr()),
+			specutil.WithPrinter(timePrinter),
+		),
 		specutil.AliasTypeSpec("double_precision", TypeDouble),
 		specutil.TypeSpec(TypeReal),
 		specutil.TypeSpec(TypeFloat8),
@@ -365,14 +407,32 @@ var TypeRegistry = specutil.NewRegistry(
 	),
 )
 
-var (
-	hclState = schemahcl.New(schemahcl.WithTypes(TypeRegistry.Specs()))
-	// UnmarshalHCL unmarshals an Atlas HCL DDL document into v.
-	UnmarshalHCL = schemaspec.UnmarshalerFunc(func(bytes []byte, i interface{}) error {
-		return UnmarshalSpec(bytes, hclState, i)
-	})
-	// MarshalHCL marshals v into an Atlas HCL DDL document.
-	MarshalHCL = schemaspec.MarshalerFunc(func(v interface{}) ([]byte, error) {
-		return MarshalSpec(v, hclState)
-	})
-)
+func precisionTypeAttr() *schemaspec.TypeAttr {
+	return &schemaspec.TypeAttr{
+		Name:     "precision",
+		Kind:     reflect.Int,
+		Required: false,
+	}
+}
+
+func timePrinter(typ *schemaspec.Type) (string, error) {
+	a, ok := attr(typ, "precision")
+	if !ok {
+		return typ.T, nil
+	}
+	p, err := a.Int()
+	if err != nil {
+		return "", fmt.Errorf(`postgres: parsing attribute "precision": %w`, err)
+	}
+	parts := strings.Split(typ.T, " ")
+	return fmt.Sprintf("%s(%d)%s", parts[0], p, strings.Join(parts[1:], " ")), nil
+}
+
+func attr(typ *schemaspec.Type, key string) (*schemaspec.Attr, bool) {
+	for _, a := range typ.Attrs {
+		if a.K == key {
+			return a, true
+		}
+	}
+	return nil, false
+}
diff --git a/sql/sqlspec/sqlspec.go b/sql/sqlspec/sqlspec.go
index b004b00a..3298ef24 100644
--- a/sql/sqlspec/sqlspec.go
+++ b/sql/sqlspec/sqlspec.go
@@ -51,9 +51,9 @@ type (
 
 	// IndexPart holds a specification for the index key part.
 	IndexPart struct {
-		Desc   bool            `spec:"desc"`
+		Desc   *bool           `spec:"desc"`
 		Column *schemaspec.Ref `spec:"column"`
-		Expr   string          `spec:"expr"`
+		Expr   *string         `spec:"expr"`
 		schemaspec.DefaultExtension
 	}
 


Test patch:
diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go
index 9e1f0434..47a314e5 100644
--- a/internal/integration/integration_test.go
+++ b/internal/integration/integration_test.go
@@ -171,6 +171,40 @@ func testCLISchemaInspect(t T, h string, dsn string, unmarshaler schemaspec.Unma
 	require.Equal(t, expected, actual)
 }
 
+func testCLIMultiSchemaApply(t T, h string, dsn string, schemas []string, unmarshaler schemaspec.Unmarshaler) {
+	// Required to have a clean "stderr" while running first time.
+	err := exec.Command("go", "run", "-mod=mod", "ariga.io/atlas/cmd/atlas").Run()
+	f := "atlas.hcl"
+	err = ioutil.WriteFile(f, []byte(h), 0644)
+	require.NoError(t, err)
+	t.Cleanup(func() {
+		os.Remove("atlas.hcl")
+	})
+	require.NoError(t, err)
+	var expected schema.Realm
+	err = unmarshaler.UnmarshalSpec([]byte(h), &expected)
+	require.NoError(t, err)
+	cmd := exec.Command("go", "run", "ariga.io/atlas/cmd/atlas",
+		"schema",
+		"apply",
+		"-f",
+		"atlas.hcl",
+		"-d",
+		dsn,
+		"-s",
+		strings.Join(schemas, ","),
+	)
+	stdout, stderr := bytes.NewBuffer(nil), bytes.NewBuffer(nil)
+	cmd.Stderr = stderr
+	cmd.Stdout = stdout
+	stdin, err := cmd.StdinPipe()
+	require.NoError(t, err)
+	defer stdin.Close()
+	_, err = io.WriteString(stdin, "\n")
+	require.NoError(t, cmd.Run(), stderr.String())
+	require.Contains(t, stdout.String(), `-- Add new schema named "test2"`)
+}
+
 func testCLIMultiSchemaInspect(t T, h string, dsn string, schemas []string, unmarshaler schemaspec.Unmarshaler) {
 	// Required to have a clean "stderr" while running first time.
 	err := exec.Command("go", "run", "-mod=mod", "ariga.io/atlas/cmd/atlas").Run()
diff --git a/internal/integration/mysql_test.go b/internal/integration/mysql_test.go
index 1e1420de..be7898c5 100644
--- a/internal/integration/mysql_test.go
+++ b/internal/integration/mysql_test.go
@@ -552,8 +552,7 @@ func TestMySQL_CLI(t *testing.T) {
 }
 
 func TestMySQL_CLI_MultiSchema(t *testing.T) {
-	t.Run("SchemaInspect", func(t *testing.T) {
-		h := `	
+	h := `	
 			schema "test" {
 				charset   = "%s"
 				collation = "%s"
@@ -580,6 +579,7 @@ func TestMySQL_CLI_MultiSchema(t *testing.T) {
 					columns = [table.users.column.id]
 				}
 			}`
+	t.Run("SchemaInspect", func(t *testing.T) {
 		myRun(t, func(t *myTest) {
 			t.dropDB("test2")
 			t.dropTables("users")
@@ -588,6 +588,15 @@ func TestMySQL_CLI_MultiSchema(t *testing.T) {
 			testCLIMultiSchemaInspect(t, fmt.Sprintf(h, charset.V, collate.V, charset.V, collate.V), t.dsn(""), []string{"test", "test2"}, mysql.UnmarshalHCL)
 		})
 	})
+	t.Run("SchemaApply", func(t *testing.T) {
+		myRun(t, func(t *myTest) {
+			t.dropDB("test2")
+			t.dropTables("users")
+			attrs := t.defaultAttrs()
+			charset, collate := attrs[0].(*schema.Charset), attrs[1].(*schema.Collation)
+			testCLIMultiSchemaApply(t, fmt.Sprintf(h, charset.V, collate.V, charset.V, collate.V), t.dsn(""), []string{"test", "test2"}, mysql.UnmarshalHCL)
+		})
+	})
 }
 
 func TestMySQL_HCL_Realm(t *testing.T) {
@@ -1345,9 +1354,13 @@ func TestMySQL_Script(t *testing.T) {
 						t.Fatal(err)
 					}
 				})
+				// Store the testscript.T for later use.
+				// See "only" function below.
+				env.Values[keyT] = env.T()
 				return nil
 			},
 			Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){
+				"only":    t.cmdOnly,
 				"apply":   t.cmdApply,
 				"exist":   t.cmdExist,
 				"synced":  t.cmdSynced,
@@ -1357,7 +1370,27 @@ func TestMySQL_Script(t *testing.T) {
 	})
 }
 
-func (t *myTest) cmdCmpShow(ts *testscript.TestScript, neg bool, args []string) {
+var keyT struct{}
+
+// cmdOnly executes only tests that their driver version matches the given pattern.
+// For example, "only 8" or "only 8 maria*"
+func (t *myTest) cmdOnly(ts *testscript.TestScript, neg bool, args []string) {
+	if neg {
+		ts.Fatalf("unsupported: ! only")
+	}
+	for i := range args {
+		re, rerr := regexp.Compile(`(?mi)` + args[i])
+		ts.Check(rerr)
+		if re.MatchString(t.version) {
+			return
+		}
+	}
+	// This is not an elegant way to get the created testing.T for the script,
+	// but we need some workaround to get it in order to skip specific tests.
+	ts.Value(keyT).(testscript.T).Skip("skip version", t.version)
+}
+
+func (t *myTest) cmdCmpShow(ts *testscript.TestScript, _ bool, args []string) {
 	if len(args) < 2 {
 		ts.Fatalf("invalid number of args to 'cmpshow': %d", len(args))
 	}
diff --git a/internal/integration/postgres_test.go b/internal/integration/postgres_test.go
index 9227c9dc..7aa16d52 100644
--- a/internal/integration/postgres_test.go
+++ b/internal/integration/postgres_test.go
@@ -481,8 +481,7 @@ func TestPostgres_CLI(t *testing.T) {
 }
 
 func TestPostgres_CLI_MultiSchema(t *testing.T) {
-	t.Run("SchemaInspect", func(t *testing.T) {
-		h := `
+	h := `
 			schema "public" {	
 			}
 			table "users" {
@@ -505,12 +504,20 @@ func TestPostgres_CLI_MultiSchema(t *testing.T) {
 					columns = [table.users.column.id]
 				}
 			}`
+	t.Run("SchemaInspect", func(t *testing.T) {
 		pgRun(t, func(t *pgTest) {
 			t.dropSchemas("test2")
 			t.dropTables("users")
 			testCLIMultiSchemaInspect(t, h, t.dsn(), []string{"public", "test2"}, postgres.UnmarshalHCL)
 		})
 	})
+	t.Run("SchemaApply", func(t *testing.T) {
+		pgRun(t, func(t *pgTest) {
+			t.dropSchemas("test2")
+			t.dropTables("users")
+			testCLIMultiSchemaApply(t, h, t.dsn(), []string{"public", "test2"}, postgres.UnmarshalHCL)
+		})
+	})
 }
 
 func TestPostgres_DefaultsHCL(t *testing.T) {
diff --git a/internal/integration/testdata/mysql/index-desc.txt b/internal/integration/testdata/mysql/index-desc.txt
new file mode 100644
index 00000000..f57273a8
--- /dev/null
+++ b/internal/integration/testdata/mysql/index-desc.txt
@@ -0,0 +1,119 @@
+# Each test runs on a clean database.
+
+# Run this test only on MySQL 8 as it is not supported by other versions.
+only 8
+
+# Apply schema "1.hcl" on fresh database.
+apply 1.hcl
+cmpshow users 1.sql
+
+# Drop the "DESC" option from the key part.
+apply 2.hcl
+cmpshow users 2.sql
+# Use of "columns" instead of "on" should not trigger a change.
+synced 2-no-change.hcl
+
+apply 3.hcl
+cmpshow users 3.sql
+
+# Below files represent HCL and SQL. File names defined their index in
+# execution order. 1.hcl is executed first, 2.hcl executed second, etc.
+-- 1.hcl --
+schema "$db" {
+    charset = "$charset"
+    collation = "$collate"
+}
+
+table "users" {
+    schema = schema.$db
+    column "rank" {
+        type = bigint
+    }
+    index "rank_idx" {
+        on {
+            desc   = true
+            column = table.users.column.rank
+        }
+    }
+}
+
+-- 1.sql --
+CREATE TABLE `users` (
+  `rank` bigint NOT NULL,
+  KEY `rank_idx` (`rank` DESC)
+)
+
+-- 2.hcl --
+schema "$db" {
+    charset = "$charset"
+    collation = "$collate"
+}
+
+table "users" {
+    schema = schema.$db
+    column "rank" {
+        type = bigint
+    }
+    index "rank_idx" {
+        on {
+            column = table.users.column.rank
+        }
+    }
+}
+
+-- 2.sql --
+CREATE TABLE `users` (
+  `rank` bigint NOT NULL,
+  KEY `rank_idx` (`rank`)
+)
+
+
+-- 2-no-change.hcl --
+schema "$db" {
+    charset = "$charset"
+    collation = "$collate"
+}
+
+table "users" {
+    schema = schema.$db
+    column "rank" {
+        type = bigint
+    }
+    index "rank_idx" {
+        columns = [
+            table.users.column.rank,
+        ]
+    }
+}
+
+-- 3.hcl --
+schema "$db" {
+    charset = "$charset"
+    collation = "$collate"
+}
+
+table "users" {
+    schema = schema.$db
+    column "rank" {
+        type = bigint
+    }
+    column "score" {
+        type = int
+    }
+    index "rank_score_idx" {
+        on {
+            column = table.users.column.rank
+        }
+        on {
+            column = table.users.column.score
+            desc = true
+        }
+    }
+}
+
+-- 3.sql --
+CREATE TABLE `users` (
+  `rank` bigint NOT NULL,
+  `score` int NOT NULL,
+  KEY `rank_score_idx` (`rank`,`score` DESC)
+)
diff --git a/schema/schemaspec/extension_test.go b/schema/schemaspec/extension_test.go
index 7fdb2ab3..6d7664f5 100644
--- a/schema/schemaspec/extension_test.go
+++ b/schema/schemaspec/extension_test.go
@@ -15,6 +15,7 @@ type OwnerBlock struct {
 	FirstName string                   `spec:"first_name"`
 	Born      int                      `spec:"born"`
 	Active    bool                     `spec:"active"`
+	BoolPtr   *bool                    `spec:"bool_ptr"`
 	Lit       *schemaspec.LiteralValue `spec:"lit"`
 }
 
@@ -45,6 +46,7 @@ func TestExtension(t *testing.T) {
 			schemautil.StrLitAttr("first_name", "tzuri"),
 			schemautil.LitAttr("born", "2019"),
 			schemautil.LitAttr("active", "true"),
+			schemautil.LitAttr("bool_ptr", "true"),
 			schemautil.LitAttr("lit", "1000"),
 			schemautil.LitAttr("extra", "true"),
 		},
@@ -62,6 +64,8 @@ func TestExtension(t *testing.T) {
 	require.EqualValues(t, "name", owner.ID)
 	require.EqualValues(t, 2019, owner.Born)
 	require.EqualValues(t, true, owner.Active)
+	require.NotNil(t, owner.BoolPtr)
+	require.EqualValues(t, true, *owner.BoolPtr)
 	require.EqualValues(t, schemautil.LitAttr("lit", "1000").V, owner.Lit)
 	attr, ok := owner.Remain().Attr("extra")
 	require.True(t, ok)
diff --git a/schema/schemaspec/schemahcl/context_test.go b/schema/schemaspec/schemahcl/context_test.go
index 4ef1b292..20c1b771 100644
--- a/schema/schemaspec/schemahcl/context_test.go
+++ b/schema/schemaspec/schemahcl/context_test.go
@@ -323,10 +323,10 @@ func TestSchemaRefParse(t *testing.T) {
 	require.NoError(t, err)
 	expected :=
 		`point {
-  z = [a, ]
+  z = [a]
 }
 point {
-  z = [b, ]
+  z = [b]
 }
 `
 	require.Equal(t, expected, string(b))
diff --git a/schema/schemaspec/schemahcl/hcl_test.go b/schema/schemaspec/schemahcl/hcl_test.go
index 49cb3c95..a086d6c9 100644
--- a/schema/schemaspec/schemahcl/hcl_test.go
+++ b/schema/schemaspec/schemahcl/hcl_test.go
@@ -14,8 +14,8 @@ func TestAttributes(t *testing.T) {
 	f := `i  = 1
 b  = true
 s  = "hello, world"
-sl = ["hello", "world", ]
-bl = [true, false, ]
+sl = ["hello", "world"]
+bl = [true, false]
 `
 	var test struct {
 		Int        int      `spec:"i"`
diff --git a/sql/mysql/migrate_test.go b/sql/mysql/migrate_test.go
index 94dc2e72..a33778dd 100644
--- a/sql/mysql/migrate_test.go
+++ b/sql/mysql/migrate_test.go
@@ -213,6 +213,56 @@ func TestPlanChanges(t *testing.T) {
 				},
 			},
 		},
+		{
+			input: []schema.Change{
+				func() schema.Change {
+					users := &schema.Table{
+						Name: "users",
+						Columns: []*schema.Column{
+							{Name: "id", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint"}}},
+						},
+					}
+					pets := &schema.Table{
+						Name: "pets",
+						Columns: []*schema.Column{
+							{Name: "id", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint"}}},
+							{Name: "user_id",
+								Type: &schema.ColumnType{
+									Type: &schema.IntegerType{T: "bigint"},
+								},
+							},
+						},
+					}
+					fk := &schema.ForeignKey{
+						Symbol:     "user_id",
+						Table:      pets,
+						OnUpdate:   schema.NoAction,
+						OnDelete:   schema.Cascade,
+						RefTable:   users,
+						Columns:    []*schema.Column{pets.Columns[1]},
+						RefColumns: []*schema.Column{users.Columns[0]},
+					}
+					pets.ForeignKeys = []*schema.ForeignKey{fk}
+					return &schema.ModifyTable{
+						T: pets,
+						Changes: []schema.Change{
+							&schema.DropForeignKey{
+								F: fk,
+							},
+						},
+					}
+				}(),
+			},
+			wantPlan: &migrate.Plan{
+				Reversible: true,
+				Changes: []*migrate.Change{
+					{
+						Cmd:     "ALTER TABLE `pets` DROP FOREIGN KEY `user_id`",
+						Reverse: "ALTER TABLE `pets` ADD CONSTRAINT `user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON UPDATE NO ACTION ON DELETE CASCADE",
+					},
+				},
+			},
+		},
 		{
 			input: []schema.Change{
 				&schema.AddSchema{S: &schema.Schema{Name: "test", Attrs: []schema.Attr{&schema.Charset{V: "latin"}}}},
diff --git a/sql/mysql/sqlspec_test.go b/sql/mysql/sqlspec_test.go
index 2d57265d..2a04eefd 100644
--- a/sql/mysql/sqlspec_test.go
+++ b/sql/mysql/sqlspec_test.go
@@ -415,7 +415,7 @@ func TestMarshalSpec_Comment(t *testing.T) {
   }
   index "index" {
     unique  = true
-    columns = [table.users.column.a, ]
+    columns = [table.users.column.a]
     comment = "index comment"
   }
 }
@@ -512,6 +512,46 @@ table "users" {
 	require.EqualValues(t, exp, &s)
 }
 
+func TestMarshalSpec_IndexParts(t *testing.T) {
+	c := schema.NewStringColumn("name", "text")
+	s := schema.New("test").
+		AddTables(
+			schema.NewTable("users").
+				AddColumns(c).
+				AddIndexes(
+					schema.NewIndex("idx").
+						AddParts(
+							schema.NewColumnPart(c).SetDesc(true),
+							schema.NewExprPart(&schema.RawExpr{X: "lower(name)"}),
+						),
+				),
+		)
+	buf, err := MarshalHCL(s)
+	require.NoError(t, err)
+	exp := `table "users" {
+  schema = schema.test
+  column "name" {
+    null = false
+    type = text
+  }
+  index "idx" {
+    unique = false
+    on {
+      desc   = true
+      column = table.users.column.name
+    }
+    on {
+      desc = false
+      expr = "lower(name)"
+    }
+  }
+}
+schema "test" {
+}
+`
+	require.EqualValues(t, exp, buf)
+}
+
 func TestMarshalSpec_TimePrecision(t *testing.T) {
 	s := schema.New("test").
 		AddTables(
diff --git a/sql/postgres/migrate_test.go b/sql/postgres/migrate_test.go
index 6e6ef1b5..3a45ac0f 100644
--- a/sql/postgres/migrate_test.go
+++ b/sql/postgres/migrate_test.go
@@ -22,6 +22,95 @@ func TestPlanChanges(t *testing.T) {
 		mock    func(mock)
 		plan    *migrate.Plan
 	}{
+		{
+			changes: []schema.Change{
+				func() schema.Change {
+					users := &schema.Table{
+						Name: "users",
+						Columns: []*schema.Column{
+							{Name: "id", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint"}}},
+						},
+					}
+					pets := &schema.Table{
+						Name: "pets",
+						Columns: []*schema.Column{
+							{Name: "id", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint"}}},
+							{Name: "user_id",
+								Type: &schema.ColumnType{
+									Type: &schema.IntegerType{T: "bigint"},
+								},
+							},
+						},
+					}
+					fk := &schema.ForeignKey{
+						Symbol:     "pets_user_id_fkey",
+						Table:      pets,
+						OnUpdate:   schema.NoAction,
+						OnDelete:   schema.Cascade,
+						RefTable:   users,
+						Columns:    []*schema.Column{pets.Columns[1]},
+						RefColumns: []*schema.Column{users.Columns[0]},
+					}
+					pets.ForeignKeys = []*schema.ForeignKey{fk}
+					return &schema.ModifyTable{
+						T: pets,
+						Changes: []schema.Change{
+							&schema.DropForeignKey{
+								F: fk,
+							},
+						},
+					}
+				}(),
+			},
+			plan: &migrate.Plan{
+				Reversible:    true,
+				Transactional: true,
+				Changes: []*migrate.Change{
+					{
+						Cmd:     `ALTER TABLE "pets" DROP CONSTRAINT "pets_user_id_fkey"`,
+						Reverse: `ALTER TABLE "pets" ADD CONSTRAINT "pets_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON UPDATE NO ACTION ON DELETE CASCADE`,
+					},
+				},
+			},
+		},
+		{
+			changes: []schema.Change{
+				func() schema.Change {
+					users := &schema.Table{
+						Name: "users",
+						Columns: []*schema.Column{
+							{Name: "id", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint"}}},
+							{
+								Name: "name",
+								Type: &schema.ColumnType{Type: &schema.StringType{T: "varchar(255)"}},
+								Indexes: []*schema.Index{
+									schema.NewIndex("name_index").
+										AddParts(schema.NewColumnPart(schema.NewColumn("name"))),
+								},
+							}},
+					}
+					return &schema.ModifyTable{
+						T: users,
+						Changes: []schema.Change{
+							&schema.DropIndex{
+								I: schema.NewIndex("name_index").
+									AddParts(schema.NewColumnPart(schema.NewColumn("name"))),
+							},
+						},
+					}
+				}(),
+			},
+			plan: &migrate.Plan{
+				Reversible:    true,
+				Transactional: true,
+				Changes: []*migrate.Change{
+					{
+						Cmd:     `DROP INDEX "name_index"`,
+						Reverse: `CREATE INDEX "name_index" ON "users" ("name")`,
+					},
+				},
+			},
+		},
 		{
 			changes: []schema.Change{
 				&schema.AddSchema{S: &schema.Schema{Name: "test"}},
diff --git a/sql/postgres/sqlspec_test.go b/sql/postgres/sqlspec_test.go
index b93e696f..21dd21a9 100644
--- a/sql/postgres/sqlspec_test.go
+++ b/sql/postgres/sqlspec_test.go
@@ -32,6 +32,14 @@ table "table" {
 	column "tags" {
 		type = hstore
 	}
+	column "created_at" {
+		type    = timestamp(4)
+		default = sql("current_timestamp(4)")
+	}
+	column "updated_at" {
+		type    = time
+		default = sql("current_time")
+	}
 	primary_key {
 		columns = [table.table.column.col]
 	}
@@ -129,6 +137,26 @@ enum "account_type" {
 						},
 					},
 				},
+				{
+					Name: "created_at",
+					Type: &schema.ColumnType{
+						Type: &schema.TimeType{
+							T:         TypeTimestamp,
+							Precision: 4,
+						},
+					},
+					Default: &schema.RawExpr{X: "current_timestamp(4)"},
+				},
+				{
+					Name: "updated_at",
+					Type: &schema.ColumnType{
+						Type: &schema.TimeType{
+							T:         TypeTime,
+							Precision: 6,
+						},
+					},
+					Default: &schema.RawExpr{X: "current_time"},
+				},
 			},
 			Attrs: []schema.Attr{
 				&schema.Check{
@@ -226,7 +254,60 @@ schema "test" {
 }
 enum "account_type" {
   schema = schema.test
-  values = ["private", "business", ]
+  values = ["private", "business"]
+}
+`
+	require.EqualValues(t, expected, string(buf))
+}
+
+func TestMarshalSpec_TimePrecision(t *testing.T) {
+	s := schema.New("test").
+		AddTables(
+			schema.NewTable("times").
+				AddColumns(
+					schema.NewTimeColumn("t_time_def", TypeTime),
+					schema.NewTimeColumn("t_time_with_time_zone", TypeTimeWTZ, schema.TimePrecision(2)),
+					schema.NewTimeColumn("t_time_without_time_zone", TypeTimeWOTZ, schema.TimePrecision(2)),
+					schema.NewTimeColumn("t_timestamp", TypeTimestamp, schema.TimePrecision(2)),
+					schema.NewTimeColumn("t_timestamptz", TypeTimestampTZ, schema.TimePrecision(2)),
+					schema.NewTimeColumn("t_timestamp_with_time_zone", TypeTimestampWTZ, schema.TimePrecision(2)),
+					schema.NewTimeColumn("t_timestamp_without_time_zone", TypeTimestampWOTZ, schema.TimePrecision(2)),
+				),
+		)
+	buf, err := MarshalSpec(s, hclState)
+	require.NoError(t, err)
+	const expected = `table "times" {
+  schema = schema.test
+  column "t_time_def" {
+    null = false
+    type = time
+  }
+  column "t_time_with_time_zone" {
+    null = false
+    type = time_with_time_zone(2)
+  }
+  column "t_time_without_time_zone" {
+    null = false
+    type = time_without_time_zone(2)
+  }
+  column "t_timestamp" {
+    null = false
+    type = timestamp(2)
+  }
+  column "t_timestamptz" {
+    null = false
+    type = timestamptz(2)
+  }
+  column "t_timestamp_with_time_zone" {
+    null = false
+    type = timestamp_with_time_zone(2)
+  }
+  column "t_timestamp_without_time_zone" {
+    null = false
+    type = timestamp_without_time_zone(2)
+  }
+}
+schema "test" {
 }
 `
 	require.EqualValues(t, expected, string(buf))
@@ -346,42 +427,65 @@ func TestTypes(t *testing.T) {
 			typeExpr: "point",
 			expected: &schema.SpatialType{T: TypePoint},
 		},
-
 		{
 			typeExpr: "date",
 			expected: &schema.TimeType{T: TypeDate},
 		},
 		{
 			typeExpr: "time",
-			expected: &schema.TimeType{T: TypeTime},
+			expected: &schema.TimeType{T: TypeTime, Precision: 6},
+		},
+		{
+			typeExpr: "time(4)",
+			expected: &schema.TimeType{T: TypeTime, Precision: 4},
 		},
 		{
 			typeExpr: "time_with_time_zone",
-			expected: &schema.TimeType{T: TypeTimeWTZ},
+			expected: &schema.TimeType{T: TypeTimeWTZ, Precision: 6},
+		},
+		{
+			typeExpr: "time_with_time_zone(4)",
+			expected: &schema.TimeType{T: TypeTimeWTZ, Precision: 4},
 		},
 		{
 			typeExpr: "time_without_time_zone",
-			expected: &schema.TimeType{T: TypeTimeWOTZ},
+			expected: &schema.TimeType{T: TypeTimeWOTZ, Precision: 6},
+		},
+		{
+			typeExpr: "time_without_time_zone(4)",
+			expected: &schema.TimeType{T: TypeTimeWOTZ, Precision: 4},
 		},
 		{
 			typeExpr: "timestamp",
-			expected: &schema.TimeType{T: TypeTimestamp},
+			expected: &schema.TimeType{T: TypeTimestamp, Precision: 6},
+		},
+		{
+			typeExpr: "timestamp(4)",
+			expected: &schema.TimeType{T: TypeTimestamp, Precision: 4},
 		},
 		{
 			typeExpr: "timestamptz",
-			expected: &schema.TimeType{T: TypeTimestampTZ},
+			expected: &schema.TimeType{T: TypeTimestampTZ, Precision: 6},
+		},
+		{
+			typeExpr: "timestamptz(4)",
+			expected: &schema.TimeType{T: TypeTimestampTZ, Precision: 4},
 		},
 		{
 			typeExpr: "timestamp_with_time_zone",
-			expected: &schema.TimeType{T: TypeTimestampWTZ},
+			expected: &schema.TimeType{T: TypeTimestampWTZ, Precision: 6},
+		},
+		{
+			typeExpr: "timestamp_with_time_zone(4)",
+			expected: &schema.TimeType{T: TypeTimestampWTZ, Precision: 4},
 		},
 		{
 			typeExpr: "timestamp_without_time_zone",
-			expected: &schema.TimeType{T: TypeTimestampWOTZ},
+			expected: &schema.TimeType{T: TypeTimestampWOTZ, Precision: 6},
 		},
 		{
-			typeExpr: "time",
-			expected: &schema.TimeType{T: TypeTime},
+			typeExpr: "timestamp_without_time_zone(4)",
+			expected: &schema.TimeType{T: TypeTimestampWOTZ, Precision: 4},
 		},
 		{
 			typeExpr: "real",