Repository: ariga/atlas
Pull request #752 (2022-04-29 13:27:49)

sql/postgres: ent based revisions for postgres


Patch:
diff --git a/README.md b/README.md
index 0abb2b8b..39cb5e64 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,6 @@
 
 # Atlas -  A Database Toolkit
+[![Discord](https://img.shields.io/discord/930720389120794674?label=discord&logo=discord&style=flat-square&logoColor=white)](https://discord.gg/zZ6sWVg6NT)
 
 <img width="50%" align="right" style="display: block; margin:40px auto;"
      src="https://atlasgo.io/uploads/images/gopher.png"/>
diff --git a/cmd/action/apply.go b/cmd/action/apply.go
deleted file mode 100644
index 3bcc5301..00000000
--- a/cmd/action/apply.go
+++ /dev/null
@@ -1,170 +0,0 @@
-// Copyright 2021-present The Atlas Authors. All rights reserved.
-// 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 action
-
-import (
-	"context"
-	"errors"
-	"io/ioutil"
-	"strings"
-
-	"ariga.io/atlas/sql/schema"
-	"ariga.io/atlas/sql/sqlclient"
-
-	"github.com/manifoldco/promptui"
-	"github.com/spf13/cobra"
-)
-
-var (
-	// ApplyFlags are the flags used in Apply command.
-	ApplyFlags struct {
-		URL         string
-		DevURL      string
-		File        string
-		Web         bool
-		Addr        string
-		DryRun      bool
-		Schema      []string
-		AutoApprove bool
-		Verbose     bool
-	}
-	// ApplyCmd represents the apply command.
-	ApplyCmd = &cobra.Command{
-		Use:   "apply",
-		Short: "Apply an atlas schema to a target database.",
-		// Use 80-columns as max width.
-		Long: `'atlas schema apply' plans and executes a database migration to bring a given
-database to the state described in the Atlas schema file. Before running the
-migration, Atlas will print the migration plan and prompt the user for approval.
-
-If run with the "--dry-run" flag, atlas will exit after printing out the planned
-migration.`,
-		Run: CmdApplyRun,
-		Example: `  atlas schema apply -u "mysql://user:pass@localhost/dbname" -f atlas.hcl
-  atlas schema apply -u "mysql://localhost" -f atlas.hcl --schema prod --schema staging
-  atlas schema apply -u "mysql://user:pass@localhost:3306/dbname" -f atlas.hcl --dry-run
-  atlas schema apply -u "mariadb://user:pass@localhost:3306/dbname" -f atlas.hcl
-  atlas schema apply --url "postgres://user:pass@host:port/dbname?sslmode=disable" -f atlas.hcl
-  atlas schema apply -u "sqlite://file:ex1.db?_fk=1" -f atlas.hcl`,
-	}
-)
-
-const (
-	answerApply = "Apply"
-	answerAbort = "Abort"
-)
-
-func init() {
-	schemaCmd.AddCommand(ApplyCmd)
-	ApplyCmd.Flags().SortFlags = false
-	ApplyCmd.Flags().StringVarP(&ApplyFlags.File, "file", "f", "", "[/path/to/file] file containing the HCL schema.")
-	ApplyCmd.Flags().StringVarP(&ApplyFlags.URL, "url", "u", "", "URL to the database using the format:\n[driver://username:password@address/dbname?param=value]")
-	ApplyCmd.Flags().StringSliceVarP(&ApplyFlags.Schema, "schema", "s", nil, "Set schema names.")
-	ApplyCmd.Flags().StringVarP(&ApplyFlags.DevURL, "dev-url", "", "", "URL for the dev database. Used to validate schemas and calculate diffs\nbefore running migration.")
-	ApplyCmd.Flags().BoolVarP(&ApplyFlags.DryRun, "dry-run", "", false, "Dry-run. Print SQL plan without prompting for execution.")
-	ApplyCmd.Flags().BoolVarP(&ApplyFlags.AutoApprove, "auto-approve", "", false, "Auto approve. Apply the schema changes without prompting for approval.")
-	ApplyCmd.Flags().BoolVarP(&ApplyFlags.Web, "web", "w", false, "Open in a local Atlas UI.")
-	ApplyCmd.Flags().StringVarP(&ApplyFlags.Addr, "addr", "", ":5800", "used with -w, local address to bind the server to.")
-	ApplyCmd.Flags().BoolVarP(&ApplyFlags.Verbose, migrateDiffFlagVerbose, "", false, "enable verbose logging")
-	cobra.CheckErr(ApplyCmd.MarkFlagRequired("url"))
-	cobra.CheckErr(ApplyCmd.MarkFlagRequired("file"))
-	dsn2url(ApplyCmd, &ApplyFlags.URL)
-}
-
-// CmdApplyRun is the command used when running CLI.
-func CmdApplyRun(cmd *cobra.Command, _ []string) {
-	if ApplyFlags.Web {
-		schemaCmd.PrintErrln("The Atlas UI is not available in this release.")
-		return
-	}
-	c, err := sqlclient.Open(cmd.Context(), ApplyFlags.URL)
-	cobra.CheckErr(err)
-	defer c.Close()
-	applyRun(cmd.Context(), c, ApplyFlags.File, ApplyFlags.DryRun, ApplyFlags.AutoApprove)
-}
-
-func applyRun(ctx context.Context, client *sqlclient.Client, file string, dryRun, autoApprove bool) {
-	schemas := ApplyFlags.Schema
-	if client.URL.Schema != "" {
-		schemas = append(schemas, client.URL.Schema)
-	}
-	realm, err := client.InspectRealm(ctx, &schema.InspectRealmOption{
-		Schemas: schemas,
-	})
-	cobra.CheckErr(err)
-	f, err := ioutil.ReadFile(file)
-	cobra.CheckErr(err)
-	desired := &schema.Realm{}
-	cobra.CheckErr(client.Eval(f, desired, nil))
-	if len(schemas) > 0 {
-		// Validate all schemas in file were selected by user.
-		sm := make(map[string]bool, len(schemas))
-		for _, s := range schemas {
-			sm[s] = true
-		}
-		for _, s := range desired.Schemas {
-			if !sm[s.Name] {
-				schemaCmd.Printf("schema %q from file %q was not selected %q, all schemas defined in file must be selected\n", s.Name, file, schemas)
-				return
-			}
-		}
-	}
-	if _, ok := client.Driver.(schema.Normalizer); ok && ApplyFlags.DevURL != "" {
-		dev, err := sqlclient.Open(ctx, ApplyFlags.DevURL)
-		cobra.CheckErr(err)
-		defer dev.Close()
-		desired, err = dev.Driver.(schema.Normalizer).NormalizeRealm(ctx, desired)
-		cobra.CheckErr(err)
-	}
-	changes, err := client.RealmDiff(realm, desired)
-	cobra.CheckErr(err)
-	if len(changes) == 0 {
-		schemaCmd.Println("Schema is synced, no changes to be made")
-		return
-	}
-	p, err := client.PlanChanges(ctx, "plan", changes)
-	cobra.CheckErr(err)
-	schemaCmd.Println("-- Planned Changes:")
-	for _, c := range p.Changes {
-		if c.Comment != "" {
-			schemaCmd.Println("--", strings.ToUpper(c.Comment[:1])+c.Comment[1:])
-		}
-		schemaCmd.Println(c.Cmd)
-	}
-	if dryRun {
-		return
-	}
-	if autoApprove || promptUser() {
-		cobra.CheckErr(client.ApplyChanges(ctx, changes))
-	}
-}
-
-func promptUser() bool {
-	prompt := promptui.Select{
-		Label: "Are you sure?",
-		Items: []string{answerApply, answerAbort},
-	}
-	_, result, err := prompt.Run()
-	cobra.CheckErr(err)
-	return result == answerApply
-}
-
-func dsn2url(cmd *cobra.Command, p *string) {
-	cmd.Flags().StringVarP(p, "dsn", "d", "", "")
-	cobra.CheckErr(cmd.Flags().MarkHidden("dsn"))
-	cmd.PreRunE = func(cmd *cobra.Command, args []string) error {
-		dsnF, urlF := cmd.Flag("dsn"), cmd.Flag("url")
-		switch {
-		case !dsnF.Changed && !urlF.Changed:
-			return errors.New(`required flag "url" was not set`)
-		case dsnF.Changed && urlF.Changed:
-			return errors.New(`both flags "url" and "dsn" were set`)
-		case dsnF.Changed && !urlF.Changed:
-			urlF.Changed = true
-			urlF.Value = dsnF.Value
-		}
-		return nil
-	}
-}
diff --git a/cmd/action/env.go b/cmd/action/env.go
deleted file mode 100644
index 040d41bb..00000000
--- a/cmd/action/env.go
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright 2021-present The Atlas Authors. All rights reserved.
-// 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 action
-
-import (
-	"fmt"
-	"os"
-
-	"ariga.io/atlas/cmd/internal/update"
-
-	"github.com/spf13/cobra"
-)
-
-// EnvCmd is the "atlas env" command.
-var EnvCmd = &cobra.Command{
-	Use:   "env",
-	Short: "Print atlas environment variables.",
-	Long: `'atlas env' prints atlas environment information.
-
-Every set environment param will be printed in the form of NAME=VALUE.
-
-List of supported environment parameters:
-* ATLAS_NO_UPDATE_NOTIFIER: On any command, the CLI will check for new releases using the GitHub API.
-  This check will happen at most once every 24 hours. To cancel this behavior, set the environment 
-  variable "ATLAS_NO_UPDATE_NOTIFIER".`,
-	Run: func(cmd *cobra.Command, args []string) {
-		keys := []string{update.AtlasNoUpdateNotifier}
-		for _, k := range keys {
-			if v, ok := os.LookupEnv(k); ok {
-				cmd.Println(fmt.Sprintf("%s=%s", k, v))
-			}
-		}
-	},
-}
-
-func init() {
-	RootCmd.AddCommand(EnvCmd)
-}
diff --git a/cmd/action/fmt.go b/cmd/action/fmt.go
deleted file mode 100644
index c112fa27..00000000
--- a/cmd/action/fmt.go
+++ /dev/null
@@ -1,111 +0,0 @@
-// Copyright 2021-present The Atlas Authors. All rights reserved.
-// 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 action
-
-import (
-	"bytes"
-	"io/fs"
-	"io/ioutil"
-	"os"
-	"path/filepath"
-	"strings"
-
-	"github.com/hashicorp/hcl/v2/hclwrite"
-	"github.com/spf13/cobra"
-)
-
-var (
-	// FmtCmd represents the fmt command.
-	FmtCmd = &cobra.Command{
-		Use:   "fmt [path ...]",
-		Short: "Formats Atlas HCL files",
-		Long: "`atlas schema fmt`" + ` formats all ".hcl" files under the given path using
-canonical HCL layout style as defined by the github.com/hashicorp/hcl/v2/hclwrite package.
-Unless stated otherwise, the fmt command will use the current directory.
-
-After running, the command will print the names of the files it has formatted. If all
-files in the directory are formatted, no input will be printed out.
-`,
-		Run: CmdFmtRun,
-	}
-)
-
-func init() {
-	schemaCmd.AddCommand(FmtCmd)
-}
-
-// CmdFmtRun formats all HCL files in a given directory using canonical HCL formatting
-// rules.
-func CmdFmtRun(cmd *cobra.Command, args []string) {
-	if len(args) == 0 {
-		args = append(args, "./")
-	}
-	for _, path := range args {
-		handlePath(cmd, path)
-	}
-}
-
-func handlePath(cmd *cobra.Command, path string) {
-	tasks, err := tasks(path)
-	cobra.CheckErr(err)
-	for _, task := range tasks {
-		changed, err := fmtFile(task)
-		cobra.CheckErr(err)
-		if changed {
-			cmd.Println(task.path)
-		}
-	}
-}
-
-func tasks(path string) ([]fmttask, error) {
-	var tasks []fmttask
-	stat, err := os.Stat(path)
-	if err != nil {
-		return nil, err
-	}
-	if !stat.IsDir() {
-		if strings.HasSuffix(path, ".hcl") {
-			tasks = append(tasks, fmttask{
-				path: path,
-				info: stat,
-			})
-		}
-		return tasks, nil
-	}
-	all, err := ioutil.ReadDir(path)
-	if err != nil {
-		return nil, err
-	}
-	for _, f := range all {
-		if f.IsDir() {
-			continue
-		}
-		if strings.HasSuffix(f.Name(), ".hcl") {
-			tasks = append(tasks, fmttask{
-				path: filepath.Join(path, f.Name()),
-				info: f,
-			})
-		}
-	}
-	return tasks, nil
-}
-
-type fmttask struct {
-	path string
-	info fs.FileInfo
-}
-
-// fmtFile tries to format a file and reports if formatting occurred.
-func fmtFile(task fmttask) (bool, error) {
-	orig, err := os.ReadFile(task.path)
-	if err != nil {
-		return false, err
-	}
-	formatted := hclwrite.Format(orig)
-	if !bytes.Equal(formatted, orig) {
-		return true, os.WriteFile(task.path, formatted, task.info.Mode())
-	}
-	return false, nil
-}
diff --git a/cmd/action/inspect.go b/cmd/action/inspect.go
deleted file mode 100644
index e0873bd0..00000000
--- a/cmd/action/inspect.go
+++ /dev/null
@@ -1,77 +0,0 @@
-// Copyright 2021-present The Atlas Authors. All rights reserved.
-// 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 action
-
-import (
-	"ariga.io/atlas/sql/schema"
-	"ariga.io/atlas/sql/sqlclient"
-
-	"github.com/spf13/cobra"
-)
-
-var (
-	// InspectFlags are the flags used in Inspect command.
-	InspectFlags struct {
-		URL    string
-		Web    bool
-		Addr   string
-		Schema []string
-	}
-	// InspectCmd represents the inspect command.
-	InspectCmd = &cobra.Command{
-		Use:   "inspect",
-		Short: "Inspect an a database's and print its schema in Atlas DDL syntax.",
-		Long: "`atlas schema inspect`" + ` connects to the given database and inspects its schema.
-It then prints to the screen the schema of that database in Atlas DDL syntax. This output can be 
-saved to a file, commonly by redirecting the output to a file named with a ".hcl" suffix:
-
-  atlas schema inspect -u "mysql://user:pass@localhost:3306/dbname" > atlas.hcl
-
-This file can then be edited and used with the` + " `atlas schema apply` " + `command to plan
-and execute schema migrations against the given database. In cases where users wish to inspect
-all multiple schemas in a given database (for instance a MySQL server may contain multiple named
-databases), omit the relevant part from the url, e.g. "mysql://user:pass@localhost:3306/".
-To select specific schemas from the databases, users may use the "--schema" (or "-s" shorthand)
-flag.
-	`,
-		Run: CmdInspectRun,
-		Example: `  atlas schema inspect -u "mysql://user:pass@localhost:3306/dbname"
-  atlas schema inspect -u "mariadb://user:pass@localhost:3306/" --schema=schemaA,schemaB -s schemaC
-  atlas schema inspect --url "postgres://user:pass@host:port/dbname?sslmode=disable"
-  atlas schema inspect -u "sqlite://file:ex1.db?_fk=1"`,
-	}
-)
-
-func init() {
-	schemaCmd.AddCommand(InspectCmd)
-	InspectCmd.Flags().StringVarP(&InspectFlags.URL, "url", "u", "", "[driver://username:password@protocol(address)/dbname?param=value] Select data source using the url format")
-	InspectCmd.Flags().BoolVarP(&InspectFlags.Web, "web", "w", false, "Open in a local Atlas UI")
-	InspectCmd.Flags().StringVarP(&InspectFlags.Addr, "addr", "", ":5800", "Used with -w, local address to bind the server to")
-	InspectCmd.Flags().StringSliceVarP(&InspectFlags.Schema, "schema", "s", nil, "Set schema name")
-	cobra.CheckErr(InspectCmd.MarkFlagRequired("url"))
-	dsn2url(InspectCmd, &InspectFlags.URL)
-}
-
-// CmdInspectRun is the command used when running CLI.
-func CmdInspectRun(cmd *cobra.Command, _ []string) {
-	if InspectFlags.Web {
-		schemaCmd.PrintErrln("The Atlas UI is not available in this release.")
-		return
-	}
-	client, err := sqlclient.Open(cmd.Context(), InspectFlags.URL)
-	cobra.CheckErr(err)
-	defer client.Close()
-	schemas := InspectFlags.Schema
-	if client.URL.Schema != "" {
-		schemas = append(schemas, client.URL.Schema)
-	}
-	s, err := client.InspectRealm(cmd.Context(), &schema.InspectRealmOption{
-		Schemas: schemas,
-	})
-	cobra.CheckErr(err)
-	ddl, err := client.MarshalSpec(s)
-	cobra.CheckErr(err)
-	schemaCmd.Print(string(ddl))
-}
diff --git a/cmd/action/root.go b/cmd/action/root.go
deleted file mode 100644
index eb20dcfb..00000000
--- a/cmd/action/root.go
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright 2021-present The Atlas Authors. All rights reserved.
-// 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 action
-
-import (
-	"ariga.io/atlas/cmd/internal/update"
-
-	"github.com/spf13/cobra"
-)
-
-// RootCmd represents the base command when called without any subcommands
-var RootCmd = &cobra.Command{
-	Use:   "atlas",
-	Short: "A database toolkit.",
-}
-
-// CheckForUpdate exposes internal update logic to CLI
-func CheckForUpdate() {
-	update.CheckForUpdate(version, RootCmd.PrintErrln)
-}
diff --git a/cmd/action/schema.go b/cmd/action/schema.go
deleted file mode 100644
index a1cef211..00000000
--- a/cmd/action/schema.go
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright 2021-present The Atlas Authors. All rights reserved.
-// 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 action
-
-import (
-	"github.com/spf13/cobra"
-)
-
-// schemaCmd represents the schema command
-var schemaCmd = &cobra.Command{
-	Use:   "schema",
-	Short: "Work with atlas schemas.",
-	Long:  "The `atlas schema` command groups subcommands for working with Atlas schemas.",
-}
-
-func init() {
-	RootCmd.AddCommand(schemaCmd)
-}
diff --git a/cmd/action/version.go b/cmd/action/version.go
deleted file mode 100644
index ab44cbed..00000000
--- a/cmd/action/version.go
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright 2021-present The Atlas Authors. All rights reserved.
-// 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 action
-
-import (
-	"fmt"
-	"strings"
-
-	"github.com/spf13/cobra"
-	"golang.org/x/mod/semver"
-)
-
-// version is the atlas CLI build version
-// Should be set by build script "-X 'ariga.io/atlas/cmd/action.version=${version}'"
-var version string
-
-var versionCmd = &cobra.Command{
-	Use:   "version",
-	Short: "Prints this Atlas CLI version information.",
-	Run: func(cmd *cobra.Command, args []string) {
-		v, u := parse(version)
-		RootCmd.Printf("atlas CLI version %s\n%s\n", v, u)
-	},
-}
-
-func init() {
-	RootCmd.AddCommand(versionCmd)
-}
-
-// parse returns a user facing version and release notes url
-func parse(version string) (string, string) {
-	u := "https://github.com/ariga/atlas/releases/latest"
-	if ok := semver.IsValid(version); !ok {
-		return "- development", u
-	}
-	s := strings.Split(version, "-")
-	if len(s) != 0 && s[len(s)-1] != "canary" {
-		u = fmt.Sprintf("https://github.com/ariga/atlas/releases/tag/%s", version)
-	}
-	return version, u
-}
diff --git a/cmd/atlas/doc.go b/cmd/atlas/doc.go
index 7794ec6b..bc422dc8 100644
--- a/cmd/atlas/doc.go
+++ b/cmd/atlas/doc.go
@@ -10,7 +10,7 @@ import (
 	"strings"
 	"text/template"
 
-	"ariga.io/atlas/cmd/action"
+	"ariga.io/atlas/cmd/atlascmd"
 
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra/doc"
@@ -37,7 +37,7 @@ func main() {
 	if err := t.ExecuteTemplate(f, "header", nil); err != nil {
 		log.Fatal(err)
 	}
-	blocks := prepare(action.RootCmd, make([]*block, 0), 0)
+	blocks := prepare(atlascmd.Root, make([]*block, 0), 0)
 	if err := t.ExecuteTemplate(f, "body", struct {
 		Blocks []*block
 	}{Blocks: blocks}); err != nil {
diff --git a/cmd/atlas/main.go b/cmd/atlas/main.go
index 187ebf2e..24643320 100644
--- a/cmd/atlas/main.go
+++ b/cmd/atlas/main.go
@@ -5,8 +5,8 @@ import (
 	"os"
 	"os/signal"
 
-	"ariga.io/atlas/cmd/action"
-	_ "ariga.io/atlas/cmd/internal/docker"
+	"ariga.io/atlas/cmd/atlascmd"
+	_ "ariga.io/atlas/cmd/atlascmd/docker"
 	_ "ariga.io/atlas/sql/mysql"
 	_ "ariga.io/atlas/sql/postgres"
 	_ "ariga.io/atlas/sql/sqlite"
@@ -18,15 +18,15 @@ import (
 
 func main() {
 	ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
-	action.RootCmd.SetOut(os.Stdout)
-	err := action.RootCmd.ExecuteContext(ctx)
+	atlascmd.Root.SetOut(os.Stdout)
+	err := atlascmd.Root.ExecuteContext(ctx)
 	// Print error from command
 	if err != nil {
-		action.RootCmd.PrintErrln("Error:", err)
+		atlascmd.Root.PrintErrln("Error:", err)
 	}
-	// Check for update
-	action.CheckForUpdate()
-	// Exit code according to command success
+	// Check for update.
+	atlascmd.CheckForUpdate()
+	// Exit code according to command success.
 	if err != nil {
 		os.Exit(1)
 	}
diff --git a/cmd/atlascmd/atlascmd.go b/cmd/atlascmd/atlascmd.go
new file mode 100644
index 00000000..6be07309
--- /dev/null
+++ b/cmd/atlascmd/atlascmd.go
@@ -0,0 +1,86 @@
+// Copyright 2021-present The Atlas Authors. All rights reserved.
+// 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 atlascmd holds the atlas commands used to build
+// an atlas distribution.
+package atlascmd
+
+import (
+	"fmt"
+	"os"
+	"strings"
+
+	"ariga.io/atlas/cmd/atlascmd/update"
+
+	"github.com/spf13/cobra"
+	"golang.org/x/mod/semver"
+)
+
+var (
+	// Root represents the root command when called without any subcommands.
+	Root = &cobra.Command{
+		Use:   "atlas",
+		Short: "A database toolkit.",
+	}
+
+	// version is the atlas CLI build version
+	// Should be set by build script "-X 'ariga.io/atlas/cmd/action.version=${version}'"
+	version string
+
+	// schemaCmd represents the subcommand 'atlas version'.
+	versionCmd = &cobra.Command{
+		Use:   "version",
+		Short: "Prints this Atlas CLI version information.",
+		Run: func(cmd *cobra.Command, args []string) {
+			v, u := parse(version)
+			Root.Printf("atlas version %s\n%s\n", v, u)
+		},
+	}
+
+	// envCmd represents the subcommand 'atlas env'.
+	envCmd = &cobra.Command{
+		Use:   "env",
+		Short: "Print atlas environment variables.",
+		Long: `'atlas env' prints atlas environment information.
+
+Every set environment param will be printed in the form of NAME=VALUE.
+
+List of supported environment parameters:
+* ATLAS_NO_UPDATE_NOTIFIER: On any command, the CLI will check for new releases using the GitHub API.
+  This check will happen at most once every 24 hours. To cancel this behavior, set the environment 
+  variable "ATLAS_NO_UPDATE_NOTIFIER".`,
+		Run: func(cmd *cobra.Command, args []string) {
+			keys := []string{update.AtlasNoUpdateNotifier}
+			for _, k := range keys {
+				if v, ok := os.LookupEnv(k); ok {
+					cmd.Println(fmt.Sprintf("%s=%s", k, v))
+				}
+			}
+		},
+	}
+)
+
+// CheckForUpdate exposes internal update logic to CLI
+func CheckForUpdate() {
+	update.CheckForUpdate(version, Root.PrintErrln)
+}
+
+func init() {
+	Root.AddCommand(envCmd)
+	Root.AddCommand(schemaCmd)
+	Root.AddCommand(versionCmd)
+}
+
+// parse returns a user facing version and release notes url
+func parse(version string) (string, string) {
+	u := "https://github.com/ariga/atlas/releases/latest"
+	if ok := semver.IsValid(version); !ok {
+		return "- development", u
+	}
+	s := strings.Split(version, "-")
+	if len(s) != 0 && s[len(s)-1] != "canary" {
+		u = fmt.Sprintf("https://github.com/ariga/atlas/releases/tag/%s", version)
+	}
+	return version, u
+}
diff --git a/cmd/action/diff.go b/cmd/atlascmd/diff.go
similarity index 96%
rename from cmd/action/diff.go
rename to cmd/atlascmd/diff.go
index bb73d12f..3a8b1a18 100644
--- a/cmd/action/diff.go
+++ b/cmd/atlascmd/diff.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 action
+package atlascmd
 
 import (
 	"strings"
@@ -23,7 +23,7 @@ func newDiffCmd() *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "diff",
 		Short: "Calculate and print the diff between two schemas.",
-		Long: "`atlas schema diff`" + ` connects to two given databases, inspects
+		Long: `'atlas schema diff' connects to two given databases, inspects
 them, calculates the difference in their schemas, and prints a plan of
 SQL statements to migrate the "from" database to the schema of the "to" database.`,
 		Run: func(cmd *cobra.Command, args []string) {
diff --git a/cmd/internal/docker/docker.go b/cmd/atlascmd/docker/docker.go
similarity index 97%
rename from cmd/internal/docker/docker.go
rename to cmd/atlascmd/docker/docker.go
index e396eb13..80feffb6 100644
--- a/cmd/internal/docker/docker.go
+++ b/cmd/atlascmd/docker/docker.go
@@ -22,7 +22,7 @@ import (
 
 	"ariga.io/atlas/sql/sqlclient"
 
-	mysqld "github.com/go-sql-driver/mysql"
+	"github.com/go-sql-driver/mysql"
 )
 
 const pass = "pass"
@@ -205,8 +205,8 @@ func (c *Container) Close() error {
 // Wait waits for this container to be ready.
 func (c *Container) Wait(ctx context.Context, timeout time.Duration) error {
 	fmt.Fprintln(c.out, "Waiting for service to be ready ... ")
-	mysqld.SetLogger(log.New(ioutil.Discard, "", 1))
-	defer mysqld.SetLogger(log.New(os.Stderr, "[mysql] ", log.Ldate|log.Ltime|log.Lshortfile))
+	mysql.SetLogger(log.New(ioutil.Discard, "", 1))
+	defer mysql.SetLogger(log.New(os.Stderr, "[mysql] ", log.Ldate|log.Ltime|log.Lshortfile))
 	if timeout > time.Minute {
 		timeout = time.Minute
 	}
diff --git a/cmd/internal/docker/docker_test.go b/cmd/atlascmd/docker/docker_test.go
similarity index 100%
rename from cmd/internal/docker/docker_test.go
rename to cmd/atlascmd/docker/docker_test.go
diff --git a/cmd/action/migrate.go b/cmd/atlascmd/migrate.go
similarity index 98%
rename from cmd/action/migrate.go
rename to cmd/atlascmd/migrate.go
index a52f40a5..dae4e6f5 100644
--- a/cmd/action/migrate.go
+++ b/cmd/atlascmd/migrate.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 action
+package atlascmd
 
 import (
 	"context"
@@ -67,7 +67,7 @@ var (
 			return nil
 		},
 	}
-	// MigrateDiffCmd represents the migrate diff command.
+	// MigrateDiffCmd represents the 'atlas migrate diff' subcommand.
 	MigrateDiffCmd = &cobra.Command{
 		Use:   "diff",
 		Short: "Compute the diff between the migration directory and a connected database and create a new migration file.",
@@ -102,7 +102,7 @@ the atlas.sum file. If there is a mismatch it will be reported.`,
 
 func init() {
 	// Add sub-commands.
-	RootCmd.AddCommand(MigrateCmd)
+	Root.AddCommand(MigrateCmd)
 	MigrateCmd.AddCommand(MigrateDiffCmd)
 	MigrateCmd.AddCommand(MigrateValidateCmd)
 	MigrateCmd.AddCommand(MigrateHashCmd)
diff --git a/cmd/action/migrations/20220318104614_initial.sql b/cmd/atlascmd/migrations/20220318104614_initial.sql
similarity index 100%
rename from cmd/action/migrations/20220318104614_initial.sql
rename to cmd/atlascmd/migrations/20220318104614_initial.sql
diff --git a/cmd/action/migrations/atlas.sum b/cmd/atlascmd/migrations/atlas.sum
similarity index 100%
rename from cmd/action/migrations/atlas.sum
rename to cmd/atlascmd/migrations/atlas.sum
diff --git a/cmd/atlascmd/schema.go b/cmd/atlascmd/schema.go
new file mode 100644
index 00000000..8e8a4799
--- /dev/null
+++ b/cmd/atlascmd/schema.go
@@ -0,0 +1,336 @@
+// Copyright 2021-present The Atlas Authors. All rights reserved.
+// 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 atlascmd
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"io/fs"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"ariga.io/atlas/sql/schema"
+	"ariga.io/atlas/sql/sqlclient"
+
+	"github.com/hashicorp/hcl/v2/hclwrite"
+	"github.com/manifoldco/promptui"
+	"github.com/spf13/cobra"
+)
+
+var (
+	// schemaCmd represents the subcommand 'atlas schema'.
+	schemaCmd = &cobra.Command{
+		Use:   "schema",
+		Short: "Work with atlas schemas.",
+		Long:  "The `atlas schema` command groups subcommands for working with Atlas schemas.",
+	}
+
+	// ApplyFlags are the flags used in SchemaApply command.
+	ApplyFlags struct {
+		URL         string
+		DevURL      string
+		File        string
+		Web         bool
+		Addr        string
+		DryRun      bool
+		Schema      []string
+		AutoApprove bool
+		Verbose     bool
+	}
+	// SchemaApply represents the 'atlas schema apply' subcommand command.
+	SchemaApply = &cobra.Command{
+		Use:   "apply",
+		Short: "Apply an atlas schema to a target database.",
+		// Use 80-columns as max width.
+		Long: `'atlas schema apply' plans and executes a database migration to bring a given
+database to the state described in the Atlas schema file. Before running the
+migration, Atlas will print the migration plan and prompt the user for approval.
+
+If run with the "--dry-run" flag, atlas will exit after printing out the planned
+migration.`,
+		Run: CmdApplyRun,
+		Example: `  atlas schema apply -u "mysql://user:pass@localhost/dbname" -f atlas.hcl
+  atlas schema apply -u "mysql://localhost" -f atlas.hcl --schema prod --schema staging
+  atlas schema apply -u "mysql://user:pass@localhost:3306/dbname" -f atlas.hcl --dry-run
+  atlas schema apply -u "mariadb://user:pass@localhost:3306/dbname" -f atlas.hcl
+  atlas schema apply --url "postgres://user:pass@host:port/dbname?sslmode=disable" -f atlas.hcl
+  atlas schema apply -u "sqlite://file:ex1.db?_fk=1" -f atlas.hcl`,
+	}
+
+	// InspectFlags are the flags used in SchemaInspect command.
+	InspectFlags struct {
+		URL    string
+		Web    bool
+		Addr   string
+		Schema []string
+	}
+	// SchemaInspect represents the 'atlas schema inspect' subcommand.
+	SchemaInspect = &cobra.Command{
+		Use:   "inspect",
+		Short: "Inspect a database's and print its schema in Atlas DDL syntax.",
+		Long: `'atlas schema inspect' connects to the given database and inspects its schema.
+It then prints to the screen the schema of that database in Atlas DDL syntax. This output can be
+saved to a file, commonly by redirecting the output to a file named with a ".hcl" suffix:
+
+  atlas schema inspect -u "mysql://user:pass@localhost:3306/dbname" > atlas.hcl
+
+This file can then be edited and used with the` + " `atlas schema apply` " + `command to plan
+and execute schema migrations against the given database. In cases where users wish to inspect
+all multiple schemas in a given database (for instance a MySQL server may contain multiple named
+databases), omit the relevant part from the url, e.g. "mysql://user:pass@localhost:3306/".
+To select specific schemas from the databases, users may use the "--schema" (or "-s" shorthand)
+flag.
+	`,
+		Run: CmdInspectRun,
+		Example: `  atlas schema inspect -u "mysql://user:pass@localhost:3306/dbname"
+  atlas schema inspect -u "mariadb://user:pass@localhost:3306/" --schema=schemaA,schemaB -s schemaC
+  atlas schema inspect --url "postgres://user:pass@host:port/dbname?sslmode=disable"
+  atlas schema inspect -u "sqlite://file:ex1.db?_fk=1"`,
+	}
+
+	// SchemaFmt represents the 'atlas schema fmt' subcommand.
+	SchemaFmt = &cobra.Command{
+		Use:   "fmt [path ...]",
+		Short: "Formats Atlas HCL files",
+		Long: `'atlas schema fmt' formats all ".hcl" files under the given path using
+canonical HCL layout style as defined by the github.com/hashicorp/hcl/v2/hclwrite package.
+Unless stated otherwise, the fmt command will use the current directory.
+
+After running, the command will print the names of the files it has formatted. If all
+files in the directory are formatted, no input will be printed out.
+`,
+		Run: CmdFmtRun,
+	}
+)
+
+const (
+	answerApply = "SchemaApply"
+	answerAbort = "Abort"
+)
+
+func init() {
+	// Schema apply flags.
+	schemaCmd.AddCommand(SchemaApply)
+	SchemaApply.Flags().SortFlags = false
+	SchemaApply.Flags().StringVarP(&ApplyFlags.File, "file", "f", "", "[/path/to/file] file containing the HCL schema.")
+	SchemaApply.Flags().StringVarP(&ApplyFlags.URL, "url", "u", "", "URL to the database using the format:\n[driver://username:password@address/dbname?param=value]")
+	SchemaApply.Flags().StringSliceVarP(&ApplyFlags.Schema, "schema", "s", nil, "Set schema names.")
+	SchemaApply.Flags().StringVarP(&ApplyFlags.DevURL, "dev-url", "", "", "URL for the dev database. Used to validate schemas and calculate diffs\nbefore running migration.")
+	SchemaApply.Flags().BoolVarP(&ApplyFlags.DryRun, "dry-run", "", false, "Dry-run. Print SQL plan without prompting for execution.")
+	SchemaApply.Flags().BoolVarP(&ApplyFlags.AutoApprove, "auto-approve", "", false, "Auto approve. Apply the schema changes without prompting for approval.")
+	SchemaApply.Flags().BoolVarP(&ApplyFlags.Web, "web", "w", false, "Open in a local Atlas UI.")
+	SchemaApply.Flags().StringVarP(&ApplyFlags.Addr, "addr", "", ":5800", "used with -w, local address to bind the server to.")
+	SchemaApply.Flags().BoolVarP(&ApplyFlags.Verbose, migrateDiffFlagVerbose, "", false, "enable verbose logging")
+	cobra.CheckErr(SchemaApply.MarkFlagRequired("url"))
+	cobra.CheckErr(SchemaApply.MarkFlagRequired("file"))
+	dsn2url(SchemaApply, &ApplyFlags.URL)
+
+	// Schema inspect flags.
+	schemaCmd.AddCommand(SchemaInspect)
+	SchemaInspect.Flags().StringVarP(&InspectFlags.URL, "url", "u", "", "[driver://username:password@protocol(address)/dbname?param=value] Select data source using the url format")
+	SchemaInspect.Flags().BoolVarP(&InspectFlags.Web, "web", "w", false, "Open in a local Atlas UI")
+	SchemaInspect.Flags().StringVarP(&InspectFlags.Addr, "addr", "", ":5800", "Used with -w, local address to bind the server to")
+	SchemaInspect.Flags().StringSliceVarP(&InspectFlags.Schema, "schema", "s", nil, "Set schema name")
+	cobra.CheckErr(SchemaInspect.MarkFlagRequired("url"))
+	dsn2url(SchemaInspect, &InspectFlags.URL)
+
+	// Schema fmt.
+	schemaCmd.AddCommand(SchemaFmt)
+}
+
+// CmdInspectRun is the command used when running CLI.
+func CmdInspectRun(cmd *cobra.Command, _ []string) {
+	if InspectFlags.Web {
+		schemaCmd.PrintErrln("The Alas UI is not available in this release.")
+		return
+	}
+	client, err := sqlclient.Open(cmd.Context(), InspectFlags.URL)
+	cobra.CheckErr(err)
+	defer client.Close()
+	schemas := InspectFlags.Schema
+	if client.URL.Schema != "" {
+		schemas = append(schemas, client.URL.Schema)
+	}
+	s, err := client.InspectRealm(cmd.Context(), &schema.InspectRealmOption{
+		Schemas: schemas,
+	})
+	cobra.CheckErr(err)
+	ddl, err := client.MarshalSpec(s)
+	cobra.CheckErr(err)
+	schemaCmd.Print(string(ddl))
+}
+
+// CmdApplyRun is the command used when running CLI.
+func CmdApplyRun(cmd *cobra.Command, _ []string) {
+	if ApplyFlags.Web {
+		schemaCmd.PrintErrln("The Atlas UI is not available in this release.")
+		return
+	}
+	c, err := sqlclient.Open(cmd.Context(), ApplyFlags.URL)
+	cobra.CheckErr(err)
+	defer c.Close()
+	applyRun(cmd.Context(), c, ApplyFlags.File, ApplyFlags.DryRun, ApplyFlags.AutoApprove)
+}
+
+// CmdFmtRun formats all HCL files in a given directory using canonical HCL formatting
+// rules.
+func CmdFmtRun(cmd *cobra.Command, args []string) {
+	if len(args) == 0 {
+		args = append(args, "./")
+	}
+	for _, path := range args {
+		handlePath(cmd, path)
+	}
+}
+
+func applyRun(ctx context.Context, client *sqlclient.Client, file string, dryRun, autoApprove bool) {
+	schemas := ApplyFlags.Schema
+	if client.URL.Schema != "" {
+		schemas = append(schemas, client.URL.Schema)
+	}
+	realm, err := client.InspectRealm(ctx, &schema.InspectRealmOption{
+		Schemas: schemas,
+	})
+	cobra.CheckErr(err)
+	f, err := ioutil.ReadFile(file)
+	cobra.CheckErr(err)
+	desired := &schema.Realm{}
+	cobra.CheckErr(client.Eval(f, desired, nil))
+	if len(schemas) > 0 {
+		// Validate all schemas in file were selected by user.
+		sm := make(map[string]bool, len(schemas))
+		for _, s := range schemas {
+			sm[s] = true
+		}
+		for _, s := range desired.Schemas {
+			if !sm[s.Name] {
+				schemaCmd.Printf("schema %q from file %q was not selected %q, all schemas defined in file must be selected\n", s.Name, file, schemas)
+				return
+			}
+		}
+	}
+	if _, ok := client.Driver.(schema.Normalizer); ok && ApplyFlags.DevURL != "" {
+		dev, err := sqlclient.Open(ctx, ApplyFlags.DevURL)
+		cobra.CheckErr(err)
+		defer dev.Close()
+		desired, err = dev.Driver.(schema.Normalizer).NormalizeRealm(ctx, desired)
+		cobra.CheckErr(err)
+	}
+	changes, err := client.RealmDiff(realm, desired)
+	cobra.CheckErr(err)
+	if len(changes) == 0 {
+		schemaCmd.Println("Schema is synced, no changes to be made")
+		return
+	}
+	p, err := client.PlanChanges(ctx, "plan", changes)
+	cobra.CheckErr(err)
+	schemaCmd.Println("-- Planned Changes:")
+	for _, c := range p.Changes {
+		if c.Comment != "" {
+			schemaCmd.Println("--", strings.ToUpper(c.Comment[:1])+c.Comment[1:])
+		}
+		schemaCmd.Println(c.Cmd)
+	}
+	if dryRun {
+		return
+	}
+	if autoApprove || promptUser() {
+		cobra.CheckErr(client.ApplyChanges(ctx, changes))
+	}
+}
+
+func promptUser() bool {
+	prompt := promptui.Select{
+		Label: "Are you sure?",
+		Items: []string{answerApply, answerAbort},
+	}
+	_, result, err := prompt.Run()
+	cobra.CheckErr(err)
+	return result == answerApply
+}
+
+func dsn2url(cmd *cobra.Command, p *string) {
+	cmd.Flags().StringVarP(p, "dsn", "d", "", "")
+	cobra.CheckErr(cmd.Flags().MarkHidden("dsn"))
+	cmd.PreRunE = func(cmd *cobra.Command, args []string) error {
+		dsnF, urlF := cmd.Flag("dsn"), cmd.Flag("url")
+		switch {
+		case !dsnF.Changed && !urlF.Changed:
+			return errors.New(`required flag "url" was not set`)
+		case dsnF.Changed && urlF.Changed:
+			return errors.New(`both flags "url" and "dsn" were set`)
+		case dsnF.Changed && !urlF.Changed:
+			urlF.Changed = true
+			urlF.Value = dsnF.Value
+		}
+		return nil
+	}
+}
+
+func handlePath(cmd *cobra.Command, path string) {
+	tasks, err := tasks(path)
+	cobra.CheckErr(err)
+	for _, task := range tasks {
+		changed, err := fmtFile(task)
+		cobra.CheckErr(err)
+		if changed {
+			cmd.Println(task.path)
+		}
+	}
+}
+
+func tasks(path string) ([]fmttask, error) {
+	var tasks []fmttask
+	stat, err := os.Stat(path)
+	if err != nil {
+		return nil, err
+	}
+	if !stat.IsDir() {
+		if strings.HasSuffix(path, ".hcl") {
+			tasks = append(tasks, fmttask{
+				path: path,
+				info: stat,
+			})
+		}
+		return tasks, nil
+	}
+	all, err := ioutil.ReadDir(path)
+	if err != nil {
+		return nil, err
+	}
+	for _, f := range all {
+		if f.IsDir() {
+			continue
+		}
+		if strings.HasSuffix(f.Name(), ".hcl") {
+			tasks = append(tasks, fmttask{
+				path: filepath.Join(path, f.Name()),
+				info: f,
+			})
+		}
+	}
+	return tasks, nil
+}
+
+type fmttask struct {
+	path string
+	info fs.FileInfo
+}
+
+// fmtFile tries to format a file and reports if formatting occurred.
+func fmtFile(task fmttask) (bool, error) {
+	orig, err := os.ReadFile(task.path)
+	if err != nil {
+		return false, err
+	}
+	formatted := hclwrite.Format(orig)
+	if !bytes.Equal(formatted, orig) {
+		return true, os.WriteFile(task.path, formatted, task.info.Mode())
+	}
+	return false, nil
+}
diff --git a/cmd/internal/update/update.go b/cmd/atlascmd/update/update.go
similarity index 100%
rename from cmd/internal/update/update.go
rename to cmd/atlascmd/update/update.go
diff --git a/cmd/internal/update/update_test.go b/cmd/atlascmd/update/update_test.go
similarity index 100%
rename from cmd/internal/update/update_test.go
rename to cmd/atlascmd/update/update_test.go
diff --git a/doc/md/cli/reference.md b/doc/md/cli/reference.md
index 91a03e78..826369e2 100644
--- a/doc/md/cli/reference.md
+++ b/doc/md/cli/reference.md
@@ -202,7 +202,7 @@ atlas schema diff [flags]
 ```
 
 #### Details
-`atlas schema diff` connects to two given databases, inspects
+'atlas schema diff' connects to two given databases, inspects
 them, calculates the difference in their schemas, and prints a plan of
 SQL statements to migrate the "from" database to the schema of the "to" database.
 
@@ -224,7 +224,7 @@ atlas schema fmt [path ...]
 ```
 
 #### Details
-`atlas schema fmt` formats all ".hcl" files under the given path using
+'atlas schema fmt' formats all ".hcl" files under the given path using
 canonical HCL layout style as defined by the github.com/hashicorp/hcl/v2/hclwrite package.
 Unless stated otherwise, the fmt command will use the current directory.
 
@@ -235,7 +235,7 @@ files in the directory are formatted, no input will be printed out.
 
 ### atlas schema inspect
 
-Inspect an a database's and print its schema in Atlas DDL syntax.
+Inspect a database's and print its schema in Atlas DDL syntax.
 
 #### Usage
 ```
@@ -243,8 +243,8 @@ atlas schema inspect [flags]
 ```
 
 #### Details
-`atlas schema inspect` connects to the given database and inspects its schema.
-It then prints to the screen the schema of that database in Atlas DDL syntax. This output can be 
+'atlas schema inspect' connects to the given database and inspects its schema.
+It then prints to the screen the schema of that database in Atlas DDL syntax. This output can be
 saved to a file, commonly by redirecting the output to a file named with a ".hcl" suffix:
 
   atlas schema inspect -u "mysql://user:pass@localhost:3306/dbname" > atlas.hcl
diff --git a/doc/md/cli/url.mdx b/doc/md/cli/url.mdx
index 43a01d36..a7626351 100644
--- a/doc/md/cli/url.mdx
+++ b/doc/md/cli/url.mdx
@@ -74,7 +74,7 @@ sqlite://file?cache=shared&mode=memory
 <TabItem value="docker">
 
 Atlas can spin up an ephemeral local docker container for you by specifying a special URL like below. This can be useful
-if you need a [dev database](dev.md) for schema validation or diffing. However, some images like `mysql` /
+if you need a [dev database](../dev.md) for schema validation or diffing. However, some images like `mysql` /
 `mariadb` take quite some time to "boot", before they are ready to be used. For a smoother developing experience
 consider spinning up a longer lived container by yourself.
 
diff --git a/doc/md/ddl/sql.mdx b/doc/md/ddl/sql.mdx
index 4cef9801..055cd33b 100644
--- a/doc/md/ddl/sql.mdx
+++ b/doc/md/ddl/sql.mdx
@@ -256,7 +256,7 @@ table "users" {
 </Tabs>
 
 :::info
-Note, it is recommended to use the [`--dev-url`](../cli/dev-database) option when generated columns are used.
+Note, it is recommended to use the [`--dev-url`](../dev-database) option when generated columns are used.
 :::
 
 ## Column Types
@@ -394,7 +394,7 @@ table "t" {
 ```
 
 :::info
-Note, it is recommended to use the [`--dev-url`](../cli/dev-database) option when index expressions are used.
+Note, it is recommended to use the [`--dev-url`](../dev-database) option when index expressions are used.
 :::
 
 ### Partial Indexes
@@ -419,7 +419,7 @@ table "t" {
 ```
 
 :::info
-Note, it is recommended to use the [`--dev-url`](../cli/dev-database) option when partial indexes are used.
+Note, it is recommended to use the [`--dev-url`](../dev-database) option when partial indexes are used.
 :::
 
 ### Index Prefixes
diff --git a/doc/md/cli/dev.md b/doc/md/dev.md
similarity index 93%
rename from doc/md/cli/dev.md
rename to doc/md/dev.md
index 02fca4ad..1212fd7e 100644
--- a/doc/md/cli/dev.md
+++ b/doc/md/dev.md
@@ -1,15 +1,15 @@
 ---
 title: Dev Database
 id: dev-database
-slug: /cli/dev-database
+slug: /dev-database
 ---
 ## Introduction
 
-Various Atlas commands accept the `--dev-url` flag. This option uses the [URL](url.mdx) format, and allows passing
+Atlas uses the concept of "Dev Database" to provide extra safety and correctness to the migration process.
 a development database (a twin environment) to validate schemas, simulate migrations and calculate the state of the
 migration directory by replaying the historical changes. Let's go over a few examples to explain the benefits of using a
 dev/twin database. For a one-time use Atlas can spin up an ephemeral local docker container for you with a special
-[docker driver](url.mdx).
+[docker driver](cli/url.mdx).
 
 ## Validation
 
@@ -27,7 +27,7 @@ table "t" {
 }
 ```
 
-After running [`schema apply`](reference.md#atlas-schema-apply), we get the following error because the `CHECK`
+After running [`schema apply`](cli/reference.md#atlas-schema-apply), we get the following error because the `CHECK`
 constraint is invalid, as column `d` does not exist.
 
 ```shell
@@ -136,7 +136,7 @@ Similarly to the previous example, we will use the `--dev-url` option to solve t
 
 ```shell
 $ atlas schema apply --url "mysql://root:pass@:3308/test" -f test.hcl
-  --dev-url="mysql://root:pass@:3308/test"
+  --dev-url="mysql://root:pass@:3307/test"
 ```
 ```text
 Schema is synced, no changes to be made
diff --git a/doc/website/docusaurus.config.js b/doc/website/docusaurus.config.js
index 9c17f8fa..9f8c6d87 100644
--- a/doc/website/docusaurus.config.js
+++ b/doc/website/docusaurus.config.js
@@ -65,6 +65,19 @@ module.exports = {
       `,
     },
   },
+  plugins: [
+    [
+      '@docusaurus/plugin-client-redirects',
+      {
+        redirects: [
+          {
+            to: '/dev-database',
+            from: '/cli/dev-database',
+          },
+        ],
+      },
+    ],
+  ],
   presets: [
     [
       '@docusaurus/preset-classic',
diff --git a/doc/website/package-lock.json b/doc/website/package-lock.json
index 32a1872f..bda7f4fe 100644
--- a/doc/website/package-lock.json
+++ b/doc/website/package-lock.json
@@ -9,6 +9,7 @@
       "version": "0.0.0",
       "dependencies": {
         "@docusaurus/core": "2.0.0-beta.15",
+        "@docusaurus/plugin-client-redirects": "2.0.0-beta.15",
         "@docusaurus/preset-classic": "2.0.0-beta.15",
         "@mdx-js/react": "^1.6.21",
         "@svgr/webpack": "^5.5.0",
@@ -2322,6 +2323,29 @@
         "react-dom": "^16.8.4 || ^17.0.0"
       }
     },
+    "node_modules/@docusaurus/plugin-client-redirects": {
+      "version": "2.0.0-beta.15",
+      "resolved": "https://registry.npmjs.org/@docusaurus/plugin-client-redirects/-/plugin-client-redirects-2.0.0-beta.15.tgz",
+      "integrity": "sha512-FcH1y+a+Bmc5x50kZn5RvLr2a6iPiGnRK4iUP7G1eNozJC+8+93OwyZYSCdEOf2LyoVOtEq06xlfORFP2mfQDQ==",
+      "dependencies": {
+        "@docusaurus/core": "2.0.0-beta.15",
+        "@docusaurus/logger": "2.0.0-beta.15",
+        "@docusaurus/utils": "2.0.0-beta.15",
+        "@docusaurus/utils-common": "2.0.0-beta.15",
+        "@docusaurus/utils-validation": "2.0.0-beta.15",
+        "eta": "^1.12.3",
+        "fs-extra": "^10.0.0",
+        "lodash": "^4.17.20",
+        "tslib": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "react": "^16.8.4 || ^17.0.0",
+        "react-dom": "^16.8.4 || ^17.0.0"
+      }
+    },
     "node_modules/@docusaurus/plugin-content-blog": {
       "version": "2.0.0-beta.15",
       "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.0.0-beta.15.tgz",
@@ -15036,6 +15060,22 @@
         "webpack": "^5.61.0"
       }
     },
+    "@docusaurus/plugin-client-redirects": {
+      "version": "2.0.0-beta.15",
+      "resolved": "https://registry.npmjs.org/@docusaurus/plugin-client-redirects/-/plugin-client-redirects-2.0.0-beta.15.tgz",
+      "integrity": "sha512-FcH1y+a+Bmc5x50kZn5RvLr2a6iPiGnRK4iUP7G1eNozJC+8+93OwyZYSCdEOf2LyoVOtEq06xlfORFP2mfQDQ==",
+      "requires": {
+        "@docusaurus/core": "2.0.0-beta.15",
+        "@docusaurus/logger": "2.0.0-beta.15",
+        "@docusaurus/utils": "2.0.0-beta.15",
+        "@docusaurus/utils-common": "2.0.0-beta.15",
+        "@docusaurus/utils-validation": "2.0.0-beta.15",
+        "eta": "^1.12.3",
+        "fs-extra": "^10.0.0",
+        "lodash": "^4.17.20",
+        "tslib": "^2.3.1"
+      }
+    },
     "@docusaurus/plugin-content-blog": {
       "version": "2.0.0-beta.15",
       "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.0.0-beta.15.tgz",
diff --git a/doc/website/package.json b/doc/website/package.json
index f1380491..836b8c8b 100644
--- a/doc/website/package.json
+++ b/doc/website/package.json
@@ -17,6 +17,7 @@
   },
   "dependencies": {
     "@docusaurus/core": "2.0.0-beta.15",
+    "@docusaurus/plugin-client-redirects": "2.0.0-beta.15",
     "@docusaurus/preset-classic": "2.0.0-beta.15",
     "@mdx-js/react": "^1.6.21",
     "@svgr/webpack": "^5.5.0",
diff --git a/doc/website/sidebars.js b/doc/website/sidebars.js
index 146834b7..c7c26c40 100644
--- a/doc/website/sidebars.js
+++ b/doc/website/sidebars.js
@@ -33,13 +33,16 @@ module.exports = {
                 'ddl/ddl-intro',
             ],
         },
+        {
+            type: 'doc',
+            id: 'dev-database'
+        },
         {
             type: 'category',
             label: 'CLI',
             items: [
                 {type: 'doc', id: 'cli/cli-reference', label: 'Reference'},
                 {type: 'doc', id: 'cli/cli-url', label: 'URLs'},
-                {type: 'doc', id: 'cli/dev-database', label: 'Dev Database'},
             ]
         },
         {
diff --git a/sql/internal/sqlx/migrate.go b/sql/internal/sqlx/migrate.go
new file mode 100644
index 00000000..5ae4dd37
--- /dev/null
+++ b/sql/internal/sqlx/migrate.go
@@ -0,0 +1,85 @@
+// Copyright 2021-present The Atlas Authors. All rights reserved.
+// 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
+
+import (
+	"context"
+
+	"ariga.io/atlas/sql/migrate"
+	"ariga.io/atlas/sql/migrate/ent"
+	"ariga.io/atlas/sql/migrate/ent/revision"
+	"ariga.io/atlas/sql/schema"
+	"entgo.io/ent/dialect/sql"
+)
+
+// A EntRevisions provides implementation for the migrate.RevisionReadWriter interface.
+type EntRevisions struct {
+	c *ent.Client
+	// sc is the function signature of the Ent migration engine.
+	// Due to cyclic dependencies between Ent and Atlas, this value is stitched in at runtime.
+	sc func(context.Context) error
+}
+
+// NewRevisionStorage creates a new EntRevisions with the given ent.Client.
+func NewRevisionStorage(db schema.ExecQuerier, dialect string) *EntRevisions {
+	return &EntRevisions{c: ent.NewClient(ent.Driver(sql.NewDriver(sql.Conn{ExecQuerier: db}, dialect)))}
+}
+
+// InitSchemaMigrator stitches in the Ent migration engine to the EntRevisions at runtime. This is necessary
+// because the Ent migration engine imports atlas and therefore would introduce a cyclic dependency.
+func (r *EntRevisions) InitSchemaMigrator(sc func(context.Context) error) {
+	r.sc = sc
+}
+
+// Init makes sure the revisions table does exist in the connected database.
+func (r *EntRevisions) Init(ctx context.Context) error {
+	return r.sc(ctx)
+}
+
+// ReadRevisions reads the revisions from the revisions table.
+func (r *EntRevisions) ReadRevisions(ctx context.Context) (migrate.Revisions, error) {
+	revs, err := r.c.Revision.Query().Order(ent.Asc(revision.FieldID)).All(ctx)
+	if err != nil {
+		return nil, err
+	}
+	ret := make(migrate.Revisions, len(revs))
+	for i, r := range revs {
+		ret[i] = &migrate.Revision{
+			Version:         r.ID,
+			Description:     r.Description,
+			ExecutionState:  string(r.ExecutionState),
+			ExecutedAt:      r.ExecutedAt,
+			ExecutionTime:   r.ExecutionTime,
+			Hash:            r.Hash,
+			OperatorVersion: r.OperatorVersion,
+			Meta:            r.Meta,
+		}
+	}
+	return ret, nil
+}
+
+// WriteRevisions writes the revisions to the revisions table.
+func (r *EntRevisions) WriteRevisions(ctx context.Context, rs migrate.Revisions) error {
+	bulk := make([]*ent.RevisionCreate, len(rs))
+	for i, rev := range rs {
+		bulk[i] = r.c.Revision.Create().
+			SetID(rev.Version).
+			SetDescription(rev.Description).
+			SetExecutionState(revision.ExecutionState(rev.ExecutionState)).
+			SetExecutedAt(rev.ExecutedAt).
+			SetExecutionTime(rev.ExecutionTime).
+			SetHash(rev.Hash).
+			SetOperatorVersion(rev.OperatorVersion).
+			SetMeta(rev.Meta)
+	}
+	return r.c.Revision.CreateBulk(bulk...).
+		OnConflict(
+			sql.ConflictColumns(revision.FieldID),
+		).
+		UpdateNewValues().
+		Exec(ctx)
+}
+
+var _ migrate.RevisionReadWriter = (*EntRevisions)(nil)
diff --git a/sql/migrate/migrate.go b/sql/migrate/migrate.go
index c2ce6538..8c3ab3b7 100644
--- a/sql/migrate/migrate.go
+++ b/sql/migrate/migrate.go
@@ -172,6 +172,8 @@ type (
 	}
 
 	// A RevisionReadWriter reads and writes information about a Revisions to a persistent storage.
+	// If the implementation happens provide an "Init() error" method,
+	// it will be called once before attempting to read or write.
 	//
 	// Atlas drivers provide a sql based implementation of this interface by default.
 	RevisionReadWriter interface {
@@ -338,6 +340,15 @@ func (e *Executor) Execute(ctx context.Context, n int) (err error) {
 	if !ok {
 		return errors.New("sql/migrate: execute: no revisions reader available")
 	}
+	// Drivers might need to do some initialization before the storage is ready. They can implement the following
+	// inlined interface and the executor will call it once before doing the migration execution.
+	if init, ok := e.drv.(interface {
+		Init(context.Context) error
+	}); ok {
+		if err := init.Init(ctx); err != nil {
+			return fmt.Errorf("sql/migrate: execute: init revision storage: %w", err)
+		}
+	}
 	// Check if the Dir implements Scanner interface.
 	sc, ok := e.dir.(Scanner)
 	if !ok {
diff --git a/sql/mysql/driver.go b/sql/mysql/driver.go
index d8496b85..2a6705dc 100644
--- a/sql/mysql/driver.go
+++ b/sql/mysql/driver.go
@@ -13,12 +13,9 @@ import (
 
 	"ariga.io/atlas/sql/internal/sqlx"
 	"ariga.io/atlas/sql/migrate"
-	"ariga.io/atlas/sql/migrate/ent"
 	"ariga.io/atlas/sql/schema"
 	"ariga.io/atlas/sql/sqlclient"
 	"entgo.io/ent/dialect"
-	"entgo.io/ent/dialect/sql"
-
 	"golang.org/x/mod/semver"
 )
 
@@ -74,20 +71,23 @@ func Open(db schema.ExecQuerier) (migrate.Driver, error) {
 		}, nil
 	}
 	return &Driver{
-		conn:        c,
-		Differ:      &sqlx.Diff{DiffDriver: &diff{c}},
-		Inspector:   &inspect{c},
-		PlanApplier: &planApply{c},
-		RevisionReadWriter: &revReadWrite{
-			c: ent.NewClient(ent.Driver(sql.NewDriver(sql.Conn{ExecQuerier: db}, dialect.MySQL))),
-		},
+		conn:               c,
+		Differ:             &sqlx.Diff{DiffDriver: &diff{c}},
+		Inspector:          &inspect{c},
+		PlanApplier:        &planApply{c},
+		RevisionReadWriter: sqlx.NewRevisionStorage(db, dialect.MySQL),
 	}, nil
 }
 
-// InitSchemaMigrator stitches in the Ent migration engine to the mysql.Driver at runtime. This is necessary
+// InitSchemaMigrator stitches in the Ent migration engine to the Driver at runtime. This is necessary
 // because the Ent migration engine imports atlas and therefore would introduce a cyclic dependency.
 func (d *Driver) InitSchemaMigrator(sc func(context.Context) error) {
-	d.RevisionReadWriter.(*revReadWrite).sc = sc
+	d.RevisionReadWriter.(*sqlx.EntRevisions).InitSchemaMigrator(sc)
+}
+
+// Init is called by the migration executor and makes sure the revisions table does exist in the connected database.
+func (d *Driver) Init(ctx context.Context) error {
+	return d.RevisionReadWriter.(*sqlx.EntRevisions).Init(ctx)
 }
 
 func (d *Driver) dev() *sqlx.DevDriver {
diff --git a/sql/mysql/migrate.go b/sql/mysql/migrate.go
index 9d66b22a..ec3496c1 100644
--- a/sql/mysql/migrate.go
+++ b/sql/mysql/migrate.go
@@ -13,8 +13,6 @@ import (
 
 	"ariga.io/atlas/sql/internal/sqlx"
 	"ariga.io/atlas/sql/migrate"
-	"ariga.io/atlas/sql/migrate/ent"
-	"ariga.io/atlas/sql/migrate/ent/revision"
 	"ariga.io/atlas/sql/schema"
 )
 
@@ -54,69 +52,6 @@ func (p *planApply) ApplyChanges(ctx context.Context, changes []schema.Change) e
 	return sqlx.ApplyChanges(ctx, changes, p)
 }
 
-// A revReadWrite provides implementation for the migrate.RevisionReadWriter interface.
-type revReadWrite struct {
-	c *ent.Client
-	// sc is the function signature of the Ent migration engine.
-	// Due to cyclic dependencies between Ent and Atlas, this value is stitched in at runtime.
-	sc func(context.Context) error
-}
-
-// ReadRevisions read the revisions from the revisions table.
-func (r *revReadWrite) ReadRevisions(ctx context.Context) (migrate.Revisions, error) {
-	rs, err := r.c.QueryContext(ctx, fmt.Sprintf("SHOW TABLES LIKE '%s'", revision.Table))
-	if err != nil {
-		return nil, err
-	}
-	if !rs.Next() {
-		// If there are no results the table does not exist and there are no revisions yet.
-		return migrate.Revisions{}, nil
-	}
-	if err := rs.Close(); err != nil {
-		return nil, err
-	}
-	revs, err := r.c.Revision.Query().Order(ent.Asc(revision.FieldID)).All(ctx)
-	if err != nil {
-		return nil, err
-	}
-	ret := make(migrate.Revisions, len(revs))
-	for i, r := range revs {
-		ret[i] = &migrate.Revision{
-			Version:         r.ID,
-			Description:     r.Description,
-			ExecutionState:  string(r.ExecutionState),
-			ExecutedAt:      r.ExecutedAt,
-			ExecutionTime:   r.ExecutionTime,
-			Hash:            r.Hash,
-			OperatorVersion: r.OperatorVersion,
-			Meta:            r.Meta,
-		}
-	}
-	return ret, nil
-}
-
-// WriteRevisions stores the revisions in the revisions table.
-func (r *revReadWrite) WriteRevisions(ctx context.Context, rs migrate.Revisions) error {
-	if err := r.sc(ctx); err != nil {
-		return err
-	}
-	bulk := make([]*ent.RevisionCreate, len(rs))
-	for i, rev := range rs {
-		bulk[i] = r.c.Revision.Create().
-			SetID(rev.Version).
-			SetDescription(rev.Description).
-			SetExecutionState(revision.ExecutionState(rev.ExecutionState)).
-			SetExecutedAt(rev.ExecutedAt).
-			SetExecutionTime(rev.ExecutionTime).
-			SetHash(rev.Hash).
-			SetOperatorVersion(rev.OperatorVersion).
-			SetMeta(rev.Meta)
-	}
-	return r.c.Revision.CreateBulk(bulk...).OnConflict().UpdateNewValues().Exec(ctx)
-}
-
-var _ migrate.RevisionReadWriter = (*revReadWrite)(nil)
-
 // state represents the state of a planning. It is not part of
 // planApply so that multiple planning/applying can be called
 // in parallel.
diff --git a/sql/postgres/driver.go b/sql/postgres/driver.go
index f4d50410..4f0a3b41 100644
--- a/sql/postgres/driver.go
+++ b/sql/postgres/driver.go
@@ -16,7 +16,7 @@ import (
 	"ariga.io/atlas/sql/migrate"
 	"ariga.io/atlas/sql/schema"
 	"ariga.io/atlas/sql/sqlclient"
-
+	"entgo.io/ent/dialect"
 	"golang.org/x/mod/semver"
 )
 
@@ -28,6 +28,7 @@ type (
 		schema.Differ
 		schema.Inspector
 		migrate.PlanApplier
+		migrate.RevisionReadWriter
 	}
 
 	// database connection and its information.
@@ -74,13 +75,25 @@ func Open(db schema.ExecQuerier) (migrate.Driver, error) {
 		return nil, fmt.Errorf("postgres: unsupported postgres version: %s", c.version)
 	}
 	return &Driver{
-		conn:        c,
-		Differ:      &sqlx.Diff{DiffDriver: &diff{c}},
-		Inspector:   &inspect{c},
-		PlanApplier: &planApply{c},
+		conn:               c,
+		Differ:             &sqlx.Diff{DiffDriver: &diff{c}},
+		Inspector:          &inspect{c},
+		PlanApplier:        &planApply{c},
+		RevisionReadWriter: sqlx.NewRevisionStorage(db, dialect.Postgres),
 	}, nil
 }
 
+// InitSchemaMigrator stitches in the Ent migration engine to the Driver at runtime. This is necessary
+// because the Ent migration engine imports atlas and therefore would introduce a cyclic dependency.
+func (d *Driver) InitSchemaMigrator(sc func(context.Context) error) {
+	d.RevisionReadWriter.(*sqlx.EntRevisions).InitSchemaMigrator(sc)
+}
+
+// Init is called by the migration executor and makes sure the revisions table does exist in the connected database.
+func (d *Driver) Init(ctx context.Context) error {
+	return d.RevisionReadWriter.(*sqlx.EntRevisions).Init(ctx)
+}
+
 func (d *Driver) dev() *sqlx.DevDriver {
 	return &sqlx.DevDriver{Driver: d, MaxNameLen: 63}
 }


Test patch:
diff --git a/cmd/action/env_test.go b/cmd/atlascmd/atlascmd_test.go
similarity index 81%
rename from cmd/action/env_test.go
rename to cmd/atlascmd/atlascmd_test.go
index 8f11ff70..e09efe37 100644
--- a/cmd/action/env_test.go
+++ b/cmd/atlascmd/atlascmd_test.go
@@ -2,19 +2,19 @@
 // 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 action
+package atlascmd
 
 import (
 	"os"
 	"testing"
 
-	"ariga.io/atlas/cmd/internal/update"
+	"ariga.io/atlas/cmd/atlascmd/update"
 
 	"github.com/stretchr/testify/require"
 )
 
 func TestEnv(t *testing.T) {
-	out, err := runCmd(RootCmd, "env")
+	out, err := runCmd(Root, "env")
 	require.NoError(t, err)
 	require.Empty(t, out)
 }
@@ -22,7 +22,7 @@ func TestEnv(t *testing.T) {
 func TestEnv_Set(t *testing.T) {
 	err := os.Setenv(update.AtlasNoUpdateNotifier, "test")
 	require.NoError(t, err)
-	out, err := runCmd(RootCmd, "env")
+	out, err := runCmd(Root, "env")
 	require.NoError(t, err)
 	require.Equal(t, "ATLAS_NO_UPDATE_NOTIFIER=test\n", out)
 }
diff --git a/cmd/action/diff_test.go b/cmd/atlascmd/diff_test.go
similarity index 99%
rename from cmd/action/diff_test.go
rename to cmd/atlascmd/diff_test.go
index 729eaee0..82e3d532 100644
--- a/cmd/action/diff_test.go
+++ b/cmd/atlascmd/diff_test.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 action
+package atlascmd
 
 import (
 	"bytes"
diff --git a/cmd/action/migrate_test.go b/cmd/atlascmd/migrate_test.go
similarity index 91%
rename from cmd/action/migrate_test.go
rename to cmd/atlascmd/migrate_test.go
index 3dd7de8b..a09d2caa 100644
--- a/cmd/action/migrate_test.go
+++ b/cmd/atlascmd/migrate_test.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 action
+package atlascmd
 
 import (
 	"context"
@@ -28,7 +28,7 @@ import (
 )
 
 func TestMigrate(t *testing.T) {
-	_, err := runCmd(RootCmd, "migrate")
+	_, err := runCmd(Root, "migrate")
 	require.NoError(t, err)
 }
 
@@ -37,7 +37,7 @@ func TestMigrate_Diff(t *testing.T) {
 
 	// Expect no clean dev error.
 	s, err := runCmd(
-		RootCmd, "migrate", "diff",
+		Root, "migrate", "diff",
 		"name",
 		"--dir", "file://"+p,
 		"--dev-url", openSQLite(t, "create table t (c int);"),
@@ -48,7 +48,7 @@ func TestMigrate_Diff(t *testing.T) {
 
 	// Works.
 	s, err = runCmd(
-		RootCmd, "migrate", "diff",
+		Root, "migrate", "diff",
 		"name",
 		"--dir", "file://"+p,
 		"--dev-url", openSQLite(t, ""),
@@ -72,7 +72,7 @@ func TestMigrate_Diff(t *testing.T) {
 	require.NoError(t, err)
 	require.NoError(t, f.Close())
 	s, err = runCmd(
-		RootCmd, "migrate", "diff",
+		Root, "migrate", "diff",
 		"name",
 		"--dir", "file://"+t.TempDir(),
 		"--dev-url", fmt.Sprintf("sqlitelock://file:%s?cache=shared&_fk=1", filepath.Join(p, "test.db")),
@@ -83,14 +83,14 @@ func TestMigrate_Diff(t *testing.T) {
 }
 
 func TestMigrate_Validate(t *testing.T) {
-	s, err := runCmd(RootCmd, "migrate", "validate", "--dir", "file://migrations")
+	s, err := runCmd(Root, "migrate", "validate", "--dir", "file://migrations")
 	require.Zero(t, s)
 	require.NoError(t, err)
 }
 
 func TestMigrate_ValidateError(t *testing.T) {
 	if os.Getenv("DO_VALIDATE") == "1" {
-		runCmd(RootCmd, "migrate", "validate", "--dir", "file://migrations")
+		runCmd(Root, "migrate", "validate", "--dir", "file://migrations")
 		return
 	}
 	f := filepath.Join("migrations", "new.sql")
@@ -106,7 +106,7 @@ func TestMigrate_ValidateError(t *testing.T) {
 }
 
 func TestMigrate_Hash(t *testing.T) {
-	s, err := runCmd(RootCmd, "migrate", "hash", "--dir", "file://migrations")
+	s, err := runCmd(Root, "migrate", "hash", "--dir", "file://migrations")
 	require.Zero(t, s)
 	require.NoError(t, err)
 
@@ -114,7 +114,7 @@ func TestMigrate_Hash(t *testing.T) {
 	err = copyFile(filepath.Join("migrations", "20220318104614_initial.sql"), filepath.Join(p, "20220318104614_initial.sql"))
 	require.NoError(t, err)
 
-	s, err = runCmd(RootCmd, "migrate", "hash", "--dir", "file://"+p, "--force")
+	s, err = runCmd(Root, "migrate", "hash", "--dir", "file://"+p, "--force")
 	require.Zero(t, s)
 	require.NoError(t, err)
 	require.FileExists(t, filepath.Join(p, "atlas.sum"))
@@ -131,7 +131,7 @@ func TestMigrate_Hash(t *testing.T) {
 
 func TestMigrate_HashError(t *testing.T) {
 	if os.Getenv("DO_HASH") == "1" {
-		runCmd(RootCmd, "migrate", "hash", "--dir", "file://"+os.Getenv("MIGRATION_DIR"))
+		runCmd(Root, "migrate", "hash", "--dir", "file://"+os.Getenv("MIGRATION_DIR"))
 		return
 	}
 	p := t.TempDir()
diff --git a/cmd/action/fmt_test.go b/cmd/atlascmd/schema_test.go
similarity index 93%
rename from cmd/action/fmt_test.go
rename to cmd/atlascmd/schema_test.go
index a267934c..e447b410 100644
--- a/cmd/action/fmt_test.go
+++ b/cmd/atlascmd/schema_test.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 action
+package atlascmd
 
 import (
 	"bytes"
@@ -105,10 +105,10 @@ func TestFmt(t *testing.T) {
 
 func runFmt(t *testing.T, args []string) string {
 	var out bytes.Buffer
-	FmtCmd.ResetCommands() // Detach from sub-commands and parents, needed to skip input validation done by them.
-	FmtCmd.SetOut(&out)
-	FmtCmd.SetArgs(args)
-	err := FmtCmd.Execute()
+	SchemaFmt.ResetCommands() // Detach from sub-commands and parents, needed to skip input validation done by them.
+	SchemaFmt.SetOut(&out)
+	SchemaFmt.SetArgs(args)
+	err := SchemaFmt.Execute()
 	require.NoError(t, err)
 	return out.String()
 }
diff --git a/internal/integration/postgres_test.go b/internal/integration/postgres_test.go
index 9de91ac9..6e91a920 100644
--- a/internal/integration/postgres_test.go
+++ b/internal/integration/postgres_test.go
@@ -14,6 +14,7 @@ import (
 	"testing"
 
 	"ariga.io/atlas/sql/migrate"
+	"ariga.io/atlas/sql/migrate/ent/runtime"
 	"ariga.io/atlas/sql/mysql"
 	"ariga.io/atlas/sql/postgres"
 	"ariga.io/atlas/sql/schema"
@@ -54,12 +55,12 @@ func pgRun(t *testing.T, fn func(*pgTest)) {
 	}
 }
 
-func pgInit(dialect string) []io.Closer {
+func pgInit(d string) []io.Closer {
 	var cs []io.Closer
-	if dialect != "" {
-		p, ok := pgTests.ports[dialect]
+	if d != "" {
+		p, ok := pgTests.ports[d]
 		if ok {
-			pgTests.ports = map[string]int{dialect: p}
+			pgTests.ports = map[string]int{d: p}
 		} else {
 			pgTests.ports = make(map[string]int)
 		}
@@ -74,11 +75,18 @@ func pgInit(dialect string) []io.Closer {
 		if err != nil {
 			log.Fatalln(err)
 		}
+		runtime.InitSchemaMigrator(drv.(*postgres.Driver), db, dialect.Postgres)
 		pgTests.drivers[version] = &pgTest{db: db, drv: drv, version: version, port: port}
 	}
 	return cs
 }
 
+func TestPostgres_Executor(t *testing.T) {
+	pgRun(t, func(t *pgTest) {
+		testExecutor(t)
+	})
+}
+
 func TestPostgres_AddDropTable(t *testing.T) {
 	pgRun(t, func(t *pgTest) {
 		testAddDrop(t)
@@ -1105,14 +1113,14 @@ func (t *pgTest) revisions() *schema.Table {
 	versionsT := &schema.Table{
 		Name: "atlas_schema_revisions",
 		Columns: []*schema.Column{
-			{Name: "version", Type: &schema.ColumnType{Type: &schema.StringType{T: t.valueByVersion(map[string]string{"mysql56": "varchar(191)"}, "varchar(255)")}}},
-			{Name: "description", Type: &schema.ColumnType{Type: &schema.StringType{T: "varchar(255)"}}},
-			{Name: "execution_state", Type: &schema.ColumnType{Type: &schema.StringType{T: "varchar(7)"}}},
-			{Name: "executed_at", Type: &schema.ColumnType{Type: &schema.TimeType{T: "datetime"}}},
-			{Name: "execution_time", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "int"}}},
-			{Name: "hash", Type: &schema.ColumnType{Type: &schema.StringType{T: "varchar(44)"}}},
-			{Name: "operator_version", Type: &schema.ColumnType{Type: &schema.StringType{T: "varchar(255)"}}},
-			{Name: "meta", Type: &schema.ColumnType{Type: &schema.StringType{T: "varchar(255)"}}},
+			{Name: "version", Type: &schema.ColumnType{Type: &schema.StringType{T: "character varying"}}},
+			{Name: "description", Type: &schema.ColumnType{Type: &schema.StringType{T: "character varying"}}},
+			{Name: "execution_state", Type: &schema.ColumnType{Type: &schema.StringType{T: "character varying"}}},
+			{Name: "executed_at", Type: &schema.ColumnType{Type: &schema.TimeType{T: "timestamp with time zone"}}},
+			{Name: "execution_time", Type: &schema.ColumnType{Type: &schema.IntegerType{T: "bigint"}}},
+			{Name: "hash", Type: &schema.ColumnType{Type: &schema.StringType{T: "character varying"}}},
+			{Name: "operator_version", Type: &schema.ColumnType{Type: &schema.StringType{T: "character varying"}}},
+			{Name: "meta", Type: &schema.ColumnType{Type: &schema.JSONType{T: "jsonb"}, Raw: "jsonb"}},
 		},
 	}
 	versionsT.PrimaryKey = &schema.Index{Parts: []*schema.IndexPart{{C: versionsT.Columns[0]}}}