Repository: ariga/atlas
Pull request #2325 (2023-11-28 11:44:35)

doc/md/guides: added a guide for Hibernate schema loader


Patch:
diff --git a/.github/workflows/ci-dialect_oss.yaml b/.github/workflows/ci-dialect_oss.yaml
index 4f1ba4b9..588859d7 100644
--- a/.github/workflows/ci-dialect_oss.yaml
+++ b/.github/workflows/ci-dialect_oss.yaml
@@ -3,18 +3,9 @@
 # # # # # # # # # # # # # # # #
 name: CI - Dialect Tests - Community Edition
 on:
-  pull_request:
-    paths-ignore:
-      - 'doc/**'
-      - 'ops/**'
-  push:
-    branches:
-      - master
-    paths-ignore:
-      - 'doc/**'
-      - 'ops/**'
+  workflow_call:
 concurrency:
-  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}-dialect
   cancel-in-progress: true
 jobs:
   integration-mysql56:
diff --git a/.github/workflows/ci-go_oss.yaml b/.github/workflows/ci-go_oss.yaml
index 9713ed57..b58efe75 100644
--- a/.github/workflows/ci-go_oss.yaml
+++ b/.github/workflows/ci-go_oss.yaml
@@ -88,4 +88,9 @@ jobs:
           go-version-file: cmd/atlas/go.mod
       - name: Run integration tests for HCL
         working-directory: internal/integration/hclsqlspec
-        run: go test -race -count=2 -v ./...
\ No newline at end of file
+        run: go test -race -count=2 -v ./...
+
+  dialect-integration:
+    needs: [lint, generate-cmp, unit, cli, integration]
+    uses: ./.github/workflows/ci-dialect_oss.yaml
+    secrets: inherit
diff --git a/cmd/atlas/internal/cmdapi/migrate.go b/cmd/atlas/internal/cmdapi/migrate.go
index 80936ea4..c2d99c18 100644
--- a/cmd/atlas/internal/cmdapi/migrate.go
+++ b/cmd/atlas/internal/cmdapi/migrate.go
@@ -880,7 +880,7 @@ func migrateImportRun(cmd *cobra.Command, _ []string, flags migrateImportFlags)
 	// Extract the statements for each of the migration files,
 	// add them to a plan to format with the DefaultFormatter.
 	for _, f := range ff {
-		stmts, err := f.StmtDecls()
+		stmts, err := f.StmtDecls() // Not driver aware.
 		if err != nil {
 			return err
 		}
diff --git a/cmd/atlas/internal/cmdlog/cmdlog.go b/cmd/atlas/internal/cmdlog/cmdlog.go
index 87c70778..033d4b9d 100644
--- a/cmd/atlas/internal/cmdlog/cmdlog.go
+++ b/cmd/atlas/internal/cmdlog/cmdlog.go
@@ -704,15 +704,15 @@ func sqlInspect(report *SchemaInspect, indent ...string) (string, error) {
 		if report.Client.URL.Schema == "" {
 			changes = append(changes, &schema.AddSchema{S: s})
 		}
+		for _, o := range s.Objects {
+			changes = append(changes, &schema.AddObject{O: o})
+		}
 		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})
-		}
 		for _, f := range s.Funcs {
 			changes = append(changes, &schema.AddFunc{F: f})
 		}
diff --git a/cmd/atlas/internal/docker/docker.go b/cmd/atlas/internal/docker/docker.go
index a017b5d6..5cde8f5e 100644
--- a/cmd/atlas/internal/docker/docker.go
+++ b/cmd/atlas/internal/docker/docker.go
@@ -25,7 +25,10 @@ import (
 	"github.com/go-sql-driver/mysql"
 )
 
