Repository: ariga/atlas
Pull request #350 (2021-12-29 12:58:59)

sql/mysql: support realm marshal and unmarshal
first PR towards multi-schema support.


Patch:
diff --git a/cmd/action/mux.go b/cmd/action/mux.go
index af39d406..c3c778d0 100644
--- a/cmd/action/mux.go
+++ b/cmd/action/mux.go
@@ -1,11 +1,14 @@
 package action
 
 import (
+	"context"
+	"database/sql"
 	"fmt"
 	"strings"
 
 	"ariga.io/atlas/schema/schemaspec"
 	"ariga.io/atlas/sql/migrate"
+	"ariga.io/atlas/sql/sqlite"
 	"github.com/go-sql-driver/mysql"
 )
 
@@ -20,7 +23,6 @@ type (
 		migrate.Driver
 		schemaspec.Marshaler
 		schemaspec.Unmarshaler
-		Types []*schemaspec.TypeSpec
 	}
 )
 
@@ -76,7 +78,28 @@ func schemaNameFromDSN(url string) (string, error) {
 		return cfg.DBName, err
 	case "postgres":
 		return "public", nil
+	case "sqlite3":
+		return schemaName(dsn)
 	default:
 		return "", fmt.Errorf("unknown database type: %q", key)
 	}
 }
+
+func schemaName(dsn string) (string, error) {
+	db, err := sql.Open("sqlite3", dsn)
+	if err != nil {
+		return "", err
+	}
+	drv, err := sqlite.Open(db)
+	if err != nil {
+		return "", err
+	}
+	r, err := drv.InspectRealm(context.Background(), nil)
+	if err != nil {
+		return "", err
+	}
+	if len(r.Schemas) > 1 {
+		return "", fmt.Errorf("number of schemas > 1, n=%d", len(r.Schemas))
+	}
+	return r.Schemas[0].Name, nil
+}
diff --git a/cmd/action/provider.go b/cmd/action/provider.go
index 975c7738..bef6d026 100644
--- a/cmd/action/provider.go
+++ b/cmd/action/provider.go
@@ -5,11 +5,13 @@ import (
 
 	"ariga.io/atlas/sql/mysql"
 	"ariga.io/atlas/sql/postgres"
+	"ariga.io/atlas/sql/sqlite"
 )
 
 func init() {
 	defaultMux.RegisterProvider("mysql", mysqlProvider)
 	defaultMux.RegisterProvider("postgres", postgresProvider)
+	defaultMux.RegisterProvider("sqlite3", sqliteProvider)
 }
 
 func mysqlProvider(dsn string) (*Driver, error) {
@@ -25,7 +27,6 @@ func mysqlProvider(dsn string) (*Driver, error) {
 		Driver:      drv,
 		Marshaler:   mysql.MarshalHCL,
 		Unmarshaler: mysql.UnmarshalHCL,
-		Types:       mysql.TypeRegistry.Specs(),
 	}, nil
 }
 func postgresProvider(dsn string) (*Driver, error) {
@@ -42,6 +43,20 @@ func postgresProvider(dsn string) (*Driver, error) {
 		Driver:      drv,
 		Marshaler:   postgres.MarshalHCL,
 		Unmarshaler: postgres.UnmarshalHCL,
-		Types:       postgres.TypeRegistry.Specs(),
+	}, nil
+}
+func sqliteProvider(dsn string) (*Driver, error) {
+	db, err := sql.Open("sqlite3", dsn)
+	if err != nil {
+		return nil, err
+	}
+	drv, err := sqlite.Open(db)
+	if err != nil {
+		return nil, err
+	}
+	return &Driver{
+		Driver:      drv,
+		Marshaler:   sqlite.MarshalHCL,
+		Unmarshaler: sqlite.UnmarshalHCL,
 	}, nil
 }
diff --git a/cmd/atlas/main.go b/cmd/atlas/main.go
index 617d804d..38fd3709 100644
--- a/cmd/atlas/main.go
+++ b/cmd/atlas/main.go
@@ -7,6 +7,7 @@ import (
 	"ariga.io/atlas/cmd/action"
 	_ "github.com/go-sql-driver/mysql"
 	_ "github.com/lib/pq"
+	_ "github.com/mattn/go-sqlite3"
 	"github.com/spf13/cobra"
 )
 
