Repository: ariga/atlas
Pull request #348 (2021-12-29 09:10:09)

sql/mysql: add support for applying CHECK constrains on migrate


Patch:
diff --git a/cmd/action/apply.go b/cmd/action/apply.go
index 7eee1c15..89c78b67 100644
--- a/cmd/action/apply.go
+++ b/cmd/action/apply.go
@@ -7,7 +7,6 @@ import (
 
 	"ariga.io/atlas/sql/schema"
 
-	"ariga.io/atlas/schema/schemaspec/schemahcl"
 	"github.com/manifoldco/promptui"
 	"github.com/spf13/cobra"
 )
@@ -48,11 +47,10 @@ func init() {
 func CmdApplyRun(cmd *cobra.Command, args []string) {
 	d, err := defaultMux.OpenAtlas(ApplyFlags.DSN)
 	cobra.CheckErr(err)
-	u := schemaUnmarshal{unmarshalSpec: d.UnmarshalSpec, unmarshaler: schemahcl.New(schemahcl.WithTypes(d.Types))}
-	applyRun(d, &u, ApplyFlags.DSN, ApplyFlags.File)
+	applyRun(d, ApplyFlags.DSN, ApplyFlags.File)
 }
 
-func applyRun(d *Driver, u schemaUnmarshaler, dsn string, file string) {
+func applyRun(d *Driver, dsn string, file string) {
 	ctx := context.Background()
 	name, err := schemaNameFromDSN(dsn)
 	cobra.CheckErr(err)
@@ -61,7 +59,7 @@ func applyRun(d *Driver, u schemaUnmarshaler, dsn string, file string) {
 	f, err := ioutil.ReadFile(file)
 	cobra.CheckErr(err)
 	var desired schema.Schema
-	err = u.unmarshal(f, &desired)
+	err = d.UnmarshalSpec(f, &desired)
 	cobra.CheckErr(err)
 	changes, err := d.SchemaDiff(s, &desired)
 	cobra.CheckErr(err)
diff --git a/cmd/action/inspect.go b/cmd/action/inspect.go
index 74d35b9f..3e8eefed 100644
--- a/cmd/action/inspect.go
+++ b/cmd/action/inspect.go
@@ -3,7 +3,6 @@ package action
 import (
 	"context"
 
-	"ariga.io/atlas/schema/schemaspec/schemahcl"
 	"github.com/spf13/cobra"
 )
 
@@ -41,17 +40,16 @@ func init() {
 func CmdInspectRun(cmd *cobra.Command, args []string) {
 	d, err := defaultMux.OpenAtlas(InspectFlags.DSN)
 	cobra.CheckErr(err)
-	m := schemaMarshal{marshalSpec: d.MarshalSpec, marshaler: schemahcl.New(schemahcl.WithTypes(d.Types))}
-	inspectRun(d, &m, InspectFlags.DSN)
+	inspectRun(d, InspectFlags.DSN)
 }
 
-func inspectRun(d *Driver, m schemaMarshaler, dsn string) {
+func inspectRun(d *Driver, dsn string) {
 	ctx := context.Background()
 	name, err := schemaNameFromDSN(dsn)
 	cobra.CheckErr(err)
 	s, err := d.InspectSchema(ctx, name, nil)
 	cobra.CheckErr(err)
-	ddl, err := m.marshal(s)
+	ddl, err := d.MarshalSpec(s)
 	cobra.CheckErr(err)
 	schemaCmd.Print(string(ddl))
 }
diff --git a/cmd/action/mux.go b/cmd/action/mux.go
index 5347f09e..af39d406 100644
--- a/cmd/action/mux.go
+++ b/cmd/action/mux.go
@@ -6,8 +6,6 @@ import (
 
 	"ariga.io/atlas/schema/schemaspec"
 	"ariga.io/atlas/sql/migrate"
-	"ariga.io/atlas/sql/schema"
-
 	"github.com/go-sql-driver/mysql"
 )
 
@@ -20,27 +18,9 @@ type (
 	// Driver implements the Atlas interface.
 	Driver struct {
 		migrate.Driver
-		MarshalSpec   func(v interface{}, marshaler schemaspec.Marshaler) ([]byte, error)
-		UnmarshalSpec func(data []byte, unmarshaler schemaspec.Unmarshaler, v interface{}) error
-		Types         []*schemaspec.TypeSpec
-	}
-
-	schemaUnmarshal struct {
-		unmarshalSpec func(data []byte, unmarshaler schemaspec.Unmarshaler, v interface{}) error
-		unmarshaler   schemaspec.Unmarshaler
-	}
-
-	schemaUnmarshaler interface {
-		unmarshal([]byte, interface{}) error
-	}
-
-	schemaMarshal struct {
-		marshalSpec func(v interface{}, marshaler schemaspec.Marshaler) ([]byte, error)
-		marshaler   schemaspec.Marshaler
-	}
-
-	schemaMarshaler interface {
-		marshal(*schema.Schema) ([]byte, error)
+		schemaspec.Marshaler
+		schemaspec.Unmarshaler
+		Types []*schemaspec.TypeSpec
 	}
 )
 
@@ -100,11 +80,3 @@ func schemaNameFromDSN(url string) (string, error) {
 		return "", fmt.Errorf("unknown database type: %q", key)
 	}
 }
-
-func (p *schemaMarshal) marshal(s *schema.Schema) ([]byte, error) {
-	return p.marshalSpec(s, p.marshaler)
-}
-
-func (p *schemaUnmarshal) unmarshal(b []byte, v interface{}) error {
-	return p.unmarshalSpec(b, p.unmarshaler, v)
-}
diff --git a/cmd/action/provider.go b/cmd/action/provider.go
index b51d679d..975c7738 100644
--- a/cmd/action/provider.go
+++ b/cmd/action/provider.go
@@ -22,10 +22,10 @@ func mysqlProvider(dsn string) (*Driver, error) {
 		return nil, err
 	}
 	return &Driver{
-		Driver:        drv,
-		MarshalSpec:   mysql.MarshalSpec,
-		UnmarshalSpec: mysql.UnmarshalSpec,
-		Types:         mysql.TypeRegistry.Specs(),
+		Driver:      drv,
+		Marshaler:   mysql.MarshalHCL,
+		Unmarshaler: mysql.UnmarshalHCL,
+		Types:       mysql.TypeRegistry.Specs(),
 	}, nil
 }
 func postgresProvider(dsn string) (*Driver, error) {
@@ -39,9 +39,9 @@ func postgresProvider(dsn string) (*Driver, error) {
 		return nil, err
 	}
 	return &Driver{
-		Driver:        drv,
-		MarshalSpec:   postgres.MarshalSpec,
-		UnmarshalSpec: postgres.UnmarshalSpec,
-		Types:         postgres.TypeRegistry.Specs(),
+		Driver:      drv,
+		Marshaler:   postgres.MarshalHCL,
+		Unmarshaler: postgres.UnmarshalHCL,
+		Types:       postgres.TypeRegistry.Specs(),
 	}, nil
 }
diff --git a/internal/integration/go.sum b/internal/integration/go.sum
index a4f64326..3ea3e613 100644
--- a/internal/integration/go.sum
+++ b/internal/integration/go.sum
@@ -66,6 +66,7 @@ github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqO
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
@@ -241,6 +242,7 @@ github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk=
 github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
+github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
 github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
@@ -250,6 +252,7 @@ github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
 github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
+github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
 github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
@@ -309,11 +312,13 @@ github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
+github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
 github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
 github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
 github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
 github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
 github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
diff --git a/sql/internal/sqlx/diff.go b/sql/internal/sqlx/diff.go
index f3abf477..7d6cca72 100644
--- a/sql/internal/sqlx/diff.go
+++ b/sql/internal/sqlx/diff.go
@@ -320,7 +320,7 @@ var (
 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))
+		panic(fmt.Sprintf("unexpected elements type: %T", elements))
 	}
 	tv := reflect.ValueOf(target)
 	if tv.Kind() != reflect.Ptr || tv.IsNil() {
diff --git a/sql/internal/sqlx/sqlx.go b/sql/internal/sqlx/sqlx.go
index 676d9de6..8f0344e2 100644
--- a/sql/internal/sqlx/sqlx.go
+++ b/sql/internal/sqlx/sqlx.go
@@ -198,13 +198,15 @@ func (b *Builder) Table(t *schema.Table) *Builder {
 	return b
 }
 
-// Comma writes a comma. If the current buffer ends
-// with whitespace, it will be replaced instead.
+// Comma writes a comma in case the buffer is not empty, or
+// replaces the last char if it is a whitespace.
 func (b *Builder) Comma() *Builder {
-	if b.lastByte() == ' ' {
+	switch {
+	case b.Len() == 0:
+	case b.lastByte() == ' ':
 		b.rewriteLastByte(',')
 		b.WriteByte(' ')
-	} else {
+	default:
 		b.WriteString(", ")
 	}
 	return b
@@ -320,3 +322,23 @@ func DefaultValue(c *schema.Column) (string, bool) {
 		panic(fmt.Sprintf("unexpected default value type: %T", x))
 	}
 }
+
+// Checks constrains from table or column attributes.
+func Checks(attr []schema.Attr) (checks []*schema.Check) {
+	for i := range attr {
+		if c, ok := attr[i].(*schema.Check); ok {
+			checks = append(checks, c)
+		}
+	}
+	return checks
+}
+
+// CheckByName returns a CHECK by its constraints name.
+func CheckByName(attr []schema.Attr, name string) (*schema.Check, bool) {
+	for i := range attr {
+		if c, ok := attr[i].(*schema.Check); ok && c.Name == name {
+			return c, true
+		}
+	}
+	return nil, false
+}
diff --git a/sql/mysql/diff.go b/sql/mysql/diff.go
index 5a93ca9b..418cd2a5 100644
--- a/sql/mysql/diff.go
+++ b/sql/mysql/diff.go
@@ -44,24 +44,24 @@ func (d *diff) TableAttrDiff(from, to *schema.Table) []schema.Change {
 		changes = append(changes, change)
 	}
 	// Drop or modify checks.
-	for _, c1 := range checks(from.Attrs) {
-		switch c2, ok := checkByName(to.Attrs, c1.Name); {
+	for _, c1 := range sqlx.Checks(from.Attrs) {
+		switch c2, ok := sqlx.CheckByName(to.Attrs, c1.Name); {
 		case !ok:
-			changes = append(changes, &schema.DropAttr{
-				A: c1,
+			changes = append(changes, &schema.DropCheck{
+				C: c1,
 			})
-		case c1.Clause != c2.Clause || c1.Enforced != c2.Enforced:
-			changes = append(changes, &schema.ModifyAttr{
+		case c1.Expr != c2.Expr || sqlx.Has(c1.Attrs, &Enforced{}) != sqlx.Has(c2.Attrs, &Enforced{}):
+			changes = append(changes, &schema.ModifyCheck{
 				From: c1,
 				To:   c2,
 			})
 		}
 	}
 	// Add checks.
-	for _, c1 := range checks(to.Attrs) {
-		if _, ok := checkByName(from.Attrs, c1.Name); !ok {
-			changes = append(changes, &schema.AddAttr{
-				A: c1,
+	for _, c1 := range sqlx.Checks(to.Attrs) {
+		if _, ok := sqlx.CheckByName(from.Attrs, c1.Name); !ok {
+			changes = append(changes, &schema.AddCheck{
+				C: c1,
 			})
 		}
 	}
@@ -207,15 +207,6 @@ func indexType(attr []schema.Attr) *IndexType {
 // noChange describes a zero change.
 var noChange struct{ schema.Change }
 
-func checks(attr []schema.Attr) (checks []*Check) {
-	for i := range attr {
-		if c, ok := attr[i].(*Check); ok {
-			checks = append(checks, c)
-		}
-	}
-	return checks
-}
-
 func (d *diff) typeChanged(from, to *schema.Column) (bool, error) {
 	fromT, toT := from.Type.Type, to.Type.Type
 	if fromT == nil || toT == nil {
@@ -374,15 +365,6 @@ func binValue(x string) (string, error) {
 	return string(d), nil
 }
 
-func checkByName(attr []schema.Attr, name string) (*Check, bool) {
-	for i := range attr {
-		if c, ok := attr[i].(*Check); ok && c.Name == name {
-			return c, true
-		}
-	}
-	return nil, false
-}
-
 func displayWidth(attr []schema.Attr) *DisplayWidth {
 	var (
 		z *ZeroFill
diff --git a/sql/mysql/driver.go b/sql/mysql/driver.go
index 5c0f3409..13db7a03 100644
--- a/sql/mysql/driver.go
+++ b/sql/mysql/driver.go
@@ -83,6 +83,12 @@ func (d *conn) supportsExprDefault() bool {
 	return d.gteV(v)
 }
 
+// supportsEnforceCheck reports if the connected database supports
+// the ENFORCED option in CHECK constraint syntax.
+func (d *conn) supportsEnforceCheck() bool {
+	return !d.mariadb() && d.gteV("8.0.16")
+}
+
 // mariadb reports if the Driver is connected to a MariaDB database.
 func (d *conn) mariadb() bool {
 	return strings.Index(d.version, "MariaDB") > 0
diff --git a/sql/mysql/inspect.go b/sql/mysql/inspect.go
index aca2bc32..0899a9b2 100644
--- a/sql/mysql/inspect.go
+++ b/sql/mysql/inspect.go
@@ -499,10 +499,12 @@ func (i *inspect) checks(ctx context.Context, t *schema.Table) error {
 		if err := rows.Scan(&name, &clause, &enforced); err != nil {
 			return fmt.Errorf("mysql: %w", err)
 		}
-		check := &Check{
-			Name:     name.String,
-			Clause:   unescape(clause.String),
-			Enforced: clause.String != "NO",
+		check := &schema.Check{
+			Name: name.String,
+			Expr: unescape(clause.String),
+		}
+		if enforced.String != "NO" {
+			check.Attrs = append(check.Attrs, &Enforced{})
 		}
 		t.Attrs = append(t.Attrs, check)
 		// In MariaDB, JSON is an alias to LONGTEXT, and the JSON_VALID
@@ -511,7 +513,7 @@ func (i *inspect) checks(ctx context.Context, t *schema.Table) error {
 		// Ent to manually add this CHECK for older versions of MariaDB.
 		if i.mariadb() {
 			c, ok := t.Column(check.Name)
-			if ok && c.Type.Raw == tLongText && check.Clause == fmt.Sprintf("json_valid(`%s`)", c.Name) {
+			if ok && c.Type.Raw == tLongText && check.Expr == fmt.Sprintf("json_valid(`%s`)", c.Name) {
 				c.Type.Raw = tJSON
 				c.Type.Type = &schema.JSONType{T: tJSON}
 			}
@@ -764,12 +766,10 @@ type (
 		Len int
 	}
 
-	// Check attributes defines a CHECK constraint.
-	Check struct {
+	// Enforced attribute defines the ENFORCED flag for CHECK constraint.
+	// Similar to AUTO_INCREMENT, checks with this attributes are enforced.
+	Enforced struct {
 		schema.Attr
-		Name     string
-		Clause   string
-		Enforced bool
 	}
 
 	// The DisplayWidth represents a display width of an integer type.
diff --git a/sql/mysql/migrate.go b/sql/mysql/migrate.go
index ad044575..23103a57 100644
--- a/sql/mysql/migrate.go
+++ b/sql/mysql/migrate.go
@@ -156,6 +156,12 @@ func (s *state) addTable(add *schema.AddTable) error {
 			b.Comma()
 			s.fks(b, add.T.ForeignKeys...)
 		}
+		for _, attr := range add.T.Attrs {
+			if c, ok := attr.(*schema.Check); ok {
+				b.Comma()
+				s.check(b, c)
+			}
+		}
 	})
 	if err := s.tableAttr(b, add.T.Attrs...); err != nil {
 		return err
@@ -242,19 +248,19 @@ func (s *state) alterTable(t *schema.Table, changes []schema.Change) error {
 	var (
 		errors     []string
 		b          = Build("ALTER TABLE").Table(t)
+		reverse    = Build("")
 		reversible = true
-		reverse    = b.Clone()
 	)
 	b.MapComma(changes, func(i int, b *sqlx.Builder) {
 		switch change := changes[i].(type) {
 		case *schema.AddColumn:
 			b.P("ADD COLUMN")
 			s.column(b, t, change.C)
-			reverse.P("DROP COLUMN").Ident(change.C.Name)
+			reverse.Comma().P("DROP COLUMN").Ident(change.C.Name)
 		case *schema.ModifyColumn:
 			b.P("MODIFY COLUMN")
 			s.column(b, t, change.To)
-			reverse.P("MODIFY COLUMN")
+			reverse.Comma().P("MODIFY COLUMN")
 			s.column(reverse, t, change.From)
 		case *schema.DropColumn:
 			b.P("DROP COLUMN").Ident(change.C.Name)
@@ -267,14 +273,14 @@ func (s *state) alterTable(t *schema.Table, changes []schema.Change) error {
 			b.P("INDEX").Ident(change.I.Name)
 			s.indexParts(b, change.I.Parts)
 			s.attr(b, change.I.Attrs...)
-			reverse.P("DROP INDEX").Ident(change.I.Name)
+			reverse.Comma().P("DROP INDEX").Ident(change.I.Name)
 		case *schema.DropIndex:
 			b.P("DROP INDEX").Ident(change.I.Name)
 			reversible = false
 		case *schema.AddForeignKey:
 			b.P("ADD")
 			s.fks(b, change.F)
-			reverse.P("DROP FOREIGN KEY").Ident(change.F.Symbol)
+			reverse.Comma().P("DROP FOREIGN KEY").Ident(change.F.Symbol)
 		case *schema.DropForeignKey:
 			b.P("DROP FOREIGN KEY").Ident(change.F.Symbol)
 			reversible = false
@@ -291,6 +297,39 @@ func (s *state) alterTable(t *schema.Table, changes []schema.Change) error {
 			if err := s.tableAttr(reverse, change.From); err != nil {
 				errors = append(errors, fmt.Sprintf("reverse modify attribute: %s", err.Error()))
 			}
+		case *schema.AddCheck:
+			s.check(b.P("ADD"), change.C)
+			// Reverse operation is supported if
+			// the constraint name is not generated.
+			if reversible = change.C.Name != ""; reversible {
+				reverse.Comma().P("DROP CHECK").Ident(change.C.Name)
+			}
+		case *schema.DropCheck:
+			b.P("DROP CHECK").Ident(change.C.Name)
+			s.check(reverse.Comma().P("ADD"), change.C)
+		case *schema.ModifyCheck:
+			switch {
+			case change.From.Name == "":
+				errors = append(errors, "cannot modify unnamed check constraint")
+			case change.From.Name != change.To.Name:
+				errors = append(errors, fmt.Sprintf("mismatch check constraint names: %q != %q", change.From.Name, change.To.Name))
+			// Enforcement added.
+			case s.supportsEnforceCheck() && sqlx.Has(change.From.Attrs, &Enforced{}) && !sqlx.Has(change.To.Attrs, &Enforced{}):
+				b.P("ALTER CHECK").Ident(change.From.Name).P("ENFORCED")
+				reverse.Comma().P("ALTER CHECK").Ident(change.From.Name).P("NOT ENFORCED")
+			// Enforcement dropped.
+			case s.supportsEnforceCheck() && !sqlx.Has(change.From.Attrs, &Enforced{}) && sqlx.Has(change.To.Attrs, &Enforced{}):
+				b.P("ALTER CHECK").Ident(change.From.Name).P("NOT ENFORCED")
+				reverse.Comma().P("ALTER CHECK").Ident(change.From.Name).P("ENFORCED")
+			// Expr was changed.
+			case change.From.Expr != change.To.Expr:
+				b.P("DROP CHECK").Ident(change.From.Name).Comma().P("ADD")
+				s.check(b, change.To)
+				reverse.Comma().P("DROP CHECK").Ident(change.To.Name).Comma().P("ADD")
+				s.check(reverse, change.From)
+			default:
+				errors = append(errors, "unknown check constraints change")
+			}
 		}
 	})
 	if len(errors) > 0 {
@@ -305,7 +344,11 @@ func (s *state) alterTable(t *schema.Table, changes []schema.Change) error {
 		Comment: fmt.Sprintf("modify %q table", t.Name),
 	}
 	if reversible {
-		change.Reverse = reverse.String()
+		b := Build("ALTER TABLE").Table(t)
+		if _, err := b.ReadFrom(reverse); err != nil {
+			return fmt.Errorf("unexpected buffer read: %w", err)
+		}
+		change.Reverse = b.String()
 	}
 	s.append(change)
 	return nil
@@ -320,7 +363,7 @@ func (s *state) column(b *sqlx.Builder, t *schema.Table, c *schema.Column) {
 	s.columnDefault(b, c)
 	// Add manually the JSON_VALID constraint for older
 	// versions < 10.4.3. See Driver.checks for full info.
-	if _, ok := c.Type.Type.(*schema.JSONType); ok && s.mariadb() && s.ltV("10.4.3") && !sqlx.Has(c.Attrs, &Check{}) {
+	if _, ok := c.Type.Type.(*schema.JSONType); ok && s.mariadb() && s.ltV("10.4.3") && !sqlx.Has(c.Attrs, &schema.Check{}) {
 		b.P("CHECK").Wrap(func(b *sqlx.Builder) {
 			b.WriteString(fmt.Sprintf("json_valid(`%s`)", c.Name))
 		})
@@ -402,6 +445,9 @@ func (s *state) tableAttr(b *sqlx.Builder, attrs ...schema.Attr) error {
 				return fmt.Errorf("missing value for table option AUTO_INCREMENT")
 			}
 			b.P("AUTO_INCREMENT", strconv.FormatInt(a.V, 10))
+		case *schema.Check:
+			// Ignore CHECK constraints as they are not real attributes,
+			// and handled on CREATE or ALTER.
 		case *schema.Charset:
 			b.P("CHARACTER SET", a.V)
 		case *schema.Collation:
@@ -495,6 +541,22 @@ search:
 	return changes
 }
 
+// checks writes the CHECK constraint to the builder.
+func (s *state) check(b *sqlx.Builder, c *schema.Check) {
+	expr := c.Expr
+	// Expressions should be wrapped with parens.
+	if t := strings.TrimSpace(expr); !strings.HasPrefix(t, "(") || !strings.HasSuffix(t, ")") {
+		expr = "(" + t + ")"
+	}
+	if c.Name != "" {
+		b.P("CONSTRAINT").Ident(c.Name)
+	}
+	b.P("CHECK", expr)
+	if s.supportsEnforceCheck() && sqlx.Has(c.Attrs, &Enforced{}) {
+		b.P("ENFORCED")
+	}
+}
+
 func quote(s string) string {
 	if sqlx.IsQuoted(s, '"', '\'') {
 		return s
diff --git a/sql/postgres/diff.go b/sql/postgres/diff.go
index 403c47fa..bab80b7d 100644
--- a/sql/postgres/diff.go
+++ b/sql/postgres/diff.go
@@ -27,13 +27,13 @@ func (d *diff) SchemaAttrDiff(_, _ *schema.Schema) []schema.Change {
 func (d *diff) TableAttrDiff(from, to *schema.Table) []schema.Change {
 	var changes []schema.Change
 	// Drop or modify checks.
-	for _, c1 := range checks(from.Attrs) {
-		switch c2, ok := checkByName(to.Attrs, c1.Name); {
+	for _, c1 := range sqlx.Checks(from.Attrs) {
+		switch c2, ok := sqlx.CheckByName(to.Attrs, c1.Name); {
 		case !ok:
 			changes = append(changes, &schema.DropAttr{
 				A: c1,
 			})
-		case c1.Clause != c2.Clause || c1.NoInherit != c2.NoInherit:
+		case c1.Expr != c2.Expr || sqlx.Has(c1.Attrs, &NoInherit{}) != sqlx.Has(c2.Attrs, &NoInherit{}):
 			changes = append(changes, &schema.ModifyAttr{
 				From: c1,
 				To:   c2,
@@ -41,8 +41,8 @@ func (d *diff) TableAttrDiff(from, to *schema.Table) []schema.Change {
 		}
 	}
 	// Add checks.
-	for _, c1 := range checks(to.Attrs) {
-		if _, ok := checkByName(from.Attrs, c1.Name); !ok {
+	for _, c1 := range sqlx.Checks(to.Attrs) {
+		if _, ok := sqlx.CheckByName(from.Attrs, c1.Name); !ok {
 			changes = append(changes, &schema.AddAttr{
 				A: c1,
 			})
@@ -279,24 +279,6 @@ func (d *diff) typesEqual(x, y string) (bool, error) {
 	return b, nil
 }
 
-func checks(attr []schema.Attr) (checks []*Check) {
-	for i := range attr {
-		if c, ok := attr[i].(*Check); ok {
-			checks = append(checks, c)
-		}
-	}
-	return checks
-}
-
-func checkByName(attr []schema.Attr, name string) (*Check, bool) {
-	for i := range attr {
-		if c, ok := attr[i].(*Check); ok && c.Name == name {
-			return c, true
-		}
-	}
-	return nil, false
-}
-
 func trimCast(s string) string {
 	i := strings.LastIndex(s, "::")
 	if i == -1 {
diff --git a/sql/postgres/inspect.go b/sql/postgres/inspect.go
index 9428653e..72d10eaa 100644
--- a/sql/postgres/inspect.go
+++ b/sql/postgres/inspect.go
@@ -421,7 +421,7 @@ func (i *inspect) checks(ctx context.Context, t *schema.Table) error {
 
 // addChecks scans the rows and adds the checks to the table.
 func (i *inspect) addChecks(t *schema.Table, rows *sql.Rows) error {
-	names := make(map[string]*Check)
+	names := make(map[string]*schema.Check)
 	for rows.Next() {
 		var (
 			noInherit                     bool
@@ -435,11 +435,15 @@ func (i *inspect) addChecks(t *schema.Table, rows *sql.Rows) error {
 		}
 		check, ok := names[name]
 		if !ok {
-			check = &Check{Name: name, Clause: clause, NoInherit: noInherit}
+			check = &schema.Check{Name: name, Expr: clause, Attrs: []schema.Attr{&CheckColumns{}}}
+			if noInherit {
+				check.Attrs = append(check.Attrs, &NoInherit{})
+			}
 			names[name] = check
 			t.Attrs = append(t.Attrs, check)
 		}
-		check.Columns = append(check.Columns, column)
+		c := check.Attrs[0].(*CheckColumns)
+		c.Columns = append(c.Columns, column)
 	}
 	return nil
 }
@@ -649,14 +653,18 @@ type (
 		NullsLast  bool
 	}
 
-	// Check attributes defines a CHECK constraint.
+	// NoInherit attribute defines the NO INHERIT flag for CHECK constraint.
 	// https://www.postgresql.org/docs/current/catalog-pg-constraint.html
-	Check struct {
+	NoInherit struct {
+		schema.Attr
+	}
+
+	// CheckColumns attribute hold the column named used by the CHECK constraints.
+	// This attribute is added on inspection for internal usage and has no meaning
+	// on migration.
+	CheckColumns struct {
 		schema.Attr
-		Name      string
-		Clause    string
-		NoInherit bool
-		Columns   []string
+		Columns []string
 	}
 )
 
diff --git a/sql/schema/migrate.go b/sql/schema/migrate.go
index d00c0bad..e15dd853 100644
--- a/sql/schema/migrate.go
+++ b/sql/schema/migrate.go
@@ -123,6 +123,22 @@ type (
 		Change   ChangeKind
 	}
 
+	// AddCheck describes a CHECK constraint creation change.
+	AddCheck struct {
+		C *Check
+	}
+
+	// DropCheck describes a CHECK constraint removal change.
+	DropCheck struct {
+		C *Check
+	}
+
+	// ModifyCheck describes a change that modifies a check.
+	ModifyCheck struct {
+		From, To *Check
+		Change   ChangeKind
+	}
+
 	// AddAttr describes an attribute addition.
 	AddAttr struct {
 		A Attr
@@ -237,6 +253,9 @@ func (*ModifyTable) change()      {}
 func (*AddIndex) change()         {}
 func (*DropIndex) change()        {}
 func (*ModifyIndex) change()      {}
+func (*AddCheck) change()         {}
+func (*DropCheck) change()        {}
+func (*ModifyCheck) change()      {}
 func (*AddColumn) change()        {}
 func (*DropColumn) change()       {}
 func (*ModifyColumn) change()     {}
diff --git a/sql/schema/schema.go b/sql/schema/schema.go
index 09479a7a..13f8e421 100644
--- a/sql/schema/schema.go
+++ b/sql/schema/schema.go
@@ -281,6 +281,13 @@ type (
 	Collation struct {
 		V string
 	}
+
+	// Check describes a CHECK constraint.
+	Check struct {
+		Name  string // Optional constraint name.
+		Expr  string // Actual CHECK.
+		Attrs []Attr // Additional attributes (e.g. ENFORCED).
+	}
 )
 
 // expressions.
@@ -301,6 +308,7 @@ func (*DecimalType) typ()     {}
 func (*UnsupportedType) typ() {}
 
 // attributes.
+func (*Check) attr()     {}
 func (*Comment) attr()   {}
 func (*Charset) attr()   {}
 func (*Collation) attr() {}


Test patch:
diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go
index d42ccfc1..44071e40 100644
--- a/internal/integration/integration_test.go
+++ b/internal/integration/integration_test.go
@@ -111,13 +111,13 @@ func testHCLIntegration(t T, full string, empty string) {
 	require.Empty(t, t.realm().Schemas[0].Tables)
 }
 
-func testCLISchemaInspect(t T, h string, dsn string, unmarshalSpec func([]byte, schemaspec.Unmarshaler, interface{}) error, unmarshaler schemaspec.Unmarshaler) {
+func testCLISchemaInspect(t T, h string, dsn 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()
 	require.NoError(t, err)
 	t.dropTables("users")
 	var expected schema.Schema
-	err = unmarshalSpec([]byte(h), unmarshaler, &expected)
+	err = unmarshaler.UnmarshalSpec([]byte(h), &expected)
 	require.NoError(t, err)
 	t.applyHcl(h)
 	cmd := exec.Command("go", "run", "ariga.io/atlas/cmd/atlas",
@@ -131,7 +131,7 @@ func testCLISchemaInspect(t T, h string, dsn string, unmarshalSpec func([]byte,
 	cmd.Stdout = stdout
 	require.NoError(t, cmd.Run(), stderr.String())
 	var actual schema.Schema
-	err = unmarshalSpec(stdout.Bytes(), unmarshaler, &actual)
+	err = unmarshaler.UnmarshalSpec(stdout.Bytes(), &actual)
 	require.NoError(t, err)
 	require.Empty(t, stderr.String())
 	require.Equal(t, expected, actual)
diff --git a/internal/integration/mysql_test.go b/internal/integration/mysql_test.go
index dd907baf..b8e107e0 100644
--- a/internal/integration/mysql_test.go
+++ b/internal/integration/mysql_test.go
@@ -12,7 +12,6 @@ import (
 	"sync"
 	"testing"
 
-	"ariga.io/atlas/schema/schemaspec/schemahcl"
 	"ariga.io/atlas/sql/mysql"
 	"ariga.io/atlas/sql/schema"
 	"entgo.io/ent/dialect"
@@ -518,7 +517,7 @@ func TestMySQL_CLI(t *testing.T) {
 		myRun(t, func(t *myTest) {
 			attrs := t.defaultAttrs()
 			charset, collate := attrs[0].(*schema.Charset), attrs[1].(*schema.Collation)
-			testCLISchemaInspect(t, fmt.Sprintf(h, charset.V, collate.V), t.dsn(), mysql.UnmarshalSpec, schemahcl.New(schemahcl.WithTypes(mysql.TypeRegistry.Specs())))
+			testCLISchemaInspect(t, fmt.Sprintf(h, charset.V, collate.V), t.dsn(), mysql.UnmarshalHCL)
 		})
 	})
 	t.Run("SchemaApply", func(t *testing.T) {
@@ -896,7 +895,7 @@ create table atlas_types_sanity
 						return []schema.Attr{
 							&schema.Charset{V: "latin1"},
 							&schema.Collation{V: "latin1_swedish_ci"},
-							&mysql.Check{Name: "tJSON", Clause: "json_valid(`tJSON`)", Enforced: true},
+							&schema.Check{Name: "tJSON", Expr: "json_valid(`tJSON`)", Attrs: []schema.Attr{&mysql.Enforced{}}},
 						}
 					}
 					return []schema.Attr{
diff --git a/internal/integration/postgres_test.go b/internal/integration/postgres_test.go
index ed730291..8edd945c 100644
--- a/internal/integration/postgres_test.go
+++ b/internal/integration/postgres_test.go
@@ -12,7 +12,6 @@ import (
 	"sync"
 	"testing"
 
-	"ariga.io/atlas/schema/schemaspec/schemahcl"
 	"ariga.io/atlas/sql/postgres"
 	"ariga.io/atlas/sql/schema"
 
@@ -429,7 +428,7 @@ func TestPostgres_CLI(t *testing.T) {
 			}`
 	t.Run("SchemaInspect", func(t *testing.T) {
 		pgRun(t, func(t *pgTest) {
-			testCLISchemaInspect(t, h, t.dsn(), postgres.UnmarshalSpec, schemahcl.New(schemahcl.WithTypes(postgres.TypeRegistry.Specs())))
+			testCLISchemaInspect(t, h, t.dsn(), postgres.UnmarshalHCL)
 		})
 	})
 	t.Run("SchemaApply", func(t *testing.T) {
diff --git a/sql/mysql/diff_test.go b/sql/mysql/diff_test.go
index e2e79cf9..f6d021f2 100644
--- a/sql/mysql/diff_test.go
+++ b/sql/mysql/diff_test.go
@@ -103,31 +103,31 @@ func TestDiff_TableDiff(t *testing.T) {
 		{
 			name: "add check",
 			from: &schema.Table{Name: "t1", Schema: &schema.Schema{Name: "public"}},
-			to:   &schema.Table{Name: "t1", Attrs: []schema.Attr{&Check{Name: "users_chk1_c1", Clause: "(`c1` <>_latin1\\'foo\\')"}}},
+			to:   &schema.Table{Name: "t1", Attrs: []schema.Attr{&schema.Check{Name: "users_chk1_c1", Expr: "(`c1` <>_latin1\\'foo\\')"}}},
 			wantChanges: []schema.Change{
-				&schema.AddAttr{
-					A: &Check{Name: "users_chk1_c1", Clause: "(`c1` <>_latin1\\'foo\\')"},
+				&schema.AddCheck{
+					C: &schema.Check{Name: "users_chk1_c1", Expr: "(`c1` <>_latin1\\'foo\\')"},
 				},
 			},
 		},
 		{
 			name: "drop check",
-			from: &schema.Table{Name: "t1", Schema: &schema.Schema{Name: "public"}, Attrs: []schema.Attr{&Check{Name: "users_chk1_c1", Clause: "(`c1` <>_latin1\\'foo\\')"}}},
+			from: &schema.Table{Name: "t1", Schema: &schema.Schema{Name: "public"}, Attrs: []schema.Attr{&schema.Check{Name: "users_chk1_c1", Expr: "(`c1` <>_latin1\\'foo\\')"}}},
 			to:   &schema.Table{Name: "t1"},
 			wantChanges: []schema.Change{
-				&schema.DropAttr{
-					A: &Check{Name: "users_chk1_c1", Clause: "(`c1` <>_latin1\\'foo\\')"},
+				&schema.DropCheck{
+					C: &schema.Check{Name: "users_chk1_c1", Expr: "(`c1` <>_latin1\\'foo\\')"},
 				},
 			},
 		},
 		{
 			name: "modify check",
-			from: &schema.Table{Name: "t1", Schema: &schema.Schema{Name: "public"}, Attrs: []schema.Attr{&Check{Name: "users_chk1_c1", Clause: "(`c1` <>_latin1\\'foo\\')"}}},
-			to:   &schema.Table{Name: "t1", Attrs: []schema.Attr{&Check{Name: "users_chk1_c1", Clause: "(`c1` <>_latin1\\'foo\\')", Enforced: true}}},
+			from: &schema.Table{Name: "t1", Schema: &schema.Schema{Name: "public"}, Attrs: []schema.Attr{&schema.Check{Name: "users_chk1_c1", Expr: "(`c1` <>_latin1\\'foo\\')"}}},
+			to:   &schema.Table{Name: "t1", Attrs: []schema.Attr{&schema.Check{Name: "users_chk1_c1", Expr: "(`c1` <>_latin1\\'foo\\')", Attrs: []schema.Attr{&Enforced{}}}}},
 			wantChanges: []schema.Change{
-				&schema.ModifyAttr{
-					From: &Check{Name: "users_chk1_c1", Clause: "(`c1` <>_latin1\\'foo\\')"},
-					To:   &Check{Name: "users_chk1_c1", Clause: "(`c1` <>_latin1\\'foo\\')", Enforced: true},
+				&schema.ModifyCheck{
+					From: &schema.Check{Name: "users_chk1_c1", Expr: "(`c1` <>_latin1\\'foo\\')"},
+					To:   &schema.Check{Name: "users_chk1_c1", Expr: "(`c1` <>_latin1\\'foo\\')", Attrs: []schema.Attr{&Enforced{}}},
 				},
 			},
 		},
diff --git a/sql/mysql/inspect_test.go b/sql/mysql/inspect_test.go
index adb5b323..8917750b 100644
--- a/sql/mysql/inspect_test.go
+++ b/sql/mysql/inspect_test.go
@@ -594,7 +594,7 @@ func TestDriver_InspectTable(t *testing.T) {
 					{Name: "c1", Type: &schema.ColumnType{Raw: "int", Type: &schema.IntegerType{T: "int"}}},
 				}
 				require.EqualValues(columns, t.Columns)
-				require.EqualValues([]schema.Attr{&Check{Name: "users_chk_1", Clause: "(`c6` <>_latin1\\'foo\\'s\\')", Enforced: true}}, t.Attrs)
+				require.EqualValues([]schema.Attr{&schema.Check{Name: "users_chk_1", Expr: "(`c6` <>_latin1\\'foo\\'s\\')", Attrs: []schema.Attr{&Enforced{}}}}, t.Attrs)
 			},
 		},
 	}
diff --git a/sql/mysql/migrate_test.go b/sql/mysql/migrate_test.go
index 600b0ac7..44c77cd3 100644
--- a/sql/mysql/migrate_test.go
+++ b/sql/mysql/migrate_test.go
@@ -223,6 +223,7 @@ func TestPlanChanges(t *testing.T) {
 							&schema.Charset{V: "utf8mb4"},
 							&schema.Collation{V: "utf8mb4_bin"},
 							&schema.Comment{Text: "posts comment"},
+							&schema.Check{Name: "id_nonzero", Expr: "(`id` > 0)"},
 						},
 					}
 					t.PrimaryKey = &schema.Index{Parts: []*schema.IndexPart{{C: t.Columns[0]}}}
@@ -231,7 +232,7 @@ func TestPlanChanges(t *testing.T) {
 			},
 			plan: &migrate.Plan{
 				Reversible: true,
-				Changes:    []*migrate.Change{{Cmd: "CREATE TABLE `posts` (`id` bigint NOT NULL AUTO_INCREMENT, `text` text NULL, PRIMARY KEY (`id`)) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT \"posts comment\" AUTO_INCREMENT 100", Reverse: "DROP TABLE `posts`"}},
+				Changes:    []*migrate.Change{{Cmd: "CREATE TABLE `posts` (`id` bigint NOT NULL AUTO_INCREMENT, `text` text NULL, PRIMARY KEY (`id`), CONSTRAINT `id_nonzero` CHECK (`id` > 0)) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT \"posts comment\" AUTO_INCREMENT 100", Reverse: "DROP TABLE `posts`"}},
 			},
 		},
 		{
@@ -288,6 +289,104 @@ func TestPlanChanges(t *testing.T) {
 									},
 								},
 							},
+							&schema.AddCheck{
+								C: &schema.Check{
+									Name:  "id_nonzero",
+									Expr:  "(id > 0)",
+									Attrs: []schema.Attr{&Enforced{}},
+								},
+							},
+						},
+					}
+				}(),
+			},
+			plan: &migrate.Plan{
+				Reversible: true,
+				Changes: []*migrate.Change{
+					{
+						Cmd:     "ALTER TABLE `users` ADD COLUMN `name` varchar(255) NOT NULL, ADD INDEX `id_key` (`id`) COMMENT \"comment\", ADD CONSTRAINT `id_nonzero` CHECK (id > 0) ENFORCED",
+						Reverse: "ALTER TABLE `users` DROP COLUMN `name`, DROP INDEX `id_key`, DROP CHECK `id_nonzero`",
+					},
+				},
+			},
+		},
+		{
+			changes: []schema.Change{
+				func() schema.Change {
+					users := &schema.Table{
+						Name: "users",
+						Columns: []*schema.Column{
+							{Name: "id", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint"}}},
+						},
+					}
+					return &schema.ModifyTable{
+						T: users,
+						Changes: []schema.Change{
+							&schema.DropCheck{
+								C: &schema.Check{
+									Name:  "id_nonzero",
+									Expr:  "(id > 0)",
+									Attrs: []schema.Attr{&Enforced{}},
+								},
+							},
+						},
+					}
+				}(),
+			},
+			plan: &migrate.Plan{
+				Reversible: true,
+				Changes: []*migrate.Change{
+					{
+						Cmd:     "ALTER TABLE `users` DROP CHECK `id_nonzero`",
+						Reverse: "ALTER TABLE `users` ADD CONSTRAINT `id_nonzero` CHECK (id > 0) ENFORCED",
+					},
+				},
+			},
+		},
+		{
+			changes: []schema.Change{
+				func() schema.Change {
+					users := &schema.Table{
+						Name: "users",
+						Columns: []*schema.Column{
+							{Name: "id", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint"}}},
+						},
+					}
+					return &schema.ModifyTable{
+						T: users,
+						Changes: []schema.Change{
+							&schema.ModifyCheck{
+								From: &schema.Check{
+									Name:  "check1",
+									Expr:  "(id > 0)",
+									Attrs: []schema.Attr{&Enforced{}},
+								},
+								To: &schema.Check{
+									Name: "check1",
+									Expr: "(id > 0)",
+								},
+							},
+							&schema.ModifyCheck{
+								From: &schema.Check{
+									Name: "check2",
+									Expr: "(id > 0)",
+								},
+								To: &schema.Check{
+									Name:  "check2",
+									Expr:  "(id > 0)",
+									Attrs: []schema.Attr{&Enforced{}},
+								},
+							},
+							&schema.ModifyCheck{
+								From: &schema.Check{
+									Name: "check3",
+									Expr: "(id > 0)",
+								},
+								To: &schema.Check{
+									Name: "check3",
+									Expr: "(id >= 0)",
+								},
+							},
 						},
 					}
 				}(),
@@ -296,15 +395,15 @@ func TestPlanChanges(t *testing.T) {
 				Reversible: true,
 				Changes: []*migrate.Change{
 					{
-						Cmd:     "ALTER TABLE `users` ADD COLUMN `name` varchar(255) NOT NULL, ADD INDEX `id_key` (`id`) COMMENT \"comment\"",
-						Reverse: "ALTER TABLE `users` DROP COLUMN `name` DROP INDEX `id_key`",
+						Cmd:     "ALTER TABLE `users` ALTER CHECK `check1` ENFORCED, ALTER CHECK `check2` NOT ENFORCED, DROP CHECK `check3`, ADD CONSTRAINT `check3` CHECK (id >= 0)",
+						Reverse: "ALTER TABLE `users` ALTER CHECK `check1` NOT ENFORCED, ALTER CHECK `check2` ENFORCED, DROP CHECK `check3`, ADD CONSTRAINT `check3` CHECK (id > 0)",
 					},
 				},
 			},
 		},
 	}
 	for _, tt := range tests {
-		db, _, err := newMigrate("8.0.13")
+		db, _, err := newMigrate("8.0.16")
 		require.NoError(t, err)
 		plan, err := db.PlanChanges(context.Background(), "plan", tt.changes)
 		require.NoError(t, err)
diff --git a/sql/postgres/diff_test.go b/sql/postgres/diff_test.go
index 46d2595b..63beebc9 100644
--- a/sql/postgres/diff_test.go
+++ b/sql/postgres/diff_test.go
@@ -42,31 +42,31 @@ func TestDiff_TableDiff(t *testing.T) {
 		{
 			name: "add check",
 			from: &schema.Table{Name: "t1", Schema: &schema.Schema{Name: "public"}},
-			to:   &schema.Table{Name: "t1", Attrs: []schema.Attr{&Check{Name: "t1_c1_check", Clause: "(c1 > 1)"}}},
+			to:   &schema.Table{Name: "t1", Attrs: []schema.Attr{&schema.Check{Name: "t1_c1_check", Expr: "(c1 > 1)"}}},
 			wantChanges: []schema.Change{
 				&schema.AddAttr{
-					A: &Check{Name: "t1_c1_check", Clause: "(c1 > 1)"},
+					A: &schema.Check{Name: "t1_c1_check", Expr: "(c1 > 1)"},
 				},
 			},
 		},
 		{
 			name: "drop check",
-			from: &schema.Table{Name: "t1", Attrs: []schema.Attr{&Check{Name: "t1_c1_check", Clause: "(c1 > 1)"}}},
+			from: &schema.Table{Name: "t1", Attrs: []schema.Attr{&schema.Check{Name: "t1_c1_check", Expr: "(c1 > 1)"}}},
 			to:   &schema.Table{Name: "t1"},
 			wantChanges: []schema.Change{
 				&schema.DropAttr{
-					A: &Check{Name: "t1_c1_check", Clause: "(c1 > 1)"},
+					A: &schema.Check{Name: "t1_c1_check", Expr: "(c1 > 1)"},
 				},
 			},
 		},
 		{
 			name: "modify check",
-			from: &schema.Table{Name: "t1", Schema: &schema.Schema{Name: "public"}, Attrs: []schema.Attr{&Check{Name: "t1_c1_check", Clause: "(c1 > 1)"}}},
-			to:   &schema.Table{Name: "t1", Attrs: []schema.Attr{&Check{Name: "t1_c1_check", Clause: "(c1 > 1)", NoInherit: true}}},
+			from: &schema.Table{Name: "t1", Schema: &schema.Schema{Name: "public"}, Attrs: []schema.Attr{&schema.Check{Name: "t1_c1_check", Expr: "(c1 > 1)"}}},
+			to:   &schema.Table{Name: "t1", Attrs: []schema.Attr{&schema.Check{Name: "t1_c1_check", Expr: "(c1 > 1)", Attrs: []schema.Attr{&NoInherit{}}}}},
 			wantChanges: []schema.Change{
 				&schema.ModifyAttr{
-					From: &Check{Name: "t1_c1_check", Clause: "(c1 > 1)"},
-					To:   &Check{Name: "t1_c1_check", Clause: "(c1 > 1)", NoInherit: true},
+					From: &schema.Check{Name: "t1_c1_check", Expr: "(c1 > 1)"},
+					To:   &schema.Check{Name: "t1_c1_check", Expr: "(c1 > 1)", Attrs: []schema.Attr{&NoInherit{}}},
 				},
 			},
 		},
diff --git a/sql/postgres/inspect_test.go b/sql/postgres/inspect_test.go
index 90952f3a..597b6111 100644
--- a/sql/postgres/inspect_test.go
+++ b/sql/postgres/inspect_test.go
@@ -280,11 +280,11 @@ func TestDriver_InspectTable(t *testing.T) {
 					{Name: "c3", Type: &schema.ColumnType{Raw: "integer", Type: &schema.IntegerType{T: "integer"}}},
 				}, t.Columns)
 				require.EqualValues([]schema.Attr{
-					&Check{Name: "boring", Clause: "(c1 > 1)", Columns: []string{"c1"}, NoInherit: true},
-					&Check{Name: "users_c2_check", Clause: "(c2 > 0)", Columns: []string{"c2"}},
-					&Check{Name: "users_c2_check1", Clause: "(c2 > 0)", Columns: []string{"c2"}},
-					&Check{Name: "users_check", Clause: "((c2 + c1) > 2)", Columns: []string{"c2", "c1"}},
-					&Check{Name: "users_check1", Clause: "(((c2 + c1) + c3) > 10)", Columns: []string{"c2", "c1", "c3"}},
+					&schema.Check{Name: "boring", Expr: "(c1 > 1)", Attrs: []schema.Attr{&CheckColumns{Columns: []string{"c1"}}, &NoInherit{}}},
+					&schema.Check{Name: "users_c2_check", Expr: "(c2 > 0)", Attrs: []schema.Attr{&CheckColumns{Columns: []string{"c2"}}}},
+					&schema.Check{Name: "users_c2_check1", Expr: "(c2 > 0)", Attrs: []schema.Attr{&CheckColumns{Columns: []string{"c2"}}}},
+					&schema.Check{Name: "users_check", Expr: "((c2 + c1) > 2)", Attrs: []schema.Attr{&CheckColumns{Columns: []string{"c2", "c1"}}}},
+					&schema.Check{Name: "users_check1", Expr: "(((c2 + c1) + c3) > 10)", Attrs: []schema.Attr{&CheckColumns{Columns: []string{"c2", "c1", "c3"}}}},
 				}, t.Attrs)
 			},
 		},