-const pass = "pass"
+const (
+	pass          = "pass"
+	passSQLServer = "P@ssw0rd0995"
+)
 
 type (
 	// Config is used to configure container creation.
@@ -70,74 +73,94 @@ func NewConfig(opts ...ConfigOption) (*Config, error) {
 }
 
 const (
-	DriverMySQL    = "mysql"
-	DriverMariaDB  = "mariadb"
-	DriverPostgres = "postgres"
+	DriverMySQL     = "mysql"
+	DriverMariaDB   = "mariadb"
+	DriverPostgres  = "postgres"
+	DriverSQLServer = "sqlserver"
 )
 
 // FromURL parses a URL in the format of
-// "docker://image/tag" and returns a Config.
+// "docker://image/tag[/dbname]" and returns a Config.
 func FromURL(u *url.URL) (*Config, error) {
 	var (
-		tag   string
-		opts  []ConfigOption
-		parts = strings.Split(strings.TrimPrefix(u.Path, "/"), "/")
+		parts  = strings.Split(strings.TrimPrefix(u.Path, "/"), "/")
+		idxTag = len(parts) - 1
+		dbName string
 	)
-	if len(parts) > 0 {
-		tag = parts[0]
-	}
-	switch n := len(parts); {
-	case n == 2 && !strings.Contains(parts[1], ":"):
-		opts = append(opts, Database(parts[1]))
-	case n == 3:
-		opts = append(opts, Database(parts[2]))
-		fallthrough
-	case n == 2:
-		parts[0] = fmt.Sprintf("%s/%s", parts[0], parts[1])
+	// Check if the last part is a tag or a database name.
+	if idxTag > 0 && !strings.ContainsRune(parts[idxTag], ':') {
+		// The last part is not a tag, so it must be a database name.
+		dbName, idxTag = parts[idxTag], idxTag-1
 	}
-	switch u.Scheme {
-	case "docker+postgres", "docker+mysql", "docker+maria":
-		img := Image(parts[0])
+	var (
+		opts []ConfigOption
+		tag  string
+	)
+	// Support docker+driver://<image>[:<tag>]
+	driver, customImage := strings.CutPrefix(u.Scheme, "docker+")
+	if customImage {
+		// The image is fully specified in the URL.
+		img := path.Join(parts[:idxTag+1]...)
 		if u.Host != "" && u.Host != "_" {
-			img = Image(u.Host, parts[0])
+			img = path.Join(u.Host, img)
+		}
+		opts = append(opts, Image(img))
+	} else {
+		driver = u.Host
+		if idxTag >= 0 {
+			tag = parts[idxTag]
 		}
-		opts = append(opts, img)
-		u.Host = u.Scheme[len("docker+"):]
 	}
 	var (
 		err error
 		cfg *Config
 	)
-	switch u.Host {
+	switch driver {
 	case DriverMySQL:
-		if len(parts) > 1 {
-			opts = append(opts, Env("MYSQL_DATABASE="+parts[1]), Setup(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s`", parts[1])))
+		if dbName != "" {
+			opts = append(opts,
+				Database(dbName),
+				Env("MYSQL_DATABASE="+dbName),
+				Setup(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s`", dbName)),
+			)
 		}
 		cfg, err = MySQL(tag, opts...)
 	case "maria":
-		u.Host = DriverMariaDB
+		driver = DriverMariaDB
 		fallthrough
 	case DriverMariaDB:
-		if len(parts) > 1 {
-			opts = append(opts, Env("MYSQL_DATABASE="+parts[1]), Setup(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s`", parts[1])))
+		if dbName != "" {
+			opts = append(opts,
+				Database(dbName),
+				Env("MYSQL_DATABASE="+dbName),
+				Setup(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s`", dbName)),
+			)
 		}
 		cfg, err = MariaDB(tag, opts...)
 	case "postgis":
 		opts = append(opts, Image("postgis/postgis:"+tag))
-		u.Host = DriverPostgres
+		driver = DriverPostgres
 		fallthrough
 	case DriverPostgres:
-		if len(parts) > 1 {
-			opts = append(opts, Env("POSTGRES_DB="+parts[1]))
+		if dbName != "" {
+			opts = append(opts, Database(dbName), Env("POSTGRES_DB="+dbName))
 		}
 		cfg, err = PostgreSQL(tag, opts...)
+	case DriverSQLServer:
+		if dbName != "" && dbName != "master" {
+			opts = append(opts,
+				Database(dbName),
+				Setup(fmt.Sprintf("CREATE DATABASE [%s]", dbName)),
+			)
+		}
+		cfg, err = SQLServer(tag, opts...)
 	default:
-		return nil, fmt.Errorf("unsupported docker image %q", u.Host)
+		return nil, fmt.Errorf("unsupported docker image %q", driver)
 	}
 	if err != nil {
 		return nil, err
 	}
-	cfg.driver = u.Host
+	cfg.driver = driver
 	return cfg, nil
 }
 
@@ -197,6 +220,25 @@ func PostgreSQL(version string, opts ...ConfigOption) (*Config, error) {
 	)
 }
 
+// SQLServer returns a new Config for a SQLServer image.
+func SQLServer(version string, opts ...ConfigOption) (*Config, error) {
+	return NewConfig(
+		append(
+			[]ConfigOption{
+				Image("mcr.microsoft.com/mssql/server:" + version),
+				Port("1433"),
+				Database("master"),
+				Env(
+					"ACCEPT_EULA=Y",
+					"MSSQL_PID=Developer",
+					"MSSQL_SA_PASSWORD="+passSQLServer,
+				),
+			},
+			opts...,
+		)...,
+	)
+}
+
 // Image sets the docker image to use. For example:
 //
 //	Image("mysql")
@@ -319,12 +361,12 @@ func (c *Container) Wait(ctx context.Context, timeout time.Duration) error {
 	if err != nil {
 		return err
 	}
+	pingURL := c.PingURL(*u)
 	for {
 		select {
 		case <-time.After(100 * time.Millisecond):
 			// Ping against the root connection.
-			u.Path = "/"
-			client, err := sqlclient.Open(ctx, u.String())
+			client, err := sqlclient.Open(ctx, pingURL)
 			if err != nil {
 				continue
 			}
@@ -352,6 +394,8 @@ func (c *Container) Wait(ctx context.Context, timeout time.Duration) error {
 // URL returns a URL to connect to the Container.
 func (c *Container) URL() (*url.URL, error) {
 	switch c.cfg.driver {
+	case DriverSQLServer:
+		return url.Parse(fmt.Sprintf("sqlserver://sa:%s@localhost:%s?database=%s", passSQLServer, c.Port, c.cfg.Database))
 	case DriverPostgres:
 		return url.Parse(fmt.Sprintf("postgres://postgres:%s@localhost:%s/%s?sslmode=disable", c.Passphrase, c.Port, c.cfg.Database))
 	case DriverMySQL, DriverMariaDB:
@@ -361,6 +405,20 @@ func (c *Container) URL() (*url.URL, error) {
 	}
 }
 
+// PingURL returns a URL to ping the Container.
+func (c *Container) PingURL(u url.URL) string {
+	switch c.cfg.driver {
+	case DriverSQLServer:
+		q := u.Query()
+		q.Del("database")
+		u.RawQuery = q.Encode()
+		return u.String()
+	default:
+		u.Path = "/"
+		return u.String()
+	}
+}
+
 // validate that no empty values are given.
 func (c *Config) validate() error {
 	if c == nil || c.Image == "" || c.Port == "" || c.Out == nil {
@@ -388,7 +446,7 @@ func init() {
 	sqlclient.Register(
 		"docker",
 		sqlclient.OpenerFunc(Open),
-		sqlclient.RegisterFlavours("docker+postgres", "docker+mysql", "docker+maria"),
+		sqlclient.RegisterFlavours("docker+postgres", "docker+mysql", "docker+maria", "docker+sqlserver"),
 	)
 }
 
diff --git a/cmd/atlas/internal/migratelint/lint.go b/cmd/atlas/internal/migratelint/lint.go
index 48774c6e..6a134e17 100644
--- a/cmd/atlas/internal/migratelint/lint.go
+++ b/cmd/atlas/internal/migratelint/lint.go
@@ -270,7 +270,7 @@ func (d *DevLoader) base(ctx context.Context, base []migrate.File) (*schema.Real
 		base = base[i:]
 	}
 	for _, f := range base {
-		stmt, err := f.StmtDecls()
+		stmt, err := migrate.FileStmtDecls(d.Dev, f)
 		if err != nil {
 			return nil, &FileError{File: f.Name(), Err: fmt.Errorf("scanning statements: %w", err)}
 		}
@@ -286,7 +286,7 @@ func (d *DevLoader) base(ctx context.Context, base []migrate.File) (*schema.Real
 // next returns the next state of the database after executing the statements in
 // the file. The changes detected by the statements are attached to the file.
 func (d *DevLoader) next(ctx context.Context, f *sqlcheck.File, start *schema.Realm) (current *schema.Realm, err error) {
-	stmts, err := f.StmtDecls()
+	stmts, err := migrate.FileStmtDecls(d.Dev, f)
 	if err != nil {
 		return nil, &FileError{File: f.Name(), Err: fmt.Errorf("scanning statements: %w", err)}
 	}
diff --git a/cmd/atlas/internal/sqlparse/myparse/myparse.go b/cmd/atlas/internal/sqlparse/myparse/myparse.go
index af7ed9ae..fa7de71c 100644
--- a/cmd/atlas/internal/sqlparse/myparse/myparse.go
+++ b/cmd/atlas/internal/sqlparse/myparse/myparse.go
@@ -24,9 +24,9 @@ import (
 type Parser struct{}
 
 // ColumnFilledBefore checks if the column was filled before the given position.
-func (p *Parser) ColumnFilledBefore(f migrate.File, t *schema.Table, c *schema.Column, pos int) (bool, error) {
+func (p *Parser) ColumnFilledBefore(stmts []*migrate.Stmt, t *schema.Table, c *schema.Column, pos int) (bool, error) {
 	pr := parser.New()
-	return parseutil.MatchStmtBefore(f, pos, func(s *migrate.Stmt) (bool, error) {
+	return parseutil.MatchStmtBefore(stmts, pos, func(s *migrate.Stmt) (bool, error) {
 		stmt, err := pr.ParseOneStmt(s.Text, "", "")
 		if err != nil {
 			return false, err
@@ -58,11 +58,7 @@ func (p *Parser) ColumnFilledBefore(f migrate.File, t *schema.Table, c *schema.C
 }
 
 // ColumnFilledAfter checks if the column that matches the given value was filled after the position.
-func (p *Parser) ColumnFilledAfter(f migrate.File, t *schema.Table, c *schema.Column, pos int, match any) (bool, error) {
-	stmts, err := f.StmtDecls()
-	if err != nil {
-		return false, err
-	}
+func (p *Parser) ColumnFilledAfter(stmts []*migrate.Stmt, t *schema.Table, c *schema.Column, pos int, match any) (bool, error) {
 	switch i := slices.IndexFunc(stmts, func(s *migrate.Stmt) bool {
 		return s.Pos >= pos
 	}); i {
@@ -156,9 +152,9 @@ func (p *Parser) ColumnHasReferences(stmt *migrate.Stmt, c1 *schema.Column) (boo
 }
 
 // CreateViewAfter checks if a view was created after the position with the given name to a table.
-func (p *Parser) CreateViewAfter(f migrate.File, old, new string, pos int) (bool, error) {
+func (p *Parser) CreateViewAfter(stmts []*migrate.Stmt, old, new string, pos int) (bool, error) {
 	pr := parser.New()
-	return parseutil.MatchStmtAfter(f, pos, func(s *migrate.Stmt) (bool, error) {
+	return parseutil.MatchStmtAfter(stmts, pos, func(s *migrate.Stmt) (bool, error) {
 		stmt, err := pr.ParseOneStmt(s.Text, "", "")
 		if err != nil {
 			return false, err
diff --git a/cmd/atlas/internal/sqlparse/parseutil/parseutil.go b/cmd/atlas/internal/sqlparse/parseutil/parseutil.go
index bf7995b9..59eb3cd3 100644
--- a/cmd/atlas/internal/sqlparse/parseutil/parseutil.go
+++ b/cmd/atlas/internal/sqlparse/parseutil/parseutil.go
@@ -77,11 +77,7 @@ func RenameTable(changes schema.Changes, r *Rename) schema.Changes {
 }
 
 // MatchStmtBefore reports if the file contains any statement that matches the predicate before the given position.
-func MatchStmtBefore(f migrate.File, pos int, p func(*migrate.Stmt) (bool, error)) (bool, error) {
-	stmts, err := f.StmtDecls()
-	if err != nil {
-		return false, err
-	}
+func MatchStmtBefore(stmts []*migrate.Stmt, pos int, p func(*migrate.Stmt) (bool, error)) (bool, error) {
 	i := slices.IndexFunc(stmts, func(s *migrate.Stmt) bool {
 		return s.Pos >= pos
 	})
@@ -101,11 +97,7 @@ func MatchStmtBefore(f migrate.File, pos int, p func(*migrate.Stmt) (bool, error
 }
 
 // MatchStmtAfter reports if the file contains any statement that matches the predicate after the given position.
-func MatchStmtAfter(f migrate.File, pos int, p func(*migrate.Stmt) (bool, error)) (bool, error) {
-	stmts, err := f.StmtDecls()
-	if err != nil {
-		return false, err
-	}
+func MatchStmtAfter(stmts []*migrate.Stmt, pos int, p func(*migrate.Stmt) (bool, error)) (bool, error) {
 	i := slices.IndexFunc(stmts, func(s *migrate.Stmt) bool {
 		return s.Pos > pos
 	})
@@ -124,17 +116,3 @@ func MatchStmtAfter(f migrate.File, pos int, p func(*migrate.Stmt) (bool, error)
 	}
 	return false, nil
 }
-
-func max(i, j int) int {
-	if i > j {
-		return i
-	}
-	return j
-}
-
-func min(i, j int) int {
-	if i < j {
-		return i
-	}
-	return j
-}
diff --git a/cmd/atlas/internal/sqlparse/pgparse/pgparse.go b/cmd/atlas/internal/sqlparse/pgparse/pgparse.go
index 7a5efe14..97c25106 100644
--- a/cmd/atlas/internal/sqlparse/pgparse/pgparse.go
+++ b/cmd/atlas/internal/sqlparse/pgparse/pgparse.go
@@ -22,8 +22,8 @@ import (
 type Parser struct{}
 
 // ColumnFilledBefore checks if the column was filled before the given position.
-func (p *Parser) ColumnFilledBefore(f migrate.File, t *schema.Table, c *schema.Column, pos int) (bool, error) {
-	return parseutil.MatchStmtBefore(f, pos, func(s *migrate.Stmt) (bool, error) {
+func (p *Parser) ColumnFilledBefore(stmts []*migrate.Stmt, t *schema.Table, c *schema.Column, pos int) (bool, error) {
+	return parseutil.MatchStmtBefore(stmts, pos, func(s *migrate.Stmt) (bool, error) {
 		tr, err := pgquery.Parse(s.Text)
 		if err != nil {
 			return false, err
@@ -61,8 +61,8 @@ func (p *Parser) ColumnFilledBefore(f migrate.File, t *schema.Table, c *schema.C
 }
 
 // CreateViewAfter checks if a view was created after the position with the given name to a table.
-func (p *Parser) CreateViewAfter(f migrate.File, old, new string, pos int) (bool, error) {
-	return parseutil.MatchStmtAfter(f, pos, func(s *migrate.Stmt) (bool, error) {
+func (p *Parser) CreateViewAfter(stmts []*migrate.Stmt, old, new string, pos int) (bool, error) {
+	return parseutil.MatchStmtAfter(stmts, pos, func(s *migrate.Stmt) (bool, error) {
 		tr, err := pgquery.Parse(s.Text)
 		if err != nil {
 			return false, err
@@ -78,7 +78,7 @@ func (p *Parser) CreateViewAfter(f migrate.File, old, new string, pos int) (bool
 			return false, nil
 		}
 		from := v.Query.GetSelectStmt().GetFromClause()
-		if f == nil || len(from) != 1 {
+		if len(from) != 1 {
 			return false, nil
 		}
 		return from[0].GetRangeVar().GetRelname() == new, nil
diff --git a/cmd/atlas/internal/sqlparse/sqliteparse/sqliteparse.go b/cmd/atlas/internal/sqlparse/sqliteparse/sqliteparse.go
index d7f66730..517f12b9 100644
--- a/cmd/atlas/internal/sqlparse/sqliteparse/sqliteparse.go
+++ b/cmd/atlas/internal/sqlparse/sqliteparse/sqliteparse.go
@@ -145,8 +145,8 @@ func (s *Stmt) CreateView(name string) (*Create_view_stmtContext, bool) {
 type FileParser struct{}
 
 // ColumnFilledBefore checks if the column was filled before the given position.
-func (p *FileParser) ColumnFilledBefore(f migrate.File, t *schema.Table, c *schema.Column, pos int) (bool, error) {
-	return parseutil.MatchStmtBefore(f, pos, func(s *migrate.Stmt) (bool, error) {
+func (p *FileParser) ColumnFilledBefore(stmts []*migrate.Stmt, t *schema.Table, c *schema.Column, pos int) (bool, error) {
+	return parseutil.MatchStmtBefore(stmts, pos, func(s *migrate.Stmt) (bool, error) {
 		stmt, err := ParseStmt(s.Text)
 		if err != nil {
 			return false, err
@@ -189,8 +189,8 @@ func (p *FileParser) ColumnFilledBefore(f migrate.File, t *schema.Table, c *sche
 }
 
 // CreateViewAfter checks if a view was created after the position with the given name to a table.
-func (p *FileParser) CreateViewAfter(f migrate.File, old, new string, pos int) (bool, error) {
-	return parseutil.MatchStmtAfter(f, pos, func(s *migrate.Stmt) (bool, error) {
+func (p *FileParser) CreateViewAfter(stmts []*migrate.Stmt, old, new string, pos int) (bool, error) {
+	return parseutil.MatchStmtAfter(stmts, pos, func(s *migrate.Stmt) (bool, error) {
 		stmt, err := ParseStmt(s.Text)
 		if err != nil {
 			return false, err
diff --git a/cmd/atlas/internal/sqlparse/sqlparse.go b/cmd/atlas/internal/sqlparse/sqlparse.go
index dd618746..710a9e30 100644
--- a/cmd/atlas/internal/sqlparse/sqlparse.go
+++ b/cmd/atlas/internal/sqlparse/sqlparse.go
@@ -28,7 +28,15 @@ type Parser interface {
 	//	UPDATE <table> SET <column> = <value>
 	//	UPDATE <table> SET <column> = <value> WHERE <column> IS NULL
 	//
-	ColumnFilledBefore(migrate.File, *schema.Table, *schema.Column, int) (bool, error)
+	ColumnFilledBefore([]*migrate.Stmt, *schema.Table, *schema.Column, int) (bool, error)
+
+	// CreateViewAfter checks if a view was created after the position with the given name
+	// to a table. For example:
+	//
+	//	ALTER TABLE `users` RENAME TO `Users`
+	//	CREATE VIEW `users` AS SELECT * FROM `Users`
+	//
+	CreateViewAfter(stmts []*migrate.Stmt, old, new string, pos int) (bool, error)
 }
 
 // drivers specific fixers.
diff --git a/cmd/atlas/main.go b/cmd/atlas/main.go
index bc12cc07..a321ac7b 100644
--- a/cmd/atlas/main.go
+++ b/cmd/atlas/main.go
@@ -9,6 +9,8 @@ import (
 	"context"
 	"fmt"
 	"os"
+	"os/signal"
+	"syscall"
 	"time"
 
 	"ariga.io/atlas/cmd/atlas/internal/cmdapi"
@@ -30,7 +32,22 @@ import (
 
 func main() {
 	cmdapi.Root.SetOut(os.Stdout)
-	ctx := newContext()
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+	go func() {
+		// On first signal seen, cancel the context. On the second signal, force stop immediately.
+		stop := make(chan os.Signal, 2)
+		defer close(stop)
+		signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
+		defer signal.Stop(stop)
+		<-stop   // wait for first interrupt
+		cancel() // cancel context to gracefully stop
+		fmt.Fprintln(cmdapi.Root.OutOrStdout(), "\ninterrupt received, wait for exit or ^C to terminate")
+		// Wait for the context to be canceled. Issuing a second interrupt will cause the process to force stop.
+		<-stop // will not block if no signal received due to main routine exiting
+		os.Exit(1)
+	}()
+	ctx = extendContext(context.Background())
 	done := initialize(ctx)
 	update := checkForUpdate(ctx)
 	err := cmdapi.Root.ExecuteContext(ctx)
diff --git a/cmd/atlas/main_oss.go b/cmd/atlas/main_oss.go
index 9d17ec34..4218a50f 100644
--- a/cmd/atlas/main_oss.go
+++ b/cmd/atlas/main_oss.go
@@ -8,14 +8,9 @@ package main
 
 import (
 	"context"
-	"os"
-	"os/signal"
 )
 
-func newContext() context.Context {
-	ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
-	return ctx
-}
+func extendContext(ctx context.Context) context.Context { return ctx }
 
 func vercheckEndpoint(context.Context) string {
 	return vercheckURL
diff --git a/doc/md/atlas-schema/hcl.mdx b/doc/md/atlas-schema/hcl.mdx
index 63edc259..db33da40 100644
--- a/doc/md/atlas-schema/hcl.mdx
+++ b/doc/md/atlas-schema/hcl.mdx
@@ -857,6 +857,77 @@ schema "public" {
 }
 ```
 
+## Sequence
+
+:::info BETA FEATURE
+Sequences are currently in beta and available to logged-in users only. To use this feature, run:
+```
+atlas login
+```
+:::
+
+The `sequence` block allows defining sequence number generator. Supported by PostgreSQL.
+
+Note, a `sequence` block is printed by Atlas on inspection, or it may be manually defined in the schema only if it
+represents a PostgreSQL sequence that is not implicitly created by the database for identity or `serial` columns.
+
+```hcl
+#highlight-next-line
+# Simple sequence with default values.
+sequence "s1" {
+  schema = schema.public
+}
+
+#highlight-next-line
+# Sequence with custom configuration.
+sequence "s2" {
+  schema    = schema.public
+  type      = smallint
+  start     = 100
+  increment = 2
+  min_value = 100
+  max_value = 1000
+}
+
+#highlight-next-line
+# Sequence that is owned by a column.
+sequence "s3" {
+  schema  = schema.public
+  owner   = table.t2.column.id
+  comment = "Sequence with column owner"
+}
+
+#highlight-next-line
+# The sequences created by this table are not printed on inspection.
+table "users" {
+  schema = schema.public
+  column "id" {
+    type = int
+    identity {
+        generated = ALWAYS
+        start = 10000
+    }
+  }
+  column "serial" {
+    type = serial
+  }
+  primary_key  {
+    columns = [column.id]
+  }
+}
+
+table "t2" {
+  schema = schema.public
+  column "id" {
+    type = int
+  }
+}
+
+schema "public" {
+  comment = "standard public schema"
+}
+```
+
 ## Enum
 
 The `enum` type allows storing a set of enumerated values. Supported by PostgreSQL.
diff --git a/doc/md/components/migrate-push-command.mdx b/doc/md/components/migrate-push-command.mdx
index 044878a4..566e4913 100644
--- a/doc/md/components/migrate-push-command.mdx
+++ b/doc/md/components/migrate-push-command.mdx
@@ -36,14 +36,9 @@ atlas migrate push app \
 </TabItem>
 <TabItem value="sqlserver" label="SQL Server">
 
-Run first the following command to start a local MSSQL server using Docker:
-
-```bash
-docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=P@ssw0rd0995' -p 1433:1433 -d mcr.microsoft.com/mssql/server:2019-latest
-```
 ```bash
 atlas migrate push app \
-    --dev-url "sqlserver://sa:P@ssw0rd0995@localhost:1433?database=master"
+  --dev-url "docker://sqlserver/2022-latest"
 ```
 
 </TabItem>
diff --git a/doc/md/concepts/dev.mdx b/doc/md/concepts/dev.mdx
index 05457e59..445b1a90 100644
--- a/doc/md/concepts/dev.mdx
+++ b/doc/md/concepts/dev.mdx
@@ -110,6 +110,28 @@ docker+postgres://org/image:tag
 --dev-url "sqlite://dev?mode=memory"
 ```
 
+</TabItem>
+<TabItem value="sqlserver" label="SQLServer">
+
+```shell
+# When working on a single database schema.
+--dev-url "docker://sqlserver/2022-latest/dev?mode=schema"
+
+# When working on multiple database schemas.
+--dev-url "docker://sqlserver/2022-latest/dev?mode=database"
+```
+
+To work with an Azure SQL Edge docker image or SQLServer version, use one of the following formats:
+
+```shell
+# Run SQLServer 2017-latest in schema mode.
+docker://sqlserver/2017-latest?mode=schema
+docker://sqlserver/2019-latest?mode=schema
+docker://sqlserver/2022-latest?mode=schema
+
+# Run Azure SQL Edge 1.0.7 in schema mode.
+docker+sqlserver://mcr.microsoft.com/azure-sql-edge:1.0.7?mode=schema
+```
 </TabItem>
 </Tabs>
 
diff --git a/doc/md/declarative/inspect.mdx b/doc/md/declarative/inspect.mdx
index ef89a2eb..3ac29ec9 100644
--- a/doc/md/declarative/inspect.mdx
+++ b/doc/md/declarative/inspect.mdx
@@ -186,7 +186,7 @@ atlas schema inspect -u "mysql://localhost" --exclude "*.prefix_*"
 
 atlas schema inspect -u "mysql://localhost" --exclude "schema.table"
 
-atlas schema inspect -u "mysql://localhost" --exclude "schema.t*[type=table]"
+atlas schema inspect -u "mysql://localhost" --exclude "schema.t*[type=table]" --exclude "schema.e*[type=enum]"
 ```
 
 </TabItem>
@@ -199,7 +199,7 @@ atlas schema inspect -u "mysql://localhost" --exclude "prefix_*"
 
 atlas schema inspect -u "mysql://localhost" --exclude "table"
 
-atlas schema inspect -u "mysql://localhost" --exclude "t*[type=table]"
+atlas schema inspect -u "mysql://localhost" --exclude "t*[type=table]" --exclude "e*[type=enum]"
 ```
 
 </TabItem>
diff --git a/doc/md/guides/deploying/secrets.mdx b/doc/md/guides/deploying/secrets.mdx
new file mode 100644
index 00000000..d410e0be
--- /dev/null
+++ b/doc/md/guides/deploying/secrets.mdx
@@ -0,0 +1,216 @@
+---
+id: secrets
+title: Working with secrets
+slug: /guides/deploying/secrets
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+Database credentials are considered sensitive information and should be treated as such. In this guide,
+we will show how to use Atlas to handle database credentials in a secure manner.  We will present two strategies
+for handling database credentials, and show how to use Atlas to implement them: IAM Authentication and Secret Stores.
+
+## Secret Stores
+
+Secret stores are systems or services that allow users to store and retrieve sensitive information. The main features
+of secret stores are encryption, access control, and auditing.  All cloud providers offer some form of secret store
+service, and there are also many open-source alternatives.  
+
+When working with secret stores, Atlas assumes that the secret store is already provisioned and configured.  Atlas
+supports the following secret stores:
+
+* [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/)
+* [GCP Secret Manager](https://cloud.google.com/secret-manager)
+
+Support for other secret stores is planned, if you have a specific request, please [open an issue](https://github.com/ariga/atlas/issues/new).
+
+## IAM Authentication
+
+IAM authentication is a mechanism that allows users to authenticate to a database using their cloud provider credentials.
+The main advantage of IAM authentication is that it allows users to avoid storing database credentials altogether. Although
+setting this up may be more cumbersome, it is considered a best practice for many cloud providers.  IAM authentication is also more secure than 
+using passwords. Even strong passwords stored in encrypted form can be leaked and used by attackers. IAM authentication
+allows users to avoid storing database credentials altogether.
+
+IAM authentication is currently supported on GCP and AWS. Support for other cloud providers is planned as well,
+if you have a specific request, please [open an issue](https://github.com/ariga/atlas/issues/new).
+
+## Retrieving Credentials from a Secret Store
+
+Atlas can retrieve information from a secret store at runtime using the `runtimevar` data source.  The `runtimevar`
+data source uses the [`runtimevar` package](https://gocloud.dev/howto/runtimevar/) from the Go [CDK](https://gocloud.dev/).
+To read more about using `runtimevar` with Atlas, view the [data source documentation](atlas-schema/projects#data-source-runtimevar).
+
+<Tabs
+defaultValue="aws-secret-manager"
+values={[
+{label: 'AWS Secret Manager', value: 'aws-secret-manager'},
+{label: 'GCP Secret Manager', value: 'gcp-secret-manager'},
+]}>
+<TabItem value="aws-secret-manager">
+
+1. Create a secret a secret to store the database password using the AWS CLI:
+
+    ```bash
+    aws secretsmanager create-secret \
+      --name db-pass-demo \
+      --secret-string "p455w0rd"
+    ```
+
+    The CLI prints out:
+
+    ```text
+    {
+        "ARN": "arn:aws:secretsmanager:us-east-1:1111111111:secret:db-pass-demo-aBiM5k",
+        "Name": "db-pass-demo",
+        "VersionId": "b702431d-174f-4a8f-aee5-b80e687b8bf1"
+    }
+    ```
+
+    Note the database secret name and the region (`us-east-1`), we will use them in the next part.
+
+2. Create a new file named `atlas.hcl` with the following contents:
+
+    ```hcl
+    data "runtimevar" "pass" {
+      url = "awssecretsmanager://db-pass-demo?region=us-east-1"
+    }
+
+    env "dev" {
+      url = "mysql://root:${data.runtimevar.pass}@host:3306/database"
+    }
+    ```
+
+    Let's breakdown the configuration:
+    * The `runtimevar` data source is used to retrieve the database password from AWS Secrets Manager.
+    * We define an `env` named `dev`.  The value retrieved by the `runtimevar` data source
+       is interpolated into the `url` attribute using the `${data.runtimevar.pass}` expression.
+
+3. Run `atlas schema inspect --env dev` to verify that Atlas is able to connect to the database.
+
+</TabItem>
+<TabItem value="gcp-secret-manager">
+
+1. Create a secret a secret to store the database password using the GCP CLI:
+
+    ```bash
+    printf "p455w0rd" | gcloud secrets create db-pass-demo --data-file=-
+    ```
+
+    The CLI prints out:
+
+    ```text
+    Created version [1] of the secret [db-pass-demo].
+    ```
+
+2. Create a new file named `atlas.hcl` with the following contents:
+
+    ```hcl
+    data "runtimevar" "pass" {
+      url = "gcpsecretmanager://projects/my-project/secrets/db-pass-demo"
+    }
+
+    env "dev" {
+      url = "mysql://root:${data.runtimevar.pass}@host:3306/database"
+    }
+    ```
+
+    Let's breakdown the configuration:
+    * The `runtimevar` data source is used to retrieve the database password from GCP Secret Manager. The URL
+      is composed of the project and secret name. If you are working locally in a multi-project environment, you
+      can find out the name of the active project by running `gcloud config get-value project`.
+    * We define an `env` named `dev`.  The value retrieved by the `runtimevar` data source
+       is interpolated into the `url` attribute using the `${data.runtimevar.pass}` expression.
+
+3. Run `atlas schema inspect --env dev` to verify that Atlas is able to connect to the database.
+
+</TabItem>
+</Tabs>
+
+## Using IAM Authentication
+
+Atlas can retrieve short-lived credentials from the cloud provider and use them to connect to the database. The
+passwords are retrieved using special data sources that are specific to each cloud provider.
+
+<Tabs
+defaultValue="aws-rds-iam"
+values={[
+{label: 'aws_rds_token', value: 'aws-rds-iam'},
+{label: 'gcp_cloudsql_token', value: 'gcp-cloud-sql-iam'},
+]}>
+<TabItem value="aws-rds-iam">
+
+1. Enable IAM Authentication for your database. For instructions on how to do this,
+   [see the AWS documentation](https://aws.github.io/aws-sdk-go-v2/docs/sdk-utilities/rds/#iam-authentication).
+
+2. Create a database user and grant it permission to authenticate using IAM, see
+   [the AWS documentation](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.DBAccounts.html)
+   for instructions.
+
+3. Create an IAM role with the "rds-db:connect" permission for the specific database and user. For instructions on how to do this,
+   [see the AWS documentation](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.IAMPolicy.html).
+
+4. Create a new file named `atlas.hcl` with the following contents:
+
+    ```hcl
+    locals {
+      user = "iamuser"
+      endpoint = "hostname-of-db.example9y7k.us-east-1.rds.amazonaws.com:5432"
+    }
+
+    data "aws_rds_token" "db" {
+        region = "us-east-1"
+        endpoint = local.endpoint
+        username = local.user
+    }
+
+    env "rds" {
+      url = "postgres://${local.user}:${urlescape(data.aws_rds_token.db)}@${local.endpoint}/postgres"
+    }
+    ```
+
+    Let's breakdown the configuration:
+    * The `aws_rds_token` data source is used to retrieve the database password from AWS Secrets Manager.
+    * We define an `env` named `rds`.  The value retrieved by the `aws_rds_token` data source
+       is interpolated into the `url` attribute using the `${data.aws_rds_token.db}` expression.
+
+</TabItem>
+<TabItem value="gcp-cloud-sql-iam">
+
+The `gcp_cloudsql_token` data source generates a short-lived token for an [GCP CloudSQL](https://cloud.google.com/sql) database
+using [IAM Authentication](https://cloud.google.com/sql/docs/mysql/authentication#manual).
+
+To use this data source:
+
+1. Enable IAM Authentication for your database. For instructions on how to do this,
+   [see the GCP documentation](https://cloud.google.com/sql/docs/mysql/create-edit-iam-instances).
+2. Create a database user and grant it permission to authenticate using IAM, see
+   [the GCP documentation](https://cloud.google.com/sql/docs/mysql/add-manage-iam-users)
+   for instructions.
+3. Create a file named `atlas.hcl` with the following contents:
+
+    ```hcl title="atlas.hcl"
+    locals {
+      user = "iamuser"
+      endpoint = "34.143.100.1:3306"
+    }
+
+    data "gcp_cloudsql_token" "db" {}
+
+    env "cloudsql" {
+      url = "mysql://${local.user}:${urlescape(data.gcp_cloudsql_token.db)}@${local.endpoint}/?allowCleartextPasswords=1&tls=skip-verify&parseTime=true"
+    }
+    ```
+
+    :::note
+    The `allowCleartextPasswords` and `tls` parameters are required for the MySQL driver to connect to CloudSQL. For PostgreSQL, use `sslmode=require` to connect to the database.
+    :::
+
+    Let's breakdown the configuration:
+    * The `gcp_cloudsql_token` data source is used to retrieve the database password from GCP CloudSQL.
+    * We define an `env` named `cloudsql`.  The value retrieved by the `gcp_cloudsql_token` data source
+       is interpolated into the `url` attribute using the `${data.gcp_cloudsql_token.db}` expression.
+
+</TabItem>
+</Tabs>
\ No newline at end of file
diff --git a/doc/md/guides/guides.mdx b/doc/md/guides/guides.mdx
index 4c40a421..bea00817 100644
--- a/doc/md/guides/guides.mdx
+++ b/doc/md/guides/guides.mdx
@@ -257,6 +257,12 @@ import { Card, CardImage } from "../components/card";
     url={"/guides/orms/typeorm"}
     image={CardImage.Tools}
   />
+  <Card
+    name="Hibernate"
+    description="This guide is for automatic migration planning for Hibernate with Atlas"
+    url="/guides/orms/hibernate"
+    image={CardImage.Tools}
+  />
 </div>
 
 ## Terraform
diff --git a/doc/md/guides/orms/hibernate.mdx b/doc/md/guides/orms/hibernate.mdx
new file mode 100644
index 00000000..8c189039
--- /dev/null
+++ b/doc/md/guides/orms/hibernate.mdx
@@ -0,0 +1,501 @@
+---
+id: hibernate
+title: Automatic migration planning for Hibernate
+slug: /guides/orms/hibernate
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+## TL;DR
+* [Hibernate](https://hibernate.org/orm/) is an ORM library that's widely used in the Java community.
+* [Atlas](https://atlasgo.io) is an open-source tool for inspecting, planning, linting and
+  executing schema changes to your database.
+* Developers using Hibernate can use Atlas to automatically plan schema migrations
+  for them, based on the desired state of their schema instead of crafting them by hand.
+
+## Automatic migration planning for Hibernate
+Hibernate is a popular ORM widely used in the Java community. Hibernate allows users to
+manage their database schemas using its [automatic schema generation](https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#schema-generation) feature,
+which is usually sufficient during development and in many simple cases.
+
+However, at some point, teams need more control and decide to employ
+the [versioned migrations](/concepts/declarative-vs-versioned#versioned-migrations)
+methodology, which is a more robust way to manage your database schema.
+Once this happens, the responsibility for planning migration scripts and making
+sure they are in line with what Hibernate expects at runtime is moved to developers.
+
+Atlas can automatically plan database schema migrations for developers using Hibernate.
+Atlas plans migrations by calculating the diff between the _current_ state of the database
+and its _desired_ state.
+
+In the context of versioned migrations, the current state can be thought of as the database schema that would have been created by applying
+all previous migration scripts.
+
+The desired schema of your application can be provided to Atlas via an [External Schema Data Source](/atlas-schema/projects#data-source-external_schema),
+which is any program that can output a SQL schema definition to stdout.
+
+To use Atlas with Hibernate, users can utilize the [Hibernate Atlas Provider](https://github.com/ariga/atlas-provider-hibernate),
+a small program that can be used to load the schema of a Hibernate project into Atlas.
+
+In this guide, we will show how Atlas can automatically plan schema migrations for Hibernate users.
+
+## Prerequisites
+
+* A local [Hibernate](https://hibernate.org/orm/) project.
+
+If you don't have a Hibernate project handy, check out the [Hibernate getting started page](https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html)
+
+## Using the Atlas Hibernate Provider
+
+In this guide, we will use the [Hibernate Atlas Provider](https://github.com/ariga/atlas-provider-hibernate)
+to automatically plan schema migrations for a Hibernate project.
+
+Using the provider does not require a connection to a live database.
+
+### Installation
+
+Install Atlas from macOS or Linux by running:
+```bash
+curl -sSf https://atlasgo.sh | sh
+```
+See [atlasgo.io](https://atlasgo.io/getting-started#installation) for more installation options.
+
+Install the provider by adding this plugin to your Gradle project:
+
+<Tabs>
+<TabItem value="Gradle Kotlin" label="Gradle Kotlin" default>
+
+```gradle
+plugins {
+    id("io.atlasgo.hibernate-provider-gradle-plugin") version "0.1"
+}
+```
+</TabItem>
+<TabItem value="Gradle Groovy" label="Gradle Groovy" default>
+
+```gradle
+plugins {
+    id "io.atlasgo.hibernate-provider-gradle-plugin" version "0.1"
+}
+```
+</TabItem>
+<TabItem value="Maven" label="Maven">
+
+```xml
+<build>
+    <pluginManagement>
+        <plugins>
+            <plugin>
+                <groupId>io.atlasgo</groupId>
+                <artifactId>hibernate-provider-maven-plugin</artifactId>
+                <version>0.1</version>
+            </plugin>
+        </plugins>
+    </pluginManagement>
+</build>
+```
+</TabItem>
+</Tabs>
+
+Verify that the plugin is installed by running this command:
+
+<Tabs>
+<TabItem value="Gradle" label="Gradle" default>
+
+```bash
+./gradlew help --task schema
+```
+</TabItem>
+<TabItem value="Maven" label="Maven" default>
+
+```bash
+mvn help:describe -Dplugin=hibernate-provider -Dgoal=schema
+```
+</TabItem>
+</Tabs>
+
+### Library vs Standalone
+
+The Atlas Hibernate Provider can be used in two modes:
+* **Standalone** - In most cases, you can use the provider as a Gradle task (or a Maven Mojo) to output
+the Hibernate schema and load the schema into Atlas.
+* **Library** - If you are initializing Hibernate in a custom way, using an older version of
+Hibernate or if you are not using Gradle or Maven, you can use the provider as a library and create
+a small application that prints the schema to stdout.
+
+### Standalone
+
+In your project directory, create a new file named `atlas.hcl` with the following contents:
+
+<Tabs>
+<TabItem value="Gradle" label="Gradle" default>
+
+```hcl title='atlas.hcl'
+data "external_schema" "hibernate" {
+    program = [
+        "./gradlew",
+        "-q",
+        "schema",
+        "--properties", "schema-export.properties"
+    ]
+}
+```
+
+</TabItem>
+<TabItem value="Maven" label="Maven">
+
+```hcl title='atlas.hcl'
+data "external_schema" "hibernate" {
+  program = [
+    "./mvn",
+    "-q",
+    "hibernate-provider:schema",
+    "-Dproperties", "schema-export.properties"
+  ]
+}
+```
+
+</TabItem>
+</Tabs>
+
+Depending on your database, add an environment configuration. For example, for MySQL and PostgreSQL:
+
+<Tabs>
+<TabItem value="MySQL" label="MySQL" default>
+
+```hcl title='atlas.hcl'
+env "hibernate" {
+  src = data.external_schema.hibernate.url
+  dev = "docker://mysql/8/dev"
+  migration {
+    dir = "file://migrations"
+  }
+  format {
+    migrate {
+      diff = "{{ sql . \"  \" }}"
+    }
+  }
+}
+```
+</TabItem>
+<TabItem value="PostgreSQL" label="PostgreSQL">
+
+```hcl title='atlas.hcl'
+env "hibernate" {
+  src = data.external_schema.hibernate.url
+  dev = "docker://postgres/15/dev?search_path=public"
+  migration {
+    dir = "file://migrations"
+  }
+  format {
+    migrate {
+      diff = "{{ sql . \"  \" }}"
+    }
+  }
+}
+```
+</TabItem>
+</Tabs>
+
+We need to let Atlas and the provider know which SQL dialect should be used. The dialect is specified in the
+properties file and in the [Dev Database](/concepts/dev-database) that Atlas will use.
+
+Create a resource named `schema-export.properties` in `src/main/resource`, it should be part of the classpath by default.
+Specify the SQL dialect that will be used to initialize Hibernate:
+
+<Tabs>
+<TabItem value="MySQL" label="MySQL" default>
+
+```properties
+jakarta.persistence.database-product-name=MySQL
+jakarta.persistence.database-major-version=8
+```
+</TabItem>
+<TabItem value="PostgreSQL" label="PostgreSQL">
+
+```properties
+jakarta.persistence.database-product-name=PostgreSQL
+```
+</TabItem>
+</Tabs>
+
+> Note: The properties being used might vary depending on the version of Hibernate and the database connector that is configured for your project.
+
+### Library mode
+
+We are going to add the provider as a dependency in Gradle (or Maven) and configure Hibernate to use
+`ConsoleSchemaManagementTool`. In this example, we are going to use Spring to initialize Hibernate.
+
+> By default, the `HibernateSchemaManagementTool` will try to connect to the
+database during initialization and generate the schema into the database. The `ConsoleSchemaManagementTool` overrides this behaviour.
+
+Start by adding the provider as a dependency:
+
+<Tabs>
+<TabItem value="Gradle" label="Gradle" default>
+
+```gradle
+dependencies {
+    implementation("io.atlasgo:hibernate-provider:0.1")
+}
+```
+</TabItem>
+<TabItem value="Maven" label="Maven">
+
+```xml
+<dependency>
+    <groupId>io.atlasgo</groupId>
+    <artifactId>hibernate-provider</artifactId>
+    <version>0.1</version>
+</dependency>
+```
+</TabItem>
+</Tabs>
+
+Create a new file named `HibernateSchemaExporter.java` with the following contents:
+
+```java
+@SpringBootApplication
+@PropertySource(value = {"classpath:schema-export.properties"})
+public class HibernateSchemaExporter {
+    public static void main(String[] args) {
+        new AnnotationConfigApplicationContext(HibernateSchemaExporter.class);
+    }
+}
+```
+
+Next, create a new resource named `schema-export.properties` (make sure it is in the classpath) with this content:
+
+<Tabs>
+<TabItem value="MySQL" label="MySQL" default>
+
+```properties
+spring.jpa.properties.jakarta.persistence.database-product-name=MySQL
+spring.jpa.properties.jakarta.persistence.database-major-version=8
+spring.jpa.properties.jakarta.persistence.schema-generation.database.action=create
+spring.jpa.properties.hibernate.schema_management_tool=io.atlasgo.ConsoleSchemaManagementTool
+```
+</TabItem>
+<TabItem value="PostgreSQL" label="PostgreSQL">
+
+```properties
+spring.jpa.properties.jakarta.persistence.database-product-name=PostgreSQL
+spring.jpa.properties.jakarta.persistence.schema-generation.database.action=create
+spring.jpa.properties.hibernate.schema_management_tool=io.atlasgo.ConsoleSchemaManagementTool
+```
+</TabItem>
+</Tabs>
+
+
+Lastly, in your project directory, create a new file named `atlas.hcl` with the following contents:
+
+```hcl title='atlas.hcl'
+data "external_schema" "hibernate" {
+  program = [
+    "./gradlew",
+    "-q",
+    "bootRun"
+  ]
+}
+```
+
+Depending on your database, add an environment configuration. For example, for MySQL and PostgreSQL:
+
+<Tabs>
+<TabItem value="MySQL" label="MySQL" default>
+
+```hcl title='atlas.hcl'
+env "hibernate" {
+  src = data.external_schema.hibernate.url
+  dev = "docker://mysql/8/dev"
+  migration {
+    dir = "file://migrations"
+  }
+  format {
+    migrate {
+      diff = "{{ sql . \"  \" }}"
+    }
+  }
+}
+```
+</TabItem>
+<TabItem value="PostgreSQL" label="PostgreSQL" default>
+
+```hcl title='atlas.hcl'
+env "hibernate" {
+  src = data.external_schema.hibernate.url
+  dev = "docker://postgres/15/dev?search_path=public"
+  migration {
+    dir = "file://migrations"
+  }
+  format {
+    migrate {
+      diff = "{{ sql . \"  \" }}"
+    }
+  }
+}
+```
+</TabItem>
+</Tabs>
+
+## Usage
+
+Atlas supports a [versioned migrations](/concepts/declarative-vs-versioned#versioned-migrations)
+workflow, where each change to the database is versioned and recorded in a migration file. You can use the
+`atlas migrate diff` command to automatically generate a migration file that will migrate the database
+from its latest revision to the current Hibernate schema.
+
+Suppose we have the following Hibernate `com.example.model` package, with two models `Event` and `Location`:
+
+<Tabs>
+<TabItem value="Event" label="Event.java" default>
+
+```java
+package com.example.model;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+
+import static jakarta.persistence.GenerationType.IDENTITY;
+
+@Entity
+@Table(name = "Events")
+public class Event {
+    private String title;
+
+    @Id
+    @GeneratedValue(strategy = IDENTITY)
+    private Long id;
+}
+
+```
+</TabItem>
+<TabItem value="Location" label="Location.java">
+
+```java
+package com.example.model;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+
+import static jakarta.persistence.GenerationType.IDENTITY;
+
+@Entity
+@Table(name = "Events")
+public class Location {
+    @Id
+    @GeneratedValue(strategy = IDENTITY)
+    private Long id;
+}
+```
+</TabItem>
+</Tabs>
+
+Atlas uses the `atlas.sum` file to protect against conflicting schema changes, you can read about it [here](/concepts/migration-directory-integrity).
+
+:::note
+Currently, Atlas does not support using generated fields that require data initialization such as `GenerationType.SEQUENCE`, `GenerationType.TABLE`, and `Generation.AUTO`.
+
+If needed, you can still export the schema using the flag --enable-table-generators (or -Denable-table-generators using Maven). When applying the schema to your database, you will need
+to make sure to apply the ignored statements (using `atlas migrate --env hibernate diff --edit`). See more information on manual migrations [here](/versioned/diff)
+
+For example, if you are adding GenerationType.SEQUENCE to the Event entity, you will need to add insert statements to your generated migration file:
+
+```diff
+diff --git a/migrations/20231210140844.sql b/examples/with_local_plugin_repository/migrations/20231210140844.sql
+index ad80a64..5955834 100644
+--- a/migrations/20231210140844.sql
++++ b/migrations/20231210140844.sql
+@@ -4,3 +4,6 @@ CREATE TABLE `Event` (`id` bigint NOT NULL AUTO_INCREMENT, `title` varchar(255)
+ -- Create "Event_SEQ" table
+ CREATE TABLE `Event_SEQ` (`next_val` bigint NULL) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
++ -- Initialize "Event_SEQ" table
++ insert into Event_SEQ values ( 1 );
+```
+
+Testing these changes can be done by running the application with a local database and creating the entity. To apply the migration directory
+to the local database, use `atlas migrate apply`.
+
+:::
+
+Using the configuration we created earlier, we can generate a migration file by running the following command:
+
+```bash
+atlas migrate diff --env hibernate
+```
+
+Running this command will generate files similar to this in the `migrations` directory:
+
+```
+migrations
+|-- 20231128110831.sql
+`-- atlas.sum
+
+0 directories, 2 files
+```
+
+Examining the contents of `20231128110831.sql`:
+
+```sql
+-- Create "Events" table
+CREATE TABLE `Events` (
+  `id` bigint NOT NULL AUTO_INCREMENT,
+  `title` varchar(255) NULL,
+  PRIMARY KEY (`id`)
+) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
+-- Create "Locations" table
+CREATE TABLE `Locations` (
+  `id` bigint NOT NULL AUTO_INCREMENT,
+  PRIMARY KEY (`id`)
+) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
+```
+
+Amazing! Atlas automatically generated a migration file that will create the `Events` and `Locations` tables in our database!
+
+Next, alter the `Event` model to add a new `Location` field:
+
+
+```diff
+ import jakarta.persistence.Id;
++import jakarta.persistence.ManyToOne;
+ import jakarta.persistence.Table;
+
+ @Entity
+@@ -11,6 +12,9 @@ import jakarta.persistence.Table;
+ public class Event {
+     private String title;
+
++    @ManyToOne
++    private Location location;
++
+     @Id
+     @GeneratedValue(strategy = GenerationType.IDENTITY)
+     private Long id;
+```
+
+Re-run this command:
+
+```bash
+atlas migrate diff --env hibernate
+```
+
+Observe that a new migration file is generated:
+
+```sql
+-- Modify "Events" table
+ALTER TABLE `Events` ADD COLUMN `location_id` bigint NULL, ADD INDEX `FK4hncpre5frbs4krenagj1xyk4` (`location_id`), ADD CONSTRAINT `FK4hncpre5frbs4krenagj1xyk4` FOREIGN KEY (`location_id`) REFERENCES `Locations` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION;
+```
+
+## Conclusion
+
+In this guide we demonstrated how projects using Hibernate can use Atlas to automatically
+plan schema migrations based solely on their data model. To learn more about executing
+these migrations against your production database, read the documentation for the
+[`migrate apply`](/versioned/apply) command.
+
+Have questions? Feedback? Find our team [on our Discord server](https://discord.gg/zZ6sWVg6NT)
\ No newline at end of file
diff --git a/doc/md/lint/analyzers.mdx b/doc/md/lint/analyzers.mdx
index 0f209f0f..226ae932 100644
--- a/doc/md/lint/analyzers.mdx
+++ b/doc/md/lint/analyzers.mdx
@@ -253,6 +253,10 @@ The following schema change checks are provided by Atlas:
 | **MY**                                    | MySQL and MariaDB specific checks                                               |
 | [MY101](#MY101)                           | Adding a non-nullable column without a `DEFAULT` value to an existing table     |
 | [MY102](#MY102)                           | Adding a column with an inline `REFERENCES` clause has no actual effect         |
+| [MY110](#MY110)                           | Removing enum values from a column requires a table copy                        |
+| [MY111](#MY111)                           | Reordering enum values of a colum requires a table copy                         |
+| [MY112](#MY112)                           | Inserting new enum values not at the end requires a table copy                  |
+| [MY113](#MY113)                           | Exceeding 256 enum values changes storage size and requires a table copy        |
 | **NM**                                    | **[Naming Conventions](#naming-conventions-policy)**                            |
 | [NM101](#NM101)                           | Schema name violates the naming convention                                      |
 | [NM102](#NM102)                           | Table name violates the naming convention                                       |
@@ -429,6 +433,38 @@ specification instead. For example:
 +CREATE TABLE pets (owner_id int, FOREIGN KEY (owner_id) REFERENCES users(id));
 ```
 
+#### MY110 {#MY110}
+
+Removing enum values from a column changes the column type and requires a table copy. During this process, the table
+is locked for write operations. In addition, operation may fail if the column contains values that are not in the new
+enum definition.
+
+#### MY111 {#MY111}
+
+Reordering enum values of a colum requires a table copy. During this process, the table is locked for write operations.
+
+Note that since the order of the enum values defines how the table is sorted when using `ORDER BY` on the column,
+controlling the ordering behavior can be achieved using the `DESC` clause or expressions as follows:
+
+```sql {1,4}
+-- Instead of reversing the enum values.
+SELECT * FROM t ORDER BY enum_column DESC;
+
+-- Instead of reordering the enum values.
+SELECT * FROM t ORDER BY CASE enum_column WHEN 'a' THEN 1 WHEN 'b' THEN 2 ELSE 3 END;
+```
+
+#### MY112 {#MY112}
+
+Inserting new enum values not at the end requires table copy. During this process, the table is locked for write operations.
+
+If possible, it is recommended to add new enum values at the end of the enum definition.
+
+#### MY113 {#MY113}
+
+Exceeding 256 enum values changes storage size and requires a table copy. During this process, the table is locked for
+write operations.
+
 #### LT101 {#LT101}
 
 Modifying a nullable column to non-nullable without setting a `DEFAULT` might fail in case it contains `NULL` values.
diff --git a/doc/website/blog/2023-10-17-automatic-migrations-for-sql-server.mdx b/doc/website/blog/2023-10-17-automatic-migrations-for-sql-server.mdx
index 29fe5c41..6988424f 100644
--- a/doc/website/blog/2023-10-17-automatic-migrations-for-sql-server.mdx
+++ b/doc/website/blog/2023-10-17-automatic-migrations-for-sql-server.mdx
@@ -95,17 +95,6 @@ CREATE TABLE posts (
 );
 ```
 
-### Spinning up a Dev-Database
-
-Atlas relies on a [_Dev-Database_](/concepts/dev-database), a temporary empty database to normalize schemas and make various
-calculations. Because we are providing the desired state of our database in plain SQL, Atlas must somehow transform
-our schema to a normalized form which requires such a database. Let's spin up an additional SQL Server instance
-on port 1434:
-
-```
-docker run --rm -e 'ACCEPT_EULA=Y' -e 'MSSQL_SA_PASSWORD=P@ssw0rd0995' -p 1434:1433 -d mcr.microsoft.com/mssql/server:latest
-```
-
 ### Applying our schema
 
 Next, let's apply this schema to our database. To do so, we will use the `atlas schema apply` command.
@@ -113,7 +102,7 @@ Next, let's apply this schema to our database. To do so, we will use the `atlas
 ```shell
 atlas schema apply -u "sqlserver://sa:P@ssw0rd0995@localhost:1433?database=master" \
   --to file://schema.sql \
-  --dev-url "sqlserver://sa:P@ssw0rd0995@localhost:1434?database=master"
+  --dev-url "docker://sqlserver"
 ```
 Atlas will connect to our target database to inspect it's current state. Next, it will use the dev-database to
 normalize our schema and finally, it will generate the SQL commands that will bring our database to the desired state:
diff --git a/doc/website/blog/2023-12-06-gitops-for-databases-part-1.mdx b/doc/website/blog/2023-12-06-gitops-for-databases-part-1.mdx
new file mode 100644
index 00000000..4e5265ec
--- /dev/null
+++ b/doc/website/blog/2023-12-06-gitops-for-databases-part-1.mdx
@@ -0,0 +1,378 @@
+---
+title: "GitOps for Databases, Part 1: CI/CD"
+authors: rotemtam
+tags: [kubernetes, gitops, migrations, argocd, github-actions, postgres]
+---
+:::info
+
+This is the first post in a two-part tutorial, which demonstrates how to use the [Atlas Operator](https://github.com/ariga/atlas-operator) in tandem with
+[Atlas Cloud](https://atlasgo.cloud) and [ArgoCD](https://argo-cd.readthedocs.io/en/stable/) to create a slick,
+modern GitOps workflow for managing your database migrations natively in Kubernetes.
+
+:::
+
+[GitOps](https://opengitops.dev/) is a software development and deployment methodology that uses Git as the central repository
+for both code and infrastructure configuration, enabling automated and auditable deployments.
+
+[ArgoCD](https://argoproj.github.io/cd/) is a Kubernetes-native continuous delivery tool that implements GitOps principles.
+It uses a declarative approach to deploy applications to Kubernetes, ensuring that the desired state of the
+application is always maintained.
+
+[Kubernetes Operators](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/) are software extensions to
+Kubernetes that enable the automation and management of complex, application-specific, operational tasks with
+domain-specific knowledge within a Kubernetes cluster.
+
+In this tutorial, we will use the [Atlas Operator](https://github.com/ariga/atlas-operator) in tandem with
+[Atlas Cloud](https://atlasgo.cloud) and ArgoCD to create a slick, modern GitOps workflow for managing your
+database migrations natively in Kubernetes.
+
+For the sake of brevity, we are going to split this guide into two parts:
+1. In **part one**, we will show how to initialize an Atlas project, and create a CI/CD pipeline that
+    will automatically plan, verify and store your database migrations in Atlas Cloud using GitHub
+    Actions.
+2. In **part two**, we will show how to deploy these migrations using the Atlas Operator and ArgoCD to demonstrate
+    a complete GitOps workflow for database migrations.
+
+## Modern CI/CD for Database Migrations
+
+![](https://atlasgo.io/uploads/ci-cd-guide/database-ci-cd-workflow.png)
+
+Atlas was built to support a modern CI/CD workflow for database migrations based on the
+following principles:
+1. **Changes to the database are planned automatically.** Given the desired state of the database,
+   the system should automatically generate a plan for how to get from the current state to the desired
+   state.
+2. **Changes to the database schema are stored in a versioned migration directory.** All planned changes
+   to the database are checked in to a versioned migration directory. This directory contains SQL scripts,
+   which are executed in lexicographic order to apply the changes to the database.
+3. **Changes to the database are validated during CI.** All changes to the database are tested and evaluated
+   against a set of governing policies.
+4. **Changes to the database are deployed via automation.**  No manual steps are required to deploy changes to the
+   database. All changes are deployed via a CI/CD pipeline.
+
+To learn more about these principles, check out our [guide to modern CI/CD for database migrations](/guides/modern-database-ci-cd).
+
+In this tutorial, we will show to apply the fourth principle to your database migrations using the Atlas Operator and ArgoCD.
+
+## Local Setup
+
+With the principles of modern CI/CD for database migrations in mind, let's see how we can apply them to a simple
+application that uses a PostgreSQL database.
+
+### Prerequisites (for part one)
+
+1. A GitHub Account - we are going to be setting up some GitHub Actions workflows, so you will need a GitHub account.
+
+2. The most recent version of Atlas. To get Atlas on Linux or macOS run:
+   ```bash
+   curl -sSf https://atlasgo.sh | sh
+   ```
+  For more installation options, [see the docs](/getting-started#installation)
+3. Docker. To install Docker, follow the instructions [here](https://docs.docker.com/get-docker/).
+
+4. The GitHub CLI, `gh`. To install `gh`:
+   ```
+   brew install gh
+   ```
+  Follow instructions for other platforms [here](https://github.com/cli/cli#installation).
+
+### Step 1: Define our desired state
+
+Atlas advocates for the _declarative_ approach in which users start their work by defining the
+_desired_ state of their database and let the system figure out the implementation details. Atlas supports
+many different ways to define the desired state of your database, called "schema loaders". In this tutorial,
+we will use a simple SQL file to define our desired state.
+
+In a fresh Git repo, create a file named `schema.sql` with the following contents:
+
+```sql title="schema.sql"
+create table users (
+    id int primary key,
+    name varchar(255) not null unique
+);
+```
+
+In this project, whenever we want to change the database schema, we will update this file to reflect the
+desired state of the database.
+
+### Step 2: Plan the initial migration
+
+Now that we have defined our desired state, we can use the Atlas CLI to plan the initial migration.
+Create the following file named `atlas.hcl`:
+
+```hcl
+env "local" {
+  src = "file://schema.sql"
+  dev = "docker://postgres/15/dev"
+  migration {
+    dir = "file://migrations"
+  }
+  format {
+    migrate {
+      diff = "{{ sql . \"  \" }}"
+    }
+  }
+}
+```
+
+Next, run the following command to plan the initial migration:
+
+```bash
+atlas migrate diff --env local
+```
+
+Observe two new files that were created in the `migrations` directory:
+
+```text
+.
+├── atlas.hcl
+├── migrations
+│   ├── 20231204121249.sql
+│   └── atlas.sum
+└── schema.sql
+```
+
+### Step 3: Push our migration directory to Atlas Cloud
+
+Atlas Cloud is a hosted service that can serve as a central repository for your database migrations. Similarly
+to how DockerHub is used to store and distribute Docker images, Atlas Cloud can be used to store and distribute
+database migration directories. Atlas Cloud has a free tier that is suitable for small teams and personal projects
+which you can use to follow along with this tutorial.
+
+Log in to Atlas Cloud using the following command:
+
+```bash
+atlas login
+```
+If you do not have an existing Atlas Cloud account, you will be prompted to create one.
+
+Next, push your migration directory to Atlas Cloud using the following command:
+
+```bash
+atlas migrate push --env local atlasdemo
+```
+
+This will create a new project named `atlasdemo` on Atlas Cloud and push your migration directory to it.
+Atlas will print a URL to the project page on Atlas Cloud similar to the following:
+
+```text
+https://rotemtam85.atlasgo.cloud/dirs/4294967359
+```
+
+## Setup GitHub Actions
+
+In this section, we will set up a GitHub Actions workflow that will add Atlas to your CI/CD
+pipeline.
+
+### Create a Bot Token
+
+To write data to your Atlas Cloud account, you will need to supply your CI/CD pipelines with
+an API key that has write access to your Atlas Cloud account. To learn how to create a
+bot token, check out our [guide on the topic](/cloud/bots). Use the instructions in this guide
+to create a token, and make a note of it. We will use it in the next steps.
+
+### Install the Atlas Extension
+
+To streamline this process, we have created a `gh` command that will create the workflow for you.
+To install the latest version, run:
+
+```shell
+gh extension install ariga/gh-atlas
+```
+
+### Ensure your `gh` CLI has sufficient permissions
+
+Make sure you have the necessary permissions to configure your action:
+
+```shell
+gh auth refresh -s write:packages,workflow
+```
+
+### Create a GitHub Actions Workflow
+
+Once installed, let's use this extension to generate our GitHub Actions workflow. Run the following command:
+
+```shell
+gh atlas init-action --token <your-bot-token>  --dir-name="atlasdemo" --driver=postgres
+```
+Atlas will scan your repository (locally) for directories containing Atlas migrations
+and ask you which one you would like to use for CI. Select the desired directory and press "Enter":
+
+```text
+Use the arrow keys to navigate: ↓ ↑ → ←
+? choose migration directory:
+▸ migrations
+```
+
+Atlas will then ask you which database driver this directory contains migrations for. Select the
+desired driver and press "Enter".
+
+Next, the GitHub extension will save your bot token to a GitHub secret and create a
+pull request with the necessary configuration for the GitHub Action.
+
+<details>
+<summary>Screenshot Example</summary>
+
+![](https://atlasgo.io/uploads/cloud/ci/gh-ext-pr-2.png)
+</details>
+
+The PR contains a GitHub Actions workflow similar to this:
+
+<details>
+
+<summary>Code Example</summary>
+
+```yaml
+name: Atlas
+on:
+  push:
+    branches:
+      - master
+    paths:
+      - .github/workflows/ci-atlas.yaml
+      - 'migrations/*'
+  pull_request:
+    paths:
+      - 'migrations/*'
+# Permissions to write comments on the pull request.
+permissions:
+  contents: read
+  pull-requests: write
+jobs:
+  atlas:
+    services:
+      # Spin up a postgres:15 container to be used as the dev-database for analysis.
+      postgres:
+        image: postgres:15
+        env:
+          POSTGRES_DB: dev
+          POSTGRES_PASSWORD: pass
+        ports:
+          - 5432:5432
+        options: >-
+          --health-cmd pg_isready
+          --health-interval 10s
+          --health-start-period 10s
+          --health-timeout 5s
+          --health-retries 5
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+      - uses: ariga/setup-atlas@v0
+        with:
+          cloud-token: ${{ secrets.ATLAS_CLOUD_TOKEN_K6MJMK }}
+      - uses: ariga/atlas-action/migrate/lint@v1
+        with:
+          dir: 'file://migrations'
+          dir-name: 'atlasdemo'
+          dev-url: 'postgres://postgres:pass@localhost:5432/dev?search_path=public&sslmode=disable'
+        env:
+          GITHUB_TOKEN: ${{ github.token }}
+      - uses: ariga/atlas-action/migrate/push@v1
+        if: github.ref == 'refs/heads/master'
+        with:
+            dir: 'file://migrations'
+            dir-name: 'atlasdemo'
+            dev-url: 'postgres://postgres:pass@localhost:5432/dev?search_path=public&sslmode=disable'
+```
+
+</details>
+
+After reviewing the changes, merge the pull request to enable the GitHub Action.
+
+## Testing our Pipeline
+
+Now that we have set everything up, let's test our pipeline end-to-end. To do so, we will
+first plan a new change to our database schema.
+
+### Edit the Desired Database Schema
+
+Edit the `schema.sql` file to add a new column to the `users` table:
+
+```sql title="schema.sql"
+create table users (
+    id int primary key,
+    name varchar(255) not null unique,
+    // highlight-next-line
+    email varchar(255) not null unique
+);
+```
+
+### Generate a New Migration
+
+Next, run the following command to automatically generate a new migration:
+
+```bash
+atlas migrate diff --env local add_email_column
+```
+
+This will create a new file in the `migrations` directory:
+
+```text
+.
+├── atlas.hcl
+├── migrations
+│   ├── 20231204121249.sql
+// highlight-next-line
+│   ├── 20231206075118_add_email_column.sql
+│   └── atlas.sum
+└── schema.sql
+```
+
+### Create a new Pull Request
+
+Next, create a branch and push the changes to GitHub:
+
+```text
+git checkout -b add-email-column
+git add .
+git commit -m "Add email column"
+git push --set-upstream origin add-email
+```
+
+Next, use the `gh` CLI to create a new pull request:
+
+```shell
+gh pr create --title "migrations: add email column" --body "adding email column to users table"
+```
+
+### Atlas Reviews the Pull Request
+
+Based on the configuration in the GitHub Actions workflow we created, Atlas will automatically
+review your migration directory whenever a new pull request that affects it is opened. When Atlas
+is done running, it will comment on your PR with the results of the review:
+
+![](https://atlasgo.io/uploads/blog/gitops/lint-comment.png)
+
+Interesting! Atlas found some issues with our migration. Let's click on the report to
+see what they are:
+
+![](https://atlasgo.io/uploads/blog/gitops/lint-report.png)
+
+Atlas warns us about two issues. The first is that adding a non-nullable varchar column "email" will fail in
+case the "users" table is not empty. The second is that creating an index non-concurrently causes write locks on the "users" table.
+Since we are in the early stages of development, we can safely ignore these issues for now. Let's merge the pull request
+and see what happens.
+
+```
+gh pr merge --squash
+```
+
+### Atlas Pushes the Migrations to Atlas Cloud
+
+Once GitHub Actions detects that a new push to the `master` branch has been merged, per our configuration,
+it will run the `atlas migrate push` command to push the migrations to Atlas Cloud. Once the push is complete,
+our schema will be updated in the Atlas Cloud schema viewer screen:
+
+![](https://atlasgo.io/uploads/blog/gitops/updated-erd.png)
+
+## Wrapping Up Part One
+
+That's it for part one! In this tutorial, we have shown how to use Atlas Cloud and GitHub Actions to
+create a slick, modern CI/CD pipeline for your database migrations. In part two, we will show how to
+deploy these migrations using the Atlas Operator and ArgoCD to demonstrate a complete GitOps workflow
+for database migrations.
+
+As always, we would love to hear your feedback and suggestions on our [Discord server](https://discord.gg/zZ6sWVg6NT).
diff --git a/doc/website/blog/2023-12-11-gitops-for-databases-part-2.mdx b/doc/website/blog/2023-12-11-gitops-for-databases-part-2.mdx
new file mode 100644
index 00000000..868c067f
--- /dev/null
+++ b/doc/website/blog/2023-12-11-gitops-for-databases-part-2.mdx
@@ -0,0 +1,306 @@
+---
+title: "GitOps for Databases, Part 2: Atlas Operator and ArgoCD"
+authors: rotemtam
+tags: [kubernetes, gitops, migrations, argocd, github-actions, postgres]
+---
+
+:::info
+
+This is the second post in a two-part tutorial, which demonstrates how to use the [Atlas Operator](https://github.com/ariga/atlas-operator) in tandem with
+[Atlas Cloud](https://atlasgo.cloud) and [ArgoCD](https://argo-cd.readthedocs.io/en/stable/) to create a slick,
+modern GitOps workflow for managing your database migrations natively in Kubernetes.
+
+:::
+
+In [part one](2023-12-06-gitops-for-databases-part-1.mdx), we demonstrated how to initialize an Atlas project,
+and create a CI/CD pipeline that automatically plans, verifies and stores your database migrations in Atlas Cloud
+using GitHub Actions.
+
+In this part, we will show how to deploy these migrations using the Atlas Operator and ArgoCD to demonstrate
+a complete GitOps workflow for database migrations.
+
+## How to GitOps your Database Migrations on Kubernetes
+
+> *"We can wrap existing schema management solutions into containers, and run them in Kubernetes as Jobs.
+> But that is SILLY. That is not how we work in Kubernetes."*
+>
+> *-Viktor Farcic, DevOps ToolKit*
+
+As applications evolve, so do their database schemas. The practice of automating the deployment of database schema
+changes has evolved hand in hand with modern devops principles into what is known as
+[database migrations](https://en.wikipedia.org/wiki/Schema_migration). As part of this evolution, hundreds of
+"migration tools" have been created to help developers manage their database migrations. These tools range from
+ORM and language specific tools like [Alembic](https://alembic.sqlalchemy.org/en/latest/) for Python, to language
+agnostic tools like [Flyway](https://flywaydb.org/) and [Liquibase](https://www.liquibase.org/).
+
+When Kubernetes came along and teams started to containerize their applications, the knee-jerk reaction was to
+wrap these legacy tools in a container and run them as part of the application deployment process. We
+discussed some of the shortcomings of this approach in a recent [KubeCon talk](https://www.youtube.com/watch?v=U-o4HUNGHsE)
+and earlier [Webinar](https://www.youtube.com/watch?v=K_Js7hzEyrA).
+
+Atlas was created from the ground up to be a modern database migration tool that embodies modern DevOps principles
+and is designed to run natively in Kubernetes. The [Atlas Operator](https://github.com/ariga/atlas) enables teams
+to extend the native Kubernetes API with new resource types that represent database schemas and migrations. By using
+these capabilities it is possible to natively integrate database migrations into your GitOps workflow.
+
+## Prerequisites
+
+* A running Kubernetes cluster - for learning purposes, you can use
+[Minikube](https://minikube.sigs.k8s.io/docs/start/), which is a tool that runs a single-node
+Kubernetes cluster on your laptop.
+* [kubectl](https://kubernetes.io/docs/tasks/tools/) - a command-line tool for interacting with Kubernetes clusters.
+* [Helm](https://helm.sh/docs/intro/install/) - a package manager for Kubernetes.
+
+## Setting up the Atlas Operator and ArgoCD
+
+### 1. Install ArgoCD
+
+To install ArgoCD run the following commands:
+
+```bash
+kubectl create namespace argocd
+kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
+```
+
+Wait until all the pods in the `argocd` namespace are running:
+
+```bash
+kubectl wait --for=condition=ready pod --all -n argocd
+```
+
+`kubectl` will print something like this:
+
+```bash
+pod/argocd-application-controller-0 condition met
+pod/argocd-applicationset-controller-69dbc8585c-6qbwr condition met
+pod/argocd-dex-server-59f89468dc-xl7rg condition met
+pod/argocd-notifications-controller-55565589db-gnjdh condition met
+pod/argocd-redis-74cb89f466-gzk4f condition met
+pod/argocd-repo-server-68444f6479-mn5gl condition met
+pod/argocd-server-579f659dd5-5djb5 condition met
+```
+
+For more information or if you run into some errors refer to the
+[Argo CD Documentation](https://argo-cd.readthedocs.io/en/stable/getting_started/).
+
+### 2. Install the Atlas Operator
+
+```bash
+helm install atlas-operator oci://ghcr.io/ariga/charts/atlas-operator
+```
+
+Helm will print something like this:
+
+```bash
+Pulled: ghcr.io/ariga/charts/atlas-operator:0.3.6
+Digest: sha256:7e29c15e846fa9c25164f4ad5a7cb7f25e9ead2882082f0352985e58c1976f99
+NAME: atlas-operator
+LAST DEPLOYED: Mon Dec 11 10:25:11 2023
+NAMESPACE: default
+STATUS: deployed
+REVISION: 1
+TEST SUITE: None
+```
+
+Wait until the atlas-operator pod is running:
+
+```bash
+kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=atlas-operator -n default
+```
+
+`kubectl` will print something like this:
+
+```bash
+pod/atlas-operator-866dfbc56d-qkkkn condition met
+```
+
+For more information on the installation process, refer to the [Atlas Operator Documentation](/integrations/kubernetes/operator#getting-started)
+
+### Step 2: Set up the Target Database
+
+Start by deploying a simple PostgreSQL database using the following command:
+
+```bash
+kubectl apply -f https://raw.githubusercontent.com/ariga/atlas-operator/master/config/integration/databases/postgres.yaml
+```
+
+This will create a `Deployment` which runs a single (non-persistent) PostgreSQL instance and a `Service` that
+exposes it on port `5432`. In addition, it will create a `Secret` that contains the database credentials.
+
+Wait until the database pod is running:
+
+```bash
+kubectl wait --for=condition=ready pod -l app=postgres -n default
+```
+
+### Step 3: Create the `AtlasMigration` resource
+
+In order for the Atlas Operator to know which migrations to apply, we need to create an `AtlasMigration` resource
+that points to the Atlas Cloud project we created in [part one](2023-12-06-gitops-for-databases-part-1.mdx).
+Create a new directory called `manifests` in your GitHub repository. In it,
+create a file called `atlas-migration.yaml` with the following contents:
+
+```yaml title="manifests/atlas-migration.yaml"
+apiVersion: db.atlasgo.io/v1alpha1
+kind: AtlasMigration
+metadata:
+  name: migration
+spec:
+  urlFrom:
+    secretKeyRef:
+      key: url
+      name: postgres-credentials
+  cloud:
+    project: "atlasdemo" # Atlas Cloud project name
+    tokenFrom:
+      secretKeyRef:
+        name: atlas-credentials
+        key: token
+  dir:
+    remote:
+      name: "atlasdemo" # Migration directory name in your atlas cloud project
+      tag: "1d579be616db48803bb21713fd836a9165030f18" # See below on how to obtain this value for your project.
+```
+
+This resource tells the Atlas Operator to apply the migrations in the `atlasdemo` project in Atlas Cloud to the
+database specified in the `postgres-credentials` secret. Notice that the `tokenFrom` field references a secret
+called `atlas-credentials`. This secret will contain the Atlas Cloud API token that we created in
+[part one](2023-12-06-gitops-for-databases-part-1.mdx).
+
+To create it run:
+
+```
+kubectl create secret generic atlas-credentials --from-literal=token=aci_<replace with your token>
+```
+
+:::info Obtaining the `tag` field
+
+Notice the `tag` field in the `dir` section. This field tells the Atlas Operator which version of the migrations
+to apply. In this case, we are telling it to apply the migrations tagged with the commit hash
+`1d579be616db48803bb21713fd836a9165030f18` which is the commit hash of the merge commit that merged the pull
+request we created in [part one](2023-12-06-gitops-for-databases-part-1.mdx).
+
+To review which tags are available for your migrations, head over to you Atlas Cloud project and click on the
+`Tags` tab. You should see something like this:
+
+![](https://atlasgo.io/uploads/blog/gitops/dir-tags.png)
+
+:::
+
+Commit and push the changes to your GitHub repository.
+
+### Step 4: Create the ArgoCD Application
+
+Now that we have created the `AtlasMigration` resource, we can create an ArgoCD application that will deploy it.
+Create a file called `Application.yaml` in the root of your GitHub repository with the following contents:
+
+```yaml title="Application.yaml"
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+  name: atlas-argocd-demo
+  namespace: argocd
+  finalizers:
+    - resources-finalizer.argocd.argoproj.io
+spec:
+  source:
+    path: manifests
+    repoURL: 'https://github.com/<your gh user>/<your repo name>'
+    targetRevision: master
+  destination:
+    namespace: default
+    server: 'https://kubernetes.default.svc'
+  project: default
+  syncPolicy:
+    automated:
+      prune: true
+      selfHeal: true
+    retry:
+      limit: 5
+      backoff:
+        duration: 5s
+        maxDuration: 3m0s
+        factor: 2
+    syncOptions:
+      - CreateNamespace=true
+```
+
+Be sure to replace the `repoURL` field with the URL of your GitHub repository.
+
+
+:::info
+
+If your repository is private, you will need to create a [GitHub Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic)
+and tell ArgoCD about it by running the following command:
+
+```bash
+export CURRENT_NS=$(kubectl config view --minify --output 'jsonpath={..namespace}')
+kubectl config set-context --current --namespace=argocd
+argocd repo add https://github.com/<user>/<repo> --username <user> --password ghp_<your token>
+kubectl config set-context --current --namespace=$CURRENT_NS
+```
+
+:::
+
+### 5. Step 5: Deploy!
+
+Next, apply the application manifest:
+
+```bash
+kubectl apply -f Application.yaml
+```
+
+Wait until the application is deployed:
+
+```bash
+kubectl wait --for=condition=ready atlasmigration/migration
+```
+
+Observe the status of the migration object:
+
+```bash
+ kubectl get atlasmigration/migration -o jsonpath='{.status}' | jq
+```
+
+The output will look similar to:
+
+```json
+{
+  "conditions": [
+    {
+      "lastTransitionTime": "2023-12-11T08:38:35Z",
+      "message": "",
+      "reason": "Applied",
+      "status": "True",
+      "type": "Ready"
+    }
+  ],
+  "lastApplied": 1702283914,
+  "lastAppliedVersion": "20231206075118",
+  "observed_hash": "6e4feac15a35d20c38e705428de507835c7c58d487eacc84ed012a17b002981d"
+}
+```
+
+You can also observe the status of the migration using the Atlas Cloud UI:
+
+![](https://atlasgo.io/uploads/blog/gitops/deploy-logs.png)
+
+## Wrapping Up
+
+Let's review the flow that we have created, from end to end:
+* Developers modify the desired state of their schema and use `atlas migrate diff` locally to generate a migration
+plan.
+* Developers commit the migration plan to their GitHub repository and create a pull request.
+* GitHub Actions runs the Atlas Continuous Integration workflow, which verifies the migration plan is correct and safe.
+* Once the pull request is merged, a GitHub Actions workflow pushes the new migration to Atlas Cloud. It is tagged
+with the commit hash of the merge commit.
+* When we are ready to deploy our changes to production, we change the value of the `tag` field in the
+`AtlasMigration` resource to the most recent tag. We push this change to our GitHub repository.
+* ArgoCD detects the change and updates our `AtlasMigration` resource.
+* The Atlas Operator detects the change and applies the migrations to the database.
+* The database is now up to date with the desired state of our schema!
+
+To summarize, in this tutorial we demonstrated how to use the Atlas Operator and ArgoCD to create a slick, modern GitOps workflow
+for managing your database migrations natively in Kubernetes.
+
+As always, we would love to hear your feedback and suggestions on our [Discord server](https://discord.gg/zZ6sWVg6NT).
diff --git a/doc/website/blog/2023-12-12-hibernate-schema-loader.mdx b/doc/website/blog/2023-12-12-hibernate-schema-loader.mdx
new file mode 100644
index 00000000..e5ac542c
--- /dev/null
+++ b/doc/website/blog/2023-12-12-hibernate-schema-loader.mdx
@@ -0,0 +1,418 @@
+---
+title: "Announcing Automatic Migrations for Hibernate Users"
+authors: dorav
+tags: [hibernate, schema, migration]
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+import InstallationInstructions from '../../md/components/_installation_instructions.mdx'
+
+:::info TL;DR
+
+You can now import the desired database schema from your Hibernate project into Atlas,
+and use it to automatically plan migrations for you.
+
+[See an example](#demo-time)
+
+:::
+
+## Introduction
+
+[Hibernate-ORM](https://hibernate.org/orm/documentation/) is one of the most popular ORMs for Java, so much so that parts of it have evolved into
+[the JPA standard](https://docs.jboss.org/hibernate/orm/6.4/introduction/html_single/Hibernate_Introduction.html#hibernate-and-jpa) and the Jakarta APIs.
+
+Today, we are excited to announce that Atlas now supports loading and managing Hibernate schemas.
+
+[Atlas](https://atlasgo.io) is a modern tool for managing your database schema. It allows you to inspect, plan, lint and apply schema changes
+to your database. It is designed to be used by developers, DBAs and DevOps engineers alike.
+
+By using Atlas, Hibernate users can now enjoy these benefits:
+* A declarative migration flow - Atlas can operate like a "Terraform for databases", where
+by running `atlas schema apply` the application schema is applied on a target database.
+* Automatic schema migration planning - Alternatively, Atlas can operate using a more traditional
+versioned migration flow. However, contrary to most tools, Atlas will automatically analyze the diff between
+the migration directory and the current application data model, and will produce correct and
+safe SQL migration files.
+* CI for schema changes - Atlas can be used during CI to make sure you never merge a pull request
+that will break your database schema.
+* Modern CD integrations - Atlas integrates seamlessly with modern deployment tools such as Kubernetes,
+Terraform, Helm, Flux, and ArgoCD. This allows you to deploy changes to your database schema
+as part of your existing deployment pipelines.
+* Visualization - Atlas users can create beautiful, shareable ERDs of their application data model
+with a single command.
+* .. and much more (read more about [Atlas features](/)).
+
+:::note
+Hibernate support is currently in Beta and we would love to hear your feedback :heart:.
+Please reach out to us on [Discord](https://discord.gg/zZ6sWVg6NT) or by opening an [issue](https://github.com/ariga/atlas-provider-hibernate/issues/new).
+:::
+
+## Integrating Atlas into your Hibernate project
+
+Hibernate ships with an automatic schema management tool called [`hbm2ddl`](https://docs.jboss.org/hibernate/orm/6.4/userguide/html_single/Hibernate_User_Guide.html#schema-generation).
+Similarly to Atlas, this tool can inspect a target database and automatically migrate the schema to the desired one. However, the Hibernate team has been advising for years **not** to use this tool in production:
+
+> Although the automatic schema generation is very useful for testing and prototyping purposes, in a production environment, it’s much more flexible to manage the schema using incremental migration scripts.
+
+This is where Atlas comes in. Atlas can read Hibernate schema and plan database schema migrations.
+
+### How does it work?
+
+Atlas compares two database schema states and plans a migration to get from ones state to the other. The database schema can be read directly
+from Hibernate, a migration directory, a database connection, or another ORM.
+
+To read the Hibernate schema, Atlas utilizes the concept of an `external_schema` [datasource](/atlas-schema/projects#data-sources).
+
+## Demo Time
+
+For this demo, we are going to use Gradle, PostgreSQL and this [example project](https://github.com/ariga/atlas-provider-hibernate/tree/master/examples/e2e_java_example).
+
+### Installation
+
+If you haven't already, install the latest version of Atlas:
+
+<InstallationInstructions />
+
+Add the `hibernate-provider` to your project via Gradle or Maven:
+
+<Tabs>
+<TabItem value="Gradle Kotlin" label="Gradle Kotlin" default>
+
+```gradle
+plugins {
+    id("io.atlasgo.hibernate-provider-gradle-plugin") version "0.1"
+}
+```
+
+> To check the installation, run: `./gradlew help --task schema`
+
+</TabItem>
+<TabItem value="Gradle Groovy" label="Gradle Groovy" default>
+
+```gradle
+plugins {
+    id "io.atlasgo.hibernate-provider-gradle-plugin" version "0.1"
+}
+```
+
+> To check the installation, run: `./gradlew help --task schema`
+
+</TabItem>
+<TabItem value="Maven" label="Maven">
+
+```xml
+<build>
+    <pluginManagement>
+        <plugins>
+            <plugin>
+                <groupId>io.atlasgo</groupId>
+                <artifactId>hibernate-provider-maven-plugin</artifactId>
+                <version>0.1</version>
+            </plugin>
+        </plugins>
+    </pluginManagement>
+</build>
+```
+
+> To check the installation, run `mvn help:describe -Dplugin=hibernate-provider -Dgoal=schema`
+
+</TabItem>
+</Tabs>
+
+### Configuration
+
+The plugin adds a configurable Gradle task (or a Maven goal) that prints the Hibernate schema without requiring a database connection.
+However, the task needs to be configured with the database dialect. We can do this by creating a `schema-export.properties` file in the
+resource directory. For example, for MySQL / PostgreSQL:
+
+<Tabs>
+<TabItem value="MySQL" label="MySQL" default>
+
+```properties
+jakarta.persistence.database-product-name=MySQL
+jakarta.persistence.database-major-version=8
+```
+</TabItem>
+<TabItem value="PostgreSQL" label="PostgreSQL">
+
+```properties
+jakarta.persistence.database-product-name=PostgreSQL
+jakarta.persistence.database-major-version=15
+```
+</TabItem>
+</Tabs>
+
+Lastly, we need to configure Atlas to use this configuration by creating an `atlas.hcl` file
+and adding the definition of the Hibernate schema:
+
+<Tabs>
+<TabItem value="Gradle" label="Gradle" default>
+
+```hcl title="atlas.hcl"
+data "external_schema" "hibernate" {
+  program = [
+    "./gradlew",
+    "-q",
+    "schema",
+    "--properties", "schema-export.properties"
+  ]
+}
+```
+</TabItem>
+<TabItem value="Maven" label="Maven">
+
+```hcl title="atlas.hcl"
+data "external_schema" "hibernate" {
+  program = [
+    "./mvn",
+    "-q",
+    "hibernate-provider:schema",
+    "-Dproperties", "schema-export.properties"
+  ]
+}
+```
+
+</TabItem>
+</Tabs>
+
+And the Atlas configuration:
+
+<Tabs>
+<TabItem value="MySQL" label="MySQL" default>
+
+```hcl title="atlas.hcl"
+env "hibernate" {
+  src = data.external_schema.hibernate.url
+  dev = "docker://mysql/8/dev"
+  migration {
+    dir = "file://migrations"
+  }
+  format {
+    migrate {
+      diff = "{{ sql . \"  \" }}"
+    }
+  }
+}
+```
+
+</TabItem>
+<TabItem value="PostgreSQL" label="PostgreSQL">
+
+```hcl title="atlas.hcl"
+env "hibernate" {
+  src = data.external_schema.hibernate.url
+  dev = "docker://postgres/15/dev?search_path=public"
+  migration {
+    dir = "file://migrations"
+  }
+  format {
+    migrate {
+      diff = "{{ sql . \"  \" }}"
+    }
+  }
+}
+```
+
+</TabItem>
+</Tabs>
+
+## Running Atlas
+
+We should now be able to view our schema using Atlas:
+
+```bash
+atlas schema inspect -w --env hibernate --url env://src
+```
+
+The `-w` flag allows us to inspect the schema in [atlas cloud](https://atlasgo.cloud/):
+
+![](https://atlasgo.io/uploads/images/hibernate-schema-inspect.png)
+
+Atlas has many more features we can explore, let's create a migration directory from our schema:
+
+```bash
+atlas migrate diff --env hibernate
+```
+
+By running `atlas migrate diff`, Atlas compares the state of our Hibernate schema and the state of the schema in the `migration` directory.
+Atlas sees that the migration directory does not exist and initializes it with the current Hibernate schema.
+Observe the migration directory, it should contain similar files:
+
+<Tabs>
+<TabItem value="migrations/20231211114848.sql" default>
+
+```sql
+-- Create "movies" table
+-- Create "movies" table
+CREATE TABLE "movies" (
+  "id" bigserial NOT NULL,
+  "numberinseries" integer NULL,
+  "title" character varying(255) NULL,
+  PRIMARY KEY ("id")
+);
+-- Create "actors" table
+CREATE TABLE "actors" (
+  "name" character varying(255) NOT NULL,
+  PRIMARY KEY ("name")
+);
+-- Create "movieparticipation" table
+CREATE TABLE "movieparticipation" (
+  "actorname" character varying(255) NOT NULL,
+  "movieid" bigint NOT NULL,
+  PRIMARY KEY ("actorname", "movieid"),
+  CONSTRAINT "fkaq2kkwvh9870847sm35vtjtiy" FOREIGN KEY ("movieid") REFERENCES "movies" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION,
+  CONSTRAINT "fktm8fbwa577lnbvwdjegwxvget" FOREIGN KEY ("actorname") REFERENCES "actors" ("name") ON UPDATE NO ACTION ON DELETE NO ACTION
+);
+```
+
+</TabItem>
+<TabItem value="migrations/atlas.sum">
+
+```
+h1:Ezh6r7A0pU4XS7PztE87h+aq5PoGGVTd6kaprtpXxas=
+20231211114848.sql h1:9ECs2QsjHE8S4PxXxFcoPYaOVUWRjpxHuF8MGFRd3dE=
+```
+
+</TabItem>
+</Tabs>
+
+Atlas uses the `atlas.sum` file to protect against conflicting schema changes, you can read about it [here](/concepts/migration-directory-integrity).
+
+:::note
+Currently, Atlas does not support using generated fields that require data initialization such as `GenerationType.SEQUENCE`, `GenerationType.TABLE`, and `Generation.AUTO`.
+
+If needed, you can still export the schema using the flag --enable-table-generators (or -Denable-table-generators using Maven). When applying the schema to your database, you will need
+to make sure to apply the ignored statements (using `atlas migrate --env hibernate diff --edit`). See more information on manual migrations [here](/versioned/diff)
+
+For example, if you are adding GenerationType.SEQUENCE to the Event entity, you will need to add insert statements to your generated migration file:
+
+```diff
+diff --git a/migrations/20231210140844.sql b/examples/with_local_plugin_repository/migrations/20231210140844.sql
+index ad80a64..5955834 100644
+--- a/migrations/20231210140844.sql
++++ b/migrations/20231210140844.sql
+@@ -4,3 +4,6 @@ CREATE TABLE `Event` (`id` bigint NOT NULL AUTO_INCREMENT, `title` varchar(255)
+ -- Create "Event_SEQ" table
+ CREATE TABLE `Event_SEQ` (`next_val` bigint NULL) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
++ -- Initialize "Event_SEQ" table
++ insert into Event_SEQ values ( 1 );
+```
+
+Testing these changes can be done by running the application with a local database and creating the entity. To apply the migration directory
+to the local database, use `atlas migrate apply`.
+:::
+
+### Testing the migrations
+
+Now that our migration directory is ready, let's see how to apply it to a target database.
+Let's start a local PostgreSQL instance:
+
+```bash
+docker run -it --rm --name mypostgres -p 5432:5432 -e 'POSTGRES_PASSWORD=password' postgres
+```
+
+Next, let's apply our migrations to the database:
+
+```bash
+atlas migrate apply --env hibernate --url 'postgres://postgres:password@0.0.0.0:5432/?search_path=public&sslmode=disable'
+```
+
+Atlas provides details on the applied migrations:
+
+```
+Migrating to version 20231211121102 (1 migrations in total):
+
+  -- migrating version 20231211121102
+    -> CREATE TABLE "movies" (
+         "id" bigserial NOT NULL,
+         "numberinseries" integer NULL,
+         "title" character varying(255) NULL,
+         PRIMARY KEY ("id")
+       );
+    -> CREATE TABLE "actors" (
+         "name" character varying(255) NOT NULL,
+         PRIMARY KEY ("name")
+       );
+    -> CREATE TABLE "movieparticipation" (
+         "actorname" character varying(255) NOT NULL,
+         "movieid" bigint NOT NULL,
+         PRIMARY KEY ("actorname", "movieid"),
+         CONSTRAINT "fkaq2kkwvh9870847sm35vtjtiy" FOREIGN KEY ("movieid") REFERENCES "movies" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION,
+         CONSTRAINT "fktm8fbwa577lnbvwdjegwxvget" FOREIGN KEY ("actorname") REFERENCES "actors" ("name") ON UPDATE NO ACTION ON DELETE NO ACTION
+       );
+  -- ok (9.282079ms)
+
+  -------------------------
+  -- 54.100203ms
+  -- 1 migrations
+  -- 3 sql statements
+```
+
+To confirm the migrations were applied, we can use Atlas to inspect the database. Run the following command:
+
+```bash
+atlas schema inspect -w --env hibernate --url 'postgres://postgres:password@0.0.0.0:5432/?search_path=public&sslmode=disable'
+````
+
+![](https://atlasgo.io/uploads/images/hibernate-schema-inspect.png)
+
+### Making changes with confidence
+
+Atlas ships with a static code analysis engine that can detect risky schema changes during development or Continuous Integration.
+This functionality is exposed to users via the `migrate lint` command.
+Let's demonstrate this capability with an example.
+
+Suppose we make the following change:
+
+```diff
+--- a/src/main/java/org/example/Movie.java
++++ b/src/main/java/org/example/Movie.java
+@@ -10,13 +10,10 @@ public class Movie {
+
+     Movie(String title, Integer numberInSeries) {
+         this.title = title;
+-        this.numberInSeries = numberInSeries;
+     }
+     @Id
+     @GeneratedValue(strategy = GenerationType.IDENTITY)
+     public Long id;
+
+     public String title;
+-
+-    public Integer numberInSeries;
+ }
+```
+
+By removing an attribute from the `Movie` model, we are removing a column from the database schema.
+Let's see how Atlas handles this change. Run `atlas migrate diff --env hibernate`, and observe the new file in the migration directory:
+```sql title="cat migrations/20231211124321.sql"
+-- Modify "movies" table
+ALTER TABLE "movies" DROP COLUMN "numberinseries";
+```
+
+While this change may be desired, it is an irreversible operation that should be done with caution.
+Atlas can help us avoid dangerous schema changes by linting the migration directory and not allowing such a change to get merged.
+
+Running the following command, we can see that Atlas will warn us about a destructive change to the database:
+
+```bash
+atlas migrate lint --env hibernate --latest 1
+20231211124321.sql: destructive changes detected:
+        L2: Dropping non-virtual column "numberinseries"
+```
+
+Running `migrate lint` locally during development can be very useful, but linting becomes much more powerful when you
+integrate into your Continuous Integration pipeline.
+Atlas offers a set of [Github Actions](/integrations/github-actions) designed to make setting this up a breeze.
+
+## Conclusion
+
+In this post, we have presented how Hibernate projects can use Atlas to automatically
+plan, lint and apply schema migrations based only on their data model.
+
+If you want to explore more configuration options or dive deeper into how this works, please take a look at this [repository](https://github.com/ariga/atlas-provider-hibernate).
+
+#### How can we make Atlas better?
+
+We would love to hear from you [on our Discord server](https://discord.gg/zZ6sWVg6NT) :heart:.
diff --git a/doc/website/blog/authors.yml b/doc/website/blog/authors.yml
index 152ea615..d3eb5c71 100644
--- a/doc/website/blog/authors.yml
+++ b/doc/website/blog/authors.yml
@@ -43,3 +43,6 @@ ronenlu:
   title: Software Engineer
   url: "https://github.com/ronenlu"
   image_url: "https://avatars.githubusercontent.com/u/63970571?v=4"
+dorav:
+  name: Dor Avraham
+  image_url: "https://avatars.githubusercontent.com/u/6278240?v=4"
\ No newline at end of file
diff --git a/doc/website/docusaurus.config.js b/doc/website/docusaurus.config.js
index c2bfad67..2eb878c5 100644
--- a/doc/website/docusaurus.config.js
+++ b/doc/website/docusaurus.config.js
@@ -11,7 +11,7 @@ module.exports = {
   themeConfig: {
     image: "https://og.atlasgo.io/image?title=Schema%20Migration%20Tool%20for%20any%20Language",
     prism: {
-      additionalLanguages: ['hcl'],
+      additionalLanguages: ['hcl', 'java', 'properties', 'gradle'],
       magicComments: [
         {
           className: 'theme-code-block-highlighted-line',
@@ -264,6 +264,7 @@ module.exports = {
       },
     ],
     "./plugins/utm-plugin",
+    "./plugins/intercom",
   ],
   presets: [
     [
diff --git a/doc/website/plugins/intercom/index.js b/doc/website/plugins/intercom/index.js
new file mode 100644
index 00000000..ee25dfad
--- /dev/null
+++ b/doc/website/plugins/intercom/index.js
@@ -0,0 +1,8 @@
+const path = require('path');
+
+module.exports = () => ({
+    name: "intercom-plugin",
+    getClientModules() {
+        return [path.resolve(__dirname, './plugin')];
+    }
+});
diff --git a/doc/website/plugins/intercom/plugin.js b/doc/website/plugins/intercom/plugin.js
new file mode 100644
index 00000000..b0a8dbdf
--- /dev/null
+++ b/doc/website/plugins/intercom/plugin.js
@@ -0,0 +1,14 @@
+import ExecutionEnvironment from "@docusaurus/ExecutionEnvironment";
+
+
+export default (function () {
+    if (!ExecutionEnvironment.canUseDOM) {
+        return null;
+    }
+    window.intercomSettings = {
+        api_base: "https://api-iam.intercom.io",
+        app_id: "ugatw0l1"
+    };
+    (function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',w.intercomSettings);}else{var d=document;var i=function(){i.c(arguments);};i.q=[];i.c=function(args){i.q.push(args);};w.Intercom=i;var l=function(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='https://widget.intercom.io/widget/ugatw0l1';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);};if(document.readyState==='complete'){l();}else if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})();
+    return {};
+})();
diff --git a/doc/website/sidebars.js b/doc/website/sidebars.js
index 4f2eacfd..b6e07be3 100644
--- a/doc/website/sidebars.js
+++ b/doc/website/sidebars.js
@@ -203,6 +203,11 @@ module.exports = {
                     type: 'doc',
                     id: 'guides/deploying/cloud-sql-via-github-actions',
                     label: 'GCP CloudSQL (GH Actions)'
+                },
+                {
+                    type: 'doc',
+                    id: 'guides/deploying/secrets',
+                    label: 'Working with secrets'
                 }
             ]
         },
@@ -291,6 +296,11 @@ module.exports = {
                     id: 'guides/orms/beego',
                     label: 'beego'
                 },
+                {
+                    type: 'doc',
+                    id: 'guides/orms/hibernate',
+                    label: 'Hibernate'
+                },
                 {
                     type: 'doc',
                     id: 'guides/orms/sequelize',
diff --git a/doc/website/src/pages/index.js b/doc/website/src/pages/index.js
index daaa30ad..4c63d97f 100644
--- a/doc/website/src/pages/index.js
+++ b/doc/website/src/pages/index.js
@@ -13,7 +13,7 @@ export default function () {
             ],
             isHidden: true,
         }}
-        projectsAmount={2000}
+        projectsAmount={3000}
       />
     </LayoutProvider>
   );
diff --git a/internal/ci/ci_dialect.tmpl b/internal/ci/ci_dialect.tmpl
index 46858e36..71f6fa99 100644
--- a/internal/ci/ci_dialect.tmpl
+++ b/internal/ci/ci_dialect.tmpl
@@ -3,16 +3,7 @@
 # # # # # # # # # # # # # # # #
 name: CI - Dialect Tests{{ with $.Flavor }} - {{ . }} Edition{{ end }}
 on:
-  pull_request:
-    paths-ignore:
-      - 'doc/**'
-      - 'ops/**'
-  push:
-    branches:
-      - master
-    paths-ignore:
-      - 'doc/**'
-      - 'ops/**'
+  workflow_call:
 {{ .Concurrency }}
 jobs:
 {{- range $.Jobs }}
diff --git a/internal/ci/ci_go.tmpl b/internal/ci/ci_go.tmpl
index 8ec2c0c6..d4bdbdc2 100644
--- a/internal/ci/ci_go.tmpl
+++ b/internal/ci/ci_go.tmpl
@@ -86,4 +86,9 @@ jobs:
           go-version-file: cmd/atlas/go.mod
       - name: Run integration tests for HCL
         working-directory: internal/integration/hclsqlspec
-        run: go test {{ with $.Tags }}-tags={{ . }} {{ end }}-race -count=2 -v ./...
\ No newline at end of file
+        run: go test {{ with $.Tags }}-tags={{ . }} {{ end }}-race -count=2 -v ./...
+
+  dialect-integration:
+    needs: [lint, generate-cmp, unit, cli, integration]
+    uses: ./.github/workflows/ci-dialect_{{ $.Suffix }}.yaml
+    secrets: inherit
diff --git a/internal/ci/main.go b/internal/ci/main.go
index c37fed75..2f52233d 100644
--- a/internal/ci/main.go
+++ b/internal/ci/main.go
@@ -175,10 +175,10 @@ type (
 )
 
 var data = struct {
-	Jobs                 []Job
-	Flavor, Tags, Runner string
-	GoVersions           goVersions
-	Concurrency          concurrency
+	Jobs                         []Job
+	Flavor, Tags, Runner, Suffix string
+	GoVersions                   goVersions
+	Concurrency                  concurrency
 }{
 	Concurrency: concurrency{
 		group:  "${{ github.workflow }}-${{ github.head_ref || github.run_id }}",
@@ -187,18 +187,25 @@ var data = struct {
 }
 
 func main() {
-	var suffix string
 	flag.StringVar(&data.Flavor, "flavor", "", "")
 	flag.StringVar(&data.Tags, "tags", "", "")
 	flag.StringVar(&data.Runner, "runner", "ubuntu-latest", "")
-	flag.StringVar(&suffix, "suffix", "", "")
+	flag.StringVar(&data.Suffix, "suffix", "", "")
 	flag.Parse()
 	for _, n := range []string{"dialect", "go", "revisions"} {
-		var buf bytes.Buffer
+		var (
+			buf bytes.Buffer
+			g   = data.Concurrency.group
+		)
+		if n == "dialect" {
+			// Dialect jobs are running after go jobs, putting them in the same concurrency group crates deadlock.
+			data.Concurrency.group = fmt.Sprintf("%s-dialect", g)
+		}
 		if err := tpl.ExecuteTemplate(&buf, fmt.Sprintf("ci_%s.tmpl", n), data); err != nil {
 			log.Fatalln(err)
 		}
-		err := os.WriteFile(filepath.Clean(fmt.Sprintf("../../.github/workflows/ci-%s_%s.yaml", n, suffix)), buf.Bytes(), 0600)
+		data.Concurrency.group = g
+		err := os.WriteFile(filepath.Clean(fmt.Sprintf("../../.github/workflows/ci-%s_%s.yaml", n, data.Suffix)), buf.Bytes(), 0600)
 		if err != nil {
 			log.Fatalln(err)
 		}
diff --git a/schemahcl/context.go b/schemahcl/context.go
index 085e9bb4..22db75ca 100644
--- a/schemahcl/context.go
+++ b/schemahcl/context.go
@@ -282,11 +282,18 @@ func setBlockVars(ctx *hcl.EvalContext, b *hclsyntax.Body) (*hcl.EvalContext, er
 	if err != nil {
 		return nil, err
 	}
-	if ctx.Variables == nil {
-		ctx.Variables = make(map[string]cty.Value)
-	}
-	for k, v := range vars {
-		ctx.Variables[k] = v
+	switch {
+	case ctx.Variables == nil:
+		ctx.Variables = vars
+	case len(ctx.Variables) >= len(vars):
+		for k, v := range vars {
+			ctx.Variables[k] = v
+		}
+	default:
+		for k, v := range ctx.Variables {
+			vars[k] = v
+		}
+		ctx.Variables = vars
 	}
 	return ctx, nil
 }
@@ -402,13 +409,11 @@ func blocksOfType(blocks hclsyntax.Blocks, typeName string) []*hclsyntax.Block {
 }
 
 func attrMap(attrs hclsyntax.Attributes) map[string]cty.Value {
-	out := make(map[string]cty.Value)
+	out := make(map[string]cty.Value, len(attrs))
 	for _, v := range attrs {
-		value, diag := v.Expr.Value(nil)
-		if diag.HasErrors() {
-			continue
+		if value, diag := v.Expr.Value(nil); !diag.HasErrors() {
+			out[v.Name] = value
 		}
-		out[v.Name] = value
 	}
 	return out
 }
diff --git a/schemahcl/extension.go b/schemahcl/extension.go
index 0a1d9b25..9e1df858 100644
--- a/schemahcl/extension.go
+++ b/schemahcl/extension.go
@@ -547,10 +547,16 @@ func scanAttr(key string, r *Resource, field reflect.Value) error {
 	return nil
 }
 
+// specCache holds a cache for type-spec fields descriptions.
+var specCache sync.Map
+
 // specFields uses reflection to find struct fields that are tagged with "spec"
 // and returns a list of mappings from the tag to the field name.
 func specFields(ext any) []fieldDesc {
 	t := indirect(reflect.TypeOf(ext))
+	if d, ok := specCache.Load(t); ok {
+		return d.([]fieldDesc)
+	}
 	var fields []fieldDesc
 	for i := 0; i < t.NumField(); i++ {
 		f := t.Field(i)
@@ -564,6 +570,7 @@ func specFields(ext any) []fieldDesc {
 		}
 		fields = append(fields, d)
 	}
+	specCache.Store(t, fields)
 	return fields
 }
 
diff --git a/schemahcl/schemahcl.go b/schemahcl/schemahcl.go
index c51ce067..15e8716d 100644
--- a/schemahcl/schemahcl.go
+++ b/schemahcl/schemahcl.go
@@ -6,7 +6,6 @@ package schemahcl
 
 import (
 	"bytes"
-	"errors"
 	"fmt"
 	"reflect"
 	"sort"
@@ -393,13 +392,13 @@ func patchRefs(spec *Resource) error {
 }
 
 func (r addrRef) patch(resource *Resource) error {
-	cp := r.copy().load(resource, "")
+	r.load(resource, "")
 	for _, attr := range resource.Attrs {
 		if !attr.IsRef() {
 			continue
 		}
 		ref := attr.V.EncapsulatedValue().(*Ref)
-		referenced, ok := cp[ref.V]
+		referenced, ok := r[ref.V]
 		if !ok {
 			return fmt.Errorf("broken reference to %q", ref.V)
 		}
@@ -408,21 +407,13 @@ func (r addrRef) patch(resource *Resource) error {
 		}
 	}
 	for _, ch := range resource.Children {
-		if err := cp.patch(ch); err != nil {
+		if err := r.patch(ch); err != nil {
 			return err
 		}
 	}
 	return nil
 }
 
-func (r addrRef) copy() addrRef {
-	n := make(addrRef)
-	for k, v := range r {
-		n[k] = v
-	}
-	return n
-}
-
 // load the references from the children of the resource.
 func (r addrRef) load(res *Resource, track string) addrRef {
 	unlabeled := 0
@@ -456,7 +447,8 @@ func (s *State) resource(ctx *hcl.EvalContext, vr SchemaValidator, file *hcl.Fil
 		return nil, err
 	}
 	res := &Resource{
-		Attrs: attrs,
+		Attrs:    attrs,
+		Children: make([]*Resource, 0, len(body.Blocks)),
 	}
 	for _, blk := range body.Blocks {
 		// variable blocks may be included in the document but are skipped in unmarshaling.
@@ -488,31 +480,15 @@ func (s *State) mayScopeContext(ctx *hcl.EvalContext, scope []string) *hcl.EvalC
 	if !ok1 && !ok2 {
 		return ctx
 	}
-	nctx := &hcl.EvalContext{
-		Variables: make(map[string]cty.Value),
-		Functions: make(map[string]function.Function),
-	}
-	for n, v := range vars {
-		nctx.Variables[n] = v
-	}
-	for n, f := range funcs {
-		nctx.Functions[n] = f
-	}
-	// A patch from the past. Should be moved
-	// to specific scopes in the future.
-	nctx.Functions["sql"] = rawExprImpl()
-	for p := ctx; p != nil; p = p.Parent() {
-		for k, v := range p.Variables {
-			if isRef(v) {
-				nctx.Variables[k] = v
-			}
-		}
-	}
-	return nctx
+	nctx := ctx.NewChild()
+	// Use the same variables/functions maps to avoid copying per scope, but return a
+	// another child context to prevent writes from different blocks to the same maps.
+	nctx.Variables, nctx.Functions = vars, funcs
+	return nctx.NewChild()
 }
 
 func (s *State) toAttrs(ctx *hcl.EvalContext, vr SchemaValidator, hclAttrs hclsyntax.Attributes, scope []string) ([]*Attr, error) {
-	var attrs []*Attr
+	attrs := make([]*Attr, 0, len(hclAttrs))
 	for _, hclAttr := range hclAttrs {
 		var (
 			scope = append(scope, hclAttr.Name)
@@ -528,6 +504,9 @@ func (s *State) toAttrs(ctx *hcl.EvalContext, vr SchemaValidator, hclAttrs hclsy
 		at := &Attr{K: hclAttr.Name}
 		switch t := value.Type(); {
 		case isRef(value):
+			if !value.Type().HasAttribute("__ref") {
+				return nil, fmt.Errorf("%s: invalid reference used in %s", hclAttr.SrcRange, hclAttr.Name)
+			}
 			at.V = cty.CapsuleVal(ctyRefType, &Ref{V: value.GetAttr("__ref").AsString()})
 		case (t.IsTupleType() || t.IsListType() || t.IsSetType()) && value.LengthInt() > 0:
 			var (
@@ -649,6 +628,10 @@ func (s *State) toResource(ctx *hcl.EvalContext, vr SchemaValidator, block *hcls
 	}
 	spec.Attrs = attrs
 	for _, blk := range block.Body.Blocks {
+		ctx, err := setBlockVars(ctx.NewChild(), blk.Body)
+		if err != nil {
+			return nil, err
+		}
 		r, err := s.toResource(ctx, vr, blk, append(scope, blk.Type))
 		if err != nil {
 			return nil, err
@@ -986,23 +969,6 @@ func (f EvalFunc) Eval(p *hclparse.Parser, i any, input map[string]cty.Value) er
 	return f(p, i, input)
 }
 
-func rawExprImpl() function.Function {
-	return function.New(&function.Spec{
-		Params: []function.Parameter{
-			{Name: "def", Type: cty.String, AllowNull: false},
-		},
-		Type: function.StaticReturnType(ctyRawExpr),
-		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
-			x := args[0].AsString()
-			if len(x) == 0 {
-				return cty.NilVal, errors.New("empty expression")
-			}
-			t := &RawExpr{X: x}
-			return cty.CapsuleVal(ctyRawExpr, t), nil
-		},
-	})
-}
-
 // typeFuncSpec returns the HCL function for defining the type in the spec.
 func typeFuncSpec(typeSpec *TypeSpec) function.Function {
 	spec := &function.Spec{
diff --git a/schemahcl/spec.go b/schemahcl/spec.go
index 3cc17fd7..88da25f4 100644
--- a/schemahcl/spec.go
+++ b/schemahcl/spec.go
@@ -384,7 +384,15 @@ func (r *Resource) Resource(t string) (*Resource, bool) {
 
 // Attr returns the Attr by the provided name and reports whether it was found.
 func (r *Resource) Attr(name string) (*Attr, bool) {
-	return attrVal(r.Attrs, name)
+	if at, ok := attrVal(r.Attrs, name); ok {
+		return at, true
+	}
+	for _, r := range r.Children {
+		if at, ok := attrVal(r.Attrs, name); ok && r.Type == "" {
+			return at, true // Match on embedded resource.
+		}
+	}
+	return nil, false
 }
 
 // SetAttr sets the Attr on the Resource. If r is nil, a zero value Resource
diff --git a/schemahcl/stdlib.go b/schemahcl/stdlib.go
index 9760158f..3d6bec8a 100644
--- a/schemahcl/stdlib.go
+++ b/schemahcl/stdlib.go
@@ -5,6 +5,7 @@
 package schemahcl
 
 import (
+	"errors"
 	"fmt"
 	"net/url"
 	"os"
@@ -174,7 +175,7 @@ func stdFuncs() map[string]function.Function {
 		"zipmap":          stdlib.ZipmapFunc,
 		// A patch from the past. Should be moved
 		// to specific scopes in the future.
-		"sql": rawExprImpl(),
+		"sql": rawExprFunc,
 	}
 }
 
@@ -255,6 +256,22 @@ func makeToFunc(wantTy cty.Type) function.Function {
 }
 
 var (
+	// rawExprFunc is a stub function for raw expressions.
+	rawExprFunc = function.New(&function.Spec{
+		Params: []function.Parameter{
+			{Name: "def", Type: cty.String, AllowNull: false},
+		},
+		Type: function.StaticReturnType(ctyRawExpr),
+		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
+			x := args[0].AsString()
+			if len(x) == 0 {
+				return cty.NilVal, errors.New("empty expression")
+			}
+			t := &RawExpr{X: x}
+			return cty.CapsuleVal(ctyRawExpr, t), nil
+		},
+	})
+
 	urlQuerySetFunc = function.New(&function.Spec{
 		Params: []function.Parameter{
 			{
diff --git a/sql/internal/specutil/convert.go b/sql/internal/specutil/convert.go
index 7aeda35f..08b95cf9 100644
--- a/sql/internal/specutil/convert.go
+++ b/sql/internal/specutil/convert.go
@@ -75,8 +75,28 @@ const (
 	typeColumn       = "column"
 	typeSchema       = "schema"
 	typeMaterialized = "materialized"
+	typeFunction     = "function"
+	typeProcedure    = "procedure"
 )
 
+// typeName returns the type name of the given object.
+func typeName(o schema.Object) string {
+	switch o := o.(type) {
+	case *schema.Table:
+		return typeTable
+	case *schema.View:
+		if o.Materialized() {
+			return typeMaterialized
+		}
+		return typeView
+	case *schema.Func:
+		return typeFunction
+	case *schema.Proc:
+		return typeProcedure
+	}
+	return "object"
+}
+
 // Scan populates the Realm from the schemas and table specs.
 func Scan(r *schema.Realm, doc *ScanDoc, funcs *ScanFuncs) error {
 	byName := make(map[string]*schema.Schema)
@@ -88,157 +108,149 @@ func Scan(r *schema.Realm, doc *ScanDoc, funcs *ScanFuncs) error {
 		r.AddSchemas(s1)
 		byName[s.Name] = s1
 	}
-	tableFKs := make(map[*schema.Table][]*sqlspec.ForeignKey)
+	var (
+		fks  = make(map[*schema.Table][]*sqlspec.ForeignKey)
+		deps = make(map[schema.Object][]*schemahcl.Ref, len(doc.Views))
+	)
 	for _, st := range doc.Tables {
 		name, err := SchemaName(st.Schema)
 		if err != nil {
-			return fmt.Errorf("specutil: cannot extract schema name for table %q: %w", st.Name, err)
+			return fmt.Errorf("cannot extract schema name for table %q: %w", st.Name, err)
 		}
 		s, ok := byName[name]
 		if !ok {
-			return fmt.Errorf("specutil: schema %q not found for table %q", name, st.Name)
+			return fmt.Errorf("schema %q not found for table %q", name, st.Name)
 		}
 		t, err := funcs.Table(st, s)
 		if err != nil {
-			return fmt.Errorf("specutil: cannot convert table %q: %w", st.Name, err)
+			return fmt.Errorf("cannot convert table %q: %w", st.Name, err)
 		}
-		tableFKs[t] = st.ForeignKeys
+		fks[t] = st.ForeignKeys
 		s.AddTables(t)
+		if d, ok := st.Attr("depends_on"); ok {
+			refs, err := d.Refs()
+			if err != nil {
+				return fmt.Errorf("expect list of references for attribute table.%s.depends_on: %w", st.Name, err)
+			}
+			deps[t] = refs
+		}
 	}
 	// Link the foreign keys.
-	for t, fks := range tableFKs {
+	for t, fks := range fks {
 		if err := linkForeignKeys(t, fks); err != nil {
 			return err
 		}
 	}
-	viewDeps := make(map[*schema.View][]*schemahcl.Ref, len(doc.Views))
 	for _, sv := range doc.Views {
 		name, err := SchemaName(sv.Schema)
 		if err != nil {
-			return fmt.Errorf("specutil: cannot extract schema name for view %q: %w", sv.Name, err)
+			return fmt.Errorf("cannot extract schema name for view %q: %w", sv.Name, err)
 		}
 		s, ok := byName[name]
 		if !ok {
-			return fmt.Errorf("specutil: schema %q not found for view %q", name, sv.Name)
+			return fmt.Errorf("schema %q not found for view %q", name, sv.Name)
 		}
 		v, err := funcs.View(sv, s)
 		if err != nil {
-			return fmt.Errorf("specutil: cannot convert view %q: %w", sv.Name, err)
+			return fmt.Errorf("cannot convert view %q: %w", sv.Name, err)
 		}
 		s.AddViews(v)
-		if deps, ok := sv.Attr("depends_on"); ok {
-			refs, err := deps.Refs()
+		if d, ok := sv.Attr("depends_on"); ok {
+			refs, err := d.Refs()
 			if err != nil {
-				return fmt.Errorf("specutil: expect list of references for attribute view.%s.depends_on: %w", sv.Name, err)
+				return fmt.Errorf("expect list of references for attribute view.%s.depends_on: %w", sv.Name, err)
 			}
-			viewDeps[v] = refs
+			deps[v] = refs
 		}
 	}
 	for _, m := range doc.Materialized {
 		name, err := SchemaName(m.Schema)
 		if err != nil {
-			return fmt.Errorf("specutil: cannot extract schema name for materialized %q: %w", m.Name, err)
+			return fmt.Errorf("cannot extract schema name for materialized %q: %w", m.Name, err)
 		}
 		s, ok := byName[name]
 		if !ok {
-			return fmt.Errorf("specutil: schema %q not found for materialized %q", name, m.Name)
+			return fmt.Errorf("schema %q not found for materialized %q", name, m.Name)
 		}
 		v, err := funcs.View(m, s)
 		if err != nil {
-			return fmt.Errorf("specutil: cannot convert materialized %q: %w", m.Name, err)
+			return fmt.Errorf("cannot convert materialized %q: %w", m.Name, err)
 		}
 		s.AddViews(v.SetMaterialized(true))
-		if deps, ok := m.Attr("depends_on"); ok {
-			refs, err := deps.Refs()
+		if d, ok := m.Attr("depends_on"); ok {
+			refs, err := d.Refs()
 			if err != nil {
-				return fmt.Errorf("specutil: expect list of references for attribute materialized.%s.depends_on: %w", m.Name, err)
-			}
-			viewDeps[v] = refs
-		}
-	}
-	// Link views' dependencies.
-	for v, refs := range viewDeps {
-		srcT := typeView
-		if v.Materialized() {
-			srcT = typeMaterialized
-		}
-		for i, r := range refs {
-			switch p, err := r.Path(); {
-			case err != nil:
-				return fmt.Errorf("specutil: extract reference for %s.%s: %w", srcT, v.Name, err)
-			case len(p) == 0:
-				return fmt.Errorf("specutil: empty reference for %s.%s", srcT, v.Name)
-			case p[0].T == typeView:
-				q, n, err := RefName(r, typeView)
-				if err != nil {
-					return fmt.Errorf("specutil: extract view name from %s.%s.depends_on[%d]: %w", srcT, v.Name, i, err)
-				}
-				v1, err := findT(v.Schema, q, n, func(s *schema.Schema, name string) (*schema.View, bool) {
-					return s.View(name)
-				})
-				if err != nil {
-					return fmt.Errorf("specutil: find view refrence for %s.%s.depends_on[%d]: %w", srcT, v.Name, i, err)
-				}
-				v.AddDeps(v1)
-			case p[0].T == typeMaterialized:
-				q, n, err := RefName(r, typeMaterialized)
-				if err != nil {
-					return fmt.Errorf("specutil: extract materialized name from %s.%s.depends_on[%d]: %w", srcT, v.Name, i, err)
-				}
-				v1, err := findT(v.Schema, q, n, func(s *schema.Schema, name string) (*schema.View, bool) {
-					return s.Materialized(name)
-				})
-				if err != nil {
-					return fmt.Errorf("specutil: find materialized refrence for %s.%s.depends_on[%d]: %w", srcT, v.Name, i, err)
-				}
-				v.AddDeps(v1)
-			case p[0].T == typeTable:
-				q, n, err := RefName(r, typeTable)
-				if err != nil {
-					return fmt.Errorf("specutil: extract table name from %s.%s.depends_on[%d]: %w", srcT, v.Name, i, err)
-				}
-				t1, err := findT(v.Schema, q, n, func(s *schema.Schema, name string) (*schema.Table, bool) {
-					return s.Table(name)
-				})
-				if err != nil {
-					return fmt.Errorf("specutil: find table refrence for %s.%s.depends_on[%d]: %w", srcT, v.Name, i, err)
-				}
-				v.AddDeps(t1)
+				return fmt.Errorf("expect list of references for attribute materialized.%s.depends_on: %w", m.Name, err)
 			}
+			deps[v] = refs
 		}
 	}
 	if funcs.Func != nil {
 		for _, sf := range doc.Funcs {
 			name, err := SchemaName(sf.Schema)
 			if err != nil {
-				return fmt.Errorf("specutil: cannot extract schema name for function %q: %w", sf.Name, err)
+				return fmt.Errorf("cannot extract schema name for function %q: %w", sf.Name, err)
 			}
 			s, ok := byName[name]
 			if !ok {
-				return fmt.Errorf("specutil: schema %q not found for function %q", name, sf.Name)
+				return fmt.Errorf("schema %q not found for function %q", name, sf.Name)
 			}
 			f, err := funcs.Func(sf)
 			if err != nil {
-				return fmt.Errorf("specutil: cannot convert function %q: %w", sf.Name, err)
+				return fmt.Errorf("cannot convert function %q: %w", sf.Name, err)
 			}
 			s.AddFuncs(f)
+			if d, ok := sf.Attr("depends_on"); ok {
+				refs, err := d.Refs()
+				if err != nil {
+					return fmt.Errorf("expect list of references for attribute function.%s.depends_on: %w", f.Name, err)
+				}
+				deps[f] = refs
+			}
 		}
 	}
 	if funcs.Proc != nil {
 		for _, sf := range doc.Procs {
 			name, err := SchemaName(sf.Schema)
 			if err != nil {
-				return fmt.Errorf("specutil: cannot extract schema name for procedure %q: %w", sf.Name, err)
+				return fmt.Errorf("cannot extract schema name for procedure %q: %w", sf.Name, err)
 			}
 			s, ok := byName[name]
 			if !ok {
-				return fmt.Errorf("specutil: schema %q not found for procedure %q", name, sf.Name)
+				return fmt.Errorf("schema %q not found for procedure %q", name, sf.Name)
 			}
 			f, err := funcs.Proc(sf)
 			if err != nil {
-				return fmt.Errorf("specutil: cannot convert procedure %q: %w", sf.Name, err)
+				return fmt.Errorf("cannot convert procedure %q: %w", sf.Name, err)
 			}
 			s.AddProcs(f)
+			if d, ok := sf.Attr("depends_on"); ok {
+				refs, err := d.Refs()
+				if err != nil {
+					return fmt.Errorf("expect list of references for attribute procedure.%s.depends_on: %w", f.Name, err)
+				}
+				deps[f] = refs
+			}
+		}
+	}
+	for o, refs := range deps {
+		var err error
+		switch o := o.(type) {
+		case *schema.Table:
+			err = fromDependsOn(fmt.Sprintf("table.%s", o.Name), o, o.Schema, refs)
+		case *schema.View:
+			t := "view"
+			if o.Materialized() {
+				t = "materialized"
+			}
+			err = fromDependsOn(fmt.Sprintf("%s.%s", t, o.Name), o, o.Schema, refs)
+		case *schema.Func:
+			err = fromDependsOn(fmt.Sprintf("function.%s", o.Name), o, o.Schema, refs)
+		case *schema.Proc:
+			err = fromDependsOn(fmt.Sprintf("procedure.%s", o.Name), o, o.Schema, refs)
+		}
+		if err != nil {
+			return err
 		}
 	}
 	return nil
@@ -291,11 +303,11 @@ func Table(spec *sqlspec.Table, parent *schema.Schema, convertColumn ConvertTabl
 func View(spec *sqlspec.View, parent *schema.Schema, convertC ConvertViewColumnFunc, convertI ConvertViewIndexFunc) (*schema.View, error) {
 	as, ok := spec.Extra.Attr("as")
 	if !ok {
-		return nil, fmt.Errorf("specutil: missing 'as' definition for view %q", spec.Name)
+		return nil, fmt.Errorf("missing 'as' definition for view %q", spec.Name)
 	}
 	def, err := as.String()
 	if err != nil {
-		return nil, fmt.Errorf("specutil: expect string definition for attribute view.%s.as: %w", spec.Name, err)
+		return nil, fmt.Errorf("expect string definition for attribute view.%s.as: %w", spec.Name, err)
 	}
 	v := schema.NewView(spec.Name, def).SetSchema(parent)
 	for _, c := range spec.Columns {
@@ -318,7 +330,7 @@ func View(spec *sqlspec.View, parent *schema.Schema, convertC ConvertViewColumnF
 	if c, ok := spec.Extra.Attr("check_option"); ok {
 		o, err := c.String()
 		if err != nil {
-			return nil, fmt.Errorf("specutil: expect string definition for attribute view.%s.check_option: %w", spec.Name, err)
+			return nil, fmt.Errorf("expect string definition for attribute view.%s.check_option: %w", spec.Name, err)
 		}
 		v.SetCheckOption(o)
 	}
@@ -601,6 +613,10 @@ func FromTable(t *schema.Table, colFn TableColumnSpecFunc, pkFn PrimaryKeySpecFu
 			spec.Checks = append(spec.Checks, ckFn(c))
 		}
 	}
+	if deps, ok := dependsOn(t.Schema.Realm, t.Deps); ok {
+		// Embedding a resource push its attributes to the end.
+		spec.Extra.Children = append(spec.Extra.Children, &schemahcl.Resource{Attrs: []*schemahcl.Attr{deps}})
+	}
 	convertCommentFromSchema(t.Attrs, &spec.Extra.Attrs)
 	return spec, nil
 }
@@ -644,51 +660,106 @@ func FromView(v *schema.View, colFn ViewColumnSpecFunc, idxFn IndexSpecFunc) (*s
 			embed.Attrs = append(embed.Attrs, schemahcl.StringAttr("check_option", c.V))
 		}
 	}
+	if deps, ok := dependsOn(v.Schema.Realm, v.Deps); ok {
+		embed.Attrs = append(embed.Attrs, deps)
+	}
+	convertCommentFromSchema(v.Attrs, &embed.Attrs)
+	spec.Extra.Children = append(spec.Extra.Children, embed)
+	return spec, nil
+}
+
+// dependsOn returns the depends_on attribute for the given objects.
+func dependsOn(realm *schema.Realm, objects []schema.Object) (*schemahcl.Attr, bool) {
 	var (
-		deps         = make([]*schemahcl.Ref, 0, len(v.Deps))
-		nameT, nameV = make(map[string]int), make(map[string]int)
+		deps  = make([]*schemahcl.Ref, 0, len(objects))
+		names = make(map[string]int)
+		name  = func(t schema.Object, n string) string { return typeName(t) + "/" + n }
 	)
-	// Qualify table/view names if there are
-	// multiple tables/views with the same name.
-	if v.Schema.Realm != nil {
-		for _, s := range v.Schema.Realm.Schemas {
+	// Qualify references if there are objects with the same name.
+	if realm != nil {
+		for _, s := range realm.Schemas {
 			for _, t := range s.Tables {
-				nameT[t.Name]++
+				names[name(t, t.Name)]++
 			}
 			for _, v := range s.Views {
-				nameV[v.Name]++
+				names[name(v, v.Name)]++
+			}
+			for _, f := range s.Funcs {
+				names[name(f, f.Name)]++
+			}
+			for _, p := range s.Procs {
+				names[name(p, p.Name)]++
 			}
 		}
 	}
-	for _, d := range v.Deps {
+	for _, o := range objects {
 		path := make([]string, 0, 2)
-		switch d := d.(type) {
+		var n, s string
+		switch d := o.(type) {
 		case *schema.Table:
-			if nameT[d.Name] > 1 {
-				path = append(path, d.Schema.Name)
-			}
-			deps = append(deps, schemahcl.BuildRef([]schemahcl.PathIndex{
-				{T: typeTable, V: append(path, d.Name)},
-			}))
+			n, s = d.Name, d.Schema.Name
 		case *schema.View:
-			vt := typeView
-			if d.Materialized() {
-				vt = typeMaterialized
-			}
-			if nameV[d.Name] > 1 {
-				path = append(path, d.Schema.Name)
-			}
-			deps = append(deps, schemahcl.BuildRef([]schemahcl.PathIndex{
-				{T: vt, V: append(path, d.Name)},
-			}))
+			n, s = d.Name, d.Schema.Name
+		case *schema.Func:
+			n, s = d.Name, d.Schema.Name
+		case *schema.Proc:
+			n, s = d.Name, d.Schema.Name
 		}
+		if names[name(o, n)] > 1 {
+			path = append(path, s)
+		}
+		deps = append(deps, schemahcl.BuildRef([]schemahcl.PathIndex{
+			{T: typeName(o), V: append(path, n)},
+		}))
 	}
 	if len(deps) > 0 {
-		embed.Attrs = append(embed.Attrs, schemahcl.RefsAttr("depends_on", deps...))
+		return schemahcl.RefsAttr("depends_on", deps...), true
 	}
-	convertCommentFromSchema(v.Attrs, &embed.Attrs)
-	spec.Extra.Children = append(spec.Extra.Children, embed)
-	return spec, nil
+	return nil, false
+}
+
+func fromDependsOn[T interface{ AddDeps(...schema.Object) T }](loc string, t T, ns *schema.Schema, refs []*schemahcl.Ref) error {
+	for i, r := range refs {
+		p, err := r.Path()
+		if err != nil {
+			return fmt.Errorf("extract %s.depends_on references: %w", loc, err)
+		}
+		if len(p) == 0 {
+			return fmt.Errorf("empty reference exists in %s.depends_on[%d]", loc, i)
+		}
+		q, n, err := RefName(r, p[0].T)
+		if err != nil {
+			return fmt.Errorf("extract %s name from %s.depends_on[%d]: %w", p[0].T, loc, i, err)
+		}
+		var o schema.Object
+		switch p[0].T {
+		case typeTable:
+			o, err = findT(ns, q, n, func(s *schema.Schema, name string) (*schema.Table, bool) {
+				return s.Table(name)
+			})
+		case typeView:
+			o, err = findT(ns, q, n, func(s *schema.Schema, name string) (*schema.View, bool) {
+				return s.View(name)
+			})
+		case typeMaterialized:
+			o, err = findT(ns, q, n, func(s *schema.Schema, name string) (*schema.View, bool) {
+				return s.Materialized(name)
+			})
+		case typeFunction:
+			o, err = findT(ns, q, n, func(s *schema.Schema, name string) (*schema.Func, bool) {
+				return s.Func(name)
+			})
+		case typeProcedure:
+			o, err = findT(ns, q, n, func(s *schema.Schema, name string) (*schema.Proc, bool) {
+				return s.Proc(name)
+			})
+		}
+		if err != nil {
+			return fmt.Errorf("find %s refrence for %s.depends_on[%d]: %w", loc, p[0].T, i, err)
+		}
+		t.AddDeps(o)
+	}
+	return nil
 }
 
 // FromPrimaryKey converts schema.Index to a sqlspec.PrimaryKey.
@@ -866,7 +937,7 @@ func FromForeignKey(s *schema.ForeignKey) (*sqlspec.ForeignKey, error) {
 	for _, v := range s.RefColumns {
 		ref := ColumnRef(v.Name)
 		if s.Table != s.RefTable {
-			ref = externalColRef(v.Name, s.RefTable.Name)
+			ref = ExternalColumnRef(v.Name, s.RefTable.Name)
 		}
 		r = append(r, ref)
 	}
@@ -899,7 +970,7 @@ func SchemaName(ref *schemahcl.Ref) (string, error) {
 		return "", err
 	}
 	if len(vs) != 1 {
-		return "", fmt.Errorf("specutil: expected 1 schema ref, got %d", len(vs))
+		return "", fmt.Errorf("expected 1 schema ref, got %d", len(vs))
 	}
 	return vs[0], nil
 }
@@ -911,17 +982,17 @@ func ColumnByRef(t *schema.Table, ref *schemahcl.Ref) (*schema.Column, error) {
 		return nil, err
 	}
 	if len(vs) != 1 {
-		return nil, fmt.Errorf("specutil: expected 1 column ref, got %d", len(vs))
+		return nil, fmt.Errorf("expected 1 column ref, got %d", len(vs))
 	}
 	c, ok := t.Column(vs[0])
 	if !ok {
-		return nil, fmt.Errorf("specutil: unknown column %q in table %q", vs[0], t.Name)
+		return nil, fmt.Errorf("unknown column %q in table %q", vs[0], t.Name)
 	}
 	return c, nil
 }
 
 func externalRef(ref *schemahcl.Ref, sch *schema.Schema) (*schema.Table, *schema.Column, error) {
-	qualifier, name, err := tableName(ref)
+	qualifier, name, err := TableName(ref)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -941,9 +1012,9 @@ func externalRef(ref *schemahcl.Ref, sch *schema.Schema) (*schema.Table, *schema
 // findT finds the table/view referenced by ref in the provided schema. If the table/view
 // is not in the provided schema.Schema other schemas in the connected schema.Realm are
 // searched as well.
-func findT[T schema.View | schema.Table](sch *schema.Schema, qualifier, name string, findT func(*schema.Schema, string) (*T, bool)) (*T, error) {
+func findT[T schema.Object](sch *schema.Schema, qualifier, name string, findT func(*schema.Schema, string) (T, bool)) (t T, err error) {
 	var (
-		matches []*T             // Found references.
+		matches []T              // Found references.
 		schemas []*schema.Schema // Schemas to search.
 	)
 	switch {
@@ -967,13 +1038,15 @@ func findT[T schema.View | schema.Table](sch *schema.Schema, qualifier, name str
 	case 1:
 		return matches[0], nil
 	case 0:
-		return nil, fmt.Errorf("specutil: refrenced table/view %q not found", name)
+		err = fmt.Errorf("refrenced %s %q not found", typeName(t), name)
 	default:
-		return nil, fmt.Errorf("specutil: multiple refrences tables/views found for %q", name)
+		err = fmt.Errorf("multiple refrences %ss found for %q", typeName(t), name)
 	}
+	return
 }
 
-func tableName(ref *schemahcl.Ref) (string, string, error) {
+// TableName returns the qualifier and name from a reference to a table.
+func TableName(ref *schemahcl.Ref) (string, string, error) {
 	return RefName(ref, typeTable)
 }
 
@@ -1005,14 +1078,16 @@ func ColumnRef(cName string) *schemahcl.Ref {
 	})
 }
 
-func externalColRef(cName string, tName string) *schemahcl.Ref {
+// ExternalColumnRef returns the reference of a column by its name and table name.
+func ExternalColumnRef(cName string, tName string) *schemahcl.Ref {
 	return schemahcl.BuildRef([]schemahcl.PathIndex{
 		{T: typeTable, V: []string{tName}},
 		{T: typeColumn, V: []string{cName}},
 	})
 }
 
-func qualifiedExternalColRef(cName, tName, sName string) *schemahcl.Ref {
+// QualifiedExternalColRef returns the reference of a column by its name and qualified table name.
+func QualifiedExternalColRef(cName, tName, sName string) *schemahcl.Ref {
 	return schemahcl.BuildRef([]schemahcl.PathIndex{
 		{T: typeTable, V: []string{sName, tName}},
 		{T: typeColumn, V: []string{cName}},
diff --git a/sql/internal/specutil/spec.go b/sql/internal/specutil/spec.go
index d2fbc9a1..e71d03df 100644
--- a/sql/internal/specutil/spec.go
+++ b/sql/internal/specutil/spec.go
@@ -152,6 +152,32 @@ func QualifyObjects[T SchemaObject](specs []T) error {
 	return nil
 }
 
+// ColumnRefFinder returns a function that finds a column reference by its table and its schema.
+func ColumnRefFinder(specs []*sqlspec.Table) func(s, t, c string) (*schemahcl.Ref, error) {
+	var refs map[struct{ s, t string }]*sqlspec.Table
+	return func(s, t, c string) (*schemahcl.Ref, error) {
+		// Lazily initialize refs.
+		if refs == nil {
+			refs = make(map[struct{ s, t string }]*sqlspec.Table, len(specs))
+			for _, st := range specs {
+				ns, err := SchemaName(st.Schema)
+				if err != nil {
+					return nil, err
+				}
+				refs[struct{ s, t string }{s: ns, t: st.Name}] = st
+			}
+		}
+		r, ok := refs[struct{ s, t string }{s: s, t: t}]
+		if !ok {
+			return nil, fmt.Errorf("table %q.%q was not found", s, t)
+		}
+		if r.Qualifier != "" {
+			return QualifiedExternalColRef(c, r.Name, r.Qualifier), nil
+		}
+		return ExternalColumnRef(c, r.Name), nil
+	}
+}
+
 // QualifyReferences qualifies any reference with qualifier.
 func QualifyReferences(tableSpecs []*sqlspec.Table, realm *schema.Realm) error {
 	type cref struct{ s, t string }
@@ -183,9 +209,9 @@ func QualifyReferences(tableSpecs []*sqlspec.Table, realm *schema.Realm) error {
 			}
 			for i, c := range fk.RefColumns {
 				if r, ok := byRef[cref{s: fk1.RefTable.Schema.Name, t: fk1.RefTable.Name}]; ok && r.Qualifier != "" {
-					fk.RefColumns[i] = qualifiedExternalColRef(fk1.RefColumns[i].Name, r.Name, r.Qualifier)
+					fk.RefColumns[i] = QualifiedExternalColRef(fk1.RefColumns[i].Name, r.Name, r.Qualifier)
 				} else if r, ok := byRef[cref{t: fk1.RefTable.Name}]; ok && r.Qualifier == "" {
-					fk.RefColumns[i] = externalColRef(fk1.RefColumns[i].Name, r.Name)
+					fk.RefColumns[i] = ExternalColumnRef(fk1.RefColumns[i].Name, r.Name)
 				} else {
 					return fmt.Errorf("missing reference for column %q in %q.%q.%q", c.V, sname, t.Name, fk.Symbol)
 				}
diff --git a/sql/internal/sqlx/diff.go b/sql/internal/sqlx/diff.go
index 22efcade..ebe429dc 100644
--- a/sql/internal/sqlx/diff.go
+++ b/sql/internal/sqlx/diff.go
@@ -65,6 +65,12 @@ type (
 		ReferenceChanged(from, to schema.ReferenceOption) bool
 	}
 
+	// DropSchemaChanger is an optional interface allows DiffDriver to drop
+	// schema objects before dropping the schema itself.
+	DropSchemaChanger interface {
+		DropSchemaChange(*schema.Schema) []schema.Change
+	}
+
 	// A Normalizer wraps the Normalize method for normalizing the from and to tables before
 	// running diffing. The "from" usually represents the inspected database state (current),
 	// and the second represents the desired state.
@@ -107,7 +113,12 @@ func (d *Diff) RealmDiff(from, to *schema.Realm, options ...schema.DiffOption) (
 	for _, s1 := range from.Schemas {
 		s2, ok := to.Schema(s1.Name)
 		if !ok {
-			changes = opts.AddOrSkip(changes, &schema.DropSchema{S: s1})
+			if ds, ok := d.DiffDriver.(DropSchemaChanger); ok {
+				// The driver can drop other objects before dropping the schema.
+				changes = opts.AddOrSkip(changes, ds.DropSchemaChange(s1)...)
+			} else {
+				changes = opts.AddOrSkip(changes, &schema.DropSchema{S: s1})
+			}
 			continue
 		}
 		change, err := d.schemaDiff(s1, s2, opts)
@@ -122,6 +133,15 @@ func (d *Diff) RealmDiff(from, to *schema.Realm, options ...schema.DiffOption) (
 			continue
 		}
 		changes = opts.AddOrSkip(changes, &schema.AddSchema{S: s1})
+		for _, o := range s1.Objects {
+			changes = opts.AddOrSkip(changes, &schema.AddObject{O: o})
+		}
+		for _, f := range s1.Funcs {
+			changes = opts.AddOrSkip(changes, &schema.AddFunc{F: f})
+		}
+		for _, p := range s1.Procs {
+			changes = opts.AddOrSkip(changes, &schema.AddProc{P: p})
+		}
 		for _, t := range s1.Tables {
 			changes = opts.AddOrSkip(changes, &schema.AddTable{T: t})
 		}
diff --git a/sql/internal/sqlx/plan.go b/sql/internal/sqlx/plan.go
index 54864d53..22ed8e98 100644
--- a/sql/internal/sqlx/plan.go
+++ b/sql/internal/sqlx/plan.go
@@ -9,6 +9,7 @@ import (
 	"database/sql"
 	"errors"
 	"fmt"
+	"slices"
 	"sort"
 
 	"ariga.io/atlas/sql/migrate"
@@ -380,3 +381,324 @@ func byKeys[T any](m map[string]T) []struct {
 	})
 	return vs
 }
+
+// SortChanges is a helper function to sort to level changes based on their priority.
+func SortChanges(changes []schema.Change) []schema.Change {
+	var views, drop, other []schema.Change
+	for _, c := range changes {
+		switch c.(type) {
+		case *schema.AddView, *schema.DropView, *schema.ModifyView:
+			views = append(views, c)
+		case *schema.DropSchema, *schema.DropTable, *schema.DropFunc, *schema.DropProc, *schema.DropObject:
+			drop = append(drop, c)
+		default:
+			other = append(other, c)
+		}
+	}
+	if planned, err := sortViewChanges(views); err == nil { // no cycles.
+		views = planned
+	}
+	// To keep backwards compatibility with previous sorting and also in case we miss any dependency between changes
+	// (see, dependsOn function) we push views and drop changes to the end, unless there is a dependency requirement.
+	changes = append(other, append(views, drop...)...)
+	edges := make(map[schema.Change][]schema.Change)
+	for _, c := range changes {
+		for _, c2 := range changes {
+			if c != c2 && dependsOn(c, c2) {
+				edges[c] = append(edges[c], c2)
+			}
+		}
+	}
+	var (
+		add     func(schema.Change)
+		added   = make(map[schema.Change]bool)
+		planned = make([]schema.Change, 0, len(changes))
+	)
+	add = func(c schema.Change) {
+		if added[c] {
+			return
+		}
+		added[c] = true
+		for _, d := range edges[c] {
+			if !added[d] {
+				add(d)
+			}
+		}
+		planned = append(planned, c)
+	}
+	for _, c := range changes {
+		if !added[c] {
+			add(c)
+		}
+	}
+	return planned
+}
+
+// Depender can be implemented by an object to determine if a change to it
+// depends on other change, or if an other change depends on it. For example:
+// A table creation depends on type creation, and a type deletion depends on
+// table deletion.
+type Depender interface {
+	DependsOn(change, other schema.Change) bool
+	DependencyOf(change, other schema.Change) bool
+}
+
+// dependsOn reports if the given change depends on the other change.
+func dependsOn(c1, c2 schema.Change) bool {
+	if dependOnOf(c1, c2) {
+		return true
+	}
+	switch c1 := c1.(type) {
+	case *schema.DropSchema:
+		switch c2 := c2.(type) {
+		case *schema.DropFunc:
+			return c1.S.Name == c2.F.Schema.Name
+		case *schema.DropProc:
+			return c1.S.Name == c2.P.Schema.Name
+		case *schema.DropTable:
+			return c1.S.Name == c2.T.Schema.Name
+		case *schema.DropView:
+			return c1.S.Name == c2.V.Schema.Name
+		}
+	case *schema.AddTable:
+		switch c2 := c2.(type) {
+		case *schema.AddSchema:
+			return c1.T.Schema.Name == c2.S.Name
+		case *schema.DropTable:
+			return c1.T.Name == c2.T.Name && c1.T.Schema == c2.T.Schema // Table recreation.
+		case *schema.AddTable:
+			if refTo(c1.T.ForeignKeys, c2.T) {
+				return true
+			}
+		case *schema.ModifyTable:
+			if refTo(c1.T.ForeignKeys, c2.T) {
+				return true
+			}
+		case *schema.AddObject:
+			t, ok := c2.O.(schema.Type)
+			if ok && slices.ContainsFunc(c1.T.Columns, func(c *schema.Column) bool {
+				return dependsOnT(c.Type.Type, t)
+			}) {
+				return true
+			}
+		}
+		return depOfAdd(c1.T.Deps, c2)
+	case *schema.DropTable:
+		// If it is a drop of a table, the change must occur
+		// after all resources that rely on it will be dropped.
+		switch c2 := c2.(type) {
+		case *schema.DropTable:
+			if refTo(c2.T.ForeignKeys, c1.T) {
+				return true
+			}
+		case *schema.ModifyTable:
+			return slices.ContainsFunc(c2.Changes, func(c schema.Change) bool {
+				fk, ok := c.(*schema.DropForeignKey)
+				return ok && fk.F.RefTable == c2.T
+			})
+		}
+		return depOfDrop(c1.T, c2)
+	case *schema.ModifyTable:
+		switch c2 := c2.(type) {
+		case *schema.AddTable:
+			// Table modification relies on its creation.
+			if c1.T.Name == c2.T.Name && c1.T.Schema == c2.T.Schema {
+				return true
+			}
+			// Tables need to be created before referencing them.
+			return slices.ContainsFunc(c1.Changes, func(c schema.Change) bool {
+				fk, ok := c.(*schema.AddForeignKey)
+				return ok && fk.F.RefTable == c2.T
+			})
+		case *schema.ModifyTable:
+			if c1.T != c2.T {
+				addC := make(map[*schema.Column]bool)
+				for _, c := range c2.Changes {
+					if add, ok := c.(*schema.AddColumn); ok {
+						addC[add.C] = true
+					}
+				}
+				return slices.ContainsFunc(c1.Changes, func(c schema.Change) bool {
+					fk, ok := c.(*schema.AddForeignKey)
+					return ok && fk.F.RefTable == c2.T && slices.ContainsFunc(fk.F.RefColumns, func(c *schema.Column) bool { return addC[c] })
+				})
+			}
+		case *schema.AddObject:
+			t, ok := c2.O.(schema.Type)
+			if ok && slices.ContainsFunc(c1.Changes, func(c schema.Change) bool {
+				switch c := c.(type) {
+				case *schema.AddColumn:
+					return dependsOnT(c.C.Type.Type, t)
+				case *schema.ModifyColumn:
+					return dependsOnT(c.To.Type.Type, t)
+				default:
+					return false
+				}
+			}) {
+				return true
+			}
+		}
+		return depOfAdd(c1.T.Deps, c2)
+	case *schema.AddView:
+		switch c2 := c2.(type) {
+		case *schema.AddSchema:
+			return c1.V.Schema.Name == c2.S.Name
+		case *schema.DropView:
+			return c1.V.Name == c2.V.Name && c1.V.Schema == c2.V.Schema // View recreation.
+		default:
+			return depOfAdd(c1.V.Deps, c2)
+		}
+	case *schema.DropView:
+		return depOfDrop(c1.V, c2)
+	case *schema.ModifyView:
+		if c2, ok := c2.(*schema.AddView); ok {
+			// View modification relies on its creation.
+			return c1.From.Name == c2.V.Name && c1.From.Schema == c2.V.Schema
+		}
+		return depOfAdd(c1.To.Deps, c2)
+	case *schema.AddFunc:
+		switch c2 := c2.(type) {
+		case *schema.AddSchema:
+			return c1.F.Schema.Name == c2.S.Name
+		case *schema.DropFunc:
+			return c1.F.Name == c2.F.Name && c1.F.Schema == c2.F.Schema // Func recreation.
+		default:
+			return depOfAdd(c1.F.Deps, c2)
+		}
+	case *schema.DropFunc:
+		return depOfDrop(c1.F, c2)
+	case *schema.ModifyFunc:
+		if c2, ok := c2.(*schema.AddFunc); ok {
+			// Func modification relies on its creation.
+			return c1.From.Name == c2.F.Name && c1.From.Schema == c2.F.Schema
+		}
+		return depOfAdd(c1.To.Deps, c2)
+	case *schema.AddProc:
+		switch c2 := c2.(type) {
+		case *schema.AddSchema:
+			return c1.P.Schema.Name == c2.S.Name
+		case *schema.DropProc:
+			return c1.P.Name == c2.P.Name && c1.P.Schema == c2.P.Schema // Proc recreation.
+		default:
+			return depOfAdd(c1.P.Deps, c2)
+		}
+	case *schema.DropProc:
+		return depOfDrop(c1.P, c2)
+	case *schema.ModifyProc:
+		if c2, ok := c2.(*schema.AddProc); ok {
+			// Proc modification relies on its creation.
+			return c1.From.Name == c2.P.Name && c1.From.Schema == c2.P.Schema
+		}
+		return depOfAdd(c1.To.Deps, c2)
+	case *schema.DropObject:
+		t, ok := c1.O.(schema.Type)
+		if !ok {
+			return false
+		}
+		// Dropping a type must occur after all its usage were dropped.
+		switch c2 := c2.(type) {
+		case *schema.DropTable:
+			return slices.ContainsFunc(c2.T.Columns, func(c *schema.Column) bool {
+				return dependsOnT(c.Type.Type, t)
+			})
+		case *schema.ModifyTable:
+			return slices.ContainsFunc(c2.Changes, func(c schema.Change) bool {
+				d, ok := c.(*schema.DropColumn)
+				return ok && dependsOnT(d.C.Type.Type, t)
+			})
+		case *schema.DropFunc:
+			return c2.F.Ret == t || slices.ContainsFunc(c2.F.Args, func(f *schema.FuncArg) bool {
+				return dependsOnT(f.Type, t)
+			})
+		case *schema.DropProc:
+			return slices.ContainsFunc(c2.P.Args, func(f *schema.FuncArg) bool {
+				return dependsOnT(f.Type, t)
+			})
+		}
+	}
+	return false
+}
+
+// dependOnOf checks if the given change depends on the other change or
+// vice versa based on their underlying object implementation.
+func dependOnOf(change, other schema.Change) bool {
+	switch change := change.(type) {
+	case *schema.AddObject:
+		if d, ok := change.O.(Depender); ok && d.DependsOn(change, other) {
+			return true
+		}
+	case *schema.ModifyObject:
+		if d, ok := change.To.(Depender); ok && d.DependsOn(change, other) {
+			return true
+		}
+	case *schema.DropObject:
+		if d, ok := change.O.(Depender); ok && d.DependsOn(change, other) {
+			return true
+		}
+	}
+	switch other := other.(type) {
+	case *schema.AddObject:
+		if d, ok := other.O.(Depender); ok && d.DependencyOf(other, change) {
+			return true
+		}
+	case *schema.ModifyObject:
+		if d, ok := other.To.(Depender); ok && d.DependencyOf(other, change) {
+			return true
+		}
+	case *schema.DropObject:
+		if d, ok := other.O.(Depender); ok && d.DependencyOf(other, change) {
+			return true
+		}
+	}
+	return false
+}
+
+// depOfDrops checks if the given object is a dependency of the given change.
+func depOfDrop(o schema.Object, c schema.Change) bool {
+	var deps []schema.Object
+	switch c := c.(type) {
+	case *schema.DropTable:
+		deps = c.T.Deps
+	case *schema.DropView:
+		deps = c.V.Deps
+	case *schema.DropFunc:
+		deps = c.F.Deps
+	case *schema.DropProc:
+		deps = c.P.Deps
+	}
+	return slices.Contains(deps, o)
+}
+
+// depOfAdd checks if the given change is a creation of a resource exists in the given list.
+func depOfAdd(refs []schema.Object, c schema.Change) bool {
+	var o schema.Object
+	switch c := c.(type) {
+	case *schema.AddTable:
+		o = c.T
+	case *schema.ModifyTable:
+		o = c.T
+	case *schema.AddView:
+		o = c.V
+	case *schema.AddFunc:
+		o = c.F
+	case *schema.AddProc:
+		o = c.P
+	case *schema.AddObject:
+		o = c.O
+	default:
+		return false
+	}
+	return slices.Contains(refs, o)
+}
+
+// refTo reports if the given foreign keys reference the given table.
+func refTo(fks []*schema.ForeignKey, to *schema.Table) bool {
+	return slices.ContainsFunc(fks, func(fk *schema.ForeignKey) bool { return fk.RefTable == to })
+}
+
+// dependsOnT reports if t1 depends on t2.
+func dependsOnT(t1, t2 schema.Type) bool {
+	// Comparing might panic due to mismatch types.
+	defer func() { recover() }()
+	return t1 == t2 || schema.UnderlyingType(t1) == t2
+}
diff --git a/sql/internal/sqlx/sqlx.go b/sql/internal/sqlx/sqlx.go
index 6f8b2699..3e7786d2 100644
--- a/sql/internal/sqlx/sqlx.go
+++ b/sql/internal/sqlx/sqlx.go
@@ -224,7 +224,7 @@ func ValuesEqual(v1, v2 []string) bool {
 // ModeInspectSchema returns the InspectMode or its default.
 func ModeInspectSchema(o *schema.InspectOptions) schema.InspectMode {
 	if o == nil || o.Mode == 0 {
-		return schema.InspectSchemas | schema.InspectTables | schema.InspectViews | schema.InspectFuncs | schema.InspectTypes
+		return schema.InspectSchemas | schema.InspectTables | schema.InspectViews | schema.InspectFuncs | schema.InspectTypes | schema.InspectObjects
 	}
 	return o.Mode
 }
@@ -232,7 +232,7 @@ func ModeInspectSchema(o *schema.InspectOptions) schema.InspectMode {
 // ModeInspectRealm returns the InspectMode or its default.
 func ModeInspectRealm(o *schema.InspectRealmOption) schema.InspectMode {
 	if o == nil || o.Mode == 0 {
-		return schema.InspectSchemas | schema.InspectTables | schema.InspectViews | schema.InspectFuncs | schema.InspectTypes
+		return schema.InspectSchemas | schema.InspectTables | schema.InspectViews | schema.InspectFuncs | schema.InspectTypes | schema.InspectObjects
 	}
 	return o.Mode
 }
@@ -265,6 +265,11 @@ func (b *Builder) P(phrases ...string) *Builder {
 	return b
 }
 
+// Int64 writes the given value to the builder in base 10.
+func (b *Builder) Int64(v int64) *Builder {
+	return b.P(strconv.FormatInt(v, 10))
+}
+
 // Ident writes the given string quoted as an SQL identifier.
 func (b *Builder) Ident(s string) *Builder {
 	if s != "" {
@@ -288,6 +293,12 @@ func (b *Builder) Table(t *schema.Table) *Builder {
 	return b.mayQualify(t.Schema, t.Name)
 }
 
+// TableColumn writes the table's resource identifier to the builder, prefixed
+// with the schema name if exists.
+func (b *Builder) TableColumn(t *schema.Table, c *schema.Column) *Builder {
+	return b.mayQualify(t.Schema, t.Name, c.Name)
+}
+
 // Func writes the function identifier to the builder, prefixed
 // with the schema name if exists.
 func (b *Builder) Func(f *schema.Func) *Builder {
@@ -305,7 +316,7 @@ func (b *Builder) Proc(p *schema.Proc) *Builder {
 func (b *Builder) TableResource(t *schema.Table, r any) *Builder {
 	switch c := r.(type) {
 	case *schema.Column:
-		return b.mayQualify(t.Schema, t.Name, c.Name)
+		return b.TableColumn(t, c)
 	case *schema.Index:
 		return b.mayQualify(t.Schema, t.Name, c.Name)
 	default:
diff --git a/sql/internal/sqlx/sqlx_oss.go b/sql/internal/sqlx/sqlx_oss.go
index f8820d42..11ee9ecc 100644
--- a/sql/internal/sqlx/sqlx_oss.go
+++ b/sql/internal/sqlx/sqlx_oss.go
@@ -10,8 +10,7 @@ import (
 	"ariga.io/atlas/sql/schema"
 )
 
-// PlanViewChanges (should) plan view changes in the current order they should be applied.
-// It is unimplemented for community version as views are not supported there.
-func PlanViewChanges(changes []schema.Change) ([]schema.Change, error) {
+// sortViewChanges is an optional function to sort to views by their dependencies.
+func sortViewChanges(changes []schema.Change) ([]schema.Change, error) {
 	return changes, nil // unimplemented.
 }
diff --git a/sql/migrate/lex.go b/sql/migrate/lex.go
index 778935e4..a1fb5a83 100644
--- a/sql/migrate/lex.go
+++ b/sql/migrate/lex.go
@@ -44,13 +44,79 @@ func (s *Stmt) Directive(name string) (ds []string) {
 
 // Stmts provides a generic implementation for extracting SQL statements from the given file contents.
 func Stmts(input string) ([]*Stmt, error) {
-	var stmts []*Stmt
-	l, err := newLex(input)
+	return (&Scanner{
+		ScannerOptions: ScannerOptions{
+			// Default options for backward compatibility.
+			MatchBegin:       false,
+			MatchBeginAtomic: true,
+			MatchDollarQuote: true,
+		},
+	}).Scan(input)
+}
+
+// FileStmtDecls scans atlas-format file statements using
+// the Driver implementation, if implemented.
+func FileStmtDecls(drv Driver, f File) ([]*Stmt, error) {
+	s, ok1 := drv.(StmtScanner)
+	_, ok2 := f.(*LocalFile)
+	if !ok1 || !ok2 {
+		return f.StmtDecls()
+	}
+	return s.ScanStmts(string(f.Bytes()))
+}
+
+// FileStmts is like FileStmtDecls but returns only the
+// statement text without the extra info.
+func FileStmts(drv Driver, f File) ([]string, error) {
+	s, err := FileStmtDecls(drv, f)
 	if err != nil {
 		return nil, err
 	}
+	stmts := make([]string, len(s))
+	for i := range s {
+		stmts[i] = s[i].Text
+	}
+	return stmts, nil
+}
+
+type (
+	// StmtScanner interface for scanning SQL statements from migration
+	// and schema files and can be optionally implemented by drivers.
+	StmtScanner interface {
+		ScanStmts(input string) ([]*Stmt, error)
+	}
+
+	// Scanner scanning SQL statements from migration and schema files.
+	Scanner struct {
+		ScannerOptions
+		// scanner state.
+		src, input string   // src and current input text
+		pos        int      // current phase position
+		total      int      // total bytes scanned so far
+		width      int      // size of latest rune
+		delim      string   // configured delimiter
+		comments   []string // collected comments
+	}
+
+	// ScannerOptions controls the behavior of the scanner.
+	ScannerOptions struct {
+		// MatchBegin enables matching for BEGIN ... END statements block.
+		MatchBegin bool
+		// MatchBeginAtomic enables matching for BEGIN ATOMIC ... END statements block.
+		MatchBeginAtomic bool
+		// MatchDollarQuote enables the PostgreSQL dollar-quoted string syntax.
+		MatchDollarQuote bool
+	}
+)
+
+// Scan scans the statement in the given input.
+func (s *Scanner) Scan(input string) ([]*Stmt, error) {
+	var stmts []*Stmt
+	if err := s.init(input); err != nil {
+		return nil, err
+	}
 	for {
-		s, err := l.stmt()
+		s, err := s.stmt()
 		if err == io.EOF {
 			return stmts, nil
 		}
@@ -61,277 +127,304 @@ func Stmts(input string) ([]*Stmt, error) {
 	}
 }
 
-type lex struct {
-	src, input string   // src and current input text
-	pos        int      // current phase position
-	total      int      // total bytes scanned so far
-	width      int      // size of latest rune
-	delim      string   // configured delimiter
-	comments   []string // collected comments
-}
-
-const (
-	eos          = -1
-	delimiter    = ";"
-	delimiterCmd = "delimiter"
-)
-
-func newLex(input string) (*lex, error) {
-	l := &lex{src: input, input: input, delim: delimiter}
+// init initializes the scanner state.
+func (s *Scanner) init(input string) error {
+	s.comments = nil
+	s.pos, s.total, s.width = 0, 0, 0
+	s.src, s.input, s.delim = input, input, delimiter
 	if d, ok := directive(input, directiveDelimiter, directivePrefixSQL); ok {
-		if err := l.setDelim(d); err != nil {
-			return nil, err
+		if err := s.setDelim(d); err != nil {
+			return err
 		}
 		parts := strings.SplitN(input, "\n", 2)
 		if len(parts) == 1 {
-			return nil, l.error(l.pos, "no input found after delimiter %q", d)
+			return s.error(s.pos, "no input found after delimiter %q", d)
 		}
-		l.input = parts[1]
+		s.input = parts[1]
 	}
-	return l, nil
+	return nil
 }
 
+const (
+	eos          = -1
+	delimiter    = ";"
+	delimiterCmd = "delimiter"
+)
+
 var (
 	// Dollar-quoted string as defined by the PostgreSQL scanner.
 	reDollarQuote = regexp.MustCompile(`^\$([A-Za-zÈ-ÿ_][\wÈ-ÿ]*)*\$`)
 	// The 'BEGIN ATOMIC' syntax as specified in the SQL 2003 standard.
 	reBeginAtomic = regexp.MustCompile(`(?i)^\s*BEGIN\s+ATOMIC\s+`)
+	reBegin       = regexp.MustCompile(`(?i)^\s*BEGIN\s+`)
 	reEnd         = regexp.MustCompile(`(?i)^\s*END\s*`)
 )
 
-func (l *lex) stmt() (*Stmt, error) {
+func (s *Scanner) stmt() (*Stmt, error) {
 	var (
 		depth, openingPos int
 		text              string
 	)
-	l.skipSpaces()
+	s.skipSpaces()
 Scan:
 	for {
-		switch r := l.next(); {
+		switch r := s.next(); {
 		case r == eos:
 			switch {
 			case depth > 0:
-				return nil, l.error(openingPos, "unclosed '('")
-			case l.pos > 0:
-				text = l.input
+				return nil, s.error(openingPos, "unclosed '('")
+			case s.pos > 0:
+				text = s.input
 				break Scan
 			default:
 				return nil, io.EOF
 			}
 		case r == '(':
 			if depth == 0 {
-				openingPos = l.pos
+				openingPos = s.pos
 			}
 			depth++
 		case r == ')':
 			if depth == 0 {
-				return nil, l.error(l.pos, "unexpected ')'")
+				return nil, s.error(s.pos, "unexpected ')'")
 			}
 			depth--
 		case r == '\'', r == '"', r == '`':
-			if err := l.skipQuote(r); err != nil {
+			if err := s.skipQuote(r); err != nil {
 				return nil, err
 			}
 		// Check if the start of the statement is the MySQL DELIMITER command.
 		// See https://dev.mysql.com/doc/refman/8.0/en/mysql-commands.html.
-		case l.pos == 1 && len(l.input) > len(delimiterCmd) && strings.EqualFold(l.input[:len(delimiterCmd)], delimiterCmd):
-			l.addPos(len(delimiterCmd) - 1)
-			if err := l.delimCmd(); err != nil {
+		case s.pos == 1 && len(s.input) > len(delimiterCmd) && strings.EqualFold(s.input[:len(delimiterCmd)], delimiterCmd):
+			s.addPos(len(delimiterCmd) - 1)
+			if err := s.delimCmd(); err != nil {
 				return nil, err
 			}
 		// Delimiters take precedence over comments.
-		case depth == 0 && strings.HasPrefix(l.input[l.pos-l.width:], l.delim):
-			l.addPos(len(l.delim) - l.width)
-			text = l.input[:l.pos]
+		case depth == 0 && strings.HasPrefix(s.input[s.pos-s.width:], s.delim):
+			s.addPos(len(s.delim) - s.width)
+			text = s.input[:s.pos]
 			break Scan
-		case r == '$' && reDollarQuote.MatchString(l.input[l.pos-1:]):
-			if err := l.skipDollarQuote(); err != nil {
+		case s.MatchDollarQuote && r == '$' && reDollarQuote.MatchString(s.input[s.pos-1:]):
+			if err := s.skipDollarQuote(); err != nil {
 				return nil, err
 			}
 		// Skip non-standard MySQL comments if they are inside
 		// expressions until we make the lexer driver-aware.
 		case depth == 0 && r == '#':
-			l.comment("#", "\n")
-		case r == '-' && l.next() == '-':
-			l.comment("--", "\n")
-		case r == '/' && l.next() == '*':
-			l.comment("/*", "*/")
-		case reBeginAtomic.MatchString(l.input[l.pos-1:]):
-			if err := l.skipBeginAtomic(); err != nil {
+			s.comment("#", "\n")
+		case r == '-' && s.next() == '-':
+			s.comment("--", "\n")
+		case r == '/' && s.next() == '*':
+			s.comment("/*", "*/")
+		case s.delim == delimiter && s.MatchBeginAtomic && reBeginAtomic.MatchString(s.input[s.pos-1:]):
+			if err := s.skipBeginAtomic(); err != nil {
+				return nil, err
+			}
+			text = s.input[:s.pos]
+			break Scan
+		case s.delim == delimiter && s.MatchBegin && reBegin.MatchString(s.input[s.pos-1:]):
+			if err := s.skipBegin(); err != nil {
 				return nil, err
 			}
-			text = l.input[:l.pos]
+			text = s.input[:s.pos]
 			break Scan
 		}
 	}
-	return l.emit(text), nil
+	return s.emit(text), nil
 }
 
-func (l *lex) next() rune {
-	if l.pos >= len(l.input) {
+func (s *Scanner) next() rune {
+	if s.pos >= len(s.input) {
 		return eos
 	}
-	r, w := utf8.DecodeRuneInString(l.input[l.pos:])
-	l.width = w
-	l.addPos(w)
+	r, w := utf8.DecodeRuneInString(s.input[s.pos:])
+	s.width = w
+	s.addPos(w)
 	return r
 }
 
-func (l *lex) pick() rune {
-	p, w := l.pos, l.width
-	r := l.next()
-	l.pos, l.width = p, w
+func (s *Scanner) pick() rune {
+	p, w := s.pos, s.width
+	r := s.next()
+	s.pos, s.width = p, w
 	return r
 }
 
-func (l *lex) addPos(p int) {
-	l.pos += p
-	l.total += p
+func (s *Scanner) addPos(p int) {
+	s.pos += p
+	s.total += p
 }
 
-func (l *lex) skipQuote(quote rune) error {
-	pos := l.pos
+func (s *Scanner) skipQuote(quote rune) error {
+	pos := s.pos
 	for {
-		switch r := l.next(); {
+		switch r := s.next(); {
 		case r == eos:
-			return l.error(pos, "unclosed quote %q", quote)
+			return s.error(pos, "unclosed quote %q", quote)
 		case r == '\\':
-			l.next()
+			s.next()
 		case r == quote:
 			return nil
 		}
 	}
 }
 
-func (l *lex) skipDollarQuote() error {
-	m := reDollarQuote.FindString(l.input[l.pos-1:])
+func (s *Scanner) skipDollarQuote() error {
+	m := reDollarQuote.FindString(s.input[s.pos-1:])
 	if m == "" {
-		return l.error(l.pos, "unexpected dollar quote")
+		return s.error(s.pos, "unexpected dollar quote")
 	}
-	l.addPos(len(m) - 1)
+	s.addPos(len(m) - 1)
 	for {
-		switch r := l.next(); {
+		switch r := s.next(); {
 		case r == eos:
 			// Fail only if a delimiter was not set.
-			if l.delim == "" {
-				return l.error(l.pos, "unclosed dollar-quoted string")
+			if s.delim == "" {
+				return s.error(s.pos, "unclosed dollar-quoted string")
 			}
 			return nil
-		case r == '$' && strings.HasPrefix(l.input[l.pos-1:], m):
-			l.addPos(len(m) - 1)
+		case r == '$' && strings.HasPrefix(s.input[s.pos-1:], m):
+			s.addPos(len(m) - 1)
 			return nil
 		}
 	}
 }
 
-func (l *lex) skipBeginAtomic() error {
-	m := reBeginAtomic.FindString(l.input[l.pos-1:])
+func (s *Scanner) skipBeginAtomic() error {
+	m := reBeginAtomic.FindString(s.input[s.pos-1:])
 	if m == "" {
-		return l.error(l.pos, "unexpected missing BEGIN ATOMIC block")
+		return s.error(s.pos, "unexpected missing BEGIN ATOMIC block")
 	}
-	l.addPos(len(m) - 1)
-	body, err := newLex(l.input[l.pos:])
-	if err != nil {
-		return l.error(l.pos, "create scanner for sql body: %v", err)
+	s.addPos(len(m) - 1)
+	body := &Scanner{ScannerOptions: s.ScannerOptions}
+	if err := body.init(s.input[s.pos:]); err != nil {
+		return err
 	}
 	for {
-		s, err := body.stmt()
+		stmt, err := body.stmt()
 		if err == io.EOF {
-			return l.error(l.pos, "unexpected eof when scanning sql body")
+			return s.error(s.pos, "unexpected eof when scanning sql body")
 		}
 		if err != nil {
-			return l.error(l.pos, "scan sql body: %v", err)
+			return s.error(s.pos, "scan sql body: %v", err)
 		}
-		if reEnd.MatchString(s.Text) {
+		if reEnd.MatchString(stmt.Text) {
 			break
 		}
 	}
-	l.addPos(body.total)
+	s.addPos(body.total)
+	return nil
+}
+
+func (s *Scanner) skipBegin() error {
+	m := reBegin.FindString(s.input[s.pos-1:])
+	if m == "" {
+		return s.error(s.pos, "unexpected missing BEGIN block")
+	}
+	s.addPos(len(m) - 1)
+	group := &Scanner{ScannerOptions: s.ScannerOptions}
+	if err := group.init(s.input[s.pos:]); err != nil {
+		return err
+	}
+	for depth := 1; depth > 0; {
+		switch stmt, err := group.stmt(); {
+		case err == io.EOF:
+			return s.error(s.pos, "unexpected eof when scanning compound statements")
+		case err != nil:
+			return s.error(s.pos, "scan compound statements: %v", err)
+		case reEnd.MatchString(stmt.Text):
+			if m := reEnd.FindString(stmt.Text); len(m) == len(stmt.Text) || strings.TrimPrefix(stmt.Text, m) == s.delim {
+				depth--
+			}
+		}
+	}
+	s.addPos(group.total)
 	return nil
 }
 
-func (l *lex) comment(left, right string) {
-	i := strings.Index(l.input[l.pos:], right)
+func (s *Scanner) comment(left, right string) {
+	i := strings.Index(s.input[s.pos:], right)
 	// Not a comment.
 	if i == -1 {
 		return
 	}
 	// If the comment reside inside a statement, collect it.
-	if l.pos != len(left) {
-		l.addPos(i + len(right))
+	if s.pos != len(left) {
+		s.addPos(i + len(right))
 		return
 	}
-	l.addPos(i + len(right))
+	s.addPos(i + len(right))
 	// If we did not scan any statement characters, it
 	// can be skipped and stored in the comments group.
-	l.comments = append(l.comments, l.input[:l.pos])
-	l.input = l.input[l.pos:]
-	l.pos = 0
+	s.comments = append(s.comments, s.input[:s.pos])
+	s.input = s.input[s.pos:]
+	s.pos = 0
 	// Double \n separate the comments group from the statement.
-	if strings.HasPrefix(l.input, "\n\n") || right == "\n" && strings.HasPrefix(l.input, "\n") {
-		l.comments = nil
+	if strings.HasPrefix(s.input, "\n\n") || right == "\n" && strings.HasPrefix(s.input, "\n") {
+		s.comments = nil
 	}
-	l.skipSpaces()
+	s.skipSpaces()
 }
 
-func (l *lex) skipSpaces() {
-	n := len(l.input)
-	l.input = strings.TrimLeftFunc(l.input, unicode.IsSpace)
-	l.total += n - len(l.input)
+func (s *Scanner) skipSpaces() {
+	n := len(s.input)
+	s.input = strings.TrimLeftFunc(s.input, unicode.IsSpace)
+	s.total += n - len(s.input)
 }
 
-func (l *lex) emit(text string) *Stmt {
-	s := &Stmt{Pos: l.total - len(text), Text: text, Comments: l.comments}
-	l.input = l.input[l.pos:]
-	l.pos = 0
-	l.comments = nil
+func (s *Scanner) emit(text string) *Stmt {
+	stmt := &Stmt{Pos: s.total - len(text), Text: text, Comments: s.comments}
+	s.input = s.input[s.pos:]
+	s.pos = 0
+	s.comments = nil
 	// Trim custom delimiter.
-	if l.delim != delimiter {
-		s.Text = strings.TrimSuffix(s.Text, l.delim)
+	if s.delim != delimiter {
+		stmt.Text = strings.TrimSuffix(stmt.Text, s.delim)
 	}
-	s.Text = strings.TrimSpace(s.Text)
-	return s
+	stmt.Text = strings.TrimSpace(stmt.Text)
+	return stmt
 }
 
 // delimCmd checks if the scanned "DELIMITER"
 // text represents an actual delimiter command.
-func (l *lex) delimCmd() error {
+func (s *Scanner) delimCmd() error {
 	// A space must come after the delimiter.
-	if l.pick() != ' ' {
+	if s.pick() != ' ' {
 		return nil
 	}
 	// Scan delimiter.
-	for r := l.pick(); r != eos && r != '\n'; r = l.next() {
+	for r := s.pick(); r != eos && r != '\n'; r = s.next() {
 	}
-	delim := strings.TrimSpace(l.input[len(delimiterCmd):l.pos])
+	delim := strings.TrimSpace(s.input[len(delimiterCmd):s.pos])
 	// MySQL client allows quoting delimiters.
 	if strings.HasPrefix(delim, "'") && strings.HasSuffix(delim, "'") {
 		delim = strings.ReplaceAll(delim[1:len(delim)-1], "''", "'")
 	}
-	if err := l.setDelim(delim); err != nil {
+	if err := s.setDelim(delim); err != nil {
 		return err
 	}
 	// Skip all we saw until now.
-	l.emit(l.input[:l.pos])
+	s.emit(s.input[:s.pos])
 	return nil
 }
 
-func (l *lex) setDelim(d string) error {
+func (s *Scanner) setDelim(d string) error {
 	if d == "" {
 		return errors.New("empty delimiter")
 	}
 	// Unescape delimiters. e.g. "\\n" => "\n".
-	l.delim = strings.NewReplacer(`\n`, "\n", `\r`, "\r", `\t`, "\t").Replace(d)
+	s.delim = strings.NewReplacer(`\n`, "\n", `\r`, "\r", `\t`, "\t").Replace(d)
 	return nil
 }
 
-func (l *lex) error(pos int, format string, args ...any) error {
+func (s *Scanner) error(pos int, format string, args ...any) error {
 	format = "%d:%d: " + format
 	var (
-		p    = len(l.src) - len(l.input) + pos
-		s    = l.src[:p]
-		col  = strings.LastIndex(s, "\n")
-		line = 1 + strings.Count(s, "\n")
+		p    = len(s.src) - len(s.input) + pos
+		src  = s.src[:p]
+		col  = strings.LastIndex(src, "\n")
+		line = 1 + strings.Count(src, "\n")
 	)
 	if line == 1 {
 		col = p
diff --git a/sql/migrate/migrate.go b/sql/migrate/migrate.go
index 7ff17d9d..ee380b5f 100644
--- a/sql/migrate/migrate.go
+++ b/sql/migrate/migrate.go
@@ -660,11 +660,11 @@ func (e *Executor) Pending(ctx context.Context) ([]File, error) {
 	if err != nil {
 		return nil, fmt.Errorf("sql/migrate: execute: read revisions: %w", err)
 	}
-	migrations, err := e.dir.Files()
+	all, err := e.dir.Files()
 	if err != nil {
 		return nil, fmt.Errorf("sql/migrate: execute: select migration files: %w", err)
 	}
-	migrations = SkipCheckpointFiles(migrations)
+	migrations := SkipCheckpointFiles(all)
 	var pending []File
 	switch {
 	// If it is the first time we run.
@@ -690,14 +690,27 @@ func (e *Executor) Pending(ctx context.Context) ([]File, error) {
 				return nil, err
 			}
 			pending = migrations[baseline+1:]
-
 			// In case the "allow-dirty" option was set, or the database is clean,
 			// the starting-point is the first migration file or the last checkpoint.
 		} else if pending, err = FilesFromLastCheckpoint(e.dir); err != nil {
 			return nil, err
 		}
-	// In case we applied/marked revisions in
-	// the past, and there is work to do.
+	// In case we applied a checkpoint, but it was only partially applied.
+	case revs[len(revs)-1].Applied != revs[len(revs)-1].Total && len(all) > 0:
+		if idx, found := slices.BinarySearchFunc(all, revs[len(revs)-1], func(f File, r *Revision) int {
+			return strings.Compare(f.Version(), r.Version)
+		}); found {
+			if f, ok := all[idx].(CheckpointFile); ok && f.IsCheckpoint() {
+				// There can only be one checkpoint file and it must be the first one applied.
+				// Thus, we can consider all migrations following the checkpoint to be pending.
+				return append([]File{f}, SkipCheckpointFiles(all[idx:])...), nil
+			}
+		}
+		if len(migrations) == 0 {
+			break // don't fall through the next case if there are no migrations
+		}
+		fallthrough // proceed normally
+	// In case we applied/marked revisions in the past, and there is work to do.
 	case len(migrations) > 0:
 		var (
 			last      = revs[len(revs)-1]
@@ -726,7 +739,6 @@ func (e *Executor) Pending(ctx context.Context) ([]File, error) {
 			idx++
 		}
 		pending = migrations[idx:]
-
 		// Capture all files (versions) between first and last revisions and ensure they
 		// were actually applied. Then, error or execute according to the execution order.
 		// Note, "first" is computed as it can be set to the first checkpoint, which may
@@ -768,7 +780,7 @@ func (e *Executor) Execute(ctx context.Context, m File) (err error) {
 	if err != nil {
 		return fmt.Errorf("sql/migrate: execute: scanning checksum from %q: %w", m.Name(), err)
 	}
-	stmts, err := m.Stmts()
+	stmts, err := FileStmts(e.drv, m)
 	if err != nil {
 		return fmt.Errorf("sql/migrate: execute: scanning statements from %q: %w", m.Name(), err)
 	}
diff --git a/sql/mysql/driver.go b/sql/mysql/driver.go
index 2547cff6..0035c2c7 100644
--- a/sql/mysql/driver.go
+++ b/sql/mysql/driver.go
@@ -44,6 +44,7 @@ type (
 
 var _ interface {
 	migrate.Snapshoter
+	migrate.StmtScanner
 	migrate.CleanChecker
 	schema.TypeParseFormatter
 } = (*Driver)(nil)
@@ -254,6 +255,18 @@ func (*Driver) ParseType(s string) (schema.Type, error) {
 	return ParseType(s)
 }
 
+// ScanStmts implements migrate.StmtScanner.
+func (*Driver) ScanStmts(input string) ([]*migrate.Stmt, error) {
+	return (&migrate.Scanner{
+		ScannerOptions: migrate.ScannerOptions{
+			MatchBegin: true,
+			// The following are not support by MySQL/MariaDB.
+			MatchBeginAtomic: false,
+			MatchDollarQuote: false,
+		},
+	}).Scan(input)
+}
+
 func acquire(ctx context.Context, conn schema.ExecQuerier, name string, timeout time.Duration) error {
 	rows, err := conn.QueryContext(ctx, "SELECT GET_LOCK(?, ?)", name, int(timeout.Seconds()))
 	if err != nil {
diff --git a/sql/mysql/inspect.go b/sql/mysql/inspect.go
index 52e6f725..3d5c6b60 100644
--- a/sql/mysql/inspect.go
+++ b/sql/mysql/inspect.go
@@ -50,7 +50,7 @@ func (i *inspect) InspectRealm(ctx context.Context, opts *schema.InspectRealmOpt
 			}
 		}
 	}
-	return sqlx.ExcludeRealm(r, opts.Exclude)
+	return schema.ExcludeRealm(r, opts.Exclude)
 }
 
 // InspectSchema returns schema descriptions of the tables in the given schema.
@@ -86,7 +86,7 @@ func (i *inspect) InspectSchema(ctx context.Context, name string, opts *schema.I
 			return nil, err
 		}
 	}
-	return sqlx.ExcludeSchema(r.Schemas[0], opts.Exclude)
+	return schema.ExcludeSchema(r.Schemas[0], opts.Exclude)
 }
 
 func (i *inspect) inspectTables(ctx context.Context, r *schema.Realm, opts *schema.InspectOptions) error {
diff --git a/sql/mysql/migrate.go b/sql/mysql/migrate.go
index fbcb2b8c..6e9544cc 100644
--- a/sql/mysql/migrate.go
+++ b/sql/mysql/migrate.go
@@ -85,8 +85,7 @@ func (s *state) plan(changes []schema.Change) error {
 	if err != nil {
 		return err
 	}
-	var views []schema.Change
-	for _, c := range planned {
+	for _, c := range sqlx.SortChanges(planned) {
 		switch c := c.(type) {
 		case *schema.AddTable:
 			err = s.addTable(c)
@@ -108,20 +107,6 @@ func (s *state) plan(changes []schema.Change) error {
 			err = s.dropFunc(c)
 		case *schema.DropProc:
 			err = s.dropProc(c)
-		case *schema.AddView, *schema.DropView, *schema.ModifyView, *schema.RenameView:
-			views = append(views, c)
-		default:
-			err = fmt.Errorf("unsupported change %T", c)
-		}
-		if err != nil {
-			return err
-		}
-	}
-	if views, err = sqlx.PlanViewChanges(views); err != nil {
-		return err
-	}
-	for _, c := range views {
-		switch c := c.(type) {
 		case *schema.AddView:
 			err = s.addView(c)
 		case *schema.DropView:
@@ -130,6 +115,8 @@ func (s *state) plan(changes []schema.Change) error {
 			err = s.modifyView(c)
 		case *schema.RenameView:
 			s.renameView(c)
+		default:
+			err = fmt.Errorf("unsupported change %T", c)
 		}
 		if err != nil {
 			return err
diff --git a/sql/mysql/mysqlcheck/mysqlcheck.go b/sql/mysql/mysqlcheck/mysqlcheck.go
index 8d8cf003..4b9b97bb 100644
--- a/sql/mysql/mysqlcheck/mysqlcheck.go
+++ b/sql/mysql/mysqlcheck/mysqlcheck.go
@@ -11,6 +11,7 @@ import (
 	"strings"
 
 	"ariga.io/atlas/schemahcl"
+	"ariga.io/atlas/sql/internal/sqlx"
 	"ariga.io/atlas/sql/migrate"
 	"ariga.io/atlas/sql/mysql"
 	"ariga.io/atlas/sql/schema"
@@ -59,11 +60,13 @@ func addNotNull(p *datadepend.ColumnPass) (diags []sqlcheck.Diagnostic, err erro
 	}
 	switch ct := p.Column.Type.Type.(type) {
 	case *mysql.BitType, *schema.BoolType, *schema.IntegerType, *schema.DecimalType, *schema.FloatType, *schema.BinaryType:
-		tt, err := mysql.FormatType(p.Column.Type.Type)
-		if err != nil {
-			return nil, err
+		if !sqlx.Has(p.Column.Attrs, &mysql.AutoIncrement{}) {
+			tt, err := mysql.FormatType(p.Column.Type.Type)
+			if err != nil {
+				return nil, err
+			}
+			implicitUpdate(tt, "0")
 		}
-		implicitUpdate(tt, "0")
 	case *schema.StringType:
 		switch ct.T {
 		case mysql.TypeVarchar, mysql.TypeChar:
@@ -142,12 +145,16 @@ func addNotNull(p *datadepend.ColumnPass) (diags []sqlcheck.Diagnostic, err erro
 // columnFilledAfter checks if the column with the given value was filled after the current change.
 func columnFilledAfter(pass *datadepend.ColumnPass, matchValue string) bool {
 	p, ok := pass.File.Parser.(interface {
-		ColumnFilledAfter(migrate.File, *schema.Table, *schema.Column, int, any) (bool, error)
+		ColumnFilledAfter([]*migrate.Stmt, *schema.Table, *schema.Column, int, any) (bool, error)
 	})
 	if !ok {
 		return false
 	}
-	filled, _ := p.ColumnFilledAfter(pass.File, pass.Table, pass.Column, pass.Change.Stmt.Pos, matchValue)
+	stmts, err := migrate.FileStmtDecls(pass.Dev, pass.File)
+	if err != nil {
+		return false
+	}
+	filled, _ := p.ColumnFilledAfter(stmts, pass.Table, pass.Column, pass.Change.Stmt.Pos, matchValue)
 	return filled
 }
 
diff --git a/sql/postgres/driver.go b/sql/postgres/driver.go
index dfadc619..9565d564 100644
--- a/sql/postgres/driver.go
+++ b/sql/postgres/driver.go
@@ -43,6 +43,7 @@ type (
 
 var _ interface {
 	migrate.Snapshoter
+	migrate.StmtScanner
 	migrate.CleanChecker
 	schema.TypeParseFormatter
 } = (*Driver)(nil)
@@ -251,17 +252,17 @@ func (d *Driver) RealmRestoreFunc(desired *schema.Realm) migrate.RestoreFunc {
 
 func withCascade(changes schema.Changes) schema.Changes {
 	for _, c := range changes {
-		if d, ok := c.(*schema.DropTable); ok {
-			d.Extra = append(d.Extra, &Cascade{})
-		}
-		if d, ok := c.(*schema.DropView); ok {
-			d.Extra = append(d.Extra, &schema.IfExists{})
-		}
-		if d, ok := c.(*schema.DropProc); ok {
-			d.Extra = append(d.Extra, &schema.IfExists{}, &Cascade{})
-		}
-		if d, ok := c.(*schema.DropFunc); ok {
-			d.Extra = append(d.Extra, &schema.IfExists{}, &Cascade{})
+		switch c := c.(type) {
+		case *schema.DropTable:
+			c.Extra = append(c.Extra, &schema.IfExists{}, &Cascade{})
+		case *schema.DropView:
+			c.Extra = append(c.Extra, &schema.IfExists{}, &Cascade{})
+		case *schema.DropProc:
+			c.Extra = append(c.Extra, &schema.IfExists{}, &Cascade{})
+		case *schema.DropFunc:
+			c.Extra = append(c.Extra, &schema.IfExists{}, &Cascade{})
+		case *schema.DropObject:
+			c.Extra = append(c.Extra, &schema.IfExists{}, &Cascade{})
 		}
 	}
 	return changes
@@ -315,6 +316,17 @@ func (*Driver) ParseType(s string) (schema.Type, error) {
 	return ParseType(s)
 }
 
+// ScanStmts implements migrate.StmtScanner.
+func (*Driver) ScanStmts(input string) ([]*migrate.Stmt, error) {
+	return (&migrate.Scanner{
+		ScannerOptions: migrate.ScannerOptions{
+			MatchBegin:       true,
+			MatchBeginAtomic: true,
+			MatchDollarQuote: true,
+		},
+	}).Scan(input)
+}
+
 func acquire(ctx context.Context, conn schema.ExecQuerier, id uint32, timeout time.Duration) error {
 	switch {
 	// With timeout (context-based).
diff --git a/sql/postgres/driver_oss.go b/sql/postgres/driver_oss.go
index b14917b6..f31b988a 100644
--- a/sql/postgres/driver_oss.go
+++ b/sql/postgres/driver_oss.go
@@ -38,7 +38,15 @@ func (*inspect) inspectFuncs(context.Context, *schema.Realm, *schema.InspectOpti
 	return nil // unimplemented.
 }
 
-func (i *inspect) inspectTypes(context.Context, *schema.Realm, *schema.InspectOptions) error {
+func (*inspect) inspectTypes(context.Context, *schema.Realm, *schema.InspectOptions) error {
+	return nil // unimplemented.
+}
+
+func (*inspect) inspectSequences(context.Context, *schema.Realm, *schema.InspectOptions) error {
+	return nil // unimplemented.
+}
+
+func (*inspect) inspectDeps(context.Context, *schema.Realm, *schema.InspectOptions) error {
 	return nil // unimplemented.
 }
 
@@ -175,18 +183,29 @@ func verifyChanges(context.Context, []schema.Change) error {
 	return nil // unimplemented.
 }
 
-func convertDomains(_ []*sqlspec.Table, domains []*Domain, _ *schema.Realm) error {
+func convertDomains(_ []*sqlspec.Table, domains []*domain, _ *schema.Realm) error {
 	if len(domains) > 0 {
 		return fmt.Errorf("postgres: domains are not supported by this version. Use: https://atlasgo.io/getting-started")
 	}
 	return nil
 }
 
+func convertSequences(_ []*sqlspec.Table, seqs []*sequence, _ *schema.Realm) error {
+	if len(seqs) > 0 {
+		return fmt.Errorf("postgres: sequences are not supported by this version. Use: https://atlasgo.io/getting-started")
+	}
+	return nil
+}
+
+func qualifySeqRefs([]*sequence, []*sqlspec.Table, *schema.Realm) error {
+	return nil // unimplemented.
+}
+
 // objectSpec converts from a concrete schema objects into specs.
 func objectSpec(d *doc, spec *specutil.SchemaSpec, s *schema.Schema) error {
 	for _, o := range s.Objects {
 		if e, ok := o.(*schema.EnumType); ok {
-			d.Enums = append(d.Enums, &Enum{
+			d.Enums = append(d.Enums, &enum{
 				Name:   e.T,
 				Values: e.Values,
 				Schema: specutil.SchemaRef(spec.Schema.Name),
diff --git a/sql/postgres/inspect.go b/sql/postgres/inspect.go
index 0a4dba97..94bf5dff 100644
--- a/sql/postgres/inspect.go
+++ b/sql/postgres/inspect.go
@@ -5,17 +5,18 @@
 package postgres
 
 import (
-	"ariga.io/atlas/schemahcl"
 	"bytes"
 	"context"
 	"database/sql"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"regexp"
 	"strconv"
 	"strings"
 	"sync"
 
+	"ariga.io/atlas/schemahcl"
 	"ariga.io/atlas/sql/internal/sqlx"
 	"ariga.io/atlas/sql/postgres/internal/postgresop"
 	"ariga.io/atlas/sql/schema"
@@ -62,8 +63,16 @@ func (i *inspect) InspectRealm(ctx context.Context, opts *schema.InspectRealmOpt
 				return nil, err
 			}
 		}
+		if mode.Is(schema.InspectObjects) {
+			if err := i.inspectSequences(ctx, r, nil); err != nil {
+				return nil, err
+			}
+		}
+		if err := i.inspectDeps(ctx, r, nil); err != nil {
+			return nil, err
+		}
 	}
-	return sqlx.ExcludeRealm(r, opts.Exclude)
+	return schema.ExcludeRealm(r, opts.Exclude)
 }
 
 // InspectSchema returns schema descriptions of the tables in the given schema.
@@ -75,6 +84,10 @@ func (i *inspect) InspectSchema(ctx context.Context, name string, opts *schema.I
 	}
 	switch n := len(schemas); {
 	case n == 0:
+		// Empty string indicates current connected schema.
+		if name == "" {
+			return nil, &schema.NotExistError{Err: errors.New("postgres: current_schema() defined in search_path was not found")}
+		}
 		return nil, &schema.NotExistError{Err: fmt.Errorf("postgres: schema %q was not found", name)}
 	case n > 1:
 		return nil, fmt.Errorf("postgres: %d schemas were found for %q", n, name)
@@ -108,7 +121,15 @@ func (i *inspect) InspectSchema(ctx context.Context, name string, opts *schema.I
 			return nil, err
 		}
 	}
-	return sqlx.ExcludeSchema(r.Schemas[0], opts.Exclude)
+	if mode.Is(schema.InspectObjects) {
+		if err := i.inspectSequences(ctx, r, opts); err != nil {
+			return nil, err
+		}
+	}
+	if err := i.inspectDeps(ctx, r, opts); err != nil {
+		return nil, err
+	}
+	return schema.ExcludeSchema(r.Schemas[0], opts.Exclude)
 }
 
 func (i *inspect) inspectTables(ctx context.Context, r *schema.Realm, opts *schema.InspectOptions) error {
@@ -903,10 +924,27 @@ type (
 	// Sequence defines (the supported) sequence options.
 	// https://postgresql.org/docs/current/sql-createsequence.html
 	Sequence struct {
-		Start, Increment int64
+		schema.Object
+		// Fields used by the Identity schema attribute.
+		Start     int64
+		Increment int64
 		// Last sequence value written to disk.
 		// https://postgresql.org/docs/current/view-pg-sequences.html.
 		Last int64
+
+		// Field used when defining and managing independent
+		// sequences (not part of IDENTITY or serial columns).
+		Name     string         // Sequence name.
+		Schema   *schema.Schema // Optional schema.
+		Type     schema.Type    // Sequence type.
+		Cache    int64          // Cache size.
+		Min, Max *int64         // Min and max values.
+		Cycle    bool           // Whether the sequence cycles.
+		Attrs    []schema.Attr  // Additional attributes (e.g., comments),
+		Owner    struct {       // Optional owner of the sequence.
+			T *schema.Table
+			C *schema.Column
+		}
 	}
 
 	// Identity defines an identity column.
@@ -1032,6 +1070,16 @@ func (d *DomainType) Ref() *schemahcl.Ref {
 	return &schemahcl.Ref{V: "$domain." + d.T}
 }
 
+// Underlying returns the underlying type of the domain.
+func (d *DomainType) Underlying() schema.Type {
+	return d.Type
+}
+
+// Underlying returns the underlying type of the array.
+func (a *ArrayType) Underlying() schema.Type {
+	return a.Type
+}
+
 // String implements fmt.Stringer interface.
 func (o ReferenceOption) String() string {
 	return string(o)
diff --git a/sql/postgres/migrate.go b/sql/postgres/migrate.go
index 10272dd3..cf3bb676 100644
--- a/sql/postgres/migrate.go
+++ b/sql/postgres/migrate.go
@@ -81,11 +81,7 @@ func (s *state) plan(changes []schema.Change) error {
 	if planned, err = sqlx.DetachCycles(planned); err != nil {
 		return err
 	}
-	var (
-		views []schema.Change
-		drop  struct{ T, O, F []schema.Change }
-	)
-	for _, c := range planned {
+	for _, c := range sqlx.SortChanges(planned) {
 		switch c := c.(type) {
 		case *schema.AddTable:
 			err = s.addTable(c)
@@ -105,26 +101,6 @@ func (s *state) plan(changes []schema.Change) error {
 			err = s.renameFunc(c)
 		case *schema.RenameProc:
 			err = s.renameProc(c)
-		case *schema.AddView, *schema.DropView, *schema.ModifyView, *schema.RenameView:
-			views = append(views, c)
-		case *schema.DropTable:
-			drop.T = append(drop.T, c)
-		case *schema.DropObject:
-			drop.O = append(drop.O, c)
-		case *schema.DropFunc, *schema.DropProc:
-			drop.F = append(drop.F, c)
-		default:
-			err = fmt.Errorf("unsupported change %T", c)
-		}
-		if err != nil {
-			return err
-		}
-	}
-	if views, err = sqlx.PlanViewChanges(views); err != nil {
-		return err
-	}
-	for _, c := range views {
-		switch c := c.(type) {
 		case *schema.AddView:
 			err = s.addView(c)
 		case *schema.DropView:
@@ -133,22 +109,20 @@ func (s *state) plan(changes []schema.Change) error {
 			err = s.modifyView(c)
 		case *schema.RenameView:
 			s.renameView(c)
-		}
-		if err != nil {
-			return err
-		}
-	}
-	for _, c := range append(drop.T, append(drop.O, drop.F...)...) {
-		var err error
-		switch c := c.(type) {
 		case *schema.DropTable:
 			err = s.dropTable(c)
-		case *schema.DropObject:
-			err = s.dropObject(c)
 		case *schema.DropFunc:
 			err = s.dropFunc(c)
 		case *schema.DropProc:
 			err = s.dropProc(c)
+		case *schema.AddObject:
+			err = s.addObject(c)
+		case *schema.ModifyObject:
+			err = s.modifyObject(c)
+		case *schema.DropObject:
+			err = s.dropObject(c)
+		default:
+			err = fmt.Errorf("unsupported change %T", c)
 		}
 		if err != nil {
 			return err
@@ -214,14 +188,6 @@ func (s *state) topLevel(changes []schema.Change) ([]schema.Change, error) {
 				Source:  c,
 				Comment: fmt.Sprintf("Drop schema named %q", c.S.Name),
 			})
-		case *schema.AddObject:
-			if err := s.addObject(c); err != nil {
-				return nil, err
-			}
-		case *schema.ModifyObject:
-			if err := s.modifyObject(c); err != nil {
-				return nil, err
-			}
 		case *schema.RenameObject:
 			e1, ok1 := c.From.(*schema.EnumType)
 			e2, ok2 := c.To.(*schema.EnumType)
diff --git a/sql/postgres/sqlspec.go b/sql/postgres/sqlspec.go
index fd9a59cd..c8965703 100644
--- a/sql/postgres/sqlspec.go
+++ b/sql/postgres/sqlspec.go
@@ -26,15 +26,16 @@ type (
 		Tables       []*sqlspec.Table  `spec:"table"`
 		Views        []*sqlspec.View   `spec:"view"`
 		Materialized []*sqlspec.View   `spec:"materialized"`
-		Enums        []*Enum           `spec:"enum"`
-		Domains      []*Domain         `spec:"domain"`
+		Enums        []*enum           `spec:"enum"`
+		Domains      []*domain         `spec:"domain"`
+		Sequences    []*sequence       `spec:"sequence"`
 		Funcs        []*sqlspec.Func   `spec:"function"`
 		Procs        []*sqlspec.Func   `spec:"procedure"`
 		Schemas      []*sqlspec.Schema `spec:"schema"`
 	}
 
 	// Enum holds a specification for an enum type.
-	Enum struct {
+	enum struct {
 		Name      string         `spec:",name"`
 		Qualifier string         `spec:",qualifier"`
 		Schema    *schemahcl.Ref `spec:"schema"`
@@ -43,7 +44,7 @@ type (
 	}
 
 	// Domain holds a specification for a domain type.
-	Domain struct {
+	domain struct {
 		Name      string           `spec:",name"`
 		Qualifier string           `spec:",qualifier"`
 		Schema    *schemahcl.Ref   `spec:"schema"`
@@ -53,47 +54,71 @@ type (
 		Checks    []*sqlspec.Check `spec:"check"`
 		schemahcl.DefaultExtension
 	}
+
+	// Sequence holds a specification for a sequence.
+	sequence struct {
+		Name      string         `spec:",name"`
+		Qualifier string         `spec:",qualifier"`
+		Schema    *schemahcl.Ref `spec:"schema"`
+		// Type, Start, Increment, Min, Max, Cache, Cycle
+		// are optionally added to the sequence definition.
+		schemahcl.DefaultExtension
+	}
 )
 
 // merge merges the doc d1 into d.
 func (d *doc) merge(d1 *doc) {
-	d.Tables = append(d.Tables, d1.Tables...)
-	d.Views = append(d.Views, d1.Views...)
-	d.Materialized = append(d.Materialized, d1.Materialized...)
-	d.Domains = append(d.Domains, d1.Domains...)
 	d.Enums = append(d.Enums, d1.Enums...)
 	d.Funcs = append(d.Funcs, d1.Funcs...)
 	d.Procs = append(d.Procs, d1.Procs...)
+	d.Views = append(d.Views, d1.Views...)
+	d.Tables = append(d.Tables, d1.Tables...)
+	d.Domains = append(d.Domains, d1.Domains...)
 	d.Schemas = append(d.Schemas, d1.Schemas...)
+	d.Sequences = append(d.Sequences, d1.Sequences...)
+	d.Materialized = append(d.Materialized, d1.Materialized...)
 }
 
 // Label returns the defaults label used for the enum resource.
-func (e *Enum) Label() string { return e.Name }
+func (e *enum) Label() string { return e.Name }
 
 // QualifierLabel returns the qualifier label used for the enum resource, if any.
-func (e *Enum) QualifierLabel() string { return e.Qualifier }
+func (e *enum) QualifierLabel() string { return e.Qualifier }
 
 // SetQualifier sets the qualifier label used for the enum resource.
-func (e *Enum) SetQualifier(q string) { e.Qualifier = q }
+func (e *enum) SetQualifier(q string) { e.Qualifier = q }
 
 // SchemaRef returns the schema reference for the enum.
-func (e *Enum) SchemaRef() *schemahcl.Ref { return e.Schema }
+func (e *enum) SchemaRef() *schemahcl.Ref { return e.Schema }
 
-// Label returns the defaults label used for the enum resource.
-func (d *Domain) Label() string { return d.Name }
+// Label returns the defaults label used for the domain resource.
+func (d *domain) Label() string { return d.Name }
 
-// QualifierLabel returns the qualifier label used for the enum resource, if any.
-func (d *Domain) QualifierLabel() string { return d.Qualifier }
+// QualifierLabel returns the qualifier label used for the domain resource, if any.
+func (d *domain) QualifierLabel() string { return d.Qualifier }
 
-// SetQualifier sets the qualifier label used for the enum resource.
-func (d *Domain) SetQualifier(q string) { d.Qualifier = q }
+// SetQualifier sets the qualifier label used for the domain resource.
+func (d *domain) SetQualifier(q string) { d.Qualifier = q }
 
-// SchemaRef returns the schema reference for the enum.
-func (d *Domain) SchemaRef() *schemahcl.Ref { return d.Schema }
+// SchemaRef returns the schema reference for the domain.
+func (d *domain) SchemaRef() *schemahcl.Ref { return d.Schema }
+
+// Label returns the defaults label used for the sequence resource.
+func (s *sequence) Label() string { return s.Name }
+
+// QualifierLabel returns the qualifier label used for the sequence resource, if any.
+func (s *sequence) QualifierLabel() string { return s.Qualifier }
+
+// SetQualifier sets the qualifier label used for the sequence resource.
+func (s *sequence) SetQualifier(q string) { s.Qualifier = q }
+
+// SchemaRef returns the schema reference for the sequence.
+func (s *sequence) SchemaRef() *schemahcl.Ref { return s.Schema }
 
 func init() {
-	schemahcl.Register("enum", &Enum{})
-	schemahcl.Register("domain", &Domain{})
+	schemahcl.Register("enum", &enum{})
+	schemahcl.Register("domain", &domain{})
+	schemahcl.Register("sequence", &sequence{})
 }
 
 // evalSpec evaluates an Atlas DDL document into v using the input.
@@ -116,6 +141,9 @@ func evalSpec(p *hclparse.Parser, v any, input map[string]cty.Value) error {
 		if err := convertDomains(d.Tables, d.Domains, v); err != nil {
 			return err
 		}
+		if err := convertSequences(d.Tables, d.Sequences, v); err != nil {
+			return err
+		}
 	case *schema.Schema:
 		var d doc
 		if err := hclState.Eval(p, &d, input); err != nil {
@@ -137,6 +165,9 @@ func evalSpec(p *hclparse.Parser, v any, input map[string]cty.Value) error {
 		if err := convertDomains(d.Tables, d.Domains, r); err != nil {
 			return err
 		}
+		if err := convertSequences(d.Tables, d.Sequences, r); err != nil {
+			return err
+		}
 		*v = *r.Schemas[0]
 	case schema.Schema, schema.Realm:
 		return fmt.Errorf("postgres: Eval expects a pointer: received %[1]T, expected *%[1]T", v)
@@ -179,6 +210,9 @@ func MarshalSpec(v any, marshaler schemahcl.Marshaler) ([]byte, error) {
 		if err := specutil.QualifyObjects(d.Domains); err != nil {
 			return nil, err
 		}
+		if err := specutil.QualifyObjects(d.Sequences); err != nil {
+			return nil, err
+		}
 		if err := specutil.QualifyObjects(d.Funcs); err != nil {
 			return nil, err
 		}
@@ -188,6 +222,9 @@ func MarshalSpec(v any, marshaler schemahcl.Marshaler) ([]byte, error) {
 		if err := specutil.QualifyReferences(d.Tables, s); err != nil {
 			return nil, err
 		}
+		if err := qualifySeqRefs(d.Sequences, d.Tables, s); err != nil {
+			return nil, err
+		}
 	default:
 		return nil, fmt.Errorf("specutil: failed marshaling spec. %T is not supported", v)
 	}
@@ -534,7 +571,7 @@ func convertColumnType(spec *sqlspec.Column) (schema.Type, error) {
 
 // convertEnums converts possibly referenced column types (like enums) to
 // an actual schema.Type and sets it on the correct schema.Column.
-func convertEnums(tables []*sqlspec.Table, enums []*Enum, r *schema.Realm) error {
+func convertEnums(tables []*sqlspec.Table, enums []*enum, r *schema.Realm) error {
 	if len(enums) == 0 {
 		return nil
 	}
@@ -633,8 +670,8 @@ func schemaSpec(s *schema.Schema) (*doc, error) {
 		Funcs:        spec.Funcs,
 		Procs:        spec.Procs,
 		Schemas:      []*sqlspec.Schema{spec.Schema},
-		Enums:        make([]*Enum, 0, len(s.Objects)),
-		Domains:      make([]*Domain, 0, len(s.Objects)),
+		Enums:        make([]*enum, 0, len(s.Objects)),
+		Domains:      make([]*domain, 0, len(s.Objects)),
 	}
 	if err := objectSpec(d, spec, s); err != nil {
 		return nil, err
@@ -863,7 +900,6 @@ var TypeRegistry = schemahcl.NewRegistry(
 		schemahcl.NewTypeSpec(TypeDateRange),
 		schemahcl.NewTypeSpec(TypeDateMultiRange),
 		schemahcl.NewTypeSpec("hstore"),
-		schemahcl.NewTypeSpec("sql", schemahcl.WithAttributes(&schemahcl.TypeAttr{Name: "def", Required: true, Kind: reflect.String})),
 	),
 	// PostgreSQL internal, pseudo, and special types.
 	schemahcl.WithSpecs(func() (specs []*schemahcl.TypeSpec) {
diff --git a/sql/schema/dsl.go b/sql/schema/dsl.go
index 096119da..36994922 100644
--- a/sql/schema/dsl.go
+++ b/sql/schema/dsl.go
@@ -244,6 +244,12 @@ func (t *Table) AddAttrs(attrs ...Attr) *Table {
 	return t
 }
 
+// AddDeps adds the given objects as dependencies to the view.
+func (t *Table) AddDeps(objs ...Object) *Table {
+	t.Deps = append(t.Deps, objs...)
+	return t
+}
+
 // NewView creates a new View.
 func NewView(name, def string) *View {
 	return &View{Name: name, Def: def}
@@ -834,6 +840,18 @@ func (f *ForeignKey) SetOnDelete(o ReferenceOption) *ForeignKey {
 	return f
 }
 
+// AddDeps adds the given objects as dependencies to the function.
+func (f *Func) AddDeps(objs ...Object) *Func {
+	f.Deps = append(f.Deps, objs...)
+	return f
+}
+
+// AddDeps adds the given objects as dependencies to the procedure.
+func (p *Proc) AddDeps(objs ...Object) *Proc {
+	p.Deps = append(p.Deps, objs...)
+	return p
+}
+
 // ReplaceOrAppend searches an attribute of the same type as v in
 // the list and replaces it. Otherwise, v is appended to the list.
 func ReplaceOrAppend(attrs *[]Attr, v Attr) {
diff --git a/sql/internal/sqlx/exclude.go b/sql/schema/exclude.go
similarity index 78%
rename from sql/internal/sqlx/exclude.go
rename to sql/schema/exclude.go
index da4d9df3..e9b8844e 100644
--- a/sql/internal/sqlx/exclude.go
+++ b/sql/schema/exclude.go
@@ -2,7 +2,7 @@
 // This source code is licensed under the Apache 2.0 license found
 // in the LICENSE file in the root directory of this source tree.
 
-package sqlx
+package schema
 
 import (
 	"encoding/csv"
@@ -10,16 +10,14 @@ import (
 	"path/filepath"
 	"regexp"
 	"strings"
-
-	"ariga.io/atlas/sql/schema"
 )
 
 // ExcludeRealm filters resources in the realm based on the given patterns.
-func ExcludeRealm(r *schema.Realm, patterns []string) (*schema.Realm, error) {
+func ExcludeRealm(r *Realm, patterns []string) (*Realm, error) {
 	if len(patterns) == 0 {
 		return r, nil
 	}
-	var schemas []*schema.Schema
+	var schemas []*Schema
 	globs, err := split(patterns)
 	if err != nil {
 		return nil, err
@@ -36,7 +34,7 @@ Filter:
 			}
 			if match {
 				// In case there is a match, and it is
-				// a single glob we exclude this schema.
+				// a single glob we exclude this
 				if len(g) == 1 {
 					continue Filter
 				}
@@ -52,7 +50,7 @@ Filter:
 }
 
 // ExcludeSchema filters resources in the schema based on the given patterns.
-func ExcludeSchema(s *schema.Schema, patterns []string) (*schema.Schema, error) {
+func ExcludeSchema(s *Schema, patterns []string) (*Schema, error) {
 	if len(patterns) == 0 {
 		return s, nil
 	}
@@ -89,9 +87,28 @@ func split(patterns []string) ([][]string, error) {
 	return globs, nil
 }
 
-func excludeS(s *schema.Schema, glob []string) error {
+func excludeS(s *Schema, glob []string) error {
+	if globE, exclude := excludeType(typeE, glob[0]); exclude {
+		var objects []Object
+		for _, t := range s.Objects {
+			e, ok := t.(*EnumType)
+			if !ok {
+				objects = append(objects, t)
+				continue
+			}
+			match, err := filepath.Match(globE, e.T)
+			if err != nil {
+				return err
+			}
+			// No match or glob has more than one pattern.
+			if !match || len(glob) != 1 {
+				objects = append(objects, t)
+			}
+		}
+		s.Objects = objects
+	}
 	if globT, exclude := excludeType(typeT, glob[0]); exclude {
-		var tables []*schema.Table
+		var tables []*Table
 		for _, t := range s.Tables {
 			match, err := filepath.Match(globT, t.Name)
 			if err != nil {
@@ -113,7 +130,7 @@ func excludeS(s *schema.Schema, glob []string) error {
 		s.Tables = tables
 	}
 	if globV, exclude := excludeType(typeV, glob[0]); exclude {
-		var views []*schema.View
+		var views []*View
 		for _, v := range s.Views {
 			match, err := filepath.Match(globV, v.Name)
 			if err != nil {
@@ -133,7 +150,7 @@ func excludeS(s *schema.Schema, glob []string) error {
 	}
 	if globF, exclude := excludeType(typeFn, glob[0]); exclude {
 		var err error
-		s.Funcs, err = filter(s.Funcs, func(f *schema.Func) (bool, error) {
+		s.Funcs, err = filter(s.Funcs, func(f *Func) (bool, error) {
 			return filepath.Match(globF, f.Name)
 		})
 		if err != nil {
@@ -142,7 +159,7 @@ func excludeS(s *schema.Schema, glob []string) error {
 	}
 	if globP, exclude := excludeType(typePr, glob[0]); exclude {
 		var err error
-		s.Procs, err = filter(s.Procs, func(p *schema.Proc) (bool, error) {
+		s.Procs, err = filter(s.Procs, func(p *Proc) (bool, error) {
 			return filepath.Match(globP, p.Name)
 		})
 		if err != nil {
@@ -152,11 +169,11 @@ func excludeS(s *schema.Schema, glob []string) error {
 	return nil
 }
 
-func excludeT(t *schema.Table, pattern string) (err error) {
-	ex := make(map[*schema.Index]struct{})
-	ef := make(map[*schema.ForeignKey]struct{})
+func excludeT(t *Table, pattern string) (err error) {
+	ex := make(map[*Index]struct{})
+	ef := make(map[*ForeignKey]struct{})
 	if p, exclude := excludeType(typeC, pattern); exclude {
-		t.Columns, err = filter(t.Columns, func(c *schema.Column) (bool, error) {
+		t.Columns, err = filter(t.Columns, func(c *Column) (bool, error) {
 			match, err := filepath.Match(p, c.Name)
 			if !match || err != nil {
 				return false, err
@@ -171,7 +188,7 @@ func excludeT(t *schema.Table, pattern string) (err error) {
 		})
 	}
 	if p, exclude := excludeType(typeI, pattern); exclude {
-		t.Indexes, err = filter(t.Indexes, func(idx *schema.Index) (bool, error) {
+		t.Indexes, err = filter(t.Indexes, func(idx *Index) (bool, error) {
 			if _, ok := ex[idx]; ok {
 				return true, nil
 			}
@@ -179,7 +196,7 @@ func excludeT(t *schema.Table, pattern string) (err error) {
 		})
 	}
 	if p, exclude := excludeType(typeF, pattern); exclude {
-		t.ForeignKeys, err = filter(t.ForeignKeys, func(fk *schema.ForeignKey) (bool, error) {
+		t.ForeignKeys, err = filter(t.ForeignKeys, func(fk *ForeignKey) (bool, error) {
 			if _, ok := ef[fk]; ok {
 				return true, nil
 			}
@@ -187,8 +204,8 @@ func excludeT(t *schema.Table, pattern string) (err error) {
 		})
 	}
 	if p, exclude := excludeType(typeK, pattern); exclude {
-		t.Attrs, err = filter(t.Attrs, func(a schema.Attr) (bool, error) {
-			c, ok := a.(*schema.Check)
+		t.Attrs, err = filter(t.Attrs, func(a Attr) (bool, error) {
+			c, ok := a.(*Check)
 			if !ok {
 				return false, nil
 			}
@@ -202,9 +219,9 @@ func excludeT(t *schema.Table, pattern string) (err error) {
 	return
 }
 
-func excludeV(t *schema.View, pattern string) (err error) {
+func excludeV(t *View, pattern string) (err error) {
 	if p, exclude := excludeType(typeC, pattern); exclude {
-		t.Columns, err = filter(t.Columns, func(c *schema.Column) (bool, error) {
+		t.Columns, err = filter(t.Columns, func(c *Column) (bool, error) {
 			match, err := filepath.Match(p, c.Name)
 			if !match || err != nil {
 				return false, err
@@ -216,6 +233,7 @@ func excludeV(t *schema.View, pattern string) (err error) {
 }
 
 const (
+	typeE  = "enum"
 	typeV  = "view"
 	typeT  = "table"
 	typeC  = "column"
diff --git a/sql/schema/inspect.go b/sql/schema/inspect.go
index 018bc031..b920dbe6 100644
--- a/sql/schema/inspect.go
+++ b/sql/schema/inspect.go
@@ -52,6 +52,10 @@ const (
 
 	// InspectTypes enables schema types inspection.
 	InspectTypes
+
+	// InspectObjects enables inspection of database specific
+	// objects like sequences and extensions.
+	InspectObjects
 )
 
 // Is reports whether the given mode is enabled.
diff --git a/sql/schema/schema.go b/sql/schema/schema.go
index 0115da06..3f0a4afb 100644
--- a/sql/schema/schema.go
+++ b/sql/schema/schema.go
@@ -571,3 +571,11 @@ func UnderlyingExpr(x Expr) Expr {
 	}
 	return x
 }
+
+// UnderlyingType returns the underlying type of t.
+func UnderlyingType(t Type) Type {
+	if w, ok := t.(interface{ Underlying() Type }); ok {
+		return UnderlyingType(w.Underlying())
+	}
+	return t
+}
diff --git a/sql/sqlcheck/datadepend/datadepend.go b/sql/sqlcheck/datadepend/datadepend.go
index 39e7fb90..417fbb79 100644
--- a/sql/sqlcheck/datadepend/datadepend.go
+++ b/sql/sqlcheck/datadepend/datadepend.go
@@ -152,7 +152,7 @@ func (a *Analyzer) Diagnostics(_ context.Context, p *sqlcheck.Pass) (diags []sql
 						diags = append(diags, d...)
 					// In case the altered column was not added in this file, and the column
 					// was changed nullable to non-nullable without back filling it with values.
-					case !ColumnFilled(p.File, m.T, c.From, sc.Stmt.Pos):
+					case !ColumnFilled(p, m.T, c.From, sc.Stmt.Pos):
 						diags = append(diags, sqlcheck.Diagnostic{
 							Code: codeModNotNullC,
 							Pos:  sc.Stmt.Pos,
@@ -181,15 +181,19 @@ func (a *Analyzer) Report(p *sqlcheck.Pass, diags []sqlcheck.Diagnostic) error {
 }
 
 // ColumnFilled checks if the column was filled with values before the given position.
-func ColumnFilled(f *sqlcheck.File, t *schema.Table, c *schema.Column, pos int) bool {
+func ColumnFilled(p *sqlcheck.Pass, t *schema.Table, c *schema.Column, pos int) bool {
 	// The parser used for parsing this file can check if the
 	// given nullable column was filled before the given position.
-	p, ok := f.Parser.(interface {
-		ColumnFilledBefore(migrate.File, *schema.Table, *schema.Column, int) (bool, error)
+	pr, ok := p.File.Parser.(interface {
+		ColumnFilledBefore([]*migrate.Stmt, *schema.Table, *schema.Column, int) (bool, error)
 	})
 	if !ok {
 		return false
 	}
-	filled, _ := p.ColumnFilledBefore(f, t, c, pos)
+	stmts, err := migrate.FileStmtDecls(p.Dev, p.File)
+	if err != nil {
+		return false
+	}
+	filled, _ := pr.ColumnFilledBefore(stmts, t, c, pos)
 	return filled
 }
diff --git a/sql/sqlcheck/incompatible/incompatible.go b/sql/sqlcheck/incompatible/incompatible.go
index a85b9f8c..52c604c3 100644
--- a/sql/sqlcheck/incompatible/incompatible.go
+++ b/sql/sqlcheck/incompatible/incompatible.go
@@ -51,7 +51,7 @@ func (a *Analyzer) Analyze(_ context.Context, p *sqlcheck.Pass) error {
 			switch c := c.(type) {
 			case *schema.RenameTable:
 				if p.File.SchemaSpan(c.From.Schema)&sqlcheck.SpanAdded == 0 && p.File.TableSpan(c.From)&sqlcheck.SpanAdded == 0 &&
-					!ViewForRenamedT(p.File, c.From.Name, c.To.Name, sc.Stmt.Pos) {
+					!ViewForRenamedT(p, c.From.Name, c.To.Name, sc.Stmt.Pos) {
 					diags = append(diags, sqlcheck.Diagnostic{
 						Code: codeRenameT,
 						Pos:  sc.Stmt.Pos,
@@ -83,16 +83,20 @@ func (a *Analyzer) Analyze(_ context.Context, p *sqlcheck.Pass) error {
 }
 
 // ViewForRenamedT checks if a view was created was a table that was renamed after the given position.
-func ViewForRenamedT(f *sqlcheck.File, old, new string, pos int) bool {
+func ViewForRenamedT(p *sqlcheck.Pass, old, new string, pos int) bool {
 	// The parser used for parsing this file can check if the
 	// given nullable column was filled before the given position.
-	p, ok := f.Parser.(interface {
-		CreateViewAfter(f migrate.File, old, new string, pos int) (bool, error)
+	pr, ok := p.File.Parser.(interface {
+		CreateViewAfter(stmts []*migrate.Stmt, old, new string, pos int) (bool, error)
 	})
 	if !ok {
 		return false
 	}
-	created, _ := p.CreateViewAfter(f, old, new, pos)
+	stmts, err := migrate.FileStmtDecls(p.Dev, p.File)
+	if err != nil {
+		return false
+	}
+	created, _ := pr.CreateViewAfter(stmts, old, new, pos)
 	return created
 }
 
diff --git a/sql/sqlite/driver.go b/sql/sqlite/driver.go
index d1c56722..5c77cff5 100644
--- a/sql/sqlite/driver.go
+++ b/sql/sqlite/driver.go
@@ -43,6 +43,7 @@ type (
 
 var _ interface {
 	migrate.Snapshoter
+	migrate.StmtScanner
 	migrate.CleanChecker
 	schema.TypeParseFormatter
 } = (*Driver)(nil)
@@ -188,6 +189,18 @@ func (*Driver) ParseType(s string) (schema.Type, error) {
 	return ParseType(s)
 }
 
+// ScanStmts implements migrate.StmtScanner.
+func (*Driver) ScanStmts(input string) ([]*migrate.Stmt, error) {
+	return (&migrate.Scanner{
+		ScannerOptions: migrate.ScannerOptions{
+			MatchBegin: true,
+			// The following are not support by SQLite.
+			MatchBeginAtomic: false,
+			MatchDollarQuote: false,
+		},
+	}).Scan(input)
+}
+
 func acquireLock(path string, timeout time.Duration) (schema.UnlockFunc, error) {
 	lock, err := os.Create(path)
 	if err != nil {
diff --git a/sql/sqlite/inspect.go b/sql/sqlite/inspect.go
index 3bfbce20..c8331c8e 100644
--- a/sql/sqlite/inspect.go
+++ b/sql/sqlite/inspect.go
@@ -54,7 +54,7 @@ func (i *inspect) InspectRealm(ctx context.Context, opts *schema.InspectRealmOpt
 			return nil, err
 		}
 	}
-	return sqlx.ExcludeRealm(r, opts.Exclude)
+	return schema.ExcludeRealm(r, opts.Exclude)
 }
 
 // InspectSchema returns schema descriptions of the tables in the given schema.
@@ -96,7 +96,7 @@ func (i *inspect) InspectSchema(ctx context.Context, name string, opts *schema.I
 			return nil, err
 		}
 	}
-	return sqlx.ExcludeSchema(r.Schemas[0], opts.Exclude)
+	return schema.ExcludeSchema(r.Schemas[0], opts.Exclude)
 }
 
 func (i *inspect) inspectTable(ctx context.Context, t *schema.Table) error {
diff --git a/sql/sqlite/sqlitecheck/sqlitecheck.go b/sql/sqlite/sqlitecheck/sqlitecheck.go
index 5567a332..3c2389d9 100644
--- a/sql/sqlite/sqlitecheck/sqlitecheck.go
+++ b/sql/sqlite/sqlitecheck/sqlitecheck.go
@@ -41,7 +41,7 @@ func addNotNull(p *datadepend.ColumnPass) (diags []sqlcheck.Diagnostic, err erro
 }
 
 func modifyNotNull(p *datadepend.ColumnPass) (diags []sqlcheck.Diagnostic, err error) {
-	if p.Column.Default != nil || datadepend.ColumnFilled(p.File, p.Table, p.Column, p.Change.Stmt.Pos) {
+	if p.Column.Default != nil || datadepend.ColumnFilled(p.Pass, p.Table, p.Column, p.Change.Stmt.Pos) {
 		return nil, nil
 	}
 	return []sqlcheck.Diagnostic{


Test patch:
diff --git a/cmd/atlas/internal/docker/docker_test.go b/cmd/atlas/internal/docker/docker_test.go
index d309f665..7bcdb6df 100644
--- a/cmd/atlas/internal/docker/docker_test.go
+++ b/cmd/atlas/internal/docker/docker_test.go
@@ -50,6 +50,21 @@ func TestDockerConfig(t *testing.T) {
 		Port:     "5432",
 		Out:      io.Discard,
 	}, cfg)
+
+	// SQL Server
+	cfg, err = SQLServer("2022-latest", Out(io.Discard))
+	require.NoError(t, err)
+	require.Equal(t, &Config{
+		Image:    "mcr.microsoft.com/mssql/server:2022-latest",
+		Port:     "1433",
+		Database: "master",
+		Out:      io.Discard,
+		Env: []string{
+			"ACCEPT_EULA=Y",
+			"MSSQL_PID=Developer",
+			"MSSQL_SA_PASSWORD=" + passSQLServer,
+		},
+	}, cfg)
 }
 
 func TestFromURL(t *testing.T) {
@@ -116,6 +131,78 @@ func TestFromURL(t *testing.T) {
 		Port:     "5432",
 		Out:      io.Discard,
 	}, cfg)
+
+	// SQL Server
+	u, err = url.Parse("docker://sqlserver")
+	require.NoError(t, err)
+	cfg, err = FromURL(u)
+	require.NoError(t, err)
+	require.Equal(t, &Config{
+		driver:   "sqlserver",
+		Image:    "mcr.microsoft.com/mssql/server",
+		Database: "master",
+		Port:     "1433",
+		Out:      io.Discard,
+		Env: []string{
+			"ACCEPT_EULA=Y",
+			"MSSQL_PID=Developer",
+			"MSSQL_SA_PASSWORD=" + passSQLServer,
+		},
+	}, cfg)
+
+	u, err = url.Parse("docker://sqlserver/2022-latest")
+	require.NoError(t, err)
+	cfg, err = FromURL(u)
+	require.NoError(t, err)
+	require.Equal(t, &Config{
+		driver:   "sqlserver",
+		Image:    "mcr.microsoft.com/mssql/server:2022-latest",
+		Database: "master",
+		Port:     "1433",
+		Out:      io.Discard,
+		Env: []string{
+			"ACCEPT_EULA=Y",
+			"MSSQL_PID=Developer",
+			"MSSQL_SA_PASSWORD=" + passSQLServer,
+		},
+	}, cfg)
+
+	u, err = url.Parse("docker://sqlserver/2019-latest/foo")
+	require.NoError(t, err)
+	cfg, err = FromURL(u)
+	require.NoError(t, err)
+	require.Equal(t, &Config{
+		driver:   "sqlserver",
+		setup:    []string{"CREATE DATABASE [foo]"},
+		Image:    "mcr.microsoft.com/mssql/server:2019-latest",
+		Database: "foo",
+		Port:     "1433",
+		Out:      io.Discard,
+		Env: []string{
+			"ACCEPT_EULA=Y",
+			"MSSQL_PID=Developer",
+			"MSSQL_SA_PASSWORD=" + passSQLServer,
+		},
+	}, cfg)
+
+	// Azure SQL Edge
+	u, err = url.Parse("docker+sqlserver://mcr.microsoft.com/azure-sql-edge:1.0.7/foo")
+	require.NoError(t, err)
+	cfg, err = FromURL(u)
+	require.NoError(t, err)
+	require.Equal(t, &Config{
+		driver:   "sqlserver",
+		setup:    []string{"CREATE DATABASE [foo]"},
+		Image:    "mcr.microsoft.com/azure-sql-edge:1.0.7",
+		Database: "foo",
+		Port:     "1433",
+		Out:      io.Discard,
+		Env: []string{
+			"ACCEPT_EULA=Y",
+			"MSSQL_PID=Developer",
+			"MSSQL_SA_PASSWORD=" + passSQLServer,
+		},
+	}, cfg)
 }
 
 func TestFromURL_CustomImage(t *testing.T) {
@@ -213,6 +300,25 @@ func TestFromURL_CustomImage(t *testing.T) {
 			db:      "dev",
 			dialect: "mysql",
 		},
+		// SQL Server.
+		{
+			url:     "docker+sqlserver://mcr.microsoft.com/mssql/server:2022-latest",
+			image:   "mcr.microsoft.com/mssql/server:2022-latest",
+			db:      "master",
+			dialect: "sqlserver",
+		},
+		{
+			url:     "docker+sqlserver://mcr.microsoft.com/mssql/server:2022-latest/dev",
+			image:   "mcr.microsoft.com/mssql/server:2022-latest",
+			db:      "dev",
+			dialect: "sqlserver",
+		},
+		{
+			url:     "docker+sqlserver://mcr.microsoft.com/mssql/server:latest",
+			image:   "mcr.microsoft.com/mssql/server:latest",
+			db:      "master",
+			dialect: "sqlserver",
+		},
 	} {
 		u, err := url.Parse(tt.url)
 		require.NoError(t, err)
@@ -235,4 +341,12 @@ func TestImageURL(t *testing.T) {
 		require.NoError(t, err)
 		require.Equal(t, u, got.String())
 	}
+	for img, u := range map[string]string{
+		"mcr.microsoft.com/azure-sql-edge:1.0.7":     "docker+sqlserver://mcr.microsoft.com/azure-sql-edge:1.0.7",
+		"mcr.microsoft.com/mssql/server:2022-latest": "docker+sqlserver://mcr.microsoft.com/mssql/server:2022-latest",
+	} {
+		got, err := ImageURL(DriverSQLServer, img)
+		require.NoError(t, err)
+		require.Equal(t, u, got.String())
+	}
 }
diff --git a/cmd/atlas/internal/sqlparse/myparse/myparse_test.go b/cmd/atlas/internal/sqlparse/myparse/myparse_test.go
index 486c14d4..bea8ba31 100644
--- a/cmd/atlas/internal/sqlparse/myparse/myparse_test.go
+++ b/cmd/atlas/internal/sqlparse/myparse/myparse_test.go
@@ -246,11 +246,10 @@ ALTER TABLE t MODIFY COLUMN c INT NOT NULL;
 		},
 	} {
 		t.Run(strconv.Itoa(i), func(t *testing.T) {
-			var (
-				p myparse.Parser
-				f = migrate.NewLocalFile("file", []byte(tt.file))
-			)
-			filled, err := p.ColumnFilledBefore(f, schema.NewTable("t"), schema.NewColumn("c"), tt.pos)
+			var p myparse.Parser
+			stmts, err := migrate.Stmts(tt.file)
+			require.NoError(t, err)
+			filled, err := p.ColumnFilledBefore(stmts, schema.NewTable("t"), schema.NewColumn("c"), tt.pos)
 			require.Equal(t, err != nil, tt.wantErr, err)
 			require.Equal(t, filled, tt.wantFilled)
 		})
@@ -321,11 +320,10 @@ UPDATE t SET c = CONCAT('tenant_', d) WHERE c = 0;
 		},
 	} {
 		t.Run(strconv.Itoa(i), func(t *testing.T) {
-			var (
-				p myparse.Parser
-				f = migrate.NewLocalFile("file", []byte(tt.file))
-			)
-			filled, err := p.ColumnFilledAfter(f, schema.NewTable("t"), schema.NewColumn("c"), tt.pos, tt.matchValue)
+			var p myparse.Parser
+			stmts, err := migrate.Stmts(tt.file)
+			require.NoError(t, err)
+			filled, err := p.ColumnFilledAfter(stmts, schema.NewTable("t"), schema.NewColumn("c"), tt.pos, tt.matchValue)
 			require.Equal(t, err != nil, tt.wantErr, err)
 			require.Equal(t, filled, tt.wantFilled)
 		})
@@ -377,11 +375,10 @@ CREATE VIEW old AS SELECT a, b, c FROM new;
 		},
 	} {
 		t.Run(strconv.Itoa(i), func(t *testing.T) {
-			var (
-				p myparse.Parser
-				f = migrate.NewLocalFile("file", []byte(tt.file))
-			)
-			created, err := p.CreateViewAfter(f, "old", "new", tt.pos)
+			var p myparse.Parser
+			stmts, err := migrate.Stmts(tt.file)
+			require.NoError(t, err)
+			created, err := p.CreateViewAfter(stmts, "old", "new", tt.pos)
 			require.Equal(t, err != nil, tt.wantErr, err)
 			require.Equal(t, created, tt.wantCreated)
 		})
diff --git a/cmd/atlas/internal/sqlparse/pgparse/pgparse_test.go b/cmd/atlas/internal/sqlparse/pgparse/pgparse_test.go
index 539c27a4..cf892244 100644
--- a/cmd/atlas/internal/sqlparse/pgparse/pgparse_test.go
+++ b/cmd/atlas/internal/sqlparse/pgparse/pgparse_test.go
@@ -281,11 +281,10 @@ func TestColumnFilledBefore(t *testing.T) {
 		},
 	} {
 		t.Run(strconv.Itoa(i), func(t *testing.T) {
-			var (
-				p pgparse.Parser
-				f = migrate.NewLocalFile("file", []byte(tt.file))
-			)
-			filled, err := p.ColumnFilledBefore(f, schema.NewTable("t"), schema.NewColumn("c"), tt.pos)
+			var p pgparse.Parser
+			stmts, err := migrate.Stmts(tt.file)
+			require.NoError(t, err)
+			filled, err := p.ColumnFilledBefore(stmts, schema.NewTable("t"), schema.NewColumn("c"), tt.pos)
 			require.Equal(t, err != nil, tt.wantErr, err)
 			require.Equal(t, filled, tt.wantFilled)
 		})
@@ -345,11 +344,10 @@ CREATE VIEW old AS SELECT * FROM new;
 		},
 	} {
 		t.Run(strconv.Itoa(i), func(t *testing.T) {
-			var (
-				p pgparse.Parser
-				f = migrate.NewLocalFile("file", []byte(tt.file))
-			)
-			created, err := p.CreateViewAfter(f, "old", "new", tt.pos)
+			var p pgparse.Parser
+			stmts, err := migrate.Stmts(tt.file)
+			require.NoError(t, err)
+			created, err := p.CreateViewAfter(stmts, "old", "new", tt.pos)
 			require.Equal(t, err != nil, tt.wantErr, err)
 			require.Equal(t, tt.wantCreated, created)
 		})
diff --git a/cmd/atlas/internal/sqlparse/sqliteparse/sqliteparse_test.go b/cmd/atlas/internal/sqlparse/sqliteparse/sqliteparse_test.go
index c077dbfa..e62d0362 100644
--- a/cmd/atlas/internal/sqlparse/sqliteparse/sqliteparse_test.go
+++ b/cmd/atlas/internal/sqlparse/sqliteparse/sqliteparse_test.go
@@ -133,11 +133,10 @@ UPDATE t1 SET c = 2 WHERE c IS NULL;
 		},
 	} {
 		t.Run(strconv.Itoa(i), func(t *testing.T) {
-			var (
-				p sqliteparse.FileParser
-				f = migrate.NewLocalFile("file", []byte(tt.file))
-			)
-			filled, err := p.ColumnFilledBefore(f, schema.NewTable("t"), schema.NewColumn("c"), tt.pos)
+			var p sqliteparse.FileParser
+			stmts, err := migrate.Stmts(tt.file)
+			require.NoError(t, err)
+			filled, err := p.ColumnFilledBefore(stmts, schema.NewTable("t"), schema.NewColumn("c"), tt.pos)
 			require.Equal(t, err != nil, tt.wantErr, err)
 			require.Equal(t, filled, tt.wantFilled)
 		})
@@ -189,11 +188,10 @@ CREATE VIEW old AS SELECT a, b, c FROM new;
 		},
 	} {
 		t.Run(strconv.Itoa(i), func(t *testing.T) {
-			var (
-				p sqliteparse.FileParser
-				f = migrate.NewLocalFile("file", []byte(tt.file))
-			)
-			created, err := p.CreateViewAfter(f, "old", "new", tt.pos)
+			var p sqliteparse.FileParser
+			stmts, err := migrate.Stmts(tt.file)
+			require.NoError(t, err)
+			created, err := p.CreateViewAfter(stmts, "old", "new", tt.pos)
 			require.Equal(t, err != nil, tt.wantErr, err)
 			require.Equal(t, created, tt.wantCreated)
 		})
diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go
index e2d2a1ff..df8c5e13 100644
--- a/internal/integration/integration_test.go
+++ b/internal/integration/integration_test.go
@@ -5,11 +5,14 @@
 package integration
 
 import (
+	"bufio"
 	"bytes"
 	"context"
 	"database/sql"
 	"flag"
+	"fmt"
 	"io"
+	"net"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -40,6 +43,78 @@ func TestMain(m *testing.M) {
 	os.Exit(code)
 }
 
+func TestCLI_Interrupt(t *testing.T) {
+	var (
+		stdout, stderr bytes.Buffer
+		connected      = make(chan struct{})
+		signal         = make(chan struct{})
+		handler        = func(c net.Conn) {
+			connected <- struct{}{}
+			<-signal // wait for signal sent
+			c.Close()
+		}
+	)
+	t.Cleanup(func() {
+		close(connected)
+		close(signal)
+	})
+
+	// First interrupt will not cancel the process, only print a warning.
+	var (
+		port = newListener(t, handler)
+		cmd  = exec.Command(
+			execPath(t),
+			"schema", "inspect",
+			"--url", fmt.Sprintf("postgres://user:pass@localhost:%d/db?sslmode=disable", port), // mock not responding db
+		)
+	)
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+	require.NoError(t, cmd.Start())
+	<-connected // wait for connection
+	require.NoError(t, cmd.Process.Signal(os.Interrupt))
+	signal <- struct{}{}
+	require.Error(t, cmd.Wait())
+	require.Equal(t, "\ninterrupt received, wait for exit or ^C to terminate\n", stdout.String())
+	require.Contains(t, stderr.String(), "read: connection reset by peer") // server closed
+
+	// Two interrupts force stop
+	r, w := io.Pipe()
+	port = newListener(t, handler)
+	cmd = exec.Command(
+		execPath(t),
+		"schema", "inspect",
+		"--url", fmt.Sprintf("postgres://user:pass@localhost:%d/db?sslmode=disable", port), // mock not responding db
+	)
+	cmd.Stdout = w
+	require.NoError(t, cmd.Start())
+	<-connected // wait for connection
+	require.NoError(t, cmd.Process.Signal(os.Interrupt))
+	// Wait for the cmd to print the warning (it works, we checked before) and then issue the second signal.
+	br := bufio.NewReader(r)
+	_, err := br.ReadString('\n')
+	require.NoError(t, err)
+	out, err := br.ReadString('\n')
+	require.NoError(t, err)
+	require.Equal(t, "interrupt received, wait for exit or ^C to terminate\n", out)
+	require.NoError(t, cmd.Process.Signal(os.Interrupt))
+	require.Error(t, cmd.Wait())
+}
+
+func newListener(t *testing.T, handler func(c net.Conn)) int {
+	a, err := net.ResolveTCPAddr("tcp", "localhost:0")
+	require.NoError(t, err)
+	l, err := net.ListenTCP("tcp", a)
+	require.NoError(t, err)
+	t.Cleanup(func() { l.Close() })
+	go func() {
+		c, err := l.Accept()
+		require.NoError(t, err)
+		handler(c)
+	}()
+	return l.Addr().(*net.TCPAddr).Port
+}
+
 // T holds the elements common between dialect tests.
 type T interface {
 	testing.TB
diff --git a/internal/integration/testdata/mysql/cli-migrate-apply-checkpoint-fail.txt b/internal/integration/testdata/mysql/cli-migrate-apply-checkpoint-fail.txt
new file mode 100644
index 00000000..4bb64e3a
--- /dev/null
+++ b/internal/integration/testdata/mysql/cli-migrate-apply-checkpoint-fail.txt
@@ -0,0 +1,50 @@
+only mysql
+
+atlas migrate hash
+
+# There will be an implicit commit in MySQL, leaving this file partially applied.
+! atlas migrate apply --url URL
+stderr 'executing statement "THIS IS A FAILING STATEMENT;" from version "3"'
+
+# Status will tell us about partial appliance.
+atlas migrate status --url URL
+stdout 'Migration Status: PENDING'
+stdout '  -- Current Version: 3 \(1 statements applied\)'
+stdout '  -- Next Version:    3 \(1 statements left\)'
+stdout '  -- Executed Files:  1 \(last one partially\)'
+stdout '  -- Pending Files:   1'
+stdout ''
+stdout 'Last migration attempt had errors:'
+stdout '  -- SQL:   THIS IS A FAILING STATEMENT;'
+
+# Running apply again with fixed file will solve it, only running the missing statement.
+cp 3_checkpoint.sql migrations/3_checkpoint.sql
+atlas migrate hash
+atlas migrate apply --url URL
+stdout 'Migrating to version 3 from 3 \(1 migrations in total\):'
+stdout ''
+stdout '  -- migrating version 3'
+stdout '    -> CREATE TABLE `tbl_2` \(`col` bigint\);'
+stdout '  -- ok'
+
+atlas migrate apply --url URL
+stdout 'No migration files to execute'
+
+-- migrations/1_first.sql --
+CREATE TABLE `tbl_1` (`col` bigint);
+
+-- migrations/2_second.sql --
+CREATE TABLE `tbl_2` (`col` bigint);
+
+-- migrations/3_checkpoint.sql --
+-- atlas:checkpoint
+
+CREATE TABLE `tbl_1` (`col` bigint);
+THIS IS A FAILING STATEMENT;
+
+-- 3_checkpoint.sql --
+-- atlas:checkpoint
+
+CREATE TABLE `tbl_1` (`col` bigint);
+CREATE TABLE `tbl_2` (`col` bigint);
+
diff --git a/internal/integration/testdata/mysql/cli-migrate-lint-add-notnull.txt b/internal/integration/testdata/mysql/cli-migrate-lint-add-notnull.txt
index 8000cc08..c6b69233 100644
--- a/internal/integration/testdata/mysql/cli-migrate-lint-add-notnull.txt
+++ b/internal/integration/testdata/mysql/cli-migrate-lint-add-notnull.txt
@@ -12,6 +12,9 @@ cmp got.txt empty.txt
 atlas migrate lint --dir file://migrations4 --dev-url URL --latest=1 > got.txt
 cmp got.txt expected4.txt
 
+atlas migrate lint --dir file://migrations5 --dev-url URL --latest=1 > got.txt
+cmp got.txt empty.txt
+
 -- empty.txt --
 -- migrations1/1.sql --
 CREATE TABLE users (id int);
@@ -51,3 +54,9 @@ UPDATE `users` SET `name` = CONCAT("name", id) WHERE `name` IS NULL;
 
 	L1: Adding a non-nullable "varchar" column "name" on table "users" without a default value implicitly sets existing rows with ""
 
+-- migrations5/1.sql --
+CREATE TABLE users (name varchar(36));
+
+-- migrations5/2.sql --
+-- Auto-increment fills existing rows.
+ALTER TABLE users ADD COLUMN `id` bigint PRIMARY KEY AUTO_INCREMENT;
diff --git a/schemahcl/schemahcl_test.go b/schemahcl/schemahcl_test.go
index 0c43ac8e..1928318d 100644
--- a/schemahcl/schemahcl_test.go
+++ b/schemahcl/schemahcl_test.go
@@ -893,3 +893,95 @@ attr2 = "b"
 	require.Equal(t, 3, cv.nb)
 	require.Equal(t, 2, cv.na)
 }
+
+func Test_ExtraReferences(t *testing.T) {
+	type (
+		Foo struct {
+			Name      string `spec:",name"`
+			Qualifier string `spec:",qualifier"`
+			DefaultExtension
+		}
+		Block1 struct {
+			Name string `spec:",name"`
+			Type string `spec:"type"`
+		}
+		Block2 struct {
+			Name string `spec:",name"`
+			Refs []*Ref `spec:"refs"`
+		}
+		ExtraBar struct {
+			Blk1 *Block1 `spec:"blk1"`
+			Blk2 *Block2 `spec:"blk2"`
+		}
+	)
+	var (
+		doc struct {
+			Foo []*Foo `spec:"foo"`
+		}
+		b = []byte(`
+foo "f1" {
+	extra_bar {
+		blk1 "c1" {
+			type = "c1"
+		}
+		blk2 "p1" {
+			refs = [blk1.c1]
+		}
+	}
+}
+`)
+	)
+	require.NoError(t, New().EvalBytes(b, &doc, nil))
+	require.Len(t, doc.Foo, 1)
+
+	extra, ok := doc.Foo[0].Extra.Resource("extra_bar")
+	require.True(t, ok)
+	var e ExtraBar
+	require.NoError(t, extra.As(&e))
+	require.Equal(t, &ExtraBar{
+		Blk1: &Block1{
+			Name: "c1",
+			Type: "c1",
+		},
+		Blk2: &Block2{
+			Name: "p1",
+			Refs: []*Ref{{V: "$blk1.c1"}},
+		},
+	}, &e)
+}
+
+func Test_ScopeContextOverride(t *testing.T) {
+	type (
+		Foo struct {
+			Name string `spec:",name"`
+		}
+		Bar struct {
+			Name string `spec:",name"`
+			Attr string `spec:"attr"`
+			Ref  *Ref   `spec:"ref"`
+		}
+	)
+	var (
+		doc struct {
+			Foo []*Foo `spec:"foo"`
+			Bar []*Bar `spec:"bar"`
+		}
+		b = []byte(`
+foo "f1" {}
+bar "b1" {
+	attr = foo
+	ref  = foo.f1
+}
+`)
+	)
+	require.NoError(t, New(
+		WithScopedEnums("bar.attr", "foo"), // foo is a valid value for bar.attr.
+	).EvalBytes(b, &doc, nil))
+	require.Len(t, doc.Foo, 1)
+	require.Len(t, doc.Bar, 1)
+	require.Equal(t, &Bar{
+		Name: "b1",
+		Attr: "foo",
+		Ref:  &Ref{V: "$foo.f1"},
+	}, doc.Bar[0])
+}
diff --git a/sql/internal/sqlx/plan_test.go b/sql/internal/sqlx/plan_test.go
index 029c19d8..a4eddb5e 100644
--- a/sql/internal/sqlx/plan_test.go
+++ b/sql/internal/sqlx/plan_test.go
@@ -153,3 +153,16 @@ func TestCheckChangesScope(t *testing.T) {
 	})
 	require.EqualError(t, err, "found 2 schemas when migration plan is scoped to one: [\"s1\" \"s2\"]")
 }
+
+func TestBadTypeComparison(t *testing.T) {
+	var o1 struct {
+		schema.Type
+		schema.Object
+		_ []func()
+	}
+	require.Panics(t, func() { _ = schema.Type(o1) == schema.Type(o1) })
+	SortChanges([]schema.Change{
+		&schema.DropObject{O: o1},
+		&schema.DropTable{T: schema.NewTable("t1").AddColumns(schema.NewColumn("c1").SetType(o1))},
+	})
+}
diff --git a/sql/migrate/lex_test.go b/sql/migrate/lex_test.go
index 799086f2..79e8ee43 100644
--- a/sql/migrate/lex_test.go
+++ b/sql/migrate/lex_test.go
@@ -21,13 +21,34 @@ func TestLocalFile_Stmts(t *testing.T) {
 	require.NoError(t, err)
 	for _, f := range files {
 		stmts, err := f.Stmts()
-		require.NoError(t, err)
+		require.NoErrorf(t, err, "file: %s", f.Name())
 		buf, err := os.ReadFile(filepath.Join(path, f.Name()+".golden"))
 		require.NoError(t, err)
 		require.Equalf(t, string(buf), strings.Join(stmts, "\n-- end --\n"), "mismatched statements in file %q", f.Name())
 	}
 }
 
+func TestScanner_StmtsGroup(t *testing.T) {
+	scan := &Scanner{}
+	scan.MatchBegin = true
+	path := filepath.Join("testdata", "lexgroup")
+	dir, err := NewLocalDir(path)
+	require.NoError(t, err)
+	files, err := dir.Files()
+	require.NoError(t, err)
+	for _, f := range files {
+		stmts, err := scan.Scan(string(f.Bytes()))
+		require.NoErrorf(t, err, "file: %s", f.Name())
+		buf, err := os.ReadFile(filepath.Join(path, f.Name()+".golden"))
+		require.NoError(t, err)
+		got := make([]string, len(stmts))
+		for i, s := range stmts {
+			got[i] = s.Text
+		}
+		require.Equalf(t, string(buf), strings.Join(got, "\n-- end --\n"), "mismatched statements in file %q", f.Name())
+	}
+}
+
 func TestLocalFile_StmtDecls(t *testing.T) {
 	f := `cmd0;
 -- test
@@ -157,9 +178,7 @@ func TestLex_Errors(t *testing.T) {
 		},
 	} {
 		t.Run(tt.name, func(t *testing.T) {
-			l, err := newLex(tt.stmt)
-			require.NoError(t, err)
-			_, err = l.stmt()
+			_, err := Stmts(tt.stmt)
 			require.EqualError(t, err, tt.err)
 		})
 	}
diff --git a/sql/migrate/migrate_test.go b/sql/migrate/migrate_test.go
index 5d098051..8f44e98b 100644
--- a/sql/migrate/migrate_test.go
+++ b/sql/migrate/migrate_test.go
@@ -263,6 +263,7 @@ func TestExecutor_Replay(t *testing.T) {
 
 func TestExecutor_Pending(t *testing.T) {
 	var (
+		ctx  = context.Background()
 		drv  = &mockDriver{}
 		rrw  = &mockRevisionReadWriter{}
 		log  = &mockLogger{}
@@ -289,80 +290,94 @@ func TestExecutor_Pending(t *testing.T) {
 			Hash:        "+O40cAXHgvMClnynHd5wggPAeZAk7zSEaNgzXCZOfmY=",
 		}
 	)
-	dir, err := migrate.NewLocalDir(filepath.Join("testdata/migrate", "sub"))
+	dir, err := migrate.NewLocalDir(filepath.Join("testdata", "migrate", "sub"))
 	require.NoError(t, err)
 	ex, err := migrate.NewExecutor(drv, dir, rrw, migrate.WithLogger(log))
 	require.NoError(t, err)
 
 	// All are pending
-	p, err := ex.Pending(context.Background())
+	p, err := ex.Pending(ctx)
 	require.NoError(t, err)
 	require.Len(t, p, 3)
 
 	// 2 are pending.
 	*rrw = []*migrate.Revision{rev1}
-	p, err = ex.Pending(context.Background())
+	p, err = ex.Pending(ctx)
 	require.NoError(t, err)
 	require.Len(t, p, 2)
 
 	// Only the last one is pending (in full).
 	*rrw = []*migrate.Revision{rev1, rev2}
-	p, err = ex.Pending(context.Background())
+	p, err = ex.Pending(ctx)
 	require.NoError(t, err)
 	require.Len(t, p, 1)
 
 	// First statement of last one is marked as applied, second isn't. Third file is still pending.
 	*rrw = []*migrate.Revision{rev1, rev2, rev3}
-	p, err = ex.Pending(context.Background())
+	p, err = ex.Pending(ctx)
 	require.NoError(t, err)
 	require.Len(t, p, 1)
 
 	// Nothing to do if all migrations are applied.
 	rev3.Applied = rev3.Total
 	*rrw = []*migrate.Revision{rev1, rev2, rev3}
-	p, err = ex.Pending(context.Background())
+	p, err = ex.Pending(ctx)
 	require.ErrorIs(t, err, migrate.ErrNoPendingFiles)
 	require.Len(t, p, 0)
 
 	// If there is a revision in the past with no existing migration, we don't care.
 	rev3.Applied = rev3.Total
 	*rrw = []*migrate.Revision{{Version: "2.11"}, rev3}
-	p, err = ex.Pending(context.Background())
+	p, err = ex.Pending(ctx)
 	require.ErrorIs(t, err, migrate.ErrNoPendingFiles)
 	require.Len(t, p, 0)
 
 	// If only the last migration file is applied, we expect there are no pending files.
 	*rrw = []*migrate.Revision{rev3}
-	p, err = ex.Pending(context.Background())
+	p, err = ex.Pending(ctx)
 	require.ErrorIs(t, err, migrate.ErrNoPendingFiles)
 	require.Len(t, p, 0)
 
 	// If the last applied revision has no matching migration file, and the last
 	// migration version precedes that revision, there is nothing to do.
 	*rrw = []*migrate.Revision{{Version: "5"}}
-	p, err = ex.Pending(context.Background())
+	p, err = ex.Pending(ctx)
 	require.ErrorIs(t, err, migrate.ErrNoPendingFiles)
 	require.Len(t, p, 0)
 
 	// The applied revision precedes every migration file. Expect all files pending.
 	*rrw = []*migrate.Revision{{Version: "1.1"}}
-	p, err = ex.Pending(context.Background())
+	p, err = ex.Pending(ctx)
 	require.NoError(t, err)
 	require.Len(t, p, 3)
 
 	// All except one file are applied. The latest revision does not exist and its version is smaller than the last
 	// migration file. Expect the last file pending.
 	*rrw = []*migrate.Revision{rev1, rev2, {Version: "2.11"}}
-	p, err = ex.Pending(context.Background())
+	p, err = ex.Pending(ctx)
 	require.NoError(t, err)
 	require.Len(t, p, 1)
 	require.Equal(t, rev3.Version, p[0].Version())
 
 	// If the last revision is partially applied and the matching migration file does not exist, we have a problem.
 	*rrw = []*migrate.Revision{{Version: "deleted", Description: "desc", Total: 1}}
-	p, err = ex.Pending(context.Background())
+	p, err = ex.Pending(ctx)
 	require.EqualError(t, err, migrate.MissingMigrationError{Version: "deleted", Description: "desc"}.Error())
 	require.Len(t, p, 0)
+
+	// If the last revision is a partially applied checkpoint, expect it to be part of
+	// the pending files. Checkpoint files following the failed one are not included.
+	dir, err = migrate.NewLocalDir(filepath.Join("testdata", "partial-checkpoint"))
+	require.NoError(t, err)
+	ex, err = migrate.NewExecutor(drv, dir, rrw, migrate.WithLogger(log))
+	require.NoError(t, err)
+	*rrw = []*migrate.Revision{{Version: "3", Description: "checkpoint", Total: 2, Applied: 1}}
+	p, err = ex.Pending(ctx)
+	require.NoError(t, err)
+	require.Len(t, p, 3)
+	require.Equal(t, "3", p[0].Version())
+	require.Equal(t, "4", p[1].Version())
+	require.Equal(t, "6", p[2].Version())
 }
 
 func TestExecutor_ExecOrderLinear(t *testing.T) {
diff --git a/sql/migrate/testdata/lex/15_dollar_quote.sql b/sql/migrate/testdata/lex/15_dollar_quote.sql
index 201af985..da728683 100644
--- a/sql/migrate/testdata/lex/15_dollar_quote.sql
+++ b/sql/migrate/testdata/lex/15_dollar_quote.sql
@@ -124,4 +124,14 @@ CREATE FUNCTION my_function() RETURNS void AS $$
 BEGIN
    -- function body here
 END;
-$$ LANGUAGE plpgsql;
\ No newline at end of file
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION f1(target regclass)
+  RETURNS VOID AS $BEGIN$
+DECLARE
+table_name TEXT := quote_ident(target :: text);
+BEGIN
+EXECUTE 'alter table ' || table_name || ' add column id serial';
+END;
+$BEGIN$
+LANGUAGE plpgsql;
\ No newline at end of file
diff --git a/sql/migrate/testdata/lex/15_dollar_quote.sql.golden b/sql/migrate/testdata/lex/15_dollar_quote.sql.golden
index dbe356f7..6acebcea 100644
--- a/sql/migrate/testdata/lex/15_dollar_quote.sql.golden
+++ b/sql/migrate/testdata/lex/15_dollar_quote.sql.golden
@@ -131,4 +131,14 @@ CREATE FUNCTION my_function() RETURNS void AS $$
 BEGIN
    -- function body here
 END;
-$$ LANGUAGE plpgsql;
\ No newline at end of file
+$$ LANGUAGE plpgsql;
+-- end --
+CREATE OR REPLACE FUNCTION f1(target regclass)
+  RETURNS VOID AS $BEGIN$
+DECLARE
+table_name TEXT := quote_ident(target :: text);
+BEGIN
+EXECUTE 'alter table ' || table_name || ' add column id serial';
+END;
+$BEGIN$
+LANGUAGE plpgsql;
\ No newline at end of file
diff --git a/sql/migrate/testdata/lexgroup/1_trigger.sql b/sql/migrate/testdata/lexgroup/1_trigger.sql
new file mode 100644
index 00000000..da741c33
--- /dev/null
+++ b/sql/migrate/testdata/lexgroup/1_trigger.sql
@@ -0,0 +1,72 @@
+-- SQLite test cases.
+
+CREATE TRIGGER before_tbl_insert BEFORE INSERT ON tbl BEGIN SELECT CASE
+    WHEN (new.a = 4) THEN RAISE(IGNORE) END;
+END;
+
+CREATE TRIGGER after_tbl_insert AFTER INSERT ON tbl BEGIN SELECT CASE
+    WHEN (new.a = 1) THEN RAISE(ABORT,    'Trigger abort')
+    WHEN (new.a = 2) THEN RAISE(FAIL,     'Trigger fail')
+    WHEN (new.a = 3) THEN RAISE(ROLLBACK, 'Trigger rollback') END;
+END;
+
+CREATE TRIGGER after_tbl2_insert AFTER INSERT ON tbl2 BEGIN
+    UPDATE tbl SET c = 10;
+    INSERT INTO tbl2 VALUES (new.a, new.b, new.c);
+END;
+
+CREATE TABLE t2(x,y,z);
+CREATE TRIGGER t2r1 AFTER INSERT ON t2 BEGIN SELECT 1; END;
+CREATE TRIGGER t2r2 BEFORE INSERT ON t2 BEGIN SELECT 1; END;
+CREATE TRIGGER t2r3 AFTER UPDATE ON t2 BEGIN SELECT 1; END;
+CREATE TRIGGER t2r4 BEFORE UPDATE ON t2 BEGIN SELECT 1; END;
+CREATE TRIGGER t2r5 AFTER DELETE ON t2 BEGIN SELECT 1; END;
+CREATE TRIGGER t2r6 BEFORE DELETE ON t2 BEGIN SELECT 1; END;
+CREATE TRIGGER t2r7 AFTER INSERT ON t2 BEGIN SELECT 1; END;
+CREATE TRIGGER t2r8 BEFORE INSERT ON t2 BEGIN SELECT 1; END;
+CREATE TRIGGER t2r9 AFTER UPDATE ON t2 BEGIN SELECT 1; END;
+CREATE TRIGGER t2r10 BEFORE UPDATE ON t2 BEGIN SELECT 1; END;
+CREATE TRIGGER t2r11 AFTER DELETE ON t2 BEGIN SELECT 1; END;
+CREATE TRIGGER t2r12 BEFORE DELETE ON t2 BEGIN SELECT 1; END;
+
+create trigger I_test instead of insert on test
+  begin
+    insert into test1 (id,a) values (NEW.id,NEW.a);
+    insert into test2 (id,b) values (NEW.id,NEW.b);
+  end;
+
+CREATE TRIGGER tr AFTER INSERT ON t3 BEGIN
+  INSERT INTO t3 SELECT new.c+1 WHERE new.c<5;
+  INSERT INTO t2 SELECT new.c*10000+xx.a*100+yy.a
+                   FROM t1 AS xx, t1 AS yy
+                  WHERE xx.a IN (1,2,3,4)
+                    AND yy.a IN (2,3,4,5);
+END;
+
+CREATE TABLE Item(
+   a integer PRIMARY KEY NOT NULL ,
+   b double NULL ,
+   c int NOT NULL DEFAULT 0
+);
+CREATE TABLE Undo(UndoAction TEXT);
+INSERT INTO Item VALUES (1,38205.60865,340);
+CREATE TRIGGER trigItem_UNDO_AD AFTER DELETE ON Item FOR EACH ROW
+BEGIN
+  INSERT INTO Undo SELECT 'INSERT INTO Item (a,b,c) VALUES ('
+   || coalesce(old.a,'NULL') || ',' || quote(old.b) || ',' || old.c || ');';
+END;
+DELETE FROM Item WHERE a = 1;
+SELECT * FROM Undo;
+
+CREATE TRIGGER transactionTrigger AFTER INSERT ON tbl10 BEGIN
+    SAVEPOINT sp1;
+    INSERT INTO tbl11 VALUES (new.f);
+    ROLLBACK TO sp1;
+END;
+
+CREATE TRIGGER errorHandlingTrigger AFTER INSERT ON tbl12 BEGIN
+    BEGIN
+        INSERT INTO tbl13 VALUES (new.g);
+        INSERT INTO tbl13 VALUES (new.g);
+    END;
+END;
diff --git a/sql/migrate/testdata/lexgroup/1_trigger.sql.golden b/sql/migrate/testdata/lexgroup/1_trigger.sql.golden
new file mode 100644
index 00000000..306a5643
--- /dev/null
+++ b/sql/migrate/testdata/lexgroup/1_trigger.sql.golden
@@ -0,0 +1,87 @@
+CREATE TRIGGER before_tbl_insert BEFORE INSERT ON tbl BEGIN SELECT CASE
+    WHEN (new.a = 4) THEN RAISE(IGNORE) END;
+END;
+-- end --
+CREATE TRIGGER after_tbl_insert AFTER INSERT ON tbl BEGIN SELECT CASE
+    WHEN (new.a = 1) THEN RAISE(ABORT,    'Trigger abort')
+    WHEN (new.a = 2) THEN RAISE(FAIL,     'Trigger fail')
+    WHEN (new.a = 3) THEN RAISE(ROLLBACK, 'Trigger rollback') END;
+END;
+-- end --
+CREATE TRIGGER after_tbl2_insert AFTER INSERT ON tbl2 BEGIN
+    UPDATE tbl SET c = 10;
+    INSERT INTO tbl2 VALUES (new.a, new.b, new.c);
+END;
+-- end --
+CREATE TABLE t2(x,y,z);
+-- end --
+CREATE TRIGGER t2r1 AFTER INSERT ON t2 BEGIN SELECT 1; END;
+-- end --
+CREATE TRIGGER t2r2 BEFORE INSERT ON t2 BEGIN SELECT 1; END;
+-- end --
+CREATE TRIGGER t2r3 AFTER UPDATE ON t2 BEGIN SELECT 1; END;
+-- end --
+CREATE TRIGGER t2r4 BEFORE UPDATE ON t2 BEGIN SELECT 1; END;
+-- end --
+CREATE TRIGGER t2r5 AFTER DELETE ON t2 BEGIN SELECT 1; END;
+-- end --
+CREATE TRIGGER t2r6 BEFORE DELETE ON t2 BEGIN SELECT 1; END;
+-- end --
+CREATE TRIGGER t2r7 AFTER INSERT ON t2 BEGIN SELECT 1; END;
+-- end --
+CREATE TRIGGER t2r8 BEFORE INSERT ON t2 BEGIN SELECT 1; END;
+-- end --
+CREATE TRIGGER t2r9 AFTER UPDATE ON t2 BEGIN SELECT 1; END;
+-- end --
+CREATE TRIGGER t2r10 BEFORE UPDATE ON t2 BEGIN SELECT 1; END;
+-- end --
+CREATE TRIGGER t2r11 AFTER DELETE ON t2 BEGIN SELECT 1; END;
+-- end --
+CREATE TRIGGER t2r12 BEFORE DELETE ON t2 BEGIN SELECT 1; END;
+-- end --
+create trigger I_test instead of insert on test
+  begin
+    insert into test1 (id,a) values (NEW.id,NEW.a);
+    insert into test2 (id,b) values (NEW.id,NEW.b);
+  end;
+-- end --
+CREATE TRIGGER tr AFTER INSERT ON t3 BEGIN
+  INSERT INTO t3 SELECT new.c+1 WHERE new.c<5;
+  INSERT INTO t2 SELECT new.c*10000+xx.a*100+yy.a
+                   FROM t1 AS xx, t1 AS yy
+                  WHERE xx.a IN (1,2,3,4)
+                    AND yy.a IN (2,3,4,5);
+END;
+-- end --
+CREATE TABLE Item(
+   a integer PRIMARY KEY NOT NULL ,
+   b double NULL ,
+   c int NOT NULL DEFAULT 0
+);
+-- end --
+CREATE TABLE Undo(UndoAction TEXT);
+-- end --
+INSERT INTO Item VALUES (1,38205.60865,340);
+-- end --
+CREATE TRIGGER trigItem_UNDO_AD AFTER DELETE ON Item FOR EACH ROW
+BEGIN
+  INSERT INTO Undo SELECT 'INSERT INTO Item (a,b,c) VALUES ('
+   || coalesce(old.a,'NULL') || ',' || quote(old.b) || ',' || old.c || ');';
+END;
+-- end --
+DELETE FROM Item WHERE a = 1;
+-- end --
+SELECT * FROM Undo;
+-- end --
+CREATE TRIGGER transactionTrigger AFTER INSERT ON tbl10 BEGIN
+    SAVEPOINT sp1;
+    INSERT INTO tbl11 VALUES (new.f);
+    ROLLBACK TO sp1;
+END;
+-- end --
+CREATE TRIGGER errorHandlingTrigger AFTER INSERT ON tbl12 BEGIN
+    BEGIN
+        INSERT INTO tbl13 VALUES (new.g);
+        INSERT INTO tbl13 VALUES (new.g);
+    END;
+END;
\ No newline at end of file
diff --git a/sql/migrate/testdata/lexgroup/2_function.sql b/sql/migrate/testdata/lexgroup/2_function.sql
new file mode 100644
index 00000000..dc8037d5
--- /dev/null
+++ b/sql/migrate/testdata/lexgroup/2_function.sql
@@ -0,0 +1,126 @@
+
+CREATE PROCEDURE CompProc(IN p INT)
+BEGIN
+    DECLARE v1 INT DEFAULT 0;
+    DECLARE v2 INT;
+    DECLARE d INT DEFAULT FALSE;
+    DECLARE c1 CURSOR FOR SELECT a FROM t1 WHERE b = p;
+    DECLARE CONTINUE HANDLER FOR NOT FOUND SET d = TRUE;
+
+    OPEN c1;
+    rl: LOOP
+        FETCH c1 INTO v2;
+        IF d THEN
+            LEAVE rl;
+        END IF;
+
+        BEGIN
+            IF v2 > 0 THEN
+                UPDATE t2 SET c = c + v2 WHERE d = p;
+            END IF;
+        END;
+    END LOOP;
+    CLOSE c1;
+
+    BEGIN
+        SELECT COUNT(*) INTO v1 FROM t2 WHERE d = p;
+        IF v1 > 100 THEN
+            CALL OtherProc(p);
+        END IF;
+    END;
+
+END;
+
+
+CREATE FUNCTION CompFunc(eID INT) RETURNS INT
+BEGIN
+    DECLARE ts INT;
+    DECLARE b INT;
+
+    BEGIN
+        SELECT SUM(s) INTO ts FROM sales WHERE e = eID;
+    END;
+
+    BEGIN
+        IF ts > 10000 THEN
+            SET b = 500;
+        ELSEIF ts BETWEEN 5000 AND 10000 THEN
+            SET b = 300;
+        ELSE
+            SET b = 0;
+        END IF;
+    END;
+
+    RETURN b;
+END;
+
+
+CREATE PROCEDURE CompLogicProc()
+BEGIN
+    DECLARE i INT DEFAULT 1;
+    DECLARE t INT DEFAULT 0;
+
+    sl: WHILE i <= 10 DO
+        BEGIN
+            IF i MOD 2 = 0 THEN
+                SET t = t + i;
+            ELSE
+                BEGIN
+                    IF i MOD 3 = 0 THEN
+                        SET t = t - i;
+                    END IF;
+                END;
+            END IF;
+            SET i = i + 1;
+        END;
+    END WHILE;
+
+    BEGIN
+        IF t < 0 THEN
+            SET t = 0;
+        END IF;
+    END;
+
+    SELECT t;
+END;
+
+
+CREATE PROCEDURE ff1()
+BEGIN
+    DECLARE v1, v2, v3, i, j INT DEFAULT 0;
+    DECLARE flag INT DEFAULT FALSE;
+    DECLARE cur CURSOR FOR SELECT col1 FROM tableX;
+    DECLARE CONTINUE HANDLER FOR NOT FOUND SET flag = TRUE;
+
+    OPEN cur;
+    main_loop: LOOP
+        FETCH cur INTO v1;
+        IF flag THEN
+            LEAVE main_loop;
+        END IF;
+
+        SET i = 1;
+        WHILE i <= 5 DO
+            SET j = 1;
+            REPEAT
+                SET v2 = (SELECT COUNT(*) FROM tableY WHERE col2 = v1 AND col3 = j);
+                IF v2 > 3 THEN
+                    UPDATE tableZ SET col4 = col4 + 1 WHERE col5 = i;
+                END IF;
+                SET j = j + 1;
+            UNTIL j > 3 END REPEAT;
+            SET i = i + 1;
+        END WHILE;
+
+        compound_logic: BEGIN
+            IF v1 < 10 THEN
+                INSERT INTO tableA (colA) VALUES (v1);
+            ELSEIF v1 BETWEEN 10 AND 20 THEN
+                UPDATE tableB SET colB = v1 WHERE colB < v1;
+            ELSE
+                DELETE FROM tableC WHERE colC = v1;
+            END IF;
+        END;
+    END LOOP;
+    CLOSE cur;
+END;
\ No newline at end of file
diff --git a/sql/migrate/testdata/lexgroup/2_function.sql.golden b/sql/migrate/testdata/lexgroup/2_function.sql.golden
new file mode 100644
index 00000000..61c319d6
--- /dev/null
+++ b/sql/migrate/testdata/lexgroup/2_function.sql.golden
@@ -0,0 +1,122 @@
+CREATE PROCEDURE CompProc(IN p INT)
+BEGIN
+    DECLARE v1 INT DEFAULT 0;
+    DECLARE v2 INT;
+    DECLARE d INT DEFAULT FALSE;
+    DECLARE c1 CURSOR FOR SELECT a FROM t1 WHERE b = p;
+    DECLARE CONTINUE HANDLER FOR NOT FOUND SET d = TRUE;
+
+    OPEN c1;
+    rl: LOOP
+        FETCH c1 INTO v2;
+        IF d THEN
+            LEAVE rl;
+        END IF;
+
+        BEGIN
+            IF v2 > 0 THEN
+                UPDATE t2 SET c = c + v2 WHERE d = p;
+            END IF;
+        END;
+    END LOOP;
+    CLOSE c1;
+
+    BEGIN
+        SELECT COUNT(*) INTO v1 FROM t2 WHERE d = p;
+        IF v1 > 100 THEN
+            CALL OtherProc(p);
+        END IF;
+    END;
+
+END;
+-- end --
+CREATE FUNCTION CompFunc(eID INT) RETURNS INT
+BEGIN
+    DECLARE ts INT;
+    DECLARE b INT;
+
+    BEGIN
+        SELECT SUM(s) INTO ts FROM sales WHERE e = eID;
+    END;
+
+    BEGIN
+        IF ts > 10000 THEN
+            SET b = 500;
+        ELSEIF ts BETWEEN 5000 AND 10000 THEN
+            SET b = 300;
+        ELSE
+            SET b = 0;
+        END IF;
+    END;
+
+    RETURN b;
+END;
+-- end --
+CREATE PROCEDURE CompLogicProc()
+BEGIN
+    DECLARE i INT DEFAULT 1;
+    DECLARE t INT DEFAULT 0;
+
+    sl: WHILE i <= 10 DO
+        BEGIN
+            IF i MOD 2 = 0 THEN
+                SET t = t + i;
+            ELSE
+                BEGIN
+                    IF i MOD 3 = 0 THEN
+                        SET t = t - i;
+                    END IF;
+                END;
+            END IF;
+            SET i = i + 1;
+        END;
+    END WHILE;
+
+    BEGIN
+        IF t < 0 THEN
+            SET t = 0;
+        END IF;
+    END;
+
+    SELECT t;
+END;
+-- end --
+CREATE PROCEDURE ff1()
+BEGIN
+    DECLARE v1, v2, v3, i, j INT DEFAULT 0;
+    DECLARE flag INT DEFAULT FALSE;
+    DECLARE cur CURSOR FOR SELECT col1 FROM tableX;
+    DECLARE CONTINUE HANDLER FOR NOT FOUND SET flag = TRUE;
+
+    OPEN cur;
+    main_loop: LOOP
+        FETCH cur INTO v1;
+        IF flag THEN
+            LEAVE main_loop;
+        END IF;
+
+        SET i = 1;
+        WHILE i <= 5 DO
+            SET j = 1;
+            REPEAT
+                SET v2 = (SELECT COUNT(*) FROM tableY WHERE col2 = v1 AND col3 = j);
+                IF v2 > 3 THEN
+                    UPDATE tableZ SET col4 = col4 + 1 WHERE col5 = i;
+                END IF;
+                SET j = j + 1;
+            UNTIL j > 3 END REPEAT;
+            SET i = i + 1;
+        END WHILE;
+
+        compound_logic: BEGIN
+            IF v1 < 10 THEN
+                INSERT INTO tableA (colA) VALUES (v1);
+            ELSEIF v1 BETWEEN 10 AND 20 THEN
+                UPDATE tableB SET colB = v1 WHERE colB < v1;
+            ELSE
+                DELETE FROM tableC WHERE colC = v1;
+            END IF;
+        END;
+    END LOOP;
+    CLOSE cur;
+END;
\ No newline at end of file
diff --git a/sql/migrate/testdata/lexgroup/3_delimiter.sql b/sql/migrate/testdata/lexgroup/3_delimiter.sql
new file mode 100644
index 00000000..d0afb6b2
--- /dev/null
+++ b/sql/migrate/testdata/lexgroup/3_delimiter.sql
@@ -0,0 +1,18 @@
+-- delimiter should not conflict with compound statements scanning.
+
+create function `add2` (a int, b int) returns int deterministic no sql return a + b;
+create function `add3` (a int, b int, c int) returns int deterministic no sql return a + b + c;
+delimiter |
+-- error 1418
+create function fn1(x int) returns int deterministic
+begin
+       insert into t1 values (x);
+       return x+2;
+end|
+create function fn2(x int) returns int deterministic
+begin
+       insert into t1 values (x);
+       return x+2;
+end|
+delimiter ;
+create function `add4` (a int, b int, c int, d int) returns int deterministic no sql return a + b + c + d;
\ No newline at end of file
diff --git a/sql/migrate/testdata/lexgroup/3_delimiter.sql.golden b/sql/migrate/testdata/lexgroup/3_delimiter.sql.golden
new file mode 100644
index 00000000..f16478a6
--- /dev/null
+++ b/sql/migrate/testdata/lexgroup/3_delimiter.sql.golden
@@ -0,0 +1,17 @@
+create function `add2` (a int, b int) returns int deterministic no sql return a + b;
+-- end --
+create function `add3` (a int, b int, c int) returns int deterministic no sql return a + b + c;
+-- end --
+create function fn1(x int) returns int deterministic
+begin
+       insert into t1 values (x);
+       return x+2;
+end
+-- end --
+create function fn2(x int) returns int deterministic
+begin
+       insert into t1 values (x);
+       return x+2;
+end
+-- end --
+create function `add4` (a int, b int, c int, d int) returns int deterministic no sql return a + b + c + d;
\ No newline at end of file
diff --git a/sql/migrate/testdata/partial-checkpoint/1_first.sql b/sql/migrate/testdata/partial-checkpoint/1_first.sql
new file mode 100644
index 00000000..b884394b
--- /dev/null
+++ b/sql/migrate/testdata/partial-checkpoint/1_first.sql
@@ -0,0 +1,1 @@
+create table `tbl_1` (`col` int);
diff --git a/sql/migrate/testdata/partial-checkpoint/2_second.sql b/sql/migrate/testdata/partial-checkpoint/2_second.sql
new file mode 100644
index 00000000..b1d0c7ff
--- /dev/null
+++ b/sql/migrate/testdata/partial-checkpoint/2_second.sql
@@ -0,0 +1,1 @@
+create table `tbl_2` (`col` int);
diff --git a/sql/migrate/testdata/partial-checkpoint/3_checkpoint.sql b/sql/migrate/testdata/partial-checkpoint/3_checkpoint.sql
new file mode 100644
index 00000000..a8eeec80
--- /dev/null
+++ b/sql/migrate/testdata/partial-checkpoint/3_checkpoint.sql
@@ -0,0 +1,4 @@
+-- atlas:checkpoint
+
+CREATE TABLE `tbl_1` (`col` int);
+CREATE TABLE `tbl_2` (`col` int);
diff --git a/sql/migrate/testdata/partial-checkpoint/4_fourth.sql b/sql/migrate/testdata/partial-checkpoint/4_fourth.sql
new file mode 100644
index 00000000..c227813f
--- /dev/null
+++ b/sql/migrate/testdata/partial-checkpoint/4_fourth.sql
@@ -0,0 +1,1 @@
+create table `tbl_3` (`col` int);
diff --git a/sql/migrate/testdata/partial-checkpoint/5_checkpoint.sql b/sql/migrate/testdata/partial-checkpoint/5_checkpoint.sql
new file mode 100644
index 00000000..d5fda506
--- /dev/null
+++ b/sql/migrate/testdata/partial-checkpoint/5_checkpoint.sql
@@ -0,0 +1,5 @@
+-- atlas:checkpoint
+
+CREATE TABLE `tbl_1` (`col` int);
+CREATE TABLE `tbl_2` (`col` int);
+create table `tbl_3` (`col` int);
diff --git a/sql/migrate/testdata/partial-checkpoint/6_sixth.sql b/sql/migrate/testdata/partial-checkpoint/6_sixth.sql
new file mode 100644
index 00000000..78bac932
--- /dev/null
+++ b/sql/migrate/testdata/partial-checkpoint/6_sixth.sql
@@ -0,0 +1,1 @@
+CREATE TABLE `tbl_4` (`col` int);
diff --git a/sql/migrate/testdata/partial-checkpoint/atlas.sum b/sql/migrate/testdata/partial-checkpoint/atlas.sum
new file mode 100644
index 00000000..380705c7
--- /dev/null
+++ b/sql/migrate/testdata/partial-checkpoint/atlas.sum
@@ -0,0 +1,7 @@
+h1:POfbsH2G2GIXAZQ2CdPa5ufTxcLpcnperz/gX0Vw6Uk=
+1_first.sql h1:uJ2MyyeTqXx4rCqtRMQAvRrfsvdwmvsZ3LcadVvCdOc=
+2_second.sql h1:TLO00GtsqSi8o0ySoaofBqxemrElL8fHOs3IRnqYvB8=
+3_checkpoint.sql h1:LR4UjDYVtrRuHLN5GQwLAYnXrSf/iO70psY8hTmLmjw=
+4_fourth.sql h1:QvaneqKxg+3R+zNzCP/wW43XkECdHkUwEpxJKPICEP8=
+5_checkpoint.sql h1:syyuaJb/hoMRO7MJlv4AMhYqMCoSb8ZgcHENO8RqN3I=
+6_sixth.sql h1:JNhpyBtH7XKqGBNRHQ+AvM9IDpO08C3G/swWPHfLkfw=
diff --git a/sql/mysql/migrate_test.go b/sql/mysql/migrate_test.go
index 922f2f5d..1973fd05 100644
--- a/sql/mysql/migrate_test.go
+++ b/sql/mysql/migrate_test.go
@@ -27,9 +27,11 @@ func TestMigrate_ApplyChanges(t *testing.T) {
 		WillReturnResult(sqlmock.NewResult(0, 1))
 	mk.ExpectExec(sqltest.Escape("DROP TABLE `users`")).
 		WillReturnResult(sqlmock.NewResult(0, 0))
+	mk.ExpectExec(sqltest.Escape("CREATE TABLE `users` (`id` bigint NOT NULL)")).
+		WillReturnResult(sqlmock.NewResult(0, 0))
 	mk.ExpectExec(sqltest.Escape("DROP TABLE IF EXISTS `public`.`pets`")).
 		WillReturnResult(sqlmock.NewResult(0, 0))
-	mk.ExpectExec(sqltest.Escape("CREATE TABLE IF NOT EXISTS `pets` (`a` int NOT NULL DEFAULT (int(rand())), `b` bigint NOT NULL DEFAULT 1, `c` bigint NULL, PRIMARY KEY (`a`, `b`), UNIQUE INDEX `b_c_unique` (`b`, `c`) COMMENT \"comment\")")).
+	mk.ExpectExec(sqltest.Escape("CREATE TABLE IF NOT EXISTS `public`.`pets` (`a` int NOT NULL DEFAULT (int(rand())), `b` bigint NOT NULL DEFAULT 1, `c` bigint NULL, PRIMARY KEY (`a`, `b`), UNIQUE INDEX `b_c_unique` (`b`, `c`) COMMENT \"comment\")")).
 		WillReturnResult(sqlmock.NewResult(0, 0))
 	mk.ExpectExec(sqltest.Escape("ALTER TABLE `users` DROP INDEX `id_spouse_id`")).
 		WillReturnResult(sqlmock.NewResult(0, 0))
@@ -39,6 +41,7 @@ func TestMigrate_ApplyChanges(t *testing.T) {
 		WillReturnResult(sqlmock.NewResult(0, 0))
 	mk.ExpectExec(sqltest.Escape("CREATE TABLE `comments` (`id` bigint NOT NULL, `post_id` bigint NULL, CONSTRAINT `comment` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`))")).
 		WillReturnResult(sqlmock.NewResult(0, 0))
+	ns := &schema.Schema{Name: "public"}
 	err = migrate.ApplyChanges(context.Background(), []schema.Change{
 		&schema.AddSchema{S: &schema.Schema{Name: "test", Attrs: []schema.Attr{&schema.Charset{V: "latin"}}}},
 		&schema.DropSchema{S: &schema.Schema{Name: "atlas", Attrs: []schema.Attr{&schema.Charset{V: "latin"}}}},
@@ -50,10 +53,18 @@ func TestMigrate_ApplyChanges(t *testing.T) {
 				},
 			},
 		},
+		&schema.AddTable{
+			T: &schema.Table{
+				Name: "users",
+				Columns: []*schema.Column{
+					{Name: "id", Type: &schema.ColumnType{Raw: "bigint", Type: &schema.IntegerType{T: "bigint"}}},
+				},
+			},
+		},
 		&schema.DropTable{
 			T: &schema.Table{
 				Name:   "pets",
-				Schema: &schema.Schema{Name: "public"},
+				Schema: ns,
 				Columns: []*schema.Column{
 					{Name: "id", Type: &schema.ColumnType{Raw: "bigint", Type: &schema.IntegerType{T: "bigint"}}},
 				},
@@ -63,7 +74,8 @@ func TestMigrate_ApplyChanges(t *testing.T) {
 		&schema.AddTable{
 			T: func() *schema.Table {
 				t := &schema.Table{
-					Name: "pets",
+					Name:   "pets",
+					Schema: ns,
 					Columns: []*schema.Column{
 						{Name: "a", Type: &schema.ColumnType{Raw: "int", Type: &schema.IntegerType{T: "int"}}, Default: &schema.RawExpr{X: "(int(rand()))"}},
 						{Name: "b", Type: &schema.ColumnType{Raw: "bigint", Type: &schema.IntegerType{T: "bigint"}}, Default: &schema.Literal{V: "1"}},
diff --git a/sql/mysql/mysqlcheck/mysqlcheck_test.go b/sql/mysql/mysqlcheck/mysqlcheck_test.go
index df87de13..75cf6a66 100644
--- a/sql/mysql/mysqlcheck/mysqlcheck_test.go
+++ b/sql/mysql/mysqlcheck/mysqlcheck_test.go
@@ -99,6 +99,7 @@ func TestDataDepend_MySQL8_ImplicitUpdate(t *testing.T) {
 									),
 								Changes: []schema.Change{
 									&schema.AddColumn{C: schema.NewTimeColumn("b", mysql.TypeTimestamp)},
+									&schema.AddColumn{C: schema.NewIntColumn("id", mysql.TypeBigInt).AddAttrs(&mysql.AutoIncrement{})},
 								},
 							},
 						},
diff --git a/sql/postgres/diff_test.go b/sql/postgres/diff_test.go
index 02515d6b..e8763f84 100644
--- a/sql/postgres/diff_test.go
+++ b/sql/postgres/diff_test.go
@@ -484,6 +484,27 @@ func TestDiff_TableDiff(t *testing.T) {
 	}
 }
 
+func TestDiff_RealmDiff(t *testing.T) {
+	db, m, err := sqlmock.New()
+	require.NoError(t, err)
+	mock{m}.version("130000")
+	drv, err := Open(db)
+	to := schema.New("public").
+		AddTables(
+			schema.NewTable("users").AddColumns(schema.NewIntColumn("t2_id", "int")),
+		).
+		AddObjects(
+			&schema.EnumType{T: "e", Values: []string{"b"}},
+		)
+	changes, err := drv.RealmDiff(schema.NewRealm(), schema.NewRealm(to))
+	require.NoError(t, err)
+	require.EqualValues(t, []schema.Change{
+		&schema.AddSchema{S: to},
+		&schema.AddObject{O: to.Objects[0]},
+		&schema.AddTable{T: to.Tables[0]},
+	}, changes)
+}
+
 func TestDiff_SchemaDiff(t *testing.T) {
 	db, m, err := sqlmock.New()
 	require.NoError(t, err)
diff --git a/sql/postgres/migrate_test.go b/sql/postgres/migrate_test.go
index da71c206..f4befb03 100644
--- a/sql/postgres/migrate_test.go
+++ b/sql/postgres/migrate_test.go
@@ -368,12 +368,12 @@ func TestPlanChanges(t *testing.T) {
 		},
 		{
 			changes: []schema.Change{
-				&schema.DropObject{O: &schema.EnumType{T: "status", Values: []string{"on", "off"}, Schema: schema.New("public")}},
 				&schema.DropTable{T: schema.NewTable("logs").
 					AddColumns(
 						schema.NewColumn("status").SetType(&schema.EnumType{T: "status", Values: []string{"on", "off"}, Schema: schema.New("public")}),
 					),
 				},
+				&schema.DropObject{O: &schema.EnumType{T: "status", Values: []string{"on", "off"}, Schema: schema.New("public")}},
 				&schema.DropObject{O: &schema.EnumType{T: "state", Values: []string{"on", "off"}, Schema: schema.New("public")}},
 			},
 			wantPlan: &migrate.Plan{
@@ -872,13 +872,6 @@ 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")).
@@ -887,6 +880,13 @@ func TestPlanChanges(t *testing.T) {
 							schema.NewEnumColumn("status", schema.EnumName("status"), schema.EnumValues("on", "off"), schema.EnumSchema(schema.New("public"))),
 						),
 				},
+				&schema.DropObject{
+					O: &schema.EnumType{
+						T:      "state",
+						Values: []string{"on", "off"},
+						Schema: schema.New("public"),
+					},
+				},
 				&schema.DropObject{
 					O: &schema.EnumType{
 						T:      "status",
@@ -940,6 +940,12 @@ func TestPlanChanges(t *testing.T) {
 							),
 					)
 				return []schema.Change{
+					&schema.DropTable{
+						T: s.Tables[0],
+					},
+					&schema.DropTable{
+						T: s.Tables[1],
+					},
 					&schema.DropObject{
 						O: &schema.EnumType{
 							T:      "state",
@@ -947,12 +953,6 @@ func TestPlanChanges(t *testing.T) {
 							Schema: schema.New("public"),
 						},
 					},
-					&schema.DropTable{
-						T: s.Tables[0],
-					},
-					&schema.DropTable{
-						T: s.Tables[1],
-					},
 				}
 			}(),
 			wantPlan: &migrate.Plan{
diff --git a/sql/postgres/sqlspec_test.go b/sql/postgres/sqlspec_test.go
index fa788677..e96c634b 100644
--- a/sql/postgres/sqlspec_test.go
+++ b/sql/postgres/sqlspec_test.go
@@ -12,7 +12,6 @@ import (
 	"ariga.io/atlas/sql/internal/spectest"
 	"ariga.io/atlas/sql/internal/sqlx"
 	"ariga.io/atlas/sql/schema"
-	"github.com/hashicorp/hcl/v2/hclparse"
 	"github.com/stretchr/testify/require"
 )
 
@@ -739,7 +738,7 @@ table "logs" {
 				}
 			}
 		`), &schema.Schema{}, nil)
-		require.EqualError(t, err, `specutil: cannot convert table "logs": missing columns or expressions for logs.partition`)
+		require.EqualError(t, err, `cannot convert table "logs": missing columns or expressions for logs.partition`)
 
 		err = EvalHCLBytes([]byte(`
 			schema "test" {}
@@ -753,7 +752,7 @@ table "logs" {
 				}
 			}
 		`), &schema.Schema{}, nil)
-		require.EqualError(t, err, `specutil: cannot convert table "logs": multiple definitions for logs.partition, use "columns" or "by"`)
+		require.EqualError(t, err, `cannot convert table "logs": multiple definitions for logs.partition, use "columns" or "by"`)
 	})
 }
 
@@ -2239,52 +2238,3 @@ schema "s3" {
 }
 `, string(buf))
 }
-
-func TestEvalHCLError(t *testing.T) {
-	for _, tt := range []struct {
-		text, err string
-	}{
-		{
-			text: `
-			table "test" {
-			  column "id" {
-			    type = string
-			  }
-			}`,
-			err: `schema.hcl:4,15-21: Unknown column.type; There is no type named "string".`,
-		},
-		{
-			text: `
-			table "test" {
-			  index "i" {
-			    type = unknown
-			  }
-			}`,
-			err: `schema.hcl:4,15-22: Unknown index.type; There is no type named "unknown".`,
-		},
-		{
-			text: `
-			table "test" {
-			  foreign_key "f" {
-			    on_delete = unknown
-			  }
-			}`,
-			err: `schema.hcl:4,20-27: Unknown foreign_key.on_delete; There is no on_delete named "unknown".`,
-		},
-		{
-			text: `
-			table "test" {
-			  partition {
-			    type = UNKNOWN
-			  }
-			}`,
-			err: `schema.hcl:4,15-22: Unknown partition.type; There is no type named "UNKNOWN".`,
-		},
-	} {
-		p := hclparse.NewParser()
-		_, diags := p.ParseHCL([]byte(tt.text), "schema.hcl")
-		require.False(t, diags.HasErrors())
-		err := EvalHCL.Eval(p, &schema.Schema{}, nil)
-		require.EqualError(t, err, tt.err)
-	}
-}
diff --git a/sql/internal/sqlx/exclude_test.go b/sql/schema/exclude_test.go
similarity index 61%
rename from sql/internal/sqlx/exclude_test.go
rename to sql/schema/exclude_test.go
index 0fd0c26b..cae7a2df 100644
--- a/sql/internal/sqlx/exclude_test.go
+++ b/sql/schema/exclude_test.go
@@ -2,18 +2,16 @@
 // This source code is licensed under the Apache 2.0 license found
 // in the LICENSE file in the root directory of this source tree.
 
-package sqlx
+package schema
 
 import (
 	"testing"
 
-	"ariga.io/atlas/sql/schema"
-
 	"github.com/stretchr/testify/require"
 )
 
 func TestExcludeRealm_Schemas(t *testing.T) {
-	r := schema.NewRealm(schema.New("s1"), schema.New("s2"), schema.New("s3"))
+	r := NewRealm(New("s1"), New("s2"), New("s3"))
 	r, err := ExcludeRealm(r, []string{"s4"})
 	require.NoError(t, err)
 	require.Len(t, r.Schemas, 3)
@@ -28,7 +26,7 @@ func TestExcludeRealm_Schemas(t *testing.T) {
 	require.NoError(t, err)
 	require.Empty(t, r.Schemas)
 
-	r = schema.NewRealm(schema.New("s1"), schema.New("s2"), schema.New("s3"))
+	r = NewRealm(New("s1"), New("s2"), New("s3"))
 	r, err = ExcludeRealm(r, []string{"s*.*", "s*.*.*"})
 	require.NoError(t, err)
 	require.Len(t, r.Schemas, 3)
@@ -38,19 +36,19 @@ func TestExcludeRealm_Schemas(t *testing.T) {
 }
 
 func TestExcludeRealm_Tables(t *testing.T) {
-	r := schema.NewRealm(
-		schema.New("s0"),
-		schema.New("s1").AddTables(
-			schema.NewTable("t1"),
+	r := NewRealm(
+		New("s0"),
+		New("s1").AddTables(
+			NewTable("t1"),
 		),
-		schema.New("s2").AddTables(
-			schema.NewTable("t1"),
-			schema.NewTable("t2"),
+		New("s2").AddTables(
+			NewTable("t1"),
+			NewTable("t2"),
 		),
-		schema.New("s3").AddTables(
-			schema.NewTable("t1"),
-			schema.NewTable("t2"),
-			schema.NewTable("t3"),
+		New("s3").AddTables(
+			NewTable("t1"),
+			NewTable("t2"),
+			NewTable("t3"),
 		),
 	)
 	r, err := ExcludeRealm(r, []string{"s4"})
@@ -94,24 +92,87 @@ func TestExcludeRealm_Tables(t *testing.T) {
 	require.Empty(t, r.Schemas[2].Tables)
 }
 
+func TestExcludeRealm_Enums(t *testing.T) {
+	e1, e2, e3 := &EnumType{T: "e1"}, &EnumType{T: "e2"}, &EnumType{T: "e3"}
+	s1, s2, s3 := New("s1").AddObjects(e1), New("s2").AddObjects(e1, e2), New("s3").AddObjects(e1, e2, e3)
+
+	r, err := ExcludeRealm(NewRealm(s1, s2, s3), []string{"s1.e1"})
+	require.NoError(t, err)
+	require.Len(t, r.Schemas, 3)
+	require.Empty(t, r.Schemas[0].Objects)
+	require.Len(t, r.Schemas[1].Objects, 2)
+	require.Len(t, r.Schemas[2].Objects, 3)
+
+	// Wrong selector.
+	r, err = ExcludeRealm(NewRealm(s1, s2, s3), []string{"*.e1[type=table]"})
+	require.NoError(t, err)
+	require.Len(t, r.Schemas, 3)
+	require.Empty(t, r.Schemas[0].Objects)
+	require.Len(t, r.Schemas[1].Objects, 2)
+	require.Len(t, r.Schemas[2].Objects, 3)
+
+	// Enum selector.
+	r, err = ExcludeRealm(NewRealm(s1, s2, s3), []string{"*.e1[type=enum]"})
+	require.NoError(t, err)
+	require.Len(t, r.Schemas, 3)
+	require.Empty(t, r.Schemas[0].Objects)
+	require.Len(t, r.Schemas[1].Objects, 1)
+	require.Equal(t, []Object{e2}, r.Schemas[1].Objects)
+	require.Len(t, r.Schemas[2].Objects, 2)
+	require.Equal(t, []Object{e2, e3}, r.Schemas[2].Objects)
+
+	// Exclude all.
+	r, err = ExcludeRealm(NewRealm(s1, s2, s3), []string{"*.*"})
+	require.NoError(t, err)
+	require.Len(t, r.Schemas, 3)
+	require.Empty(t, r.Schemas[0].Objects)
+	require.Empty(t, r.Schemas[1].Objects)
+	require.Empty(t, r.Schemas[2].Objects)
+}
+
+func TestExcludeSchema_Enums(t *testing.T) {
+	e1, e2, e3 := &EnumType{T: "e1"}, &EnumType{T: "e2"}, &EnumType{T: "e3"}
+	r := NewRealm(New("s1").AddObjects(e1, e2, e3))
+
+	s, err := ExcludeSchema(r.Schemas[0], []string{"e1"})
+	require.NoError(t, err)
+	require.Len(t, s.Objects, 2)
+	require.Equal(t, []Object{e2, e3}, s.Objects)
+
+	s, err = ExcludeSchema(r.Schemas[0], []string{"e1", "e2"})
+	require.NoError(t, err)
+	require.Len(t, s.Objects, 1)
+	require.Equal(t, []Object{e3}, s.Objects)
+
+	// Wrong selector.
+	s, err = ExcludeSchema(r.Schemas[0], []string{"e*[type=view]"})
+	require.NoError(t, err)
+	require.Len(t, s.Objects, 1)
+
+	// Enum selector.
+	s, err = ExcludeSchema(r.Schemas[0], []string{"*[type=enum]"})
+	require.NoError(t, err)
+	require.Empty(t, s.Objects)
+}
+
 func TestExcludeRealm_Selector(t *testing.T) {
-	r := schema.NewRealm(
-		schema.New("s1").
+	r := NewRealm(
+		New("s1").
 			AddTables(
-				schema.NewTable("t1"),
+				NewTable("t1"),
 			).
 			AddViews(
-				schema.NewView("v1", "SELECT * FROM t1"),
+				NewView("v1", "SELECT * FROM t1"),
 			).
 			AddFuncs(
-				&schema.Func{Name: "f1"},
-				&schema.Func{Name: "f2"},
-				&schema.Func{Name: "f3"},
+				&Func{Name: "f1"},
+				&Func{Name: "f2"},
+				&Func{Name: "f3"},
 			).
 			AddProcs(
-				&schema.Proc{Name: "p1"},
-				&schema.Proc{Name: "p2"},
-				&schema.Proc{Name: "p3"},
+				&Proc{Name: "p1"},
+				&Proc{Name: "p2"},
+				&Proc{Name: "p3"},
 			),
 	)
 	r, err := ExcludeRealm(r, []string{"*.*[type=table]"})
@@ -135,9 +196,9 @@ func TestExcludeRealm_Selector(t *testing.T) {
 	require.Empty(t, r.Schemas[0].Procs)
 
 	r.Schemas[0].AddTables(
-		schema.NewTable("t1"),
-		schema.NewTable("t2"),
-		schema.NewTable("t3"),
+		NewTable("t1"),
+		NewTable("t2"),
+		NewTable("t3"),
 	)
 	r, err = ExcludeRealm(r, []string{"*.*[type=view]"})
 	require.NoError(t, err)
@@ -149,7 +210,7 @@ func TestExcludeRealm_Selector(t *testing.T) {
 	require.Len(t, r.Schemas[0].Tables, 1)
 
 	r.Schemas[0].AddViews(
-		schema.NewView("v1", "SELECT * FROM t1"),
+		NewView("v1", "SELECT * FROM t1"),
 	)
 	r, err = ExcludeRealm(r, []string{"*.*[type=view|table]"})
 	require.NoError(t, err)
@@ -158,10 +219,10 @@ func TestExcludeRealm_Selector(t *testing.T) {
 
 	r.Schemas[0].
 		AddTables(
-			schema.NewTable("t1").AddColumns(schema.NewColumn("c1")).AddIndexes(schema.NewIndex("i1")).AddChecks(schema.NewCheck().SetName("k1")),
+			NewTable("t1").AddColumns(NewColumn("c1")).AddIndexes(NewIndex("i1")).AddChecks(NewCheck().SetName("k1")),
 		).
 		AddViews(
-			schema.NewView("v1", "SELECT * FROM t1").AddColumns(schema.NewColumn("c1")),
+			NewView("v1", "SELECT * FROM t1").AddColumns(NewColumn("c1")),
 		)
 	r, err = ExcludeRealm(r, []string{"*.*[type=table].*1[type=column|check]"})
 	require.NoError(t, err)
@@ -177,9 +238,9 @@ func TestExcludeRealm_Selector(t *testing.T) {
 }
 
 func TestExcludeRealm_Checks(t *testing.T) {
-	r := schema.NewRealm(
-		schema.New("s1").AddTables(
-			schema.NewTable("t1").AddChecks(schema.NewCheck().SetName("c1")),
+	r := NewRealm(
+		New("s1").AddTables(
+			NewTable("t1").AddChecks(NewCheck().SetName("c1")),
 		),
 	)
 	r, err := ExcludeRealm(r, []string{"s1.t1.c1"})
@@ -188,40 +249,40 @@ func TestExcludeRealm_Checks(t *testing.T) {
 }
 
 func TestExcludeRealm_Columns(t *testing.T) {
-	r := schema.NewRealm(
-		schema.New("s1").AddTables(
-			func() *schema.Table {
-				t := schema.NewTable("t1").AddColumns(schema.NewColumn("c1"))
-				t.AddIndexes(schema.NewIndex("i1").AddColumns(t.Columns[0]))
+	r := NewRealm(
+		New("s1").AddTables(
+			func() *Table {
+				t := NewTable("t1").AddColumns(NewColumn("c1"))
+				t.AddIndexes(NewIndex("i1").AddColumns(t.Columns[0]))
 				return t
 			}(),
 		),
-		schema.New("s2").AddTables(
-			func() *schema.Table {
-				t := schema.NewTable("t1").AddColumns(schema.NewColumn("c1"))
-				t.AddIndexes(schema.NewIndex("i1").AddColumns(t.Columns[0]))
+		New("s2").AddTables(
+			func() *Table {
+				t := NewTable("t1").AddColumns(NewColumn("c1"))
+				t.AddIndexes(NewIndex("i1").AddColumns(t.Columns[0]))
 				return t
 			}(),
-			func() *schema.Table {
-				t := schema.NewTable("t2").AddColumns(schema.NewColumn("c1"), schema.NewColumn("c2"))
-				t.AddIndexes(schema.NewIndex("i1").AddColumns(t.Columns[0]), schema.NewIndex("i2").AddColumns(t.Columns[1]))
+			func() *Table {
+				t := NewTable("t2").AddColumns(NewColumn("c1"), NewColumn("c2"))
+				t.AddIndexes(NewIndex("i1").AddColumns(t.Columns[0]), NewIndex("i2").AddColumns(t.Columns[1]))
 				return t
 			}(),
 		),
-		schema.New("s3").AddTables(
-			func() *schema.Table {
-				t := schema.NewTable("t1").AddColumns(schema.NewColumn("c1"))
-				t.AddIndexes(schema.NewIndex("i1").AddColumns(t.Columns[0]))
+		New("s3").AddTables(
+			func() *Table {
+				t := NewTable("t1").AddColumns(NewColumn("c1"))
+				t.AddIndexes(NewIndex("i1").AddColumns(t.Columns[0]))
 				return t
 			}(),
-			func() *schema.Table {
-				t := schema.NewTable("t2").AddColumns(schema.NewColumn("c1"), schema.NewColumn("c2"))
-				t.AddIndexes(schema.NewIndex("i1").AddColumns(t.Columns[0]), schema.NewIndex("i2").AddColumns(t.Columns[1]))
+			func() *Table {
+				t := NewTable("t2").AddColumns(NewColumn("c1"), NewColumn("c2"))
+				t.AddIndexes(NewIndex("i1").AddColumns(t.Columns[0]), NewIndex("i2").AddColumns(t.Columns[1]))
 				return t
 			}(),
-			func() *schema.Table {
-				t := schema.NewTable("t3").AddColumns(schema.NewColumn("c1"), schema.NewColumn("c2"), schema.NewColumn("c3"))
-				t.AddIndexes(schema.NewIndex("i1").AddColumns(t.Columns[0]), schema.NewIndex("i2").AddColumns(t.Columns[1]))
+			func() *Table {
+				t := NewTable("t3").AddColumns(NewColumn("c1"), NewColumn("c2"), NewColumn("c3"))
+				t.AddIndexes(NewIndex("i1").AddColumns(t.Columns[0]), NewIndex("i2").AddColumns(t.Columns[1]))
 				return t
 			}(),
 		),
@@ -277,10 +338,10 @@ func TestExcludeRealm_Columns(t *testing.T) {
 }
 
 func TestExcludeSchema(t *testing.T) {
-	r := schema.NewRealm(
-		schema.New("s1").AddTables(
-			schema.NewTable("t1"),
-			schema.NewTable("t2"),
+	r := NewRealm(
+		New("s1").AddTables(
+			NewTable("t1"),
+			NewTable("t2"),
 		),
 	)
 	_, err := ExcludeSchema(r.Schemas[0], []string{"t2"})
diff --git a/sql/sqlcheck/incompatible/incompatible_test.go b/sql/sqlcheck/incompatible/incompatible_test.go
index c5d085a9..5f5168da 100644
--- a/sql/sqlcheck/incompatible/incompatible_test.go
+++ b/sql/sqlcheck/incompatible/incompatible_test.go
@@ -275,6 +275,6 @@ func (t testFile) StmtDecls() ([]*migrate.Stmt, error) {
 
 type testParser struct{ matchOn string }
 
-func (t testParser) CreateViewAfter(_ migrate.File, old, _ string, _ int) (bool, error) {
+func (t testParser) CreateViewAfter(_ []*migrate.Stmt, old, _ string, _ int) (bool, error) {
 	return t.matchOn == old, nil
 }