diff --git a/go.mod b/go.mod
index 6a9916d1..40270f6b 100644
--- a/go.mod
+++ b/go.mod
@@ -21,6 +21,7 @@ require (
 
 require (
 	github.com/go-openapi/inflect v0.19.0
+	github.com/mattn/go-sqlite3 v1.14.10
 	github.com/mitchellh/go-homedir v1.0.0
 )
 
diff --git a/go.sum b/go.sum
index 5dfd7d83..e2a8606f 100644
--- a/go.sum
+++ b/go.sum
@@ -207,6 +207,8 @@ github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYt
 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=
+github.com/mattn/go-sqlite3 v1.14.10 h1:MLn+5bFRlWMGoSRmJour3CL1w/qL96mvipqpwQW/Sfk=
+github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
 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=
diff --git a/internal/integration/go.mod b/internal/integration/go.mod
index a04ad032..9bbf07dd 100644
--- a/internal/integration/go.mod
+++ b/internal/integration/go.mod
@@ -10,7 +10,7 @@ require (
 	github.com/go-sql-driver/mysql v1.6.0
 	github.com/hashicorp/hcl/v2 v2.10.1 // indirect
 	github.com/lib/pq v1.10.4
-	github.com/mattn/go-sqlite3 v1.14.9
+	github.com/mattn/go-sqlite3 v1.14.10
 	github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942
 	github.com/zclconf/go-cty v1.10.0 // indirect
 )
diff --git a/internal/integration/go.sum b/internal/integration/go.sum
index 3ea3e613..31e6d9de 100644
--- a/internal/integration/go.sum
+++ b/internal/integration/go.sum
@@ -247,8 +247,9 @@ github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GW
 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=
 github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
-github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
 github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/mattn/go-sqlite3 v1.14.10 h1:MLn+5bFRlWMGoSRmJour3CL1w/qL96mvipqpwQW/Sfk=
+github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
 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=
@@ -276,7 +277,6 @@ github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
-github.com/pelletier/go-toml/v2 v2.0.0-beta.4/go.mod h1:ke6xncR3W76Ba8xnVxkrZG0js6Rd2BsQEAYrfgJ6eQA=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
diff --git a/sql/internal/specutil/convert.go b/sql/internal/specutil/convert.go
index 26e73fa3..ccd53e74 100644
--- a/sql/internal/specutil/convert.go
+++ b/sql/internal/specutil/convert.go
@@ -30,6 +30,29 @@ type (
 	ForeignKeySpecFunc    func(fk *schema.ForeignKey) (*sqlspec.ForeignKey, error)
 )
 
+// Realm converts the schemas and tables into a schema.Realm.
+func Realm(schemas []*sqlspec.Schema, tables []*sqlspec.Table, convertTable ConvertTableFunc) (*schema.Realm, error) {
+	r := &schema.Realm{}
+	for _, schemaSpec := range schemas {
+		var schemaTables []*sqlspec.Table
+		for _, tableSpec := range tables {
+			name, err := schemaName(tableSpec.Schema)
+			if err != nil {
+				return nil, fmt.Errorf("specutil: cannot extract schema name for table %q: %w", tableSpec.Name, err)
+			}
+			if name == schemaSpec.Name {
+				schemaTables = append(schemaTables, tableSpec)
+			}
+		}
+		sch, err := Schema(schemaSpec, schemaTables, convertTable)
+		if err != nil {
+			return nil, err
+		}
+		r.Schemas = append(r.Schemas, sch)
+	}
+	return r, nil
+}
+
 // Schema converts a sqlspec.Schema with its relevant []sqlspec.Tables
 // into a schema.Schema.
 func Schema(spec *sqlspec.Schema, tables []*sqlspec.Table, convertTable ConvertTableFunc) (*schema.Schema, error) {
@@ -222,6 +245,23 @@ func resolveCol(ref *schemaspec.Ref, sch *schema.Schema) (*schema.Column, error)
 	return col, nil
 }
 
+// FromRealm converts a schema.Realm into []sqlspec.Schema and []sqlspec.Table.
+func FromRealm(r *schema.Realm, fn TableSpecFunc) ([]*sqlspec.Schema, []*sqlspec.Table, error) {
+	var (
+		schemas []*sqlspec.Schema
+		tables  []*sqlspec.Table
+	)
+	for _, sch := range r.Schemas {
+		schemaSpec, tableSpecs, err := FromSchema(sch, fn)
+		if err != nil {
+			return nil, nil, fmt.Errorf("specutil: cannot convert schema %q: %w", sch.Name, err)
+		}
+		schemas = append(schemas, schemaSpec)
+		tables = append(tables, tableSpecs...)
+	}
+	return schemas, tables, nil
+}
+
 // FromSchema converts a schema.Schema into sqlspec.Schema and []sqlspec.Table.
 func FromSchema(s *schema.Schema, fn TableSpecFunc) (*sqlspec.Schema, []*sqlspec.Table, error) {
 	spec := &sqlspec.Schema{
@@ -378,6 +418,14 @@ func FromForeignKey(s *schema.ForeignKey) (*sqlspec.ForeignKey, error) {
 	}, nil
 }
 
+func schemaName(ref *schemaspec.Ref) (string, error) {
+	parts := strings.Split(ref.V, ".")
+	if len(parts) < 2 || parts[0] != "$schema" {
+		return "", fmt.Errorf("expected ref format of $schema.name")
+	}
+	return parts[1], nil
+}
+
 func columnName(ref *schemaspec.Ref) (string, error) {
 	s := strings.Split(ref.V, "$column.")
 	if len(s) != 2 {
diff --git a/sql/internal/specutil/spec.go b/sql/internal/specutil/spec.go
index 5f1729f9..92258eeb 100644
--- a/sql/internal/specutil/spec.go
+++ b/sql/internal/specutil/spec.go
@@ -1,9 +1,11 @@
 package specutil
 
 import (
+	"fmt"
 	"strconv"
 
 	"ariga.io/atlas/schema/schemaspec"
+	"ariga.io/atlas/sql/schema"
 	"ariga.io/atlas/sql/sqlspec"
 )
 
@@ -45,3 +47,64 @@ func ListAttr(k string, litValues ...string) *schemaspec.Attr {
 		V: lv,
 	}
 }
+
+type doc struct {
+	Tables  []*sqlspec.Table  `spec:"table"`
+	Schemas []*sqlspec.Schema `spec:"schema"`
+}
+
+// Marshal marshals v into an Atlas DDL document using a schemaspec.Marshaler. Marshal uses the given
+// schemaSpec function to convert a *schema.Schema into *sqlspec.Schema and []*sqlspec.Table.
+func Marshal(v interface{}, marshaler schemaspec.Marshaler, schemaSpec func(schem *schema.Schema) (*sqlspec.Schema, []*sqlspec.Table, error)) ([]byte, error) {
+	d := &doc{}
+	switch s := v.(type) {
+	case *schema.Schema:
+		spec, tables, err := schemaSpec(s)
+		if err != nil {
+			return nil, fmt.Errorf("specutil: failed converting schema to spec: %w", err)
+		}
+		d.Tables = tables
+		d.Schemas = []*sqlspec.Schema{spec}
+	case *schema.Realm:
+		for _, s := range s.Schemas {
+			spec, tables, err := schemaSpec(s)
+			if err != nil {
+				return nil, fmt.Errorf("specutil: failed converting schema to spec: %w", err)
+			}
+			d.Tables = append(d.Tables, tables...)
+			d.Schemas = append(d.Schemas, spec)
+		}
+	default:
+		return nil, fmt.Errorf("specutil: failed marshaling spec. %T is not supported", v)
+	}
+	return marshaler.MarshalSpec(d)
+}
+
+// Unmarshal unmarshals an Atlas DDL document using an unmarshaler into v. Unmarshal uses the
+// given convertTable function to convert a *sqlspec.Table into a *schema.Table.
+func Unmarshal(data []byte, unmarshaler schemaspec.Unmarshaler, v interface{}, convertTable func(spec *sqlspec.Table, parent *schema.Schema) (*schema.Table, error)) error {
+	var d doc
+	if err := unmarshaler.UnmarshalSpec(data, &d); err != nil {
+		return err
+	}
+	switch v := v.(type) {
+	case *schema.Realm:
+		realm, err := Realm(d.Schemas, d.Tables, convertTable)
+		if err != nil {
+			return fmt.Errorf("specutil: failed converting to *schema.Realm: %w", err)
+		}
+		*v = *realm
+	case *schema.Schema:
+		if len(d.Schemas) != 1 {
+			return fmt.Errorf("specutil: expecting document to contain a single schema, got %d", len(d.Schemas))
+		}
+		conv, err := Schema(d.Schemas[0], d.Tables, convertTable)
+		if err != nil {
+			return fmt.Errorf("specutil: failed converting to *schema.Schema: %w", err)
+		}
+		*v = *conv
+	default:
+		return fmt.Errorf("specutil: failed unmarshaling spec. %T is not supported", v)
+	}
+	return nil
+}
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/mysql/sqlspec.go b/sql/mysql/sqlspec.go
index b890fbfb..1675e51f 100644
--- a/sql/mysql/sqlspec.go
+++ b/sql/mysql/sqlspec.go
@@ -23,41 +23,43 @@ func UnmarshalSpec(data []byte, unmarshaler schemaspec.Unmarshaler, v interface{
 	if err := unmarshaler.UnmarshalSpec(data, &d); err != nil {
 		return err
 	}
-	s, ok := v.(*schema.Schema)
-	if !ok {
+	switch v := v.(type) {
+	case *schema.Realm:
+		realm, err := specutil.Realm(d.Schemas, d.Tables, convertTable)
+		if err != nil {
+			return fmt.Errorf("mysql: failed converting to *schema.Realm: %w", err)
+		}
+		for _, schemaSpec := range d.Schemas {
+			schm, ok := realm.Schema(schemaSpec.Name)
+			if !ok {
+				return fmt.Errorf("could not find schema: %q", schemaSpec.Name)
+			}
+			if err := convertCharset(schemaSpec, &schm.Attrs); err != nil {
+				return err
+			}
+		}
+		*v = *realm
+	case *schema.Schema:
+		if len(d.Schemas) != 1 {
+			return fmt.Errorf("mysql: expecting document to contain a single schema, got %d", len(d.Schemas))
+		}
+		conv, err := specutil.Schema(d.Schemas[0], d.Tables, convertTable)
+		if err != nil {
+			return fmt.Errorf("mysql: failed converting to *schema.Schema: %w", err)
+		}
+		if err := convertCharset(d.Schemas[0], &conv.Attrs); err != nil {
+			return err
+		}
+		*v = *conv
+	default:
 		return fmt.Errorf("mysql: failed unmarshaling spec. %T is not supported", v)
 	}
-	if len(d.Schemas) != 1 {
-		return fmt.Errorf("mysql: expecting document to contain a single schema, got %d", len(d.Schemas))
-	}
-	conv, err := specutil.Schema(d.Schemas[0], d.Tables, convertTable)
-	if err != nil {
-		return fmt.Errorf("mysql: failed converting to *schema.Schema: %w", err)
-	}
-	if err := convertCharset(d.Schemas[0], &conv.Attrs); err != nil {
-		return err
-	}
-	*s = *conv
 	return nil
 }
 
 // MarshalSpec marshals v into an Atlas DDL document using a schemaspec.Marshaler.
 func MarshalSpec(v interface{}, marshaler schemaspec.Marshaler) ([]byte, error) {
-	var (
-		s  *schema.Schema
-		ok bool
-	)
-	if s, ok = v.(*schema.Schema); !ok {
-		return nil, fmt.Errorf("mysql: failed marshaling spec. %T is not supported", v)
-	}
-	spec, tables, err := schemaSpec(s)
-	if err != nil {
-		return nil, fmt.Errorf("mysql: failed converting schema to spec: %w", err)
-	}
-	return marshaler.MarshalSpec(&doc{
-		Tables:  tables,
-		Schemas: []*sqlspec.Schema{spec},
-	})
+	return specutil.Marshal(v, marshaler, schemaSpec)
 }
 
 var (
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/postgres/migrate.go b/sql/postgres/migrate.go
index 02300cff..ce79c067 100644
--- a/sql/postgres/migrate.go
+++ b/sql/postgres/migrate.go
@@ -134,6 +134,12 @@ func (s *state) addTable(ctx context.Context, 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()
+				check(b, c)
+			}
+		}
 	})
 	s.append(&migrate.Change{
 		Cmd:     b.String(),
@@ -214,25 +220,28 @@ func (s *state) modifyTable(ctx context.Context, modify *schema.ModifyTable) err
 	}
 	s.dropIndexes(modify.T, dropI...)
 	if len(changes) > 0 {
-		s.alterTable(modify.T, changes)
+		if err := s.alterTable(modify.T, changes); err != nil {
+			return err
+		}
 	}
 	s.addIndexes(modify.T, addI...)
 	return nil
 }
 
 // alterTable modifies the given table by executing on it a list of changes in one SQL statement.
-func (s *state) alterTable(t *schema.Table, changes []schema.Change) {
+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, change.C)
-			reverse.P("DROP COLUMN").Ident(change.C.Name)
+			reverse.Comma().P("DROP COLUMN").Ident(change.C.Name)
 		case *schema.DropColumn:
 			b.P("DROP COLUMN").Ident(change.C.Name)
 			reversible = false
@@ -242,12 +251,41 @@ func (s *state) alterTable(t *schema.Table, changes []schema.Change) {
 		case *schema.AddForeignKey:
 			b.P("ADD")
 			s.fks(b, change.F)
-			reverse.P("DROP CONSTRAINT").Ident(change.F.Symbol)
+			reverse.Comma().P("DROP CONSTRAINT").Ident(change.F.Symbol)
 		case *schema.DropForeignKey:
 			b.P("DROP CONSTRAINT").Ident(change.F.Symbol)
 			reversible = false
+		case *schema.AddCheck:
+			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 CONSTRAINT").Ident(change.C.Name)
+			}
+		case *schema.DropCheck:
+			b.P("DROP CONSTRAINT").Ident(change.C.Name)
+			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))
+			case change.From.Expr != change.To.Expr,
+				sqlx.Has(change.From.Attrs, &NoInherit{}) && !sqlx.Has(change.To.Attrs, &NoInherit{}),
+				!sqlx.Has(change.From.Attrs, &NoInherit{}) && sqlx.Has(change.To.Attrs, &NoInherit{}):
+				b.P("DROP CONSTRAINT").Ident(change.From.Name).Comma().P("ADD")
+				check(b, change.To)
+				reverse.Comma().P("DROP CONSTRAINT").Ident(change.To.Name).Comma().P("ADD")
+				check(reverse, change.From)
+			default:
+				errors = append(errors, "unknown check constraints change")
+			}
 		}
 	})
