Repository: ariga/atlas
Pull request #1789 (2023-06-26 07:57:44)

sql/postgres: support diffing objects
Fixed https://github.com/ariga/atlas/issues/1775


Patch:
diff --git a/cmd/atlas/internal/cmdlog/cmdlog.go b/cmd/atlas/internal/cmdlog/cmdlog.go
index dc534a31..124bc09e 100644
--- a/cmd/atlas/internal/cmdlog/cmdlog.go
+++ b/cmd/atlas/internal/cmdlog/cmdlog.go
@@ -659,6 +659,12 @@ func sqlInspect(report *SchemaInspect, indent ...string) (string, error) {
 		for _, t := range s.Tables {
 			changes = append(changes, &schema.AddTable{T: t})
 		}
+		for _, v := range s.Views {
+			changes = append(changes, &schema.AddView{V: v})
+		}
+		for _, o := range s.Objects {
+			changes = append(changes, &schema.AddObject{O: o})
+		}
 	}
 	return fmtPlan(report.Client, changes, indent)
 }
diff --git a/sql/internal/sqlx/dev.go b/sql/internal/sqlx/dev.go
index d3ef8fef..82c05997 100644
--- a/sql/internal/sqlx/dev.go
+++ b/sql/internal/sqlx/dev.go
@@ -21,10 +21,9 @@ type DevDriver struct {
 		migrate.CleanChecker
 		migrate.Snapshoter
 	}
-
-	// PatchColumn allows providing a custom function to patch
-	// columns that hold a schema reference.
-	PatchColumn func(*schema.Schema, *schema.Column)
+	// PathObject allows providing a custom function to patch
+	// objects that hold a schema reference.
+	PatchObject func(*schema.Schema, schema.Object)
 }
 
 // NormalizeRealm implements the schema.Normalizer interface.
@@ -67,6 +66,12 @@ func (d *DevDriver) NormalizeRealm(ctx context.Context, r *schema.Realm) (nr *sc
 				T: t,
 			})
 		}
+		for _, v := range s.Views {
+			changes = append(changes, &schema.AddView{V: v})
+		}
+		for _, o := range s.Objects {
+			changes = append(changes, &schema.AddObject{O: o})
+		}
 	}
 	if err := d.Driver.ApplyChanges(ctx, changes); err != nil {
 		return nil, err
@@ -117,12 +122,22 @@ func (d *DevDriver) NormalizeSchema(ctx context.Context, s *schema.Schema) (*sch
 			if e, ok := c.Type.Type.(*schema.EnumType); ok && e.Schema != s {
 				e.Schema = s
 			}
-			if d.PatchColumn != nil {
-				d.PatchColumn(s, c)
-			}
 		}
 		changes = append(changes, &schema.AddTable{T: t})
 	}
+	for _, v := range s.Views {
+		// If objects are not strongly connected.
+		if v.Schema != s {
+			v.Schema = s
+		}
+		changes = append(changes, &schema.AddView{V: v})
+	}
+	for _, o := range s.Objects {
+		if d.PatchObject != nil {
+			d.PatchObject(s, o)
+		}
+		changes = append(changes, &schema.AddObject{O: o})
+	}
 	if err := d.Driver.ApplyChanges(ctx, changes, func(opts *migrate.PlanOptions) {
 		noQualifier := ""
 		opts.SchemaQualifier = &noQualifier
diff --git a/sql/internal/sqlx/diff.go b/sql/internal/sqlx/diff.go
index 40d759ea..7ebb660d 100644
--- a/sql/internal/sqlx/diff.go
+++ b/sql/internal/sqlx/diff.go
@@ -31,6 +31,10 @@ type (
 		// from one state to the other. For example, changing schema collation.
 		SchemaAttrDiff(from, to *schema.Schema) []schema.Change
 
+		// SchemaObjectDiff returns a changeset for migrating schema objects from
+		// one state to the other. For example, changing schema custom types.
+		SchemaObjectDiff(from, to *schema.Schema) ([]schema.Change, error)
+
 		// TableAttrDiff returns a changeset for migrating table attributes from
 		// one state to the other. For example, dropping or adding a `CHECK` constraint.
 		TableAttrDiff(from, to *schema.Table) ([]schema.Change, error)
@@ -136,6 +140,12 @@ func (d *Diff) schemaDiff(from, to *schema.Schema, opts *schema.DiffOptions) ([]
 			Changes: change,
 		})
 	}
+	// Add, drop or modify objects.
+	change, err := d.SchemaObjectDiff(from, to)
+	if err != nil {
+		return nil, err
+	}
+	changes = opts.AddOrSkip(changes, change...)
 
 	// Drop or modify tables.
 	for _, t1 := range from.Tables {
diff --git a/sql/mysql/diff.go b/sql/mysql/diff.go
index 05585bf2..8035af44 100644
--- a/sql/mysql/diff.go
+++ b/sql/mysql/diff.go
@@ -53,6 +53,12 @@ func (d *diff) SchemaAttrDiff(from, to *schema.Schema) []schema.Change {
 	return changes
 }
 
+// SchemaObjectDiff returns a changeset for migrating schema objects from
+// one state to the other.
+func (*diff) SchemaObjectDiff(_, _ *schema.Schema) ([]schema.Change, error) {
+	return nil, nil
+}
+
 // TableAttrDiff returns a changeset for migrating table attributes from one state to the other.
 func (d *diff) TableAttrDiff(from, to *schema.Table) ([]schema.Change, error) {
 	var changes []schema.Change
diff --git a/sql/postgres/diff.go b/sql/postgres/diff.go
index aa275739..94bc8cfd 100644
--- a/sql/postgres/diff.go
+++ b/sql/postgres/diff.go
@@ -27,11 +27,49 @@ var DefaultDiff schema.Differ = &sqlx.Diff{DiffDriver: &diff{}}
 type diff struct{ conn }
 
 // SchemaAttrDiff returns a changeset for migrating schema attributes from one state to the other.
-func (d *diff) SchemaAttrDiff(_, _ *schema.Schema) []schema.Change {
+func (*diff) SchemaAttrDiff(_, _ *schema.Schema) []schema.Change {
 	// No special schema attribute diffing for PostgreSQL.
 	return nil
 }
 
+// SchemaObjectDiff returns a changeset for migrating schema objects from
+// one state to the other.
+func (*diff) SchemaObjectDiff(from, to *schema.Schema) ([]schema.Change, error) {
+	var changes []schema.Change
+	// Drop or modify enums.
+	for _, o1 := range from.Objects {
+		e1, ok := o1.(*schema.EnumType)
+		if !ok {
+			return nil, fmt.Errorf("unsupported object type %T", o1)
+		}
+		o2, ok := to.Object(func(o schema.Object) bool {
+			e2, ok := o.(*schema.EnumType)
+			return ok && e1.T == e2.T
+		})
+		if !ok {
+			changes = append(changes, &schema.DropObject{O: o1})
+			continue
+		}
+		if e2 := o2.(*schema.EnumType); !sqlx.ValuesEqual(e1.Values, e2.Values) {
+			changes = append(changes, &schema.ModifyObject{From: e1, To: e2})
+		}
+	}
+	// Add new enums.
+	for _, o1 := range to.Objects {
+		e1, ok := o1.(*schema.EnumType)
+		if !ok {
+			return nil, fmt.Errorf("unsupported object type %T", o1)
+		}
+		if _, ok := from.Object(func(o schema.Object) bool {
+			e2, ok := o.(*schema.EnumType)
+			return ok && e1.T == e2.T
+		}); !ok {
+			changes = append(changes, &schema.AddObject{O: e1})
+		}
+	}
+	return changes, nil
+}
+
 // TableAttrDiff returns a changeset for migrating table attributes from one state to the other.
 func (d *diff) TableAttrDiff(from, to *schema.Table) ([]schema.Change, error) {
 	var changes []schema.Change
@@ -293,9 +331,8 @@ func (d *diff) typeChanged(from, to *schema.Column) (bool, error) {
 		changed = t1 != t2
 	case *schema.EnumType:
 		toT := toT.(*schema.EnumType)
-		// Column type was changed if the underlying enum type was changed or values are not equal.
-		changed = !sqlx.ValuesEqual(fromT.Values, toT.Values) || fromT.T != toT.T ||
-			(toT.Schema != nil && fromT.Schema != nil && fromT.Schema.Name != toT.Schema.Name)
+		// Column type was changed if the underlying enum type was changed.
+		changed = fromT.T != toT.T || (toT.Schema != nil && fromT.Schema != nil && fromT.Schema.Name != toT.Schema.Name)
 	case *CurrencyType:
 		toT := toT.(*CurrencyType)
 		changed = fromT.T != toT.T
@@ -304,14 +341,6 @@ func (d *diff) typeChanged(from, to *schema.Column) (bool, error) {
 		changed = fromT.T != toT.T
 	case *ArrayType:
 		toT := toT.(*ArrayType)
-		// Same type.
-		if changed = fromT.T != toT.T; !changed {
-			// In case it is an enum type, compare its values.
-			fromE, ok1 := fromT.Type.(*schema.EnumType)
-			toE, ok2 := toT.Type.(*schema.EnumType)
-			changed = ok1 && ok2 && !sqlx.ValuesEqual(fromE.Values, toE.Values)
-			break
-		}
 		// In case the desired schema is not normalized, the string type can look different even
 		// if the two strings represent the same array type (varchar(1), character varying (1)).
 		// Therefore, we try by comparing the underlying types if they were defined.
diff --git a/sql/postgres/driver.go b/sql/postgres/driver.go
index 0e070d25..25542ae7 100644
--- a/sql/postgres/driver.go
+++ b/sql/postgres/driver.go
@@ -127,8 +127,8 @@ func Open(db schema.ExecQuerier) (migrate.Driver, error) {
 func (d *Driver) dev() *sqlx.DevDriver {
 	return &sqlx.DevDriver{
 		Driver: d,
-		PatchColumn: func(s *schema.Schema, c *schema.Column) {
-			if e, ok := hasEnumType(c); ok {
+		PatchObject: func(s *schema.Schema, o schema.Object) {
+			if e, ok := o.(*schema.EnumType); ok {
 				e.Schema = s
 			}
 		},
diff --git a/sql/postgres/migrate.go b/sql/postgres/migrate.go
index f1ba004b..a8ca38d6 100644
--- a/sql/postgres/migrate.go
+++ b/sql/postgres/migrate.go
@@ -33,9 +33,6 @@ func (p *planApply) PlanChanges(ctx context.Context, name string, changes []sche
 			Name:          name,
 			Transactional: true,
 		},
-		createdE: make(map[string]*schema.EnumType),
-		alteredE: make(map[string]*schema.EnumType),
-		droppedE: make(map[string]*schema.EnumType),
 	}
 	for _, o := range opts {
 		o(&s.PlanOptions)
@@ -64,9 +61,6 @@ type state struct {
 	migrate.Plan
 	migrate.PlanOptions
 	droppedT []*schema.Table
-	// Track the enums that were created, altered and
-	// dropped, in this phase to avoid duplicate updates.
-	createdE, alteredE, droppedE map[string]*schema.EnumType
 }
 
 // Exec executes the changes on the database. An error is returned
@@ -77,11 +71,14 @@ func (s *state) plan(ctx context.Context, changes []schema.Change) error {
 			return err
 		}
 	}
-	planned := s.topLevel(changes)
-	planned, err := sqlx.DetachCycles(planned)
+	planned, err := s.topLevel(changes)
 	if err != nil {
 		return err
 	}
+	if planned, err = sqlx.DetachCycles(planned); err != nil {
+		return err
+	}
+	deferred := make([]schema.Change, 0, len(planned))
 	for _, c := range planned {
 		switch c := c.(type) {
 		case *schema.AddTable:
@@ -92,6 +89,18 @@ func (s *state) plan(ctx context.Context, changes []schema.Change) error {
 			err = s.modifyTable(ctx, c)
 		case *schema.RenameTable:
 			s.renameTable(c)
+		case *schema.DropObject, *schema.AddView, *schema.DropView,
+			*schema.ModifyView, *schema.RenameView:
+			deferred = append(deferred, c)
+		default:
+			err = fmt.Errorf("unsupported change %T", c)
+		}
+		if err != nil {
+			return err
+		}
+	}
+	for _, c := range deferred {
+		switch c := c.(type) {
 		case *schema.AddView:
 			err = s.addView(ctx, c)
 		case *schema.DropView:
@@ -100,18 +109,26 @@ func (s *state) plan(ctx context.Context, changes []schema.Change) error {
 			err = s.modifyView(ctx, c)
 		case *schema.RenameView:
 			s.renameView(ctx, c)
-		default:
-			err = fmt.Errorf("unsupported change %T", c)
-		}
-		if err != nil {
-			return err
+		case *schema.DropObject:
+			e, ok := c.O.(*schema.EnumType)
+			if !ok {
+				return fmt.Errorf("unsupported object %T", c.O)
+			}
+			create, drop := s.createDropEnum(e)
+			s.append(&migrate.Change{
+				Source:  c,
+				Cmd:     drop,
+				Reverse: create,
+				Comment: fmt.Sprintf("drop enum type %q", e.T),
+			})
 		}
 	}
 	return nil
 }
 
-// topLevel executes first the changes for creating or dropping schemas (top-level schema elements).
-func (s *state) topLevel(changes []schema.Change) []schema.Change {
+// topLevel executes first the changes for creating or dropping schemas and
+// create objects that tables might depend on.
+func (s *state) topLevel(changes []schema.Change) ([]schema.Change, error) {
 	planned := make([]schema.Change, 0, len(changes))
 	for _, c := range changes {
 		switch c := c.(type) {
@@ -141,19 +158,43 @@ func (s *state) topLevel(changes []schema.Change) []schema.Change {
 				Source:  c,
 				Comment: fmt.Sprintf("Drop schema named %q", c.S.Name),
 			})
+		case *schema.AddObject:
+			e, ok := c.O.(*schema.EnumType)
+			if !ok {
+				return nil, fmt.Errorf("unsupported object %T", c.O)
+			}
+			create, drop := s.createDropEnum(e)
+			s.append(&migrate.Change{
+				Source:  c,
+				Cmd:     create,
+				Reverse: drop,
+				Comment: fmt.Sprintf("create enum type %q", e.T),
+			})
+		case *schema.ModifyObject:
+			if err := s.alterEnum(c); err != nil {
+				return nil, err
+			}
+		case *schema.RenameObject:
+			e1, ok1 := c.From.(*schema.EnumType)
+			e2, ok2 := c.To.(*schema.EnumType)
+			if !ok1 || !ok2 {
+				return nil, fmt.Errorf("unsupported rename types %T -> %T", c.From, c.To)
+			}
+			s.append(&migrate.Change{
+				Source:  c,
+				Cmd:     s.Build("ALTER TYPE").Ident(e1.T).P("RENAME TO").Ident(e2.T).String(),
+				Reverse: s.Build("ALTER TYPE").Ident(e2.T).P("RENAME TO").Ident(e1.T).String(),
+				Comment: fmt.Sprintf("rename an enum from %q to %q", e1.T, e2.T),
+			})
 		default:
 			planned = append(planned, c)
 		}
 	}
-	return planned
+	return planned, nil
 }
 
 // addTable builds and executes the query for creating a table in a schema.
-func (s *state) addTable(ctx context.Context, add *schema.AddTable) error {
-	// Create enum types before using them in the 'CREATE TABLE' statement.
-	if err := s.mayAddEnums(ctx, add.T, add.T.Columns...); err != nil {
-		return err
-	}
+func (s *state) addTable(_ context.Context, add *schema.AddTable) error {
 	var (
 		errs []string
 		b    = s.Build("CREATE TABLE")
@@ -164,7 +205,7 @@ func (s *state) addTable(ctx context.Context, add *schema.AddTable) error {
 	b.Table(add.T)
 	b.WrapIndent(func(b *sqlx.Builder) {
 		b.MapIndent(add.T.Columns, func(i int, b *sqlx.Builder) {
-			if err := s.column(b, add.T, add.T.Columns[i]); err != nil {
+			if err := s.column(b, add.T.Columns[i]); err != nil {
 				errs = append(errs, err.Error())
 			}
 		})
@@ -215,19 +256,9 @@ func (s *state) addTable(ctx context.Context, add *schema.AddTable) error {
 func (s *state) dropTable(ctx context.Context, drop *schema.DropTable) error {
 	cmd := &changeGroup{}
 	s.droppedT = append(s.droppedT, drop.T)
-	for _, e := range s.enumTypes(drop.T) {
-		if err := s.mayDropEnum(cmd, drop.T.Schema, e); err != nil {
-			return err
-		}
-	}
 	rs := &state{
 		conn:        s.conn,
 		PlanOptions: s.PlanOptions,
-		alteredE:    s.alteredE,
-		// Enums that were dropped above, were
-		// also created in the reverse commands.
-		createdE: s.droppedE,
-		droppedE: s.createdE,
 	}
 	if err := rs.addTable(ctx, &schema.AddTable{T: drop.T}); err != nil {
 		return fmt.Errorf("calculate reverse for drop table %q: %w", drop.T.Name, err)
@@ -262,7 +293,7 @@ func (s *state) dropTable(ctx context.Context, drop *schema.DropTable) error {
 }
 
 // modifyTable builds the statements that bring the table into its modified state.
-func (s *state) modifyTable(ctx context.Context, modify *schema.ModifyTable) error {
+func (s *state) modifyTable(_ context.Context, modify *schema.ModifyTable) error {
 	var (
 		alter   []schema.Change
 		addI    []*schema.AddIndex
@@ -332,9 +363,6 @@ func (s *state) modifyTable(ctx context.Context, modify *schema.ModifyTable) err
 				F: change.To,
 			})
 		case *schema.AddColumn:
-			if err := s.mayAddEnums(ctx, modify.T, change.C); err != nil {
-				return err
-			}
 			if c := (schema.Comment{}); sqlx.Has(change.C.Attrs, &c) {
 				changes = append(changes, s.columnComment(modify.T, change.C, c.Text, ""))
 			}
@@ -352,26 +380,6 @@ func (s *state) modifyTable(ctx context.Context, modify *schema.ModifyTable) err
 					continue
 				}
 			}
-			from, ok1 := hasEnumType(change.From)
-			to, ok2 := hasEnumType(change.To)
-			switch {
-			// Enum was changed (underlying values).
-			case ok1 && ok2 && s.enumIdent(modify.T.Schema, from) == s.enumIdent(modify.T.Schema, to):
-				if err := s.alterEnum(modify.T, from, to); err != nil {
-					return err
-				}
-				// If only the enum values were changed,
-				// there is no need to ALTER the table.
-				if k == schema.ChangeType {
-					continue
-				}
-			// Enum was added or changed.
-			case !ok1 && ok2 ||
-				ok1 && ok2 && s.enumIdent(modify.T.Schema, from) != s.enumIdent(modify.T.Schema, to):
-				if err := s.mayAddEnums(ctx, modify.T, change.To); err != nil {
-					return err
-				}
-			}
 			alter = append(alter, &schema.ModifyColumn{To: change.To, From: change.From, Change: k})
 		case *schema.RenameColumn:
 			// "RENAME COLUMN" cannot be combined with other alterations.
@@ -418,7 +426,7 @@ func (s *state) alterTable(t *schema.Table, changes []schema.Change) error {
 			switch change := changes[i].(type) {
 			case *schema.AddColumn:
 				b.P("ADD COLUMN")
-				if err := s.column(b, t, change.C); err != nil {
+				if err := s.column(b, change.C); err != nil {
 					return err
 				}
 				reverse = append(reverse, &schema.DropColumn{C: change.C})
@@ -434,22 +442,9 @@ func (s *state) alterTable(t *schema.Table, changes []schema.Change) error {
 					To:     change.From,
 					Change: change.Change & ^schema.ChangeGenerated,
 				})
-				toE, toHas := hasEnumType(change.To)
-				fromE, fromHas := hasEnumType(change.From)
-				// In case the enum was dropped or replaced with a different one.
-				if fromHas && !toHas || fromHas && toHas && s.enumIdent(t.Schema, fromE) != s.enumIdent(t.Schema, toE) {
-					if err := s.mayDropEnum(alter, t.Schema, fromE); err != nil {
-						return err
-					}
-				}
 			case *schema.DropColumn:
 				b.P("DROP COLUMN").Ident(change.C.Name)
 				reverse = append(reverse, &schema.AddColumn{C: change.C})
-				if e, ok := hasEnumType(change.C); ok {
-					if err := s.mayDropEnum(alter, t.Schema, e); err != nil {
-						return err
-					}
-				}
 			case *schema.AddIndex:
 				// Skip reversing this operation as it is the inverse of
 				// the operation above and should not be used besides this.
@@ -680,7 +675,7 @@ func (s *state) alterType(b *sqlx.Builder, alter *changeGroup, t *schema.Table,
 			err error
 		)
 		if e, ok := c.To.Type.Type.(*schema.EnumType); ok {
-			f = s.enumIdent(t.Schema, e)
+			f = s.enumIdent(e)
 		} else if f, err = FormatType(c.To.Type.Type); err != nil {
 			return err
 		}
@@ -766,44 +761,12 @@ func (s *state) dropIndexes(t *schema.Table, drops ...*schema.DropIndex) error {
 	return nil
 }
 
-func (s *state) mayAddEnums(ctx context.Context, t *schema.Table, columns ...*schema.Column) error {
-	for _, c := range columns {
-		e, ok := hasEnumType(c)
-		if !ok {
-			continue
-		}
-		if e.T == "" {
-			return fmt.Errorf("missing enum name for column %q", c.Name)
-		}
-		// Database connection is ignored on dump mode.
-		if !s.Mode.Is(migrate.PlanModeDump) {
-			if exists, err := s.enumExists(ctx, t.Schema, e); err != nil {
-				return err
-			} else if exists {
-				// Enum exists and was not created
-				// on this migration phase.
-				continue
-			}
-		}
-		name := s.enumIdent(t.Schema, e)
-		if prev, ok := s.createdE[name]; ok {
-			if !sqlx.ValuesEqual(prev.Values, e.Values) {
-				return fmt.Errorf("enum type %s has inconsistent desired state: %q != %q", name, prev.Values, e.Values)
-			}
-			continue
-		}
-		s.createdE[name] = e
-		create, drop := s.createDropEnum(t.Schema, e)
-		s.append(&migrate.Change{
-			Cmd:     create,
-			Reverse: drop,
-			Comment: fmt.Sprintf("create enum type %q", e.T),
-		})
+func (s *state) alterEnum(modify *schema.ModifyObject) error {
+	from, ok1 := modify.From.(*schema.EnumType)
+	to, ok2 := modify.To.(*schema.EnumType)
+	if !ok1 || !ok2 {
+		return fmt.Errorf("altering objects (%T) to (%T) is not supported", modify.From, modify.To)
 	}
-	return nil
-}
-
-func (s *state) alterEnum(t *schema.Table, from, to *schema.EnumType) error {
 	if len(from.Values) > len(to.Values) {
 		return fmt.Errorf("dropping enum (%q) value is not supported", from.T)
 	}
@@ -812,14 +775,7 @@ func (s *state) alterEnum(t *schema.Table, from, to *schema.EnumType) error {
 			return fmt.Errorf("replacing or reordering enum (%q) value is not supported: %q != %q", to.T, to.Values, from.Values)
 		}
 	}
-	name := s.enumIdent(t.Schema, from)
-	if prev, ok := s.alteredE[name]; ok {
-		if !sqlx.ValuesEqual(prev.Values, to.Values) {
-			return fmt.Errorf("enum type %s has inconsistent desired state: %q != %q", name, prev.Values, to.Values)
-		}
-		return nil
-	}
-	s.alteredE[name] = to
+	name := s.enumIdent(from)
 	for _, v := range to.Values[len(from.Values):] {
 		s.append(&migrate.Change{
 			Cmd:     s.Build("ALTER TYPE").P(name, "ADD VALUE", quote(v)).String(),
@@ -829,58 +785,6 @@ func (s *state) alterEnum(t *schema.Table, from, to *schema.EnumType) error {
 	return nil
 }
 
-func (s *state) enumExists(ctx context.Context, ns *schema.Schema, e *schema.EnumType) (bool, error) {
-	query, args := `SELECT * FROM pg_type t JOIN pg_namespace n on t.typnamespace = n.oid WHERE t.typname = $1 AND t.typtype = 'e'`, []any{e.T}
-	if es := s.enumSchema(ns, e); es != "" {
-		query += " AND n.nspname = $2"
-		args = append(args, es)
-	}
-	rows, err := s.QueryContext(ctx, query, args...)
-	if err != nil {
-		return false, fmt.Errorf("check enum existence: %w", err)
-	}
-	defer rows.Close()
-	return rows.Next(), rows.Err()
-}
-
-// mayDropEnum drops dangling enum types from the schema. An optional
-// "dropped" list can be provided to skip while searching for usage.
-func (s *state) mayDropEnum(cmd *changeGroup, ns *schema.Schema, e *schema.EnumType) error {
-	name := s.enumIdent(ns, e)
-	if _, ok := s.droppedE[name]; ok {
-		return nil
-	}
-	schemas := []*schema.Schema{ns}
-	// In case there is a realm attached, traverse the entire tree.
-	if ns.Realm != nil && len(ns.Realm.Schemas) > 0 {
-		schemas = ns.Realm.Schemas
-	}
-	for i := range schemas {
-		for _, t := range schemas[i].Tables {
-			// Skip dropped tables.
-			if containsT(s.droppedT, t) {
-				continue
-			}
-			for _, c := range t.Columns {
-				e1, ok := hasEnumType(c)
-				// Although we search in siblings schemas, use the
-				// table's one for building the enum identifier.
-				if ok && s.enumIdent(ns, e1) == name {
-					return nil
-				}
-			}
-		}
-	}
-	s.droppedE[name] = e
-	create, drop := s.createDropEnum(ns, e)
-	cmd.after = append(cmd.after, &migrate.Change{
-		Cmd:     drop,
-		Reverse: create,
-		Comment: fmt.Sprintf("drop enum type %q", e.T),
-	})
-	return nil
-}
-
 func (s *state) addIndexes(t *schema.Table, adds ...*schema.AddIndex) error {
 	for _, add := range adds {
 		b, idx := s.Build("CREATE"), add.I
@@ -921,8 +825,8 @@ func (s *state) addIndexes(t *schema.Table, adds ...*schema.AddIndex) error {
 	return nil
 }
 
-func (s *state) column(b *sqlx.Builder, t *schema.Table, c *schema.Column) error {
-	f, err := s.formatType(t, c)
+func (s *state) column(b *sqlx.Builder, c *schema.Column) error {
+	f, err := s.formatType(c)
 	if err != nil {
 		return err
 	}
@@ -1235,8 +1139,8 @@ func quote(s string) string {
 	return "'" + strings.ReplaceAll(s, "'", "''") + "'"
 }
 
-func (s *state) createDropEnum(sc *schema.Schema, e *schema.EnumType) (string, string) {
-	name := s.enumIdent(sc, e)
+func (s *state) createDropEnum(e *schema.EnumType) (string, string) {
+	name := s.enumIdent(e)
 	return s.Build("CREATE TYPE").
 			P(name, "AS ENUM").
 			Wrap(func(b *sqlx.Builder) {
@@ -1248,42 +1152,19 @@ func (s *state) createDropEnum(sc *schema.Schema, e *schema.EnumType) (string, s
 		s.Build("DROP TYPE").P(name).String()
 }
 
-func (s *state) enumIdent(ns *schema.Schema, e *schema.EnumType) string {
-	es := s.enumSchema(ns, e)
-	if es != "" {
-		return fmt.Sprintf("%q.%q", es, e.T)
-	}
-	return strconv.Quote(e.T)
-}
-
-func (s *state) enumSchema(ns *schema.Schema, e *schema.EnumType) (es string) {
+func (s *state) enumIdent(e *schema.EnumType) string {
 	switch {
 	// In case the plan uses a specific schema qualifier.
 	case s.SchemaQualifier != nil:
-		es = *s.SchemaQualifier
-	// Enum schema has higher precedence.
-	case e.Schema != nil:
-		es = e.Schema.Name
-	// Fallback to table schema if exists.
-	case ns != nil:
-		es = ns.Name
-	}
-	return
-}
-
-// enumType returns all the enum types used by the given table.
-func (s *state) enumTypes(t *schema.Table) []*schema.EnumType {
-	var (
-		es   []*schema.EnumType
-		seen = make(map[string]bool)
-	)
-	for _, c := range t.Columns {
-		if e, ok := hasEnumType(c); ok && !seen[s.enumIdent(t.Schema, e)] {
-			seen[s.enumIdent(t.Schema, e)] = true
-			es = append(es, e)
+		if *s.SchemaQualifier != "" {
+			return fmt.Sprintf("%q.%q", *s.SchemaQualifier, e.T)
 		}
+		return strconv.Quote(e.T)
+	case e.Schema != nil:
+		return fmt.Sprintf("%q.%q", e.Schema.Name, e.T)
+	default:
+		return strconv.Quote(e.T)
 	}
-	return es
 }
 
 // schemaPrefix returns the schema prefix based on the planner config.
@@ -1301,39 +1182,18 @@ func (s *state) schemaPrefix(ns *schema.Schema) string {
 }
 
 // formatType formats the type but takes into account the qualifier.
-func (s *state) formatType(t *schema.Table, c *schema.Column) (string, error) {
+func (s *state) formatType(c *schema.Column) (string, error) {
 	switch tt := c.Type.Type.(type) {
 	case *schema.EnumType:
-		return s.enumIdent(t.Schema, tt), nil
+		return s.enumIdent(tt), nil
 	case *ArrayType:
 		if e, ok := tt.Type.(*schema.EnumType); ok {
-			return s.enumIdent(t.Schema, e) + "[]", nil
+			return s.enumIdent(e) + "[]", nil
 		}
 	}
 	return FormatType(c.Type.Type)
 }
 
-func hasEnumType(c *schema.Column) (*schema.EnumType, bool) {
-	switch t := c.Type.Type.(type) {
-	case *schema.EnumType:
-		return t, true
-	case *ArrayType:
-		if e, ok := t.Type.(*schema.EnumType); ok {
-			return e, true
-		}
-	}
-	return nil, false
-}
-
-func containsT(ts []*schema.Table, t *schema.Table) bool {
-	for _, t1 := range ts {
-		if t1.Schema.Name == t.Schema.Name && t1.Name == t.Name {
-			return true
-		}
-	}
-	return false
-}
-
 func pkName(t *schema.Table, pk *schema.Index) string {
 	if pk.Name != "" {
 		return pk.Name
diff --git a/sql/schema/migrate.go b/sql/schema/migrate.go
index ec8fd340..964c584a 100644
--- a/sql/schema/migrate.go
+++ b/sql/schema/migrate.go
@@ -377,9 +377,11 @@ func (o *DiffOptions) Skipped(c Change) bool {
 }
 
 // AddOrSkip adds the given change to the list of changes if it is not skipped.
-func (o *DiffOptions) AddOrSkip(changes Changes, c Change) Changes {
-	if !o.Skipped(c) {
-		return append(changes, c)
+func (o *DiffOptions) AddOrSkip(changes Changes, cs ...Change) Changes {
+	for _, c := range cs {
+		if !o.Skipped(c) {
+			changes = append(changes, c)
+		}
 	}
 	return changes
 
@@ -540,6 +542,10 @@ func (*AddView) change()          {}
 func (*DropView) change()         {}
 func (*ModifyView) change()       {}
 func (*RenameView) change()       {}
+func (*AddObject) change()        {}
+func (*DropObject) change()       {}
+func (*ModifyObject) change()     {}
+func (*RenameObject) change()     {}
 func (*AddIndex) change()         {}
 func (*DropIndex) change()        {}
 func (*ModifyIndex) change()      {}
diff --git a/sql/sqlite/diff.go b/sql/sqlite/diff.go
index b2b18d26..12d41c7b 100644
--- a/sql/sqlite/diff.go
+++ b/sql/sqlite/diff.go
@@ -23,11 +23,17 @@ var DefaultDiff schema.Differ = &sqlx.Diff{DiffDriver: &diff{}}
 type diff struct{}
 
 // SchemaAttrDiff returns a changeset for migrating schema attributes from one state to the other.
-func (d *diff) SchemaAttrDiff(_, _ *schema.Schema) []schema.Change {
+func (*diff) SchemaAttrDiff(_, _ *schema.Schema) []schema.Change {
 	// No special schema attribute diffing for SQLite.
 	return nil
 }
 
+// SchemaObjectDiff returns a changeset for migrating schema objects from
+// one state to the other.
+func (*diff) SchemaObjectDiff(_, _ *schema.Schema) ([]schema.Change, error) {
+	return nil, nil
+}
+
 // TableAttrDiff returns a changeset for migrating table attributes from one state to the other.
 func (d *diff) TableAttrDiff(from, to *schema.Table) ([]schema.Change, error) {
 	var changes []schema.Change


Test patch:
diff --git a/internal/integration/cockroach_test.go b/internal/integration/cockroach_test.go
index 99f56b76..41dc0b0d 100644
--- a/internal/integration/cockroach_test.go
+++ b/internal/integration/cockroach_test.go
@@ -233,7 +233,10 @@ func TestCockroach_Enums(t *testing.T) {
 		})
 
 		// Create table with an enum column.
-		err := t.drv.ApplyChanges(ctx, []schema.Change{&schema.AddTable{T: usersT}})
+		err := t.drv.ApplyChanges(ctx, []schema.Change{
+			&schema.AddObject{O: &schema.EnumType{T: "state", Values: []string{"on", "off"}, Schema: usersT.Schema}},
+			&schema.AddTable{T: usersT},
+		})
 		require.NoError(t, err, "create a new table with an enum column")
 		t.dropTables(usersT.Name)
 		ensureNoChange(t, usersT)
@@ -245,27 +248,12 @@ func TestCockroach_Enums(t *testing.T) {
 		)
 		changes := t.diff(t.loadUsers(), usersT)
 		require.Len(t, changes, 1)
-		err = t.drv.ApplyChanges(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
+		err = t.drv.ApplyChanges(ctx, []schema.Change{
+			&schema.AddObject{O: &schema.EnumType{T: "day", Values: []string{"sunday", "monday"}, Schema: usersT.Schema}},
+			&schema.ModifyTable{T: usersT, Changes: changes},
+		})
 		require.NoError(t, err, "add a new enum column to existing table")
 		ensureNoChange(t, usersT)
-
-		// Add a new value to an existing enum.
-		e := usersT.Columns[2].Type.Type.(*schema.EnumType)
-		e.Values = append(e.Values, "tuesday")
-		changes = t.diff(t.loadUsers(), usersT)
-		require.Len(t, changes, 1)
-		err = t.drv.ApplyChanges(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
-		require.NoError(t, err, "append a value to existing enum")
-		ensureNoChange(t, usersT)
-
-		// Add multiple new values to an existing enum.
-		e = usersT.Columns[2].Type.Type.(*schema.EnumType)
-		e.Values = append(e.Values, "wednesday", "thursday", "friday", "saturday")
-		changes = t.diff(t.loadUsers(), usersT)
-		require.Len(t, changes, 1)
-		err = t.drv.ApplyChanges(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
-		require.NoError(t, err, "append multiple values to existing enum")
-		ensureNoChange(t, usersT)
 	})
 }
 
diff --git a/internal/integration/postgres_test.go b/internal/integration/postgres_test.go
index 263cfba3..4f2795b0 100644
--- a/internal/integration/postgres_test.go
+++ b/internal/integration/postgres_test.go
@@ -293,7 +293,10 @@ func TestPostgres_Enums(t *testing.T) {
 		})
 
 		// Create table with an enum column.
-		err := t.drv.ApplyChanges(ctx, []schema.Change{&schema.AddTable{T: usersT}})
+		err := t.drv.ApplyChanges(ctx, []schema.Change{
+			&schema.AddObject{O: &schema.EnumType{T: "state", Values: []string{"on", "off"}, Schema: usersT.Schema}},
+			&schema.AddTable{T: usersT},
+		})
 		require.NoError(t, err, "create a new table with an enum column")
 		t.dropTables(usersT.Name)
 		ensureNoChange(t, usersT)
@@ -305,27 +308,12 @@ func TestPostgres_Enums(t *testing.T) {
 		)
 		changes := t.diff(t.loadUsers(), usersT)
 		require.Len(t, changes, 1)
-		err = t.drv.ApplyChanges(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
+		err = t.drv.ApplyChanges(ctx, []schema.Change{
+			&schema.AddObject{O: &schema.EnumType{T: "day", Values: []string{"sunday", "monday"}, Schema: usersT.Schema}},
+			&schema.ModifyTable{T: usersT, Changes: changes},
+		})
 		require.NoError(t, err, "add a new enum column to existing table")
 		ensureNoChange(t, usersT)
-
-		// Add a new value to an existing enum.
-		e := usersT.Columns[1].Type.Type.(*schema.EnumType)
-		e.Values = append(e.Values, "tuesday")
-		changes = t.diff(t.loadUsers(), usersT)
-		require.Len(t, changes, 1)
-		err = t.drv.ApplyChanges(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
-		require.NoError(t, err, "append a value to existing enum")
-		ensureNoChange(t, usersT)
-
-		// Add multiple new values to an existing enum.
-		e = usersT.Columns[1].Type.Type.(*schema.EnumType)
-		e.Values = append(e.Values, "wednesday", "thursday", "friday", "saturday")
-		changes = t.diff(t.loadUsers(), usersT)
-		require.Len(t, changes, 1)
-		err = t.drv.ApplyChanges(ctx, []schema.Change{&schema.ModifyTable{T: usersT, Changes: changes}})
-		require.NoError(t, err, "append multiple values to existing enum")
-		ensureNoChange(t, usersT)
 	})
 }
 
diff --git a/internal/integration/testdata/postgres/column-enum.txt b/internal/integration/testdata/postgres/column-enum.txt
index c3ac3030..61b60706 100644
--- a/internal/integration/testdata/postgres/column-enum.txt
+++ b/internal/integration/testdata/postgres/column-enum.txt
@@ -16,6 +16,10 @@ cmpshow users 2.sql
 atlas schema inspect -u URL --format '{{ sql . "  " }}' > got.sql
 cmp got.sql want.sql
 
+# Rename the column.
+apply 4.hcl
+cmpshow users 4.sql
+
 -- 1.hcl --
 schema "$db" {}
 
@@ -84,4 +88,25 @@ CREATE TYPE "status" AS ENUM ('active', 'inactive');
 -- Create "users" table
 CREATE TABLE "users" (
   "type" "status" NOT NULL DEFAULT 'inactive'
-);
\ No newline at end of file
+);
+-- 4.hcl --
+schema "$db" {}
+
+enum "status" {
+  schema = schema.$db
+  values = ["active", "inactive"]
+}
+
+table "users" {
+  schema = schema.$db
+  column "renamed" {
+    type = enum.status
+    default = "inactive"
+  }
+}
+
+-- 4.sql --
+                                  Table "script_column_enum.users"
+ Column  |           Type            | Collation | Nullable |                Default
+---------+---------------------------+-----------+----------+---------------------------------------
+ renamed | script_column_enum.status |           | not null | 'inactive'::script_column_enum.status
\ No newline at end of file
diff --git a/sql/postgres/diff_test.go b/sql/postgres/diff_test.go
index 13200e8d..14ac707a 100644
--- a/sql/postgres/diff_test.go
+++ b/sql/postgres/diff_test.go
@@ -244,7 +244,6 @@ func TestDiff_TableDiff(t *testing.T) {
 				from = schema.NewTable("users").
 					SetSchema(schema.New("public")).
 					AddColumns(
-						schema.NewEnumColumn("state", schema.EnumName("state"), schema.EnumValues("on")),
 						schema.NewEnumColumn("enum1", schema.EnumName("enum1"), schema.EnumValues("a")),
 						schema.NewEnumColumn("enum3", schema.EnumName("enum3"), schema.EnumValues("a")),
 						schema.NewEnumColumn("enum4", schema.EnumName("enum4"), schema.EnumValues("a"), schema.EnumSchema(schema.New("public"))),
@@ -252,8 +251,6 @@ func TestDiff_TableDiff(t *testing.T) {
 				to = schema.NewTable("users").
 					SetSchema(schema.New("public")).
 					AddColumns(
-						// Add value.
-						schema.NewEnumColumn("state", schema.EnumName("state"), schema.EnumValues("on", "off")),
 						// Change type.
 						schema.NewEnumColumn("enum1", schema.EnumName("enum2"), schema.EnumValues("a")),
 						// No change as schema is optional.
@@ -268,8 +265,7 @@ func TestDiff_TableDiff(t *testing.T) {
 				to:   to,
 				wantChanges: []schema.Change{
 					&schema.ModifyColumn{From: from.Columns[0], To: to.Columns[0], Change: schema.ChangeType},
-					&schema.ModifyColumn{From: from.Columns[1], To: to.Columns[1], Change: schema.ChangeType},
-					&schema.ModifyColumn{From: from.Columns[3], To: to.Columns[3], Change: schema.ChangeType},
+					&schema.ModifyColumn{From: from.Columns[2], To: to.Columns[2], Change: schema.ChangeType},
 				},
 			}
 		}(),
@@ -279,18 +275,13 @@ func TestDiff_TableDiff(t *testing.T) {
 				from = schema.NewTable("users").
 					SetSchema(schema.New("public")).
 					AddColumns(
-						schema.NewColumn("a1").SetType(&ArrayType{T: "state[]", Type: &schema.EnumType{T: "state", Values: []string{"on"}}}),
-						schema.NewColumn("a2").SetType(&ArrayType{T: "state[]", Type: &schema.EnumType{T: "state", Values: []string{"on", "off"}}}),
+						schema.NewColumn("a1").SetType(&ArrayType{T: "state[]", Type: &schema.EnumType{T: "state", Values: []string{"on", "off"}}}),
 						schema.NewColumn("a3").SetType(&ArrayType{T: "state[]", Type: &schema.EnumType{T: "state", Values: []string{"on", "off"}}}),
 					)
 				to = schema.NewTable("users").
 					SetSchema(schema.New("public")).
 					AddColumns(
-						// Add value.
-						schema.NewColumn("a1").SetType(&ArrayType{T: "state[]", Type: &schema.EnumType{T: "state", Values: []string{"on", "off"}}}),
-						// Drop value.
-						schema.NewColumn("a2").SetType(&ArrayType{T: "state[]", Type: &schema.EnumType{T: "state", Values: []string{"on"}}}),
-						// Same values.
+						schema.NewColumn("a1").SetType(&ArrayType{T: "status[]", Type: &schema.EnumType{T: "status", Values: []string{"on", "off"}}}),
 						schema.NewColumn("a3").SetType(&ArrayType{T: "state[]", Type: &schema.EnumType{T: "state", Values: []string{"on", "off"}}}),
 					)
 			)
@@ -300,7 +291,6 @@ func TestDiff_TableDiff(t *testing.T) {
 				to:   to,
 				wantChanges: []schema.Change{
 					&schema.ModifyColumn{From: from.Columns[0], To: to.Columns[0], Change: schema.ChangeType},
-					&schema.ModifyColumn{From: from.Columns[1], To: to.Columns[1], Change: schema.ChangeType},
 				},
 			}
 		}(),
@@ -497,28 +487,17 @@ func TestDiff_SchemaDiff(t *testing.T) {
 	mock{m}.version("130000")
 	drv, err := Open(db)
 	require.NoError(t, err)
-	from := &schema.Schema{
-		Tables: []*schema.Table{
-			{Name: "users"},
-			{Name: "pets"},
-		},
-	}
-	to := &schema.Schema{
-		Tables: []*schema.Table{
-			{
-				Name: "users",
-				Columns: []*schema.Column{
-					{Name: "t2_id", Type: &schema.ColumnType{Raw: "int", Type: &schema.IntegerType{T: "int"}}},
-				},
-			},
-			{Name: "groups"},
-		},
-	}
-	from.Tables[0].Schema = from
-	from.Tables[1].Schema = from
+	from := schema.New("public").
+		AddTables(schema.NewTable("users"), schema.NewTable("pets")).
+		AddObjects(&schema.EnumType{T: "dropped"}, &schema.EnumType{T: "modified", Values: []string{"a"}}, &schema.EnumType{T: "unchanged"})
+	to := schema.New("public").AddTables(schema.NewTable("users").AddColumns(schema.NewIntColumn("t2_id", "int")), schema.NewTable("groups")).
+		AddObjects(&schema.EnumType{T: "modified", Values: []string{"b"}}, &schema.EnumType{T: "unchanged"}, &schema.EnumType{T: "added"})
 	changes, err := drv.SchemaDiff(from, to)
 	require.NoError(t, err)
 	require.EqualValues(t, []schema.Change{
+		&schema.DropObject{O: from.Objects[0]},
+		&schema.ModifyObject{From: from.Objects[1], To: to.Objects[0]},
+		&schema.AddObject{O: to.Objects[2]},
 		&schema.ModifyTable{T: to.Tables[0], Changes: []schema.Change{&schema.AddColumn{C: to.Tables[0].Columns[0]}}},
 		&schema.DropTable{T: from.Tables[1]},
 		&schema.AddTable{T: to.Tables[1]},
diff --git a/sql/postgres/migrate_test.go b/sql/postgres/migrate_test.go
index 06fc80df..04aa76fc 100644
--- a/sql/postgres/migrate_test.go
+++ b/sql/postgres/migrate_test.go
@@ -187,6 +187,13 @@ func TestPlanChanges(t *testing.T) {
 		},
 		{
 			changes: []schema.Change{
+				&schema.AddObject{
+					O: &schema.EnumType{
+						T:      "direction",
+						Values: []string{"NORTH", "SOUTH"},
+						Schema: schema.New("public"),
+					},
+				},
 				&schema.AddTable{
 					T: &schema.Table{
 						Name: "posts",
@@ -223,36 +230,6 @@ func TestPlanChanges(t *testing.T) {
 				},
 			},
 		},
-		// Planner should create all enums without querying the database on dump mode.
-		{
-			options: []migrate.PlanOption{
-				func(opts *migrate.PlanOptions) {
-					opts.Mode = migrate.PlanModeDump
-					opts.SchemaQualifier = new(string)
-				},
-			},
-			changes: []schema.Change{
-				&schema.AddTable{
-					T: &schema.Table{
-						Schema: schema.New("public"),
-						Name:   "posts",
-						Columns: []*schema.Column{
-							{Name: "a", Type: &schema.ColumnType{Type: &schema.EnumType{T: "direction", Values: []string{"NORTH", "SOUTH"}, Schema: schema.New("public")}}},
-							{Name: "b", Type: &schema.ColumnType{Type: &schema.EnumType{T: "state", Values: []string{"ON", "OFF"}}}},
-						},
-					},
-				},
-			},
-			wantPlan: &migrate.Plan{
-				Reversible:    true,
-				Transactional: true,
-				Changes: []*migrate.Change{
-					{Cmd: `CREATE TYPE "direction" AS ENUM ('NORTH', 'SOUTH')`, Reverse: `DROP TYPE "direction"`},
-					{Cmd: `CREATE TYPE "state" AS ENUM ('ON', 'OFF')`, Reverse: `DROP TYPE "state"`},
-					{Cmd: `CREATE TABLE "posts" ("a" "direction" NOT NULL, "b" "state" NOT NULL)`, Reverse: `DROP TABLE "posts"`},
-				},
-			},
-		},
 		{
 			changes: []schema.Change{
 				&schema.AddTable{
@@ -763,11 +740,25 @@ func TestPlanChanges(t *testing.T) {
 		},
 		{
 			changes: []schema.Change{
+				&schema.AddObject{
+					O: &schema.EnumType{
+						T:      "state",
+						Values: []string{"on", "off"},
+						Schema: schema.New("public"),
+					},
+				},
+				&schema.AddObject{
+					O: &schema.EnumType{
+						T:      "status",
+						Values: []string{"a", "b"},
+						Schema: schema.New("test"),
+					},
+				},
 				func() schema.Change {
 					users := schema.NewTable("users").
 						SetSchema(schema.New("public")).
 						AddColumns(
-							schema.NewEnumColumn("state", schema.EnumName("state"), schema.EnumValues("on", "off")),
+							schema.NewEnumColumn("state", schema.EnumName("state"), schema.EnumValues("on", "off"), schema.EnumSchema(schema.New("public"))),
 							schema.NewEnumColumn("status", schema.EnumName("status"), schema.EnumValues("a", "b"), schema.EnumSchema(schema.New("test"))),
 						)
 					return &schema.ModifyTable{
@@ -784,23 +775,15 @@ func TestPlanChanges(t *testing.T) {
 								Change: schema.ChangeType,
 							},
 							&schema.DropColumn{
-								C: schema.NewEnumColumn("dc1", schema.EnumName("de"), schema.EnumValues("on")),
+								C: schema.NewEnumColumn("dc1", schema.EnumName("de"), schema.EnumValues("on"), schema.EnumSchema(schema.New("public"))),
 							},
 							&schema.DropColumn{
-								C: schema.NewEnumColumn("dc2", schema.EnumName("de"), schema.EnumValues("on")),
+								C: schema.NewEnumColumn("dc2", schema.EnumName("de"), schema.EnumValues("on"), schema.EnumSchema(schema.New("public"))),
 							},
 						},
 					}
 				}(),
 			},
-			mock: func(m mock) {
-				m.ExpectQuery(sqltest.Escape("SELECT * FROM pg_type t JOIN pg_namespace n on t.typnamespace = n.oid WHERE t.typname = $1 AND t.typtype = 'e' AND n.nspname = $2 ")).
-					WithArgs("state", "public").
-					WillReturnRows(sqlmock.NewRows([]string{"name"}))
-				m.ExpectQuery(sqltest.Escape("SELECT * FROM pg_type t JOIN pg_namespace n on t.typnamespace = n.oid WHERE t.typname = $1 AND t.typtype = 'e' AND n.nspname = $2 ")).
-					WithArgs("status", "test").
-					WillReturnRows(sqlmock.NewRows([]string{"name"}))
-			},
 			wantPlan: &migrate.Plan{
 				Reversible:    true,
 				Transactional: true,
@@ -814,22 +797,28 @@ func TestPlanChanges(t *testing.T) {
 		},
 		{
 			changes: []schema.Change{
+				&schema.DropObject{
+					O: &schema.EnumType{
+						T:      "state",
+						Values: []string{"on", "off"},
+						Schema: schema.New("public"),
+					},
+				},
 				&schema.DropTable{
 					T: schema.NewTable("users").
 						SetSchema(schema.New("public")).
 						AddColumns(
-							schema.NewEnumColumn("state", schema.EnumName("state"), schema.EnumValues("on", "off")),
-							schema.NewEnumColumn("status", schema.EnumName("status"), schema.EnumValues("on", "off")),
+							schema.NewEnumColumn("state", schema.EnumName("state"), schema.EnumValues("on", "off"), schema.EnumSchema(schema.New("public"))),
+							schema.NewEnumColumn("status", schema.EnumName("status"), schema.EnumValues("on", "off"), schema.EnumSchema(schema.New("public"))),
 						),
 				},
-			},
-			mock: func(m mock) {
-				m.ExpectQuery(sqltest.Escape("SELECT * FROM pg_type t JOIN pg_namespace n on t.typnamespace = n.oid WHERE t.typname = $1 AND t.typtype = 'e' AND n.nspname = $2 ")).
-					WithArgs("state", "public").
-					WillReturnRows(sqlmock.NewRows([]string{"name"}))
-				m.ExpectQuery(sqltest.Escape("SELECT * FROM pg_type t JOIN pg_namespace n on t.typnamespace = n.oid WHERE t.typname = $1 AND t.typtype = 'e' AND n.nspname = $2 ")).
-					WithArgs("status", "public").
-					WillReturnRows(sqlmock.NewRows([]string{"name"}))
+				&schema.DropObject{
+					O: &schema.EnumType{
+						T:      "status",
+						Values: []string{"on", "off"},
+						Schema: schema.New("public"),
+					},
+				},
 			},
 			wantPlan: &migrate.Plan{
 				Reversible:    true,
@@ -847,19 +836,11 @@ func TestPlanChanges(t *testing.T) {
 					T: schema.NewTable("users").
 						SetSchema(schema.New("public")).
 						AddColumns(
-							schema.NewEnumColumn("state", schema.EnumName("state"), schema.EnumValues("on", "off")),
-							schema.NewEnumColumn("status", schema.EnumName("status"), schema.EnumValues("on", "off")),
+							schema.NewEnumColumn("state", schema.EnumName("state"), schema.EnumValues("on", "off"), schema.EnumSchema(schema.New("public"))),
+							schema.NewEnumColumn("status", schema.EnumName("status"), schema.EnumValues("on", "off"), schema.EnumSchema(schema.New("public"))),
 						),
 				},
 			},
-			mock: func(m mock) {
-				m.ExpectQuery(sqltest.Escape("SELECT * FROM pg_type t JOIN pg_namespace n on t.typnamespace = n.oid WHERE t.typname = $1 AND t.typtype = 'e' AND n.nspname = $2 ")).
-					WithArgs("state", "public").
-					WillReturnRows(sqlmock.NewRows([]string{"name"}))
-				m.ExpectQuery(sqltest.Escape("SELECT * FROM pg_type t JOIN pg_namespace n on t.typnamespace = n.oid WHERE t.typname = $1 AND t.typtype = 'e' AND n.nspname = $2 ")).
-					WithArgs("status", "public").
-					WillReturnRows(sqlmock.NewRows([]string{"name"}))
-			},
 			wantPlan: &migrate.Plan{
 				Reversible:    true,
 				Transactional: true,
@@ -876,14 +857,21 @@ func TestPlanChanges(t *testing.T) {
 					AddTables(
 						schema.NewTable("t1").
 							AddColumns(
-								schema.NewEnumColumn("state", schema.EnumName("state"), schema.EnumValues("on", "off")),
+								schema.NewEnumColumn("state", schema.EnumName("state"), schema.EnumValues("on", "off"), schema.EnumSchema(schema.New("public"))),
 							),
 						schema.NewTable("t2").
 							AddColumns(
-								schema.NewEnumColumn("state", schema.EnumName("state"), schema.EnumValues("on", "off")),
+								schema.NewEnumColumn("state", schema.EnumName("state"), schema.EnumValues("on", "off"), schema.EnumSchema(schema.New("public"))),
 							),
 					)
 				return []schema.Change{
+					&schema.DropObject{
+						O: &schema.EnumType{
+							T:      "state",
+							Values: []string{"on", "off"},
+							Schema: schema.New("public"),
+						},
+					},
 					&schema.DropTable{
 						T: s.Tables[0],
 					},
@@ -892,14 +880,6 @@ func TestPlanChanges(t *testing.T) {
 					},
 				}
 			}(),
-			mock: func(m mock) {
-				m.ExpectQuery(sqltest.Escape("SELECT * FROM pg_type t JOIN pg_namespace n on t.typnamespace = n.oid WHERE t.typname = $1 AND t.typtype = 'e' AND n.nspname = $2 ")).
-					WithArgs("state", "public").
-					WillReturnRows(sqlmock.NewRows([]string{"name"}).AddRow("state"))
-				m.ExpectQuery(sqltest.Escape("SELECT * FROM pg_type t JOIN pg_namespace n on t.typnamespace = n.oid WHERE t.typname = $1 AND t.typtype = 'e' AND n.nspname = $2 ")).
-					WithArgs("state", "public").
-					WillReturnRows(sqlmock.NewRows([]string{"name"}).AddRow("state"))
-			},
 			wantPlan: &migrate.Plan{
 				Reversible:    true,
 				Transactional: true,
@@ -921,23 +901,18 @@ func TestPlanChanges(t *testing.T) {
 		},
 		{
 			changes: []schema.Change{
-				func() schema.Change {
-					users := schema.NewTable("users").
-						SetSchema(schema.New("public")).
-						AddColumns(
-							schema.NewEnumColumn("state", schema.EnumName("state"), schema.EnumValues("on", "off")),
-						)
-					return &schema.ModifyTable{
-						T: users,
-						Changes: []schema.Change{
-							&schema.ModifyColumn{
-								From:   users.Columns[0],
-								To:     &schema.Column{Name: "state", Type: &schema.ColumnType{Type: &schema.EnumType{T: "state", Values: []string{"on", "off", "unknown"}}}},
-								Change: schema.ChangeType,
-							},
-						},
-					}
-				}(),
+				&schema.ModifyObject{
+					From: &schema.EnumType{
+						T:      "state",
+						Values: []string{"on", "off"},
+						Schema: schema.New("public"),
+					},
+					To: &schema.EnumType{
+						T:      "state",
+						Values: []string{"on", "off", "unknown"},
+						Schema: schema.New("public"),
+					},
+				},
 			},
 			wantPlan: &migrate.Plan{
 				Reversible:    false,
@@ -950,6 +925,18 @@ func TestPlanChanges(t *testing.T) {
 		// Modify column type and drop comment.
 		{
 			changes: []schema.Change{
+				&schema.ModifyObject{
+					From: &schema.EnumType{
+						T:      "state",
+						Values: []string{"on", "off"},
+						Schema: schema.New("public"),
+					},
+					To: &schema.EnumType{
+						T:      "state",
+						Values: []string{"on", "off", "unknown"},
+						Schema: schema.New("public"),
+					},
+				},
 				func() schema.Change {
 					users := schema.NewTable("users").
 						SetSchema(schema.New("public")).
@@ -962,7 +949,7 @@ func TestPlanChanges(t *testing.T) {
 							&schema.ModifyColumn{
 								From:   &schema.Column{Name: "state", Type: &schema.ColumnType{Type: &schema.EnumType{T: "state", Values: []string{"on", "off"}}}, Attrs: []schema.Attr{&schema.Comment{Text: "foo"}}},
 								To:     &schema.Column{Name: "state", Type: &schema.ColumnType{Type: &schema.EnumType{T: "state", Values: []string{"on", "off", "unknown"}}}},
-								Change: schema.ChangeType | schema.ChangeComment,
+								Change: schema.ChangeComment,
 							},
 						},
 					}
@@ -977,37 +964,6 @@ func TestPlanChanges(t *testing.T) {
 				},
 			},
 		},
-		// Modify column type and add comment.
-		{
-			changes: []schema.Change{
-				func() schema.Change {
-					users := &schema.Table{
-						Name: "users",
-						Columns: []*schema.Column{
-							{Name: "state", Type: &schema.ColumnType{Type: &schema.EnumType{T: "state", Values: []string{"on", "off"}}}},
-						},
-					}
-					return &schema.ModifyTable{
-						T: users,
-						Changes: []schema.Change{
-							&schema.ModifyColumn{
-								From:   &schema.Column{Name: "state", Type: &schema.ColumnType{Type: &schema.EnumType{T: "state", Values: []string{"on", "off"}}}},
-								To:     &schema.Column{Name: "state", Type: &schema.ColumnType{Type: &schema.EnumType{T: "state", Values: []string{"on", "off", "unknown"}}}, Attrs: []schema.Attr{&schema.Comment{Text: "foo"}}},
-								Change: schema.ChangeType | schema.ChangeComment,
-							},
-						},
-					}
-				}(),
-			},
-			wantPlan: &migrate.Plan{
-				Reversible:    false,
-				Transactional: true,
-				Changes: []*migrate.Change{
-					{Cmd: `ALTER TYPE "state" ADD VALUE 'unknown'`},
-					{Cmd: `COMMENT ON COLUMN "users" ."state" IS 'foo'`, Reverse: `COMMENT ON COLUMN "users" ."state" IS ''`},
-				},
-			},
-		},
 		// Modify column comment.
 		{
 			changes: []schema.Change{
@@ -1349,6 +1305,13 @@ func TestPlanChanges(t *testing.T) {
 		// Empty qualifier.
 		{
 			changes: []schema.Change{
+				&schema.AddObject{
+					O: &schema.EnumType{
+						T:      "enum",
+						Values: []string{"a"},
+						Schema: schema.New("ignored"),
+					},
+				},
 				&schema.AddTable{
 					T: schema.NewTable("posts").
 						SetSchema(schema.New("test1")).
@@ -1360,11 +1323,6 @@ func TestPlanChanges(t *testing.T) {
 			options: []migrate.PlanOption{
 				func(o *migrate.PlanOptions) { o.SchemaQualifier = new(string) },
 			},
-			mock: func(m mock) {
-				m.ExpectQuery(sqltest.Escape("SELECT * FROM pg_type t JOIN pg_namespace n on t.typnamespace = n.oid WHERE t.typname = $1 AND t.typtype = 'e'")).
-					WithArgs("enum").
-					WillReturnRows(sqlmock.NewRows([]string{"name"}))
-			},
 			wantPlan: &migrate.Plan{
 				Reversible:    true,
 				Transactional: true,