+	if len(errors) > 0 {
+		return fmt.Errorf("alter table: %s", strings.Join(errors, ", "))
+	}
 	change := &migrate.Change{
 		Cmd: b.String(),
 		Source: &schema.ModifyTable{
@@ -257,9 +295,14 @@ func (s *state) alterTable(t *schema.Table, changes []schema.Change) {
 		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
 }
 
 func (s *state) addComments(t *schema.Table) {
@@ -588,6 +631,22 @@ func skipAutoChanges(changes []schema.Change) []schema.Change {
 	return changes
 }
 
+// checks writes the CHECK constraint to the builder.
+func 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 sqlx.Has(c.Attrs, &NoInherit{}) {
+		b.P("NO INHERIT")
+	}
+}
+
 func quote(s string) string {
 	if sqlx.IsQuoted(s, '\'') {
 		return s
diff --git a/sql/postgres/sqlspec.go b/sql/postgres/sqlspec.go
index 4a09ca54..08dac5cc 100644
--- a/sql/postgres/sqlspec.go
+++ b/sql/postgres/sqlspec.go
@@ -1,7 +1,6 @@
 package postgres
 
 import (
-	"fmt"
 	"reflect"
 	"strconv"
 	"strings"
@@ -15,49 +14,14 @@ import (
 	"ariga.io/atlas/sql/sqlspec"
 )
 
-type doc struct {
-	Tables  []*sqlspec.Table  `spec:"table"`
-	Schemas []*sqlspec.Schema `spec:"schema"`
-}
-
 // UnmarshalSpec unmarshals an Atlas DDL document using an unmarshaler into v.
 func UnmarshalSpec(data []byte, unmarshaler schemaspec.Unmarshaler, v interface{}) error {
-	var d doc
-	if err := unmarshaler.UnmarshalSpec(data, &d); err != nil {
-		return err
-	}
-	s, ok := v.(*schema.Schema)
-	if !ok {
-		return fmt.Errorf("postgres: failed unmarshaling spec. %T is not supported", v)
-	}
-	if len(d.Schemas) != 1 {
-		return fmt.Errorf("postgres: expecting document to contain a single schema, got %d", len(d.Schemas))
-	}
-	conv, err := specutil.Schema(d.Schemas[0], d.Tables, convertTable)
-	if err != nil {
-		return fmt.Errorf("postgres: failed converting to *schema.Schema: %w", err)
-	}
-	*s = *conv
-	return nil
+	return specutil.Unmarshal(data, unmarshaler, v, convertTable)
 }
 
 // MarshalSpec marshals v into an Atlas DDL document using a schemaspec.Marshaler.
 func MarshalSpec(v interface{}, marshaler schemaspec.Marshaler) ([]byte, error) {
-	var (
-		s  *schema.Schema
-		ok bool
-	)
-	if s, ok = v.(*schema.Schema); !ok {
-		return nil, fmt.Errorf("failed marshaling spec. %T is not supported", v)
-	}
-	spec, tables, err := schemaSpec(s)
-	if err != nil {
-		return nil, fmt.Errorf("failed converting schema to spec: %w", err)
-	}
-	return marshaler.MarshalSpec(&doc{
-		Tables:  tables,
-		Schemas: []*sqlspec.Schema{spec},
-	})
+	return specutil.Marshal(v, marshaler, schemaSpec)
 }
 
 // convertTable converts a sqlspec.Table to a schema.Table. Table conversion is done without converting
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() {}
diff --git a/sql/sqlite/sqlspec.go b/sql/sqlite/sqlspec.go
index 38800ba3..64121abb 100644
--- a/sql/sqlite/sqlspec.go
+++ b/sql/sqlite/sqlspec.go
@@ -1,7 +1,6 @@
 package sqlite
 
 import (
-	"fmt"
 	"reflect"
 
 	"ariga.io/atlas/schema/schemaspec"
@@ -11,46 +10,14 @@ import (
 	"ariga.io/atlas/sql/sqlspec"
 )
 
-type doc struct {
-	Tables  []*sqlspec.Table  `spec:"table"`
-	Schemas []*sqlspec.Schema `spec:"schema"`
-}
-
 // UnmarshalSpec unmarshals an Atlas DDL document using an unmarshaler into v.
 func UnmarshalSpec(data []byte, unmarshaler schemaspec.Unmarshaler, v interface{}) error {
-	var d doc
-	if err := unmarshaler.UnmarshalSpec(data, &d); err != nil {
-		return err
-	}
-	s, ok := v.(*schema.Schema)
-	if !ok {
-		return fmt.Errorf("sqlite: failed unmarshaling spec. %T is not supported", v)
-	}
-	if len(d.Schemas) != 1 {
-		return fmt.Errorf("sqlite: expecting document to contain a single schema, got %d", len(d.Schemas))
-	}
-	conv, err := specutil.Schema(d.Schemas[0], d.Tables, convertTable)
-	if err != nil {
-		return fmt.Errorf("sqlite: failed converting to *schema.Schema: %w", err)
-	}
-	*s = *conv
-	return nil
+	return specutil.Unmarshal(data, unmarshaler, v, convertTable)
 }
 
 // MarshalSpec marshals v into an Atlas DDL document using a schemaspec.Marshaler.
 func MarshalSpec(v interface{}, marshaler schemaspec.Marshaler) ([]byte, error) {
-	s, ok := v.(*schema.Schema)
-	if !ok {
-		return nil, fmt.Errorf("sqlite: failed marshaling spec. %T is not supported", v)
-	}
-	spec, tables, err := schemaSpec(s)
-	if err != nil {
-		return nil, fmt.Errorf("sqlite: failed converting schema to spec: %w", err)
-	}
-	return marshaler.MarshalSpec(&doc{
-		Tables:  tables,
-		Schemas: []*sqlspec.Schema{spec},
-	})
+	return specutil.Marshal(v, marshaler, schemaSpec)
 }
 
 // convertTable converts a sqlspec.Table to a schema.Table. Table conversion is done without converting


Test patch:
diff --git a/internal/integration/hclsqlspec/hclsqlspec_test.go b/internal/integration/hclsqlspec/hclsqlspec_test.go
index b2a0dc4c..65f2c781 100644
--- a/internal/integration/hclsqlspec/hclsqlspec_test.go
+++ b/internal/integration/hclsqlspec/hclsqlspec_test.go
@@ -5,7 +5,10 @@ import (
 	"testing"
 
 	"ariga.io/atlas/schema/schemaspec"
+	"ariga.io/atlas/sql/mysql"
+	"ariga.io/atlas/sql/postgres"
 	"ariga.io/atlas/sql/schema"
+	"ariga.io/atlas/sql/sqlite"
 	"ariga.io/atlas/sql/sqlspec"
 	"github.com/stretchr/testify/require"
 )
@@ -331,6 +334,72 @@ func TestMarshalTopLevel(t *testing.T) {
 `, string(h))
 }
 
+func TestRealm(t *testing.T) {
+	f := `schema "account_a" {
+}
+table "t1" {
+	schema = schema.account_a
+}
+schema "account_b" {
+}
+table "t2" {
+	schema = schema.account_b
+}
+`
+	for _, tt := range []struct {
+		name string
+		schemaspec.Marshaler
+		schemaspec.Unmarshaler
+	}{
+		{
+			name:        "mysql",
+			Marshaler:   mysql.MarshalHCL,
+			Unmarshaler: mysql.UnmarshalHCL,
+		},
+		{
+			name:        "postgres",
+			Marshaler:   postgres.MarshalHCL,
+			Unmarshaler: postgres.UnmarshalHCL,
+		},
+		{
+			name:        "sqlite",
+			Marshaler:   sqlite.MarshalHCL,
+			Unmarshaler: sqlite.UnmarshalHCL,
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			var r schema.Realm
+			err := tt.UnmarshalSpec([]byte(f), &r)
+			require.NoError(t, err)
+			exp := &schema.Realm{
+				Schemas: []*schema.Schema{
+					{
+						Name: "account_a",
+						Tables: []*schema.Table{
+							{Name: "t1"},
+						},
+					},
+					{
+						Name: "account_b",
+						Tables: []*schema.Table{
+							{Name: "t2"},
+						},
+					},
+				},
+			}
+			exp.Schemas[0].Tables[0].Schema = exp.Schemas[0]
+			exp.Schemas[1].Tables[0].Schema = exp.Schemas[1]
+			require.EqualValues(t, exp, &r)
+			hcl, err := tt.MarshalSpec(&r)
+			require.NoError(t, err)
+			var after schema.Realm
+			err = tt.UnmarshalSpec(hcl, &after)
+			require.NoError(t, err)
+			require.EqualValues(t, exp, &after)
+		})
+	}
+}
+
 func decode(f string) (*db, error) {
 	d := &db{}
 	if err := hcl.UnmarshalSpec([]byte(f), d); err != nil {
diff --git a/internal/integration/mysql_test.go b/internal/integration/mysql_test.go
index 5fe24260..8b0bcabd 100644
--- a/internal/integration/mysql_test.go
+++ b/internal/integration/mysql_test.go
@@ -529,6 +529,26 @@ func TestMySQL_CLI(t *testing.T) {
 	})
 }
 
+func TestMySQL_HCL_Realm(t *testing.T) {
+	myRun(t, func(t *myTest) {
+		t.dropDB("second")
+		realm := t.loadRealm()
+		hcl, err := mysql.MarshalHCL(realm)
+		require.NoError(t, err)
+		wa := string(hcl) + `
+schema "second" {
+}
+`
+		t.applyRealmHcl(wa)
+		realm, err = t.drv.InspectRealm(context.Background(), &schema.InspectRealmOption{})
+		require.NoError(t, err)
+		_, ok := realm.Schema("test")
+		require.True(t, ok)
+		_, ok = realm.Schema("second")
+		require.True(t, ok)
+	})
+}
+
 func TestMySQL_DefaultsHCL(t *testing.T) {
 	n := "atlas_defaults"
 	myRun(t, func(t *myTest) {
@@ -548,7 +568,7 @@ create table atlas_defaults
 		realm := t.loadRealm()
 		spec, err := mysql.MarshalHCL(realm.Schemas[0])
 		require.NoError(t, err)
-		var s schema.Schema
+		var s schema.Realm
 		err = mysql.UnmarshalHCL(spec, &s)
 		require.NoError(t, err)
 		t.dropTables(n)
@@ -895,7 +915,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{
@@ -949,6 +969,17 @@ func (t *myTest) applyHcl(spec string) {
 	require.NoError(t, err)
 }
 
+func (t *myTest) applyRealmHcl(spec string) {
+	realm := t.loadRealm()
+	var desired schema.Realm
+	err := mysql.UnmarshalHCL([]byte(spec), &desired)
+	require.NoError(t, err)
+	diff, err := t.drv.RealmDiff(realm, &desired)
+	require.NoError(t, err)
+	err = t.drv.ApplyChanges(context.Background(), diff)
+	require.NoError(t, err)
+}
+
 func (t *myTest) diff(t1, t2 *schema.Table) []schema.Change {
 	changes, err := t.drv.TableDiff(t1, t2)
 	require.NoError(t, err)
@@ -967,6 +998,13 @@ func (t *myTest) dropTables(names ...string) {
 	})
 }
 
+func (t *myTest) dropDB(names ...string) {
+	t.Cleanup(func() {
+		_, err := t.db.Exec("DROP DATABASE IF EXISTS " + strings.Join(names, ", "))
+		require.NoError(t.T, err, "drop db %q", names)
+	})
+}
+
 func (t *myTest) realm() *schema.Realm {
 	r := &schema.Realm{
 		Schemas: []*schema.Schema{
diff --git a/internal/integration/postgres_test.go b/internal/integration/postgres_test.go
index 8edd945c..98242a9e 100644
--- a/internal/integration/postgres_test.go
+++ b/internal/integration/postgres_test.go
@@ -12,6 +12,7 @@ import (
 	"sync"
 	"testing"
 
+	"ariga.io/atlas/sql/mysql"
 	"ariga.io/atlas/sql/postgres"
 	"ariga.io/atlas/sql/schema"
 
@@ -413,6 +414,37 @@ schema "public" {
 	})
 }
 
+func TestPostgres_HCL_Realm(t *testing.T) {
+	pgRun(t, func(t *pgTest) {
+		t.dropSchemas("second")
+		realm := t.loadRealm()
+		hcl, err := mysql.MarshalHCL(realm)
+		require.NoError(t, err)
+		wa := string(hcl) + `
+schema "second" {
+}
+`
+		t.applyRealmHcl(wa)
+		realm, err = t.drv.InspectRealm(context.Background(), &schema.InspectRealmOption{})
+		require.NoError(t, err)
+		_, ok := realm.Schema("public")
+		require.True(t, ok)
+		_, ok = realm.Schema("second")
+		require.True(t, ok)
+	})
+}
+
+func (t *pgTest) applyRealmHcl(spec string) {
+	realm := t.loadRealm()
+	var desired schema.Realm
+	err := mysql.UnmarshalHCL([]byte(spec), &desired)
+	require.NoError(t, err)
+	diff, err := t.drv.RealmDiff(realm, &desired)
+	require.NoError(t, err)
+	err = t.drv.ApplyChanges(context.Background(), diff)
+	require.NoError(t, err)
+}
+
 func TestPostgres_CLI(t *testing.T) {
 	h := `
 			schema "public" {
@@ -985,3 +1017,10 @@ func (t *pgTest) dropTables(names ...string) {
 		require.NoError(t.T, err, "drop tables %q", names)
 	})
 }
+
+func (t *pgTest) dropSchemas(names ...string) {
+	t.Cleanup(func() {
+		_, err := t.db.Exec("DROP SCHEMA IF EXISTS " + strings.Join(names, ", "))
+		require.NoError(t.T, err, "drop schema %q", names)
+	})
+}
diff --git a/internal/integration/sqlite_test.go b/internal/integration/sqlite_test.go
index 595e5bad..05f23333 100644
--- a/internal/integration/sqlite_test.go
+++ b/internal/integration/sqlite_test.go
@@ -8,6 +8,9 @@ import (
 	"context"
 	"database/sql"
 	"database/sql/driver"
+	"fmt"
+	"path"
+	"strings"
 	"testing"
 
 	"ariga.io/atlas/sql/postgres"
@@ -21,19 +24,21 @@ import (
 
 type liteTest struct {
 	*testing.T
-	db  *sql.DB
-	drv *sqlite.Driver
+	db   *sql.DB
+	drv  *sqlite.Driver
+	file string
 }
 
 func liteRun(t *testing.T, fn func(test *liteTest)) {
-	db, err := sql.Open("sqlite3", "file:atlas?mode=memory&cache=shared&_fk=1")
+	f := path.Join(t.TempDir(), strings.ReplaceAll(t.Name(), "/", "_"))
+	db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&_fk=1", f))
 	require.NoError(t, err)
 	t.Cleanup(func() {
 		require.NoError(t, db.Close())
 	})
 	drv, err := sqlite.Open(db)
 	require.NoError(t, err)
-	tt := &liteTest{T: t, db: db, drv: drv}
+	tt := &liteTest{T: t, db: db, drv: drv, file: f}
 	fn(tt)
 }
 
@@ -422,6 +427,28 @@ create table atlas_defaults
 	})
 }
 
+func TestSQLite_CLI(t *testing.T) {
+	h := `
+			schema "main" {
+			}
+			table "users" {
+				schema = schema.main
+				column "id" {
+					type = int
+				}
+			}`
+	t.Run("SchemaInspect", func(t *testing.T) {
+		liteRun(t, func(t *liteTest) {
+			testCLISchemaInspect(t, h, t.dsn(), sqlite.UnmarshalHCL)
+		})
+	})
+	t.Run("SchemaApply", func(t *testing.T) {
+		liteRun(t, func(t *liteTest) {
+			testCLISchemaApply(t, h, t.dsn())
+		})
+	})
+}
+
 func TestSQLite_Sanity(t *testing.T) {
 	n := "atlas_types_sanity"
 	ddl := `
@@ -764,7 +791,7 @@ func (t *liteTest) realm() *schema.Realm {
 			{
 				Name: "main",
 				Attrs: []schema.Attr{
-					&sqlite.File{Name: ":memory:"},
+					&sqlite.File{Name: t.file},
 				},
 			},
 		},
@@ -792,3 +819,7 @@ func (t *liteTest) dropTables(names ...string) {
 		}
 	})
 }
+
+func (t *liteTest) dsn() string {
+	return fmt.Sprintf("sqlite3://file:%s?cache=shared&_fk=1", t.file)
+}
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)
 			},
 		},
diff --git a/sql/postgres/migrate_test.go b/sql/postgres/migrate_test.go
index 518e07a6..4f68d5f0 100644
--- a/sql/postgres/migrate_test.go
+++ b/sql/postgres/migrate_test.go
@@ -46,13 +46,17 @@ func TestPlanChanges(t *testing.T) {
 							{Name: "id", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "integer"}}},
 							{Name: "text", Type: &schema.ColumnType{Type: &schema.StringType{T: "text"}, Null: true}},
 						},
+						Attrs: []schema.Attr{
+							&schema.Check{Name: "id_nonzero", Expr: `("id" > 0)`},
+							&schema.Check{Name: "text_len", Expr: `(length("text") > 0)`, Attrs: []schema.Attr{&NoInherit{}}},
+						},
 					},
 				},
 			},
 			plan: &migrate.Plan{
 				Reversible:    true,
 				Transactional: true,
-				Changes:       []*migrate.Change{{Cmd: `CREATE TABLE "posts" ("id" integer NOT NULL, "text" text NULL)`, Reverse: `DROP TABLE "posts"`}},
+				Changes:       []*migrate.Change{{Cmd: `CREATE TABLE "posts" ("id" integer NOT NULL, "text" text NULL, CONSTRAINT "id_nonzero" CHECK ("id" > 0), CONSTRAINT "text_len" CHECK (length("text") > 0) NO INHERIT)`, Reverse: `DROP TABLE "posts"`}},
 			},
 		},
 		{
@@ -93,6 +97,16 @@ func TestPlanChanges(t *testing.T) {
 									},
 								},
 							},
+							&schema.AddCheck{
+								C: &schema.Check{Name: "name_not_empty", Expr: `("name" <> '')`},
+							},
+							&schema.DropCheck{
+								C: &schema.Check{Name: "id_nonzero", Expr: `("id" <> 0)`},
+							},
+							&schema.ModifyCheck{
+								From: &schema.Check{Name: "id_iseven", Expr: `("id" % 2 = 0)`},
+								To:   &schema.Check{Name: "id_iseven", Expr: `(("id") % 2 = 0)`},
+							},
 						},
 					}
 				}(),
@@ -101,8 +115,14 @@ func TestPlanChanges(t *testing.T) {
 				Reversible:    true,
 				Transactional: true,
 				Changes: []*migrate.Change{
-					{Cmd: `ALTER TABLE "users" ADD COLUMN "name" character varying(255) NOT NULL`, Reverse: `ALTER TABLE "users" DROP COLUMN "name"`},
-					{Cmd: `CREATE INDEX "id_key" ON "users" ("id")`, Reverse: `DROP INDEX "id_key"`},
+					{
+						Cmd:     `ALTER TABLE "users" ADD COLUMN "name" character varying(255) NOT NULL, ADD CONSTRAINT "name_not_empty" CHECK ("name" <> ''), DROP CONSTRAINT "id_nonzero", DROP CONSTRAINT "id_iseven", ADD CONSTRAINT "id_iseven" CHECK (("id") % 2 = 0)`,
+						Reverse: `ALTER TABLE "users" DROP COLUMN "name", DROP CONSTRAINT "name_not_empty", ADD CONSTRAINT "id_nonzero" CHECK ("id" <> 0), DROP CONSTRAINT "id_iseven", ADD CONSTRAINT "id_iseven" CHECK ("id" % 2 = 0)`,
+					},
+					{
+						Cmd:     `CREATE INDEX "id_key" ON "users" ("id")`,
+						Reverse: `DROP INDEX "id_key"`,
+					},
 				},
 			},
 		},